prime-ui-kit 0.20.1 → 0.21.0

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 (149) hide show
  1. package/README.md +1 -1
  2. package/dist/chunks/{chunk-4KONMO7Z.js → chunk-6L7VWEY6.js} +1 -1
  3. package/dist/chunks/{chunk-3PY52JP3.js → chunk-S27VR4ON.js} +5828 -5596
  4. package/dist/chunks/{chunk-4374EBWV.js → chunk-S4H2ZKWJ.js} +1049 -644
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/badge/Badge.d.ts +19 -2
  7. package/dist/components/card/Card.d.ts +10 -3
  8. package/dist/components/composer/Composer.d.ts +49 -1
  9. package/dist/components/crossfade/Crossfade.d.ts +6 -4
  10. package/dist/components/data-table/DataTable.d.ts +10 -2
  11. package/dist/components/data-table/Row.d.ts +12 -1
  12. package/dist/components/data-table/SelectionMenu.d.ts +20 -0
  13. package/dist/components/data-table/useRowSelection.d.ts +2 -0
  14. package/dist/components/empty-page/EmptyPage.d.ts +5 -2
  15. package/dist/components/hint/Hint.d.ts +1 -1
  16. package/dist/components/index.d.ts +1 -1
  17. package/dist/components/input/Input.d.ts +15 -3
  18. package/dist/components/message/Message.d.ts +6 -3
  19. package/dist/components/segmented-control/SegmentedControl.d.ts +5 -1
  20. package/dist/components/smart-filter/SmartFilter.d.ts +4 -0
  21. package/dist/components/spinner/Spinner.d.ts +3 -2
  22. package/dist/components/tabs/Tabs.d.ts +10 -1
  23. package/dist/hooks/useModalLayer.d.ts +15 -8
  24. package/dist/index.css +1 -1
  25. package/dist/index.js +2 -2
  26. package/dist/internal/RegionSwap.d.ts +18 -10
  27. package/dist/internal/RollingNumber.d.ts +7 -5
  28. package/dist/internal/ToolRow.d.ts +6 -0
  29. package/dist/internal/mediaSlot.d.ts +29 -0
  30. package/dist/internal/overlay/DialogParts.d.ts +30 -6
  31. package/dist/internal/overlay/focus.d.ts +18 -3
  32. package/dist/internal/partPresence.d.ts +19 -2
  33. package/dist/layout/sidebar/Sidebar.d.ts +3 -2
  34. package/dist/skill/anti-slop.md +35 -37
  35. package/dist/skill/api-contract.md +50 -2
  36. package/dist/skill/cheatsheet.md +15 -10
  37. package/dist/skill/checklist.md +19 -6
  38. package/dist/skill/choosing.md +79 -6
  39. package/dist/skill/components.md +1 -1
  40. package/dist/skill/composition.md +211 -51
  41. package/dist/skill/foundations.md +17 -6
  42. package/dist/skill/layouts.md +64 -14
  43. package/dist/skill/motion.md +9 -3
  44. package/dist/skill/reference/components/avatar/COMPONENT.md +1 -0
  45. package/dist/skill/reference/components/badge/COMPONENT.md +29 -5
  46. package/dist/skill/reference/components/badge/examples/examples.module.css +14 -0
  47. package/dist/skill/reference/components/badge/examples/overflow-count.tsx +32 -0
  48. package/dist/skill/reference/components/banner/COMPONENT.md +1 -1
  49. package/dist/skill/reference/components/card/COMPONENT.md +8 -4
  50. package/dist/skill/reference/components/card/examples/person-header.tsx +38 -0
  51. package/dist/skill/reference/components/composer/COMPONENT.md +35 -5
  52. package/dist/skill/reference/components/composer/examples/bot-keyboard.tsx +43 -0
  53. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -1
  54. package/dist/skill/reference/components/crossfade/COMPONENT.md +18 -10
  55. package/dist/skill/reference/components/data-table/COMPONENT.md +17 -11
  56. package/dist/skill/reference/components/data-table/examples/screen.tsx +6 -9
  57. package/dist/skill/reference/components/data-table/examples/selection.tsx +60 -18
  58. package/dist/skill/reference/components/drawer/COMPONENT.md +41 -19
  59. package/dist/skill/reference/components/drawer/examples/footer-actions.tsx +54 -0
  60. package/dist/skill/reference/components/drawer/examples/header-actions.tsx +46 -25
  61. package/dist/skill/reference/components/drawer/examples/link-loading.tsx +57 -0
  62. package/dist/skill/reference/components/drawer/examples/screen.tsx +3 -5
  63. package/dist/skill/reference/components/empty-page/COMPONENT.md +2 -2
  64. package/dist/skill/reference/components/field-list/COMPONENT.md +4 -4
  65. package/dist/skill/reference/components/field-list/examples/row-menu.tsx +1 -1
  66. package/dist/skill/reference/components/hint/COMPONENT.md +1 -1
  67. package/dist/skill/reference/components/input/COMPONENT.md +16 -3
  68. package/dist/skill/reference/components/input/examples/password-policy.tsx +65 -0
  69. package/dist/skill/reference/components/list/COMPONENT.md +1 -1
  70. package/dist/skill/reference/components/login-form/COMPONENT.md +13 -11
  71. package/dist/skill/reference/components/login-form/examples/overview.tsx +5 -5
  72. package/dist/skill/reference/components/login-form/examples/states.tsx +7 -6
  73. package/dist/skill/reference/components/message/COMPONENT.md +5 -5
  74. package/dist/skill/reference/components/message/examples/bot-actions.tsx +47 -28
  75. package/dist/skill/reference/components/modal/COMPONENT.md +31 -12
  76. package/dist/skill/reference/components/modal/examples/structure.tsx +2 -4
  77. package/dist/skill/reference/components/segmented-control/COMPONENT.md +11 -6
  78. package/dist/skill/reference/components/segmented-control/examples/router.tsx +22 -0
  79. package/dist/skill/reference/components/select/COMPONENT.md +4 -1
  80. package/dist/skill/reference/components/sheet/COMPONENT.md +31 -12
  81. package/dist/skill/reference/components/sheet/examples/header-actions.tsx +5 -3
  82. package/dist/skill/reference/components/skeleton/COMPONENT.md +3 -2
  83. package/dist/skill/reference/components/smart-filter/COMPONENT.md +2 -1
  84. package/dist/skill/reference/components/smart-filter/examples/ranges.tsx +8 -2
  85. package/dist/skill/reference/components/spinner/COMPONENT.md +1 -1
  86. package/dist/skill/reference/components/tabs/COMPONENT.md +7 -4
  87. package/dist/skill/reference/components/tabs/examples/in-drawer.tsx +18 -2
  88. package/dist/skill/reference/components/thumbnail/COMPONENT.md +1 -0
  89. package/dist/skill/reference/components/tooltip/COMPONENT.md +3 -3
  90. package/dist/skill/reference/foundation.md +15 -9
  91. package/dist/skill/reference/layout/page/COMPONENT.md +1 -0
  92. package/dist/skill/reference/layout/sidebar/COMPONENT.md +4 -2
  93. package/dist/skill/reference/layout/sidebar/examples/account-profile.tsx +66 -0
  94. package/dist/skill/responsive.md +28 -6
  95. package/dist/skill/ux.md +97 -2
  96. package/dist/tokens/semantic.d.ts +2 -1
  97. package/dist/tokens/themes/dark.d.ts +1 -0
  98. package/package.json +1 -1
  99. package/src/components/avatar/COMPONENT.md +1 -0
  100. package/src/components/badge/COMPONENT.md +29 -5
  101. package/src/components/badge/examples/examples.module.css +14 -0
  102. package/src/components/badge/examples/overflow-count.tsx +32 -0
  103. package/src/components/banner/COMPONENT.md +1 -1
  104. package/src/components/card/COMPONENT.md +8 -4
  105. package/src/components/card/examples/person-header.tsx +38 -0
  106. package/src/components/composer/COMPONENT.md +35 -5
  107. package/src/components/composer/examples/bot-keyboard.tsx +43 -0
  108. package/src/components/confirm-dialog/COMPONENT.md +1 -1
  109. package/src/components/crossfade/COMPONENT.md +18 -10
  110. package/src/components/data-table/COMPONENT.md +17 -11
  111. package/src/components/data-table/examples/screen.tsx +6 -9
  112. package/src/components/data-table/examples/selection.tsx +60 -18
  113. package/src/components/drawer/COMPONENT.md +41 -19
  114. package/src/components/drawer/examples/footer-actions.tsx +54 -0
  115. package/src/components/drawer/examples/header-actions.tsx +46 -25
  116. package/src/components/drawer/examples/link-loading.tsx +57 -0
  117. package/src/components/drawer/examples/screen.tsx +3 -5
  118. package/src/components/empty-page/COMPONENT.md +2 -2
  119. package/src/components/field-list/COMPONENT.md +4 -4
  120. package/src/components/field-list/examples/row-menu.tsx +1 -1
  121. package/src/components/hint/COMPONENT.md +1 -1
  122. package/src/components/input/COMPONENT.md +16 -3
  123. package/src/components/input/examples/password-policy.tsx +65 -0
  124. package/src/components/list/COMPONENT.md +1 -1
  125. package/src/components/login-form/COMPONENT.md +13 -11
  126. package/src/components/login-form/examples/overview.tsx +5 -5
  127. package/src/components/login-form/examples/states.tsx +7 -6
  128. package/src/components/message/COMPONENT.md +5 -5
  129. package/src/components/message/examples/bot-actions.tsx +47 -28
  130. package/src/components/modal/COMPONENT.md +31 -12
  131. package/src/components/modal/examples/structure.tsx +2 -4
  132. package/src/components/segmented-control/COMPONENT.md +11 -6
  133. package/src/components/segmented-control/examples/router.tsx +22 -0
  134. package/src/components/select/COMPONENT.md +4 -1
  135. package/src/components/sheet/COMPONENT.md +31 -12
  136. package/src/components/sheet/examples/header-actions.tsx +5 -3
  137. package/src/components/skeleton/COMPONENT.md +3 -2
  138. package/src/components/smart-filter/COMPONENT.md +2 -1
  139. package/src/components/smart-filter/examples/ranges.tsx +8 -2
  140. package/src/components/spinner/COMPONENT.md +1 -1
  141. package/src/components/tabs/COMPONENT.md +7 -4
  142. package/src/components/tabs/examples/in-drawer.tsx +18 -2
  143. package/src/components/thumbnail/COMPONENT.md +1 -0
  144. package/src/components/tooltip/COMPONENT.md +3 -3
  145. package/src/layout/page/COMPONENT.md +1 -0
  146. package/src/layout/sidebar/COMPONENT.md +4 -2
  147. package/src/layout/sidebar/examples/account-profile.tsx +66 -0
  148. package/src/styles/theme-dark.css +1 -0
  149. package/src/styles/theme-light.css +1 -0
@@ -93,6 +93,7 @@ One entry of `fields`: data, not a part. Selected values missing from `options`
93
93
  | `finite` | `boolean` | `true` | `options` fields: a fixed set — hiding every value is refused. `false` for an open set (services, users). |
94
94
  | `multiple` | `boolean` | `true` | `options` fields: many values at once. `false` — one value and no hiding: a press picks only that value, a second press clears it (an API that takes one value per field). |
95
95
  | `formatValue` | `(value: number) => string` | Russian digit groups | `range` fields: a bound in the chip («1 000 ₽»). |
96
+ | `calendar` | `Pick<DatepickerPanelProps, "months" \| "presets" \| "prompt" \| "footer" \| "withTime" \| "isDayDisabled" \| "disableFuture" \| "today" \| "locale" \| "weekStartsOn" \| "labels">` | — | `date-range` fields: the calendar of the period, as on a report's own Datepicker — `presets`, `months`, `prompt`, `footer`, `withTime`; the chip names a matching preset («Период: Этот месяц»). Without it the panel opens one month with no presets. |
96
97
 
97
98
  ### Functions
98
99
 
@@ -233,7 +234,7 @@ same height and type.
233
234
  | [overview.tsx](examples/overview.tsx) | Filters above a request list: the filter button and the search, applied filters as tags, rows narrowed by the value and the text — `fields`, `value`, `search`, `matchesSmartFilter`. |
234
235
  | [sizes.tsx](examples/sizes.tsx) | Every size tier: the filter button, the search, the tags and the panel follow one tier — `size`. |
235
236
  | [many-values.tsx](examples/many-values.tsx) | An open set of twenty services, degraded ones marked with an icon, collapsed after eight; typing narrows the panel and marks the match in accent — `finite`, `icon`, `collapsedLimit`. |
236
- | [ranges.tsx](examples/ranges.tsx) | Deals narrowed by an amount from–to and a period from the calendar; each range is one readable chip, and the bounds go to the server as from / to parameters — `kind`, `formatValue`, `matchesSmartFilter`, `isSmartFilterRange`. |
237
+ | [ranges.tsx](examples/ranges.tsx) | Deals narrowed by an amount from–to and a period from a calendar with presets, two months and an Apply footer; each range is one readable chip, and the bounds go to the server as from / to parameters — `kind`, `formatValue`, `calendar`, `matchesSmartFilter`, `isSmartFilterRange`. |
237
238
  | [single-value.tsx](examples/single-value.tsx) | For an API that takes one value per field: the filter button without a search, a press picks only that value and nothing can be hidden — `searchable`, `multiple`. |
238
239
  | [option-groups.tsx](examples/option-groups.tsx) | Values of different kinds in one field: owner types and people under their own headings — `options`, `group`. |
239
240
  | [controlled.tsx](examples/controlled.tsx) | A saved view sets the value from outside; the selection is read back as the list to ask the server for — `value`, `onValueChange`, `resolveSmartFilterValues`. |
@@ -1,6 +1,7 @@
1
- /** Deals narrowed by an amount from–to and a period from the calendar; each range is one readable chip, and the bounds go to the server as from / to parameters — `kind`, `formatValue`, `matchesSmartFilter`, `isSmartFilterRange`. */
1
+ /** Deals narrowed by an amount from–to and a period from a calendar with presets, two months and an Apply footer; each range is one readable chip, and the bounds go to the server as from / to parameters — `kind`, `formatValue`, `calendar`, `matchesSmartFilter`, `isSmartFilterRange`. */
2
2
  import {
3
3
  Card,
4
+ DEFAULT_DATEPICKER_PRESETS,
4
5
  formatDateValue,
5
6
  isSmartFilterRange,
6
7
  matchesSmartFilter,
@@ -17,7 +18,12 @@ const formatRubles = (value: number) => `${value.toLocaleString("ru-RU")} ₽`;
17
18
 
18
19
  const FIELDS: SmartFilterField[] = [
19
20
  { key: "amount", label: "Сумма", kind: "range", formatValue: formatRubles },
20
- { key: "closed", label: "Период", kind: "date-range" },
21
+ {
22
+ key: "closed",
23
+ label: "Период",
24
+ kind: "date-range",
25
+ calendar: { presets: DEFAULT_DATEPICKER_PRESETS, months: 2, prompt: true, footer: true },
26
+ },
21
27
  {
22
28
  key: "stage",
23
29
  label: "Этап",
@@ -68,7 +68,7 @@ Spinner <span role="status">; size and tone
68
68
  ## States
69
69
  | State | Driven by | DOM |
70
70
  |---|---|---|
71
- | appearing | mounted | fades in over `fast` (`internal/enterMotion`), so a spinner that comes and goes never blinks |
71
+ | appearing | mounted into a host already shown | fades in over `fast` (`internal/enterMotion`), so a spinner that comes and goes never blinks; one that mounts with its page stands still |
72
72
  | spinning | mounted | the ring turns at a constant speed (`--prime-motion-loop × 3` per turn, linear) |
73
73
  | reduced motion | `prefers-reduced-motion` | the ring keeps turning, twice as slow (`--prime-motion-loop × 6` per turn) and linear: a loading cue is information, so the token is not zeroed |
74
74
  | size / tone | `size`, `tone` | `data-size` (the explicit size, else the host control tier, else `m`), `data-tone` (only for a non-default tone) |
@@ -35,7 +35,7 @@ Tabs.Root value, size (else the host tier), orientation, tone, f
35
35
  │ │ ├─ Tabs.Count counter Badge, one tier below the tabs size
36
36
  │ │ └─ Tabs.Description muted second line; makes the tab two-line
37
37
  │ └─ Tabs.Separator Divider hairline between groups of tabs
38
- └─ Tabs.Panel role="tabpanel", rendered only while its tab is active; no fill, no padding
38
+ └─ Tabs.Panel role="tabpanel", rendered only while its tab is active (keepMounted: kept hidden once opened); no fill, no padding
39
39
  ```
40
40
 
41
41
  ## API
@@ -120,11 +120,12 @@ Tabs.Root value, size (else the host tier), orientation, tone, f
120
120
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children">` | — | `className` and the other div attributes. |
121
121
 
122
122
  ### Tabs.Panel
123
- `ref` → `HTMLDivElement`. `<div role="tabpanel">`, focusable, rendered only while its tab is active. No fill and no padding: its content is the host's content, aligned with the strip's edges. Its content enters each time it opens.
123
+ `ref` → `HTMLDivElement`. `<div role="tabpanel">`, focusable, rendered only while its tab is active (with `keepMounted`, kept hidden once opened). No fill and no padding: its content is the host's content, aligned with the strip's edges. Content opened after mount enters.
124
124
 
125
125
  | Prop | Type | Default | Description |
126
126
  |---|---|---|---|
127
127
  | `value` | `string` | — (required) | Tab value this panel belongs to. |
128
+ | `keepMounted` | `boolean` | `false` | Once opened, the panel stays mounted and `hidden` while another tab is active: its data, form draft and scroll survive a switch. A panel never opened is not mounted. |
128
129
  | `children` | `ReactNode` | — | Panel content. |
129
130
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className` and the other div attributes. |
130
131
 
@@ -187,13 +188,14 @@ Avoid icons or descriptions on only some tabs (uneven rows; without an icon on e
187
188
  | disabled | `disabled` on Item | native `disabled`, `data-disabled="true"`, `text-disabled`; a removable tab loses its close button |
188
189
  | removable | `onRemove` on Item | item wrapper `data-removable="true"`, close button, tab `aria-keyshortcuts="Delete"` |
189
190
  | closing, confirm pending | `onRemove` returned a promise that has not settled | the close button is `loading` (`aria-busy`); the tab, its selection and focus stay; resolved → the neighbour takes over, rejected → nothing changes |
191
+ | kept | `keepMounted` on Panel, its tab once opened and now inactive | the panel stays mounted with `hidden` (out of the layout, the tab order and the accessibility tree); its tab keeps `aria-controls`; shown again, its content enters as on a first open |
190
192
  | two-line | `Tabs.Description` child | `data-two-line="true"` |
191
193
  | 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` takes labels, counts and close buttons out of the row — a `VisuallyHidden` copy of the label names the tab and a `Tooltip` shows it — only when every tab has an icon |
192
194
  | 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 (the switcher row, a ScrollContainer), faded edges as deep as the hidden part at most (they grow and melt with the scroll, never switch); when the end edge would cut only a tab's empty side, the tabs at `minItemWidth` widen a little (`--tabs-peek`, up to 30%) so a label fades out at the edge and shows that more tabs follow; 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. A column (a wide vertical list) has plain edges |
193
195
 
194
196
  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.
195
197
 
196
- Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-width`, inline `--tabs-item-min` / `--tabs-item-max` when set; the folder (the list's first child, `data-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, and a tab points at its panel (`aria-controls`) only while the panel is in the document; a panel's content enters each time it opens.
198
+ Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-width`, inline `--tabs-item-min` / `--tabs-item-max` when set; the folder (the list's first child, `data-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 (unless `keepMounted`), and a tab points at its panel (`aria-controls`) only while the panel is in the document; a panel's content enters each time it opens after mount (the panel shown on mount stands still).
197
199
 
198
200
  ## Layout & spacing
199
201
  - Tabs are a bar, not a surface: the root has no fill, padding, radius or `data-depth`. On a page or in a `Card.Body`, put it straight into the host's padded region and the panel content straight into `Tabs.Panel`. The strip's edges and the panel content share the host's content edge; the panel stands one tier gap below the strip (`--prime-space-3` … `--prime-space-8`).
@@ -261,7 +263,7 @@ Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-
261
263
  | [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`. |
262
264
  | [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`. |
263
265
  | [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`. |
264
- | [in-drawer.tsx](examples/in-drawer.tsx) | Order details in a Drawer: the tab bar runs along the bottom of the header, the active tab is the body rising into it, and every panel is the body's own content — `Tabs.Root`, `Tabs.List`, `Tabs.Panel`. |
266
+ | [in-drawer.tsx](examples/in-drawer.tsx) | Order details in a Drawer: the tab bar runs along the bottom of the header and scrolls sideways when the tabs outgrow the drawer, the active tab is the body rising into it, every panel is the body's own content, and the delivery form keeps its edits across tabs — `Tabs.Root`, `Tabs.List`, `Tabs.Panel`, `keepMounted`. |
265
267
  | [controlled.tsx](examples/controlled.tsx) | The active tab lives in parent state, e.g. synced with the URL — `value`, `onValueChange`. |
266
268
 
267
269
  ## Mistakes
@@ -274,6 +276,7 @@ Other attributes: Root `data-orientation`, `data-size`, `data-tone`, `data-full-
274
276
  - A hand-made × inside `Tabs.Item` → a button inside a tab button; pass `onRemove` instead.
275
277
  - `onRemove` that does not drop the tab from your list → the tab stays; the kit only moves the selection.
276
278
  - `Tabs.List` without `aria-label` → add one.
279
+ - Lifting a panel's data or form draft to the parent only so it survives a tab switch → `keepMounted` on that `Tabs.Panel`.
277
280
  - Tabs inside `Drawer.Body` / `Modal.Body` under the header → a floating strip under the header's hairline; wrap the header and the body in `Tabs.Root` and put `Tabs.List` into the header.
278
281
 
279
282
  ## Related
@@ -1,4 +1,4 @@
1
- /** Order details in a Drawer: the tab bar runs along the bottom of the header, the active tab is the body rising into it, and every panel is the body's own content — `Tabs.Root`, `Tabs.List`, `Tabs.Panel`. */
1
+ /** Order details in a Drawer: the tab bar runs along the bottom of the header and scrolls sideways when the tabs outgrow the drawer, the active tab is the body rising into it, every panel is the body's own content, and the delivery form keeps its edits across tabs — `Tabs.Root`, `Tabs.List`, `Tabs.Panel`, `keepMounted`. */
2
2
  import { Badge, Button, DescriptionList, Drawer, Fieldset, Input, Tabs } from "prime-ui-kit";
3
3
 
4
4
  const ITEMS = [
@@ -7,6 +7,11 @@ const ITEMS = [
7
7
  { name: "Сборка и подъём на этаж", amount: "1 × 3 200 ₽" },
8
8
  ];
9
9
 
10
+ const DOCUMENTS = [
11
+ { name: "Счёт № 1042-С", date: "3 октября" },
12
+ { name: "Накладная № 1042-Н", date: "4 октября" },
13
+ ];
14
+
10
15
  const HISTORY = [
11
16
  { time: "3 октября, 10:14", event: "Заказ оформлен" },
12
17
  { time: "3 октября, 10:20", event: "Оплата прошла" },
@@ -33,6 +38,7 @@ export default function TabsInDrawerExample() {
33
38
  <Tabs.Count>{ITEMS.length}</Tabs.Count>
34
39
  </Tabs.Item>
35
40
  <Tabs.Item value="delivery">Доставка</Tabs.Item>
41
+ <Tabs.Item value="documents">Документы</Tabs.Item>
36
42
  <Tabs.Item value="history">История</Tabs.Item>
37
43
  </Tabs.List>
38
44
  </Drawer.Header>
@@ -58,7 +64,7 @@ export default function TabsInDrawerExample() {
58
64
  </DescriptionList.Root>
59
65
  </Tabs.Panel>
60
66
 
61
- <Tabs.Panel value="delivery">
67
+ <Tabs.Panel value="delivery" keepMounted>
62
68
  <Fieldset aria-label="Доставка">
63
69
  <Input.Root label="Адрес">
64
70
  <Input.Wrapper>
@@ -73,6 +79,16 @@ export default function TabsInDrawerExample() {
73
79
  </Fieldset>
74
80
  </Tabs.Panel>
75
81
 
82
+ <Tabs.Panel value="documents">
83
+ <DescriptionList.Root>
84
+ {DOCUMENTS.map((document) => (
85
+ <DescriptionList.Item key={document.name} label={document.name}>
86
+ {document.date}
87
+ </DescriptionList.Item>
88
+ ))}
89
+ </DescriptionList.Root>
90
+ </Tabs.Panel>
91
+
76
92
  <Tabs.Panel value="history">
77
93
  <DescriptionList.Root>
78
94
  {HISTORY.map((entry) => (
@@ -142,6 +142,7 @@ Thumbnail.Root frame (a <div>; the child with asChild; a <button role="
142
142
  - In a table the row grows to the thumbnail; never shrink the thumbnail to the row.
143
143
  - Card covers: `fullWidth` inside the card body, the body's gap to the title.
144
144
  - No outline by default: the fill separates the preview.
145
+ - In a leading media slot (`Drawer.Icon`, `Modal.Icon`, `Sheet.Icon`, `Card.Icon`) the thumbnail takes the slot's size (`m`, 40px) whatever `size` it was given, also through a wrapper component of yours; the slot then draws no tile.
145
146
 
146
147
  ## Accessibility
147
148
 
@@ -110,7 +110,7 @@ A chip of tier T is as tall as a menu item of tier T; text, padding and radius s
110
110
  ## States
111
111
  | State | Driven by | DOM |
112
112
  |---|---|---|
113
- | open / closed | hover (mouse, pen) or focus after `delayDuration`; `open` / `defaultOpen` / `onOpenChange` | `data-state="open" \| "closed"` on the chip (closed during the exit) and on the trigger |
113
+ | open / closed | hover (mouse, pen) or keyboard focus after `delayDuration` (not the focus a click causes or a dialog moves in after a click); `open` / `defaultOpen` / `onOpenChange` | `data-state="open" \| "closed"` on the chip (closed during the exit) and on the trigger |
114
114
  | closing | blur, a press on the trigger, Escape, pointer leaving (unless it moves onto the chip) | — |
115
115
  | warm group | another tooltip of the Provider was just shown | `data-instant="true"`: opens and leaves without motion |
116
116
  | side | `side` and the room around the trigger | `data-side` (resolved), `data-align` |
@@ -128,7 +128,7 @@ A chip of tier T is as tall as a menu item of tier T; text, padding and radius s
128
128
 
129
129
  | Key | Action |
130
130
  |---|---|
131
- | `Tab` | Focus on the trigger opens the tooltip after the delay. |
131
+ | `Tab` | Keyboard focus on the trigger opens the tooltip after the delay; focus a click causes, or a layer moves in after a click, does not. |
132
132
  | `Escape` | Hides the tooltip; focus stays on the trigger. |
133
133
 
134
134
  ### ARIA
@@ -137,7 +137,7 @@ A chip of tier T is as tall as a menu item of tier T; text, padding and radius s
137
137
  - The chip is `role="tooltip"`; while open the trigger points at it with `aria-describedby` (appended to the child's own).
138
138
  - `Tooltip.Trigger` composes the child's `ref`, sets `data-state` and chains the child's `onPointerEnter`, `onPointerLeave`, `onPointerDown`, `onFocus` and `onBlur`.
139
139
  - The tooltip is a description, not the name: an icon-only button needs its own `aria-label`.
140
- - The pointer may move onto the chip without closing it (WCAG 1.4.13); a touch does not open it, focus does.
140
+ - The pointer may move onto the chip without closing it (WCAG 1.4.13); a touch does not open it, keyboard focus does.
141
141
  - Tooltip has no `closeOnEscape` / `closeOnOutsideClick` (foundation §8): it closes on pointer-leave, blur, a press on the trigger and Escape, and Escape cannot be turned off — WCAG 1.4.13 requires the chip to be dismissable without moving the pointer or focus.
142
142
  - A `disabled` control (its own prop or a disabled `Fieldset`) is wrapped by the kit in a focusable `<span>`: the host takes hover and Tab, the focus ring is drawn on the control. Around a button the host is that button for assistive tech — `role="button"`, `aria-disabled`, named by the control through `aria-labelledby` — and the control itself leaves the tree.
143
143
  - A trigger that is no tab stop (Badge, Avatar, ProgressCircle, `Icon` with `label`) gets `tabIndex={0}` and the focus ring; its name comes from `aria-label` or the icon `label`.
@@ -51,11 +51,14 @@ The design contract for every component. Tokens live in `tokens/` and are genera
51
51
  perfectly in a row (Button, Input, Select, Datepicker trigger, SegmentedControl, Tabs).
52
52
  5. **Nested radius = outer radius − padding.** Panel 12 with padding 4 → items 8.
53
53
  6. **Air is hierarchy.** Important content gets more space; secondary content is grouped tighter.
54
- 7. **State changes are continuous.** Nothing on screen flips from one state to another. Controls move
55
- into their new state (§7 Motion, rule 8); a region that changes what it shows — loading → data →
54
+ 7. **State changes are continuous; loading never unfolds.** Nothing on screen flips from one state to
55
+ another. Controls move into their new state (§7 Motion, rule 8). Whatever loads is a `Skeleton` in
56
+ the data's geometry from the first paint — never nothing, never a region that grows from zero —
57
+ and the data then replaces it in place: no height glide, no reveal, no slide, at most a short
58
+ cross-fade, the layout settled in one frame. A region swapping between settled states — data →
56
59
  empty → error, one record → another — cross-fades and glides to its new height (`Crossfade`). A
57
- loading region holds the shape of what is coming (`Skeleton` with the data's geometry), so the
58
- swap moves nothing. This holds for every region of every screen, not only for prominent ones.
60
+ region that mounts in its state shows it still. This holds for every region of every screen, not
61
+ only for prominent ones.
59
62
 
60
63
  ## 2. Token layers and naming
61
64
 
@@ -339,8 +342,8 @@ Every component is designed with its micro-animations, not only its static state
339
342
  `padding`, `top/left`; never `transition: all` — list the properties. Where size must animate, prefer
340
343
  `transform` / `scale` (progress fills). The exceptions are a closed list, each with one engine,
341
344
  and `src/styles/motion-perf.test.ts` allows nothing else:
342
- - **height glide** — a region that swaps its content glides to the new height (`Crossfade`, the
343
- Notification stack);
345
+ - **height glide** — a region that swaps its content between settled states glides to the new
346
+ height (`Crossfade`, the Notification stack); a swap into or out of loading never glides;
344
347
  - **row fold** — a part that folds open or closed animates `grid-template-rows` 0fr ↔ 1fr
345
348
  (`Disclosure`: Accordion, DataTable expanded rows);
346
349
  - **text width** — a label that morphs into another text glides its `width` (`MorphText`).
@@ -353,8 +356,11 @@ Every component is designed with its micro-animations, not only its static state
353
356
  8. **State moves, it does not swap.** Checkmark draws/scales in, switch thumb slides, selection indicator
354
357
  (tabs, segmented control) glides to the new item, chevrons rotate, progress fills, counters and fills
355
358
  ease. Two states cross-fade rather than flip when they replace each other in place: a region
356
- swapping its content uses `Crossfade` (inside a component: `RegionSwap`), and its
357
- loading state is a `Skeleton` of the coming content (principle 7).
359
+ swapping its content uses `Crossfade` (inside a component: `RegionSwap`). Its loading state is
360
+ the state `"loading"`: a `Skeleton` of the coming content, drawn from the first paint, which the
361
+ data replaces in place — the height settles in one frame and the fade takes `fast`; only a swap
362
+ between settled states fades on `base` and glides its height (principle 7). Nothing moves on the
363
+ first render of a region.
358
364
  9. **Interruptible.** Toggled state uses CSS transitions (retarget mid-flight), not keyframes. Keyframes only
359
365
  for one-shot enter/exit (overlay presence, spinner, skeleton) and one-shot gestures (icons, rule 13;
360
366
  a field's error shake, rule 14).
@@ -394,7 +400,7 @@ should stay still, and say so in a CSS comment only when the omission is non-obv
394
400
  | selected | `accent-soft` bg + `accent-text`, or check icon in `accent-text` |
395
401
  | disabled | `fill-muted`/`field-bg-disabled` bg, `text-disabled` color, no shadow, `cursor: not-allowed` |
396
402
  | loading (control) | spinner replaces leading icon, label stays, width does not change, `aria-busy` |
397
- | loading (region) | `Skeleton` in the data's geometry inside `Crossfade`, `aria-busy` on the region; data already shown stays during a refresh |
403
+ | loading (region) | `Skeleton` in the data's geometry from the first paint, as the `"loading"` state of `Crossfade` (the data replaces it in place, no height glide), `aria-busy` on the region; data already shown stays during a refresh |
398
404
  | error | `danger-border` inset ring + `danger-text` message; layout does not shift (support row reserves height when the component asks for it) |
399
405
  | empty | centered muted text in `body-s`, optional action; reached through `Crossfade` like every region state |
400
406
 
@@ -292,6 +292,7 @@ A wide cell (`span` 7–11) written next to a narrow one (`span` 4–5) is a pai
292
292
  - The edge air comes from the host: `AppShell.Main` sets `--page-gutter` (the page gutter, 16 → 24 → 32) and `--page-top` (24 → 32 from 1024px); `Page.Root` pads itself with them. Outside a host the page brings no air. The page resets both for what it holds, so a page nested in it starts clean.
293
293
  - `Page.TopBar` steps out of the gutter to the panel's edges and keeps the gutter inside; padding 12 above (or the top safe-area inset) and below; what follows starts 24 under its controls (header → body; the bar's own 12 counts in). The first line is as high as the Sidebar brand row (control m + 8), so the two read as one line. Groups 16 apart on a line, lines 12 apart; tools 8 apart; buttons 8 apart.
294
294
  - `Page.Tools` and `Page.Chips` are the kit's one tool row and chips line (`ToolRow`), the same as the DataTable `toolbar` and `toolbarChips`: the bar only places them. On the first line every tool keeps its natural width; on a line of their own they take the line and lay out from its width (container `prime-tool-row`), never from the window.
295
+ - The tool row lays out the elements it is given, each in a cell: pass the tools as direct children of `Page.Tools` or a fragment. A wrapper component that returns several controls is one cell — no gaps between them, no growing search.
295
296
  - In the bar only the heading's column gives (`minmax(0, 1fr)`): the tools and the actions keep their width. The heading keeps its natural width until every button step is taken; then a heading with a path stacks (the path over the title, the title and the description wrapping), so no level is cut to a fragment; a heading without a path truncates its title.
296
297
  - The plan is measured, never chosen by breakpoints: the bar lays itself out from its own width.
297
298
  - Header (flow) → Body 24; blocks inside Body 24; a `Page.Section` 40 from its neighbours (the body's 24 + 16); tile → tile in a Grid 16 on both axes; section heading → content 16, heading → description 4; flow Title → Description 8; heading → Actions 24 side by side, 16 once they wrap below.
@@ -70,7 +70,7 @@ Sidebar.Root rail wrapper; renders <nav> inside; size tier
70
70
  │ ├─ Sidebar.SubTrigger disclosure row, chevron at the end
71
71
  │ └─ Sidebar.SubContent child Items on a guide line (a flyout on the compact rail)
72
72
  └─ Sidebar.Footer bottom items, the item Toggle, the account
73
- └─ Sidebar.Account avatar + name + muted line; a Dropdown.Trigger child
73
+ └─ Sidebar.Account avatar + name + muted line; a ↕ chevron as a Dropdown.Trigger child
74
74
  useSidebar() state hook for custom parts inside Root (useAppShellNav outside it, in a shell)
75
75
  ```
76
76
 
@@ -220,7 +220,7 @@ useSidebar() state hook for custom parts inside Root (useA
220
220
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "role">` | — | `children` (Items), `className` (on the list) and the other div attributes. |
221
221
 
222
222
  ### Sidebar.Account
223
- `ref` → `HTMLButtonElement`. The signed-in person at the bottom of `Sidebar.Footer`: avatar, name, a muted line and a ↕ chevron. A button, so it is a `Dropdown.Trigger` child; in compact mode only the avatar stays, with the name as a tooltip.
223
+ `ref` → `HTMLButtonElement`. The signed-in person at the bottom of `Sidebar.Footer`: avatar, name, a muted line; a ↕ chevron only as a `Dropdown.Trigger` child. In compact mode only the avatar stays, with the name as a tooltip.
224
224
 
225
225
  | Prop | Type | Default | Description |
226
226
  |---|---|---|---|
@@ -311,6 +311,7 @@ Put `<Sidebar.Toggle variant="header" />` next to `Sidebar.Brand` in the header,
311
311
  | flyout | hover / click / keyboard on a SubTrigger, or on the `…` row of a folded group, in compact mode | the row `aria-haspopup="dialog"`, `aria-expanded`; a Popover `role="dialog"` |
312
312
  | attention dot | a coloured `Sidebar.ItemCount` inside a folded Sub (always on the rail) or a folded Group | `Badge.Dot` with `data-attention="shown" \| "hidden"` on the parent row / heading / `…` row |
313
313
  | persisted | `persistKey` on Root | the mode and the open state of collapsible groups and sub-lists are read from and written to `localStorage` |
314
+ | account menu | `Sidebar.Account` inside a `Dropdown.Trigger` (`aria-haspopup="menu"`) | the ↕ chevron at the end of the row; without a menu (a plain `onClick`, a `Drawer.Trigger`) no chevron |
314
315
  | disabled | `disabled` | Item `disabled` / `aria-disabled`, `data-disabled` |
315
316
 
316
317
  `persistKey` stores collapsible groups and sub-lists by their `id`, else by their label text — give an `id` when labels are translated or change. The stored state wins over `defaultMode` / `defaultOpen`; controlled `mode` / `open` are not stored, and unavailable storage is ignored. A hidden rail is stored as the mode it left, so a reload never strands a person on a hidden rail.
@@ -388,6 +389,7 @@ Leaving the narrow viewport closes the off-canvas panel; activating an item (a l
388
389
  | [collapsible-groups.tsx](examples/collapsible-groups.tsx) | Group headings that fold their items away, one group open at a time, with a chevron at the end of the heading; on the compact rail a folded group becomes one row whose items open in a flyout — `collapsible`, `open`, `onOpenChange`. |
389
390
  | [nested-items.tsx](examples/nested-items.tsx) | A parent item with child items on a guide line: a current child opens it and marks the parent; on the compact rail the children open in a flyout — `Sidebar.Sub`, `Sidebar.SubTrigger`, `Sidebar.SubContent`. |
390
391
  | [account.tsx](examples/account.tsx) | Footer items and the collapse row above the signed-in person: avatar, name and email open the account menu; collapse the rail from that row and only the avatar stays, on the icon axis — `Sidebar.Footer`, `Sidebar.Account`, `description`, `Sidebar.Toggle`. |
392
+ | [account-profile.tsx](examples/account-profile.tsx) | The signed-in person opens a profile panel, not a menu, so the row draws no chevron — `Sidebar.Account`, `description`. |
391
393
  | [router.tsx](examples/router.tsx) | Router links as the brand and the items: the router marks the current link and the item shows it as current; render it inside a router — `asChild`. |
392
394
  | [persist.tsx](examples/persist.tsx) | The first visit opens on the icon rail; the mode and the folded groups are remembered and come back after a reload — `persistKey`, `defaultMode`. |
393
395
  | [custom-part.tsx](examples/custom-part.tsx) | A part of your own in the footer reads the rail state: the storage meter shows on the full rail and in the off-canvas panel and steps aside on the icon rail — `useSidebar`. |
@@ -0,0 +1,66 @@
1
+ /** The signed-in person opens a profile panel, not a menu, so the row draws no chevron — `Sidebar.Account`, `description`. */
2
+ import { Avatar, Button, DescriptionList, Drawer, Icon, Sidebar } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ const PROFILE = [
7
+ { term: "Почта", value: "anna@company.ru" },
8
+ { term: "Роль", value: "Руководитель отдела продаж" },
9
+ { term: "Часовой пояс", value: "Москва, UTC+3" },
10
+ ];
11
+
12
+ export default function SidebarAccountProfileExample() {
13
+ return (
14
+ <div className={styles.stage}>
15
+ <Sidebar.Root offCanvas="never">
16
+ <Sidebar.Content>
17
+ <Sidebar.Item current>
18
+ <Sidebar.ItemIcon>
19
+ <Icon name="nav.home" />
20
+ </Sidebar.ItemIcon>
21
+ Обзор
22
+ </Sidebar.Item>
23
+ <Sidebar.Item>
24
+ <Sidebar.ItemIcon>
25
+ <Icon name="object.document" />
26
+ </Sidebar.ItemIcon>
27
+ Сделки
28
+ </Sidebar.Item>
29
+ </Sidebar.Content>
30
+ <Sidebar.Footer>
31
+ <Drawer.Root>
32
+ <Drawer.Trigger>
33
+ <Sidebar.Account description="anna@company.ru">
34
+ <Avatar.Root color="purple">
35
+ <Avatar.Fallback>АС</Avatar.Fallback>
36
+ </Avatar.Root>
37
+ Анна Смирнова
38
+ </Sidebar.Account>
39
+ </Drawer.Trigger>
40
+ <Drawer.Content size="s">
41
+ <Drawer.Header>
42
+ <Drawer.Title>Анна Смирнова</Drawer.Title>
43
+ <Drawer.Description>Профиль</Drawer.Description>
44
+ </Drawer.Header>
45
+ <Drawer.Body>
46
+ <DescriptionList.Root>
47
+ {PROFILE.map(({ term, value }) => (
48
+ <DescriptionList.Item key={term} label={term}>
49
+ {value}
50
+ </DescriptionList.Item>
51
+ ))}
52
+ </DescriptionList.Root>
53
+ </Drawer.Body>
54
+ <Drawer.Footer>
55
+ <Button.Root variant="outline" tone="neutral">
56
+ Выйти
57
+ </Button.Root>
58
+ </Drawer.Footer>
59
+ </Drawer.Content>
60
+ </Drawer.Root>
61
+ </Sidebar.Footer>
62
+ </Sidebar.Root>
63
+ <div className={styles.content} />
64
+ </div>
65
+ );
66
+ }
@@ -119,7 +119,7 @@ const FIELDS: SmartFilterField[] = [
119
119
  { key: "city", label: "Город", options: [{ value: "msk", label: "Москва" }] },
120
120
  ];
121
121
 
122
- export function OrdersPage() {
122
+ export function DealsPage() {
123
123
  const [section, setSection] = useState("all");
124
124
  const [period, setPeriod] = useState("30d");
125
125
  const [filter, setFilter] = useState<SmartFilterValue>({});
@@ -136,10 +136,10 @@ export function OrdersPage() {
136
136
  <Page.Root>
137
137
  <Page.TopBar>
138
138
  <Page.Heading>
139
- <Page.Title>Заказы</Page.Title>
140
- <Page.Description>128 в работе · 6 ждут оплаты</Page.Description>
139
+ <Page.Title>Сделки</Page.Title>
140
+ <Page.Description>128 в работе · 6,5 млн ₽</Page.Description>
141
141
  </Page.Heading>
142
- <Page.Tools aria-label="Заказы">
142
+ <Page.Tools aria-label="Сделки">
143
143
  <SegmentedControl.Root value={section} onValueChange={setSection} aria-label="Раздел">
144
144
  <SegmentedControl.Item value="all">
145
145
  Все<SegmentedControl.Count>128</SegmentedControl.Count>
@@ -170,14 +170,14 @@ export function OrdersPage() {
170
170
  <Button.Icon>
171
171
  <Icon name="action.add" />
172
172
  </Button.Icon>
173
- Новый заказ
173
+ Новая сделка
174
174
  </Button.Root>
175
175
  </Page.Actions>
176
176
  <Page.Chips>
177
177
  <SmartFilter.Chips />
178
178
  </Page.Chips>
179
179
  </Page.TopBar>
180
- <Page.Body>{/* the orders table */}</Page.Body>
180
+ <Page.Body>{/* the deals as a board or a list: the filter in the bar narrows both views */}</Page.Body>
181
181
  </Page.Root>
182
182
  </SmartFilter.Root>
183
183
  );
@@ -210,6 +210,28 @@ export function OrdersPage() {
210
210
  segments' padding, and one whose items all have icons then collapses to icons (each named by its
211
211
  tooltip) before it scrolls. On a 320px phone two sections with counts fit a line of their own;
212
212
  more, or long labels, scroll — keep the section a person needs first at the start.
213
+ - **Overflow is never fixed by merging.** A tab or a section is one topic; when the topics outgrow
214
+ the line the strip scrolls as above — never two topics joined in one tab («Доступ и история»),
215
+ never the last ones hidden in a «Ещё» of your own, never a second row. Shorten a label to one word
216
+ first; a topic that is rarely opened still keeps its own tab, at the end.
217
+
218
+ ```tsx partial
219
+ // bad — the history folded into «Доступ» because five tabs did not fit the drawer
220
+ <Tabs.Item value="access">Доступ и история</Tabs.Item>
221
+ ```
222
+
223
+ ```tsx good
224
+ // good — every topic its own tab; the strip scrolls when it runs out of width
225
+ <Tabs.Root defaultValue="profile">
226
+ <Tabs.List>
227
+ <Tabs.Item value="profile">Профиль</Tabs.Item>
228
+ <Tabs.Item value="access">Доступ</Tabs.Item>
229
+ <Tabs.Item value="notifications">Уведомления</Tabs.Item>
230
+ <Tabs.Item value="security">Безопасность</Tabs.Item>
231
+ <Tabs.Item value="history">История</Tabs.Item>
232
+ </Tabs.List>
233
+ </Tabs.Root>
234
+ ```
213
235
  - Your duty as its host: give the switcher a bounded width. A flex or grid child that holds one sets
214
236
  `min-width: 0`, otherwise the switcher grows the row instead of scrolling
215
237
  ([foundation §9](reference/foundation.md#9-layout-and-responsiveness)).
package/dist/skill/ux.md CHANGED
@@ -32,19 +32,55 @@ adds text that explains what the layout failed to show.
32
32
  | Records of one shape a person reads, sorts, filters | a table | `DataTable` |
33
33
  | One value in several units or currencies (a deposit in ₽, $, ฿) | the fields side by side in one row under one heading, the unit in each field | `Input.InlineAffix` |
34
34
  | A base value and its overrides (a price and its changes by month) | one list: the base is the first, pinned row | `FieldList` `getLocked` |
35
- | Small attributes of a record (an icon, a color, a flag) | compact controls in the record's row, not fields of their own | icon-only `Select`, `ColorPresets`, `Checkbox` |
35
+ | Small attributes of a record (an icon, a color, a flag) | compact controls in the record's row, not fields of their own | icon-only `Select`, `ColorPresets`, `Switch` |
36
36
  | Short texts in an order the person sets | one column, a handle per row | `FieldList` `reorderable` |
37
37
  | Settings that most people never touch | behind a disclosure, after the common ones | `Accordion`, a «Дополнительно» section |
38
38
  | One of a few options, sent with a form | all visible | `Radio.Group` |
39
39
  | One of a few modes of what the screen shows | all visible | `SegmentedControl` |
40
40
  | One of many options | a list that opens | `Select` |
41
41
  | Many values picked from options | tags | `TagSelect` |
42
+ | A section named after a set of records (sessions, devices, API keys, members) | the records themselves, each with its own actions | `List` with `List.ItemActions` |
42
43
 
43
44
  How many options count as «a few» and the other selection cases: [choosing.md](choosing.md#2-inside-a-category).
44
45
 
45
46
  One record is one row or one block, never half a record per line. If a record does not fit one
46
47
  row, it is not a row: open it in a `Drawer`.
47
48
 
49
+ **A section is what its name says.** A heading that names a set of records — «Сеансы», «Устройства»,
50
+ «Ключи API», «Участники» — promises those records: show them, one row each (where, when, which one is
51
+ this one), with the action on one record in its row and the action on all of them in the section's
52
+ actions. One button standing for the set («Выйти на других устройствах») leaves the person to act on
53
+ records they cannot see. A set that can be empty shows its empty state, never a missing section.
54
+
55
+ ```tsx partial
56
+ // bad — «Сеансы» with nothing but a button
57
+ <Drawer.Section label="Сеансы">
58
+ <Button.Root variant="soft" tone="neutral">Выйти на других устройствах</Button.Root>
59
+ </Drawer.Section>
60
+ ```
61
+
62
+ ```tsx good
63
+ // good — the sessions themselves, an end per row, the end of all of them beside the heading
64
+ <Drawer.Section label="Сеансы">
65
+ <Drawer.Actions>
66
+ <Button.Root variant="soft" tone="neutral">Завершить остальные</Button.Root>
67
+ </Drawer.Actions>
68
+ <List.Root aria-label="Сеансы">
69
+ <List.Item value="mac">
70
+ <List.ItemText>Chrome · macOS</List.ItemText>
71
+ <List.ItemDescription>Москва · это устройство</List.ItemDescription>
72
+ </List.Item>
73
+ <List.Item value="iphone">
74
+ <List.ItemText>Safari · iPhone</List.ItemText>
75
+ <List.ItemDescription>Казань · вчера в 21:40</List.ItemDescription>
76
+ <List.ItemActions>
77
+ <Button.Root size="s" variant="ghost" tone="neutral">Завершить</Button.Root>
78
+ </List.ItemActions>
79
+ </List.Item>
80
+ </List.Root>
81
+ </Drawer.Section>
82
+ ```
83
+
48
84
  ## 3. Label economy
49
85
 
50
86
  Every word on screen must carry something the person does not see already.
@@ -60,7 +96,21 @@ Every word on screen must carry something the person does not see already.
60
96
  beside the number when the row stacks, and a screen reader hears it with the field (the affix
61
97
  describes the input; `aria-label="рублей"` on it when the sign reads badly).
62
98
  - **No text that explains the layout.** «Двигатель, места и скорость всегда первыми» — the lock on
63
- those rows says it. A hint says what the person cannot see (a format, a consequence, a limit).
99
+ those rows says it.
100
+ - **A hint is the few words the label and the control leave out** — two to five: a format («ДД.ММ.ГГГГ»),
101
+ a limit («До 10 МБ»), a consequence nobody would expect («Увидит клиент»). Never what the control
102
+ already shows («Применяется сразу» under a Switch, «Время в CRM показывается в этом поясе» under
103
+ «Часовой пояс»), never the obvious outcome of the action or what stays unchanged («пароль
104
+ останется прежним», «история его действий останется»), never the label again in other words. A
105
+ field that needs no hint has none; a hint that needs a sentence is a missing label or a missing
106
+ control.
107
+
108
+ | bad hint | good |
109
+ |---|---|
110
+ | «Часовой пояс» — «Время в CRM показывается в этом поясе» | no hint |
111
+ | «Звук» (Switch) — «Применяется сразу, без сохранения» | no hint |
112
+ | «Отключить доступ» — «Сотрудник не сможет войти, история его действий останется» | «Входы прекратятся» |
113
+ | «Почта для входа» — «На эту почту придёт ссылка для входа в систему» | «Для входа и уведомлений» |
64
114
  - **One name per thing.** The same action, entity and status keep one word on the whole screen
65
115
  (and across screens): «Счёт», not «Счёт» in the title and «Инвойс» in the button.
66
116
  - **Count the labels.** On a form of repeating rows, labels on screen ≤ number of columns + the
@@ -157,3 +207,48 @@ Code that passes every check can still read badly. Open the screen in a browser
157
207
  button without an object — remove or rewrite it.
158
208
  - **Fill it badly:** the longest real name, an empty list, an error on every field, the slowest
159
209
  load. Nothing overflows, jumps or flips.
210
+
211
+ ## 10. Where a setting lives
212
+
213
+ A setting lives with what it governs, as a field of the form that already holds its neighbours.
214
+
215
+ - **Group by what it changes, not by how it looks.** A signal (a sound, a desktop banner, an e-mail
216
+ digest) changes how the person is told — it goes to the notifications; the theme, the language and
217
+ the time zone change how the app reads — profile preferences; what one record does goes to that
218
+ record. Ask «what would a person open to change this?» and put it there.
219
+ - **No section for one setting.** A section with a single field joins the closest section; a new
220
+ section starts at three related fields.
221
+ - **A stored preference is a field.** A setting with options (the theme: «Системная», «Светлая»,
222
+ «Тёмная») is a `Select` beside its related fields in the profile form — never a `SegmentedControl`,
223
+ which switches what a screen shows right now ([a flag, a level, a set](choosing.md#a-flag-a-level-a-set)).
224
+
225
+ ```tsx partial
226
+ // bad — a section «Оформление» for the theme as a switch, and the dialog sounds next to it
227
+ <Drawer.Section label="Оформление">
228
+ <SegmentedControl.Root label="Тема" defaultValue="system">…</SegmentedControl.Root>
229
+ <Switch.Root><Switch.Label>Звуки диалогов</Switch.Label></Switch.Root>
230
+ </Drawer.Section>
231
+ ```
232
+
233
+ ```tsx good
234
+ // good — the theme is one field of the profile, the sounds live with the notifications
235
+ <>
236
+ <Drawer.Section label="Профиль">
237
+ <Select.Root label="Тема" defaultValue="system">
238
+ <Select.Trigger>
239
+ <Select.Value />
240
+ </Select.Trigger>
241
+ <Select.Content>
242
+ <Select.Item value="system">Системная</Select.Item>
243
+ <Select.Item value="light">Светлая</Select.Item>
244
+ <Select.Item value="dark">Тёмная</Select.Item>
245
+ </Select.Content>
246
+ </Select.Root>
247
+ </Drawer.Section>
248
+ <Drawer.Section label="Уведомления">
249
+ <Switch.Root name="dialogSounds">
250
+ <Switch.Label>Звуки диалогов</Switch.Label>
251
+ </Switch.Root>
252
+ </Drawer.Section>
253
+ </>
254
+ ```
@@ -117,6 +117,7 @@ export declare const semanticTokens: {
117
117
  readonly fg: "{color.white}";
118
118
  readonly soft: "{color.green.100}";
119
119
  readonly text: "{color.green.700}";
120
+ readonly border: "{color.green.500}";
120
121
  };
121
122
  readonly warning: {
122
123
  readonly default: "{color.orange.700}";
@@ -410,7 +411,7 @@ export declare const semanticTokens: {
410
411
  /** State that glides into place (thumbs, indicators, checkmarks); pair with `base`. */
411
412
  readonly emphasized: "{easing.emphasized}";
412
413
  };
413
- /** Step between items of a staggered first render. */
414
+ /** Step between items of a staggered entrance of parts that appear after their page is shown. */
414
415
  readonly stagger: "{duration.80}";
415
416
  /**
416
417
  * Period unit of a loading loop (Spinner turns once in 3 units, 6 under reduced motion). A
@@ -60,6 +60,7 @@ export declare const darkThemeOverrides: {
60
60
  readonly fg: "{color.gray.950}";
61
61
  readonly soft: "color-mix(in srgb, {color.green.500} 16%, transparent)";
62
62
  readonly text: "{color.green.300}";
63
+ readonly border: "{color.green.400}";
63
64
  };
64
65
  readonly warning: {
65
66
  readonly default: "{color.orange.400}";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prime-ui-kit",
3
- "version": "0.20.1",
3
+ "version": "0.21.0",
4
4
  "packageManager": "bun@1.3.13",
5
5
  "description": "React 19 UI kit for product interfaces: 79 composable components on one design contract — forms, selects, tables, overlays, navigation, app shell. CSS Modules, --prime-* tokens, light and dark themes, TypeScript, a11y, Next.js App Router ready. Ships COMPONENT.md docs, copyable examples and an agent skill.",
6
6
  "bin": {
@@ -121,6 +121,7 @@ The soft fill is opaque: the soft hue is composited over the ground the avatar s
121
121
  - Avatar → name: space step 3 (12px) in lists and cards, step 2 (8px) in table cells.
122
122
  - Group: members overlap by an eighth of the diameter (initials are never cut) with a 2px ring in the ground they stand on: the current layer of the surface around them (no surface sets anything), or `--avatar-ring` from a host whose ground is not a layer (a reaction chip sets its own fill); the fills of initials and «+N» are composited over the same ground.
123
123
  - An icon fallback is half the diameter: a kit `Icon` reads `--prime-icon-size`, a raw svg too.
124
+ - In a leading media slot (`Drawer.Icon`, `Modal.Icon`, `Sheet.Icon`, `Card.Icon`) the avatar takes the slot's size (`l`, 40px) whatever `size` it was given, also through a wrapper component of yours; the slot then draws no tile.
124
125
 
125
126
  ## Accessibility
126
127