tangem-app-android-audited/features/tester/STORYBOOK.md
2026-02-27 15:54:11 +03:00

263 lines
No EOL
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 }
}
```
---
### 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
### Layout
Use a `LazyColumn` as the root for component showcases so the page scrolls
when content is taller than the screen.
```kotlin
LazyColumn(
contentPadding = PaddingValues(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier.fillMaxSize().background(TangemTheme.colors2.surface.level1),
) { /* items */ }
```
### Showing all variants
Show every meaningful axis of variation in one place:
| Axis | How to display |
|---|---|
| **States** (Default, Disabled, Pressed, Loading) | One row per state |
| **Shapes** (Default, Rounded) | One labeled group (`ShapeGroup`) per shape, iterate `TangemButtonShape.entries` |
| **Content** (text+icon vs icon-only) | Two columns per row |
| **Sizes** | Separate `LazyColumn` item per size group if needed |
| **Styles / Effects** (e.g. `TangemMessageEffect`) | Chip toggle — see below |
> **Prefer vertical stacking over horizontal.** A row should contain at most
> 23 components; more than that overflows on narrow screens. Use
> `Modifier.weight(1f)` on columns instead of fixed widths.
### Toggle for style/effect axes
When a discrete axis (e.g. a visual effect enum) would produce too many full-width
components on one screen, use a **sticky chip-picker** instead of stacking all values.
Make the page **stateful** and store the selected value in the `StoryBookPage` data class.
```
┌─────────────────────────────────┐ ← stickyHeader
│ Magic │ Card │ Warning │ None│ ← chip row (EffectToggle)
└─────────────────────────────────┘
No icon, no buttons
[ message with selected effect ]
With icon
[ message with selected effect ]
```
**Pattern:**
1. Add the selected value + callback to the `StoryBookPage` data class:
```kotlin
internal data class FooStory(
val selectedVariant: Variant,
val onVariantChange: (Variant) -> Unit,
) : StoryBookPage
```
2. Use a stateful `Build.kt` (see [Step 2](#2-create-pagefoobuildk)).
3. In the story composable, add a `stickyHeader` with a chip row:
```kotlin
stickyHeader("toggle") {
VariantToggle(
selected = state.selectedVariant,
onSelect = state.onVariantChange,
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors2.surface.level1)
.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
```
4. Each `item` below uses `state.selectedVariant` for the component under test.
See `TangemMessageStory` for a complete example.
### Section structure (component grids)
Follow the pattern used in `ButtonsStory`:
- **Section title** — `TangemTheme.typography.subtitle1`
- **Group sub-header** (shape/size/variant name) — `TangemTheme.typography.body2`
- **Column headers** (Text + Icon, Icon only, etc.) — `TangemTheme.typography.caption2`
- **State label** (Default, Disabled…) — `TangemTheme.typography.caption2`, fixed width ~80 dp
```
Primary ← subtitle1
Default ← body2 (shape/group sub-header)
Text + Icon Icon only ← caption2 column headers
Default [■ Continue] [■] ← state row
Disabled [■ Continue] [■]
Pressed [■ Continue] [■]
Loading [ ⟳ ] [⟳]
Rounded ← body2
...
```
### 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`).