prime-ui-kit 0.9.1 → 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 (169) hide show
  1. package/README.md +30 -12
  2. package/dist/chunks/{chunk-NHOWNIAR.js → chunk-6JTZOA5I.js} +1472 -671
  3. package/dist/chunks/{chunk-DB2Y3XXA.js → chunk-OAE4IQX2.js} +2845 -1732
  4. package/dist/chunks/{chunk-KCQFOORX.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/DataTable.d.ts +6 -1
  11. package/dist/components/data-table/Row.d.ts +3 -1
  12. package/dist/components/data-table/rows.d.ts +4 -0
  13. package/dist/components/data-table/types.d.ts +5 -0
  14. package/dist/components/digit-input/DigitInput.d.ts +6 -1
  15. package/dist/components/dnd/Dnd.d.ts +1 -1
  16. package/dist/components/dnd/dragSession.d.ts +9 -0
  17. package/dist/components/dnd/useDropTarget.d.ts +5 -0
  18. package/dist/components/dnd/useFlipList.d.ts +3 -3
  19. package/dist/components/drawer/Drawer.d.ts +8 -5
  20. package/dist/components/dropdown/Dropdown.d.ts +17 -0
  21. package/dist/components/index.d.ts +9 -2
  22. package/dist/components/input/Input.d.ts +16 -1
  23. package/dist/components/input/passwordStrength.d.ts +9 -0
  24. package/dist/components/kanban/Kanban.d.ts +129 -0
  25. package/dist/components/modal/Modal.d.ts +5 -2
  26. package/dist/components/page-toolbar/PageToolbar.d.ts +53 -0
  27. package/dist/components/progress-bar/ProgressBar.d.ts +16 -1
  28. package/dist/components/segmented-control/SegmentedControl.d.ts +5 -2
  29. package/dist/components/sparkline/Sparkline.d.ts +26 -0
  30. package/dist/components/tabs/Tabs.d.ts +5 -2
  31. package/dist/hooks/useEdgeOverflow.d.ts +16 -0
  32. package/dist/hooks/useMediaQuery.d.ts +7 -0
  33. package/dist/hooks/useSwipeDismiss.d.ts +34 -0
  34. package/dist/icons/registry.d.ts +2 -0
  35. package/dist/index.css +2977 -1306
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +17 -3
  38. package/dist/internal/CheckMark.d.ts +12 -0
  39. package/dist/internal/MorphText.d.ts +12 -0
  40. package/dist/internal/RollingNumber.d.ts +14 -0
  41. package/dist/internal/overlay/FloatingPanel.d.ts +3 -1
  42. package/dist/internal/overlay/useFloatingLayer.d.ts +11 -1
  43. package/dist/internal/surfaceDepth.d.ts +25 -0
  44. package/dist/internal/swatch.d.ts +4 -1
  45. package/dist/internal/touchTarget.d.ts +4 -0
  46. package/dist/layout/app-header/AppHeader.d.ts +110 -0
  47. package/dist/layout/app-shell/AppShell.d.ts +16 -11
  48. package/dist/layout/bottom-nav/BottomNav.d.ts +80 -0
  49. package/dist/layout/index.d.ts +5 -1
  50. package/dist/motion/celebrate.d.ts +18 -0
  51. package/dist/tokens/layers.d.ts +37 -0
  52. package/dist/tokens/naming.d.ts +1 -1
  53. package/dist/tokens/primitives.d.ts +4 -0
  54. package/dist/tokens/semantic.d.ts +122 -56
  55. package/dist/tokens/themes/dark.d.ts +10 -14
  56. package/package.json +3 -2
  57. package/src/components/accordion/COMPONENT.md +5 -5
  58. package/src/components/avatar/COMPONENT.md +1 -1
  59. package/src/components/badge/COMPONENT.md +2 -2
  60. package/src/components/banner/COMPONENT.md +1 -1
  61. package/src/components/button/COMPONENT.md +26 -5
  62. package/src/components/button/examples/download.tsx +45 -0
  63. package/src/components/button/examples/hold-to-confirm.tsx +23 -0
  64. package/src/components/button/examples/label-morph.tsx +33 -0
  65. package/src/components/card/COMPONENT.md +4 -2
  66. package/src/components/card/examples/narrow.tsx +2 -2
  67. package/src/components/card/examples/panel-chart.tsx +1 -1
  68. package/src/components/card/examples/variants.tsx +1 -1
  69. package/src/components/checkbox/examples/examples.module.css +5 -3
  70. package/src/components/code-block/COMPONENT.md +6 -6
  71. package/src/components/code-block/examples/overview.tsx +1 -1
  72. package/src/components/code-block/examples/variants.tsx +1 -1
  73. package/src/components/color-picker/COMPONENT.md +1 -1
  74. package/src/components/color-swatches/COMPONENT.md +5 -5
  75. package/src/components/command-menu/COMPONENT.md +1 -1
  76. package/src/components/crossfade/COMPONENT.md +1 -1
  77. package/src/components/crossfade/examples/overview.tsx +3 -3
  78. package/src/components/data-table/COMPONENT.md +24 -10
  79. package/src/components/data-table/examples/columns-visibility.tsx +167 -0
  80. package/src/components/digit-input/COMPONENT.md +15 -13
  81. package/src/components/digit-input/examples/examples.module.css +8 -0
  82. package/src/components/digit-input/examples/in-form.tsx +2 -5
  83. package/src/components/digit-input/examples/on-complete.tsx +7 -6
  84. package/src/components/digit-input/examples/sizes.tsx +1 -1
  85. package/src/components/divider/COMPONENT.md +1 -1
  86. package/src/components/dnd/COMPONENT.md +3 -3
  87. package/src/components/dnd/examples/examples.module.css +4 -4
  88. package/src/components/drawer/COMPONENT.md +17 -9
  89. package/src/components/drawer/examples/date-sheet.tsx +41 -0
  90. package/src/components/drawer/examples/examples.module.css +5 -0
  91. package/src/components/drawer/examples/placement.tsx +4 -2
  92. package/src/components/dropdown/COMPONENT.md +20 -6
  93. package/src/components/dropdown/examples/checkbox-items.tsx +48 -0
  94. package/src/components/dropdown/examples/examples.module.css +1 -1
  95. package/src/components/empty-page/COMPONENT.md +1 -1
  96. package/src/components/empty-page/examples/examples.module.css +1 -1
  97. package/src/components/example-frame/COMPONENT.md +1 -1
  98. package/src/components/input/COMPONENT.md +10 -1
  99. package/src/components/input/examples/password-strength.tsx +12 -0
  100. package/src/components/kanban/COMPONENT.md +181 -0
  101. package/src/components/kanban/examples/controlled.tsx +78 -0
  102. package/src/components/kanban/examples/examples.module.css +31 -0
  103. package/src/components/kanban/examples/limits.tsx +58 -0
  104. package/src/components/kanban/examples/narrow.tsx +76 -0
  105. package/src/components/kanban/examples/overview.tsx +130 -0
  106. package/src/components/kanban/examples/states.tsx +75 -0
  107. package/src/components/kanban/examples/structure.tsx +76 -0
  108. package/src/components/login-form/COMPONENT.md +5 -5
  109. package/src/components/login-form/examples/register.tsx +2 -2
  110. package/src/components/login-form/examples/reset-password.tsx +2 -2
  111. package/src/components/modal/COMPONENT.md +11 -8
  112. package/src/components/modal/examples/examples.module.css +2 -2
  113. package/src/components/notification/COMPONENT.md +2 -2
  114. package/src/components/page-content/COMPONENT.md +1 -1
  115. package/src/components/page-content/examples/examples.module.css +1 -1
  116. package/src/components/page-toolbar/COMPONENT.md +142 -0
  117. package/src/components/page-toolbar/examples/examples.module.css +16 -0
  118. package/src/components/page-toolbar/examples/narrow.tsx +103 -0
  119. package/src/components/page-toolbar/examples/overview.tsx +113 -0
  120. package/src/components/page-toolbar/examples/save-changes.tsx +40 -0
  121. package/src/components/page-toolbar/examples/without-search.tsx +60 -0
  122. package/src/components/popover/COMPONENT.md +5 -4
  123. package/src/components/progress-bar/COMPONENT.md +6 -2
  124. package/src/components/progress-bar/examples/examples.module.css +7 -0
  125. package/src/components/progress-bar/examples/milestone.tsx +43 -0
  126. package/src/components/progress-bar/examples/steps.tsx +35 -0
  127. package/src/components/progress-circle/COMPONENT.md +1 -1
  128. package/src/components/scroll-container/COMPONENT.md +1 -1
  129. package/src/components/segmented-control/COMPONENT.md +8 -8
  130. package/src/components/segmented-control/examples/full-width.tsx +1 -1
  131. package/src/components/select/COMPONENT.md +2 -1
  132. package/src/components/skeleton/COMPONENT.md +1 -1
  133. package/src/components/skeleton/examples/with-crossfade.tsx +3 -3
  134. package/src/components/smart-filter/COMPONENT.md +2 -0
  135. package/src/components/sparkline/COMPONENT.md +98 -0
  136. package/src/components/sparkline/examples/examples.module.css +13 -0
  137. package/src/components/sparkline/examples/overview.tsx +27 -0
  138. package/src/components/sparkline/examples/period.tsx +46 -0
  139. package/src/components/spinner/COMPONENT.md +1 -1
  140. package/src/components/tabs/COMPONENT.md +24 -22
  141. package/src/components/tabs/examples/closable.tsx +1 -1
  142. package/src/components/tabs/examples/examples.module.css +14 -0
  143. package/src/components/tabs/examples/orientation.tsx +87 -33
  144. package/src/components/tooltip/COMPONENT.md +8 -6
  145. package/src/components/tooltip/examples/examples.module.css +2 -2
  146. package/src/layout/app-header/COMPONENT.md +178 -0
  147. package/src/layout/app-header/examples/breadcrumbs.tsx +36 -0
  148. package/src/layout/app-header/examples/examples.module.css +47 -0
  149. package/src/layout/app-header/examples/in-app-shell.tsx +70 -0
  150. package/src/layout/app-header/examples/narrow.tsx +29 -0
  151. package/src/layout/app-header/examples/overview.tsx +36 -0
  152. package/src/layout/app-shell/COMPONENT.md +35 -29
  153. package/src/layout/app-shell/examples/examples.module.css +17 -2
  154. package/src/layout/app-shell/examples/footer.tsx +59 -0
  155. package/src/layout/app-shell/examples/overview.tsx +19 -8
  156. package/src/layout/bottom-nav/COMPONENT.md +153 -0
  157. package/src/layout/bottom-nav/examples/examples.module.css +39 -0
  158. package/src/layout/bottom-nav/examples/floating.tsx +89 -0
  159. package/src/layout/bottom-nav/examples/icon-only.tsx +41 -0
  160. package/src/layout/bottom-nav/examples/in-app-shell.tsx +64 -0
  161. package/src/layout/bottom-nav/examples/overview.tsx +44 -0
  162. package/src/layout/bottom-nav/examples/router.tsx +31 -0
  163. package/src/layout/bottom-nav/examples/structure.tsx +47 -0
  164. package/src/layout/sidebar/COMPONENT.md +14 -13
  165. package/src/layout/sidebar/examples/examples.module.css +5 -5
  166. package/src/styles/globals.css +125 -2
  167. package/src/styles/theme-dark.css +39 -13
  168. package/src/styles/theme-light.css +84 -16
  169. package/src/styles/tokens.css +2 -0
@@ -0,0 +1,40 @@
1
+ /** A settings page: sections of the form on the left, the save action with the number of changes at the end of the top row — never a sticky bar at the bottom of a phone screen — `PageToolbar.Actions`. */
2
+ import { Button, PageToolbar, SegmentedControl } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ const SECTIONS = [
6
+ { value: "profile", label: "Профиль" },
7
+ { value: "billing", label: "Оплата" },
8
+ { value: "team", label: "Команда" },
9
+ ];
10
+
11
+ const CHANGES = 3;
12
+
13
+ export default function PageToolbarSaveChangesExample() {
14
+ const [section, setSection] = React.useState("profile");
15
+
16
+ return (
17
+ <PageToolbar.Root aria-label="Настройки">
18
+ <PageToolbar.Sections>
19
+ <SegmentedControl.Root
20
+ fullWidth
21
+ value={section}
22
+ onValueChange={setSection}
23
+ aria-label="Раздел настроек"
24
+ >
25
+ {SECTIONS.map((item) => (
26
+ <SegmentedControl.Item key={item.value} value={item.value}>
27
+ {item.label}
28
+ </SegmentedControl.Item>
29
+ ))}
30
+ </SegmentedControl.Root>
31
+ </PageToolbar.Sections>
32
+ <PageToolbar.Actions>
33
+ <Button.Root variant="ghost" tone="neutral">
34
+ Отменить
35
+ </Button.Root>
36
+ <Button.Root>Сохранить {CHANGES} изменения</Button.Root>
37
+ </PageToolbar.Actions>
38
+ </PageToolbar.Root>
39
+ );
40
+ }
@@ -0,0 +1,60 @@
1
+ /** A dashboard panel without search on a tablet-width column: the sections fill the top row, the period selector alone in the bottom row stretches to the full width — `PageToolbar.Sections`, `PageToolbar.View`. */
2
+ import { Icon, PageToolbar, SegmentedControl, Select } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ const SECTIONS = [
8
+ { value: "overview", label: "Обзор" },
9
+ { value: "activity", label: "Активность" },
10
+ { value: "resources", label: "Ресурсы" },
11
+ ];
12
+
13
+ const PERIODS = [
14
+ { value: "1h", label: "1 час" },
15
+ { value: "24h", label: "24 часа" },
16
+ { value: "7d", label: "7 дней" },
17
+ ];
18
+
19
+ export default function PageToolbarWithoutSearchExample() {
20
+ const [section, setSection] = React.useState("overview");
21
+ const [period, setPeriod] = React.useState("24h");
22
+
23
+ return (
24
+ <div className={styles.tablet}>
25
+ <PageToolbar.Root aria-label="Панель мониторинга">
26
+ <PageToolbar.Sections>
27
+ <SegmentedControl.Root
28
+ fullWidth
29
+ value={section}
30
+ onValueChange={setSection}
31
+ aria-label="Раздел"
32
+ >
33
+ {SECTIONS.map((item) => (
34
+ <SegmentedControl.Item key={item.value} value={item.value}>
35
+ {item.label}
36
+ </SegmentedControl.Item>
37
+ ))}
38
+ </SegmentedControl.Root>
39
+ </PageToolbar.Sections>
40
+ <PageToolbar.View>
41
+ <Select.Root value={period} onValueChange={setPeriod}>
42
+ <Select.Trigger aria-label="Период">
43
+ <Select.TriggerIcon>
44
+ <Icon name="field.calendar" />
45
+ </Select.TriggerIcon>
46
+ <Select.Value />
47
+ </Select.Trigger>
48
+ <Select.Content>
49
+ {PERIODS.map((item) => (
50
+ <Select.Item key={item.value} value={item.value}>
51
+ {item.label}
52
+ </Select.Item>
53
+ ))}
54
+ </Select.Content>
55
+ </Select.Root>
56
+ </PageToolbar.View>
57
+ </PageToolbar.Root>
58
+ </div>
59
+ );
60
+ }
@@ -61,7 +61,7 @@ No DOM: clone the single child and merge `ref` and `onClick`. Trigger toggles th
61
61
  | `children` | `ReactElement` | — (required) | One element, usually a Button (Anchor: any element, e.g. a toolbar). |
62
62
 
63
63
  ### Popover.Content
64
- `ref` → `HTMLDivElement`. Portal + `role="dialog"` on the floating surface (a ScrollContainer); renders while open and during its exit animation. Named by `Popover.Title`, else by the trigger. Opened from a Trigger, focus moves into the panel (`[data-autofocus]`, else the first tab stop, else the panel).
64
+ `ref` → `HTMLDivElement`. Portal + `role="dialog"` on the floating surface (a ScrollContainer); renders while open and during its exit animation. Named by `Popover.Title`, else by the trigger. Opened from a Trigger, focus moves into the panel (`[data-autofocus]`, else the first tab stop, else the panel). Below 640px of viewport a panel opened from a Trigger (not an Anchor) is a bottom sheet: a scrim, a grab handle, swipe down to close (with `closeOnOutsideClick`), page scroll locked.
65
65
 
66
66
  | Prop | Type | Default | Description |
67
67
  |---|---|---|---|
@@ -74,10 +74,10 @@ No DOM: clone the single child and merge `ref` and `onClick`. Trigger toggles th
74
74
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "role">` | — | `className` and the other attributes of the panel. |
75
75
 
76
76
  ### Popover.Header · Popover.Title · Popover.Description · Popover.Actions
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 below 480 px). + native props.
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 |
@@ -121,6 +121,7 @@ Title uses the tier text size with title weight; Description uses the tier label
121
121
  | size | `size` on Content | `data-size` |
122
122
  | width | `matchTriggerWidth` | `data-match-trigger-width="true"` |
123
123
  | flush | `flush` | `data-flush="true"` |
124
+ | sheet | viewport below 640px, opened from a Trigger (an Anchor keeps the panel next to its field) | a scrim and a full-width bottom sheet with an `aria-hidden` grab handle around the panel; the sheet carries `data-state`, the panel keeps its role and ref; page scroll locked; a swipe down from the handle closes it (with `closeOnOutsideClick`) |
124
125
 
125
126
  There is no `disabled` on Popover: a disabled trigger never opens it.
126
127
 
@@ -128,7 +129,7 @@ There is no `disabled` on Popover: a disabled trigger never opens it.
128
129
  - Width is `max-content` between `--prime-panel-min-width` and twice that, never wider than the viewport; give forms an explicit width with `className`.
129
130
  - Max height follows the free space on the resolved side; the panel scrolls inside.
130
131
  - Kept `--prime-space-2` from viewport edges; follows the trigger on scroll and resize.
131
- - Actions: at the end, `--prime-space-2` gap; below 480px they stack full width.
132
+ - Actions: at the end, `--prime-space-2` gap; on phones (below 640px) they stack full width.
132
133
 
133
134
  ## Accessibility
134
135
 
@@ -1,6 +1,6 @@
1
1
  # ProgressBar
2
2
 
3
- **Category:** feedback
3
+ **Category:** status
4
4
  **Kind:** primitive
5
5
 
6
6
  > Linear progress: one value on a native `<progress>`, or `segments` that split a whole (storage by type, task statuses), with a label, a percentage and status colors.
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  # ProgressCircle
2
2
 
3
- **Category:** feedback
3
+ **Category:** status
4
4
  **Kind:** primitive
5
5
 
6
6
  > Circular progress — the ring version of ProgressBar: one value or `segments` that split a whole, with status colors and optional content in the center.
@@ -1,6 +1,6 @@
1
1
  # ScrollContainer
2
2
 
3
- **Category:** layout
3
+ **Category:** page
4
4
  **Kind:** layout
5
5
 
6
6
  > A scroll region with the kit's thin scrollbar that shrinks correctly inside flex and grid parents.
@@ -27,7 +27,7 @@ import { SegmentedControl } from "prime-ui-kit";
27
27
  SegmentedControl.Root role="radiogroup"; track + sliding thumb, scrolling row, keyboard
28
28
  └─ SegmentedControl.Item role="radio" button, one option
29
29
  ├─ SegmentedControl.Icon icon slot (optional; icon-only → square segment)
30
- ├─ SegmentedControl.Label title, truncates (optional; plain text is wrapped automatically)
30
+ ├─ SegmentedControl.Label title, one line (optional; plain text is wrapped automatically)
31
31
  ├─ SegmentedControl.Count counter Badge after the label (optional)
32
32
  └─ SegmentedControl.Description second line; makes the segment two-line (optional)
33
33
  ```
@@ -46,12 +46,12 @@ SegmentedControl.Root role="radiogroup"; track + sliding thumb,
46
46
  | `onValueChange` | `(value: string) => void` | — | Called with the new value on click or arrow keys. |
47
47
  | `disabled` | `boolean` | `false` | Disables the whole group. |
48
48
  | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | `host tier, else "m"` | Control tier; the outer height equals the control height 28 · 32 · 36 · 40 · 48. Without it the tier of its host (a toolbar, a panel with a size), else `m`. |
49
- | `fullWidth` | `boolean` | `false` | Fills the container; single-line segments share the width equally and truncate. |
49
+ | `fullWidth` | `boolean` | `false` | Fills the container; single-line segments tend to equal widths but never get narrower than their label — when they do not fit, the row scrolls. |
50
50
  | `children` | `ReactNode` | — | `SegmentedControl.Item`s (may be wrapped, e.g. in `Tooltip.Trigger`). |
51
51
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "defaultValue" \| "onChange">` | — | `aria-label` / `aria-labelledby` (name the group), `className` and the other div attributes. |
52
52
 
53
53
  ### SegmentedControl.Item
54
- `ref` → `HTMLButtonElement`. One option, a `<button role="radio">` with roving `tabIndex`; plain text is wrapped to truncate.
54
+ `ref` → `HTMLButtonElement`. One option, a `<button role="radio">` with roving `tabIndex`; plain text is wrapped in a `SegmentedControl.Label`.
55
55
 
56
56
  | Prop | Type | Default | Description |
57
57
  |---|---|---|---|
@@ -70,7 +70,7 @@ SegmentedControl.Root role="radiogroup"; track + sliding thumb,
70
70
  | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other span attributes. |
71
71
 
72
72
  ### SegmentedControl.Label
73
- `ref` → `HTMLSpanElement`. Segment title; truncates with an ellipsis. Plain text is wrapped automatically.
73
+ `ref` → `HTMLSpanElement`. Segment title on one line; never truncated. Plain text is wrapped automatically.
74
74
 
75
75
  | Prop | Type | Default | Description |
76
76
  |---|---|---|---|
@@ -123,7 +123,7 @@ Track is `fill-muted` with a faint 1px ring; the selected segment is a floating
123
123
  ### Flags
124
124
  | Value | Looks like | Use when | Default |
125
125
  |---|---|---|---|
126
- | `fullWidth` | fills the container; single-line segments are equal and truncate, two-line segments grow from their content | modals, mobile panels, overview strips | off |
126
+ | `fullWidth` | fills the container; single-line segments tend to equal widths, a long label keeps its own width and the row scrolls when they do not fit; two-line segments grow from their content | modals, mobile panels, overview strips | off |
127
127
  | `disabled` | every segment disabled, thumb `fill-muted-hover` without shadow | the choice is temporarily unavailable | off |
128
128
  | icon-only (only `SegmentedControl.Icon`) | square segment (`data-icon-only`) | toolbar view toggles; needs `aria-label` | — |
129
129
 
@@ -140,13 +140,13 @@ Colour all items or none; do not give an item and its Count the same hue; prefer
140
140
  | focus-visible | keyboard | focus ring inside the segment |
141
141
  | overflow | row wider than the container | root `data-overflow-start` / `data-overflow-end`: edge fades in the track color |
142
142
 
143
- Item DOM: `data-state`, `data-disabled`, `data-icon-only`, `data-two-line`, `data-value`, `data-color`. The thumb glides (`emphasized` + `base`, `data-animate`) only into a user's choice (click, arrows); a value set from outside and layout changes snap it; it stays still under `prefers-reduced-motion`. It sits inside the segment track, which clips it, so moving the selection never changes the row's scroll area; when the row scrolls, the chosen segment is scrolled into view.
143
+ Item DOM: `data-state`, `data-disabled`, `data-icon-only`, `data-two-line`, `data-value`, `data-color`. The thumb glides (`emphasized` + `base`, `data-animate`) only into a user's choice (click, arrows); a value set from outside and layout changes snap it; it stays still under `prefers-reduced-motion`. It sits inside the segment track, which clips it, so moving the selection never changes the row's scroll area. When the row scrolls, the selected segment is kept in view on mount and on every value change: only the row scrolls, never the page, and the scroll glides only after a user's choice.
144
144
 
145
145
  ## Layout & spacing
146
146
  - Width is content-based by default; segments never wrap. In a narrow container the row scrolls horizontally with fading edges.
147
147
  - In a toolbar: gap `--prime-space-3` between controls; keep every control at one size.
148
148
  - Caption under a group: gap `--prime-space-2`.
149
- - `fullWidth` inside modals and mobile sheets.
149
+ - `fullWidth` inside modals and mobile sheets. Single-line segments are `flex: 1 1 0` with `min-width: max-content`: short labels share the spare width equally, a long label keeps its own width, nothing truncates; when the labels do not fit, the row scrolls.
150
150
 
151
151
  ## Accessibility
152
152
 
@@ -177,7 +177,7 @@ No `labels`.
177
177
  | [states.tsx](examples/states.tsx) | No selection yet, a disabled segment and a disabled group next to the default — `disabled`. |
178
178
  | [with-icon.tsx](examples/with-icon.tsx) | An icon before the label and square icon-only segments named for screen readers — `SegmentedControl.Icon`, `aria-label`. |
179
179
  | [colors.tsx](examples/colors.tsx) | A status per option: a dot of its hue before the label and a tinted thumb when chosen — `color`. |
180
- | [full-width.tsx](examples/full-width.tsx) | The group fills its column; segments share the width equally and truncate long labels — `fullWidth`. |
180
+ | [full-width.tsx](examples/full-width.tsx) | The group fills its column; segments tend to equal widths and a long label keeps its own — `fullWidth`. |
181
181
  | [scroll.tsx](examples/scroll.tsx) | Segments never wrap: in a column narrower than the row it scrolls with edge fades and brings the chosen segment into view. |
182
182
  | [two-line.tsx](examples/two-line.tsx) | A label and a counter on the first line, a metric on the second — `SegmentedControl.Label`, `SegmentedControl.Count`, `SegmentedControl.Description`. |
183
183
  | [controlled.tsx](examples/controlled.tsx) | The parent owns the choice and updates other content from it — `value`, `onValueChange`. |
@@ -1,4 +1,4 @@
1
- /** The group fills its column; segments share the width equally and truncate long labels — `fullWidth`. */
1
+ /** The group fills its column; segments tend to equal widths and a long label keeps its own — `fullWidth`. */
2
2
  import { SegmentedControl } from "prime-ui-kit";
3
3
 
4
4
  export default function SegmentedControlFullWidthExample() {
@@ -95,7 +95,7 @@ Select.Root field frame: label · control · hint/error; v
95
95
  `ref` → `HTMLSpanElement`. An `aria-hidden` `<span>` with a leading glyph at the tier icon size: before the value / before the option label. + native `<span>` props.
96
96
 
97
97
  ### Select.Content
98
- `ref` → `HTMLDivElement` (the panel). Portal + the floating list panel, mounted while open and during its exit animation (closed, the items render hidden in place so the trigger knows their labels), shown with the overlay motion. Holds the search row, the `role="listbox"` and the empty state.
98
+ `ref` → `HTMLDivElement` (the panel). Portal + the floating list panel, mounted while open and during its exit animation (closed, the items render hidden in place so the trigger knows their labels), shown with the overlay motion. Holds the search row, the `role="listbox"` and the empty state. Below 640px of viewport the panel is a bottom sheet: a scrim, a grab handle, swipe down to close, page scroll locked.
99
99
 
100
100
  | Prop | Type | Default | Description |
101
101
  |---|---|---|---|
@@ -163,6 +163,7 @@ No `variant` or `tone`. The trigger is the field look (fill, inset control borde
163
163
  | disabled | `disabled` | native `disabled`, `data-disabled="true"` |
164
164
  | loading | `loading` | `data-loading="true"`, `aria-busy`; status row in the list |
165
165
  | searching | typing in the search | panel `data-searching="true"`; groups without matches and separators hide; the match in an option's title, description and plain text is a `<mark>` in `accent-text` (not in the trigger) |
166
+ | sheet | viewport below 640px | a scrim and a full-width bottom sheet with an `aria-hidden` grab handle around the panel; the sheet carries `data-state`, the panel keeps its role and ref; page scroll locked; a swipe down from the handle closes it |
166
167
  | option highlighted / selected / disabled | keyboard and pointer / value / `disabled` | `data-highlighted`, `data-selected` + `aria-selected`, `data-disabled` + `aria-disabled`; highlight is a fill only |
167
168
 
168
169
  ## Layout & spacing
@@ -1,6 +1,6 @@
1
1
  # Skeleton
2
2
 
3
- **Category:** feedback
3
+ **Category:** status
4
4
  **Kind:** primitive
5
5
 
6
6
  > A placeholder in the shape of the content that is loading — text lines, a control, an avatar, a block — so the layout is in place before the data arrives.
@@ -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;
@@ -117,6 +117,8 @@ same height and type.
117
117
 
118
118
  ## Layout & spacing
119
119
  - The panel is a flush Popover as wide as the toolbar; every section is a full-width row with its own padding (`--prime-space-3` × `--prime-space-4`, footer `--prime-space-2` × `--prime-space-4`), Dividers run edge to edge, the field label column is `--prime-space-24`.
120
+ - The toolbar is one row of controls (`--prime-space-2` gap): the filter button keeps its width, the search grows from 9rem (`--prime-space-24` × 1.5). When less than 9rem is left beside the button, the search moves under it at full width; every row stays one control high.
121
+ - Chips are Badges of the toolbar tier on the badge scale (`xs` → `s`), so a tag sits lower than the controls above it; «Добавить фильтр» is a tag of the same tier and «Сбросить все» a LinkButton with the tag's text size.
120
122
  - Values and chips wrap with `--prime-space-2` gaps; «Сбросить все» is pushed to the end.
121
123
  - The root is a flex column (`--prime-space-3`): Toolbar, then Chips.
122
124
 
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  # Spinner
2
2
 
3
- **Category:** feedback
3
+ **Category:** status
4
4
  **Kind:** primitive
5
5
 
6
6
  > An indeterminate loading indicator: a ring with a gap that turns while a request runs.