prime-ui-kit 0.18.0 → 0.20.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 (145) hide show
  1. package/README.md +1 -1
  2. package/dist/chunks/{chunk-OAXJWF3L.js → chunk-3PY52JP3.js} +1764 -1764
  3. package/dist/chunks/{chunk-X3JT5CLR.js → chunk-4KONMO7Z.js} +1 -1
  4. package/dist/chunks/{chunk-MB4CXSLL.js → chunk-W6EGTNG6.js} +3772 -2888
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/data-table/ColumnsMenu.d.ts +39 -17
  7. package/dist/components/data-table/DataTable.d.ts +41 -12
  8. package/dist/components/data-table/DataTableColumnsMenu.d.ts +45 -0
  9. package/dist/components/data-table/Head.d.ts +18 -5
  10. package/dist/components/data-table/Row.d.ts +10 -4
  11. package/dist/components/data-table/StateRows.d.ts +5 -3
  12. package/dist/components/data-table/columnOrder.d.ts +14 -0
  13. package/dist/components/data-table/reorder.d.ts +15 -5
  14. package/dist/components/data-table/types.d.ts +5 -0
  15. package/dist/components/data-table/useFrozenColumns.d.ts +6 -2
  16. package/dist/components/datepicker/DatepickerPanel.d.ts +1 -1
  17. package/dist/components/dnd/Dnd.d.ts +9 -5
  18. package/dist/components/dnd/inPlaceRow.d.ts +7 -0
  19. package/dist/components/dnd/useDragSource.d.ts +2 -0
  20. package/dist/components/dnd/useSortableList.d.ts +5 -0
  21. package/dist/components/field-list/FieldList.d.ts +37 -15
  22. package/dist/components/gantt/Gantt.d.ts +1 -1
  23. package/dist/components/index.d.ts +3 -1
  24. package/dist/components/kanban/Kanban.d.ts +4 -0
  25. package/dist/components/list/List.d.ts +9 -4
  26. package/dist/components/scroll-container/ScrollContainer.d.ts +1 -1
  27. package/dist/index.css +1 -1
  28. package/dist/index.js +4 -2
  29. package/dist/internal/ToolRow.d.ts +23 -0
  30. package/dist/internal/leaveGuard.d.ts +6 -0
  31. package/dist/internal/overlay/DialogGuard.d.ts +3 -2
  32. package/dist/internal/overlay/DialogParts.d.ts +14 -5
  33. package/dist/layout/index.d.ts +1 -1
  34. package/dist/layout/page/Page.d.ts +8 -14
  35. package/dist/skill/anti-slop.md +55 -1
  36. package/dist/skill/api-contract.md +2 -2
  37. package/dist/skill/cheatsheet.md +12 -4
  38. package/dist/skill/checklist.md +7 -0
  39. package/dist/skill/choosing.md +1 -0
  40. package/dist/skill/components.md +1 -1
  41. package/dist/skill/composition.md +196 -21
  42. package/dist/skill/layouts.md +1 -1
  43. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -1
  44. package/dist/skill/reference/components/data-table/COMPONENT.md +110 -49
  45. package/dist/skill/reference/components/data-table/examples/actions-column.tsx +154 -0
  46. package/dist/skill/reference/components/data-table/examples/column-order.tsx +152 -0
  47. package/dist/skill/reference/components/data-table/examples/columns-visibility.tsx +18 -18
  48. package/dist/skill/reference/components/data-table/examples/controlled.tsx +32 -17
  49. package/dist/skill/reference/components/data-table/examples/examples.module.css +14 -36
  50. package/dist/skill/reference/components/data-table/examples/narrow.tsx +25 -19
  51. package/dist/skill/reference/components/data-table/examples/reorder.tsx +24 -2
  52. package/dist/skill/reference/components/data-table/examples/row-link.tsx +18 -22
  53. package/dist/skill/reference/components/data-table/examples/screen.tsx +12 -1
  54. package/dist/skill/reference/components/data-table/examples/selection.tsx +13 -18
  55. package/dist/skill/reference/components/data-table/examples/server.tsx +45 -31
  56. package/dist/skill/reference/components/data-table/examples/toolbar.tsx +22 -35
  57. package/dist/skill/reference/components/datepicker/COMPONENT.md +2 -2
  58. package/dist/skill/reference/components/dnd/COMPONENT.md +13 -7
  59. package/dist/skill/reference/components/dnd/examples/examples.module.css +4 -3
  60. package/dist/skill/reference/components/dnd/examples/handle.tsx +5 -5
  61. package/dist/skill/reference/components/dnd/examples/locked.tsx +1 -1
  62. package/dist/skill/reference/components/dnd/examples/table-columns.tsx +75 -0
  63. package/dist/skill/reference/components/dnd/examples/table.tsx +6 -6
  64. package/dist/skill/reference/components/drawer/COMPONENT.md +26 -10
  65. package/dist/skill/reference/components/drawer/examples/header-actions.tsx +1 -1
  66. package/dist/skill/reference/components/drawer/examples/loading.tsx +58 -0
  67. package/dist/skill/reference/components/field-list/COMPONENT.md +78 -47
  68. package/dist/skill/reference/components/field-list/examples/base-row.tsx +2 -1
  69. package/dist/skill/reference/components/field-list/examples/locked.tsx +11 -11
  70. package/dist/skill/reference/components/field-list/examples/narrow.tsx +108 -33
  71. package/dist/skill/reference/components/field-list/examples/reorder.tsx +1 -1
  72. package/dist/skill/reference/components/field-list/examples/row-menu.tsx +71 -0
  73. package/dist/skill/reference/components/field-list/examples/screen.module.css +1 -1
  74. package/dist/skill/reference/components/field-list/examples/screen.tsx +169 -162
  75. package/dist/skill/reference/components/field-list/examples/totals.tsx +9 -8
  76. package/dist/skill/reference/components/field-list/examples/variants.tsx +51 -0
  77. package/dist/skill/reference/components/gantt/COMPONENT.md +4 -4
  78. package/dist/skill/reference/components/kanban/COMPONENT.md +6 -2
  79. package/dist/skill/reference/components/kanban/examples/states.tsx +7 -5
  80. package/dist/skill/reference/components/list/COMPONENT.md +21 -11
  81. package/dist/skill/reference/components/list/examples/reorder.tsx +42 -7
  82. package/dist/skill/reference/components/modal/COMPONENT.md +23 -10
  83. package/dist/skill/reference/components/scroll-container/COMPONENT.md +1 -1
  84. package/dist/skill/reference/components/segmented-control/COMPONENT.md +3 -2
  85. package/dist/skill/reference/components/sheet/COMPONENT.md +18 -7
  86. package/dist/skill/reference/components/smart-filter/COMPONENT.md +2 -2
  87. package/dist/skill/reference/layout/page/COMPONENT.md +13 -17
  88. package/dist/skill/reference/layout/page/examples/screen.module.css +1 -1
  89. package/dist/skill/reference/layout/page/examples/screen.tsx +93 -104
  90. package/dist/skill/reference/layout/page/examples/sections.tsx +2 -15
  91. package/dist/skill/responsive.md +14 -10
  92. package/dist/tokens/semantic.d.ts +2 -2
  93. package/dist/tokens/themes/dark.d.ts +1 -1
  94. package/package.json +1 -1
  95. package/src/components/confirm-dialog/COMPONENT.md +1 -1
  96. package/src/components/data-table/COMPONENT.md +110 -49
  97. package/src/components/data-table/examples/actions-column.tsx +154 -0
  98. package/src/components/data-table/examples/column-order.tsx +152 -0
  99. package/src/components/data-table/examples/columns-visibility.tsx +18 -18
  100. package/src/components/data-table/examples/controlled.tsx +32 -17
  101. package/src/components/data-table/examples/examples.module.css +14 -36
  102. package/src/components/data-table/examples/narrow.tsx +25 -19
  103. package/src/components/data-table/examples/reorder.tsx +24 -2
  104. package/src/components/data-table/examples/row-link.tsx +18 -22
  105. package/src/components/data-table/examples/screen.tsx +12 -1
  106. package/src/components/data-table/examples/selection.tsx +13 -18
  107. package/src/components/data-table/examples/server.tsx +45 -31
  108. package/src/components/data-table/examples/toolbar.tsx +22 -35
  109. package/src/components/datepicker/COMPONENT.md +2 -2
  110. package/src/components/dnd/COMPONENT.md +13 -7
  111. package/src/components/dnd/examples/examples.module.css +4 -3
  112. package/src/components/dnd/examples/handle.tsx +5 -5
  113. package/src/components/dnd/examples/locked.tsx +1 -1
  114. package/src/components/dnd/examples/table-columns.tsx +75 -0
  115. package/src/components/dnd/examples/table.tsx +6 -6
  116. package/src/components/drawer/COMPONENT.md +26 -10
  117. package/src/components/drawer/examples/header-actions.tsx +1 -1
  118. package/src/components/drawer/examples/loading.tsx +58 -0
  119. package/src/components/field-list/COMPONENT.md +78 -47
  120. package/src/components/field-list/examples/base-row.tsx +2 -1
  121. package/src/components/field-list/examples/locked.tsx +11 -11
  122. package/src/components/field-list/examples/narrow.tsx +108 -33
  123. package/src/components/field-list/examples/reorder.tsx +1 -1
  124. package/src/components/field-list/examples/row-menu.tsx +71 -0
  125. package/src/components/field-list/examples/screen.module.css +1 -1
  126. package/src/components/field-list/examples/screen.tsx +169 -162
  127. package/src/components/field-list/examples/totals.tsx +9 -8
  128. package/src/components/field-list/examples/variants.tsx +51 -0
  129. package/src/components/gantt/COMPONENT.md +4 -4
  130. package/src/components/kanban/COMPONENT.md +6 -2
  131. package/src/components/kanban/examples/states.tsx +7 -5
  132. package/src/components/list/COMPONENT.md +21 -11
  133. package/src/components/list/examples/reorder.tsx +42 -7
  134. package/src/components/modal/COMPONENT.md +23 -10
  135. package/src/components/scroll-container/COMPONENT.md +1 -1
  136. package/src/components/segmented-control/COMPONENT.md +3 -2
  137. package/src/components/sheet/COMPONENT.md +18 -7
  138. package/src/components/smart-filter/COMPONENT.md +2 -2
  139. package/src/layout/page/COMPONENT.md +13 -17
  140. package/src/layout/page/examples/screen.module.css +1 -1
  141. package/src/layout/page/examples/screen.tsx +93 -104
  142. package/src/layout/page/examples/sections.tsx +2 -15
  143. package/src/styles/theme-dark.css +1 -1
  144. package/src/styles/theme-light.css +1 -1
  145. package/dist/components/data-table/context.d.ts +0 -14
@@ -1,8 +1,9 @@
1
- /** A CRM deal list the server sorts, pages and searches; a new search returns to page 1 — `source`, `rowCount`, `sort`, `page`, `loading`. */
1
+ /** A CRM deal list the server sorts, pages and searches, with its tools in its own row — search, filter and column chooser, the active filters under them; a new search returns to page 1 — `source`, `rowCount`, `sort`, `page`, `loading`, `toolbar`, `toolbarChips`. */
2
2
  import {
3
3
  Badge,
4
4
  DataTable,
5
5
  type DataTableColumn,
6
+ DataTableColumnsMenu,
6
7
  type DataTableSortState,
7
8
  matchesSmartFilter,
8
9
  SmartFilter,
@@ -11,8 +12,6 @@ import {
11
12
  } from "prime-ui-kit";
12
13
  import * as React from "react";
13
14
 
14
- import styles from "./examples.module.css";
15
-
16
15
  type Stage = "new" | "talks" | "won" | "lost";
17
16
  type Deal = { id: number; client: string; manager: string; stage: Stage; amount: number };
18
17
  type DealsRequest = {
@@ -79,7 +78,15 @@ const RUB = new Intl.NumberFormat("ru-RU", {
79
78
  });
80
79
 
81
80
  const COLUMNS: DataTableColumn<Deal>[] = [
82
- { id: "id", header: "Сделка", accessor: "id", sortable: true, numeric: true, align: "start" },
81
+ {
82
+ id: "id",
83
+ header: "Сделка",
84
+ accessor: "id",
85
+ sortable: true,
86
+ numeric: true,
87
+ align: "start",
88
+ hideable: false,
89
+ },
83
90
  { id: "client", header: "Клиент", accessor: "client", sortable: true },
84
91
  { id: "manager", header: "Менеджер", accessor: "manager", sortable: true },
85
92
  {
@@ -125,6 +132,7 @@ export default function DataTableServerExample() {
125
132
  const [search, setSearch] = React.useState("");
126
133
  const [sort, setSort] = React.useState<DataTableSortState>({ columnId: "amount", order: "desc" });
127
134
  const [page, setPage] = React.useState(1);
135
+ const [hidden, setHidden] = React.useState<string[]>([]);
128
136
  const [result, setResult] = React.useState<DealsPage>({ rows: [], total: 0 });
129
137
  const [loading, setLoading] = React.useState(true);
130
138
 
@@ -153,33 +161,39 @@ export default function DataTableServerExample() {
153
161
  };
154
162
 
155
163
  return (
156
- <DataTable
157
- source="server"
158
- columns={COLUMNS}
159
- rows={result.rows}
160
- rowCount={result.total}
161
- getRowKey={(row) => row.id}
162
- pageSize={PAGE_SIZE}
163
- page={page}
164
- onPageChange={setPage}
165
- sort={sort}
166
- onSortChange={setSort}
167
- loading={loading}
168
- empty="Сделок не найдено — измените запрос или фильтры"
169
- toolbar={
170
- <SmartFilter.Root
171
- fields={FIELDS}
172
- value={filter}
173
- onValueChange={changeFilter}
174
- search={search}
175
- onSearchChange={changeSearch}
176
- >
177
- <div className={styles.filterColumn}>
164
+ <SmartFilter.Root
165
+ fields={FIELDS}
166
+ value={filter}
167
+ onValueChange={changeFilter}
168
+ search={search}
169
+ onSearchChange={changeSearch}
170
+ >
171
+ <DataTable
172
+ source="server"
173
+ columns={COLUMNS}
174
+ hiddenColumns={hidden}
175
+ rows={result.rows}
176
+ rowCount={result.total}
177
+ getRowKey={(row) => row.id}
178
+ pageSize={PAGE_SIZE}
179
+ page={page}
180
+ onPageChange={setPage}
181
+ sort={sort}
182
+ onSortChange={setSort}
183
+ loading={loading}
184
+ empty="Сделок не найдено — измените запрос или фильтры"
185
+ toolbar={
186
+ <>
178
187
  <SmartFilter.Toolbar />
179
- <SmartFilter.Chips />
180
- </div>
181
- </SmartFilter.Root>
182
- }
183
- />
188
+ <DataTableColumnsMenu
189
+ columns={COLUMNS}
190
+ hiddenColumns={hidden}
191
+ onHiddenColumnsChange={setHidden}
192
+ />
193
+ </>
194
+ }
195
+ toolbarChips={<SmartFilter.Chips />}
196
+ />
197
+ </SmartFilter.Root>
184
198
  );
185
199
  }
@@ -1,7 +1,6 @@
1
- /** A campaign list with search, a status filter and export above the table; the empty text when nothing matches — `toolbar`, `empty`. */
1
+ /** A campaign table with its own tools row: the search takes the free room, the status switch keeps its width at the end, and on a narrow table they wrap; the empty text when nothing matches — `toolbar`, `empty`. */
2
2
  import {
3
3
  Badge,
4
- Button,
5
4
  DataTable,
6
5
  type DataTableColumn,
7
6
  Icon,
@@ -10,8 +9,6 @@ import {
10
9
  } from "prime-ui-kit";
11
10
  import * as React from "react";
12
11
 
13
- import styles from "./examples.module.css";
14
-
15
12
  type Status = "active" | "paused" | "archived";
16
13
  type Campaign = { id: string; name: string; status: Status; clicks: number; ctr: number };
17
14
 
@@ -87,37 +84,27 @@ export default function DataTableToolbarExample() {
87
84
  pageSize={5}
88
85
  empty="Ничего не найдено — измените запрос или фильтр"
89
86
  toolbar={
90
- <div className={styles.toolbar}>
91
- <div className={styles.filters}>
92
- <Input.Root size="s" className={styles.search}>
93
- <Input.Wrapper>
94
- <Input.Icon side="start">
95
- <Icon name="action.search" />
96
- </Input.Icon>
97
- <Input.Field
98
- type="search"
99
- placeholder="Поиск кампаний"
100
- aria-label="Поиск кампаний"
101
- value={query}
102
- onValueChange={setQuery}
103
- />
104
- </Input.Wrapper>
105
- </Input.Root>
106
- <SegmentedControl.Root
107
- size="s"
108
- aria-label="Статус"
109
- value={status}
110
- onValueChange={setStatus}
111
- >
112
- <SegmentedControl.Item value="all">Все</SegmentedControl.Item>
113
- <SegmentedControl.Item value="active">Активные</SegmentedControl.Item>
114
- <SegmentedControl.Item value="paused">На паузе</SegmentedControl.Item>
115
- </SegmentedControl.Root>
116
- </div>
117
- <Button.Root variant="outline" tone="neutral" size="s">
118
- Экспорт
119
- </Button.Root>
120
- </div>
87
+ <>
88
+ <Input.Root>
89
+ <Input.Wrapper>
90
+ <Input.Icon side="start">
91
+ <Icon name="action.search" />
92
+ </Input.Icon>
93
+ <Input.Field
94
+ type="search"
95
+ placeholder="Поиск кампаний"
96
+ aria-label="Поиск кампаний"
97
+ value={query}
98
+ onValueChange={setQuery}
99
+ />
100
+ </Input.Wrapper>
101
+ </Input.Root>
102
+ <SegmentedControl.Root aria-label="Статус" value={status} onValueChange={setStatus}>
103
+ <SegmentedControl.Item value="all">Все</SegmentedControl.Item>
104
+ <SegmentedControl.Item value="active">Активные</SegmentedControl.Item>
105
+ <SegmentedControl.Item value="paused">На паузе</SegmentedControl.Item>
106
+ </SegmentedControl.Root>
107
+ </>
121
108
  }
122
109
  />
123
110
  );
@@ -78,7 +78,7 @@ panel presets column (range) · 1–2 month grids · promp
78
78
  | `name` | `string` | — | Submits the value with a form through hidden inputs: `yyyy-MM-dd` (`yyyy-MM-ddTHH:mm` with `withTime`); a range submits two values under the name, `from` then `to` (empty for an open bound). |
79
79
  | `aria-label · aria-labelledby · aria-describedby` | `string` | — | Name without a `label` (the value is appended) and extra description ids. |
80
80
  | `months` | `1 \| 2` | `1` | Months side by side; one when there is no room. |
81
- | `presets` | `DatepickerPreset[] \| false` | `false` | Preset column (`range` only): `DEFAULT_DATEPICKER_PRESETS` or picks from `datepickerPresets`; a row above the calendar when narrow. |
81
+ | `presets` | `DatepickerPreset[] \| false` | `false` | Preset column (`range` only): `DEFAULT_DATEPICKER_PRESETS` (ends with «Всё время») or picks from `datepickerPresets`; a row above the calendar when narrow. |
82
82
  | `prompt` | `boolean` | `false` | Step prompt under the calendar (`labels.pickStart` / `pickEnd` / `pickDate`). |
83
83
  | `footer` | `boolean` | `false` | Footer with date fields and Reset / Apply; without it a pick applies at once. |
84
84
  | `withTime` | `boolean` | `false` | Time fields in the footer (00:00 — 23:59 by default). |
@@ -103,7 +103,7 @@ panel presets column (range) · 1–2 month grids · promp
103
103
  | `onValueChange` | `(value) => void` | — | Called when a pick, a preset, Apply or Reset changes the value. |
104
104
  | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | host tier, else `"m"` | Panel tier (day cell 24 · 28 · 32 · 36 · 40). Without it the tier of its host, else `m`. |
105
105
  | `months` | `1 \| 2` | `1` | Months side by side; one when there is no room. |
106
- | `presets` | `DatepickerPreset[] \| false` | `false` | Preset column (`range` only): `DEFAULT_DATEPICKER_PRESETS` or picks from `datepickerPresets`; a row above the calendar when narrow. |
106
+ | `presets` | `DatepickerPreset[] \| false` | `false` | Preset column (`range` only): `DEFAULT_DATEPICKER_PRESETS` (ends with «Всё время») or picks from `datepickerPresets`; a row above the calendar when narrow. |
107
107
  | `prompt` | `boolean` | `false` | Step prompt under the calendar (`labels.pickStart` / `pickEnd` / `pickDate`). |
108
108
  | `footer` | `boolean` | `false` | Footer with date fields and Reset / Apply; without it a pick applies at once. |
109
109
  | `withTime` | `boolean` | `false` | Time fields in the footer (00:00 — 23:59 by default). |
@@ -11,6 +11,7 @@
11
11
  - Moving an item between lists to an exact position: a board of columns (several `Dnd.Sortable` with one `kind`; Alt + ← / → moves across from the keyboard).
12
12
  - A list with a pinned first row or a system item that must hold its place (`locked`).
13
13
  - Reordering the rows of a table of your own (`as="tbody"`; a group's heading row in `header`). For records in a [DataTable](../data-table/COMPONENT.md) use its `onReorder`.
14
+ - Reordering the columns of a table of your own (`as="tr"` on the head row; the body follows the order on the drop). For a [DataTable](../data-table/COMPONENT.md) use its `columnOrder`.
14
15
  - Moving an item onto a target with no inner order: a folder, an assignee (`Dnd.Draggable` + `Dnd.DropZone`).
15
16
 
16
17
  ## When not to use
@@ -29,8 +30,8 @@ import { Dnd, moveBefore } from "prime-ui-kit";
29
30
  Dnd.Root session owner: overlay (portal), live region, labels — mount once
30
31
  ├─ Dnd.Sortable list container and drop target; renders items in the drawn order
31
32
  │ ├─ header optional content before the items that never moves (a group's heading row)
32
- │ ├─ Dnd.SortableItem one item (li in ul/ol, div otherwise); the gap is drawn before it
33
- │ │ └─ Dnd.Handle optional grip: a ghost icon-only Button (with `handle` on the list); a lock in a `locked` item; busy while the item's move is saved
33
+ │ ├─ Dnd.SortableItem one item (li in ul/ol, tr in tbody, th in tr, div otherwise); the gap is drawn before it
34
+ │ │ └─ Dnd.Handle optional grip, last in the item: a ghost icon-only Button (with `handle` on the list); a lock in a `locked` item; busy while the item's move is saved
34
35
  │ ├─ empty the `empty` content while the list has no items
35
36
  │ └─ children optional content after the items that never moves (an add tile)
36
37
  ├─ Dnd.Draggable an item to carry elsewhere; stays in place, faded, until the drop
@@ -67,25 +68,26 @@ Generic `Dnd.Sortable<T>`; `ref` → `HTMLElement`. The list and its drop target
67
68
  | `kind` | `string` | unique per list | Lists sharing a `kind` exchange items (a board): an item from another list arrives in `onReorder` with an `id` not in `items`. Alt + the arrows across the axis move it by keyboard. Name each list: it is spoken. |
68
69
  | `canDrop` | `(id: string) => boolean \| string` | — | Refuses an item by id: `false`, or a string that says why (spoken and shown under the carried copy). The list turns `danger` before the release. |
69
70
  | `empty` | `ReactNode` | — | Shown while the list has no items, inside a dashed drop slot while the list takes drops; fades out when a gap opens in the list, so the gap takes its place. |
70
- | `as` | `"div" \| "ul" \| "ol" \| "tbody"` | `"div"` | Root element; `ul` / `ol` render items as `li`, `tbody` as table rows `tr` (the gap is a row across every column, the carried copy keeps every cell's width). |
71
- | `header` | `ReactNode` | — | Content in front of the items that never moves and takes no drop position: the heading row of a group in a `tbody`. |
71
+ | `as` | `"div" \| "ul" \| "ol" \| "tbody" \| "tr"` | `"div"` | Root element; `ul` / `ol` render items as `li`, `tbody` as rows `tr` (a gap row, a one-row copy), `tr` as head cells `th` with `axis="x"` (cells keep their columns, the gap glides among them). |
72
+ | `header` | `ReactNode` | — | Content in front of the items that never moves and takes no drop position: the heading row of a group in a `tbody`, the leading cells of a head row. |
72
73
  | `aria-label` | `string` | — | Accessible name of the list. |
73
74
  | `…rest` | `Omit<HTMLAttributes<HTMLElement>, "children">` | — | `className` (the list layout), `aria-label` (the name of the list) and the other attributes of the list element. |
74
75
 
75
76
  ### Dnd.SortableItem
76
- `ref` → `HTMLElement`. `<li>` inside `ul` / `ol`, `<tr>` inside `tbody`, `<div>` otherwise; throws outside `Dnd.Sortable`. One item; presses on controls inside never drag. Without `handle` it takes the keys, and a named `<div>` item is a `button`.
77
+ `ref` → `HTMLElement`. `<li>` inside `ul` / `ol`, `<tr>` inside `tbody`, `<th>` inside `tr`, `<div>` otherwise; throws outside `Dnd.Sortable`. One item; presses on controls inside never drag. Without `handle` it takes the keys, and a named `<div>` item is a `button`.
77
78
 
78
79
  | Prop | Type | Default | Description |
79
80
  |---|---|---|---|
80
81
  | `id` | `string` | — (required) | The item id (same as `getId`). |
81
82
  | `disabled` | `boolean` | `false` | This item cannot be dragged or moved (others may still move past it). |
82
83
  | `locked` | `boolean` | `false` | Holds its place: it does not move and nothing is put where it would shift it. Its `Dnd.Handle` turns into a lock. A pinned first row, a system column. |
84
+ | `dragFromControls` | `boolean` | `false` | Without `handle`: a press on a control inside picks the item up too once it travels (a finger: holds); a click stays the control's. A table header whose sort button fills it. |
83
85
  | `className` | `string` | — | The row look: fill, radius, padding. |
84
86
  | `children` | `ReactNode` | — | The row content. |
85
87
  | `…rest` | `Omit<HTMLAttributes<HTMLElement>, "id">` | — | `aria-labelledby` / `aria-describedby` (the visible title and description) and the other attributes. |
86
88
 
87
89
  ### Dnd.Handle
88
- `ref` → `HTMLButtonElement` (`HTMLSpanElement` in a `locked` item). The grip: a ghost icon-only `xs` Button that starts the drag and takes the item's keys; busy while the item's move is saved. In a `locked` item it is a lock.
90
+ `ref` → `HTMLButtonElement` (`HTMLSpanElement` in a `locked` item). The grip: a ghost icon-only `xs` Button that starts the drag and takes the item's keys; busy while saving. In a `locked` item it is a lock. Place it last in the item, after its controls.
89
91
 
90
92
  | Prop | Type | Default | Description |
91
93
  |---|---|---|---|
@@ -148,6 +150,7 @@ Returns `items` with `id` moved in front of `beforeId` (`null` = last): the body
148
150
  | `div` | a flex column / row / grid of `div` items | most lists | yes |
149
151
  | `ul` · `ol` | the same with `li` items | a list that reads as one to assistive tech | |
150
152
  | `tbody` | table rows (`tr` items) in the table's own layout; the gap is a row across every column; the row flies inside a one-row copy of its table at its column widths | rows of a table (`axis="y"`) | |
153
+ | `tr` | a head row whose cells (`th` items) are the columns: every cell keeps its column, so the body stays still while a header is carried; the carried cell stays in the layout as the gap — the same dashed, tinted slot as every sortable gap, drawn inside the cell — and it and the others glide by transforms to where they will stand; the copy is the head cell alone in a one-cell copy of the table; the body takes the new order on the drop | reorderable columns of a table (`axis="x"`); keep each column's width by id across the reorder so the head cells land where they were drawn (DataTable does) | |
151
154
 
152
155
  ### Flags
153
156
  | Value | Looks like | Use when | Default |
@@ -155,6 +158,7 @@ Returns `items` with `id` moved in front of `beforeId` (`null` = last): the body
155
158
  | `handle` | only the grip starts a drag; the row keeps its controls | rows with buttons, switches, text | `false` |
156
159
  | `kind` shared | lists exchange items at an exact position; Alt + ← / → moves across | boards | unique per list |
157
160
  | `locked` (item) | a lock where the grip would be; the item holds its place, nothing moves past it | a pinned first row, a system column | `false` |
161
+ | `dragFromControls` (item) | without `handle`: a press on a control inside picks the item up once it travels (a finger: holds); a click stays the control's, and the click that ends a drag is swallowed | a table header whose sort button fills it | `false` |
158
162
  | `empty` | the list's own line inside a dashed drop slot while it is empty (no slot when the list is `disabled`); a gap opening takes its place | columns, lists that may run out | — |
159
163
  | `canDrop` → string | the refusal says why: live region and a chip under the clone | workflow rules a person must understand | — |
160
164
  | `flashOnDrop` | after a drop the zone's fill starts at `accent-soft` and fades back to its own | the eye must follow where the item went | `false` |
@@ -201,6 +205,7 @@ An item's own `onPointerDown` / `onKeyDown` run before the kit's: `preventDefaul
201
205
  ## Layout & spacing
202
206
  - The distance between items is `--prime-space-2`; a host sets `--dnd-sortable-gap` on the list for another one.
203
207
  - A grid (`axis="grid"`) is `display: grid`: the host sets the tracks (`grid-template-columns`) and the cells' size; the gap takes a cell, and a carried item resizes to the cell it would land in.
208
+ - A `Dnd.Handle` ends its item: the last child, after the item's own trailing controls (a remove button), at the inline end (on the left in RTL); a `locked` item's lock takes the same place, so the grips of every row stand on one line. In a table it is the last cell. An on / off switch leads the item (its first child), so its state reads first and nothing stands between the text and the grip.
204
209
  - The reason chip stands `--prime-tooltip-offset` under the carried copy, start-aligned, at most `--prime-tooltip-max-width` wide.
205
210
  - The look of a row is the consumer's: give `Dnd.SortableItem` / `Dnd.Draggable` a radius on its own class (the gap and the clone's fill take it) and keep the row's look within its ancestors up to the nearest surface (`data-depth`) — the clone flies inside empty copies (`display: contents`) of exactly those, so their variables and selectors still apply. A table row flies in a one-row copy of its table inside them.
206
211
  - A scrollable ancestor under the pointer auto-scrolls within 64 px of its edge (a `ScrollContainer` strip works).
@@ -251,12 +256,13 @@ An item's own `onPointerDown` / `onKeyDown` run before the kit's: `preventDefaul
251
256
  | Example | Shows |
252
257
  |---|---|
253
258
  | [overview.tsx](examples/overview.tsx) | Project tasks reordered by dragging the whole row; one root above, the move applied with a helper — `Dnd.Root`, `Dnd.Sortable`, `onReorder`, `moveBefore`. |
254
- | [handle.tsx](examples/handle.tsx) | Notification channels with switches: only the grip starts a drag, and the async save can roll the order back — `handle`, `Dnd.Handle`, `onReorder`. |
259
+ | [handle.tsx](examples/handle.tsx) | Notification channels, each switch leading its row and the grip ending it: only the grip starts a drag, and the async save can roll the order back — `handle`, `Dnd.Handle`, `onReorder`. |
255
260
  | [grid.tsx](examples/grid.tsx) | Product photos in a wrapping grid: a photo carried across rows lands in reading order, the gap takes a cell, Alt + ← / → steps it one place — `axis`. |
256
261
  | [locked.tsx](examples/locked.tsx) | Sections of a workspace menu: the home section is pinned first with a lock in place of its grip, the rest reorder around nothing past it; each row is named by its visible title — `locked`, `handle`, `aria-labelledby`. |
257
262
  | [board.tsx](examples/board.tsx) | A ticket board: columns share one kind, so a ticket lands in another column at an exact position or moves there with Alt + ← / →, an empty column says so, and a full column refuses it before the release with the reason under the ticket — `kind`, `canDrop`, `empty`, `onReorder`. |
258
263
  | [drop-zones.tsx](examples/drop-zones.tsx) | Documents carried onto folders: a zone takes items by kind and gets the document as data, a locked folder refuses before the release and says why under the carried file, a syncing folder is no target at all, a document on signing stays put, the line above follows the drag, the target flashes where the file landed — `Dnd.Draggable`, `Dnd.DropZone`, `data`, `canDrop`, `disabled`, `onDragStart`, `onDragEnd`, `onEnter`, `onLeave`, `flashOnDrop`. |
259
264
  | [table.tsx](examples/table.tsx) | A project estimate as a table: each stage is its own body of rows under a heading row that never moves, a row is carried by its grip within a stage or into another, and the rows stay table rows — `as`, `header`, `kind`, `handle`. |
265
+ | [table-columns.tsx](examples/table-columns.tsx) | A sales report whose columns change places: the head row is a sortable list, a header is carried alone while the rows stay still and the gap opens among the headers where it lands, and the month column holds its place — `as`, `axis`, `locked`. |
260
266
  | [states.tsx](examples/states.tsx) | A contract's approval route: a step already signed stays where it is while the rest reorder around it, and once the route is approved the whole list holds still — `Dnd.SortableItem`, `Dnd.Sortable`, `disabled`. |
261
267
  | [narrow.tsx](examples/narrow.tsx) | A row of status filters in a 320 px strip: tags reorder sideways and the strip scrolls when a tag is held near its edge — `axis`. |
262
268
 
@@ -18,9 +18,9 @@
18
18
  box-shadow: var(--prime-shadow-raised);
19
19
  }
20
20
 
21
- /* The grip sits closer to the edge: it is a square button with its own inner room. */
21
+ /* The grip ends the row and sits closer to the edge: a square button with its own inner room. */
22
22
  .handleRow {
23
- padding-inline-start: var(--prime-space-2);
23
+ padding-inline-end: var(--prime-space-2);
24
24
  }
25
25
 
26
26
  .rowText {
@@ -134,9 +134,10 @@
134
134
  text-align: end;
135
135
  }
136
136
 
137
+ /* The grip column ends the row. */
137
138
  .grip {
138
139
  inline-size: var(--prime-space-10);
139
- padding-inline-end: 0;
140
+ padding-inline-start: 0;
140
141
  }
141
142
 
142
143
  /* A stage's heading row: a fill marks where a new stage starts. */
@@ -1,4 +1,4 @@
1
- /** Notification channels with switches: only the grip starts a drag, and the async save can roll the order back — `handle`, `Dnd.Handle`, `onReorder`. */
1
+ /** Notification channels, each switch leading its row and the grip ending it: only the grip starts a drag, and the async save can roll the order back — `handle`, `Dnd.Handle`, `onReorder`. */
2
2
  import { Dnd, moveBefore, Switch, Typography } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
@@ -37,10 +37,6 @@ export default function DndHandleExample() {
37
37
  }}
38
38
  renderItem={(channel) => (
39
39
  <Dnd.SortableItem id={channel.id} className={styles.handleRow}>
40
- <Dnd.Handle />
41
- <Typography as="span" variant="body-m" className={styles.rowText}>
42
- {channel.title}
43
- </Typography>
44
40
  <Switch.Root
45
41
  aria-label={channel.title}
46
42
  checked={channel.enabled}
@@ -50,6 +46,10 @@ export default function DndHandleExample() {
50
46
  )
51
47
  }
52
48
  />
49
+ <Typography as="span" variant="body-m" className={styles.rowText}>
50
+ {channel.title}
51
+ </Typography>
52
+ <Dnd.Handle />
53
53
  </Dnd.SortableItem>
54
54
  )}
55
55
  />
@@ -40,7 +40,6 @@ export default function DndLockedExample() {
40
40
  aria-labelledby={`${uid}-${section.id}`}
41
41
  aria-describedby={`${uid}-${section.id}-note`}
42
42
  >
43
- <Dnd.Handle aria-label={`Переместить раздел «${section.title}»`} />
44
43
  <span className={styles.rowText}>
45
44
  <Typography as="span" variant="body-m" id={`${uid}-${section.id}`}>
46
45
  {section.title}
@@ -49,6 +48,7 @@ export default function DndLockedExample() {
49
48
  {section.description}
50
49
  </Typography>
51
50
  </span>
51
+ <Dnd.Handle aria-label={`Переместить раздел «${section.title}»`} />
52
52
  </Dnd.SortableItem>
53
53
  )}
54
54
  />
@@ -0,0 +1,75 @@
1
+ /** A sales report whose columns change places: the head row is a sortable list, a header is carried alone while the rows stay still and the gap opens among the headers where it lands, and the month column holds its place — `as`, `axis`, `locked`. */
2
+ import { Dnd, moveBefore, Typography } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ type Month = { id: string; month: string; deals: number; revenue: string; average: string };
8
+ type Column = { id: keyof Month; title: string; numeric: boolean; locked: boolean };
9
+
10
+ const COLUMNS: Column[] = [
11
+ { id: "month", title: "Месяц", numeric: false, locked: true },
12
+ { id: "deals", title: "Сделки", numeric: true, locked: false },
13
+ { id: "revenue", title: "Выручка", numeric: true, locked: false },
14
+ { id: "average", title: "Средний чек", numeric: true, locked: false },
15
+ ];
16
+
17
+ const MONTHS: Month[] = [
18
+ { id: "jul", month: "Июль", deals: 42, revenue: "3 150 000 ₽", average: "75 000 ₽" },
19
+ { id: "aug", month: "Август", deals: 37, revenue: "2 960 000 ₽", average: "80 000 ₽" },
20
+ { id: "sep", month: "Сентябрь", deals: 51, revenue: "4 080 000 ₽", average: "80 000 ₽" },
21
+ ];
22
+
23
+ export default function DndTableColumnsExample() {
24
+ const [columns, setColumns] = React.useState(COLUMNS);
25
+
26
+ return (
27
+ <Dnd.Root>
28
+ <table className={styles.table}>
29
+ <caption className={styles.caption}>
30
+ <Typography as="span" variant="title-s">
31
+ Продажи по месяцам
32
+ </Typography>
33
+ </caption>
34
+ <thead>
35
+ <Dnd.Sortable
36
+ as="tr"
37
+ axis="x"
38
+ aria-label="Колонки отчёта"
39
+ items={columns}
40
+ getId={(column) => column.id}
41
+ getLabel={(column) => column.title}
42
+ onReorder={(id, beforeId) =>
43
+ setColumns((current) => moveBefore(current, id, beforeId, (column) => column.id))
44
+ }
45
+ renderItem={(column) => (
46
+ <Dnd.SortableItem
47
+ id={column.id}
48
+ locked={column.locked}
49
+ scope="col"
50
+ className={column.numeric ? styles.numeric : styles.cell}
51
+ >
52
+ <Typography as="span" variant="caption" tone="muted">
53
+ {column.title}
54
+ </Typography>
55
+ </Dnd.SortableItem>
56
+ )}
57
+ />
58
+ </thead>
59
+ <tbody>
60
+ {MONTHS.map((row) => (
61
+ <tr key={row.id}>
62
+ {columns.map((column) => (
63
+ <td key={column.id} className={column.numeric ? styles.numeric : styles.cell}>
64
+ <Typography as="span" variant="body-m">
65
+ {row[column.id]}
66
+ </Typography>
67
+ </td>
68
+ ))}
69
+ </tr>
70
+ ))}
71
+ </tbody>
72
+ </table>
73
+ </Dnd.Root>
74
+ );
75
+ }
@@ -51,9 +51,6 @@ export default function DndTableExample() {
51
51
  </caption>
52
52
  <thead>
53
53
  <tr>
54
- <th scope="col" className={styles.grip}>
55
- <VisuallyHidden>Порядок</VisuallyHidden>
56
- </th>
57
54
  <th scope="col" className={styles.cell}>
58
55
  <Typography as="span" variant="caption" tone="muted">
59
56
  Работа
@@ -64,6 +61,9 @@ export default function DndTableExample() {
64
61
  Часы
65
62
  </Typography>
66
63
  </th>
64
+ <th scope="col" className={styles.grip}>
65
+ <VisuallyHidden>Порядок</VisuallyHidden>
66
+ </th>
67
67
  </tr>
68
68
  </thead>
69
69
  {stages.map((stage) => (
@@ -90,9 +90,6 @@ export default function DndTableExample() {
90
90
  }
91
91
  renderItem={(line) => (
92
92
  <Dnd.SortableItem id={line.id}>
93
- <td className={styles.grip}>
94
- <Dnd.Handle />
95
- </td>
96
93
  <td className={styles.cell}>
97
94
  <Typography as="span" variant="body-m">
98
95
  {line.title}
@@ -103,6 +100,9 @@ export default function DndTableExample() {
103
100
  {line.hours}
104
101
  </Typography>
105
102
  </td>
103
+ <td className={styles.grip}>
104
+ <Dnd.Handle />
105
+ </td>
106
106
  </Dnd.SortableItem>
107
107
  )}
108
108
  />
@@ -81,7 +81,7 @@ No DOM, no ref. State and dismiss policy.
81
81
  | `…rest` | `HTMLAttributes<HTMLElement>` | — | `className` and the other attributes of the `<header>`. |
82
82
 
83
83
  ### Drawer.Actions
84
- `ref` → `HTMLDivElement`. A row of controls at the end of the header, before the close button: a status Badge, a ⋯ Dropdown, an icon button; or at the end of a `Drawer.Section` heading. Controls inside take tier `s`.
84
+ `ref` → `HTMLDivElement`. A row of controls at the end of the header, before the close button: a status Badge, a ⋯ Dropdown; under the title when it would get less than 12rem; or at the end of a `Drawer.Section` heading. Controls inside take tier `s`.
85
85
 
86
86
  | Prop | Type | Default | Description |
87
87
  |---|---|---|---|
@@ -96,12 +96,20 @@ No DOM, no ref. State and dismiss policy.
96
96
  | `children` | `ReactNode` | — (required) | An `Icon` (sized to the m icon), or the record's picture: an `Avatar.Root` (sized to the tile) or a `Thumbnail.Root`. |
97
97
  | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other attributes of the tile. |
98
98
 
99
- ### Drawer.Title · Drawer.Description
100
- `ref` → `HTMLHeadingElement` / `HTMLParagraphElement`. `<h2>` (title-m) / `<p>` (body-s, muted); their ids name and describe the dialog.
99
+ ### Drawer.Title
100
+ `ref` → `HTMLHeadingElement`. `<h2>` (title-m); its id names the dialog.
101
101
 
102
102
  | Prop | Type | Default | Description |
103
103
  |---|---|---|---|
104
- | `…rest` | `Omit<HTMLAttributes<HTMLHeadingElement \| HTMLParagraphElement>, "id">` | — | `className`, `children` and the other attributes except `id` (the dialog owns it). |
104
+ | `…rest` | `Omit<HTMLAttributes<HTMLHeadingElement>, "id">` | — | `className`, `children` and the other attributes except `id` (the dialog owns it). |
105
+
106
+ ### Drawer.Description
107
+ `ref` → `HTMLParagraphElement`. `<p>` (body-s, muted) under the title; its id describes the dialog.
108
+
109
+ | Prop | Type | Default | Description |
110
+ |---|---|---|---|
111
+ | `loading` | `boolean` | `false` | Its data loads: a `Skeleton` holds the line's place and `aria-busy` is set, so the header never moves; the data fades in. |
112
+ | `…rest` | `Omit<HTMLAttributes<HTMLParagraphElement>, "id">` | — | `className`, `children` and the other attributes except `id` (the dialog owns it). |
105
113
 
106
114
  ### Drawer.Body
107
115
  `ref` → `HTMLDivElement`. The only scrolling zone (a ScrollContainer), 16 gap between blocks.
@@ -111,7 +119,7 @@ No DOM, no ref. State and dismiss policy.
111
119
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className`, `children` and the other attributes of the zone. |
112
120
 
113
121
  ### Drawer.Section
114
- `ref` → `HTMLElement`. A titled `<section>` of the body: an `<h3>` (title-s, under the `<h2>` title), an optional description, a `Drawer.Actions` at the end of the heading row; the content 16 below, sections 32 apart. Fields group in a `Fieldset`.
122
+ `ref` → `HTMLElement`. A titled `<section>` of the body: an `<h3>` (title-s, under the `<h2>` title), an optional description, a `Drawer.Actions` at the end of the heading row; the content 16 below, sections 32 apart, in any wrapper. Fields group in a `Fieldset`.
115
123
 
116
124
  | Prop | Type | Default | Description |
117
125
  |---|---|---|---|
@@ -124,7 +132,7 @@ No DOM, no ref. State and dismiss policy.
124
132
 
125
133
  | Prop | Type | Default | Description |
126
134
  |---|---|---|---|
127
- | `layout` | `"fill" \| "end"` | `"end"` | `fill`: equal-width buttons in one row; `end`: auto width, at the end. Stacked on phones (viewport below 640 px) and in a dialog narrower than 360 px. |
135
+ | `layout` | `"fill" \| "end"` | `"end"` | `fill`: equal-width buttons in one row, wrapping rather than cutting a label; `end`: auto width, at the end. Stacked on phones (viewport below 640 px) and in a dialog narrower than 360 px. |
128
136
  | `…rest` | `HTMLAttributes<HTMLElement>` | — | `className`, `children` and the other attributes of the `<footer>`. |
129
137
 
130
138
  ### Drawer.Meta
@@ -135,7 +143,7 @@ No DOM, no ref. State and dismiss policy.
135
143
  | `…rest` | `HTMLAttributes<HTMLParagraphElement>` | — | `className`, `children` and the other attributes of the `<p>`. |
136
144
 
137
145
  ### Drawer.Guard
138
- No DOM, no ref. Holds the dialog from anywhere inside Content, where the form state lives; renders only the built-in «discard changes?» confirm. Closing from code is never held.
146
+ No DOM, no ref. Holds the dialog from anywhere inside Content, where the form state lives; renders only the built-in «discard changes?» confirm. While `busy` or `dirty` the browser also asks before the tab closes or reloads. Closing from code is never held.
139
147
 
140
148
  | Prop | Type | Default | Description |
141
149
  |---|---|---|---|
@@ -186,6 +194,9 @@ Below 640px of viewport every size is full width and loses its corners.
186
194
  | closing | `open` turns false | the panel keeps showing the children it had open while it slides out |
187
195
  | busy | `Drawer.Guard busy` | Escape, the scrim, the close button and `Drawer.Close` do nothing; the close button and the `Close` child are `disabled` in place |
188
196
  | dirty | `Drawer.Guard dirty` | a dismiss opens the «Не сохранять изменения?» ConfirmDialog (`labels.discard*`); its confirm closes the drawer |
197
+ | leaving the tab | `Drawer.Guard busy` or `dirty` | the browser asks before the tab closes or reloads (`beforeunload`), as with `Page.Guard` |
198
+ | description loading | `Drawer.Description loading` | a `Skeleton` holds the line, `aria-busy` on the `<p>`; the data fades in under the same element |
199
+ | actions below | the title would get less than 12rem beside `Drawer.Actions` | `data-actions-below` on the header: the actions move under the title and the description |
189
200
 
190
201
  Motion: the scrim fades, the panel slides from its side over `slow` and leaves over `base` (`overlayMotion`); under `prefers-reduced-motion` it unmounts at once. The drawer has no touch gestures: it is not dragged or swiped away — it closes by its buttons, Escape and the scrim, with the same slide-out.
191
202
 
@@ -199,8 +210,10 @@ Motion: the scrim fades, the panel slides from its side over `slow` and leaves o
199
210
  - Full viewport height; only the body scrolls, with its own side padding.
200
211
  - Tabs in the header: a `Tabs.List` inside `Drawer.Header` (with `Tabs.Root` around the header and the body) spans the header's last row. The header becomes the tab strip, edge to edge and without the hairline; the active tab is the body rising into it, the panels sit in the body.
201
212
  - Fields inside get the surface field fill.
202
- - Titled groups of the body (a record's contacts, its deals, its history): `Drawer.Section label` — an `<h3>` under the drawer's `<h2>` title, an optional `description`, a `Drawer.Actions` at the end of its heading row; content 16 below, sections 32 apart. A group of fields is a `Fieldset`; a page section is `Page.Section`.
213
+ - Titled groups of the body (a record's contacts, its deals, its history): `Drawer.Section label` — an `<h3>` under the drawer's `<h2>` title, an optional `description`, a `Drawer.Actions` at the end of its heading row; content 16 below, sections 32 apart — directly in the body, in a `Tabs.Panel` or in a form, with no spacing wrapper of your own. A group of fields is a `Fieldset`; a page section is `Page.Section`.
203
214
  - With a `Drawer.Meta` the buttons stay one row: when the footer runs short the status goes above them first.
215
+ - `Drawer.Actions` keep their place beside the title while it has 12rem; on a phone, where a status and a ⋯ beside the × would fold the title into letters, they move under it by themselves.
216
+ - A record whose data is still loading: the title from the list row, `Drawer.Description loading` and a `Skeleton` body in the data's geometry under a `Crossfade` — not a skeleton of your own in the header.
204
217
 
205
218
  ## Accessibility
206
219
 
@@ -240,8 +253,9 @@ Motion: the scrim fades, the panel slides from its side over `slow` and leaves o
240
253
  | [structure.tsx](examples/structure.tsx) | Optional parts: an icon tile in the header and one full-width action in the footer — `Drawer.Icon`, `Drawer.Footer`, `layout`. |
241
254
  | [sizes.tsx](examples/sizes.tsx) | Every panel width, 360 to 800 px; below 640 px of viewport the panel takes the full width — `size`. |
242
255
  | [placement.tsx](examples/placement.tsx) | The panel slides from the right for details and from the left for filters, rounded only on the edge facing the page — `side`. |
243
- | [header-actions.tsx](examples/header-actions.tsx) | A record card: its status and a ⋯ menu sit in the header before the close button, one tier smaller — `Drawer.Actions`. |
256
+ | [header-actions.tsx](examples/header-actions.tsx) | A record card: its status and a ⋯ menu sit in the header before the close button, one tier smaller; on a phone, where the title has no room beside them, they move under it — `Drawer.Actions`. |
244
257
  | [client-card.tsx](examples/client-card.tsx) | A client card from the list: the contact's avatar heads the panel in place of an icon tile, then the details in titled sections, one with its own action — `Drawer.Icon`, `Avatar.Root`, `Drawer.Section`, `Drawer.Actions`. |
258
+ | [loading.tsx](examples/loading.tsx) | A record card that opens before its data arrives: the title comes from the list row, the line under it and the body hold their place in skeletons, then the data fades in — `Drawer.Description`, `loading`. |
245
259
  | [guard.tsx](examples/guard.tsx) | A form that holds the drawer from inside: after an edit Escape, the scrim and Cancel ask before discarding, the footer says so, and saving locks every way out — `Drawer.Guard`, `Drawer.Meta`. |
246
260
  | [long-content.tsx](examples/long-content.tsx) | A long body scrolls on its own while the header and the footer stay in place — `Drawer.Body`. |
247
261
  | [dismiss.tsx](examples/dismiss.tsx) | An import closes only from its buttons, and not at all while it runs — `closeOnOutsideClick`, `closeOnEscape`. |
@@ -254,6 +268,8 @@ Motion: the scrim fades, the panel slides from its side over `slow` and leaves o
254
268
  - Save or Cancel buttons at the end of the body, or in the header → they go in `Drawer.Footer`, which stays in place while the body scrolls.
255
269
  - A delete confirmation in a Drawer → use ConfirmDialog.
256
270
  - A hand-made heading row (`Typography as="h3"` + buttons) over a group in the body → `Drawer.Section label` with `Drawer.Actions`.
271
+ - A wrapper with its own gap around `Drawer.Section`s inside a `Tabs.Panel` or a form → not needed: sections keep 32 between them in any wrapper.
272
+ - A `Skeleton` placed by hand in `Drawer.Description` while the record loads → `Drawer.Description loading`.
257
273
  - Busy or unsaved state lifted out of the form into Root props → put `Drawer.Guard` next to the form, inside Content.
258
274
  - Clearing the record together with `open` and rendering a skeleton for «no record» → not needed: the panel keeps its last content while it slides out.
259
275
  - A Badge or a menu as a custom child of the header text column → put them in `Drawer.Actions`.
@@ -263,5 +279,5 @@ Motion: the scrim fades, the panel slides from its side over `slow` and leaves o
263
279
  - Padding on a wrapper inside Body → Body already pads and scrolls.
264
280
 
265
281
  ## Related
266
- - **Built from:** the dialog shared with Modal and Sheet (`internal/overlay/DialogParts`), [Button](../button/COMPONENT.md) (close button), [ScrollContainer](../scroll-container/COMPONENT.md) (`Drawer.Body`), [Avatar](../avatar/COMPONENT.md) and [Thumbnail](../thumbnail/COMPONENT.md) (a record's picture in `Drawer.Icon`), [ConfirmDialog](../confirm-dialog/COMPONENT.md) (the `Drawer.Guard` discard confirm), `Icon` (the close glyph)
282
+ - **Built from:** the dialog shared with Modal and Sheet (`internal/overlay/DialogParts`), [Button](../button/COMPONENT.md) (close button), [ScrollContainer](../scroll-container/COMPONENT.md) (`Drawer.Body`), [Avatar](../avatar/COMPONENT.md) and [Thumbnail](../thumbnail/COMPONENT.md) (a record's picture in `Drawer.Icon`), [Skeleton](../skeleton/COMPONENT.md) (`Drawer.Description loading`), [ConfirmDialog](../confirm-dialog/COMPONENT.md) (the `Drawer.Guard` discard confirm), `Icon` (the close glyph)
267
283
  - **See also:** [Modal](../modal/COMPONENT.md) (the same header, body, footer and `Guard` in the centre), [Sheet](../sheet/COMPONENT.md) (the same parts raised from the bottom edge for a short task), [ConfirmDialog](../confirm-dialog/COMPONENT.md) (a destructive confirm), [Sidebar](../../layout/sidebar/COMPONENT.md), [Popover](../popover/COMPONENT.md), [Avatar](../avatar/COMPONENT.md) (a person's picture in `Drawer.Icon`)
@@ -1,4 +1,4 @@
1
- /** A record card: its status and a ⋯ menu sit in the header before the close button, one tier smaller — `Drawer.Actions`. */
1
+ /** A record card: its status and a ⋯ menu sit in the header before the close button, one tier smaller; on a phone, where the title has no room beside them, they move under it — `Drawer.Actions`. */
2
2
  import { Badge, Button, DescriptionList, Drawer, Dropdown, Icon } from "prime-ui-kit";
3
3
 
4
4
  const DEAL = {