prime-ui-kit 0.20.1 → 0.21.1

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 (154) 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/page/Page.d.ts +1 -1
  34. package/dist/layout/sidebar/Sidebar.d.ts +3 -2
  35. package/dist/skill/anti-slop.md +35 -37
  36. package/dist/skill/api-contract.md +50 -2
  37. package/dist/skill/cheatsheet.md +15 -10
  38. package/dist/skill/checklist.md +20 -6
  39. package/dist/skill/choosing.md +82 -8
  40. package/dist/skill/components.md +1 -1
  41. package/dist/skill/composition.md +268 -51
  42. package/dist/skill/foundations.md +17 -6
  43. package/dist/skill/layouts.md +65 -14
  44. package/dist/skill/motion.md +9 -3
  45. package/dist/skill/reference/components/avatar/COMPONENT.md +1 -0
  46. package/dist/skill/reference/components/badge/COMPONENT.md +29 -5
  47. package/dist/skill/reference/components/badge/examples/examples.module.css +14 -0
  48. package/dist/skill/reference/components/badge/examples/overflow-count.tsx +32 -0
  49. package/dist/skill/reference/components/banner/COMPONENT.md +1 -1
  50. package/dist/skill/reference/components/card/COMPONENT.md +8 -4
  51. package/dist/skill/reference/components/card/examples/person-header.tsx +38 -0
  52. package/dist/skill/reference/components/composer/COMPONENT.md +35 -5
  53. package/dist/skill/reference/components/composer/examples/bot-keyboard.tsx +43 -0
  54. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -1
  55. package/dist/skill/reference/components/crossfade/COMPONENT.md +18 -10
  56. package/dist/skill/reference/components/data-table/COMPONENT.md +17 -11
  57. package/dist/skill/reference/components/data-table/examples/screen.tsx +6 -9
  58. package/dist/skill/reference/components/data-table/examples/selection.tsx +60 -18
  59. package/dist/skill/reference/components/drawer/COMPONENT.md +41 -19
  60. package/dist/skill/reference/components/drawer/examples/footer-actions.tsx +54 -0
  61. package/dist/skill/reference/components/drawer/examples/header-actions.tsx +46 -25
  62. package/dist/skill/reference/components/drawer/examples/link-loading.tsx +57 -0
  63. package/dist/skill/reference/components/drawer/examples/screen.tsx +3 -5
  64. package/dist/skill/reference/components/empty-page/COMPONENT.md +2 -2
  65. package/dist/skill/reference/components/field-list/COMPONENT.md +4 -4
  66. package/dist/skill/reference/components/field-list/examples/row-menu.tsx +1 -1
  67. package/dist/skill/reference/components/hint/COMPONENT.md +1 -1
  68. package/dist/skill/reference/components/input/COMPONENT.md +16 -3
  69. package/dist/skill/reference/components/input/examples/password-policy.tsx +65 -0
  70. package/dist/skill/reference/components/list/COMPONENT.md +1 -1
  71. package/dist/skill/reference/components/login-form/COMPONENT.md +13 -11
  72. package/dist/skill/reference/components/login-form/examples/overview.tsx +5 -5
  73. package/dist/skill/reference/components/login-form/examples/states.tsx +7 -6
  74. package/dist/skill/reference/components/message/COMPONENT.md +5 -5
  75. package/dist/skill/reference/components/message/examples/bot-actions.tsx +47 -28
  76. package/dist/skill/reference/components/modal/COMPONENT.md +31 -12
  77. package/dist/skill/reference/components/modal/examples/structure.tsx +2 -4
  78. package/dist/skill/reference/components/segmented-control/COMPONENT.md +11 -6
  79. package/dist/skill/reference/components/segmented-control/examples/router.tsx +22 -0
  80. package/dist/skill/reference/components/select/COMPONENT.md +4 -1
  81. package/dist/skill/reference/components/sheet/COMPONENT.md +31 -12
  82. package/dist/skill/reference/components/sheet/examples/header-actions.tsx +5 -3
  83. package/dist/skill/reference/components/skeleton/COMPONENT.md +3 -2
  84. package/dist/skill/reference/components/smart-filter/COMPONENT.md +2 -1
  85. package/dist/skill/reference/components/smart-filter/examples/ranges.tsx +8 -2
  86. package/dist/skill/reference/components/spinner/COMPONENT.md +1 -1
  87. package/dist/skill/reference/components/tabs/COMPONENT.md +7 -4
  88. package/dist/skill/reference/components/tabs/examples/in-drawer.tsx +18 -2
  89. package/dist/skill/reference/components/thumbnail/COMPONENT.md +1 -0
  90. package/dist/skill/reference/components/tooltip/COMPONENT.md +3 -3
  91. package/dist/skill/reference/foundation.md +15 -9
  92. package/dist/skill/reference/layout/page/COMPONENT.md +10 -6
  93. package/dist/skill/reference/layout/page/examples/router.tsx +124 -0
  94. package/dist/skill/reference/layout/page/examples/tools.tsx +12 -22
  95. package/dist/skill/reference/layout/sidebar/COMPONENT.md +8 -3
  96. package/dist/skill/reference/layout/sidebar/examples/account-profile.tsx +66 -0
  97. package/dist/skill/responsive.md +28 -6
  98. package/dist/skill/ux.md +97 -2
  99. package/dist/tokens/semantic.d.ts +2 -1
  100. package/dist/tokens/themes/dark.d.ts +1 -0
  101. package/package.json +1 -1
  102. package/src/components/avatar/COMPONENT.md +1 -0
  103. package/src/components/badge/COMPONENT.md +29 -5
  104. package/src/components/badge/examples/examples.module.css +14 -0
  105. package/src/components/badge/examples/overflow-count.tsx +32 -0
  106. package/src/components/banner/COMPONENT.md +1 -1
  107. package/src/components/card/COMPONENT.md +8 -4
  108. package/src/components/card/examples/person-header.tsx +38 -0
  109. package/src/components/composer/COMPONENT.md +35 -5
  110. package/src/components/composer/examples/bot-keyboard.tsx +43 -0
  111. package/src/components/confirm-dialog/COMPONENT.md +1 -1
  112. package/src/components/crossfade/COMPONENT.md +18 -10
  113. package/src/components/data-table/COMPONENT.md +17 -11
  114. package/src/components/data-table/examples/screen.tsx +6 -9
  115. package/src/components/data-table/examples/selection.tsx +60 -18
  116. package/src/components/drawer/COMPONENT.md +41 -19
  117. package/src/components/drawer/examples/footer-actions.tsx +54 -0
  118. package/src/components/drawer/examples/header-actions.tsx +46 -25
  119. package/src/components/drawer/examples/link-loading.tsx +57 -0
  120. package/src/components/drawer/examples/screen.tsx +3 -5
  121. package/src/components/empty-page/COMPONENT.md +2 -2
  122. package/src/components/field-list/COMPONENT.md +4 -4
  123. package/src/components/field-list/examples/row-menu.tsx +1 -1
  124. package/src/components/hint/COMPONENT.md +1 -1
  125. package/src/components/input/COMPONENT.md +16 -3
  126. package/src/components/input/examples/password-policy.tsx +65 -0
  127. package/src/components/list/COMPONENT.md +1 -1
  128. package/src/components/login-form/COMPONENT.md +13 -11
  129. package/src/components/login-form/examples/overview.tsx +5 -5
  130. package/src/components/login-form/examples/states.tsx +7 -6
  131. package/src/components/message/COMPONENT.md +5 -5
  132. package/src/components/message/examples/bot-actions.tsx +47 -28
  133. package/src/components/modal/COMPONENT.md +31 -12
  134. package/src/components/modal/examples/structure.tsx +2 -4
  135. package/src/components/segmented-control/COMPONENT.md +11 -6
  136. package/src/components/segmented-control/examples/router.tsx +22 -0
  137. package/src/components/select/COMPONENT.md +4 -1
  138. package/src/components/sheet/COMPONENT.md +31 -12
  139. package/src/components/sheet/examples/header-actions.tsx +5 -3
  140. package/src/components/skeleton/COMPONENT.md +3 -2
  141. package/src/components/smart-filter/COMPONENT.md +2 -1
  142. package/src/components/smart-filter/examples/ranges.tsx +8 -2
  143. package/src/components/spinner/COMPONENT.md +1 -1
  144. package/src/components/tabs/COMPONENT.md +7 -4
  145. package/src/components/tabs/examples/in-drawer.tsx +18 -2
  146. package/src/components/thumbnail/COMPONENT.md +1 -0
  147. package/src/components/tooltip/COMPONENT.md +3 -3
  148. package/src/layout/page/COMPONENT.md +10 -6
  149. package/src/layout/page/examples/router.tsx +124 -0
  150. package/src/layout/page/examples/tools.tsx +12 -22
  151. package/src/layout/sidebar/COMPONENT.md +8 -3
  152. package/src/layout/sidebar/examples/account-profile.tsx +66 -0
  153. package/src/styles/theme-dark.css +1 -0
  154. 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
 
@@ -8,7 +8,8 @@
8
8
  ## When to use
9
9
  - Every route rendered inside `AppShell.Main`: the app is navigation and the page, and the page carries its own top (`Page.TopBar`), title (`Page.Title`, the `<h1>`) and body.
10
10
  - A work screen (a list, a board, a chat, a schedule): `Page.TopBar` with the title (`Page.Heading`), the page's tools (`Page.Tools`: sections, search and filter, view), all its buttons (`Page.Actions`) and the active filters (`Page.Chips`).
11
- - A filter stands next to what it filters. `Page.Tools` holds what acts on the whole page: sections, the view, a period that drives several blocks (the KPI cards and the table). The search, filter, chips and column chooser of a page that is one list stay there too — the list is the page. On a page that holds more than its table (KPI cards, a chart), they belong to the table's own tools row (DataTable `toolbar` + `toolbarChips`), not the bar.
11
+ - A filter stands next to what it filters. `Page.Tools` holds what acts on the whole page: sections, the view, a period that drives several blocks (the KPI cards and the table), and a search and filter only when it narrows several views of one set (a board and its list). A table's search, filter, chips, «Колонки» and selection menu always belong to its own tools row (DataTable `toolbar` + `toolbarChips`), on a page that is only the table too.
12
+ - A section with sub-pages (the «Мессенджеры» item of the Sidebar with «Каналы» and «Статистика»): each sub-page is a route of its own, `Page.Title` stays the section's name, and the switch is a `SegmentedControl` of links (`SegmentedControl.Item asChild`) in `Page.Tools`; the body is the current sub-page. The Sidebar keeps the section's item current on every sub-route — a router `NavLink` to the section's path without `end`, or `current={pathname.startsWith("/messengers")}`.
12
13
  - A document page (settings, a long form, help) or an overview (a dashboard): `Page.Header` in the flow — the large title and its buttons — and `Page.TopBar collapse`, which repeats the title once it scrolls away and keeps the path and the buttons in reach.
13
14
  - A nested record: a `Breadcrumb` path in `Page.Heading`; in the bar the title reads as its last level.
14
15
  - A text page held to a reading measure (`maxWidth="readable"`), a wide dashboard capped at the content width (`maxWidth="wide"`).
@@ -44,8 +45,8 @@ Page.Root the page column (<div>); maxWidth, fill, labe
44
45
  │ │ ├─ Page.Title <h1>, title-m, the path's last level, truncates
45
46
  │ │ └─ Page.Description <p>, muted caption under the whole line, truncates
46
47
  │ ├─ Page.Tools what acts on the whole page, in the order written: the one tool row (as the DataTable toolbar)
47
- │ │ ├─ SegmentedControl.Root sections, view (fullWidth unless set)
48
- │ │ ├─ SmartFilter.Toolbar search and filter: grows first on a line of its own
48
+ │ │ ├─ SegmentedControl.Root sections (route links of sub-pages), view (fullWidth unless set)
49
+ │ │ ├─ SmartFilter.Toolbar a search and filter over several views: grows first on a line of its own
49
50
  │ │ ├─ Datepicker.Root a period (fullWidth unless set)
50
51
  │ │ └─ Select.Root … a period, columns; the trigger fills its place
51
52
  │ ├─ Page.Actions every button of the page; icons, then «⋯» when short
@@ -122,7 +123,7 @@ Page.Root the page column (<div>); maxWidth, fill, labe
122
123
  | `…rest` | `HTMLAttributes<HTMLParagraphElement>` | — | `children`, `className` and the other paragraph attributes. |
123
124
 
124
125
  ### Page.Tools
125
- `ref` → `HTMLDivElement`. What acts on the whole page: sections, a shared period, view; the search of a one-list page. The tool row of the DataTable `toolbar`: a search takes the free room, the rest keep their width at the end. Named, a `group`.
126
+ `ref` → `HTMLDivElement`. What acts on the whole page: sections (route links of sub-pages), view, a shared period; a filter only over several views — a table's go in its `toolbar`. The DataTable tool row: a search takes the free room, the rest keep their width. Named, a `group`.
126
127
 
127
128
  | Prop | Type | Default | Description |
128
129
  |---|---|---|---|
@@ -292,6 +293,7 @@ A wide cell (`span` 7–11) written next to a narrow one (`span` 4–5) is a pai
292
293
  - 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
294
  - `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
295
  - `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.
296
+ - 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
297
  - 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
298
  - The plan is measured, never chosen by breakpoints: the bar lays itself out from its own width.
297
299
  - 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.
@@ -341,7 +343,8 @@ A wide cell (`span` 7–11) written next to a narrow one (`span` 4–5) is a pai
341
343
  | [top-bar.tsx](examples/top-bar.tsx) | A nested record in the top bar: the path ending in the title, a line of data, the page's tools and its buttons — `Page.TopBar`, `Page.Heading`, `Page.Tools`, `Page.Actions`. |
342
344
  | [loading.tsx](examples/loading.tsx) | A page whose figures load after it opens: the line of data keeps its place under the title while it loads, so nothing in the bar moves — `loading`. |
343
345
  | [collapse.tsx](examples/collapse.tsx) | An overview whose large title scrolls away under the top bar: the bar repeats it with its line of data and keeps the period and the page's buttons in reach — `collapse`. |
344
- | [tools.tsx](examples/tools.tsx) | The orders page's tools in its top bar: sections with counts, search and filter, the period, in the order written; the active filters on a line under everything — `Page.Tools`, `Page.Chips`. |
346
+ | [tools.tsx](examples/tools.tsx) | The orders page's tools in its top bar: sections with counts, the search and filter that narrow both views, the view, in the order written; the active filters on a line under everything — `Page.Tools`, `Page.Chips`. |
347
+ | [router.tsx](examples/router.tsx) | A section with sub-pages: the title is the section, each sub-page is a route of its own, the switch in the bar is their links and the body cross-fades between them; render it inside a router — `Page.Tools`, `asChild`. |
345
348
  | [actions-collapse.tsx](examples/actions-collapse.tsx) | The buttons of an invoices page on a tablet-wide column: the line runs short, so the secondary buttons show only their icons with the label as the name and the tooltip, the primary keeps its label — `Page.Actions`, `Button.Icon`. |
346
349
  | [sections.tsx](examples/sections.tsx) | A page with titled sections: tiles of one screen close together, a new topic set off by air, a link at the end of the heading — `Page.Section`, `label`, `description`, `Page.Actions`. |
347
350
  | [grid.tsx](examples/grid.tsx) | A dashboard board: four KPI tiles, a chart two rows high beside two panels, one gap on both axes — `Page.Grid`, `Page.Cell`, `span`, `rows`. |
@@ -360,7 +363,8 @@ A wide cell (`span` 7–11) written next to a narrow one (`span` 4–5) is a pai
360
363
  - Buttons without an icon in a crowded bar → they cannot give way to an icon; give each `Button.Root` a `Button.Icon` beside its label.
361
364
  - Two places for buttons (some beside the title, the primary among the tools) → every button of the page goes into the one `Page.Actions`, the primary last.
362
365
  - `SmartFilter.Root` inside the bar, around `Page.Tools` → it holds no DOM; wrap it around `Page.Root` so its Toolbar (in `Page.Tools`) and its Chips (in `Page.Chips`) share it.
363
- - A table's search, filter, chips or «Колонки» in `Page.Tools` / `Page.Chips` above KPI cards or a chart they do not filter → they read as filters of the whole page; move them into the table's `toolbar` / `toolbarChips`. The bar keeps the title, a period that drives every block, sections and the primary action.
366
+ - A table's search, filter, chips or «Колонки» in `Page.Tools` / `Page.Chips` → they read as filters of the whole page and move when the page grows a block; they go in the table's `toolbar` / `toolbarChips` on every page, the one that is only the table too. The bar keeps the title, sections, the view, a period that drives every block and the page's buttons.
367
+ - Sub-pages of a section as a second Sidebar level, a tab strip inside `Page.Body`, or a title that names the sub-page → the title stays the section, the sub-pages are route links in `Page.Tools` (`SegmentedControl.Item asChild`), and the section's Sidebar item stays current on every sub-route.
364
368
  - `Page.Header` inside `Page.TopBar` → the bar takes `Page.Heading` and `Page.Actions` directly; `Page.Header` is the flow's document header.
365
369
  - A hand-made menu button for the phone Sidebar → the bar draws it while the Sidebar is off-canvas.
366
370
  - Hiding buttons with media queries on a phone → `Page.Actions` in the bar collapses and folds them by measured width.
@@ -0,0 +1,124 @@
1
+ /** A section with sub-pages: the title is the section, each sub-page is a route of its own, the switch in the bar is their links and the body cross-fades between them; render it inside a router — `Page.Tools`, `asChild`. */
2
+ import {
3
+ Button,
4
+ Card,
5
+ Crossfade,
6
+ DataTable,
7
+ type DataTableColumn,
8
+ Icon,
9
+ Page,
10
+ SegmentedControl,
11
+ } from "prime-ui-kit";
12
+ import { Link, Route, Routes, useLocation } from "react-router-dom";
13
+
14
+ type Channel = { id: string; name: string; dialogs: number; reply: string };
15
+
16
+ const SUB_PAGES = [
17
+ { to: "/messengers/channels", label: "Каналы" },
18
+ { to: "/messengers/stats", label: "Статистика" },
19
+ ];
20
+
21
+ const CHANNELS: Channel[] = [
22
+ { id: "tg", name: "Telegram · @prime_support", dialogs: 642, reply: "4 мин" },
23
+ { id: "wa", name: "WhatsApp · +7 495 120-44-10", dialogs: 391, reply: "7 мин" },
24
+ { id: "vk", name: "ВКонтакте · Прайм CRM", dialogs: 188, reply: "12 мин" },
25
+ { id: "avito", name: "Авито · Склад на Южной", dialogs: 59, reply: "25 мин" },
26
+ ];
27
+
28
+ const COLUMNS: DataTableColumn<Channel>[] = [
29
+ { id: "name", header: "Канал", cell: (row) => row.name },
30
+ {
31
+ id: "dialogs",
32
+ header: "Диалогов",
33
+ numeric: true,
34
+ headerAlign: "end",
35
+ cell: (row) => row.dialogs,
36
+ },
37
+ {
38
+ id: "reply",
39
+ header: "Первый ответ",
40
+ numeric: true,
41
+ headerAlign: "end",
42
+ cell: (row) => row.reply,
43
+ },
44
+ ];
45
+
46
+ export default function PageRouterExample() {
47
+ const location = useLocation();
48
+ // The first sub-page answers every other path of the section (here the preview's «/»).
49
+ const current = SUB_PAGES.find((page) => page.to === location.pathname)?.to ?? SUB_PAGES[0].to;
50
+
51
+ return (
52
+ <Page.Root>
53
+ <Page.TopBar sticky={false}>
54
+ <Page.Heading>
55
+ <Page.Title>Мессенджеры</Page.Title>
56
+ <Page.Description>4 канала · 1 280 диалогов за октябрь</Page.Description>
57
+ </Page.Heading>
58
+ <Page.Tools aria-label="Мессенджеры">
59
+ <SegmentedControl.Root value={current} aria-label="Подразделы">
60
+ {SUB_PAGES.map((page) => (
61
+ <SegmentedControl.Item key={page.to} value={page.to} asChild>
62
+ <Link to={page.to}>{page.label}</Link>
63
+ </SegmentedControl.Item>
64
+ ))}
65
+ </SegmentedControl.Root>
66
+ </Page.Tools>
67
+ <Page.Actions>
68
+ <Button.Root>
69
+ <Button.Icon>
70
+ <Icon name="action.add" />
71
+ </Button.Icon>
72
+ Подключить канал
73
+ </Button.Root>
74
+ </Page.Actions>
75
+ </Page.TopBar>
76
+ <Page.Body>
77
+ {/* `location` keeps the leaving sub-page on its own route while it fades out. */}
78
+ <Crossfade state={current}>
79
+ <Routes location={location}>
80
+ <Route
81
+ path="/messengers/stats"
82
+ element={
83
+ <Page.Grid>
84
+ <Page.Cell span={4}>
85
+ <Card.Root variant="stat-trend">
86
+ <Card.Label>Диалогов</Card.Label>
87
+ <Card.Value>1 280</Card.Value>
88
+ <Card.Delta tone="success">+14% к сентябрю</Card.Delta>
89
+ </Card.Root>
90
+ </Page.Cell>
91
+ <Page.Cell span={4}>
92
+ <Card.Root variant="stat-trend">
93
+ <Card.Label>Первый ответ</Card.Label>
94
+ <Card.Value>6 мин</Card.Value>
95
+ <Card.Delta tone="success">на 2 мин быстрее</Card.Delta>
96
+ </Card.Root>
97
+ </Page.Cell>
98
+ <Page.Cell span={4}>
99
+ <Card.Root variant="stat-trend">
100
+ <Card.Label>Без ответа</Card.Label>
101
+ <Card.Value>23</Card.Value>
102
+ <Card.Delta tone="danger">+5 за сутки</Card.Delta>
103
+ </Card.Root>
104
+ </Page.Cell>
105
+ </Page.Grid>
106
+ }
107
+ />
108
+ <Route
109
+ path="*"
110
+ element={
111
+ <DataTable
112
+ columns={COLUMNS}
113
+ rows={CHANNELS}
114
+ getRowKey={(row) => row.id}
115
+ paging="none"
116
+ />
117
+ }
118
+ />
119
+ </Routes>
120
+ </Crossfade>
121
+ </Page.Body>
122
+ </Page.Root>
123
+ );
124
+ }
@@ -1,10 +1,9 @@
1
- /** The orders page's tools in its top bar: sections with counts, search and filter, the period, in the order written; the active filters on a line under everything — `Page.Tools`, `Page.Chips`. */
1
+ /** The orders page's tools in its top bar: sections with counts, the search and filter that narrow both views, the view, in the order written; the active filters on a line under everything — `Page.Tools`, `Page.Chips`. */
2
2
  import {
3
3
  Button,
4
4
  Icon,
5
5
  Page,
6
6
  SegmentedControl,
7
- Select,
8
7
  SmartFilter,
9
8
  type SmartFilterField,
10
9
  type SmartFilterValue,
@@ -18,10 +17,9 @@ const SECTIONS = [
18
17
  { value: "cancelled", label: "Отменены", count: 20 },
19
18
  ];
20
19
 
21
- const PERIODS = [
22
- { value: "7d", label: "7 дней" },
23
- { value: "30d", label: "30 дней" },
24
- { value: "quarter", label: "Квартал" },
20
+ const VIEWS = [
21
+ { value: "list", label: "Список" },
22
+ { value: "board", label: "Доска" },
25
23
  ];
26
24
 
27
25
  const FIELDS: SmartFilterField[] = [
@@ -46,7 +44,7 @@ const FIELDS: SmartFilterField[] = [
46
44
 
47
45
  export default function PageToolsExample() {
48
46
  const [section, setSection] = React.useState("all");
49
- const [period, setPeriod] = React.useState("30d");
47
+ const [view, setView] = React.useState("list");
50
48
  const [filter, setFilter] = React.useState<SmartFilterValue>({
51
49
  city: { include: ["msk"], exclude: [] },
52
50
  });
@@ -75,21 +73,13 @@ export default function PageToolsExample() {
75
73
  ))}
76
74
  </SegmentedControl.Root>
77
75
  <SmartFilter.Toolbar />
78
- <Select.Root value={period} onValueChange={setPeriod}>
79
- <Select.Trigger aria-label="Период">
80
- <Select.TriggerIcon>
81
- <Icon name="field.calendar" />
82
- </Select.TriggerIcon>
83
- <Select.Value />
84
- </Select.Trigger>
85
- <Select.Content>
86
- {PERIODS.map((item) => (
87
- <Select.Item key={item.value} value={item.value}>
88
- {item.label}
89
- </Select.Item>
90
- ))}
91
- </Select.Content>
92
- </Select.Root>
76
+ <SegmentedControl.Root value={view} onValueChange={setView} aria-label="Вид">
77
+ {VIEWS.map((item) => (
78
+ <SegmentedControl.Item key={item.value} value={item.value}>
79
+ {item.label}
80
+ </SegmentedControl.Item>
81
+ ))}
82
+ </SegmentedControl.Root>
93
83
  </Page.Tools>
94
84
  <Page.Actions>
95
85
  <Button.Root>
@@ -26,7 +26,10 @@ same place. Structure it by what people do, not by how the backend is split.
26
26
  it stays folded; its `…` row marks the page).
27
27
  - **One level of nesting.** `Sidebar.Sub` is one section with 2–6 views of the same objects
28
28
  (Сделки → Новые · В работе · Закрытые). The parent only discloses; an overview page is the first
29
- child («Все сделки»). Never a sub-list inside a sub-list — split the section or use page tabs.
29
+ child («Все сделки»). Never a sub-list inside a sub-list — split the section. Facets of one section
30
+ («Мессенджеры» → «Каналы» · «Статистика») are no sub-list: they are route links in the page's
31
+ `Page.Tools`, and the section's item stays `current` on every sub-route (a `NavLink` without `end`,
32
+ or `current={pathname.startsWith("/messengers")}`).
30
33
  - **Icons.** Every top-level item and every sub-list parent has a distinct icon (on the rail the
31
34
  icon is all that is left); children of a sub-list have none.
32
35
  - **Numbers and dots.** A plain `Sidebar.ItemCount` is information (12 new deals): it leaves the
@@ -70,7 +73,7 @@ Sidebar.Root rail wrapper; renders <nav> inside; size tier
70
73
  │ ├─ Sidebar.SubTrigger disclosure row, chevron at the end
71
74
  │ └─ Sidebar.SubContent child Items on a guide line (a flyout on the compact rail)
72
75
  └─ Sidebar.Footer bottom items, the item Toggle, the account
73
- └─ Sidebar.Account avatar + name + muted line; a Dropdown.Trigger child
76
+ └─ Sidebar.Account avatar + name + muted line; a ↕ chevron as a Dropdown.Trigger child
74
77
  useSidebar() state hook for custom parts inside Root (useAppShellNav outside it, in a shell)
75
78
  ```
76
79
 
@@ -220,7 +223,7 @@ useSidebar() state hook for custom parts inside Root (useA
220
223
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "role">` | — | `children` (Items), `className` (on the list) and the other div attributes. |
221
224
 
222
225
  ### 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.
226
+ `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
227
 
225
228
  | Prop | Type | Default | Description |
226
229
  |---|---|---|---|
@@ -311,6 +314,7 @@ Put `<Sidebar.Toggle variant="header" />` next to `Sidebar.Brand` in the header,
311
314
  | 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
315
  | 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
316
  | persisted | `persistKey` on Root | the mode and the open state of collapsible groups and sub-lists are read from and written to `localStorage` |
317
+ | 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
318
  | disabled | `disabled` | Item `disabled` / `aria-disabled`, `data-disabled` |
315
319
 
316
320
  `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 +392,7 @@ Leaving the narrow viewport closes the off-canvas panel; activating an item (a l
388
392
  | [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
393
  | [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
394
  | [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`. |
395
+ | [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
396
  | [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
397
  | [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
398
  | [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`. |