prime-ui-kit 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +9 -5
  2. package/dist/chunks/{chunk-ZEFCDEEQ.js → chunk-6JTZOA5I.js} +878 -331
  3. package/dist/chunks/{chunk-LXGRKN34.js → chunk-OAE4IQX2.js} +769 -479
  4. package/dist/chunks/{chunk-IQELQNMR.js → chunk-U5SR2LZR.js} +17 -4
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/button/Button.d.ts +19 -2
  7. package/dist/components/card/Card.d.ts +1 -1
  8. package/dist/components/code-block/CodeBlock.d.ts +1 -1
  9. package/dist/components/color-picker/ColorPicker.d.ts +1 -1
  10. package/dist/components/data-table/Row.d.ts +3 -1
  11. package/dist/components/data-table/rows.d.ts +4 -0
  12. package/dist/components/digit-input/DigitInput.d.ts +6 -1
  13. package/dist/components/index.d.ts +4 -1
  14. package/dist/components/input/Input.d.ts +16 -1
  15. package/dist/components/input/passwordStrength.d.ts +9 -0
  16. package/dist/components/progress-bar/ProgressBar.d.ts +16 -1
  17. package/dist/components/sparkline/Sparkline.d.ts +26 -0
  18. package/dist/hooks/useEdgeOverflow.d.ts +4 -0
  19. package/dist/icons/registry.d.ts +2 -0
  20. package/dist/index.css +830 -267
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +9 -3
  23. package/dist/internal/CheckMark.d.ts +12 -0
  24. package/dist/internal/MorphText.d.ts +12 -0
  25. package/dist/internal/RollingNumber.d.ts +14 -0
  26. package/dist/internal/surfaceDepth.d.ts +25 -0
  27. package/dist/internal/swatch.d.ts +4 -1
  28. package/dist/layout/app-shell/AppShell.d.ts +2 -2
  29. package/dist/motion/celebrate.d.ts +18 -0
  30. package/dist/tokens/layers.d.ts +37 -0
  31. package/dist/tokens/naming.d.ts +1 -1
  32. package/dist/tokens/semantic.d.ts +66 -56
  33. package/dist/tokens/themes/dark.d.ts +6 -14
  34. package/package.json +1 -1
  35. package/src/components/accordion/COMPONENT.md +4 -4
  36. package/src/components/avatar/COMPONENT.md +1 -1
  37. package/src/components/button/COMPONENT.md +26 -5
  38. package/src/components/button/examples/download.tsx +45 -0
  39. package/src/components/button/examples/hold-to-confirm.tsx +23 -0
  40. package/src/components/button/examples/label-morph.tsx +33 -0
  41. package/src/components/card/COMPONENT.md +4 -2
  42. package/src/components/card/examples/narrow.tsx +2 -2
  43. package/src/components/card/examples/panel-chart.tsx +1 -1
  44. package/src/components/card/examples/variants.tsx +1 -1
  45. package/src/components/checkbox/examples/examples.module.css +1 -1
  46. package/src/components/code-block/COMPONENT.md +6 -6
  47. package/src/components/code-block/examples/overview.tsx +1 -1
  48. package/src/components/code-block/examples/variants.tsx +1 -1
  49. package/src/components/color-picker/COMPONENT.md +1 -1
  50. package/src/components/color-swatches/COMPONENT.md +5 -5
  51. package/src/components/command-menu/COMPONENT.md +1 -1
  52. package/src/components/crossfade/examples/overview.tsx +3 -3
  53. package/src/components/data-table/COMPONENT.md +5 -4
  54. package/src/components/digit-input/COMPONENT.md +5 -3
  55. package/src/components/digit-input/examples/on-complete.tsx +7 -6
  56. package/src/components/dnd/COMPONENT.md +1 -1
  57. package/src/components/dnd/examples/examples.module.css +4 -4
  58. package/src/components/dropdown/COMPONENT.md +1 -1
  59. package/src/components/dropdown/examples/examples.module.css +1 -1
  60. package/src/components/empty-page/examples/examples.module.css +1 -1
  61. package/src/components/example-frame/COMPONENT.md +1 -1
  62. package/src/components/input/COMPONENT.md +10 -1
  63. package/src/components/input/examples/password-strength.tsx +12 -0
  64. package/src/components/kanban/COMPONENT.md +4 -4
  65. package/src/components/login-form/COMPONENT.md +4 -4
  66. package/src/components/login-form/examples/register.tsx +2 -2
  67. package/src/components/login-form/examples/reset-password.tsx +2 -2
  68. package/src/components/modal/COMPONENT.md +2 -2
  69. package/src/components/modal/examples/examples.module.css +2 -2
  70. package/src/components/page-content/examples/examples.module.css +1 -1
  71. package/src/components/popover/COMPONENT.md +1 -1
  72. package/src/components/progress-bar/COMPONENT.md +5 -1
  73. package/src/components/progress-bar/examples/examples.module.css +7 -0
  74. package/src/components/progress-bar/examples/milestone.tsx +43 -0
  75. package/src/components/progress-bar/examples/steps.tsx +35 -0
  76. package/src/components/skeleton/examples/with-crossfade.tsx +3 -3
  77. package/src/components/sparkline/COMPONENT.md +98 -0
  78. package/src/components/sparkline/examples/examples.module.css +13 -0
  79. package/src/components/sparkline/examples/overview.tsx +27 -0
  80. package/src/components/sparkline/examples/period.tsx +46 -0
  81. package/src/components/tabs/COMPONENT.md +15 -16
  82. package/src/components/tabs/examples/examples.module.css +14 -0
  83. package/src/components/tabs/examples/orientation.tsx +87 -33
  84. package/src/components/tooltip/COMPONENT.md +8 -6
  85. package/src/components/tooltip/examples/examples.module.css +2 -2
  86. package/src/layout/app-header/COMPONENT.md +1 -1
  87. package/src/layout/app-header/examples/examples.module.css +4 -4
  88. package/src/layout/app-shell/COMPONENT.md +8 -8
  89. package/src/layout/app-shell/examples/examples.module.css +3 -3
  90. package/src/layout/bottom-nav/COMPONENT.md +1 -1
  91. package/src/layout/bottom-nav/examples/examples.module.css +1 -1
  92. package/src/layout/sidebar/COMPONENT.md +4 -4
  93. package/src/layout/sidebar/examples/examples.module.css +5 -5
  94. package/src/styles/globals.css +125 -2
  95. package/src/styles/theme-dark.css +36 -13
  96. package/src/styles/theme-light.css +60 -16
@@ -0,0 +1,45 @@
1
+ /** A long download inside the button: the fill shows how far it got, the label counts and then offers the file — `progress`. */
2
+ import { Button, Icon } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ /** Share of the file received at each network tick. */
6
+ const TICKS = [0.06, 0.15, 0.27, 0.34, 0.48, 0.61, 0.66, 0.79, 0.88, 0.97, 1];
7
+
8
+ export default function ButtonDownloadExample() {
9
+ const [tick, setTick] = React.useState<number | null>(null);
10
+ const [ready, setReady] = React.useState(false);
11
+
12
+ React.useEffect(() => {
13
+ if (tick === null) return;
14
+ const timer = window.setTimeout(() => {
15
+ if (tick + 1 < TICKS.length) setTick(tick + 1);
16
+ else {
17
+ setTick(null);
18
+ setReady(true);
19
+ }
20
+ }, 220);
21
+ return () => window.clearTimeout(timer);
22
+ }, [tick]);
23
+
24
+ const progress = tick === null ? undefined : TICKS[tick];
25
+
26
+ return (
27
+ <Button.Root
28
+ variant={ready ? "soft" : "solid"}
29
+ progress={progress}
30
+ onClick={() => {
31
+ if (ready) setReady(false);
32
+ else if (tick === null) setTick(0);
33
+ }}
34
+ >
35
+ <Button.Icon>
36
+ <Icon name={ready ? "action.externalLink" : "action.download"} />
37
+ </Button.Icon>
38
+ {progress !== undefined
39
+ ? `Скачивание ${Math.round(progress * 100)}%`
40
+ : ready
41
+ ? "Открыть отчёт"
42
+ : "Скачать отчёт за октябрь"}
43
+ </Button.Root>
44
+ );
45
+ }
@@ -0,0 +1,23 @@
1
+ /** A destructive action that needs a held press; letting go early rolls the fill back and does nothing — `holdToConfirm`, `onConfirm`. */
2
+ import { Button, Icon } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ export default function ButtonHoldToConfirmExample() {
6
+ const [deleted, setDeleted] = React.useState(false);
7
+
8
+ return deleted ? (
9
+ <Button.Root variant="ghost" tone="neutral" onClick={() => setDeleted(false)}>
10
+ <Button.Icon>
11
+ <Icon name="action.refresh" />
12
+ </Button.Icon>
13
+ Восстановить проект
14
+ </Button.Root>
15
+ ) : (
16
+ <Button.Root variant="soft" tone="danger" holdToConfirm onConfirm={() => setDeleted(true)}>
17
+ <Button.Icon>
18
+ <Icon name="action.delete" />
19
+ </Button.Icon>
20
+ Удерживайте, чтобы удалить проект
21
+ </Button.Root>
22
+ );
23
+ }
@@ -0,0 +1,33 @@
1
+ /** A new label flows into the button letter by letter while the width glides: save, then saved; a step flow — `children`, `loading`. */
2
+ import { Button } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ const STEPS = ["Продолжить", "Подтвердить оплату", "Оплачено"];
6
+
7
+ export default function ButtonLabelMorphExample() {
8
+ const [save, setSave] = React.useState<"idle" | "saving" | "saved">("idle");
9
+ const [step, setStep] = React.useState(0);
10
+
11
+ React.useEffect(() => {
12
+ if (save === "idle") return;
13
+ const timer = window.setTimeout(
14
+ () => setSave(save === "saving" ? "saved" : "idle"),
15
+ save === "saving" ? 1200 : 2000,
16
+ );
17
+ return () => window.clearTimeout(timer);
18
+ }, [save]);
19
+
20
+ return (
21
+ <>
22
+ <Button.Root
23
+ variant="soft"
24
+ tone="neutral"
25
+ loading={save === "saving"}
26
+ onClick={() => setSave("saving")}
27
+ >
28
+ {save === "saved" ? "Изменения сохранены" : "Сохранить изменения"}
29
+ </Button.Root>
30
+ <Button.Root onClick={() => setStep((step + 1) % STEPS.length)}>{STEPS[step]}</Button.Root>
31
+ </>
32
+ );
33
+ }
@@ -39,7 +39,7 @@ Card.Root variant="cover" Media · Body(Title, Description) · Footer
39
39
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
40
40
 
41
41
  ### Card.Root
42
- `ref` → `HTMLDivElement`. The filled surface (card fill, radius 12, raised shadow, no border) and a size container; `variant` picks the template layout of its parts.
42
+ `ref` → `HTMLDivElement`. A layer of the surface ladder (`data-depth` one above the surface around it: white on the light page, the next layer when nested; radius 12, the raised whisper only on the page, no border) and a size container; `variant` picks the template layout of its parts.
43
43
 
44
44
  | Prop | Type | Default | Description |
45
45
  |---|---|---|---|
@@ -143,13 +143,15 @@ Card.Root variant="cover" Media · Body(Title, Description) · Footer
143
143
  |---|---|---|
144
144
  | template | `variant` | `data-variant` |
145
145
  | flat | `flat` | `data-flat` |
146
+ | depth | nesting: one above the nearest surface (the page and the AppShell panel are 0) | `data-depth` (`1`–`4`, `floating-1`/`floating-2` inside a floating layer); the fill is `--prime-color-layer-current`, the shadow only at depth 1 |
146
147
  | card width | container queries | `split` stacks below 22rem, `Header` wraps below 22rem, the `stat-trend` value changes at 20rem / 36rem |
147
148
 
148
149
  Card is static: no hover, focus or press of its own.
149
150
 
150
151
  ## Layout & spacing
151
152
  - Grid of cards: `repeat(auto-fit, minmax(min(100%, 14rem), 1fr))`, gap `--prime-space-4`.
152
- - Padding `--prime-card-padding-m` (20); `Body` gap `--prime-card-gap` (16); fields inside `Body` 20 apart, on the surface field fill.
153
+ - Padding `--prime-card-padding-m` (20); `Body` gap `--prime-card-gap` (16); fields inside `Body` 20 apart, one ladder step off the card.
154
+ - Depth comes from nesting (foundation §4): on the page — the AppShell panel is the page — a card is layer 1 (white in light) with the raised whisper; inside another card it is the next layer, a flat tile. The ladder stops at layer 4.
153
155
  - Long values wrap, labels truncate; a chart SVG needs a CSS height.
154
156
  - In a `Header` the title takes the free width; a control or a caption after it sits at the end without a wrapper.
155
157
 
@@ -28,7 +28,7 @@ export default function CardNarrowExample() {
28
28
  <Icon name="object.wallet" />
29
29
  </Card.Icon>
30
30
  <Card.Label>Средний чек</Card.Label>
31
- <Card.Value>₽ 2 450</Card.Value>
31
+ <Card.Value>2 450 ₽</Card.Value>
32
32
  </div>
33
33
  </Card.Body>
34
34
  </Card.Root>
@@ -36,7 +36,7 @@ export default function CardNarrowExample() {
36
36
  <div className={width.className}>
37
37
  <Card.Root variant="stat-trend">
38
38
  <Card.Label>Выручка</Card.Label>
39
- <Card.Value>₽ 4,2 млн</Card.Value>
39
+ <Card.Value>4,2 млн ₽</Card.Value>
40
40
  <Card.Delta tone="success">+18% к прошлому месяцу</Card.Delta>
41
41
  </Card.Root>
42
42
  </div>
@@ -23,7 +23,7 @@ export default function CardPanelChartExample() {
23
23
  </Card.Header>
24
24
  <Card.Body>
25
25
  <Typography variant="body-s" tone="secondary">
26
- С начала квартала: ₽ 12,6 млн, план выполнен на 84%.
26
+ С начала квартала: 12,6 млн ₽, план выполнен на 84%.
27
27
  </Typography>
28
28
  </Card.Body>
29
29
  <Card.Media>
@@ -24,7 +24,7 @@ export default function CardVariantsExample() {
24
24
 
25
25
  <Card.Root variant="stat-trend">
26
26
  <Card.Label>Выручка за месяц</Card.Label>
27
- <Card.Value>₽ 4,2 млн</Card.Value>
27
+ <Card.Value>4,2 млн ₽</Card.Value>
28
28
  <Card.Delta tone="success">+18% к сентябрю</Card.Delta>
29
29
  </Card.Root>
30
30
 
@@ -41,7 +41,7 @@
41
41
  width: min(100%, var(--prime-panel-min-width));
42
42
  padding: var(--prime-panel-padding);
43
43
  border-radius: var(--prime-panel-radius);
44
- background: var(--prime-color-bg-raised);
44
+ background: var(--prime-color-layer-floating-bg);
45
45
  box-shadow: var(--prime-shadow-overlay);
46
46
  }
47
47
 
@@ -3,7 +3,7 @@
3
3
  **Category:** data-display
4
4
  **Kind:** primitive
5
5
 
6
- > A static TypeScript / TSX snippet with syntax highlighting, on a sunken panel or bare inside a host.
6
+ > A static TypeScript / TSX snippet with syntax highlighting, on a filled panel or bare inside a host.
7
7
 
8
8
  ## When to use
9
9
  - Code samples in docs, settings («API key usage») and onboarding.
@@ -37,7 +37,7 @@ CodeBlock <pre>; variant, colorScheme (data-theme)
37
37
  | Prop | Type | Default | Description |
38
38
  |---|---|---|---|
39
39
  | `code` | `string` | — (required) | TS / TSX source; trailing whitespace is trimmed, then highlighted. |
40
- | `variant` | `"soft" \| "ghost"` | `"soft"` | `soft` — sunken panel with padding and the `code` text role; `ghost` — bare `pre` that inherits type and background from its host. |
40
+ | `variant` | `"soft" \| "ghost"` | `"soft"` | `soft` — a filled panel (one ladder step off its host) with padding and the `code` text role; `ghost` — bare `pre` that inherits type and background from its host. |
41
41
  | `colorScheme` | `"light" \| "dark"` | — | Fixes the theme for this block only (`data-theme`). Omit to follow the page theme. |
42
42
  | `tabIndex` | `number` | — | Default `0` for `soft` (a scrolling block stays reachable from the keyboard), none for `ghost`; pass `-1` when it never overflows. |
43
43
  | `…rest` | `Omit<HTMLAttributes<HTMLPreElement>, "children" \| "dangerouslySetInnerHTML">` | — | `className`, `aria-label` and the other `<pre>` attributes. |
@@ -54,8 +54,8 @@ CodeBlock <pre>; variant, colorScheme (data-theme)
54
54
  | Value | Looks like | Use when | Default |
55
55
  |---|---|---|---|
56
56
  | — (omitted) | token colors and fill follow the page theme | normal case | yes |
57
- | `light` | light palette; the soft fill becomes that scheme's `bg-raised` | a light sample inside a dark page | |
58
- | `dark` | dark palette; the soft fill becomes that scheme's `bg-raised` | a terminal-like sample inside a light page | |
57
+ | `light` | light palette; the soft fill becomes that scheme's floating layer (`layer-floating-bg`) | a light sample inside a dark page | |
58
+ | `dark` | dark palette; the soft fill becomes that scheme's floating layer (`layer-floating-bg`) | a terminal-like sample inside a light page | |
59
59
 
60
60
  Syntax tokens: keywords purple (weight 500), strings teal, numbers orange, JSX tags blue, comments muted italic — palette text tokens, readable in both themes.
61
61
 
@@ -92,8 +92,8 @@ No `labels`.
92
92
  ## Examples
93
93
  | Example | Shows |
94
94
  |---|---|
95
- | [overview.tsx](examples/overview.tsx) | An API response sample on a sunken panel, named for screen readers — `code`, `aria-label`. |
96
- | [variants.tsx](examples/variants.tsx) | A sunken panel and a bare block that takes type and background from its host — `variant`. |
95
+ | [overview.tsx](examples/overview.tsx) | An API response sample on a filled panel, named for screen readers — `code`, `aria-label`. |
96
+ | [variants.tsx](examples/variants.tsx) | A filled panel and a bare block that takes type and background from its host — `variant`. |
97
97
  | [color-scheme.tsx](examples/color-scheme.tsx) | A block fixed to one scheme looks the same in both page themes — `colorScheme`. |
98
98
  | [narrow.tsx](examples/narrow.tsx) | In a narrow column a long line scrolls inside the block and never wraps; Tab, then arrow keys. |
99
99
 
@@ -1,4 +1,4 @@
1
- /** An API response sample on a sunken panel, named for screen readers — `code`, `aria-label`. */
1
+ /** An API response sample on a filled panel, named for screen readers — `code`, `aria-label`. */
2
2
  import { CodeBlock } from "prime-ui-kit";
3
3
 
4
4
  import styles from "./examples.module.css";
@@ -1,4 +1,4 @@
1
- /** A sunken panel and a bare block that takes type and background from its host — `variant`. */
1
+ /** A filled panel and a bare block that takes type and background from its host — `variant`. */
2
2
  import { CodeBlock, Typography } from "prime-ui-kit";
3
3
 
4
4
  const SAMPLE = `const total = formatPrice(14990);`;
@@ -211,7 +211,7 @@ No `variant` / `tone` / `color`.
211
211
  | Value | Looks like | Use when | Default |
212
212
  |---|---|---|---|
213
213
  | `none` | parts stacked with a 12px gap, no own background | inside a Popover or a Card | yes |
214
- | `raised` | `bg-raised`, panel radius and padding, `shadow-overlay`; fields switch to `field-bg-surface` | a standalone panel on the page | |
214
+ | `raised` | the floating layer of the ladder (`data-depth="floating"`), panel radius and padding, `shadow-overlay`; fields take that layer's fill | a standalone panel on the page | |
215
215
 
216
216
  ### defaultFormat (ColorPicker.Root)
217
217
  | Value | Looks like | Use when | Default |
@@ -48,7 +48,7 @@ ColorSwatches field frame: label → swatches → hint | error
48
48
  | `optional` | `boolean` | — | Muted marker right after the label text (`labels.optional`). |
49
49
  | `hint` | `ReactNode` | — | Help text under the swatches. Hidden while `error` is shown. |
50
50
  | `error` | `ReactNode` | — | Error message in the hint slot; implies `invalid`. |
51
- | `invalid` | `boolean` | — | Danger selection ring and `aria-invalid`. A non-empty `error` implies it. |
51
+ | `invalid` | `boolean` | — | Danger inner edge on every swatch and `aria-invalid`. A non-empty `error` implies it. |
52
52
  | `disabled` | `boolean` | `false` | Disables every swatch (grey, half transparent). |
53
53
  | `allowEmpty` | `boolean` | `false` | Adds the «no color» checkerboard swatch after the presets (value `null`). |
54
54
  | `name` | `string` | — | Form field name: a hidden input submits the selected color (empty string for no color). |
@@ -76,7 +76,7 @@ ColorSwatches field frame: label → swatches → hint | error
76
76
  | Value | Looks like | Use when | Default |
77
77
  |---|---|---|---|
78
78
  | `allowEmpty` | an extra checkerboard swatch «Без цвета» at the end | the color is optional | `false` |
79
- | `invalid` | the selection ring turns danger | the message is shown elsewhere; otherwise pass `error` | — |
79
+ | `invalid` | the inner edge of every swatch turns danger | the message is shown elsewhere; otherwise pass `error` | — |
80
80
  | `disabled` | every swatch grey at half opacity | the choice is unavailable | `false` |
81
81
 
82
82
  **Combinations**
@@ -87,17 +87,17 @@ ColorSwatches field frame: label → swatches → hint | error
87
87
  ## States
88
88
  | State | Driven by | DOM |
89
89
  |---|---|---|
90
- | selected | `value` / `defaultValue` | `aria-checked="true"`, `data-state="checked"` on the radio; 2px accent ring with a 2px gap and a check mark (light or dark by color contrast) |
90
+ | selected | `value` / `defaultValue` | `aria-checked="true"`, `data-state="checked"` on the radio; the check mark (light or dark by color contrast) scales in from half size (`base` · emphasized) and leaves faster (`fast` · exit); no ring — the check is the selection |
91
91
  | hover | pointer | the swatch scales to 1.08 (fine pointers only) |
92
92
  | focus-visible | keyboard | focus ring outside the swatch |
93
- | invalid | `invalid` / `error` | `aria-invalid`, `data-invalid` on the group; the selected ring turns `danger-border` |
93
+ | invalid | `invalid` / `error` | `aria-invalid`, `data-invalid` on the group; the inner edge of every swatch turns `danger-border` |
94
94
  | disabled | `disabled` | native `disabled` on every radio, `aria-disabled` and `data-disabled` on the group |
95
95
 
96
96
  Controlled with `value` + `onValueChange`; uncontrolled with `defaultValue`.
97
97
 
98
98
  ## Layout & spacing
99
99
  - The swatches take the container width and wrap to new rows by themselves; no column count to set. In a content-sized container they stay in one row.
100
- - The selection / focus rings sit outside the swatches; kit hosts (Modal / Drawer body, panels, cards) already keep `--prime-focus-space` padding, so the rings are never clipped. In your own `overflow` container, keep that padding.
100
+ - The keyboard focus ring sits outside the swatches; kit hosts (Modal / Drawer body, panels, cards) already keep `--prime-focus-space` padding, so the ring is never clipped. In your own `overflow` container, keep that padding.
101
101
  - Label → swatches and swatches → hint use the tier `label-gap` / `hint-gap`; field → field 20.
102
102
 
103
103
  ## Accessibility
@@ -111,7 +111,7 @@ CommandMenu.Root Modal + top-aligned palette panel, query state
111
111
  | `keys` | `ReactNode[]` | — (required) | FooterHint: the keys (text or icons). |
112
112
 
113
113
  ## Variants
114
- One look: `bg-raised`, `--prime-modal-radius`, `shadow-modal`, `--prime-modal-width-l` wide, placed in the top part of the viewport so the search row never jumps while the list filters.
114
+ One look: the floating layer of the ladder (it is a Modal), `--prime-modal-radius`, `shadow-modal`, `--prime-modal-width-l` wide, placed in the top part of the viewport so the search row never jumps while the list filters.
115
115
 
116
116
  ### size (Root)
117
117
  | Value | Looks like | Use when | Default |
@@ -16,9 +16,9 @@ import styles from "./examples.module.css";
16
16
  type Status = "loading" | "ready" | "empty" | "error";
17
17
 
18
18
  const PAYMENTS = [
19
- { id: "p1", payer: "ООО «Гранит»", amount: "₽ 94 750" },
20
- { id: "p2", payer: "ИП Орлов Д. С.", amount: "₽ 18 000" },
21
- { id: "p3", payer: "АО «Альфа Медиа»", amount: "₽ 380 000" },
19
+ { id: "p1", payer: "ООО «Гранит»", amount: "94 750 ₽" },
20
+ { id: "p2", payer: "ИП Орлов Д. С.", amount: "18 000 ₽" },
21
+ { id: "p3", payer: "АО «Альфа Медиа»", amount: "380 000 ₽" },
22
22
  ];
23
23
 
24
24
  export default function CrossfadeOverviewExample() {
@@ -150,8 +150,8 @@ Heads keep `start` whatever the cell alignment, with the sort icon at the end ed
150
150
  | `rowDividers` | hairlines between rows | default; off together with `striped` | `true` |
151
151
  | `columnDividers` | hairlines between content columns (none inside the lead columns) | default; off for 2–3 column key/value lists | `true` |
152
152
  | `striped` | every second row one opaque step off the fill | long numeric grids | `false` |
153
- | `highlightRowOnHover` | `fill-subtle` wash on the hovered row | interactive lists | `true` |
154
- | `highlightColumnOnHover` | `fill-subtle` wash on the hovered column | comparison tables | `false` |
153
+ | `highlightRowOnHover` | `fill-subtle-active` wash on the hovered row (stronger than a zebra step, so a hovered row never takes a zebra row's color) | interactive lists | `true` |
154
+ | `highlightColumnOnHover` | `fill-subtle-active` wash on the hovered column | comparison tables | `false` |
155
155
  | `stickyHeader` · `stickyFirstColumn` | opaque sticky head / first column with a hairline edge; on screens lower than 480px the head scrolls with the rows | long / wide tables in a `scrollHeight` window | `false` |
156
156
  | column `hideable` | `false`: the column stays even when listed in `hiddenColumns` | the key column that names the row | `true` |
157
157
  | `showHeader` | head row shown | off only for self-explanatory key/value tables | `true` |
@@ -169,7 +169,8 @@ Heads keep `start` whatever the cell alignment, with the sort icon at the end ed
169
169
  | sorted | `sort` / `defaultSort` | `aria-sort` and `data-sorted` on the head; the sort icon: unsorted `text-disabled`, hover `text-muted`, sorted `text-secondary` — never accent |
170
170
  | selected | `selected` / `defaultSelected` | `aria-selected` rows with `accent-soft` fill; polite `labels.selectedCount` |
171
171
  | expanded | `expanded` / `defaultExpanded` | `data-expanded` on the parent row (one step darker); toggle `aria-expanded`, chevron turns 90° |
172
- | new rows | rows mounted by an expand or new in `rows` (by `getRowKey`) | `data-animate`: cells drop in from above (`enterMotion`); detail panels open and close through `grid-template-rows` (`data-state` on the detail row; it unmounts after the close) |
172
+ | new rows | rows new in `rows` (by `getRowKey`) | `data-animate`: top-level cells drop in from above (`enterMotion`) |
173
+ | sub-rows and detail panels | an expand / a collapse | both fold open and shut through `grid-template-rows` (0fr ↔ 1fr) with the fade on the same `base` curve; the row height and padding live inside the track, so nothing is left to jump. Sub-rows: `data-fold` on the row (`opening` · `open` · `closed`); a collapsed row's sub-rows stay mounted until the fold ends. Detail row: `data-state`; it unmounts after the close |
173
174
  | overflowing sideways | the table is wider than its viewport | `data-overflow-start` (scrolled from the start) and `data-overflow-end` (more to the end) on the root: a soft shadow over that edge, from the end edge of the sticky first column for the start; fades in and out over `fast` |
174
175
  | hidden columns | `hiddenColumns` | the listed columns (except `hideable: false`) are not rendered in the head, rows, skeleton or state rows; widths are measured again |
175
176
  | state swap | the body moves between loading, rows, empty and error | the body fades in over `base` (opacity only, the same swap motion as [Crossfade](../crossfade/COMPONENT.md)); the head stays still; nothing moves on the first render |
@@ -177,7 +178,7 @@ Heads keep `start` whatever the cell alignment, with the sort icon at the end ed
177
178
  Root attributes: `data-size`, `data-row-dividers`, `data-column-dividers`, `data-sticky-header`, `data-sticky-first-column`, `data-table-width` (`fill` · `auto` · `grow`), `data-highlight-row`, `data-highlight-column`, `data-striped`, `data-loading`, `data-selectable`, `data-expandable`, `data-dragging` (while drag-selecting), `data-overflow-start` · `data-overflow-end` (edges that hide columns). Sorting and paging swap rows instantly; everything is still under `prefers-reduced-motion`.
178
179
 
179
180
  ## Layout & spacing
180
- - The root fills its container on `--prime-color-card-bg`, radius 12, and clips full-bleed head and rows; on a card or a modal it becomes a sunken block — never wrap it in a Card.
181
+ - The root is a layer of the surface ladder (`data-depth`, one above its host): it fills its container on `--prime-color-layer-current`, radius 12, and clips full-bleed head and rows; zebra rows and the sticky column are one ladder step off it, the head two. On a card or in a modal it is the next layer by itself — never wrap it in a Card.
181
182
  - Toolbar padding `--prime-space-3` × cell padding, items `gap: --prime-space-2`; below 30rem of table width toolbar and footer stack.
182
183
  - The footer has a hairline on top: range on the left, Pagination on the right. The range shows only with more than one page or with infinite scroll.
183
184
  - Columns scroll sideways inside the table, never the page; pin the identifying column with `stickyFirstColumn`. Works from 320 px.
@@ -44,6 +44,7 @@ DigitInput field frame: label row → cells → support row (hint | e
44
44
  | `hint` | `ReactNode` | — | Help text under the cells. Hidden while `error` is shown. |
45
45
  | `error` | `ReactNode` | — | Error message in the hint slot; implies `invalid`. |
46
46
  | `invalid` | `boolean` | — | Danger ring and danger digits on every cell, `aria-invalid`. A non-empty `error` implies it. |
47
+ | `success` | `boolean` | `false` | The code was accepted (after the server checked it): the cells turn success one after another. Ignored while invalid. Say it in words too (`hint`). |
47
48
  | `disabled` | `boolean` | — | Disables the fieldset and every cell. |
48
49
  | `value` | `string` | — | Controlled code; non-digits are dropped, extra digits cut to `length`. |
49
50
  | `defaultValue` | `string` | `""` | Initial code when uncontrolled. |
@@ -92,8 +93,9 @@ No `variant` or `tone`.
92
93
  | State | Driven by | DOM |
93
94
  |---|---|---|
94
95
  | hover | — | cell fill darkens (not on focused or disabled cells) |
95
- | focus | keyboard / click | focused cell: focus fill + inset focus ring; its content is selected so a new digit replaces it |
96
- | filled cell | a digit in the cell | `data-filled="true"` on the cell |
96
+ | focus | keyboard / click | focused cell: focus fill and its content selected so a new digit replaces it; one inset ring for the row appears on the cell and glides to the next one (`fast` · emphasized) instead of each cell drawing its own |
97
+ | filled cell | a digit in the cell | `data-filled="true"` on the cell; the digit fades in as it lands |
98
+ | success | `success` (not while invalid) | `data-success="true"` on the fieldset; the cells turn `success-soft` one after another, half a stagger step apart |
97
99
  | full width | `fullWidth` | `data-full-width="true"` on the fieldset |
98
100
  | group start | `groupSize` | `data-group-start="true"` on the first cell of every group |
99
101
  | invalid | `invalid` or a non-empty `error` | `data-invalid="true"` on the fieldset and the frame, `aria-invalid="true"` on cells |
@@ -142,7 +144,7 @@ No `variant` or `tone`.
142
144
  | [validation.tsx](examples/validation.tsx) | Required and optional markers, a hint and an error that replaces it — `required`, `optional`, `hint`, `error`. |
143
145
  | [grouped.tsx](examples/grouped.tsx) | A long code read in chunks with a wider gap between groups — `groupSize`. |
144
146
  | [full-width.tsx](examples/full-width.tsx) | Cells that share the container width and keep the tier height, above a full-width button — `fullWidth`. |
145
- | [on-complete.tsx](examples/on-complete.tsx) | The code is checked as soon as the last cell is filled; a wrong code turns the hint into an error — `onComplete`, `error`. |
147
+ | [on-complete.tsx](examples/on-complete.tsx) | The code is checked as soon as the last cell is filled: a right one turns the cells success, a wrong one turns the hint into an error — `onComplete`, `success`, `error`. |
146
148
  | [controlled.tsx](examples/controlled.tsx) | The parent owns the code and clears it with a button — `value`, `onValueChange`. |
147
149
  | [in-form.tsx](examples/in-form.tsx) | A masked card PIN submitted with a form; a short PIN shows an error — `name`, `mask`, `required`, `error`. |
148
150
 
@@ -1,20 +1,21 @@
1
- /** The code is checked as soon as the last cell is filled; a wrong code turns the hint into an error — `onComplete`, `error`. */
1
+ /** The code is checked as soon as the last cell is filled: a right one turns the cells success, a wrong one turns the hint into an error — `onComplete`, `success`, `error`. */
2
2
  import { DigitInput } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
5
5
  const VALID_CODE = "123456";
6
6
 
7
7
  export default function DigitInputOnCompleteExample() {
8
- const [error, setError] = React.useState<string>();
8
+ const [result, setResult] = React.useState<"accepted" | "rejected">();
9
9
 
10
10
  return (
11
11
  <DigitInput
12
12
  label="Код подтверждения входа"
13
13
  length={6}
14
- hint={`Для проверки подходит ${VALID_CODE}`}
15
- error={error}
16
- onValueChange={() => setError(undefined)}
17
- onComplete={(code) => setError(code === VALID_CODE ? undefined : "Неверный код")}
14
+ hint={result === "accepted" ? "Код подтверждён" : `Для проверки подходит ${VALID_CODE}`}
15
+ error={result === "rejected" ? "Неверный код" : undefined}
16
+ success={result === "accepted"}
17
+ onValueChange={() => setResult(undefined)}
18
+ onComplete={(code) => setResult(code === VALID_CODE ? "accepted" : "rejected")}
18
19
  />
19
20
  );
20
21
  }
@@ -134,7 +134,7 @@ Generic `Dnd.DropZone<TData>`; `ref` → `HTMLElement`. Renders the `as` element
134
134
  | carried | a drag | the clone follows the pointer, lifted on `--prime-shadow-modal`, scale 1.02; a sortable source leaves the layout (`data-lifted`) and a dashed accent gap of its size and radius fades in and glides with the items as the landing spot moves; entering a connected list, it fades out in the list it left and in where it opens; crossing empty space (the gutter between lists) leaves the gap in the last list that takes the item; neighbours glide aside; a `Dnd.Draggable` stays at 0.4 opacity (`data-dragging`) |
135
135
  | over a zone | pointer over an accepting zone | `data-dnd-over`: `accent-soft` fill, dashed accent outline |
136
136
  | refused | `canDrop` returns `false` | `data-dnd-reject`: `danger-soft` fill; the clone gets a dashed `danger` outline |
137
- | dropped | release over a target, or over empty space while a list holds the gap | the clone glides to the item's new place and is swapped for the real element; `data-dnd-flash` with `flashOnDrop` |
137
+ | dropped | release over a target, or over empty space while a list holds the gap | the clone glides to the item's new place and is swapped for the real element; `data-dnd-flash` with `flashOnDrop`; the clone sets down into the look the item will have in place — its resting shadow, or under the mouse its hover look: the item gets `data-dnd-hover` until the pointer leaves it or presses, so a host styles its hover lift on `:is(:hover, [data-dnd-hover])` and the card never dips to flat and lifts again |
138
138
  | cancelled | Escape, a system-cancelled pointer | the clone flies back, nothing changes |
139
139
  | disabled | `disabled` | `data-disabled`, no `aria-roledescription` |
140
140
 
@@ -14,8 +14,8 @@
14
14
  min-width: 0;
15
15
  padding: var(--prime-space-3) var(--prime-space-4);
16
16
  border-radius: var(--prime-radius-m);
17
- background: var(--prime-color-card-bg);
18
- box-shadow: var(--prime-card-shadow);
17
+ background: var(--prime-color-layer-nested);
18
+ box-shadow: var(--prime-shadow-raised);
19
19
  }
20
20
 
21
21
  /* The grip sits closer to the edge: it is a square button with its own inner room. */
@@ -45,7 +45,7 @@
45
45
  min-height: calc(var(--prime-space-24) * 2);
46
46
  padding: var(--prime-space-3);
47
47
  border-radius: var(--prime-radius-l);
48
- background: var(--prime-color-bg-sunken);
48
+ background: var(--prime-color-fill-muted);
49
49
  }
50
50
 
51
51
  .columnHeader {
@@ -61,7 +61,7 @@
61
61
  gap: var(--prime-space-1);
62
62
  padding: var(--prime-space-3) var(--prime-space-4);
63
63
  border-radius: var(--prime-radius-m);
64
- background: var(--prime-color-bg-surface);
64
+ background: var(--prime-color-layer-nested);
65
65
  box-shadow: var(--prime-shadow-raised);
66
66
  }
67
67
 
@@ -115,7 +115,7 @@ No DOM: clones the single child, merges `ref` and `onClick` (toggles), sets `ari
115
115
  `ref` → `HTMLDivElement`. A non-interactive row at the top (who is signed in, the plan): an avatar, Title + Description stacked in one column, a trailing badge or button — in the written order / the medium heading line / the muted line under it; both truncate. + native `<div>` props.
116
116
 
117
117
  ## Variants
118
- The panel is the shared floating surface: `bg-raised`, `--prime-panel-radius` (12), padding 4, `shadow-overlay`; rows have radius 8 and the tier item height.
118
+ The panel is the shared floating surface: the floating layer of the ladder (`data-depth="floating"`), `--prime-panel-radius` (12), padding 4, `shadow-overlay`; rows have radius 8 and the tier item height.
119
119
 
120
120
  ### size (Content)
121
121
  | Value | Looks like | Use when | Default |
@@ -18,5 +18,5 @@
18
18
  padding-block: var(--prime-space-1);
19
19
  padding-inline: var(--prime-space-4) var(--prime-space-1);
20
20
  border-radius: var(--prime-radius-l);
21
- background: var(--prime-color-card-bg);
21
+ background: var(--prime-color-layer-nested);
22
22
  }
@@ -27,7 +27,7 @@
27
27
  width: min(100%, var(--prime-panel-min-width));
28
28
  padding: var(--prime-panel-padding);
29
29
  border-radius: var(--prime-panel-radius);
30
- background: var(--prime-color-bg-raised);
30
+ background: var(--prime-color-layer-floating-bg);
31
31
  box-shadow: var(--prime-shadow-overlay);
32
32
  }
33
33
 
@@ -74,7 +74,7 @@ ExampleFrame frame: toolbar + preview stage or code pane
74
74
  | `light` | stage and code pane in the light theme | default | yes |
75
75
  | `dark` | stage and code pane in the dark theme (`data-theme="dark"`), switched without color transitions | checking dark mode | |
76
76
 
77
- Frame look: `bg-sunken` chrome with a `border-subtle` hairline and `radius-xl`; the stage and code pane are `bg-canvas` wells with `radius-l` (16 − 4 padding = 12). The stage resets the card context (`--prime-color-card-bg: bg-surface`).
77
+ Frame look: the chrome is a layer of the surface ladder (`data-depth`, one above its host) with a `border-subtle` hairline and `radius-xl`; the stage and code pane are wells with `radius-l` (16 − 4 padding = 12). Both wells carry `data-theme`, so each is a page of its own: layer 0 of its scheme, and surfaces in the example count from there.
78
78
 
79
79
  ## States
80
80
  | State | Driven by | DOM |
@@ -57,6 +57,8 @@ The trailing side has a fixed CSS `order`, independent of JSX order: value · en
57
57
  | `counter` | `ReactNode` | — | Right side of the support row, usually `<Input.Counter />`. |
58
58
  | `reserveSupportRow` | `boolean` | `false` | Always render the support row (min height = hint line height), so an appearing error does not shift the layout. |
59
59
  | `id` | `string` | — | Id of the `<input>` (auto-generated when omitted); hint id is `<id>-hint`, error id is `<id>-error`. |
60
+ | `strength` | `boolean` | `false` | A password strength meter: a stepped `ProgressBar` (4 steps) under the field and the level word at the end of the support row — weak, easy, medium, hard (`labels.strengthWeak` … `strengthHard`), toned danger → warning → accent → success. Follows the value of `Input.Field`, controlled or not. For new-password fields. |
61
+ | `getStrength` | `(value: string) => 0 \| 1 \| 2 \| 3 \| 4` | `getPasswordStrength` | Replaces the kit's estimate: every trait adds a point on its own, in any order — lowercase, uppercase, digits, symbols, 10+ and 14+ characters; 0–2 points weak, 3 easy, 4 medium, 5+ hard, and under 8 characters at most easy. Return 0 for empty, 1 weak … 4 hard. A hint for the person typing, not a security check. |
60
62
  | `labels` | `Partial<InputLabels>` | — | Built-in strings, see Labels. |
61
63
  | `children` | `ReactNode` | — (required) | Usually `Input.Wrapper`. |
62
64
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "id" \| "children" \| "defaultValue" \| "defaultChecked" \| "onChange">` | — | `className`, `data-*` and the other attributes of the field frame `<div>` (field-root rule: `className`, `ref` and the rest → frame, `id` → control). |
@@ -168,7 +170,8 @@ Input has no `variant` or `tone`: there is one field treatment (fill, no visible
168
170
  | disabled | native `disabled` on `Input.Field` | disabled fill, disabled text and icons, `cursor: not-allowed`, clear button disabled look |
169
171
  | read-only | native `readOnly` on `Input.Field` | no hover, default cursor |
170
172
  | reserved support row | `reserveSupportRow` | `data-reserve="true"` on the support row |
171
- | counter over limit | `current > max` | `data-invalid="true"` on `Input.Counter` |
173
+ | counter over limit | `current > max` | `data-invalid="true"` on `Input.Counter`; the current count rolls its digits to each new value |
174
+ | strength | `strength` + the value of `Input.Field` | a stepped `ProgressBar` (4 cells, `aria-label` = `labels.strength`) under the field fills one cell after another, weak · easy · medium · hard, toned danger → warning → accent → success; the level word at the end of the support row flows in letter by letter and is announced (`aria-live`); the support row is reserved |
172
175
 
173
176
  Size: `data-size` on root and wrapper. Slots: `data-side="start" | "end"`.
174
177
 
@@ -204,6 +207,11 @@ Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontro
204
207
  | `optional` | `"необязательно"` | Marker after the label when `optional`. |
205
208
  | `clear` | `"Очистить"` | Accessible name of `Input.ClearButton`. |
206
209
  | `counter` | `"{current} из {max} символов"` | Screen-reader text of `Input.Counter`; `{current}` and `{max}` are replaced. |
210
+ | `strength` | `"Надёжность пароля"` | Accessible name of the `strength` meter and the spoken prefix of the level word. |
211
+ | `strengthWeak` | `"Слабый"` | Level word for strength 1. |
212
+ | `strengthEasy` | `"Лёгкий"` | Level word for strength 2. |
213
+ | `strengthMedium` | `"Средний"` | Level word for strength 3. |
214
+ | `strengthHard` | `"Сложный"` | Level word for strength 4. |
207
215
 
208
216
  ## Examples
209
217
  | Example | Shows |
@@ -215,6 +223,7 @@ Controlled: `value` + `onChange` (or `onValueChange`) on `Input.Field`. Uncontro
215
223
  | [with-icon.tsx](examples/with-icon.tsx) | A decorative icon at either end of the value — `Input.Icon`, `side`. |
216
224
  | [affixes.tsx](examples/affixes.tsx) | A fixed prefix and suffix flush with the edges and a unit next to the value — `Input.Affix`, `Input.InlineAffix`. |
217
225
  | [without-focus-ring.tsx](examples/without-focus-ring.tsx) | A single search field where the caret and the lighter fill show focus — `focusRing`. |
226
+ | [password-strength.tsx](examples/password-strength.tsx) | A new password with a meter that fills step by step and names the level beside the hint — `strength`. |
218
227
  | [controlled.tsx](examples/controlled.tsx) | The parent owns the value: a clear button and a character counter follow it — `value`, `onValueChange`, `Input.ClearButton`, `Input.Counter`. |
219
228
  | [in-form.tsx](examples/in-form.tsx) | Company details form: required fields checked on submit, neighbours keep their bottoms aligned — `required`, `error`, `reserveSupportRow`. |
220
229
 
@@ -0,0 +1,12 @@
1
+ /** A new password with a meter that fills step by step and names the level beside the hint — `strength`. */
2
+ import { Input } from "prime-ui-kit";
3
+
4
+ export default function InputPasswordStrengthExample() {
5
+ return (
6
+ <Input.Root label="Новый пароль" hint="Не короче 8 символов" strength>
7
+ <Input.Wrapper>
8
+ <Input.Field type="password" autoComplete="new-password" defaultValue="Graphite2026" />
9
+ </Input.Wrapper>
10
+ </Input.Root>
11
+ );
12
+ }
@@ -24,7 +24,7 @@ import { Kanban, type KanbanColumn, type KanbanMove, type KanbanValue } from "pr
24
24
  ## Anatomy
25
25
  ```
26
26
  Kanban.Root board: horizontal strip of columns, its own Dnd.Root if none above
27
- └─ column (from `columns`) `fill-faint` well; header + own vertical scroll
27
+ └─ column (from `columns`) `fill-muted` well (one ladder step off the board's surface); header + own vertical scroll
28
28
  ├─ header title (h3) · count Badge · renderColumnActions(column)
29
29
  └─ <ul> (Dnd.Sortable) drop area; Skeleton cards while loading; EmptyPage when empty
30
30
  └─ Kanban.Item <li> card from renderItem(item)
@@ -61,7 +61,7 @@ Generic `Kanban.Root<T>`; `ref` → `HTMLDivElement`. The board: a strip of colu
61
61
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children" \| "defaultValue">` | — | `className` (the board height), `aria-label` and the other div attributes. |
62
62
 
63
63
  ### Kanban.Item
64
- `ref` → `HTMLLIElement`. One card: an `<li>` on the raised fill (`bg-raised`) with the card shadow; return it from `renderItem` (it takes its id from there). Draggable by the whole card, focusable, `aria-keyshortcuts` for Alt + arrows. Presses on buttons, fields and links inside never start a drag.
64
+ `ref` → `HTMLLIElement`. One card: an `<li>` two ladder steps lighter than its column (`control-selected`: white in light) with the raised shadow, lifted to the overlay shadow on hover; return it from `renderItem` (it takes its id from there). Draggable by the whole card, focusable, `aria-keyshortcuts` for Alt + arrows. Presses on buttons, fields and links inside never start a drag.
65
65
 
66
66
  | Prop | Type | Default | Description |
67
67
  |---|---|---|---|
@@ -111,7 +111,7 @@ The board has no visual variants: one look, fills over lines.
111
111
  ## States
112
112
  | State | Driven by | DOM |
113
113
  |---|---|---|
114
- | idle | — | cards show `cursor: grab`; under a mouse the card gets a `fill-subtle` wash (`@media (hover: hover)` only) |
114
+ | idle | — | cards show `cursor: grab`; under a mouse the card lifts to `--prime-shadow-overlay` (`@media (hover: hover)` only) — no wash: a wash is one ladder step, the column's own color |
115
115
  | carried | a drag | Dnd: the clone lifts on `--prime-shadow-modal`, a dashed accent gap opens where the card lands, cards glide aside |
116
116
  | over a column | pointer over another column | the gap opens there; an empty column's well fades out |
117
117
  | refused | `limit` reached, `canDrop` returns `false` | `data-dnd-reject` on the `<ul>`: `danger-soft` fill |
@@ -120,7 +120,7 @@ The board has no visual variants: one look, fills over lines.
120
120
  | loading | `loading` | `aria-busy` and `data-loading` on the root; `Skeleton` cards; `Crossfade` swaps them for the cards |
121
121
  | disabled | `disabled` / `Kanban.Item disabled` | `data-disabled` on the root / card; no `aria-roledescription`, no shortcuts |
122
122
 
123
- Motion: the hover wash and the empty well fade on opacity (`fast`, `standard`); the swap from skeletons is `Crossfade`; drag motion is Dnd's (lift, gap, glide, landing). Under `prefers-reduced-motion` all of it is off.
123
+ Motion: the hover lift (shadow, the card rises above its neighbours) and the empty well fade (`fast`, `standard`); a card dropped under the mouse lands straight into its hover lift (`data-dnd-hover`); the swap from skeletons is `Crossfade`; drag motion is Dnd's (lift, gap, glide, landing). Under `prefers-reduced-motion` all of it is off.
124
124
 
125
125
  ## Layout & spacing
126
126
  - Give the root a height (`className`): columns span it and every column body scrolls on its own; without one the columns take the height of the tallest.
@@ -119,8 +119,8 @@ Root carries `data-size`, `data-align` and `data-flat`.
119
119
  ## Layout & spacing
120
120
  - header → body `--prime-space-8` (gap on Root); provider buttons → divider → form `--prime-space-4`; field → field → submit `--prime-space-5`; submit → back action `--prime-space-3`; form → footer `--prime-space-6`.
121
121
  - Spacing is `gap` on parents; no margins between parts.
122
- - The card has no border and is capped at the small modal width. To center it on a page, wrap it in your own flex container on the canvas.
123
- - Fields inside take `--prime-color-field-bg-surface`, like in Card.
122
+ - The card has no border and is capped at the small modal width. To center it on a page, wrap it in your own flex container on the page.
123
+ - The form is a layer of the surface ladder like Card (`data-depth`): on the page it is white in light with the raised whisper, fields inside are one step off it.
124
124
 
125
125
  ## Accessibility
126
126
 
@@ -144,9 +144,9 @@ No `labels`.
144
144
  | [overview.tsx](examples/overview.tsx) | Sign in with a provider button, e-mail and password, a «forgot password» link and a sign-up link under a centered header — `align`, `LoginForm.Actions`, `LoginForm.Form`, `LoginForm.Footer`. |
145
145
  | [sizes.tsx](examples/sizes.tsx) | Padding, gaps and text roles of the card follow the tier; the fields and buttons take the same one — `size`. |
146
146
  | [structure.tsx](examples/structure.tsx) | The minimal form: no logo, description or footer, and no shadow for a host that already is a surface — `flat`. |
147
- | [register.tsx](examples/register.tsx) | Sign-up: five fields in one column, a password mismatch shown as the field error — `error`, `LoginForm.Form`. |
147
+ | [register.tsx](examples/register.tsx) | Sign-up: five fields in one column, a strength meter on the new password and a mismatch shown as the field error — `strength`, `error`, `LoginForm.Form`. |
148
148
  | [forgot-password.tsx](examples/forgot-password.tsx) | Password reset request: one field, the primary action and a quiet way back — `LoginForm.Actions`. |
149
- | [reset-password.tsx](examples/reset-password.tsx) | A new password after the e-mail link: two fields and a mismatch error under the second one — `error`. |
149
+ | [reset-password.tsx](examples/reset-password.tsx) | A new password after the e-mail link: a strength meter on the first field and a mismatch error under the second one — `strength`, `error`. |
150
150
  | [verification-code.tsx](examples/verification-code.tsx) | The second step with a one-time code: a rejected code shows its error under the cells, resend is a quiet action — `DigitInput`, `LoginForm.Actions`. |
151
151
  | [states.tsx](examples/states.tsx) | The submit cycle: the button is busy while the request runs, a failed request shows a danger Banner and marks the password — `loading`, `invalid`. |
152
152
  | [narrow.tsx](examples/narrow.tsx) | On a phone-width screen the card keeps its padding, the header wraps and the buttons stay full width. |