Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-10 18:21:20 +03:00
parent 8781945418
commit 740da6cddd
12 changed files with 1154 additions and 0 deletions

View file

@ -0,0 +1,163 @@
# Design System
The app currently hosts **three generations of the design system (DS)** side by side. They differ by
folder, token set (colors / typography / dimensions), and the `@Preview` wrapper. Knowing which
generation a component belongs to is essential so you don't mix tokens or pull the wrong building blocks.
## Three generations
| Generation | Folder | Colors | Typography | Dimensions | Preview wrapper |
|---|---|---|---|---|---|
| **DS1** (legacy) | `core/ui/src/main/java/com/tangem/core/ui/components/` | `TangemTheme.colors` | `TangemTheme.typography` | `TangemTheme.dimens` | `TangemThemePreview` |
| **DS2** (redesign) | `core/ui/src/main/java/com/tangem/core/ui/ds/` | `TangemTheme.colors2` | `TangemTheme.typography2` | `TangemTheme.dimens2` | `TangemThemePreviewRedesign` |
| **DS3** (target) | `core/ui/src/main/java/com/tangem/core/ui/ds2/` | `TangemTheme.colors3` | `TangemTheme.typography3` | `TangemTheme.dimens2` | `TangemThemePreviewRedesign` |
> Mind the numbering mismatch: **folder `ds` is DS2**, **folder `ds2` is DS3**.
> The `colors2` / `typography2` tokens are `@Deprecated` (ReplaceWith `colors3` / `typography3`).
- **DS1** — the entire current app is built on it. Do **not** add new components here.
- **DS2** — redesign components. A transitional generation; don't write new components in it, only
maintain what already exists.
- **DS3** — the newest design system; **the whole app is being migrated to it**. Build new DS
components here.
## Using DS3 in features
**All DS3 components (folder `ds2`) may be used in features starting from app version 6.0.** Before
6.0 they must not be used on product screens.
If a needed component does not yet exist in DS3, **add it by analogy with the existing ones** (see the
pattern below).
## DS3 component pattern
Study the existing components as references:
- Simple: `ds2/checkbox/TangemCheckmark.kt` — single file, a public `@Composable` function + `@Preview`.
- Composite: `ds2/button/``TangemButton.kt` (public API), `TangemButtonInternal.kt` (private inner
layout), `TangemButtonExt.kt` (variant / size tokens).
Pattern rules:
1. **Package & location.** `com.tangem.core.ui.ds2.<component>`, folder
`core/ui/.../ds2/<component>/`. The component name is `Tangem<Name>`.
2. **DS3 tokens only.** Colors — `TangemTheme.colors3.*`, text — `TangemTheme.typography3.*`,
dimensions — `TangemTheme.dimens2.*`. No `colors` / `colors2` / hardcoded values (literal dp/colors
are acceptable only inside `@Preview`, where you add `@Suppress("MagicNumber")`).
3. **Signature.** `modifier: Modifier = Modifier` is mandatory (defaulting to `Modifier`, placed first
among the optional params or right after the required ones). Express variants/sizes via a nested
`enum` in `object Tangem<Name>` (like `TangemButton.Variant` / `TangemButton.Size`), not boolean flags.
4. **Accessibility.** Pass `contentDescription`, set the `Role`, mark `disabled()` in `semantics`, and
handle focus/press state via `interactionSource`.
5. **KDoc + Figma link.** Above the public function — KDoc describing behavior, every parameter, and a
link to the Figma node (see the KDoc requirements below).
6. **Previews.** Two `@Preview`s (Light + Dark via `UI_MODE_NIGHT_YES`), wrapped in
`TangemThemePreviewRedesign { ... }`, with `TangemTheme.colors3.bg.primary` as the background.
Preview helpers (`PreviewRow`, `Section`, etc.) are private in the same file.
7. **Composite components** (many variants / heavy layout) are split into 3 files like the button:
public `Tangem<Name>.kt`, private `Tangem<Name>Internal.kt`, tokens `Tangem<Name>Ext.kt`.
## API conventions
### Public properties live in the `object`
Any public type the component exposes — variant/size/role/align enums, status classes, constants —
is declared inside the namesake `object Tangem<Name>`, **not** as a top-level type. This keeps a single
`Tangem<Name>.Variant` / `Tangem<Name>.Size` / `Tangem<Name>.Role` namespace at the call site and
avoids polluting the package.
```kotlin
object TangemTopNavigation {
/** Horizontal alignment of the center content slot. */
enum class ContentAlign { Start, Center }
}
// usage: TangemTopNavigation.ContentAlign.Center
```
References: `TangemTopNavigation.ContentAlign`, `TangemNavigationText.Role`, `TangemButton.Variant` /
`TangemButton.Size`.
### Provide convenient overloads
A component should ship ergonomic overloads so callers don't assemble boilerplate for the common case.
Two acceptable shapes:
1. **Additional `@Composable fun` overloads** with simpler parameters that delegate to the base one.
`TangemTopNavigation` has a low-level slot-based overload (`startButton`/`endButton`/`contentColumn`
lambdas) plus several high-level overloads taking `title` / `subtitle` / `onBack` / `onClose` that
wire the predefined buttons and the title/subtitle center for you.
2. **Extension functions on the `object`** for named presets — e.g. `@Composable fun TangemButton.Back(…)`
and `TangemButton.Close(…)` in `TangemButtonExt.kt` expose ready-made button presets while reading
as `TangemButton.Back { … }` at the call site.
Each overload keeps the same rules as the base component (`modifier` first among optionals, DS3 tokens,
its own KDoc — see below).
### Sub-components are first-class
Internal building blocks that are themselves public (e.g. `TangemNavigationText`, used for the
`TangemTopNavigation` title/subtitle slots) follow the **exact same rules** as a top-level component:
DS3 tokens only, `modifier: Modifier = Modifier`, public properties in their own `object`
(`TangemNavigationText.Role`), full KDoc, and their own Storybook entry where it makes sense. Don't
treat "helper" composables as second-class — if a feature can call it, it is a documented DS component.
## KDoc requirements for components
Every public DS component (and any non-trivial public composable) must carry a KDoc block. Use
`ds2/button/TangemButton.kt` and `ds2/checkbox/TangemCheckmark.kt` as the canonical examples.
A component KDoc must contain, in order:
1. **Summary line.** One sentence stating what the component is and which generation it belongs to —
start with `Design-system v2 …` for DS3 components (matches the existing wording).
2. **Figma link.** A markdown link to the exact Figma node:
`[Figma](https://www.figma.com/design/…?node-id=…)`. A component without a Figma reference is not
review-ready.
3. **Behavior notes** (when behavior is non-obvious). A short prose paragraph or a bulleted
`Behavior notes:` list covering state-dependent rendering — loading, disabled/enabled, icon-only
vs. labeled, focus ring, animations, what overrides what. Describe *observable behavior*, not the
implementation.
4. **`@param` for every parameter.** No parameter may be left undocumented — including `modifier`
when its effect is non-trivial (e.g. "Pass `Modifier.fillMaxWidth()` to switch to fixed-width
layout"). Each `@param` states the meaning **and** the consequences of notable values
(`null` → non-interactive, `false` → dimmed & clicks ignored, etc.).
5. **Accessibility guidance** where relevant — e.g. when `contentDescription` should be supplied
(icon-only buttons, loading state, disabled state) and what it announces.
Additional rules:
- Document the **nested `enum`s** (`Variant`, `Size`, `Status`, …) too: a short KDoc on the enum and,
where the options aren't self-explanatory, a one-line description per entry (see `TangemButton.Variant`).
- Keep KDoc about **contract and behavior**, not internals. Implementation comments explaining *why*
a specific approach was taken belong to inline `//` comments inside the body, not the KDoc.
- Reference other DS types with `[TangemSurface]` / `[TangemButton.Variant]` link syntax so they
resolve in the IDE.
- Detekt enforces missing-KDoc-on-public-API style checks on `core:ui`; run `./gradlew :core:ui:detektMain`.
## Storybook
Add every DS3 component to the **Storybook** (module `features/tester`) — a live on-device/emulator
component gallery (Tester → Storybook → DS Components).
Use the **`add-storybook-component`** skill — it wires the entity, the Build factory, the Composable
page, and registers it in the correct list. Run: `/add-storybook-component TangemCheckmark (DS)`.
Page layout guidelines live in
`features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/STORYBOOK.md`.
## Checklist: adding a new DS3 component
- [ ] Component created under `core/ui/.../ds2/<component>/`, package `com.tangem.core.ui.ds2.<component>`.
- [ ] Named `Tangem<Name>`; first optional parameter is `modifier: Modifier = Modifier`.
- [ ] Uses **only** DS3 tokens: `colors3`, `typography3`, `dimens2`. No hardcoded values outside previews.
- [ ] Variants/sizes expressed as an `enum` inside `object Tangem<Name>` (not a set of boolean flags).
- [ ] All public types (enums, statuses, constants) declared inside the `object Tangem<Name>`.
- [ ] Convenient overloads provided (simpler `@Composable` overloads and/or `object` extension presets).
- [ ] Public sub-components (e.g. `TangemNavigationText`) follow the same rules + KDoc as a full component.
- [ ] States handled: enabled/disabled, press/focus (`interactionSource`), loading (if applicable).
- [ ] Accessibility: `contentDescription`, `Role`, `disabled()` in `semantics`.
- [ ] KDoc per the requirements above (summary + Figma link + behavior notes + every `@param` + a11y).
- [ ] Two `@Preview`s (Light/Dark) in `TangemThemePreviewRedesign`, background `colors3.bg.primary`.
- [ ] Heavy component split into `Tangem<Name>.kt` / `…Internal.kt` / `…Ext.kt`.
- [ ] Storybook page added (`add-storybook-component` skill).
- [ ] Detekt passes: `./gradlew :core:ui:detektMain` (plus
`./gradlew :features:tester:impl:assembleGoogleDebug` if you touched the Storybook).
- [ ] Use in product features only from app version **6.0** onward.

View file

@ -0,0 +1,150 @@
---
name: add-storybook-component
description: Add a component showcase page to the Tangem storybook (in features/tester). Wires the entity, Build factory, Composable page, and registers it either in the "DS Components" sub-list (first/default target — for design-system components under core.ui.ds2.*) or in the root storybook list (second target — for any other component). Use when asked to "add a storybook page/story", "add <Component> to the storybook", "сделай сторибук для <компонент>", "добавь стори/историю в storybook", or to showcase a DS component in the tester.
allowed-tools: Read, Grep, Glob, Bash, Edit, Write
argument-hint: [component to add, e.g. "TangemCheckbox (DS)" or "MyLegacyCard"]
---
Add a new component page to the Tangem storybook. The storybook lives in
`features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/`
and renders interactive DS/component showcases on a device or emulator.
This is an **interactive** skill: read the real production component first to get its actual
parameters, enums, and package — never guess the API. Then mirror the closest existing story.
## Two placement targets — pick one
| Target | Use for | List screen | Page dir | Entity supertype |
|---|---|---|---|---|
| **1. DS Components (default)** | Design-system components under `com.tangem.core.ui.ds2.*` (the newest "DS3"/redesign components: `TangemButton`, `TangemBadge`, `TangemRow`, `TangemLoader`, …) | `page/ds/DsComponentsListScreen.kt``buildDsStories()` | `page/ds/<component>/` | `DsStoryBookPage` |
| **2. Other components** | Anything else (legacy/cross-cutting components, backgrounds, effects, typography demos) | `ui/StoryBookListScreen.kt``buildStories()` | `page/<component>/` | `StoryBookPage` |
**Default to Target 1 (DS Components)** when the component lives under `core.ui.ds2.*` or the user
mentions "DS"/"ds3"/"design system". Only the **list screen** and **page directory** differ between
the two targets — everything else (entity declaration file, `StoryBookScreen.kt` routing, factory
pattern) is identical.
> The ONLY behavioral difference of `DsStoryBookPage` vs `StoryBookPage`: `StoryBookViewModel.onBackClick`
> routes a `DsStoryBookPage` back to the DS sub-list, while a plain `StoryBookPage` routes back to the
> root list. That's it.
## Reference
`features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/STORYBOOK.md` is the canonical doc — read it for the **design guidelines** (mandatory
page layout: single live preview pinned at top + one control per parameter below, chip-selector pattern,
colors, realistic text). This skill covers the *wiring*; STORYBOOK.md covers the *look*.
Best reference implementations to mirror:
- **Stateful DS page with many controls:** `page/ds/button/` (TangemButton — variant/size/background
selectors, toggles, text-scale slider, blur backdrop). Read all three files: `Build.kt`,
`TangemButtonStory.kt`, and the `TangemButtonStory` entity in `entity/StoryBookPage.kt`.
- **Simple stateful page:** `page/ds/loader/` (TangemLoader — single size selector).
- **Stateless page (no params):** a `data object` sibling such as `ButtonsStory`.
## Workflow
1. **Read the production component.** Grep `core/ui/src/main/java/com/tangem/core/ui/ds2/<name>/`
(or wherever it lives) for the composable signature, its `enum`s (Variant/Size/Status/…), and
required vs optional params. The set of parameters becomes the set of controls.
2. **Decide stateless vs stateful:**
- **Stateless** (`data object`) — ONLY if the component has no configurable parameters at all.
- **Stateful** (`data class`) — the normal case: one field per parameter the user can change, each
paired with an `onXxxChange`/`onXxxToggle` lambda.
3. **Pick the target** (see table above) and **mirror the closest sibling**.
4. **Do the 4 edits + 1 new dir** (Steps AE below).
5. **Verify it compiles** (see Build).
## The edits
Assume component `Foo` rendered by `com.tangem.core.ui.ds2.foo.TangemFoo` with a `Variant` enum and an
`isEnabled` flag. Adjust names to the real component. `<page-dir>` =
`page/ds/foo/` for Target 1, or `page/foo/` for Target 2.
### A. Declare the entity in `entity/StoryBookPage.kt`
Stateful (normal):
```kotlin
internal data class TangemFooStory(
val variant: TangemFoo.Variant,
val isEnabled: Boolean,
val onVariantChange: (TangemFoo.Variant) -> Unit,
val onEnabledToggle: () -> Unit,
) : DsStoryBookPage // <- StoryBookPage for Target 2
```
Stateless: `internal data object TangemFooStory : DsStoryBookPage` (or `StoryBookPage`).
Add the matching import for the production type at the top of the file.
### B. Create `<page-dir>/Build.kt`
Stateful — uses `storyPageFactory` + `StateUpdater`:
```kotlin
internal fun StateUpdater<TangemFooStory>.build(): TangemFooStory {
return TangemFooStory(
variant = TangemFoo.Variant.Primary,
isEnabled = true,
onVariantChange = { v -> updateStory { it.copy(variant = v) } },
onEnabledToggle = { updateStory { it.copy(isEnabled = !it.isEnabled) } },
)
}
internal val tangemFooStoryFactory
get() = storyPageFactory(StateUpdater<TangemFooStory>::build)
```
Stateless: `internal val tangemFooStoryFactory: StoryPageFactory = StoryPageFactory { TangemFooStory }`
### C. Create `<page-dir>/TangemFooStory.kt`
`@Composable internal fun TangemFooStory(state: TangemFooStory, modifier: Modifier = Modifier)`
(drop `state` for stateless). Follow STORYBOOK.md design guidelines: live preview pinned at the top
in a `Column`, controls scrolling below. Reuse the chip-selector / toggle-row patterns from
`page/ds/button/TangemButtonStory.kt` (its `Section`, `ChipGrid`, `Chip`, `ToggleRow` are private —
copy the ones you need into the new file). Use representative text, not "Btn".
### D. Register routing in `ui/StoryBookScreen.kt`
Add both imports (entity + page composable share the simple name — Kotlin resolves them by position):
```kotlin
import com.tangem.feature.tester.presentation.storybook.entity.TangemFooStory
import com.tangem.feature.tester.presentation.storybook.page.ds.foo.TangemFooStory
```
Add a branch to the `when (storyState)`:
```kotlin
is TangemFooStory -> TangemFooStory(state = storyState) // stateless: TangemFooStory -> TangemFooStory()
```
### E. Register in the list screen (target-specific)
- **Target 1 (DS):** in `page/ds/DsComponentsListScreen.kt` add the factory import and a row to
`buildDsStories()`:
```kotlin
DsStoryItem(title = "🔘 TangemFoo", factory = tangemFooStoryFactory),
```
- **Target 2 (other):** in `ui/StoryBookListScreen.kt` add the factory import and a row to
`buildStories()`:
```kotlin
StoryItem(title = "🔘 Foo", factory = tangemFooStoryFactory),
```
**Every title must start with an emoji** matching the component category (🔘 buttons, 🏷️ badge,
📋 row, ⏳ loader, 🔤 typography, 🔍 search, 🧭 navigation, 💀 placeholder, ✨ effects, 🪙 token…).
## Build
```bash
./gradlew :features:tester:impl:assembleGoogleDebug
```
Detekt runs via the convention plugin; keep `@file:Suppress("MagicNumber")` on showcase files that use
literal dp/colors (the button story does this). Then run the app, open Tester → Storybook → (DS
Components →) your entry, and confirm the preview + every control works.
## Checklist
- [ ] Read the real component; every meaningful parameter has a control.
- [ ] Entity in `StoryBookPage.kt` extends the correct supertype (`DsStoryBookPage` for DS, else `StoryBookPage`).
- [ ] `Build.kt` factory name is `<camelCaseName>StoryFactory`.
- [ ] Page composable shares the entity's simple name; both imported in `StoryBookScreen.kt`.
- [ ] `when` branch added in `StoryBookScreen.kt` (`is` prefix for stateful, bare for stateless).
- [ ] Registered in the correct list screen with an emoji-prefixed title.
- [ ] Live preview pinned at top, controls below (STORYBOOK.md layout rule).
- [ ] `:features:tester:impl:assembleGoogleDebug` passes.