prime-ui-kit 0.19.0 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +1 -1
  2. package/dist/chunks/{chunk-RZNGLELR.js → chunk-3PY52JP3.js} +1603 -1609
  3. package/dist/chunks/{chunk-WYCTXD63.js → chunk-4374EBWV.js} +3593 -2889
  4. package/dist/chunks/{chunk-ZTZ2ML2B.js → chunk-4KONMO7Z.js} +1 -1
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/data-table/ColumnsMenu.d.ts +34 -8
  7. package/dist/components/data-table/DataTable.d.ts +31 -5
  8. package/dist/components/data-table/DataTableColumnsMenu.d.ts +45 -0
  9. package/dist/components/data-table/Head.d.ts +14 -8
  10. package/dist/components/data-table/Row.d.ts +8 -8
  11. package/dist/components/data-table/StateRows.d.ts +5 -5
  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 +34 -12
  22. package/dist/components/gantt/Gantt.d.ts +1 -1
  23. package/dist/components/index.d.ts +2 -0
  24. package/dist/components/scroll-container/ScrollContainer.d.ts +1 -1
  25. package/dist/index.css +1 -1
  26. package/dist/index.js +4 -2
  27. package/dist/internal/ToolRow.d.ts +23 -0
  28. package/dist/internal/fieldCell.d.ts +0 -6
  29. package/dist/layout/page/Page.d.ts +7 -7
  30. package/dist/skill/anti-slop.md +43 -12
  31. package/dist/skill/api-contract.md +1 -1
  32. package/dist/skill/cheatsheet.md +7 -3
  33. package/dist/skill/checklist.md +4 -1
  34. package/dist/skill/choosing.md +1 -0
  35. package/dist/skill/components.md +1 -1
  36. package/dist/skill/composition.md +137 -20
  37. package/dist/skill/reference/components/data-table/COMPONENT.md +101 -40
  38. package/dist/skill/reference/components/data-table/examples/column-order.tsx +152 -0
  39. package/dist/skill/reference/components/data-table/examples/columns-visibility.tsx +20 -11
  40. package/dist/skill/reference/components/data-table/examples/controlled.tsx +32 -17
  41. package/dist/skill/reference/components/data-table/examples/examples.module.css +14 -36
  42. package/dist/skill/reference/components/data-table/examples/narrow.tsx +24 -11
  43. package/dist/skill/reference/components/data-table/examples/reorder.tsx +24 -2
  44. package/dist/skill/reference/components/data-table/examples/selection.tsx +13 -18
  45. package/dist/skill/reference/components/data-table/examples/server.tsx +45 -31
  46. package/dist/skill/reference/components/data-table/examples/toolbar.tsx +5 -12
  47. package/dist/skill/reference/components/datepicker/COMPONENT.md +2 -2
  48. package/dist/skill/reference/components/dnd/COMPONENT.md +12 -7
  49. package/dist/skill/reference/components/dnd/examples/table-columns.tsx +75 -0
  50. package/dist/skill/reference/components/field-list/COMPONENT.md +70 -43
  51. package/dist/skill/reference/components/field-list/examples/narrow.tsx +108 -33
  52. package/dist/skill/reference/components/field-list/examples/reorder.tsx +1 -1
  53. package/dist/skill/reference/components/field-list/examples/row-menu.tsx +71 -0
  54. package/dist/skill/reference/components/field-list/examples/screen.tsx +2 -1
  55. package/dist/skill/reference/components/field-list/examples/variants.tsx +51 -0
  56. package/dist/skill/reference/components/gantt/COMPONENT.md +4 -4
  57. package/dist/skill/reference/components/scroll-container/COMPONENT.md +1 -1
  58. package/dist/skill/reference/components/smart-filter/COMPONENT.md +2 -2
  59. package/dist/skill/reference/layout/page/COMPONENT.md +8 -5
  60. package/dist/skill/responsive.md +10 -8
  61. package/dist/tokens/semantic.d.ts +1 -1
  62. package/package.json +1 -1
  63. package/src/components/data-table/COMPONENT.md +101 -40
  64. package/src/components/data-table/examples/column-order.tsx +152 -0
  65. package/src/components/data-table/examples/columns-visibility.tsx +20 -11
  66. package/src/components/data-table/examples/controlled.tsx +32 -17
  67. package/src/components/data-table/examples/examples.module.css +14 -36
  68. package/src/components/data-table/examples/narrow.tsx +24 -11
  69. package/src/components/data-table/examples/reorder.tsx +24 -2
  70. package/src/components/data-table/examples/selection.tsx +13 -18
  71. package/src/components/data-table/examples/server.tsx +45 -31
  72. package/src/components/data-table/examples/toolbar.tsx +5 -12
  73. package/src/components/datepicker/COMPONENT.md +2 -2
  74. package/src/components/dnd/COMPONENT.md +12 -7
  75. package/src/components/dnd/examples/table-columns.tsx +75 -0
  76. package/src/components/field-list/COMPONENT.md +70 -43
  77. package/src/components/field-list/examples/narrow.tsx +108 -33
  78. package/src/components/field-list/examples/reorder.tsx +1 -1
  79. package/src/components/field-list/examples/row-menu.tsx +71 -0
  80. package/src/components/field-list/examples/screen.tsx +2 -1
  81. package/src/components/field-list/examples/variants.tsx +51 -0
  82. package/src/components/gantt/COMPONENT.md +4 -4
  83. package/src/components/scroll-container/COMPONENT.md +1 -1
  84. package/src/components/smart-filter/COMPONENT.md +2 -2
  85. package/src/layout/page/COMPONENT.md +8 -5
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  Crossfade,
20
20
  DEFAULT_DATEPICKER_PRESETS,
21
21
  DataTable,
22
+ DataTableColumnsMenu,
22
23
  Datepicker,
23
24
  DescriptionList,
24
25
  DigitInput,
@@ -87,7 +88,7 @@ import {
87
88
  useSidebar,
88
89
  useTokenValue,
89
90
  useUploadQueue
90
- } from "./chunks/chunk-WYCTXD63.js";
91
+ } from "./chunks/chunk-4374EBWV.js";
91
92
  import {
92
93
  Avatar,
93
94
  Badge,
@@ -122,7 +123,7 @@ import {
122
123
  tierMappings,
123
124
  tones,
124
125
  variants
125
- } from "./chunks/chunk-RZNGLELR.js";
126
+ } from "./chunks/chunk-3PY52JP3.js";
126
127
  import {
127
128
  ControlSizeProvider,
128
129
  useOptionalControlSize
@@ -153,6 +154,7 @@ export {
153
154
  Crossfade,
154
155
  DEFAULT_DATEPICKER_PRESETS,
155
156
  DataTable,
157
+ DataTableColumnsMenu,
156
158
  Datepicker,
157
159
  DescriptionList,
158
160
  DigitInput,
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ type DivProps = React.HTMLAttributes<HTMLDivElement> & {
3
+ ref?: React.Ref<HTMLDivElement>;
4
+ };
5
+ type ToolGroupProps = DivProps;
6
+ /**
7
+ * A host's own items kept together in one cell that takes the free room (the Gantt's legend or
8
+ * title before its zoom): they wrap inside it.
9
+ */
10
+ export declare function ToolGroup({ className, ...rest }: ToolGroupProps): import("react/jsx-runtime").JSX.Element;
11
+ type ToolRowProps = DivProps & {
12
+ /** The row has a line of its own: it wraps and its cells fill it by kind. Off — one line, every cell at its natural width. */
13
+ fill?: boolean;
14
+ };
15
+ /**
16
+ * A row of tools as written, each in a cell of its kind: a search grows, switchers and pickers
17
+ * keep their content width beside it and fill a short line, buttons keep their width at the end.
18
+ */
19
+ export declare function ToolRow({ fill, className, children, ...rest }: ToolRowProps): import("react/jsx-runtime").JSX.Element;
20
+ type ToolChipsProps = DivProps;
21
+ /** The applied filters (`SmartFilter.Chips`) on a line of their own under a tool row; gone while empty. */
22
+ export declare function ToolChips({ className, ...rest }: ToolChipsProps): import("react/jsx-runtime").JSX.Element;
23
+ export {};
@@ -17,12 +17,6 @@ export type FieldCell = {
17
17
  hidden: "wide" | "always";
18
18
  /** The column is required: the field in the cell is required too (`useFieldFrame`). */
19
19
  required: boolean;
20
- /**
21
- * The row's action the field holds at its end: a one-column list puts each row's remove button
22
- * inside its field. A text field (`Input.Wrapper`, `Textarea`) renders it as its last action
23
- * segment; a list whose field holds none keeps the remove in a column of its own.
24
- */
25
- action?: React.ReactNode;
26
20
  };
27
21
  export declare const FieldCellContext: React.Context<FieldCell | null>;
28
22
  export declare function useFieldCell(): FieldCell | null;
@@ -128,14 +128,14 @@ declare namespace PageActions {
128
128
  }
129
129
  export type PageToolsProps = DivProps;
130
130
  /**
131
- * The page's tools — sections, search and filter, view — as kit controls in the order written.
132
- * On the bar's first line each keeps its natural width; on a line of their own they fill it:
133
- * a search (`SmartFilter.Toolbar`, `Input.Root`) takes the room first, switchers and fields share
134
- * the rest (a `SegmentedControl.Root` and a `Datepicker.Root` are `fullWidth` unless set; a Select
135
- * trigger fills by itself), and they wrap when one line is not enough. Named (`aria-label`), it is
136
- * a `group`.
131
+ * The page's tools — sections, search and filter, view — as kit controls in the order written, laid
132
+ * out by the one tool row (`ToolRow`, as the DataTable `toolbar`). On the bar's first line each keeps
133
+ * its natural width; on a line of their own a search takes the free room, switchers and pickers
134
+ * keep their width at the end (a `SegmentedControl.Root` and a `Datepicker.Root` are `fullWidth`
135
+ * unless set, so they fill a short line), and they wrap when one line is not enough. Named
136
+ * (`aria-label`), it is a `group`.
137
137
  */
138
- declare function PageTools({ className, children, ref, ...rest }: PageToolsProps): import("react/jsx-runtime").JSX.Element;
138
+ declare function PageTools({ className, ...rest }: PageToolsProps): import("react/jsx-runtime").JSX.Element;
139
139
  declare namespace PageTools {
140
140
  var displayName: string;
141
141
  }
@@ -242,7 +242,8 @@ The same for one value in several units — one heading, the fields in a row, th
242
242
  | `<Button.Root onClick={() => location.assign(url)}>` | `<Button.Root asChild><a href={url}>…</a></Button.Root>` or `LinkButton` |
243
243
  | a hand-built spinner, shimmer, grey placeholder box or «Loading…» text | `loading` on Button / Select / DataTable; a region: `Skeleton` of its content inside `Crossfade`; no shape to hold: `Spinner` |
244
244
  | `{loading ? <Spinner /> : <List />}` — a state that flips | `<Crossfade state={status}>` around the region, `Skeleton` while loading |
245
- | a search Input + a row of filter Selects above a table | `SmartFilter` in the page's top bar: its toolbar in `Page.Tools`, its chips in `Page.Chips` ([where the filters go](composition.md#where-the-filters-go)) |
245
+ | a search Input + a row of filter Selects above a table | `SmartFilter` next to the table it filters: its toolbar and chips in the table's `toolbar` / `toolbarChips` beside KPI cards, in `Page.Tools` / `Page.Chips` when the table is the page ([where the filters go](composition.md#where-the-filters-go)) |
246
+ | the table's search, filter or «Колонки» in the page's bar above KPI cards | the bar keeps the title, the period that drives every block and the primary action; the table's tools go in its own `toolbar` ([where the filters go](composition.md#where-the-filters-go)) |
246
247
  | a grid of «label: value» text pairs for one record | `DescriptionList.Root` + `DescriptionList.Item` |
247
248
  | a Modal hand-built as «Удалить?» with your own pending flag | `ConfirmDialog.Root` with an async `onConfirm` |
248
249
  | a `<fieldset>` reset by hand, or `disabled` on every field while saving | `<Fieldset label disabled={saving}>` |
@@ -300,19 +301,49 @@ the page's actions ([controls of a table](composition.md#controls-of-a-table)):
300
301
  <DataTable columns={columns} rows={orders} toolbar={<Button.Root>Колонки</Button.Root>} />
301
302
  ```
302
303
 
304
+ A chooser cell in the header of a table that has no row actions is a column of nothing — an empty
305
+ cell in every row:
306
+
307
+ ```tsx partial
308
+ // bad — columnsMenu with no row-actions column to stand over
309
+ <DataTable columns={columns} rows={orders} columnsMenu />
310
+ ```
311
+
303
312
  ```tsx good
304
- // good — the chooser sits in the header row; the page's own actions live in its top bar
313
+ // good — a page that is the table, no row actions: the chooser is in the bar, on the state the table reads
305
314
  // declare: const orders: { id: string; client: string; total: number }[]
306
- <DataTable
307
- columns={[
308
- { id: "id", header: "Заказ", accessor: "id", hideable: false },
309
- { id: "client", header: "Клиент", accessor: "client" },
310
- { id: "total", header: "Сумма", accessor: "total", numeric: true },
311
- ]}
312
- rows={orders}
313
- getRowKey={(row) => row.id}
314
- columnsMenu
315
- />
315
+ // declare: const hidden: string[]
316
+ // declare: const setHidden: (hidden: string[]) => void
317
+ <Page.Root>
318
+ <Page.TopBar>
319
+ <Page.Heading>
320
+ <Page.Title>Заказы</Page.Title>
321
+ </Page.Heading>
322
+ <Page.Tools>
323
+ <DataTableColumnsMenu
324
+ columns={[
325
+ { id: "id", header: "Заказ", accessor: "id", hideable: false },
326
+ { id: "client", header: "Клиент", accessor: "client" },
327
+ { id: "total", header: "Сумма", accessor: "total", numeric: true },
328
+ ]}
329
+ hiddenColumns={hidden}
330
+ onHiddenColumnsChange={setHidden}
331
+ />
332
+ </Page.Tools>
333
+ </Page.TopBar>
334
+ <Page.Body>
335
+ <DataTable
336
+ columns={[
337
+ { id: "id", header: "Заказ", accessor: "id", hideable: false },
338
+ { id: "client", header: "Клиент", accessor: "client" },
339
+ { id: "total", header: "Сумма", accessor: "total", numeric: true },
340
+ ]}
341
+ hiddenColumns={hidden}
342
+ rows={orders}
343
+ getRowKey={(row) => row.id}
344
+ />
345
+ </Page.Body>
346
+ </Page.Root>
316
347
  ```
317
348
 
318
349
  ## 7b. A top bar out of its groups
@@ -23,7 +23,7 @@ import type { ControlSize, PaletteColor, TextTone, Tone, Variant } from "prime-u
23
23
  | Validation | `invalid`, `hint`, `error` | Props, never parts. A non-empty `error` implies `invalid`; sets `aria-invalid`, `data-invalid`. |
24
24
  | Value | `value` / `defaultValue` / `onValueChange(value)` | Accordion, Chat.Pinned, ChatList, ColorField, ColorPicker, ColorPresets, ColorSwatches, Composer, Datepicker, Datepicker.Panel, DigitInput, FieldList, FileUpload, FileUpload.Queue, Kanban, NativeSelect, Pagination, Radio.Group, SegmentedControl, Select, Slider, SmartFilter, Stepper, Tabs, TagSelect. |
25
25
  | Text value | native `value` / `onChange` + `onValueChange(string)` | Input (`Input.Field`), Textarea. |
26
- | Other state | `x` / `defaultX` / `onXChange(x)` | Badge `pressed`; Button `pressed`; ChartFrame `hiddenSeries`; Chat `selected`, `search`; ChatList `search`; ColorPicker `format`; CommandMenu `search`; Composer `recordMode`; DataTable `hiddenColumns`, `page`, `sort`, `selected`, `expanded`, `collapsedGroups`; ExampleFrame `viewport`, `colorScheme`; Gallery `index`; Gantt `scale`; Kanban `columnOrder`; MediaViewer `index`; Message.Media `index`; Select `search`; SmartFilter `search`; VideoNote `playing`, `transcriptExpanded`; VoicePlayer `rate`, `playing`, `transcriptExpanded`; Sidebar `mode`. |
26
+ | Other state | `x` / `defaultX` / `onXChange(x)` | Badge `pressed`; Button `pressed`; ChartFrame `hiddenSeries`; Chat `selected`, `search`; ChatList `search`; ColorPicker `format`; CommandMenu `search`; Composer `recordMode`; DataTable `hiddenColumns`, `columnOrder`, `page`, `sort`, `selected`, `expanded`, `collapsedGroups`; ExampleFrame `viewport`, `colorScheme`; Gallery `index`; Gantt `scale`; Kanban `columnOrder`; MediaViewer `index`; Message.Media `index`; Select `search`; SmartFilter `search`; VideoNote `playing`, `transcriptExpanded`; VoicePlayer `rate`, `playing`, `transcriptExpanded`; Sidebar `mode`. |
27
27
  | Checked | `checked` / `defaultChecked` / `onCheckedChange(checked)` | Checkbox, Dropdown.CheckboxItem, Switch, Thumbnail. |
28
28
  | Open | `open` / `defaultOpen` / `onOpenChange(open)` | ColorPresets, CommandMenu, ConfirmDialog, Datepicker, Drawer, Dropdown, Gallery, MediaViewer, Message.Media, Modal, Popover, Select, Sheet, TagSelect, Tooltip, Page.Split, Sidebar, Sidebar.Group, Sidebar.Sub. |
29
29
  | Dismiss | `closeOnOutsideClick` (default `true`), `closeOnEscape` (default `true`) | Both on every overlay that opens a layer: the dialogs, the floating panels, the panels of Select, TagSelect, Datepicker and ColorPresets, the `suggestions` list of Input, the off-canvas Sidebar and the reaction bar of a message. `ConfirmDialog` takes both with `closeOnOutsideClick` default `false` (Escape is held while `onConfirm` runs). Tooltip and Notification take neither. A form holds closing with `Modal.Guard` / `Drawer.Guard` / `Sheet.Guard`. |
@@ -44,6 +44,7 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
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
45
  - 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
46
  - 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.
47
48
  - 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.
48
49
  - 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.
49
50
  - 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.
@@ -84,7 +85,7 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
84
85
 
85
86
  - 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.
86
87
  - 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.
87
- - The bar holds only its four groups: `<Page.Heading>`, `<Page.Tools aria-label>` (sections, `<SmartFilter.Toolbar />`, the view — 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)).
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)).
88
89
  - 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`.
89
90
  - 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.
90
91
  - 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.
@@ -109,7 +110,8 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
109
110
  ## Tables
110
111
 
111
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.
112
- - The column chooser is `<DataTable columnsMenu>` in the header row; `toolbar` only for several controls of the table itself (bulk actions, a section table's own search and filters) ([controls of a table](composition.md#controls-of-a-table)) — never a toolbar for one button.
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.
113
115
  - 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.
114
116
  - 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.
115
117
  - 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`).
@@ -121,6 +123,8 @@ reference is each component's `COMPONENT.md` ([components.md](components.md)), t
121
123
  - A column header that needs a word more («Маржа»): the column's `description` (an info glyph with a tooltip) — not the explanation squeezed into the header, not your own icon and Tooltip in `header`.
122
124
  - A list row with controls of its own (a Switch, a ⋯ menu): `List.ItemActions` inside `List.Item`, a stage colour as `Badge.Dot color` in `List.ItemIcon` — not a control inside the row's button, not a row rebuilt as a `div`.
123
125
  - Rows people order by hand: `onReorder(id, beforeId)` (apply it with `moveBefore`, return the save's Promise) — not a Dnd list rebuilt next to the table.
126
+ - A DataTable's grip stays at the end of the row (`reorderHandle` default); `start` or `row` only by the rule ([choosing](choosing.md#3-pairs-that-are-easy-to-confuse)) — never a grip column of your own.
127
+ - Columns people arrange: `columnOrder` + `onColumnOrderChange` next to `hiddenColumns` and the column chooser, both saved by the app — not up / down buttons, a settings dialog or a header drag of your own.
124
128
  - A short list of records people order by hand (stages, priorities) is `<List.Root onReorder>` with `List.Item value` — not up / down buttons or a hand-made `Dnd.Sortable`.
125
129
  - A toggle in a reorderable row leads it (`List.ItemActions side="start"`) and a plain settings row ends with it ([list rows](composition.md#list-rows)) — never a Switch between the label and the grip.
126
130
  - The grip ends its row: a `Dnd.Handle` is the last child of its item, after the row's own controls, and a locked row's lock takes the same place (List, FieldList and Kanban place theirs there) — not before the row's text.
@@ -208,7 +212,7 @@ Full rules: [responsive.md](responsive.md).
208
212
 
209
213
  - Rearrange, never hide: every desktop function is on the phone, in another place or a menu — not `display: none` below a breakpoint.
210
214
  - Breakpoints only in [their scope](responsive.md#breakpoints); inside a page — [intrinsic grids](responsive.md#intrinsic-grids) and container queries.
211
- - A wide table scrolls inside itself; offer the column chooser with `columnsMenu` — never drop columns by breakpoint, never hand-build the chooser.
215
+ - A wide table scrolls inside itself; offer the column chooser (`columnsMenu` or `DataTableColumnsMenu`) — never drop columns by breakpoint, never hand-build the chooser.
212
216
  - A row that opens a record page is `getRowHref` (a real link: ⌘-click, new tab), a row that opens a drawer is `onRowClick` — never `navigate()` in `onRowClick`, never your own `event.target` checks for buttons in the row.
213
217
  - A list + detail screen is `Page.Split` (`Page.List`, `Page.Detail`) in a `Page.Root fill` page, its records a `List` (`current` on the open one) — never ghost Buttons with a soft current; a full-height table is `scrollHeight="fill"` — never a height computed from `100vh`.
214
218
  - App sections on phones: `BottomNav` in `AppShell.Footer` — not in Main, not `position: fixed`, no own media query: the shell hides the footer while the Sidebar rail is on screen; a short task from the bottom edge: `Sheet` — Select, Dropdown, Popover and Modal become sheets below 640 by themselves.
@@ -10,13 +10,16 @@ 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
14
  - [ ] A Card only for a standalone titled block — never around a table, a single field or the page ([surfaces](composition.md#4-surfaces)).
14
15
  - [ ] 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)).
15
- - [ ] The column chooser is `columnsMenu` in the table's header row; a DataTable `toolbar` holds two or more table controls or does not exist ([controls of a table](composition.md#controls-of-a-table)).
16
+ - [ ] 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)).
16
18
 
17
19
  ## UX ([ux.md](ux.md))
18
20
  - [ ] I wrote the five-line brief before the JSX ([brief](ux.md#1-brief-before-code)).
19
21
  - [ ] 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)).
20
23
  - [ ] Labels on a form of rows ≤ columns + the list label; none repeated, numbered or repeating its heading ([label economy](ux.md#3-label-economy)).
21
24
  - [ ] No text explains the layout; every hint says what the person cannot see ([label economy](ux.md#3-label-economy)).
22
25
  - [ ] Buttons say the verb and the object, errors say how to fix it, empty states offer the next step ([words](ux.md#4-words)).
@@ -171,6 +171,7 @@ the records (tickets, letters, invoices; `current` on the open one; a row's own
171
171
  | Accordion / Tabs | Accordion when the user may need several sections open or reads them in order (FAQ, settings groups). Tabs when sections are alternatives viewed one at a time. |
172
172
  | Divider / gap | Divider inside a block between rows or a form and its footer. Between blocks and groups use air (`gap`), not lines. |
173
173
  | Breadcrumb / back button | Breadcrumb on pages below a top-level section (Счета → № 2026-0418) and deeper; not on top-level pages. A single “Back” without hierarchy → `LinkButton` or `Button.Root variant="ghost"`. |
174
+ | DataTable grip `end` / `start` / `row`, column order | Rows whose order is data (stages, a price list): `onReorder` with the grip at the end of the row (`reorderHandle="end"`, the default — after the row actions); `start` only when the order is the list's main point and nothing is selected; `row` (the whole row carries) only for short rows with nothing to select, copy or follow as a link. Sorted by a column, rows do not reorder — say so, never hide the grip. Columns people arrange for themselves: `columnOrder` with `hiddenColumns` and the column chooser — a header drag (the header moves alone), Alt + ← / → and the chooser's grips; the key column `reorderable: false` (or pinned), the actions column `hideHeader`; the app saves both lists. |
174
175
  | Pagination / infinite scroll | Pages for tables people return to or reference: DataTable `paging="pages"` (default, Pagination in the footer). `paging="infinite"` for feeds. Standalone Pagination only for lists that are not a DataTable. |
175
176
  | Stepper / Tabs | Stepper for a sequential process with completion status; Tabs for non-sequential views. |
176
177
  | Gantt / Timeline / Kanban / DataTable | Gantt: records with a start and an end compared across time (who is busy when, a plan with phases and milestones), moved, stretched and drawn by hand (`onBarChange`, `onBarCreate`). Timeline: a history of events at points in time, read top to bottom. Kanban: work items moved between statuses, time does not matter. DataTable: many records compared by many attributes, sorted, filtered and selected — dates are only columns. |
@@ -40,7 +40,7 @@ Type a value, and the field anatomy: label, hint, error.
40
40
  | Label | Field label (native `<label>`) with required and optional markers. | [COMPONENT.md](reference/components/label/COMPONENT.md) · [examples](reference/components/label/examples/) |
41
41
  | Hint | Help text or a validation error under a field. | [COMPONENT.md](reference/components/hint/COMPONENT.md) · [examples](reference/components/hint/examples/) |
42
42
  | Fieldset | A frameless group of related form fields: a native `<fieldset>` with a `title-s` legend, the form rhythm between its fields and one `disabled` for all of them. | [COMPONENT.md](reference/components/fieldset/COMPONENT.md) · [examples](reference/components/fieldset/examples/) |
43
- | FieldList | An editable list of records of one shape — invoice lines, specs, contacts, prices by period: one row per record, the column headings once above the rows, a remove button per row and an add button under them. | [COMPONENT.md](reference/components/field-list/COMPONENT.md) · [examples](reference/components/field-list/examples/) |
43
+ | FieldList | An editable list of records of one shape — invoice lines, specs, contacts, prices by period: one row per record on one grid, the column headings once above the rows, the row's actions at its end and an add button under them. | [COMPONENT.md](reference/components/field-list/COMPONENT.md) · [examples](reference/components/field-list/examples/) |
44
44
 
45
45
  ## Selection (`selection`)
46
46
 
@@ -66,7 +66,7 @@ AppShell.Root once per app (layouts.md#app-frame-once-per-a
66
66
  │ │ ├─ Breadcrumb.Root only on a nested record: the levels above this page
67
67
  │ │ ├─ Page.Title the one h1, the path's last level
68
68
  │ │ └─ Page.Description a line of data
69
- │ ├─ Page.Tools sections, search and filter, view — kit controls in reading order
69
+ │ ├─ Page.Tools what acts on the whole page — sections, a period, view; a one-list page's search
70
70
  │ ├─ Page.Actions every button of the page, each with an icon; the primary last
71
71
  │ └─ Page.Chips SmartFilter.Chips: a line under everything, gone while empty
72
72
  └─ Page.Body blocks 24 apart, the first 24 under the bar's controls
@@ -98,9 +98,11 @@ order is Heading, Tools, Actions, Chips whatever the JSX order.
98
98
  then `Page.Title` with its name, on one line (the heading draws the chevron between them), and
99
99
  `Page.Description` under the whole line. The title is never a row of its own under the path, and
100
100
  the path never repeats the title as its last item.
101
- - **`Page.Tools`** — the page's own tools as kit controls, in the order a person reads them: sections
102
- (a `SegmentedControl` with counts), search and filter (`SmartFilter.Toolbar`), the view or the
103
- period. Named with `aria-label`; no buttons and no wrappers inside.
101
+ - **`Page.Tools`** — what acts on the whole page, as kit controls in the order a person reads them:
102
+ sections (a `SegmentedControl` with counts), the view, a period that drives several blocks; the
103
+ search and filter (`SmartFilter.Toolbar`) only when the page is one list
104
+ ([where the filters go](#where-the-filters-go)). Named with `aria-label`; no buttons and no
105
+ wrappers inside.
104
106
  - **`Page.Actions`** — the one place for the page's buttons: secondary ones (`soft` + `neutral`)
105
107
  first, the primary (`solid`) last. Every Button carries a `Button.Icon` beside its label: the bar
106
108
  shows it as the icon alone when the line runs short.
@@ -306,8 +308,15 @@ Hierarchy comes from space and a handful of type roles, not from size jumps, col
306
308
  - Records of one shape that a person adds, edits and removes are a `FieldList`: one row per record,
307
309
  the column headings once above the rows, small attributes (an icon, then its color) in `compact`
308
310
  columns. Never a label in every row, never numbered labels, never one record split over lines.
309
- FieldList draws every row's actions itself — the remove inside the field of a one-column list, a
310
- pinned row's remove off with its reason (`labels.lockedReason`) — so never add your own.
311
+ FieldList draws every row's actions itself — the remove «×» and the handle after it, a pinned
312
+ row's remove off with its reason (`labels.lockedReason`) — so never add your own.
313
+ - The row's actions (`rowActions`): `"buttons"` (default) for short lists edited often on a desktop,
314
+ where moving rows is a main task — remove and handle in sight on every row; `"menu"` — one «⋯» with
315
+ «Выше», «Ниже», «Удалить» — for dense rows (three or more columns), touch-heavy screens, or a row
316
+ that would carry more than remove and move.
317
+ - The look (`variant`): `"plain"` (default) when the list is the content of its card or section;
318
+ `"surface"` when the list stands among other fields of a form or card, so its rows read as one
319
+ block apart from the fields around it.
311
320
  - One value in several units is one row of fields under one heading, each unit in its field's
312
321
  `Input.InlineAffix`; a base value with overrides is one `FieldList` with the base pinned first.
313
322
  The shape of the data decides the layout: [ux.md](ux.md#2-the-shape-of-the-data-decides-the-layout).
@@ -336,22 +345,130 @@ Hierarchy comes from space and a handful of type roles, not from size jumps, col
336
345
 
337
346
  ### Where the filters go
338
347
 
339
- - The page's search and filters are the page's: `SmartFilter.Root` around `Page.Root`,
340
- `SmartFilter.Toolbar` in the bar's `Page.Tools`, `SmartFilter.Chips` in its `Page.Chips`
341
- ([the list page](#list-page)).
342
- A search alone is the same SmartFilter with `fields={[]}`.
348
+ A filter stands next to what it filters.
349
+
350
+ - `Page.TopBar` holds only what acts on the whole page: the heading with its line of data, a period
351
+ or a date range that drives several blocks (the KPI cards and the table), the page's sections or
352
+ view, the primary action.
353
+ - A page that holds more than its table — KPI cards, a chart, another block: the table's own
354
+ controls (search and filter, the chips, the column chooser, a table / cards view, bulk actions)
355
+ stand in the table's tools row. `SmartFilter.Root` wraps the `DataTable`; `SmartFilter.Toolbar`
356
+ and `DataTableColumnsMenu` go in its `toolbar`, `SmartFilter.Chips` in its `toolbarChips`
357
+ ([server.tsx](reference/components/data-table/examples/server.tsx)).
358
+ - A page that is one list — the table, the board or the schedule is the whole page: «next to the
359
+ list» and «on the page» are one place, so its search and filter stay in the bar. `SmartFilter.Root`
360
+ wraps `Page.Root`, `SmartFilter.Toolbar` in `Page.Tools`, `SmartFilter.Chips` in `Page.Chips`
361
+ ([the list page](#list-page)). A search alone is the same SmartFilter with `fields={[]}`.
362
+ - What picks the place: does the control change everything under the bar? A period that recomputes
363
+ the cards and the table does — the bar. A search that narrows only the rows does not — the
364
+ table's row.
365
+ - Both rows are one tool row of the kit: written in reading order, a search takes the free room,
366
+ switchers and buttons keep their width at the end, and a short row wraps by itself — pass the
367
+ controls as they are, never inside a flex wrapper of your own.
368
+
369
+ ```tsx partial
370
+ // bad — the table's search, filter, chips and chooser in the bar, above KPI cards they do not filter
371
+ <SmartFilter.Root fields={FIELDS}>
372
+ <Page.Root>
373
+ <Page.TopBar>
374
+ <Page.Heading>
375
+ <Page.Title>Модели</Page.Title>
376
+ </Page.Heading>
377
+ <Page.Tools aria-label="Модели">
378
+ <SmartFilter.Toolbar />
379
+ <DataTableColumnsMenu columns={COLUMNS} hiddenColumns={hidden} onHiddenColumnsChange={setHidden} />
380
+ </Page.Tools>
381
+ <Page.Chips>
382
+ <SmartFilter.Chips />
383
+ </Page.Chips>
384
+ </Page.TopBar>
385
+ <Page.Body>
386
+ <Page.Grid>{/* four KPI cards */}</Page.Grid>
387
+ <DataTable columns={COLUMNS} rows={models} hiddenColumns={hidden} />
388
+ </Page.Body>
389
+ </Page.Root>
390
+ </SmartFilter.Root>
391
+ ```
392
+
393
+ ```tsx good
394
+ // good — the bar keeps the title, the period that drives the cards and the table, the primary
395
+ // action; the table's search, filter, chips and chooser stand in the table's own tools row
396
+ // declare: const FIELDS: Parameters<typeof SmartFilter.Root>[0]["fields"]
397
+ // declare: const models: { id: string; name: string; rides: number }[]
398
+ // declare: const kpis: { label: string; value: string }[]
399
+ // declare: const hidden: string[]
400
+ // declare: const setHidden: (hidden: string[]) => void
401
+ <Page.Root>
402
+ <Page.TopBar>
403
+ <Page.Heading>
404
+ <Page.Title>Модели</Page.Title>
405
+ </Page.Heading>
406
+ <Page.Tools aria-label="Модели">
407
+ <Datepicker.Root mode="range" aria-label="Период" />
408
+ </Page.Tools>
409
+ <Page.Actions>
410
+ <Button.Root>
411
+ <Button.Icon>
412
+ <Icon name="action.add" />
413
+ </Button.Icon>
414
+ Добавить модель
415
+ </Button.Root>
416
+ </Page.Actions>
417
+ </Page.TopBar>
418
+ <Page.Body>
419
+ <Page.Grid>
420
+ {kpis.map((kpi) => (
421
+ <Page.Cell key={kpi.label} span={3}>
422
+ <Card.Root variant="stat-trend">
423
+ <Card.Label>{kpi.label}</Card.Label>
424
+ <Card.Value>{kpi.value}</Card.Value>
425
+ </Card.Root>
426
+ </Page.Cell>
427
+ ))}
428
+ </Page.Grid>
429
+ <SmartFilter.Root fields={FIELDS}>
430
+ <DataTable
431
+ columns={[
432
+ { id: "name", header: "Модель", accessor: "name", hideable: false },
433
+ { id: "rides", header: "Поездки", accessor: "rides", numeric: true },
434
+ ]}
435
+ rows={models}
436
+ getRowKey={(row) => row.id}
437
+ hiddenColumns={hidden}
438
+ toolbar={
439
+ <>
440
+ <SmartFilter.Toolbar />
441
+ <DataTableColumnsMenu
442
+ columns={[
443
+ { id: "name", header: "Модель", accessor: "name", hideable: false },
444
+ { id: "rides", header: "Поездки", accessor: "rides", numeric: true },
445
+ ]}
446
+ hiddenColumns={hidden}
447
+ onHiddenColumnsChange={setHidden}
448
+ />
449
+ </>
450
+ }
451
+ toolbarChips={<SmartFilter.Chips />}
452
+ />
453
+ </SmartFilter.Root>
454
+ </Page.Body>
455
+ </Page.Root>
456
+ ```
343
457
 
344
458
  ### Controls of a table
345
459
 
346
- - The column chooser is part of the table: `columnsMenu` puts it at the end of the header row — in
347
- the head cell of a trailing row-actions column (`hideHeader`), else in a narrow cell of its own —
348
- and it sticks with `stickyLastColumn`. Give the key column and the actions column `hideable: false`.
349
- - The DataTable `toolbar` holds several controls of the table itself: a bulk bar of the selected
350
- rows, or the search and filters of a table that is not the page's main region (a table in a
351
- section of a record, in a Drawer).
352
- - One control never gets a bar of its own. A lone «Экспорт» or «Создать» is the page's
353
- (`Page.Actions`); the period, the view switch, the search and filters of the page go to
354
- `Page.Tools`; a count or a total of the list is data in `Page.Description`.
460
+ - The column chooser has one place per table. Over the row actions: `columnsMenu` puts the icon
461
+ trigger in the head cell of the row-actions column (the last column, `hideHeader`), and it sticks
462
+ with `stickyLastColumn`. A table without row actions gets no chooser cell in its header: the
463
+ chooser is `<DataTableColumnsMenu>` beside the table's search ([where the filters go](#where-the-filters-go)),
464
+ bound to the same `hiddenColumns` / `columnOrder` state the table reads. Give the key column and
465
+ the actions column `hideable: false`.
466
+ - The DataTable `toolbar` holds two or more controls of the table itself — its search and filter,
467
+ the chooser, a view, a bulk bar of the selected rows — as they are, in reading order; its chips go
468
+ to `toolbarChips`.
469
+ - One control never gets a row of its own. A lone «Экспорт» or «Создать» is the page's
470
+ (`Page.Actions`); a lone chooser joins the table's search or the page's tools; a count or a total
471
+ of the list is data in `Page.Description`.
355
472
 
356
473
  ### Tables
357
474
 
@@ -513,7 +630,7 @@ What each screen in [the index](#screens) demonstrates — keep these when you s
513
630
  [data-table/examples/screen.tsx](reference/components/data-table/examples/screen.tsx)
514
631
 
515
632
  - A work screen titles itself in its `Page.TopBar`: `Page.Title` is the only heading, its `Page.Description` is one line of figures («10 счетов · просрочено 2 на …»), not a sentence about the page; there is no second header in the flow.
516
- - `SmartFilter.Root` wraps the page: `SmartFilter.Toolbar` in `Page.Tools`, `SmartFilter.Chips` in `Page.Chips`; export (`soft` + `neutral`, an icon beside its label) and the one primary action (`solid`, last) in `Page.Actions`. The table itself has no `toolbar`.
633
+ - The table is the whole page, so its filters are the page's: `SmartFilter.Root` wraps the page, `SmartFilter.Toolbar` in `Page.Tools`, `SmartFilter.Chips` in `Page.Chips`; export (`soft` + `neutral`, an icon beside its label) and the one primary action (`solid`, last) in `Page.Actions`. The table itself has no `toolbar`; add a KPI row above it and the search, filter and chips move into the table's `toolbar` / `toolbarChips`.
517
634
  - A status is a `Badge` with one `tone` per meaning across the product (`STATUS` is one map); the text carries the meaning, the tone repeats it.
518
635
  - Numbers and dates are `numeric` columns with `headerAlign="end"` (tabular figures, values and heading on the end edge). On a phone the table scrolls inside itself and `stickyFirstColumn` keeps the number in place — the page never scrolls sideways.
519
636
  - Row actions are a `Dropdown` behind an `action.more` icon of size `s` in the last column (`hideHeader`), pinned to the end edge with `stickyLastColumn` so they stay in reach while the columns scroll: controls in cells are one tier below the table. Delete is `tone="danger"` at the end of the menu.