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
@@ -42,9 +42,10 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
42
42
  - A status of the value under a field («Осталось 12 дней»): `hint` with `hintTone="warning"`, the words carrying the status — not a coloured text of your own, and never `hintTone="danger"` for a validation error (that is `error`).
43
43
  - A unit or a prefix stands in the field's affix (`Input.InlineAffix`, `Input.Affix`) — not in its `label` too: the affix is read with the field.
44
44
  - Copy or call next to a value: `Input.CopyButton`, `Input.Action` inside `Input.Wrapper` — not a Button beside the field; a password: `Input.RevealButton`.
45
+ - A permission or a setting that is on or off: `<Switch.Root>` with its `Switch.Label`, in a saved form too; a `Checkbox` only picks items out of a set, takes a consent or an option of the action sent. A level per entity (access per section): a `FieldList` of `Select`s, not a stack of `SegmentedControl`s; a stored preference (the theme): a `Select` field ([a flag, a level, a set](choosing.md#a-flag-a-level-a-set)).
45
46
  - Records of one shape a person adds and removes (invoice lines, contacts, specs, prices by period): `<FieldList columns={[{ label: "Услуга" }, { label: "Цена", width: "m" }]} value onValueChange getId createItem addLabel renderItem={(item, row) => <>…fields without label…</>} />` — the headings stand once, every field takes its column heading as its label; not a `.map()` of fields with a `label` in every row, not «Позиция 1», «Позиция 2».
46
47
  - Small attributes in a row of fields: an icon-only `Select` (`<Select.Trigger><Select.TriggerIcon>…</Select.TriggerIcon></Select.Trigger>`, no `Select.Value`) and a `ColorPresets` swatch in `width: "compact"` columns — not a full-width Select and a ColorField on a line of their own.
47
- - FieldList row actions: `rowActions="menu"` (one «⋯») for dense rows or touch, the default «×» + handle for short lists reordered often; `variant="surface"` when the list sits among other fields, `plain` when it is the card's content — not a hand-made «⋯» or a Card around the list.
48
+ - FieldList row actions: the default «×» (and the grip with `reorderable`) in sight on every row, however many columns; `rowActions="menu"` only for a `reorderable` list on a touch-first screen — not a «⋯» that holds one «Удалить» ([a menu from two actions](composition.md#a-menu-from-two-actions)); `variant="surface"` when the list sits among other fields, `plain` when it is the card's content — not a Card around the list.
48
49
  - A base value with overrides (a price and its changes by month): one `FieldList`, the base as the first row pinned by `getLocked` — not a separate field above a list in another geometry.
49
50
  - Rows that add up: `FieldList renderTotals={(items) => <>…one node per column…</>}` draws «Итого» under the rows on the same tracks — not a line of text typed under the list, not a DataTable of fields.
50
51
  - One value in several units (a deposit in ₽, $, ฿): the fields side by side in one row under one heading, each unit in `Input.InlineAffix` — not a label per field, not the unit in the label and in the field.
@@ -76,6 +77,7 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
76
77
  ## Buttons and actions
77
78
 
78
79
  - One primary per area (page header, card, dialog): the default `solid` accent, last in the row. Others `variant="soft" tone="neutral"`; dismiss `variant="ghost" tone="neutral"` (pages), `variant="outline" tone="neutral"` (Modal / Drawer footer).
80
+ - One secondary action is in sight — an icon `Button.Root` with a `Tooltip` — and a «⋯» `Dropdown` only gathers two or more ([a menu from two actions](composition.md#a-menu-from-two-actions)).
79
81
  - Destructive is `tone="danger"` — never `tone="error"`; which variant where and how it is confirmed: [api-contract.md](api-contract.md#destructive-actions) and [choosing.md](choosing.md).
80
82
  - Going somewhere is a link: `<LinkButton href>` or `<Button.Root asChild><a href>…</a></Button.Root>` — not `onClick={() => location.assign(url)}`.
81
83
  - Busy action: `<Button.Root loading>` — not a hand-made spinner or «Загрузка…» text.
@@ -85,13 +87,14 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
85
87
 
86
88
  - The app is navigation and the page: everything at the top belongs to the page's `<Page.Root>` — not an app-wide header, a global search or notifications above every page.
87
89
  - A work screen (a list, a board, a chat, a record) titles itself in `<Page.TopBar>`: `<Page.Heading>` with `<Page.Title>` + `<Page.Description>` carrying a line of data («128 в работе · 6 ждут оплаты») — not a sentence about the page.
88
- - The bar holds only its four groups: `<Page.Heading>`, `<Page.Tools aria-label>` (sections, the view, a shared period; `<SmartFilter.Toolbar />` of a one-list page — in reading order), `<Page.Actions>`, `<Page.Chips>` (`<SmartFilter.Chips />`) — not a hand-made flex row of title, tabs, search and buttons ([the top bar](composition.md#the-top-bar)).
90
+ - The bar holds only its four groups: `<Page.Heading>`, `<Page.Tools aria-label>` (sections, the view, a shared period; `<SmartFilter.Toolbar />` only when it filters several views — in reading order), `<Page.Actions>`, `<Page.Chips>` (`<SmartFilter.Chips />`) — not a hand-made flex row of title, tabs, search and buttons ([the top bar](composition.md#the-top-bar)).
89
91
  - A nested record: `<Breadcrumb.Root>` first in `<Page.Heading>`, then `<Page.Title>` with the record's name — the path ends in the title on one line; not a title row under the path, not a breadcrumb in the body. With a router each crumb is `<Breadcrumb.Item asChild>` around its `Link`.
90
92
  - A line of data that loads after the page opens: `<Page.Description loading={!summary}>` from the first render (`Drawer.Description` / `Modal.Description` alike) — not a Description mounted when the data arrives, which moves the title, nor a `Skeleton` placed in it by hand.
91
93
  - A page that edits a record: `<Page.Guard dirty={changed} busy={saving} />` inside `Page.Root` — the Sidebar, BottomNav and path links ask before the changes are lost; not your own `beforeunload` or a confirm wired to each link.
92
94
  - Every button of the page goes into one `<Page.Actions>`, secondary first, the primary last, each with a `<Button.Icon>` beside its label — not buttons split between two places, not a button with only text.
93
95
  - A filtered page: `<SmartFilter.Root>` wraps `<Page.Root>` — it draws nothing; not `SmartFilter.Root` inside the bar.
94
96
  - No size knob on the title: the bar sizes the heading and gives way by width itself ([the five steps](responsive.md#the-pages-top-bar--pagetopbar)).
97
+ - Sign-in, sign-up, an invite and a password reset are modes of one `/login` screen, one `LoginForm` with the action's icon in `LoginForm.Logo` and the default `align` — not a page per mode, not a centred brand mark ([auth](layouts.md#auth-sign-in-code)).
95
98
  - A document page (settings, a long form, help) and an overview (a dashboard) put `<Page.Header>` with its `<Page.Heading>` in the flow — the large title — with an optional `<Page.TopBar collapse>` above it that repeats the title once it scrolls away. One `<Page.Title>` per page — never two titles, never no `<h1>`.
96
99
 
97
100
  ## Cards and surfaces
@@ -109,9 +112,10 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
109
112
 
110
113
  ## Tables
111
114
 
112
- - A list page is a `<DataTable columns rows getRowKey>` under the page's `<SmartFilter.Root fields value onValueChange>` with `<SmartFilter.Toolbar />` in its `Page.Tools` and `<SmartFilter.Chips />` in its `Page.Chips` ([where the filters go](composition.md#where-the-filters-go)); filter rows with `matchesSmartFilter` — not a search Input and a row of Selects above the table.
113
- - A table beside KPI cards or a chart: `<SmartFilter.Root>` around the `<DataTable toolbar={<><SmartFilter.Toolbar /><DataTableColumnsMenu … /></>} toolbarChips={<SmartFilter.Chips />}>` — not its search, filter and «Колонки» in the page's bar above blocks they do not filter ([where the filters go](composition.md#where-the-filters-go)).
114
- - The column chooser is `<DataTable columnsMenu>` over the row actions, else `<DataTableColumnsMenu>` beside the table's search on the same state; `toolbar` takes two or more controls of the table as they are — no wrapper ([controls of a table](composition.md#controls-of-a-table)) — never a toolbar for one button.
115
+ - A table's search, filters, chips, column chooser and selection menu are the DataTable's on every page, the page that is only this table included: `<SmartFilter.Root>` around `<DataTable toolbar={<SmartFilter.Toolbar />} toolbarChips={<SmartFilter.Chips />} renderSelectionActions={…}>`, rows filtered with `matchesSmartFilter` — not in `Page.Tools`, which holds sections, the view and a shared period ([where the filters go](composition.md#where-the-filters-go)).
116
+ - A table section filters by its categorical columns (role, status, type, owner): an `options` field each, a `date-range` field per date column — not a search alone.
117
+ - Actions on selected rows: `renderSelectionActions` returns `Dropdown.Item`s — the table adds «Действия · N» last in its tools row; not a bar that replaces the search, not a button per action ([controls of a table](composition.md#controls-of-a-table)).
118
+ - The column chooser is `<DataTable columnsMenu>` over the row actions, else `<DataTableColumnsMenu>` beside the table's search on the same state; `toolbar` takes the table's controls as they are, a search first — no wrapper ([controls of a table](composition.md#controls-of-a-table)) — never a toolbar for one button.
115
119
  - Search over a table or a list page is always SmartFilter, even with no filter fields yet (`fields={[]}` is a plain search that grows into filters). A search `Input` only outside a filtered list: inside a picker, a settings section, a panel.
116
120
  - Paging is built in: `paging="pages"` (default) with `pageSize`, `paging="infinite"` for feeds, `paging="none"` for short lists — not a separate `<Pagination>` under a DataTable.
117
121
  - Long lists are virtualized inside the kit: pass the whole array to `DataTable` (with `scrollHeight` or `paging="infinite"`; over 100 rows — grouped, `onReorder` and `renderExpanded` bodies render every row, keep those short or paged), `TagSelect options`, `Chat.Messages items`, `Gantt rows` — never slice it by hand, never wrap a kit list in react-window / react-virtual or your own windowing. Big remote data is still paged or loaded on scroll by the server (`source="server"`, `onLoadMore`).
@@ -161,6 +165,7 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
161
165
 
162
166
  - Blocking decision or ≤ 4 fields → `Modal` (`<Modal.Content size="s">` for a confirm); long form, filters, details → `Drawer`; a short task that ends by itself (a date, an action, a note) → `Sheet`; small panel at a control → `Popover`; list of actions → `Dropdown`; a field value → `Select`; a name for an icon button → `Tooltip`; ⌘K → `CommandMenu`; a photo, a scan or a video full screen from its thumbnail → `MediaViewer` (an album in a grid is `Gallery`, which opens it for you; a standalone viewer keeps `open` and `index` in two states) — not a Modal with an `<img>`.
163
167
  - A record's drawer or modal shows its person or object as `Drawer.Icon` / `Modal.Icon` around `Avatar.Root` / `Thumbnail.Root`. A confirm that asks for input (a reason) is a `Modal size="s"` with the field, `Modal.Confirm` and `Modal.Guard busy` — `ConfirmDialog` is a question only.
168
+ - A record opened beside the page is one drawer component per entity, opened from everywhere: permissions make fields `readOnly` and hide tabs and actions; one form over all tabs with one «Сохранить» in `Drawer.Footer` while dirty and `Drawer.Guard`; data loaded by the drawer, not by each tab; the history its own tab; `Drawer.Actions` act on the record, never lead elsewhere ([a record in a drawer](composition.md#a-record-in-a-drawer)).
164
169
  - A titled group in a dialog's body (contacts, deals, history): `<Drawer.Section label="Контакты">` (`Modal.Section`), its button in `Drawer.Actions` inside it — not a `Typography as="h3"` row with buttons of your own.
165
170
  - Never a hand-made `div` backdrop, dialog, menu or tooltip.
166
171
  - Every Modal and Drawer has its `Header` (title, ×); `Body` (content, scrolls) and `Footer` (actions) are there when it has content and actions. Actions always go into the `Footer`: details `<Drawer.Footer><Drawer.Close><Button.Root variant="outline" tone="neutral">Закрыть</Button.Root></Drawer.Close></Drawer.Footer>`, plus the main action last; a form: «Отмена» + submit — not Save at the bottom of the body. A `Sheet` (from the bottom edge) is the opposite: no footer by default — a pick or a swipe ends it — and its header is optional: keep it when the content needs a title, drop it (× and hairline go too) when the content speaks for itself, with `aria-label` on `Sheet.Content`. Never `Drawer.Content side="bottom"`.
@@ -173,13 +178,13 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
173
178
  ## Motion — it comes with the components
174
179
 
175
180
  - Changed button text is new children: `<Button.Root>{saved ? "Сохранено" : "Сохранить"}</Button.Root>` flows in by letters — not two buttons swapped with a condition.
176
- - Saving a form, one rule: the form stays on screen after saving (a Drawer or card that stays open, a settings section) → the button says «Сохранено», no notification; the form goes away (a Drawer or Modal closes, the page navigates) → a notification. A failure is always a notification (or the field's `error`), never only the button.
181
+ - Saving a form, one rule: the save button stays on screen after saving (a settings card) → it says «Сохранено», no notification; the button goes away (a Drawer or Modal closes, the page navigates, a record drawer's save that shows only while dirty) → a notification. A failure is always a notification (or the field's `error`), never only the button.
177
182
  - A long job from a button: `<Button.Root progress={0.42}>Экспорт 42%</Button.Root>` — not a ProgressBar next to it.
178
183
  - A round icon action (play, download, cancel or retry a transfer): `<Button.Root shape="circle" aria-label="Отменить отправку" progress={share}>` — its ring is the progress, it stays pressable; over a picture or video `variant="shade"`. Not a `border-radius: 50%` button of your own, not a ProgressCircle with `onClick`.
179
184
  - One ring in the kit: a share → `ProgressCircle value` (`null` while unknown, `spin` while it grows); an anonymous busy mark → `Spinner`; a ring you press → circle Button `progress`. Never your own `@keyframes` ring or conic gradient.
180
185
  - A single destructive action that is easy to redo by hand (clear a draft, revoke a token), no list and no dialog: `<Button.Root tone="danger" variant="soft" holdToConfirm onConfirm={remove}>` — the action in `onConfirm`, not `onClick`. A row in a list: delete at once and offer «Отменить» in the notification.
181
186
  - Counts: a number child of `Badge` rolls its digits — not a string you re-render with your own fade.
182
- - New password: `<Input.Root strength>`; steps of a whole: `<ProgressBar label="Шаги настройки" steps value={2} max={5} />`; a trend tile: `<Sparkline data label formatValue />`.
187
+ - New password: `<Input.Root strength minStrength={3} hint="…requirement…">` (the policy stops the submit; the server checks it too), a repeat that matches: `valid` — a check, not a «Пароли совпадают» hint ([field outcomes](api-contract.md#field-outcomes-a-mark-for-success-words-for-a-problem)); steps of a whole: `<ProgressBar label="Шаги настройки" steps value={2} max={5} />`; a trend tile: `<Sparkline data label formatValue />`.
183
188
  - A rare milestone only: `celebrate({ origin: event.currentTarget })` — never for a saved form.
184
189
  - No motion on keyboard-driven or high-frequency actions; tokens only, no bounce. Full guide: [motion.md](motion.md).
185
190
 
@@ -188,13 +193,13 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
188
193
  State change is continuous in the whole kit: every region that changes what it shows cross-fades, and loading holds the shape of what is coming.
189
194
 
190
195
  - Every region with loading / data / empty / error (or one record at a time): its content in `<Crossfade state={status} aria-busy={status === "loading"}>` — not `{loading ? <A /> : <B />}` that flips and makes the page jump. DataTable does this itself.
191
- - Region loading: `<Skeleton>` in the geometry of the data — same rows, same gaps (`<Skeleton />` for a text line, `shape="circle"` for an avatar, `shape="control"` for a field, `shape="block"` for an image or chart) — not a spinner in an empty card, not a grey box or shimmer you draw.
196
+ - Region loading: the state `"loading"`, rendered from the first paint (never `null`), a `<Skeleton>` in the geometry of the data — same rows, same gaps (`<Skeleton />` for a text line, `shape="circle"` for an avatar, `shape="control"` for a field, `shape="block"` for an image or chart) — not a spinner in an empty card, not a grey box or shimmer you draw.
192
197
  - `Spinner` only where there is no shape to hold (a running job, a status line); a busy button is `<Button.Root loading>`.
193
198
  - Refreshing data already on screen: keep it, set `aria-busy` — not back to a skeleton on every refetch.
194
199
  - Table states in place: `loading` (+ `loadingRows`), `empty`, `loadError` on `DataTable` — the head and toolbar stay.
195
200
  - First run of a page or region: `EmptyPage.Root` + `EmptyPage.Icon` + `EmptyPage.Title` + `EmptyPage.Description` + `EmptyPage.Actions` with one action — not «Здесь пока ничего нет».
196
201
  - Page-level persistent problem: `<Banner.Root tone="danger">` first in `Page.Body`; the result of an action: `useNotifications().notify({ tone, title })` — a toast never closes the Drawer or Modal it was raised from.
197
- - A refused sign-in: `invalid` on the fields it concerns and the reason in a `<VisuallyHidden live="assertive">` mounted from the start — not a Banner in a `LoginForm`.
202
+ - A refused sign-in: `invalid` on the e-mail and `error="Неверная почта или пароль"` on the password — not a Banner in a `LoginForm`, not red rings and a shake with no words.
198
203
 
199
204
  ## Sidebar
200
205
 
@@ -311,7 +316,7 @@ type InvoicesCardProps = {
311
316
  const PLACEHOLDER_ROWS = ["a", "b", "c"];
312
317
 
313
318
  export function InvoicesCard({ status, invoices, onRetry }: InvoicesCardProps) {
314
- // One key per state: Crossfade cross-fades whenever it changes and glides the height.
319
+ // One key per state: "loading" hands over to the data in place; settled states glide the height.
315
320
  const state = status === "ready" && invoices.length === 0 ? "empty" : status;
316
321
 
317
322
  return (
@@ -10,18 +10,22 @@ checks.
10
10
  - [ ] A nested record's path ends in its title on one line inside `Page.Heading`; no title row under the path ([the top bar](composition.md#the-top-bar)).
11
11
  - [ ] Every button of the page is in one `Page.Actions`, the primary last, each with a `Button.Icon` ([the top bar](composition.md#the-top-bar)).
12
12
  - [ ] `SmartFilter.Root` wraps `Page.Root`; its toolbar is in `Page.Tools`, its chips in `Page.Chips` ([the top bar](composition.md#the-top-bar)).
13
- - [ ] A filter stands next to what it filters: beside KPI cards or a chart, the table's search, filter, chips and chooser are in its `toolbar` / `toolbarChips`; the bar keeps the title, the shared period and the primary action ([where the filters go](composition.md#where-the-filters-go)).
13
+ - [ ] A control stands with what it changes: a table's search, filter, chips and chooser are in its `toolbar` / `toolbarChips` on every page; the bar keeps the title, sections, the view, the shared period and the buttons ([where the filters go](composition.md#where-the-filters-go)).
14
+ - [ ] A table section has an `options` filter field for each categorical column and a `date-range` field per date column, its value in the URL ([where the filters go](composition.md#where-the-filters-go)).
15
+ - [ ] Actions on selected rows are `renderSelectionActions` — one «Действия · N» menu added to the tools row, which stays ([controls of a table](composition.md#controls-of-a-table)).
14
16
  - [ ] A Card only for a standalone titled block — never around a table, a single field or the page ([surfaces](composition.md#4-surfaces)).
15
17
  - [ ] A titled panel carries its heading in its `Card.Header` and stacks in the flow; no heading column beside a panel ([surfaces](composition.md#4-surfaces)).
16
18
  - [ ] A DataTable's row grip and column order follow the rule: grip at the end, columns by `columnOrder` saved with `hiddenColumns` ([choosing](choosing.md#3-pairs-that-are-easy-to-confuse)).
17
- - [ ] The column chooser is `columnsMenu` only over a row-actions column, else `DataTableColumnsMenu` beside the table's search; a DataTable `toolbar` holds two or more table controls, unwrapped, or does not exist ([controls of a table](composition.md#controls-of-a-table)).
19
+ - [ ] The column chooser is `columnsMenu` only over a row-actions column, else `DataTableColumnsMenu` beside the table's search; a DataTable `toolbar` holds the table's controls unwrapped, never a lone button ([controls of a table](composition.md#controls-of-a-table)).
18
20
 
19
21
  ## UX ([ux.md](ux.md))
20
22
  - [ ] I wrote the five-line brief before the JSX ([brief](ux.md#1-brief-before-code)).
21
23
  - [ ] Every entity is laid out by its shape: a record is one row or one block ([data shape](ux.md#2-the-shape-of-the-data-decides-the-layout)).
22
- - [ ] A FieldList picks its `rowActions` (buttons or menu) and its `variant` (plain or surface) by the rules ([forms](composition.md#7-forms)).
24
+ - [ ] A FieldList row shows its «×» in sight whatever its columns; `rowActions="menu"` only on a reorderable touch-first list; `variant` by the rule ([forms](composition.md#7-forms)).
25
+ - [ ] A section named after a set of records shows the records, with an action per row ([data shape](ux.md#2-the-shape-of-the-data-decides-the-layout)).
23
26
  - [ ] Labels on a form of rows ≤ columns + the list label; none repeated, numbered or repeating its heading ([label economy](ux.md#3-label-economy)).
24
- - [ ] No text explains the layout; every hint says what the person cannot see ([label economy](ux.md#3-label-economy)).
27
+ - [ ] No text explains the layout; every hint is two to five words the label and the control leave out, never the obvious outcome ([label economy](ux.md#3-label-economy)).
28
+ - [ ] Every setting sits with what it governs, as a field beside its neighbours; a stored choice is a `Select`, no section holds one setting ([where a setting lives](ux.md#10-where-a-setting-lives)).
25
29
  - [ ] Buttons say the verb and the object, errors say how to fix it, empty states offer the next step ([words](ux.md#4-words)).
26
30
  - [ ] Money, numbers and dates go through `Intl` with `ru-RU`, in Russian typography ([words](ux.md#4-words)).
27
31
  - [ ] The friction of every action matches the cost of a mistake ([friction](ux.md#5-friction-matches-the-cost-of-a-mistake)).
@@ -65,7 +69,12 @@ checks.
65
69
 
66
70
  ## Hierarchy
67
71
  - [ ] One primary action per area, last in its row; the rest `soft` / `ghost` / `outline` neutral ([actions](composition.md#6-actions)).
68
- - [ ] A dialog footer holds Cancel and the primary only; deleting the edited record is the last item of the header ⋯ menu ([actions](composition.md#6-actions)).
72
+ - [ ] A dialog footer holds Cancel and the primary only; deleting the edited record stands in the header's actions ([actions](composition.md#6-actions)).
73
+ - [ ] One secondary action is an icon button with a Tooltip; a «⋯» menu holds two or more ([a menu from two actions](composition.md#a-menu-from-two-actions)).
74
+ - [ ] An entity has one drawer opened from everywhere; permissions lock fields and hide tabs and actions; header actions act on the record, never lead elsewhere ([a record in a drawer](composition.md#a-record-in-a-drawer)).
75
+ - [ ] A record drawer is one form with one «Сохранить» in its footer while dirty and `Drawer.Guard`; no save per section, no unsaved-status text ([a record in a drawer](composition.md#a-record-in-a-drawer)).
76
+ - [ ] A drawer loads its tabs' data itself, the history has its own tab, and returning to a tab never shows a skeleton again ([a record in a drawer](composition.md#a-record-in-a-drawer)).
77
+ - [ ] An on/off permission or setting is a `Switch`, a level per entity a `FieldList` of `Select`s, a set of values in a table cell `Badge.Group overflow="count"` ([a flag, a level, a set](choosing.md#a-flag-a-level-a-set)).
69
78
  - [ ] The same action has the same label and variant everywhere on the screen ([secondary actions](api-contract.md#secondary-actions)).
70
79
 
71
80
  ## States
@@ -74,6 +83,7 @@ checks.
74
83
  - [ ] Fields show `hint` and `error`; only the minority is marked `required` or `optional` ([forms](api-contract.md#forms)).
75
84
  - [ ] Every data block has its loading, empty and error states ([feedback](composition.md#9-feedback)).
76
85
  - [ ] Every region swaps its states through `Crossfade`, loading is a `Skeleton` of the data ([continuity](foundations.md#11-state-changes-are-continuous)).
86
+ - [ ] Whatever loads shows its skeleton on the first render under the state `"loading"`, and the data takes its place without unfolding, sliding or growing from zero ([continuity](foundations.md#11-state-changes-are-continuous)).
77
87
  - [ ] A `Page.Description` that waits for data stays mounted with `loading`, so the title in the bar never jumps ([screen states](composition.md#screen-states)).
78
88
  - [ ] A KPI tile that waits for its figure is `Card.Root loading`, its parts mounted ([inside a category](choosing.md#2-inside-a-category)).
79
89
  - [ ] While a form saves, its submit is `loading` and its fields sit in a `Fieldset disabled` ([forms](api-contract.md#forms)).
@@ -86,6 +96,7 @@ checks.
86
96
  ## Themes and widths
87
97
  - [ ] The screen passes the responsive checklist in both themes ([responsive](responsive.md#checklist-before-handing-a-screen-over)).
88
98
  - [ ] On a phone the Sidebar opens from the menu button the top bar draws; the page title stays in the bar ([app frame](layouts.md#app-frame-once-per-app)).
99
+ - [ ] Tabs that outgrow the width scroll; no two topics merged into one tab ([switchers](responsive.md#switchers)).
89
100
  - [ ] At every width the bar fits: tools on their own line, then icon-only buttons, then «⋯», the primary always on screen ([the page's top bar](responsive.md#the-pages-top-bar--pagetopbar)).
90
101
 
91
102
  ## Keyboard and a11y
@@ -93,5 +104,7 @@ checks.
93
104
  - [ ] Every field has a label (visible, or `aria-label` for a toolbar search) ([forms](api-contract.md#forms)).
94
105
  - [ ] Meaning is never colour alone ([tone and color](foundations.md#6-tone-and-color)).
95
106
  - [ ] A toned hint says its status in words; a validation problem is `error`, never a `danger` hint ([forms](api-contract.md#forms)).
96
- - [ ] A refused sign-in marks its fields `invalid` and says why in a hidden live region, with no Banner ([feedback](composition.md#9-feedback)).
107
+ - [ ] A refused sign-in marks the e-mail `invalid` and says why as the password's `error`, with no Banner ([field outcomes](api-contract.md#field-outcomes-a-mark-for-success-words-for-a-problem)).
108
+ - [ ] A confirmed value is `valid` (a check, no text); a new password has `minStrength` and its requirement in the hint ([field outcomes](api-contract.md#field-outcomes-a-mark-for-success-words-for-a-problem)).
109
+ - [ ] Sign-in, sign-up, invite and reset are modes of one auth screen whose head names the action, `align` left at `start` ([auth](layouts.md#auth-sign-in-code)).
97
110
  - [ ] Custom `labels` replace the default Russian strings where they do not fit ([system strings](api-contract.md#shared-vocabulary-exported-types)).
@@ -40,10 +40,10 @@ under them → its `renderTotals`.
40
40
 
41
41
  | Situation | Component |
42
42
  |---|---|
43
- | one independent yes/no that is submitted with a form | Checkbox |
44
- | one setting that applies immediately | Switch |
43
+ | a capability or a setting that is on or off, in a form or applied at once | Switch ([a flag, a level, a set](#a-flag-a-level-a-set)) |
44
+ | items picked out of a set, a consent, an option of the action being sent | Checkbox |
45
45
  | exactly one of 2–5 visible options in a form | Radio (`Radio.Group` with `label`) |
46
- | exactly one of 2–5 modes that change the view right now | SegmentedControl |
46
+ | exactly one of 2–5 modes that change the view right now — never a stored setting | SegmentedControl |
47
47
  | one of many (> 5) or a long list; icons, descriptions, search, groups | Select (`Select.Content searchable`) |
48
48
  | one record out of thousands for a form field (a client, a contract), searched on the server | Select with `search` / `onSearchChange`, `filter={false}`, `loading`, `hasMore` + `onLoadMore` — never a read-only field that opens a CommandMenu |
49
49
  | one value from a plain text list on a mobile-first form, posted with the form (`name`) | NativeSelect — the OS picker opens on phones |
@@ -61,7 +61,7 @@ under them → its `renderTotals`.
61
61
 
62
62
  **Data.** Status, count, removable value or applied filter → Badge (`tone` for a status, `color` for a
63
63
  category, `onRemove` to remove, `pressed` / `onPressedChange` to toggle); several badges as one value
64
- (a client's tags) → `Badge.Group`. Person or organization → Avatar. Object (product, vehicle, file, cover) → Thumbnail with a
64
+ (a client's tags) → `Badge.Group`, on one line with «+N» in a table cell. Person or organization → Avatar. Object (product, vehicle, file, cover) → Thumbnail with a
65
65
  `ratio` — never a round Avatar for things. Keyboard shortcut → Kbd. One block of related
66
66
  numbers/content → Card (pick the template; a whole tile that opens its details → `Card.Root asChild`
67
67
  with a link). Read-only properties of one record («label — value») → DescriptionList, never a
@@ -147,13 +147,86 @@ the records (tickets, letters, invoices; `current` on the open one; a row's own
147
147
  `Sidebar.Sub`; the signed-in user → `Sidebar.Account` in a `Dropdown.Trigger` at the bottom of
148
148
  `Sidebar.Footer`.
149
149
 
150
+ ### A flag, a level, a set
151
+
152
+ Pick the control by what the value is, never by when it saves or by what fits in the row.
153
+
154
+ | The value | Control |
155
+ |---|---|
156
+ | a capability or a setting that is on or off (a permission, «Звук», «Вход по ссылке») | `Switch` with its label: inside a form it saves with the form, outside one it applies at once and its section has no save |
157
+ | items picked out of a set (rows, recipients, «Выбрать все»), a consent, a one-off option of the action being sent («Отправить копию себе») | `Checkbox` |
158
+ | one level for each of several entities (access per section, a frequency per channel) | one row per entity, its level and scope as `Select`s in columns: a `FieldList` from five entities or whenever most entities have no level — an absent row is «no access», «Добавить раздел» adds one; up to four entities that every record has may stand as one labelled `Select` each |
159
+ | a stored preference with options (the theme, the language, the start page) | a `Select` field beside the related fields (`Radio.Group` for 2–3 options that each need a line of description) |
160
+ | a mode of what this screen shows now (a period, board / list, a section) | `SegmentedControl` — it switches the view and is never a stored value |
161
+ | several values of one record in a table cell (roles, tags, access) | `Badge.Group overflow="count"`: one line, the rest behind a «+N» that lists them — never a string joined by «·» with a «+5» typed by hand |
162
+
163
+ A stack of captioned `SegmentedControl`s, one per entity, makes a person read every caption to find
164
+ the few entities that matter, and fails at 320px; a list of rows names each entity once, in its
165
+ column, and shows only the ones that have a level.
166
+
167
+ ```tsx partial
168
+ // bad — thirteen sections, each a caption, a description and a level switch
169
+ <SegmentedControl.Root label="Сделки" hint="Карточки сделок и воронка" defaultValue="none">
170
+ <SegmentedControl.Item value="none">Нет</SegmentedControl.Item>
171
+ <SegmentedControl.Item value="view">Просмотр</SegmentedControl.Item>
172
+ <SegmentedControl.Item value="edit">Изменение</SegmentedControl.Item>
173
+ <SegmentedControl.Item value="full">Полный</SegmentedControl.Item>
174
+ </SegmentedControl.Root>
175
+ <SegmentedControl.Root label="Клиенты" …>…</SegmentedControl.Root>
176
+ <Checkbox.Root><Checkbox.Label>Экспорт данных</Checkbox.Label></Checkbox.Root>
177
+ ```
178
+
179
+ ```tsx good
180
+ // good — a row per section that has access, the level in a column; a permission flag is a Switch
181
+ // declare: type Grant = { id: string; section: string; level: string }
182
+ // declare: const grants: Grant[]
183
+ // declare: const setGrants: (grants: Grant[]) => void
184
+ <>
185
+ <FieldList
186
+ label="Доступ к разделам"
187
+ columns={[{ label: "Раздел" }, { label: "Уровень", width: "m" }]}
188
+ value={grants}
189
+ onValueChange={setGrants}
190
+ getId={(grant) => grant.id}
191
+ createItem={() => ({ id: crypto.randomUUID(), section: "", level: "view" })}
192
+ addLabel="Добавить раздел"
193
+ renderItem={(grant, row) => (
194
+ <>
195
+ <Select.Root value={grant.section} onValueChange={(section) => row.update({ section })}>
196
+ <Select.Trigger>
197
+ <Select.Value />
198
+ </Select.Trigger>
199
+ <Select.Content>
200
+ <Select.Item value="deals">Сделки</Select.Item>
201
+ <Select.Item value="clients">Клиенты</Select.Item>
202
+ </Select.Content>
203
+ </Select.Root>
204
+ <Select.Root value={grant.level} onValueChange={(level) => row.update({ level })}>
205
+ <Select.Trigger>
206
+ <Select.Value />
207
+ </Select.Trigger>
208
+ <Select.Content>
209
+ <Select.Item value="view">Просмотр</Select.Item>
210
+ <Select.Item value="edit">Изменение</Select.Item>
211
+ <Select.Item value="full">Полный</Select.Item>
212
+ </Select.Content>
213
+ </Select.Root>
214
+ </>
215
+ )}
216
+ />
217
+ <Switch.Root name="export">
218
+ <Switch.Label>Экспорт данных</Switch.Label>
219
+ </Switch.Root>
220
+ </>
221
+ ```
222
+
150
223
  ## 3. Pairs that are easy to confuse
151
224
 
152
225
  | Pair | Rule |
153
226
  |---|---|
154
227
  | List / ChatList / Sidebar / Tabs / Card list / DataTable | List: the records of a list + detail screen that are not conversations (tickets, letters, invoices), one open at a time (`current`), each row a button or a link with a text line, a preview, meta and a count. ChatList: the conversations of a messenger or an inbox. Sidebar: the sections of the **app**. Tabs: panels of **one screen**. `Card.List` (`variant="list"`): static facts in a card, nothing opens. DataTable: many records compared by columns, sorted, filtered, selected in bulk. |
155
228
  | FieldList / DataTable / Fieldset / TagSelect | FieldList: records of one shape a person **edits** in a form, a row of fields each, added and removed by hand, up to four short fields per row. DataTable: records a person **reads**, sorts, filters and selects; edits open a Drawer. Fieldset: **one** record's fields under a heading. TagSelect: values **picked** from options, shown as tags. One value in several units is none of them — fields in one row under one heading. |
156
- | Tabs / SegmentedControl | Tabs switch **panels of content** (navigation inside a screen). SegmentedControl switches a **value or mode** of the same content (period, view mode, filter). |
229
+ | Tabs / SegmentedControl | Tabs switch **panels of content** (navigation inside a screen). SegmentedControl switches a **mode** of the same content (period, view mode, filter) — never a stored setting, which is a `Select` field. |
157
230
  | Modal / Drawer / Sheet / Popover / Tooltip | Modal: blocking decision or a short form (≤ 4 fields) that needs full attention. Drawer: long form, filters, record details beside the page, while keeping page context. Sheet: a short task that ends by itself — a pick, an action, a short note — raised from the bottom edge. Popover: small non-modal panel anchored to a control (quick edit, explanation with a link). Tooltip: a few words naming or explaining a control, no interactive content. |
158
231
  | Select / NativeSelect | Select: the kit's floating list — icons, descriptions, groups, search, `multiple`, `clearable`. NativeSelect: a plain text list where the OS picker is better (phones) or the value must post with a form by `name`. |
159
232
  | Select / Dropdown / CommandMenu / TagSelect | Select picks a **value** for a field (form data). Dropdown runs **actions** (Edit, Duplicate, Delete). CommandMenu searches commands and pages across the app (locally or on the server) — it is not a field. TagSelect picks **several values** as tags. |
@@ -162,7 +235,7 @@ the records (tickets, letters, invoices; `current` on the open one; a row's own
162
235
  | Banner / Notification / Hint | Banner: persistent message in the page flow about a page/section state (sync failed, trial ends). Notification: transient toast about the result of an action (saved, failed). Hint / `error`: help or validation text under one field; a status of its value (a deadline that nears) → `hintTone` on the field, `tone` on a lone Hint. |
163
236
  | ProgressBar / ProgressCircle / Spinner | ProgressBar: a share with width to spare and words beside it (an upload row, an import, a quota line). ProgressCircle: a share on the data scale (24–80) where width is short or a ring reads better (a KPI goal, a ring breakdown, a named import card); `value={null}` while the share is unknown, `spin` while a share grows, an icon in the centre. Spinner: an anonymous «busy» on the icon scale inside a control or a status line, no share, no name of its own. A ring you can press (cancel, retry, download a transfer) → Button `shape="circle"` with `progress`, not any of the three. |
164
237
  | Button / LinkButton | Button does something (submit, open, delete). LinkButton goes somewhere (a URL, «Подробнее», «Все заказы»). |
165
- | Checkbox / Switch / Radio | Checkbox: choice submitted later with a button, can be multiple. Switch: on/off that applies at once (no Save button). Radio: one of several, submitted later. A privacy toggle inside a create form that is submitted → Checkbox. |
238
+ | Checkbox / Switch / Radio | Switch: a capability or a setting that is on or off — a permission, a privacy toggle — whether it saves with a form or at once. Checkbox: items picked out of a set, a consent, a one-off option of the action being sent. Radio: one of several, submitted later ([a flag, a level, a set](#a-flag-a-level-a-set)). |
166
239
  | Gallery / MediaViewer / Thumbnail | Thumbnail: one picture or video of an object in a list, card or picker (selectable with `checked`). Gallery: an album of 1–10 photos and videos laid out by their ratios; it opens MediaViewer itself. MediaViewer: the full-screen viewer on its own, opened from your own thumbnails or a button (`getOrigin` for the zoom out of the tile). A grid of many images to browse → a CSS grid of Thumbnails, not a Gallery. |
167
240
  | Sparkline / ChartFrame | Sparkline: one series as a headline number with its change and a small line or bars you can scrub (`variant="bars"` for signed values). ChartFrame: a panel around a real chart (axes, several series, legend, tooltip, states) drawn by a library or your SVG. Rows to compare → DataTable. |
168
241
  | VoicePlayer / VideoNote | The same player props (`src`, `duration`, `playing`, transcript, `transfer`). VoicePlayer: audio, a waveform seek slider and a playback rate. VideoNote: a round face-to-camera video with a `poster`; alone in `Message.Root` it is the message. A video with controls → `Thumbnail.Video` / `Message.Media`. |
@@ -111,7 +111,7 @@ Say what happens: system messages, progress, loading placeholders, state swaps,
111
111
  | Spinner | An indeterminate loading indicator: the kit ring with the share unknown — a quarter arc turning on a faint track while a request runs. | [COMPONENT.md](reference/components/spinner/COMPONENT.md) · [examples](reference/components/spinner/examples/) |
112
112
  | Skeleton | A placeholder in the shape of the content that is loading — text lines, a control, an avatar, a block — so the layout is in place before the data arrives. | [COMPONENT.md](reference/components/skeleton/COMPONENT.md) · [examples](reference/components/skeleton/examples/) |
113
113
  | EmptyPage | Empty or failed state of a page, a block or a menu: icon, title, explanation and an action — a retry for a failed load. | [COMPONENT.md](reference/components/empty-page/COMPONENT.md) · [examples](reference/components/empty-page/examples/) |
114
- | Crossfade | A region that cross-fades between its states (loading → data → empty → error) and glides to the new height, so the page below does not jump. | [COMPONENT.md](reference/components/crossfade/COMPONENT.md) · [examples](reference/components/crossfade/examples/) |
114
+ | Crossfade | A region that cross-fades between its states (loading → data → empty → error): the data replaces the loading skeleton in place, settled states glide to the new height, so the page below does not jump. | [COMPONENT.md](reference/components/crossfade/COMPONENT.md) · [examples](reference/components/crossfade/examples/) |
115
115
  | VisuallyHidden | Text that screen readers read and nobody sees: the words behind an icon, a badge or a change on the screen. | [COMPONENT.md](reference/components/visually-hidden/COMPONENT.md) · [examples](reference/components/visually-hidden/examples/) |
116
116
 
117
117
  ## Navigation (`navigation`)