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
@@ -1,7 +1,14 @@
1
1
  /** Sort, page and the column choice owned by the parent (a URL or a store): a header click goes asc → desc → none, a new sort returns to page 1 — `sort`, `onSortChange`, `page`, `onPageChange`, `hiddenColumns`, `onHiddenColumnsChange`. */
2
- import { DataTable, type DataTableColumn, type DataTableSortState } from "prime-ui-kit";
2
+ import {
3
+ DataTable,
4
+ type DataTableColumn,
5
+ DataTableColumnsMenu,
6
+ type DataTableSortState,
7
+ } from "prime-ui-kit";
3
8
  import * as React from "react";
4
9
 
10
+ import styles from "./examples.module.css";
11
+
5
12
  type Order = { id: number; customer: string; city: string; total: number };
6
13
 
7
14
  const CUSTOMERS = ["Анна Соколова", "Борис Кравец", "Вера Львова", "Глеб Миронов", "Дарья Носова"];
@@ -48,21 +55,29 @@ export default function DataTableControlledExample() {
48
55
  const [hiddenColumns, setHiddenColumns] = React.useState<string[]>(["city"]);
49
56
 
50
57
  return (
51
- <DataTable
52
- columns={COLUMNS}
53
- rows={ORDERS}
54
- getRowKey={(row) => row.id}
55
- columnsMenu
56
- hiddenColumns={hiddenColumns}
57
- onHiddenColumnsChange={setHiddenColumns}
58
- sort={sort}
59
- onSortChange={(next) => {
60
- setSort(next);
61
- setPage(1);
62
- }}
63
- page={page}
64
- onPageChange={setPage}
65
- pageSize={5}
66
- />
58
+ <div className={styles.withTools}>
59
+ <div className={styles.tools}>
60
+ <DataTableColumnsMenu
61
+ columns={COLUMNS}
62
+ hiddenColumns={hiddenColumns}
63
+ onHiddenColumnsChange={setHiddenColumns}
64
+ />
65
+ </div>
66
+ <DataTable
67
+ columns={COLUMNS}
68
+ rows={ORDERS}
69
+ getRowKey={(row) => row.id}
70
+ hiddenColumns={hiddenColumns}
71
+ onHiddenColumnsChange={setHiddenColumns}
72
+ sort={sort}
73
+ onSortChange={(next) => {
74
+ setSort(next);
75
+ setPage(1);
76
+ }}
77
+ page={page}
78
+ onPageChange={setPage}
79
+ pageSize={5}
80
+ />
81
+ </div>
67
82
  );
68
83
  }
@@ -1,41 +1,5 @@
1
1
  /* Layout of the DataTable scenarios: semantic tokens only. */
2
2
 
3
- /* Toolbar content: two groups pushed to the edges; they wrap on narrow widths. */
4
- .toolbar {
5
- display: flex;
6
- flex-wrap: wrap;
7
- align-items: center;
8
- justify-content: space-between;
9
- gap: var(--prime-space-2);
10
- width: 100%;
11
- min-width: 0;
12
- }
13
-
14
- /* A SmartFilter in the toolbar: the search and filter row over the applied filters. */
15
- .filterColumn {
16
- display: flex;
17
- flex: 1 1 auto;
18
- flex-direction: column;
19
- gap: var(--prime-space-3);
20
- min-width: 0;
21
- }
22
-
23
- .filters {
24
- display: flex;
25
- flex: 1 1 auto;
26
- flex-wrap: wrap;
27
- align-items: center;
28
- gap: var(--prime-space-2);
29
- min-width: 0;
30
- }
31
-
32
- /* The search grows from 12rem, at most 20rem. */
33
- .search {
34
- flex: 1 1 calc(var(--prime-space-24) * 2);
35
- min-width: 0;
36
- max-width: calc(var(--prime-space-20) * 4);
37
- }
38
-
39
3
  /* appearance / content-width: a table with its caption under it. */
40
4
  .specimen {
41
5
  display: flex;
@@ -73,3 +37,17 @@
73
37
  min-width: 0;
74
38
  margin-inline: auto;
75
39
  }
40
+
41
+ /* A table under the tools of its page: the column chooser (`DataTableColumnsMenu`) at the end. */
42
+ .withTools {
43
+ display: grid;
44
+ grid-template-columns: minmax(0, 1fr);
45
+ gap: var(--prime-space-3);
46
+ min-width: 0;
47
+ }
48
+
49
+ .tools {
50
+ display: flex;
51
+ justify-content: flex-end;
52
+ min-width: 0;
53
+ }
@@ -1,5 +1,6 @@
1
- /** A 320 px support queue: columns scroll inside the table with the first one pinned, the column chooser stays at the end of the header row, the footer reflows and the pager turns compact — `stickyFirstColumn`, `columnsMenu`, `size`. */
2
- import { Badge, DataTable, type DataTableColumn } from "prime-ui-kit";
1
+ /** A 320 px support queue: columns scroll inside the table with the first one pinned, the column chooser stays in reach above it, the footer reflows and the pager turns compact — `stickyFirstColumn`, `DataTableColumnsMenu`, `size`. */
2
+ import { Badge, DataTable, type DataTableColumn, DataTableColumnsMenu } from "prime-ui-kit";
3
+ import * as React from "react";
3
4
 
4
5
  import styles from "./examples.module.css";
5
6
 
@@ -31,17 +32,29 @@ const COLUMNS: DataTableColumn<Ticket>[] = [
31
32
  ];
32
33
 
33
34
  export default function DataTableNarrowExample() {
35
+ const [hidden, setHidden] = React.useState<string[]>([]);
36
+
34
37
  return (
35
38
  <div className={styles.narrow}>
36
- <DataTable
37
- size="s"
38
- columns={COLUMNS}
39
- rows={TICKETS}
40
- getRowKey={(row) => row.id}
41
- stickyFirstColumn
42
- columnsMenu
43
- pageSize={5}
44
- />
39
+ <div className={styles.withTools}>
40
+ <div className={styles.tools}>
41
+ <DataTableColumnsMenu
42
+ size="s"
43
+ columns={COLUMNS}
44
+ hiddenColumns={hidden}
45
+ onHiddenColumnsChange={setHidden}
46
+ />
47
+ </div>
48
+ <DataTable
49
+ size="s"
50
+ columns={COLUMNS}
51
+ hiddenColumns={hidden}
52
+ rows={TICKETS}
53
+ getRowKey={(row) => row.id}
54
+ stickyFirstColumn
55
+ pageSize={5}
56
+ />
57
+ </div>
45
58
  </div>
46
59
  );
47
60
  }
@@ -1,8 +1,9 @@
1
- /** Pipeline stages put in order by the handle or Alt + ↑ / ↓; each move is saved with a spinner in its handle, and sorting by a column turns the handles off — `onReorder`, `getRowLabel`. */
2
- import { Badge, DataTable, type DataTableColumn, moveBefore } from "prime-ui-kit";
1
+ /** Pipeline stages put in order by a grip at the end of the row, at its start or by the whole row, or with Alt + ↑ / ↓; each move is saved with a spinner in its grip, and sorting by a column turns reordering off — `onReorder`, `reorderHandle`, `getRowLabel`. */
2
+ import { Badge, DataTable, type DataTableColumn, moveBefore, SegmentedControl } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
5
5
  type Stage = { id: string; name: string; owner: string; deals: number; probability: number };
6
+ type Handle = "end" | "start" | "row";
6
7
 
7
8
  const STAGES: Stage[] = [
8
9
  { id: "lead", name: "Новый лид", owner: "Отдел продаж", deals: 48, probability: 10 },
@@ -19,6 +20,12 @@ const STAGES: Stage[] = [
19
20
  { id: "payment", name: "Оплата", owner: "Бухгалтерия", deals: 4, probability: 95 },
20
21
  ];
21
22
 
23
+ const HANDLES: { value: Handle; label: string }[] = [
24
+ { value: "end", label: "Ручка в конце" },
25
+ { value: "start", label: "Ручка в начале" },
26
+ { value: "row", label: "Вся строка" },
27
+ ];
28
+
22
29
  const COLUMNS: DataTableColumn<Stage>[] = [
23
30
  { id: "name", header: "Этап", accessor: "name", hideable: false },
24
31
  { id: "owner", header: "Ответственный", accessor: "owner" },
@@ -38,6 +45,7 @@ const saveOrder = () => new Promise<void>((resolve) => setTimeout(resolve, 700))
38
45
 
39
46
  export default function DataTableReorderExample() {
40
47
  const [stages, setStages] = React.useState(STAGES);
48
+ const [handle, setHandle] = React.useState<Handle>("end");
41
49
  return (
42
50
  <DataTable
43
51
  columns={COLUMNS}
@@ -45,6 +53,20 @@ export default function DataTableReorderExample() {
45
53
  paging="none"
46
54
  getRowKey={(row) => row.id}
47
55
  getRowLabel={(row) => row.name}
56
+ reorderHandle={handle}
57
+ toolbar={
58
+ <SegmentedControl.Root
59
+ value={handle}
60
+ onValueChange={(next) => setHandle(next as Handle)}
61
+ aria-label="За что брать строку"
62
+ >
63
+ {HANDLES.map((item) => (
64
+ <SegmentedControl.Item key={item.value} value={item.value}>
65
+ {item.label}
66
+ </SegmentedControl.Item>
67
+ ))}
68
+ </SegmentedControl.Root>
69
+ }
48
70
  onReorder={async (id, beforeId) => {
49
71
  await saveOrder();
50
72
  setStages((current) =>
@@ -2,8 +2,6 @@
2
2
  import { Button, DataTable, type DataTableColumn, Typography } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
5
- import styles from "./examples.module.css";
6
-
7
5
  type Member = { id: string; name: string; email: string; role: string };
8
6
 
9
7
  const MEMBERS: Member[] = [
@@ -35,25 +33,22 @@ export default function DataTableSelectionExample() {
35
33
  onSelectedChange={setSelected}
36
34
  paging="none"
37
35
  toolbar={
38
- <div className={styles.toolbar}>
36
+ <>
39
37
  <Typography as="span" variant="body-s" tone="secondary">
40
38
  Выбрано: {selected.length} из {MEMBERS.length}
41
39
  </Typography>
42
- <div className={styles.filters}>
43
- <Button.Root
44
- variant="outline"
45
- tone="neutral"
46
- size="s"
47
- disabled={selected.length === 0}
48
- onClick={() => setSelected([])}
49
- >
50
- Снять выбор
51
- </Button.Root>
52
- <Button.Root variant="soft" tone="danger" size="s" disabled={selected.length === 0}>
53
- Удалить
54
- </Button.Root>
55
- </div>
56
- </div>
40
+ <Button.Root
41
+ variant="outline"
42
+ tone="neutral"
43
+ disabled={selected.length === 0}
44
+ onClick={() => setSelected([])}
45
+ >
46
+ Снять выбор
47
+ </Button.Root>
48
+ <Button.Root variant="soft" tone="danger" disabled={selected.length === 0}>
49
+ Удалить
50
+ </Button.Root>
51
+ </>
57
52
  }
58
53
  />
59
54
  );
@@ -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,4 +1,4 @@
1
- /** A campaign table with its own search and status filter above it — two table-scoped controls; 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
4
  DataTable,
@@ -9,8 +9,6 @@ import {
9
9
  } from "prime-ui-kit";
10
10
  import * as React from "react";
11
11
 
12
- import styles from "./examples.module.css";
13
-
14
12
  type Status = "active" | "paused" | "archived";
15
13
  type Campaign = { id: string; name: string; status: Status; clicks: number; ctr: number };
16
14
 
@@ -86,8 +84,8 @@ export default function DataTableToolbarExample() {
86
84
  pageSize={5}
87
85
  empty="Ничего не найдено — измените запрос или фильтр"
88
86
  toolbar={
89
- <div className={styles.filters}>
90
- <Input.Root size="s" className={styles.search}>
87
+ <>
88
+ <Input.Root>
91
89
  <Input.Wrapper>
92
90
  <Input.Icon side="start">
93
91
  <Icon name="action.search" />
@@ -101,17 +99,12 @@ export default function DataTableToolbarExample() {
101
99
  />
102
100
  </Input.Wrapper>
103
101
  </Input.Root>
104
- <SegmentedControl.Root
105
- size="s"
106
- aria-label="Статус"
107
- value={status}
108
- onValueChange={setStatus}
109
- >
102
+ <SegmentedControl.Root aria-label="Статус" value={status} onValueChange={setStatus}>
110
103
  <SegmentedControl.Item value="all">Все</SegmentedControl.Item>
111
104
  <SegmentedControl.Item value="active">Активные</SegmentedControl.Item>
112
105
  <SegmentedControl.Item value="paused">На паузе</SegmentedControl.Item>
113
106
  </SegmentedControl.Root>
114
- </div>
107
+ </>
115
108
  }
116
109
  />
117
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,14 +30,14 @@ 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.SortableItem one item (li in ul/ol, tr in tbody, th in tr, div otherwise); the gap is drawn before it
33
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
37
38
  └─ Dnd.DropZone a box that takes dropped items
38
39
  ```
39
- A drag in flight is drawn by `Dnd.Root` as a clone of the lifted element above every layer, following the pointer. The clone flies inside empty copies (`display: contents`) of its ancestors up to the nearest surface (`data-depth`) and that surface's `data-theme`, so the host's variables and selectors (a List row's tier padding and radius) dress it exactly as the item in place, and it sets down without a jump; an item without a fill of its own flies on the fill of the surface it was lifted from (`--prime-color-layer-current`). A refusal with a reason shows the reason in the tooltip's chip under the clone. The page gets `data-dnd-active="mouse | touch | pen"` on `<html>` for the length of the gesture (the holding cursor), and every list of the carried item's kind gets `data-dnd-carrying`.
40
+ A drag in flight is drawn by `Dnd.Root` as a clone of the lifted element above every layer, following the pointer. The clone keeps the original's size and flies inside empty copies of all its ancestors up to the page — `display: contents`, except a size container (`container-type`), which flies as an invisible box of its measured content width, so its container queries resolve as at the source — and the host's variables, `[data-…]` selectors, surface and scoped theme (a List row's tier padding and radius, a narrow FieldList's stacked row) dress it exactly as the item in place, and it sets down without a jump; an item without a fill of its own flies on the fill of the surface it was lifted from (`--prime-color-layer-current`). A refusal with a reason shows the reason in the tooltip's chip under the clone. The page gets `data-dnd-active="mouse | touch | pen"` on `<html>` for the length of the gesture (the holding cursor), and every list of the carried item's kind gets `data-dnd-carrying`.
40
41
 
41
42
  ## API
42
43
 
@@ -67,19 +68,20 @@ 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. |
@@ -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` |
@@ -203,7 +207,7 @@ An item's own `onPointerDown` / `onKeyDown` run before the kit's: `preventDefaul
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.
204
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.
205
209
  - The reason chip stands `--prime-tooltip-offset` under the carried copy, start-aligned, at most `--prime-tooltip-max-width` wide.
206
- - 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.
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 the row's ancestors may dress it — the clone flies inside empty copies of all of them (a size container as a box of its width), so their variables, selectors and container queries still apply. A table row flies in a one-row copy of its table inside them.
207
211
  - A scrollable ancestor under the pointer auto-scrolls within 64 px of its edge (a `ScrollContainer` strip works).
208
212
  - Keep a zone's height stable between states; it only changes fill and outline.
209
213
 
@@ -258,6 +262,7 @@ An item's own `onPointerDown` / `onKeyDown` run before the kit's: `preventDefaul
258
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`. |
259
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`. |
260
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`. |
261
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`. |
262
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`. |
263
268
 
@@ -266,7 +271,7 @@ An item's own `onPointerDown` / `onKeyDown` run before the kit's: `preventDefaul
266
271
  - A `Dnd.SortableItem` `id` that differs from `getId(item)` → the item never moves.
267
272
  - Rendering items yourself instead of through `renderItem` → the list cannot draw the gap or the optimistic order.
268
273
  - Using `index` as the id → ids must survive a reorder.
269
- - Styling a row from an ancestor above the nearest surface (`data-depth`) → the flying clone loses it; keep the row's look on the row and its hosts inside the surface.
274
+ - Styling a row from its siblings (`li + li`, `:first-child`) → the flying clone stands alone and loses it; keep the row's look on the row and its ancestors.
270
275
  - Starting a drag from a button inside a row without `handle` → presses on controls are theirs; use `Dnd.Handle`.
271
276
  - A drag-only move with no alternative → add a button or a menu for the same action.
272
277
  - `aria-label` on an item that shows a title and a description → it hides them; point `aria-labelledby` / `aria-describedby` at them.
@@ -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
+ }