Updated on 2026-08-14
This commit is contained in:
parent
8781945418
commit
740da6cddd
12 changed files with 1154 additions and 0 deletions
|
|
@ -0,0 +1,274 @@
|
|||
# Storybook — Adding New Design System Pages
|
||||
|
||||
The storybook lives in `features/tester` and lets developers browse and validate
|
||||
design system components at runtime on a device or emulator.
|
||||
|
||||
---
|
||||
|
||||
## Architecture overview
|
||||
|
||||
```
|
||||
storybook/
|
||||
├── entity/
|
||||
│ ├── StoryBookPage.kt ← sealed interface + all page state classes
|
||||
│ ├── StoryBookUM.kt ← top-level UI model (current page, navigation)
|
||||
│ └── StoryPageFactory.kt ← factory interface used by the list screen
|
||||
├── page/
|
||||
│ └── <component>/
|
||||
│ ├── Build.kt ← creates the StoryPageFactory for this page
|
||||
│ └── <Component>Story.kt ← the Composable that renders the showcase
|
||||
├── ui/
|
||||
│ ├── StoryBookListScreen.kt ← list of all stories (add your entry here)
|
||||
│ └── StoryBookScreen.kt ← routes currentPage → correct Composable
|
||||
└── viewmodel/
|
||||
├── StoryBookViewModel.kt
|
||||
└── StateUpdater.kt ← helper for stateful pages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step-by-step: adding a new page
|
||||
|
||||
### 1. Declare the page type in `StoryBookPage.kt`
|
||||
|
||||
For a **stateless** showcase (no user interaction that mutates page state):
|
||||
```kotlin
|
||||
internal data object FooStory : StoryBookPage
|
||||
```
|
||||
|
||||
For a **stateful** page (e.g. toggle between variants like NorthernLightsStory):
|
||||
```kotlin
|
||||
internal data class FooStory(
|
||||
val selectedVariant: Variant,
|
||||
val onVariantChange: (Variant) -> Unit,
|
||||
) : StoryBookPage {
|
||||
enum class Variant { A, B }
|
||||
}
|
||||
```
|
||||
|
||||
> **DS components section.** If the page belongs to the DS components sub-list,
|
||||
> implement [`DsStoryBookPage`] instead of `StoryBookPage` directly. The view model
|
||||
> uses this marker to route back-navigation to the DS list rather than the root
|
||||
> story list. `DsComponentsListStory` itself stays on `StoryBookPage`, so back
|
||||
> from the DS list still goes to the root.
|
||||
>
|
||||
> ```kotlin
|
||||
> internal data class TangemLoaderStory(
|
||||
> val selectedSize: TangemLoaderSize,
|
||||
> val onSizeChange: (TangemLoaderSize) -> Unit,
|
||||
> ) : DsStoryBookPage
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
### 2. Create `page/foo/Build.kt`
|
||||
|
||||
**Stateless:**
|
||||
```kotlin
|
||||
internal val fooStoryFactory: StoryPageFactory = StoryPageFactory { FooStory }
|
||||
```
|
||||
|
||||
**Stateful** (use `storyPageFactory` + `StateUpdater`):
|
||||
```kotlin
|
||||
internal fun StateUpdater<FooStory>.build(): FooStory {
|
||||
return FooStory(
|
||||
selectedVariant = FooStory.Variant.A,
|
||||
onVariantChange = { newVariant ->
|
||||
updateStory { it.copy(selectedVariant = newVariant) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal val fooStoryFactory
|
||||
get() = storyPageFactory(StateUpdater<FooStory>::build)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Create `page/foo/FooStory.kt`
|
||||
|
||||
Write a `@Composable internal fun FooStory(...)` that renders the showcase.
|
||||
See [Design guidelines](#design-guidelines) below for layout advice.
|
||||
|
||||
**Stateless example skeleton:**
|
||||
```kotlin
|
||||
@Composable
|
||||
internal fun FooStory(modifier: Modifier = Modifier) {
|
||||
LazyColumn(modifier = modifier.fillMaxSize()) {
|
||||
item("section_a") { /* ... */ }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Stateful example skeleton:**
|
||||
```kotlin
|
||||
@Composable
|
||||
internal fun FooStory(state: FooStory, modifier: Modifier = Modifier) {
|
||||
// use state.selectedVariant, state.onVariantChange
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Register in `StoryBookScreen.kt`
|
||||
|
||||
Add a branch to the `when` block.
|
||||
|
||||
> **Naming note:** the entity type and the Composable function will share the
|
||||
> same simple name (e.g. `FooStory`). Kotlin resolves them correctly — the
|
||||
> entity import is used in the pattern position, the function import is used
|
||||
> as a call. This is the same pattern used for `NorthernLightsStory` and
|
||||
> `ButtonsStory`.
|
||||
|
||||
```kotlin
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.FooStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.foo.FooStory
|
||||
|
||||
when (storyState) {
|
||||
StoryList -> StoryBookListScreen(state = state)
|
||||
is NorthernLightsStory -> NorthernLightsStory(state = storyState)
|
||||
ButtonsStory -> ButtonsStory()
|
||||
FooStory -> FooStory() // stateless
|
||||
is FooStory -> FooStory(storyState) // stateful (note `is`)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Register in `StoryBookListScreen.kt`
|
||||
|
||||
Add one entry to `buildStories()`. **Every title must start with an emoji** that
|
||||
represents the component category — this makes the list easier to scan at a glance.
|
||||
|
||||
```kotlin
|
||||
private fun buildStories() = listOf(
|
||||
StoryItem(title = "🃏 Foo Component", factory = fooStoryFactory),
|
||||
// existing entries...
|
||||
)
|
||||
```
|
||||
|
||||
Pick an emoji that reflects the component's visual nature or purpose, e.g.:
|
||||
- Buttons → 🔘
|
||||
- Background effects → 🌌
|
||||
- Typography → 🔤
|
||||
- Icons → 🎨
|
||||
- Cards → 🃏
|
||||
- Inputs / Text fields → ✏️
|
||||
- Navigation → 🧭
|
||||
- Loaders / Progress → ⏳
|
||||
|
||||
---
|
||||
|
||||
## Design guidelines
|
||||
|
||||
### Page layout rule (mandatory)
|
||||
|
||||
> **Every DS component page must show a SINGLE instance of the component at the
|
||||
> top, with configuration controls below it for almost all of its parameters.**
|
||||
|
||||
The storybook is an interactive playground, not a static catalog. Pages must NOT
|
||||
render a grid of every possible variant; instead, expose every meaningful
|
||||
parameter as a control and let the user pick the configuration.
|
||||
|
||||
**Mapping parameter kinds to controls:**
|
||||
|
||||
| Parameter kind | Control |
|
||||
|---|---|
|
||||
| Enum-like (`size`, `color`, `shape`, `type`, `variant`) | **Chips / segmented selector** |
|
||||
| Boolean (`enabled`, `selected`, `withIcon`) | **Toggle / switch** |
|
||||
| Selectable boolean state | **Checkbox** |
|
||||
|
||||
The selected values live in the page's `StoryBookPage` data class
|
||||
(e.g. `TangemLoaderStory(selectedSize, onSizeChange)`) and are wired through
|
||||
`storyPageFactory` + `StateUpdater<T>` (see [Step 2](#2-create-pagefoobuildk)).
|
||||
|
||||
**Skeleton:**
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
internal fun FooStory(state: FooStory, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize().background(TangemTheme.colors2.surface.level1),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
// 1. Single component preview at the top
|
||||
ComponentPreview(/* uses state.* */)
|
||||
|
||||
// 2. One control per configurable parameter below
|
||||
SizeSelector(selected = state.selectedSize, onSelect = state.onSizeChange)
|
||||
ShapeSelector(selected = state.selectedShape, onSelect = state.onShapeChange)
|
||||
EnabledToggle(checked = state.isEnabled, onCheckedChange = state.onEnabledChange)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Stateless (`data object`) pages are reserved for components with no
|
||||
configurable parameters at all.**
|
||||
|
||||
See `TangemLoaderStory` and `TangemBadgeStory` for reference implementations.
|
||||
|
||||
### Layout
|
||||
|
||||
Use a `Column` (or `LazyColumn` if the controls overflow vertically) as the
|
||||
root, with the component preview on top and the controls grouped below.
|
||||
|
||||
```kotlin
|
||||
Column(
|
||||
modifier = modifier
|
||||
.statusBarsPadding()
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors2.surface.level1),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) { /* preview, then controls */ }
|
||||
```
|
||||
|
||||
### Chip selector pattern
|
||||
|
||||
For enum-like parameters, use a pill-shaped row of chips. The selected chip
|
||||
gets `surface.level3`; unselected chips stay on `surface.level2`.
|
||||
|
||||
```kotlin
|
||||
val shape = RoundedCornerShape(50)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(shape)
|
||||
.background(TangemTheme.colors2.surface.level2)
|
||||
.border(1.dp, TangemTheme.colors2.border.neutral.secondary, shape)
|
||||
.padding(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
SomeEnum.entries.forEach { value ->
|
||||
Chip(
|
||||
label = value.name,
|
||||
selected = value == state.selected,
|
||||
onClick = { state.onSelect(value) },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See `TangemLoaderStory` (size selector) and `TangemBadgeStory.ColorToggle`
|
||||
for reference implementations.
|
||||
|
||||
### Colors
|
||||
|
||||
- Page background: `TangemTheme.colors2.surface.level1`
|
||||
- Sections that need a contrasting background (e.g. PrimaryInverse):
|
||||
`TangemTheme.colors2.surface.level2`
|
||||
- Section divider: `HorizontalDivider(color = TangemTheme.colors2.border.neutral.secondary)`
|
||||
|
||||
### Realistic text
|
||||
|
||||
Use representative text strings, not placeholders like "Btn". Pick labels that
|
||||
match how the component would appear in the product (e.g. `"Continue"`,
|
||||
`"Send payment"`, `"Confirm"`).
|
||||
|
||||
### DS component imports
|
||||
|
||||
All design system components (`PrimaryTangemButton`, `TangemButtonSize`, etc.)
|
||||
live in `com.tangem.core.ui.ds.*` and are `public`, so they are directly
|
||||
importable from the `features/tester` module.
|
||||
|
||||
Use `com.tangem.core.ui.R` for drawable resources (e.g. `R.drawable.ic_tangem_24`).
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.tester.presentation.storybook.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import com.tangem.core.ui.ds.badge.TangemBadgeColor
|
||||
import com.tangem.core.ui.ds.field.search.TangemFieldShape
|
||||
import com.tangem.core.ui.ds.message.TangemMessageEffect
|
||||
|
|
@ -318,6 +319,20 @@ internal data class TangemSearchStory(
|
|||
}
|
||||
}
|
||||
|
||||
internal data class TangemCheckboxV2Story(
|
||||
val state: ToggleableState,
|
||||
val isEnabled: Boolean,
|
||||
val onStateChange: (ToggleableState) -> Unit,
|
||||
val onEnabledToggle: () -> Unit,
|
||||
) : DsStoryBookPage
|
||||
|
||||
internal data class TangemCheckmarkStory(
|
||||
val isChecked: Boolean,
|
||||
val isEnabled: Boolean,
|
||||
val onCheckedChange: (Boolean) -> Unit,
|
||||
val onEnabledToggle: () -> Unit,
|
||||
) : DsStoryBookPage
|
||||
|
||||
internal data class TangemBadgeV2Story(
|
||||
val variant: TangemBadge.Variant,
|
||||
val status: TangemBadge.Status,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import com.tangem.feature.tester.presentation.storybook.entity.DsComponentsListS
|
|||
import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.badge.tangemBadgeV2StoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.button.tangemButtonStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.checkbox.tangemCheckboxV2StoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.checkmark.tangemCheckmarkStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.tangemFadeStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.tangemLoaderStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.row.tangemRowStoryFactory
|
||||
|
|
@ -30,6 +32,8 @@ private fun buildDsStories() = listOf(
|
|||
DsStoryItem(title = "⏳ TangemLoader", factory = tangemLoaderStoryFactory),
|
||||
DsStoryItem(title = "🔘 TangemButton", factory = tangemButtonStoryFactory),
|
||||
DsStoryItem(title = "🏷️ TangemBadge", factory = tangemBadgeV2StoryFactory),
|
||||
DsStoryItem(title = "☑️ TangemCheckbox", factory = tangemCheckboxV2StoryFactory),
|
||||
DsStoryItem(title = "⭕ TangemCheckmark", factory = tangemCheckmarkStoryFactory),
|
||||
DsStoryItem(title = "📋 TangemRow", factory = tangemRowStoryFactory),
|
||||
DsStoryItem(title = "🔎 TangemSearch", factory = tangemSearchStoryFactory),
|
||||
DsStoryItem(title = "✨ TangemShimmer", factory = tangemShimmerStoryFactory),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.feature.tester.presentation.storybook.page.ds.checkbox
|
||||
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckboxV2Story
|
||||
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
|
||||
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
|
||||
|
||||
internal fun StateUpdater<TangemCheckboxV2Story>.build(): TangemCheckboxV2Story {
|
||||
return TangemCheckboxV2Story(
|
||||
state = ToggleableState.Off,
|
||||
isEnabled = true,
|
||||
onStateChange = { state ->
|
||||
updateStory { it.copy(state = state) }
|
||||
},
|
||||
onEnabledToggle = {
|
||||
updateStory { it.copy(isEnabled = !it.isEnabled) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal val tangemCheckboxV2StoryFactory
|
||||
get() = storyPageFactory(StateUpdater<TangemCheckboxV2Story>::build)
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.feature.tester.presentation.storybook.page.ds.checkbox
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.ds2.checkbox.TangemCheckbox
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckboxV2Story
|
||||
|
||||
@Composable
|
||||
internal fun TangemCheckboxV2Story(state: TangemCheckboxV2Story, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
// Preview stays pinned at the top.
|
||||
ComponentPreview(state = state)
|
||||
// Only the controls scroll.
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
StateSelector(selected = state.state, onSelect = state.onStateChange)
|
||||
Toggles(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComponentPreview(state: TangemCheckboxV2Story) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors3.bg.primary)
|
||||
.padding(vertical = 48.dp),
|
||||
) {
|
||||
// Clicking the live checkbox cycles Off -> On -> Indeterminate -> Off.
|
||||
TangemCheckbox(
|
||||
state = state.state,
|
||||
onClick = { state.onStateChange(state.state.next()) },
|
||||
isEnabled = state.isEnabled,
|
||||
modifier = Modifier.scale(2f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ToggleableState.next(): ToggleableState = when (this) {
|
||||
ToggleableState.Off -> ToggleableState.On
|
||||
ToggleableState.On -> ToggleableState.Indeterminate
|
||||
ToggleableState.Indeterminate -> ToggleableState.Off
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StateSelector(selected: ToggleableState, onSelect: (ToggleableState) -> Unit) {
|
||||
Section(label = "State") {
|
||||
ChipGrid(
|
||||
items = ToggleableState.entries,
|
||||
label = { it.name },
|
||||
isSelected = { it == selected },
|
||||
onSelect = onSelect,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Toggles(state: TangemCheckboxV2Story) {
|
||||
Section(label = "Flags") {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
ToggleRow(label = "isEnabled", checked = state.isEnabled, onToggle = state.onEnabledToggle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Section(label: String, content: @Composable () -> Unit) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = label,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T> ChipGrid(items: List<T>, label: (T) -> String, isSelected: (T) -> Boolean, onSelect: (T) -> Unit) {
|
||||
val shape = RoundedCornerShape(50)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(shape)
|
||||
.background(TangemTheme.colors2.surface.level2)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors2.border.neutral.secondary,
|
||||
shape = shape,
|
||||
)
|
||||
.padding(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items.forEach { item ->
|
||||
Chip(
|
||||
label = label(item),
|
||||
selected = isSelected(item),
|
||||
onClick = { onSelect(item) },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Chip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
val chipShape = RoundedCornerShape(50)
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
.clip(chipShape)
|
||||
.background(
|
||||
if (selected) TangemTheme.colors2.surface.level3 else TangemTheme.colors2.surface.level2,
|
||||
)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleRow(label: String, checked: Boolean, onToggle: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(TangemTheme.colors2.surface.level2)
|
||||
.clickable(onClick = onToggle)
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = if (checked) "ON" else "OFF",
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = if (checked) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.feature.tester.presentation.storybook.page.ds.checkmark
|
||||
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckmarkStory
|
||||
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
|
||||
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
|
||||
|
||||
internal fun StateUpdater<TangemCheckmarkStory>.build(): TangemCheckmarkStory {
|
||||
return TangemCheckmarkStory(
|
||||
isChecked = false,
|
||||
isEnabled = true,
|
||||
onCheckedChange = { checked ->
|
||||
updateStory { it.copy(isChecked = checked) }
|
||||
},
|
||||
onEnabledToggle = {
|
||||
updateStory { it.copy(isEnabled = !it.isEnabled) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal val tangemCheckmarkStoryFactory
|
||||
get() = storyPageFactory(StateUpdater<TangemCheckmarkStory>::build)
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.feature.tester.presentation.storybook.page.ds.checkmark
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.ds2.checkbox.TangemCheckmark
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckmarkStory
|
||||
|
||||
@Composable
|
||||
internal fun TangemCheckmarkStory(state: TangemCheckmarkStory, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
// Preview stays pinned at the top.
|
||||
ComponentPreview(state = state)
|
||||
// Only the controls scroll.
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Toggles(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComponentPreview(state: TangemCheckmarkStory) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors3.bg.primary)
|
||||
.padding(vertical = 48.dp),
|
||||
) {
|
||||
// Clicking the live checkmark toggles checked on/off.
|
||||
TangemCheckmark(
|
||||
checked = state.isChecked,
|
||||
onCheckedChange = state.onCheckedChange,
|
||||
isEnabled = state.isEnabled,
|
||||
modifier = Modifier.scale(2f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Toggles(state: TangemCheckmarkStory) {
|
||||
Section(label = "Flags") {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
ToggleRow(
|
||||
label = "checked",
|
||||
checked = state.isChecked,
|
||||
onToggle = { state.onCheckedChange(!state.isChecked) },
|
||||
)
|
||||
ToggleRow(label = "isEnabled", checked = state.isEnabled, onToggle = state.onEnabledToggle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Section(label: String, content: @Composable () -> Unit) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = label,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleRow(label: String, checked: Boolean, onToggle: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(TangemTheme.colors2.surface.level2)
|
||||
.clickable(onClick = onToggle)
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = if (checked) "ON" else "OFF",
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = if (checked) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,8 @@ import com.tangem.feature.tester.presentation.storybook.page.deviceicon.DeviceIc
|
|||
import com.tangem.feature.tester.presentation.storybook.page.ds.DsComponentsListStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.badge.TangemBadgeV2Story
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.button.TangemButtonStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.checkbox.TangemCheckboxV2Story
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.checkmark.TangemCheckmarkStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.TangemFadeStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.TangemLoaderStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.row.TangemRowStory
|
||||
|
|
@ -92,6 +94,8 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier)
|
|||
is TangemLoaderStory -> TangemLoaderStory(state = storyState)
|
||||
is TangemButtonStory -> TangemButtonStory(state = storyState)
|
||||
is TangemBadgeV2Story -> TangemBadgeV2Story(state = storyState)
|
||||
is TangemCheckboxV2Story -> TangemCheckboxV2Story(state = storyState)
|
||||
is TangemCheckmarkStory -> TangemCheckmarkStory(state = storyState)
|
||||
is TangemRowStory -> TangemRowStory(state = storyState)
|
||||
is TangemSearchStory -> TangemSearchStory(state = storyState)
|
||||
is TangemShimmerStory -> TangemShimmerStory(state = storyState)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue