# 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/ │ └── / │ ├── Build.kt ← creates the StoryPageFactory for this page │ └── 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.build(): FooStory { return FooStory( selectedVariant = FooStory.Variant.A, onVariantChange = { newVariant -> updateStory { it.copy(selectedVariant = newVariant) } }, ) } internal val fooStoryFactory get() = storyPageFactory(StateUpdater::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` (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`).