8.3 KiB
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):
internal data object FooStory : StoryBookPage
For a stateful page (e.g. toggle between variants like NorthernLightsStory):
internal data class FooStory(
val selectedVariant: Variant,
val onVariantChange: (Variant) -> Unit,
) : StoryBookPage {
enum class Variant { A, B }
}
2. Create page/foo/Build.kt
Stateless:
internal val fooStoryFactory: StoryPageFactory = StoryPageFactory { FooStory }
Stateful (use storyPageFactory + StateUpdater):
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 below for layout advice.
Stateless example skeleton:
@Composable
internal fun FooStory(modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier.fillMaxSize()) {
item("section_a") { /* ... */ }
}
}
Stateful example skeleton:
@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 forNorthernLightsStoryandButtonsStory.
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.
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.
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 2–3 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:
- Add the selected value + callback to the
StoryBookPagedata class:internal data class FooStory( val selectedVariant: Variant, val onVariantChange: (Variant) -> Unit, ) : StoryBookPage - Use a stateful
Build.kt(see Step 2). - In the story composable, add a
stickyHeaderwith a chip row:stickyHeader("toggle") { VariantToggle( selected = state.selectedVariant, onSelect = state.onVariantChange, modifier = Modifier .fillMaxWidth() .background(TangemTheme.colors2.surface.level1) .padding(horizontal = 16.dp, vertical = 8.dp), ) } - Each
itembelow usesstate.selectedVariantfor 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).