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
@@ -1,4 +1,4 @@
1
- /** Sign-up: five fields in one column, a password mismatch shown as the field error — `error`, `LoginForm.Form`. */
1
+ /** 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`. */
2
2
  import { Button, Divider, Icon, Input, LinkButton, LoginForm } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
@@ -45,7 +45,7 @@ export default function LoginFormRegisterExample() {
45
45
  <Input.Field placeholder="@login" />
46
46
  </Input.Wrapper>
47
47
  </Input.Root>
48
- <Input.Root label="Пароль" required hint="Минимум 6 символов">
48
+ <Input.Root label="Пароль" required hint="Минимум 8 символов" strength>
49
49
  <Input.Wrapper>
50
50
  <Input.Field
51
51
  type="password"
@@ -1,4 +1,4 @@
1
- /** A new password after the e-mail link: two fields and a mismatch error under the second one — `error`. */
1
+ /** 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`. */
2
2
  import { Button, Icon, Input, LoginForm } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
@@ -23,7 +23,7 @@ export default function LoginFormResetPasswordExample() {
23
23
  </LoginForm.Header>
24
24
  <LoginForm.Body>
25
25
  <LoginForm.Form onSubmit={(e) => e.preventDefault()}>
26
- <Input.Root label="Новый пароль" required hint="Минимум 6 символов">
26
+ <Input.Root label="Новый пароль" required hint="Минимум 8 символов" strength>
27
27
  <Input.Wrapper>
28
28
  <Input.Field
29
29
  type="password"
@@ -107,7 +107,7 @@ No DOM: clone the single child and chain its `onClick` (unless the child's handl
107
107
  | `children` | `ReactElement` | — (required) | One element, usually a Button. |
108
108
 
109
109
  ## Variants
110
- The dialog has one look: `bg-raised`, `--prime-modal-radius` (16), `shadow-modal`, on a `bg-scrim`. Header, body and footer are separated by faint inset hairlines.
110
+ The dialog has one look: the floating layer of the ladder (`data-depth="floating"`: white in light, a step above a card in dark), `--prime-modal-radius` (16), `shadow-modal`, on a `bg-scrim`. Header, body and footer are separated by faint inset hairlines.
111
111
 
112
112
  ### size (Content)
113
113
  | Value | Looks like | Use when | Default |
@@ -165,7 +165,7 @@ Loading or disabled actions use the Button props (`loading`, `disabled`) inside
165
165
  ## Layout & spacing
166
166
  - Dialog padding `--prime-modal-padding` (24) on every zone; header 24 top / 20 bottom, body 20 with a 16 gap between blocks, footer 20 top / 24 bottom.
167
167
  - Max height = viewport minus `--prime-modal-viewport-padding` on both sides; only the body scrolls.
168
- - Fields inside get the surface field fill; cards inside become sunken tiles. Group fields with a 20 gap.
168
+ - Fields inside take the floating layer's fill; a card inside is the next layer (`floating-1`), a flat tile. Group fields with a 20 gap.
169
169
  - Narrow viewport (< 640px): bottom sheet, full width, top corners `--prime-sheet-radius`, a grab handle strip on top, up to `100dvh − --prime-sheet-top-gap` high, the bottom safe-area inset below the content.
170
170
 
171
171
  ## Accessibility
@@ -16,6 +16,6 @@
16
16
  width: 100%;
17
17
  padding: var(--prime-space-4);
18
18
  border-radius: var(--prime-card-radius);
19
- background: var(--prime-color-card-bg);
20
- box-shadow: var(--prime-card-shadow);
19
+ background: var(--prime-color-layer-nested);
20
+ box-shadow: var(--prime-shadow-raised);
21
21
  }
@@ -5,7 +5,7 @@
5
5
  width: 100%;
6
6
  padding: var(--prime-layout-gutter-m);
7
7
  border-radius: var(--prime-card-radius);
8
- background: var(--prime-color-bg-canvas);
8
+ background: var(--prime-color-layer-current);
9
9
  container-type: inline-size;
10
10
  }
11
11
 
@@ -77,7 +77,7 @@ No DOM: clone the single child and merge `ref` and `onClick`. Trigger toggles th
77
77
  `ref` → the element. `<div>` (title + description with a 4 px step) / `<h2>` (names the dialog) / `<p>` (describes it) / `<div>` (buttons at the end, stacked full width on phones, below 640 px). + native props.
78
78
 
79
79
  ## Variants
80
- The panel has one look: `bg-raised` fill, `--prime-panel-radius`, `shadow-overlay`, no border. Fields inside get the surface field fill; cards inside become sunken tiles.
80
+ The panel has one look: the floating layer of the ladder (`data-depth="floating"`), `--prime-panel-radius`, `shadow-overlay`, no border. Fields inside take that layer's fill; a card inside is the next layer, a flat tile.
81
81
 
82
82
  ### size (Content)
83
83
  | Value | Looks like | Use when | Default |
@@ -51,6 +51,7 @@ A native `<progress>` (transparent, for assistive tech) under the drawn line.
51
51
  | `value` | `number` | — (required) | The current value, clamped to `0…max`. |
52
52
  | `max` | `number` | `100` | Top of the scale. |
53
53
  | `tone` | `"neutral" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"accent"` | Fill color, telling the outcome. |
54
+ | `steps` | `boolean` | `false` | Draws the scale as `max` equal cells (2–12), each filled whole; `value` rounds to a cell. In one change the cells go one after another — from the start when filling, from the end when emptying. For a level out of a few: password strength, onboarding steps. |
54
55
 
55
56
  ### ProgressBar · segments mode
56
57
  A `role="group"` bar of parts, named by `label` and described by the distribution text.
@@ -100,8 +101,9 @@ A `role="group"` bar of parts, named by `label` and described by the distributio
100
101
  | tone / size | props | `data-tone` (value mode), `data-size` on the root |
101
102
  | value | `value`, `max` | native `value` / `max`; the drawn pill slides by `translate` over `slow` + `standard`, RTL aware |
102
103
  | segments | `segments`, `max`, `segmentGap` | `data-segment-gap` on the group, `data-tone` per part; widths animate by `flex-grow` over `slow` |
104
+ | steps | `steps` (value mode) | `data-steps="true"` on the bar; `max` cells (2–12), filled ones `data-filled`; a cell fills from its start edge (`base` · emphasized), cells of one change go half a stagger step apart — from the start when filling, from the end when emptying |
103
105
  | empty segments | `segments` empty or all `0` | only the track; accessible text `labels.empty` / `labels.allEmpty` |
104
- | reduced motion | `prefers-reduced-motion` | fill and widths change instantly |
106
+ | reduced motion | `prefers-reduced-motion` | fill, widths and cells change instantly |
105
107
 
106
108
  Always controlled: pass the current `value` or `segments` on every render.
107
109
 
@@ -137,6 +139,8 @@ No keyboard interaction.
137
139
  | [sizes.tsx](examples/sizes.tsx) | Every tier: the line grows from 4 to 8 px like the Slider track — `size`. |
138
140
  | [custom-max.tsx](examples/custom-max.tsx) | A scale of steps instead of percent: 3 of 5 profile steps — `max`. |
139
141
  | [segments.tsx](examples/segments.tsx) | Parts of a whole in one bar: joined or separate pills, free capacity up to `max` and an empty list — `segments`, `segmentGap`, `max`. |
142
+ | [steps.tsx](examples/steps.tsx) | The scale as whole cells that fill one after another: 2 of 5 onboarding steps, forward and back — `steps`, `max`. |
143
+ | [milestone.tsx](examples/milestone.tsx) | A rare milestone: the last invoice of the quarter fills the bar, the tone turns success and a short confetti burst marks it — `value`, `tone`, `celebrate()`. |
140
144
 
141
145
  ## Mistakes
142
146
  - No `label` and no `aria-label` → the bar has no accessible name.
@@ -8,3 +8,10 @@
8
8
  width: min(100%, var(--prime-modal-width-s));
9
9
  min-width: 0;
10
10
  }
11
+
12
+ /* Buttons under a bar that drive it. */
13
+ .actions {
14
+ display: flex;
15
+ flex-wrap: wrap;
16
+ gap: var(--prime-space-2);
17
+ }
@@ -0,0 +1,43 @@
1
+ /** A rare milestone: the last invoice of the quarter fills the bar, the tone turns success and a short confetti burst marks it — `value`, `tone`, `celebrate()`. */
2
+ import { Button, celebrate, ProgressBar } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const INVOICES = 12;
8
+
9
+ export default function ProgressBarMilestoneExample() {
10
+ const [paid, setPaid] = React.useState(INVOICES - 1);
11
+ const closed = paid === INVOICES;
12
+
13
+ return (
14
+ <div className={styles.column}>
15
+ <ProgressBar
16
+ value={paid}
17
+ max={INVOICES}
18
+ tone={closed ? "success" : "accent"}
19
+ label={
20
+ closed
21
+ ? "III квартал закрыт: все счета оплачены"
22
+ : `Оплачено счетов: ${paid} из ${INVOICES}`
23
+ }
24
+ />
25
+ <div className={styles.actions}>
26
+ <Button.Root
27
+ variant={closed ? "soft" : "solid"}
28
+ tone={closed ? "neutral" : "accent"}
29
+ onClick={(event) => {
30
+ if (closed) {
31
+ setPaid(INVOICES - 1);
32
+ return;
33
+ }
34
+ setPaid(INVOICES);
35
+ celebrate({ origin: event.currentTarget });
36
+ }}
37
+ >
38
+ {closed ? "Вернуть последний счёт" : "Отметить оплату ООО «Вектор»"}
39
+ </Button.Root>
40
+ </div>
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,35 @@
1
+ /** The scale as whole cells that fill one after another: 2 of 5 onboarding steps, forward and back — `steps`, `max`. */
2
+ import { Button, ProgressBar } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const TOTAL = 5;
8
+
9
+ export default function ProgressBarStepsExample() {
10
+ const [done, setDone] = React.useState(2);
11
+
12
+ return (
13
+ <div className={styles.column}>
14
+ <ProgressBar
15
+ steps
16
+ value={done}
17
+ max={TOTAL}
18
+ label={`Настройка компании: ${done} из ${TOTAL}`}
19
+ />
20
+ <div className={styles.actions}>
21
+ <Button.Root
22
+ variant="soft"
23
+ tone="neutral"
24
+ disabled={done === 0}
25
+ onClick={() => setDone(done - 1)}
26
+ >
27
+ Назад
28
+ </Button.Root>
29
+ <Button.Root disabled={done === TOTAL} onClick={() => setDone(done + 1)}>
30
+ Следующий шаг
31
+ </Button.Root>
32
+ </div>
33
+ </div>
34
+ );
35
+ }
@@ -5,9 +5,9 @@ import * as React from "react";
5
5
  import styles from "./examples.module.css";
6
6
 
7
7
  const PAYMENTS = [
8
- { id: "p1", payer: "ООО «Гранит»", amount: "₽ 94 750" },
9
- { id: "p2", payer: "ИП Орлов Д. С.", amount: "₽ 18 000" },
10
- { id: "p3", payer: "АО «Альфа Медиа»", amount: "₽ 380 000" },
8
+ { id: "p1", payer: "ООО «Гранит»", amount: "94 750 ₽" },
9
+ { id: "p2", payer: "ИП Орлов Д. С.", amount: "18 000 ₽" },
10
+ { id: "p3", payer: "АО «Альфа Медиа»", amount: "380 000 ₽" },
11
11
  ];
12
12
 
13
13
  const LOAD_MS = 1500;
@@ -0,0 +1,98 @@
1
+ # Sparkline
2
+
3
+ **Category:** data-display
4
+ **Kind:** primitive
5
+
6
+ > A small line chart with its headline — the latest value, its change against the point before and its date — where any point can be scrubbed.
7
+
8
+ ## When to use
9
+ - A metric card on a dashboard: revenue, orders, sign-ups over the last days or weeks.
10
+ - A trend next to a number where the shape matters more than exact axes.
11
+ - A detail page header that shows how a value moved.
12
+
13
+ ## When not to use
14
+ - Exact values on axes, several series, or a legend → a full chart library; Sparkline has no axes.
15
+ - Progress toward a goal → [ProgressBar](../progress-bar/COMPONENT.md) or [ProgressCircle](../progress-circle/COMPONENT.md).
16
+ - A list of values to compare → [DataTable](../data-table/COMPONENT.md).
17
+
18
+ ## Import
19
+ ```tsx
20
+ import { Sparkline, type SparklinePoint } from "prime-ui-kit";
21
+ ```
22
+
23
+ ## Anatomy
24
+ ```
25
+ Sparkline <div>; the headline and the chart
26
+ ├─ head title (label) · date of the shown point
27
+ ├─ value row value (rolling digits when settled) · change Badge with the trend arrow
28
+ └─ plot role="slider" over the svg line and area
29
+ └─ cursor guide line and dot of the shown point (aria-hidden)
30
+ ```
31
+
32
+ ## API
33
+
34
+ <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
35
+
36
+ ### Sparkline
37
+ `ref` → `HTMLDivElement` (the root). A headline (title · date, value · change) over a line chart; the chart is a `role="slider"` that picks a point.
38
+
39
+ | Prop | Type | Default | Description |
40
+ |---|---|---|---|
41
+ | `data` | `SparklinePoint[]` | — (required) | Points oldest first, `{ label, value }`; `label` names the point («8 окт»). Two or more draw a line. A new array cross-fades the line in. |
42
+ | `label` | `string` | — (required) | Visible title above the value and the accessible name of the chart. |
43
+ | `formatValue` | `(value: number) => string` | `Russian digit groups` | Display of a value in the headline and the spoken point («312 400 ₽»). |
44
+ | `labels` | `Partial<SparklineLabels>` | — | Built-in strings, see Labels. |
45
+ | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className`, `data-*` and the other div attributes on the root. |
46
+
47
+ ## Variants
48
+ No variants: one accent line on the surface it sits on. Put it in a `Card` for a dashboard tile.
49
+
50
+ ## States
51
+ | State | Driven by | DOM |
52
+ |---|---|---|
53
+ | at rest | — | the latest point; the value rolls its digits when `data` changes |
54
+ | scrubbing | pointer or touch over the plot | `data-scrubbing` on the root; the cursor and the headline follow the pointer with no easing (direct manipulation), digits change in place |
55
+ | keyboard | arrows, Home, End on the focused plot | `aria-valuenow` / `aria-valuetext`; the cursor glides to the point (`base` · emphasized) |
56
+ | settle | pointer leaves, touch ends, focus leaves | the cursor glides back to the latest point |
57
+ | new series | a new `data` array | the line cross-fades in (swap motion); the trend arrow turns half a turn between up and down |
58
+
59
+ ## Layout & spacing
60
+ - Takes the width of its container; the plot is 80 px high and inset by the dot radius so the cursor is never clipped.
61
+ - In a dashboard, wrap it in `Card.Root` + `Card.Body`; several cards go in a grid.
62
+ - Below 320 px the value row wraps the change badge under the value.
63
+
64
+ ## Accessibility
65
+
66
+ ### Keyboard
67
+ | Key | Action |
68
+ |---|---|
69
+ | `←` · `→` | Previous or next point. |
70
+ | `Home` · `End` | First or last point. |
71
+ | `Tab` | Focuses the chart; leaving focus returns to the latest point. |
72
+
73
+ ### ARIA
74
+ - The plot is `role="slider"` named by `label`; `aria-valuetext` speaks the point («8 окт: 312 400 ₽», `labels.point`).
75
+ - The headline with the latest value and change reads as plain text.
76
+ - The change carries a sign and an arrow, never color alone.
77
+
78
+ ### Labels
79
+ <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
80
+
81
+ | Key | Default | Used for |
82
+ |---|---|---|
83
+ | `point` | `"{label}: {value}"` | Spoken value of the chosen point (`aria-valuetext`); `{label}` and `{value}` are replaced. |
84
+
85
+ ## Examples
86
+ | Example | Shows |
87
+ |---|---|
88
+ | [overview.tsx](examples/overview.tsx) | Revenue for 30 days in a dashboard card: drag across the chart, the headline follows and the trend arrow turns — `data`, `label`, `formatValue`. |
89
+ | [period.tsx](examples/period.tsx) | Another period swaps the series: the line cross-fades in and the headline digits roll to the new total — `data`. |
90
+
91
+ ## Mistakes
92
+ - A new array on every render (`data={rows.map(...)}` inline) → the line cross-fades on every render; memoize the series.
93
+ - Formatting inside `label` of a point («312 400 ₽») → the point label is its name («8 окт»); format values with `formatValue`.
94
+ - A Sparkline without a container width (inside an inline flex row) → give it a width or put it in a `Card`.
95
+
96
+ ## Related
97
+ - **Built from:** [Badge](../badge/COMPONENT.md)
98
+ - **See also:** [Card](../card/COMPONENT.md), [ProgressBar](../progress-bar/COMPONENT.md), [SegmentedControl](../segmented-control/COMPONENT.md)
@@ -0,0 +1,13 @@
1
+ /* Layout of the Sparkline scenarios: semantic tokens only. */
2
+
3
+ /* A dashboard card's width: the chart takes the card width. */
4
+ .card {
5
+ width: min(100%, var(--prime-modal-width-s));
6
+ }
7
+
8
+ /* The period switch above the chart inside the card body. */
9
+ .body {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: var(--prime-space-4);
13
+ }
@@ -0,0 +1,27 @@
1
+ /** Revenue for 30 days in a dashboard card: drag across the chart, the headline follows and the trend arrow turns — `data`, `label`, `formatValue`. */
2
+ import { Card, Sparkline, type SparklinePoint } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ const MONTHS = ["янв", "фев", "мар", "апр", "мая", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"];
7
+
8
+ /** Daily revenue, 9 September — 8 October 2026. */
9
+ const REVENUE: SparklinePoint[] = Array.from({ length: 30 }, (_, i) => {
10
+ const day = new Date(2026, 8, 9 + i);
11
+ return {
12
+ label: `${day.getDate()} ${MONTHS[day.getMonth()]}`,
13
+ value: Math.round(260 + 40 * Math.sin(i / 3.2) + 28 * Math.sin(i / 1.4 + 1) + i * 2.6) * 1000,
14
+ };
15
+ });
16
+
17
+ const formatRubles = (value: number) => `${value.toLocaleString("ru-RU")} ₽`;
18
+
19
+ export default function SparklineOverviewExample() {
20
+ return (
21
+ <Card.Root className={styles.card}>
22
+ <Card.Body>
23
+ <Sparkline data={REVENUE} label="Выручка за 30 дней" formatValue={formatRubles} />
24
+ </Card.Body>
25
+ </Card.Root>
26
+ );
27
+ }
@@ -0,0 +1,46 @@
1
+ /** Another period swaps the series: the line cross-fades in and the headline digits roll to the new total — `data`. */
2
+ import { Card, SegmentedControl, Sparkline, type SparklinePoint } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ type Period = "week" | "month" | "quarter";
8
+
9
+ const series = (labels: string[], base: number, swing: number): SparklinePoint[] =>
10
+ labels.map((label, i) => ({
11
+ label,
12
+ value: Math.round((base + swing * Math.sin(i / 1.7) + (swing / 4) * i) / 100) * 100,
13
+ }));
14
+
15
+ const SERIES: Record<Period, SparklinePoint[]> = {
16
+ week: series(["2 окт", "3 окт", "4 окт", "5 окт", "6 окт", "7 окт", "8 окт"], 41_000, 6_000),
17
+ month: series(
18
+ ["сен, 1 нед.", "сен, 2 нед.", "сен, 3 нед.", "сен, 4 нед.", "окт, 1 нед."],
19
+ 286_000,
20
+ 32_000,
21
+ ),
22
+ quarter: series(["авг", "сен", "окт"], 1_240_000, 160_000),
23
+ };
24
+
25
+ const formatRubles = (value: number) => `${value.toLocaleString("ru-RU")} ₽`;
26
+
27
+ export default function SparklinePeriodExample() {
28
+ const [period, setPeriod] = React.useState<Period>("month");
29
+
30
+ return (
31
+ <Card.Root className={styles.card}>
32
+ <Card.Body className={styles.body}>
33
+ <SegmentedControl.Root
34
+ value={period}
35
+ onValueChange={(value) => setPeriod(value as Period)}
36
+ aria-label="Период"
37
+ >
38
+ <SegmentedControl.Item value="week">Неделя</SegmentedControl.Item>
39
+ <SegmentedControl.Item value="month">Месяц</SegmentedControl.Item>
40
+ <SegmentedControl.Item value="quarter">Квартал</SegmentedControl.Item>
41
+ </SegmentedControl.Root>
42
+ <Sparkline data={SERIES[period]} label="Новые заказы" formatValue={formatRubles} />
43
+ </Card.Body>
44
+ </Card.Root>
45
+ );
46
+ }
@@ -26,8 +26,8 @@ import { Tabs } from "prime-ui-kit";
26
26
 
27
27
  ## Anatomy
28
28
  ```
29
- Tabs.Root value, size, orientation, tone, fullWidth, min/maxItemWidth; horizontal: the frame
30
- ├─ Tabs.List role="tablist" on a ScrollContainer, sliding folder / pill; collapses, then scrolls
29
+ Tabs.Root value, size, orientation, tone, fullWidth, min/maxItemWidth; the frame (a layer)
30
+ ├─ Tabs.List role="tablist" on a ScrollContainer, sliding folder; collapses, then scrolls
31
31
  │ ├─ Tabs.Item one tab (role="tab") in an item wrapper; onRemove adds a close button; parts:
32
32
  │ │ ├─ Tabs.Icon decorative icon; hidden first, kept last
33
33
  │ │ ├─ Tabs.Label title; keeps its width when it turns medium weight
@@ -42,14 +42,14 @@ Tabs.Root value, size, orientation, tone, fullWidth, min/maxItem
42
42
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
43
43
 
44
44
  ### Tabs.Root
45
- `ref` → `HTMLDivElement`. `<div>` that owns the active value, size and orientation and lays out the list and the panel; horizontal, it is the frame: a sunken strip over the panel surface.
45
+ `ref` → `HTMLDivElement`. `<div>` that owns the active value, size and orientation and lays out the list and the panel. It is the frame and a layer of the surface ladder in both orientations: a strip two steps off the layer and the panel in the layer's own color — above the panel when horizontal, at its inline start when vertical.
46
46
 
47
47
  | Prop | Type | Default | Description |
48
48
  |---|---|---|---|
49
49
  | `value` | `string` | — | Active tab (controlled). |
50
50
  | `defaultValue` | `string` | `""` | Initial active tab (uncontrolled). With `""` no tab is selected. |
51
51
  | `onValueChange` | `(value: string) => void` | — | Called with the new active value. |
52
- | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | List direction and arrow keys. A vertical list stacks above the panel when the container is narrower than 600px. |
52
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | List direction and arrow keys. Vertical: a column of sections at the panel's inline start, the active one the panel reaching into the column; when the container is narrower than 600px the column becomes the horizontal strip above the panel. |
53
53
  | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | `"m"` | Control tier: text, icon, spacing, folder radius and panel padding. A horizontal tab is the control height plus the folder rise on both sides; a vertical one is the control height. |
54
54
  | `fullWidth` | `boolean` | `true` | Horizontal tabs fill the list and tend to equal widths within `minItemWidth` … `maxItemWidth`, never narrower than their content: a long label keeps its width, short ones share the rest; when they do not fit, the list collapses, then scrolls. `false` sizes each tab to its content within the same bounds. |
55
55
  | `tone` | `"neutral" \| "accent"` | `"neutral"` | Colour of the active tab: `neutral` is primary text with an accent icon, `accent` puts text and icon in accent. |
@@ -60,7 +60,7 @@ Tabs.Root value, size, orientation, tone, fullWidth, min/maxItem
60
60
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "defaultValue">` | — | `className` and the other div attributes. |
61
61
 
62
62
  ### Tabs.List
63
- `ref` → `HTMLDivElement`. `role="tablist"` on a `ScrollContainer` (horizontal, edge fade, hidden scrollbar) with the sliding folder (vertical: pill), which glides only into a choice by click or keys and is placed without motion on mount, resize and collapse. When tabs do not fit or a label would be cut, it hides icons and descriptions, then labels (icons with tooltips stay; only when every tab has an icon); then the list scrolls; a label is never cut to make room. The active tab is kept in view: only the list scrolls, never the page.
63
+ `ref` → `HTMLDivElement`. `role="tablist"` on a `ScrollContainer` (horizontal, edge fade, hidden scrollbar) with the sliding folder (in both orientations), which glides only into a choice by click or keys and is placed without motion on mount, resize and collapse. When tabs do not fit or a label would be cut, it hides icons and descriptions, then labels (icons with tooltips stay; only when every tab has an icon); then the list scrolls; a label is never cut to make room. The active tab is kept in view and clear of the edge fades (the first and the last tab scroll all the way to the end); only the list scrolls, never the page.
64
64
 
65
65
  | Prop | Type | Default | Description |
66
66
  |---|---|---|---|
@@ -128,7 +128,7 @@ Tabs.Root value, size, orientation, tone, fullWidth, min/maxItem
128
128
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className` and the other div attributes. |
129
129
 
130
130
  ## Variants
131
- Tabs have no `variant`: navigation tabs are always a folder (horizontal) or pills (vertical).
131
+ Tabs have no `variant`: navigation tabs are always a folder — rising from the panel's top edge (horizontal) or reaching out of its side (vertical).
132
132
 
133
133
  ### tone
134
134
  | Value | Looks like | Use when | Default |
@@ -139,8 +139,8 @@ Tabs have no `variant`: navigation tabs are always a folder (horizontal) or pill
139
139
  ### orientation
140
140
  | Value | Looks like | Use when | Default |
141
141
  |---|---|---|---|
142
- | `horizontal` | a `bg-sunken` strip over a padded `bg-surface` panel, tabs edge to edge; the active tab is the panel surface rising into the strip with concave flares, gliding between tabs; primary text, medium weight, accent icon; hover is the same shape in `fill-faint` | switching sections above the content | yes |
143
- | `vertical` | items padded like controls, no rail; active item is a `fill-muted` pill with a short accent mark at the start; hover `fill-subtle`; below a 600px container it becomes a scrolling row above the panel | settings pages with a side list of sections | |
142
+ | `horizontal` | a layer of the surface ladder: a `fill-muted-hover` strip (two steps off the layer) over a padded panel in the layer's own color, tabs edge to edge; the active tab is the panel rising into the strip with concave flares, gliding between tabs; primary text, medium weight, accent icon; hover is the same shape in `fill-faint` | switching sections above the content | yes |
143
+ | `vertical` | the same folder on its side: a column of sections in the `fill-muted-hover` strip at the panel's inline start; the active section is the panel reaching into the column, rounded on the outer side, with concave flares above and below where it meets the panel; hover is the same shape in `fill-faint`. Below a 600px container the column becomes the horizontal strip above the panel (it scrolls, never wraps) | settings pages with a side list of sections | |
144
144
 
145
145
  ### size
146
146
  | Value | Looks like | Use when | Default |
@@ -180,26 +180,25 @@ Avoid icons or descriptions on only some tabs (uneven rows; without an icon on e
180
180
  | State | Driven by | DOM |
181
181
  |---|---|---|
182
182
  | active | `value` / `defaultValue` | item `aria-selected="true"`, `data-state="active"`, `tabIndex=0`; others `data-state="inactive"`, `tabIndex=-1` |
183
- | hover | pointer | text → primary; horizontal: the folder shape in `fill-faint`, under the folder; vertical: `fill-subtle` background |
183
+ | hover | pointer | text → primary; the folder shape in `fill-faint`, under the folder |
184
184
  | pressed | pointer down | the tab content scales to `--prime-motion-press-scale-compact` |
185
- | focus-visible | keyboard | focus ring inside the tab, above the folder; the panel shows an inset ring (vertical: outer ring) |
185
+ | focus-visible | keyboard | focus ring inside the tab, above the folder; the panel shows an inset ring |
186
186
  | disabled | `disabled` on Item | native `disabled`, `data-disabled="true"`, `text-disabled`; a removable tab loses its close button |
187
187
  | removable | `onRemove` on Item | item wrapper `data-removable="true"`, close button, tab `aria-keyshortcuts="Delete"` |
188
188
  | two-line | `Tabs.Description` child | `data-two-line="true"` |
189
189
  | collapsed | tabs wider than the list, or a label that would be cut (horizontal) | list `data-collapse="full" \| "compact" \| "icon"`: `compact` hides icons and descriptions; `icon` hides labels (sr-only, shown in a `Tooltip`), counts and close buttons, only when every tab has an icon |
190
- | overflow | tabs at their narrowest (content width, `minItemWidth` or icon squares) still wider than the list | `data-overflow-start` / `data-overflow-end` on the list (ScrollContainer), faded edges; the active tab is kept in view on mount and on every value change — only the list scrolls, never the page |
190
+ | overflow | tabs at their narrowest (content width, `minItemWidth` or icon squares) still wider than the list | `data-overflow-start` / `data-overflow-end` on the list (ScrollContainer), faded edges as deep as the hidden part at most (they grow and melt with the scroll, never switch); the active tab is kept in view and clear of the fades on mount and on every value change, the first and the last tab scroll all the way to the end — only the list scrolls, never the page |
191
191
 
192
192
  The indicator glides (`emphasized` + `base`, `data-animate="true"`) only into a choice by click, arrow keys or closing the active tab; on mount, a value set from outside, a resize, font loading, tabs added or removed and collapse it is placed without motion, and it stays still under `prefers-reduced-motion`. The list scrolls smoothly to the active tab only after such a choice too.
193
193
 
194
- Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-width`, inline `--tabs-item-min` / `--tabs-item-max` when set; List `data-indicator="folder" | "pill"`; Item `data-value` on the tab, `data-state` / `data-disabled` on its wrapper too; Label `data-text`. Inactive panels are unmounted; a panel's content enters each time it opens.
194
+ Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-width`, inline `--tabs-item-min` / `--tabs-item-max` when set; List `data-indicator="folder"`, its indicator `data-axis="x" | "y"` (follows the list's direction, so a vertical list in a narrow container gets the top folder); Item `data-value` on the tab, `data-state` / `data-disabled` on its wrapper too; Label `data-text`. Inactive panels are unmounted; a panel's content enters each time it opens.
195
195
 
196
196
  ## Layout & spacing
197
- - Horizontal: the root is the frame (`radius-xl`, `bg-sunken`), the list sits flush on the panel and runs edge to edge; the panel pads its content by tier (`--prime-space-4` … `--prime-space-8`). Do not wrap Tabs in a Card — it already is one.
198
- - Vertical list → panel: `var(--prime-space-6)`.
197
+ - The root is the frame and a layer of the ladder in both orientations (`data-depth`, `radius-xl`, the strip two steps off the layer); the list sits flush on the panel; the panel pads its content by tier (`--prime-space-4` … `--prime-space-8`). Do not wrap Tabs in a Card — it already is one.
198
+ - Vertical: the column is as wide as its widest section (at least four control heights, at most 40% of the root), inset by the folder rise from the frame on the outer side, flush with the panel on the other.
199
199
  - The list never wraps: it collapses, then scrolls horizontally with faded edges (hidden scrollbar, `overscroll-behavior: contain`). Give the list's container a width (`min-width: 0` on flex children).
200
200
  - A horizontal tab is never narrower than its content (`min-width: max-content`; the tab bounds that content between `minItemWidth` and `maxItemWidth`), so a label is never cut to make room. Full width, tabs tend to equal widths, not widths in proportion to their labels (`flex: 1 1 0`): a long label keeps its width, the short ones share the rest.
201
201
  - A vertical Tabs.Root is a size container: give it a width (it measures itself to switch to a row).
202
- - In a vertical layout align the panel heading with the first tab by making the heading row one control tall (`min-height: var(--prime-control-m-height)`).
203
202
 
204
203
  ## Accessibility
205
204
 
@@ -235,7 +234,7 @@ Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-
235
234
  | [sizes.tsx](examples/sizes.tsx) | Every size tier: text, icon, folder radius and spacing grow together — `size`. |
236
235
  | [states.tsx](examples/states.tsx) | A disabled tab that clicks and arrow keys skip — `disabled`. |
237
236
  | [with-icon.tsx](examples/with-icon.tsx) | A muted icon before the label and a hairline before the service section — `Tabs.Icon`, `Tabs.Label`, `Tabs.Separator`. |
238
- | [orientation.tsx](examples/orientation.tsx) | Tabs over the panel and a side list of sections that stacks on top below 600px — `orientation`. |
237
+ | [orientation.tsx](examples/orientation.tsx) | Account settings with a side list of sections: the active section reaches out of the panel, and below 600px the list moves above it — `orientation`. |
239
238
  | [overflow.tsx](examples/overflow.tsx) | Drag the frame narrower: icons go first, then labels, and icons with tooltips stay; then the list scrolls — `Tabs.Icon`. |
240
239
  | [closable.tsx](examples/closable.tsx) | Open order cards as browser tabs: each closes by its button, Delete or a middle click; tabs stay between two widths, and the list scrolls when they do not fit — `onRemove`, `minItemWidth`, `maxItemWidth`. |
241
240
  | [two-line.tsx](examples/two-line.tsx) | A label and a counter on the first line, a summary on the second — `Tabs.Count`, `Tabs.Description`. |
@@ -8,6 +8,20 @@
8
8
  margin-inline: auto;
9
9
  }
10
10
 
11
+ /* A settings block: wide enough for the side list next to a form, never wider than the preview. */
12
+ .wide {
13
+ box-sizing: border-box;
14
+ width: min(100%, calc(20 * var(--prime-control-m-height)));
15
+ margin-inline: auto;
16
+ }
17
+
18
+ /* Fields and switches of a settings section: field → field rhythm (foundation §1). */
19
+ .form {
20
+ display: grid;
21
+ gap: var(--prime-space-5);
22
+ max-width: calc(10 * var(--prime-control-m-height));
23
+ }
24
+
11
25
  /* The same column, which the reader can drag narrower to watch the tabs collapse and scroll. */
12
26
  .resizable {
13
27
  composes: frame;