From 740da6cddd63bde331f5e58723b67138df1743b1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jun 2026 18:21:20 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .claude/rules/codestyle/design-system.md | 163 ++++++++++++ .../skills/add-storybook-component/SKILL.md | 150 +++++++++++ .../core/ui/ds2/checkbox/TangemCheckbox.kt | 246 ++++++++++++++++++ .../core/ui/ds2/checkbox/TangemCheckmark.kt | 214 +++++++++++++++ .../presentation/storybook}/STORYBOOK.md | 0 .../storybook/entity/StoryBookPage.kt | 15 ++ .../page/ds/DsComponentsListScreen.kt | 4 + .../storybook/page/ds/checkbox/Build.kt | 22 ++ .../page/ds/checkbox/TangemCheckboxV2Story.kt | 190 ++++++++++++++ .../storybook/page/ds/checkmark/Build.kt | 21 ++ .../page/ds/checkmark/TangemCheckmarkStory.kt | 125 +++++++++ .../storybook/ui/StoryBookScreen.kt | 4 + 12 files changed, 1154 insertions(+) create mode 100644 .claude/rules/codestyle/design-system.md create mode 100644 .claude/skills/add-storybook-component/SKILL.md create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds2/checkbox/TangemCheckbox.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds2/checkbox/TangemCheckmark.kt rename features/tester/{ => impl/src/main/java/com/tangem/feature/tester/presentation/storybook}/STORYBOOK.md (100%) create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkbox/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkbox/TangemCheckboxV2Story.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkmark/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkmark/TangemCheckmarkStory.kt diff --git a/.claude/rules/codestyle/design-system.md b/.claude/rules/codestyle/design-system.md new file mode 100644 index 0000000000..f41727ac79 --- /dev/null +++ b/.claude/rules/codestyle/design-system.md @@ -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.`, folder + `core/ui/.../ds2//`. The component name is `Tangem`. +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` (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.kt`, private `TangemInternal.kt`, tokens `TangemExt.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`, **not** as a top-level type. This keeps a single +`Tangem.Variant` / `Tangem.Size` / `Tangem.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//`, package `com.tangem.core.ui.ds2.`. +- [ ] Named `Tangem`; 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` (not a set of boolean flags). +- [ ] All public types (enums, statuses, constants) declared inside the `object Tangem`. +- [ ] 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.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. \ No newline at end of file diff --git a/.claude/skills/add-storybook-component/SKILL.md b/.claude/skills/add-storybook-component/SKILL.md new file mode 100644 index 0000000000..1a85fa944c --- /dev/null +++ b/.claude/skills/add-storybook-component/SKILL.md @@ -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 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//` | `DsStoryBookPage` | +| **2. Other components** | Anything else (legacy/cross-cutting components, backgrounds, effects, typography demos) | `ui/StoryBookListScreen.kt` → `buildStories()` | `page//` | `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//` + (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 A–E 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/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 `/Build.kt` + +Stateful — uses `storyPageFactory` + `StateUpdater`: +```kotlin +internal fun StateUpdater.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::build) +``` +Stateless: `internal val tangemFooStoryFactory: StoryPageFactory = StoryPageFactory { TangemFooStory }` + +### C. Create `/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 `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. \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds2/checkbox/TangemCheckbox.kt b/core/ui/src/main/java/com/tangem/core/ui/ds2/checkbox/TangemCheckbox.kt new file mode 100644 index 0000000000..89665b8586 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds2/checkbox/TangemCheckbox.kt @@ -0,0 +1,246 @@ +package com.tangem.core.ui.ds2.checkbox + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.triStateToggleable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.lerp +import com.tangem.core.ui.extensions.conditionalCompose +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.res.generated.icons.Icons +import com.tangem.core.ui.res.generated.icons.ic_control_box_24 +import com.tangem.core.ui.res.generated.icons.ic_control_box_24_filled +import com.tangem.core.ui.res.generated.icons.ic_control_checkmark_24 +import com.tangem.core.ui.res.generated.icons.ic_control_indeterminate_24 + +/** + * Design-system v2 tri-state checkbox: an `unchecked` outline box, a `checked` filled box with a + * checkmark, or an `indeterminate` filled box with a dash. + * + * [Figma](https://www.figma.com/design/y8arHOHCa6HjMpOMJ0Ykj6/DS-64-%7C-Token-Icon?node-id=3650-628) + * + * @param state Current tri-state value. See [ToggleableState]. + * @param onClick Invoked on toggle. `null` makes the checkbox non-interactive. + * @param isEnabled When `false`, the checkbox is dimmed and clicks are ignored. + * @param contentDescription Accessibility label announced by TalkBack. + * @param interactionSource Interaction source for press/focus state. + */ +@Suppress("MagicNumber", "LongMethod") +@Composable +fun TangemCheckbox( + state: ToggleableState, + onClick: (() -> Unit)?, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, + contentDescription: String? = null, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + val isPressed by interactionSource.collectIsPressedAsState() + val isFocused by interactionSource.collectIsFocusedAsState() + + val contentAlpha = if (isEnabled) 1f else 0.4f // opacity/disabled + + // Whole control shrinks slightly while pressed and springs back on release. + val pressScale by animateFloatAsState( + targetValue = if (isPressed) 0.92f else 1f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMediumLow), + label = "pressScale", + ) + // Press fill fades in/out instead of toggling instantly. + val pressColor by animateColorAsState( + targetValue = if (isPressed) TangemTheme.colors3.interaction.press.default else Color.Transparent, + animationSpec = tween(durationMillis = 100), + label = "pressColor", + ) + // Drives the filled box growing over the outline (0 = unchecked, 1 = filled). + val fillProgress by animateFloatAsState( + targetValue = if (state == ToggleableState.Off) 0f else 1f, + animationSpec = tween(durationMillis = 150), + label = "fillProgress", + ) + + Box( + modifier = modifier + .graphicsLayer { + alpha = contentAlpha + scaleX = pressScale + scaleY = pressScale + } + .semantics(mergeDescendants = true) { + if (!isEnabled) disabled() + contentDescription?.let { this.contentDescription = it } + } + .conditionalCompose(onClick != null) { + triStateToggleable( + state = state, + onClick = requireNotNull(onClick), + enabled = isEnabled, + role = Role.Checkbox, + interactionSource = interactionSource, + indication = null, + ) + } + .size(24.dp) + .clip(CheckboxShape) + .drawBehind { drawRect(pressColor) } + .conditionalCompose(isFocused) { + border( + width = 2.dp, // border-width/md + color = TangemTheme.colors3.interaction.focusRing.brand, + shape = CheckboxShape, + ) + }, + contentAlignment = Alignment.Center, + ) { + // Outline box — always present; the filled box grows over it. + Icon( + imageVector = Icons.ic_control_box_24, + contentDescription = null, + tint = TangemTheme.colors3.icon.primary, + ) + // Filled box — fades and scales in from the center as the checkbox becomes filled. + Icon( + imageVector = Icons.ic_control_box_24_filled, + contentDescription = null, + tint = TangemTheme.colors3.icon.primary, + modifier = Modifier.graphicsLayer { + alpha = fillProgress + val markScale = lerp(start = 0.5f, stop = 1f, fraction = fillProgress) + scaleX = markScale + scaleY = markScale + }, + ) + // Mark — checkmark or dash pops in, and crossfades when switching between the two. + AnimatedContent( + targetState = state, + transitionSpec = { + val enter = scaleIn( + initialScale = 0.5f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMediumLow, + ), + ) + fadeIn() + val exit = scaleOut(targetScale = 0.5f) + fadeOut() + enter togetherWith exit + }, + label = "mark", + ) { current -> + when (current) { + ToggleableState.On -> Icon( + imageVector = Icons.ic_control_checkmark_24, + contentDescription = null, + tint = TangemTheme.colors3.icon.inverse, + ) + ToggleableState.Indeterminate -> Icon( + imageVector = Icons.ic_control_indeterminate_24, + contentDescription = null, + tint = TangemTheme.colors3.icon.inverse, + ) + ToggleableState.Off -> Box(Modifier.size(24.dp)) + } + } + } +} + +/** + * Boolean (checked / unchecked) overload of [TangemCheckbox] for the common two-state case. + * + * @param checked Whether the checkbox is checked. + * @param onCheckedChange Invoked with the toggled value. `null` makes the checkbox non-interactive. + */ +@Composable +fun TangemCheckbox( + checked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, + contentDescription: String? = null, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + TangemCheckbox( + state = ToggleableState(checked), + onClick = onCheckedChange?.let { { it(!checked) } }, + modifier = modifier, + isEnabled = isEnabled, + contentDescription = contentDescription, + interactionSource = interactionSource, + ) +} + +private val CheckboxShape = RoundedCornerShape(6.dp) + +@Preview(name = "Light", showBackground = true) +@Preview(name = "Dark", uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, showBackground = true) +@Composable +private fun TangemCheckboxPreview() { + TangemThemePreviewRedesign { + Column( + modifier = Modifier + .background(TangemTheme.colors3.bg.primary) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + PreviewRow(label = "Enabled", isEnabled = true) + PreviewRow(label = "Disabled", isEnabled = false) + } + } +} + +@Composable +private fun PreviewRow(label: String, isEnabled: Boolean) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = label, + color = TangemTheme.colors3.text.secondary, + style = TangemTheme.typography3.body.medium, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + ToggleableState.entries.forEach { state -> + TangemCheckbox(state = state, onClick = {}, isEnabled = isEnabled) + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds2/checkbox/TangemCheckmark.kt b/core/ui/src/main/java/com/tangem/core/ui/ds2/checkbox/TangemCheckmark.kt new file mode 100644 index 0000000000..ec4d732723 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds2/checkbox/TangemCheckmark.kt @@ -0,0 +1,214 @@ +package com.tangem.core.ui.ds2.checkbox + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.disabled +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.lerp +import com.tangem.core.ui.extensions.conditionalCompose +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.res.generated.icons.Icons +import com.tangem.core.ui.res.generated.icons.ic_control_checkmark_24 +import com.tangem.core.ui.res.generated.icons.ic_control_circle_24 +import com.tangem.core.ui.res.generated.icons.ic_control_circle_24_filled + +/** + * Design-system v2 circular checkmark: an `unchecked` outline circle or a `checked` filled circle + * with a checkmark. Unlike [TangemCheckbox], this control is round (border-radius/full) and + * boolean-only — it has no indeterminate state. + * + * [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=3671-5693) + * + * @param checked Whether the checkmark is checked. + * @param onCheckedChange Invoked with the toggled value. `null` makes the control non-interactive. + * @param isEnabled When `false`, the control is dimmed and clicks are ignored. + * @param contentDescription Accessibility label announced by TalkBack. + * @param interactionSource Interaction source for press/focus state. + */ +@Suppress("MagicNumber", "LongMethod") +@Composable +fun TangemCheckmark( + checked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, + contentDescription: String? = null, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + val isPressed by interactionSource.collectIsPressedAsState() + val isFocused by interactionSource.collectIsFocusedAsState() + + val contentAlpha = if (isEnabled) 1f else 0.4f + + // Whole control shrinks slightly while pressed and springs back on release. + val pressScale by animateFloatAsState( + targetValue = if (isPressed) 0.92f else 1f, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessMediumLow), + label = "pressScale", + ) + // Press fill fades in/out instead of toggling instantly. + val pressColor by animateColorAsState( + targetValue = if (isPressed) TangemTheme.colors3.interaction.press.default else Color.Transparent, + animationSpec = tween(durationMillis = 100), + label = "pressColor", + ) + // Drives the filled circle growing over the outline (0 = unchecked, 1 = filled). + val fillProgress by animateFloatAsState( + targetValue = if (checked) 1f else 0f, + animationSpec = tween(durationMillis = 150), + label = "fillProgress", + ) + + Box( + modifier = modifier + .graphicsLayer { + alpha = contentAlpha + scaleX = pressScale + scaleY = pressScale + } + .semantics(mergeDescendants = true) { + if (!isEnabled) disabled() + contentDescription?.let { this.contentDescription = it } + } + .conditionalCompose(onCheckedChange != null) { + toggleable( + value = checked, + onValueChange = requireNotNull(onCheckedChange), + enabled = isEnabled, + role = Role.Checkbox, + interactionSource = interactionSource, + indication = null, + ) + } + .size(24.dp) + .clip(CircleShape) + .drawBehind { drawRect(pressColor) } + .conditionalCompose(isFocused) { + border( + width = 2.dp, + color = TangemTheme.colors3.interaction.focusRing.brand, + shape = CircleShape, + ) + }, + contentAlignment = Alignment.Center, + ) { + // Outline circle — always present; the filled circle grows over it. + Icon( + imageVector = Icons.ic_control_circle_24, + contentDescription = null, + tint = TangemTheme.colors3.icon.primary, + ) + // Filled circle — fades and scales in from the center as the checkmark becomes filled. + Icon( + imageVector = Icons.ic_control_circle_24_filled, + contentDescription = null, + tint = TangemTheme.colors3.icon.primary, + modifier = Modifier.graphicsLayer { + alpha = fillProgress + val markScale = lerp(start = 0.5f, stop = 1f, fraction = fillProgress) + scaleX = markScale + scaleY = markScale + }, + ) + // Checkmark pops in and fades out as the checked state toggles. + AnimatedContent( + targetState = checked, + transitionSpec = { + val enter = scaleIn( + initialScale = 0.5f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMediumLow, + ), + ) + fadeIn() + val exit = scaleOut(targetScale = 0.5f) + fadeOut() + enter togetherWith exit + }, + label = "mark", + ) { isChecked -> + if (isChecked) { + Icon( + imageVector = Icons.ic_control_checkmark_24, + contentDescription = null, + tint = TangemTheme.colors3.icon.inverse, + ) + } else { + Box(Modifier.size(24.dp)) + } + } + } +} + +@Preview(name = "Light", showBackground = true) +@Preview(name = "Dark", uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, showBackground = true) +@Composable +private fun TangemCheckmarkPreview() { + TangemThemePreviewRedesign { + Column( + modifier = Modifier + .background(TangemTheme.colors3.bg.primary) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + PreviewRow(label = "Enabled", isEnabled = true) + PreviewRow(label = "Disabled", isEnabled = false) + } + } +} + +@Composable +private fun PreviewRow(label: String, isEnabled: Boolean) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = label, + color = TangemTheme.colors3.text.secondary, + style = TangemTheme.typography3.body.medium, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + listOf(false, true).forEach { checked -> + TangemCheckmark(checked = checked, onCheckedChange = {}, isEnabled = isEnabled) + } + } + } +} \ No newline at end of file diff --git a/features/tester/STORYBOOK.md b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/STORYBOOK.md similarity index 100% rename from features/tester/STORYBOOK.md rename to features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/STORYBOOK.md diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt index 80da0f8c8d..1d461a65c9 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt @@ -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, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/DsComponentsListScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/DsComponentsListScreen.kt index 5fb1754b5a..1083082fea 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/DsComponentsListScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/DsComponentsListScreen.kt @@ -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), diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkbox/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkbox/Build.kt new file mode 100644 index 0000000000..d607c7ebfa --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkbox/Build.kt @@ -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.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::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkbox/TangemCheckboxV2Story.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkbox/TangemCheckboxV2Story.kt new file mode 100644 index 0000000000..55b8fc8205 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkbox/TangemCheckboxV2Story.kt @@ -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 ChipGrid(items: List, 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, + ) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkmark/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkmark/Build.kt new file mode 100644 index 0000000000..b91360bb21 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkmark/Build.kt @@ -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.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::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkmark/TangemCheckmarkStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkmark/TangemCheckmarkStory.kt new file mode 100644 index 0000000000..385ca5cdae --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/checkmark/TangemCheckmarkStory.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt index 0ca00f04d3..fb4ee3a9a7 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt @@ -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)