prime-ui-kit 0.18.0 → 0.19.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 (104) hide show
  1. package/dist/chunks/{chunk-OAXJWF3L.js → chunk-RZNGLELR.js} +120 -114
  2. package/dist/chunks/{chunk-MB4CXSLL.js → chunk-WYCTXD63.js} +832 -619
  3. package/dist/chunks/{chunk-X3JT5CLR.js → chunk-ZTZ2ML2B.js} +1 -1
  4. package/dist/color-picker.js +2 -2
  5. package/dist/components/data-table/ColumnsMenu.d.ts +16 -20
  6. package/dist/components/data-table/DataTable.d.ts +14 -11
  7. package/dist/components/data-table/Head.d.ts +8 -1
  8. package/dist/components/data-table/Row.d.ts +7 -1
  9. package/dist/components/data-table/StateRows.d.ts +3 -1
  10. package/dist/components/field-list/FieldList.d.ts +8 -8
  11. package/dist/components/index.d.ts +1 -1
  12. package/dist/components/kanban/Kanban.d.ts +4 -0
  13. package/dist/components/list/List.d.ts +9 -4
  14. package/dist/index.css +1 -1
  15. package/dist/index.js +2 -2
  16. package/dist/internal/fieldCell.d.ts +6 -0
  17. package/dist/internal/leaveGuard.d.ts +6 -0
  18. package/dist/internal/overlay/DialogGuard.d.ts +3 -2
  19. package/dist/internal/overlay/DialogParts.d.ts +14 -5
  20. package/dist/layout/index.d.ts +1 -1
  21. package/dist/layout/page/Page.d.ts +1 -7
  22. package/dist/skill/anti-slop.md +23 -0
  23. package/dist/skill/api-contract.md +1 -1
  24. package/dist/skill/cheatsheet.md +7 -3
  25. package/dist/skill/checklist.md +4 -0
  26. package/dist/skill/composition.md +70 -12
  27. package/dist/skill/layouts.md +1 -1
  28. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -1
  29. package/dist/skill/reference/components/data-table/COMPONENT.md +40 -40
  30. package/dist/skill/reference/components/data-table/examples/actions-column.tsx +154 -0
  31. package/dist/skill/reference/components/data-table/examples/columns-visibility.tsx +3 -12
  32. package/dist/skill/reference/components/data-table/examples/controlled.tsx +1 -1
  33. package/dist/skill/reference/components/data-table/examples/narrow.tsx +4 -11
  34. package/dist/skill/reference/components/data-table/examples/row-link.tsx +18 -22
  35. package/dist/skill/reference/components/data-table/examples/screen.tsx +12 -1
  36. package/dist/skill/reference/components/data-table/examples/toolbar.tsx +26 -32
  37. package/dist/skill/reference/components/dnd/COMPONENT.md +4 -3
  38. package/dist/skill/reference/components/dnd/examples/examples.module.css +4 -3
  39. package/dist/skill/reference/components/dnd/examples/handle.tsx +5 -5
  40. package/dist/skill/reference/components/dnd/examples/locked.tsx +1 -1
  41. package/dist/skill/reference/components/dnd/examples/table.tsx +6 -6
  42. package/dist/skill/reference/components/drawer/COMPONENT.md +26 -10
  43. package/dist/skill/reference/components/drawer/examples/header-actions.tsx +1 -1
  44. package/dist/skill/reference/components/drawer/examples/loading.tsx +58 -0
  45. package/dist/skill/reference/components/field-list/COMPONENT.md +29 -25
  46. package/dist/skill/reference/components/field-list/examples/base-row.tsx +2 -1
  47. package/dist/skill/reference/components/field-list/examples/locked.tsx +11 -11
  48. package/dist/skill/reference/components/field-list/examples/reorder.tsx +1 -1
  49. package/dist/skill/reference/components/field-list/examples/screen.module.css +1 -1
  50. package/dist/skill/reference/components/field-list/examples/screen.tsx +168 -162
  51. package/dist/skill/reference/components/field-list/examples/totals.tsx +9 -8
  52. package/dist/skill/reference/components/kanban/COMPONENT.md +6 -2
  53. package/dist/skill/reference/components/kanban/examples/states.tsx +7 -5
  54. package/dist/skill/reference/components/list/COMPONENT.md +21 -11
  55. package/dist/skill/reference/components/list/examples/reorder.tsx +42 -7
  56. package/dist/skill/reference/components/modal/COMPONENT.md +23 -10
  57. package/dist/skill/reference/components/segmented-control/COMPONENT.md +3 -2
  58. package/dist/skill/reference/components/sheet/COMPONENT.md +18 -7
  59. package/dist/skill/reference/layout/page/COMPONENT.md +5 -12
  60. package/dist/skill/reference/layout/page/examples/screen.module.css +1 -1
  61. package/dist/skill/reference/layout/page/examples/screen.tsx +93 -104
  62. package/dist/skill/reference/layout/page/examples/sections.tsx +2 -15
  63. package/dist/skill/responsive.md +7 -5
  64. package/dist/tokens/semantic.d.ts +1 -1
  65. package/dist/tokens/themes/dark.d.ts +1 -1
  66. package/package.json +1 -1
  67. package/src/components/confirm-dialog/COMPONENT.md +1 -1
  68. package/src/components/data-table/COMPONENT.md +40 -40
  69. package/src/components/data-table/examples/actions-column.tsx +154 -0
  70. package/src/components/data-table/examples/columns-visibility.tsx +3 -12
  71. package/src/components/data-table/examples/controlled.tsx +1 -1
  72. package/src/components/data-table/examples/narrow.tsx +4 -11
  73. package/src/components/data-table/examples/row-link.tsx +18 -22
  74. package/src/components/data-table/examples/screen.tsx +12 -1
  75. package/src/components/data-table/examples/toolbar.tsx +26 -32
  76. package/src/components/dnd/COMPONENT.md +4 -3
  77. package/src/components/dnd/examples/examples.module.css +4 -3
  78. package/src/components/dnd/examples/handle.tsx +5 -5
  79. package/src/components/dnd/examples/locked.tsx +1 -1
  80. package/src/components/dnd/examples/table.tsx +6 -6
  81. package/src/components/drawer/COMPONENT.md +26 -10
  82. package/src/components/drawer/examples/header-actions.tsx +1 -1
  83. package/src/components/drawer/examples/loading.tsx +58 -0
  84. package/src/components/field-list/COMPONENT.md +29 -25
  85. package/src/components/field-list/examples/base-row.tsx +2 -1
  86. package/src/components/field-list/examples/locked.tsx +11 -11
  87. package/src/components/field-list/examples/reorder.tsx +1 -1
  88. package/src/components/field-list/examples/screen.module.css +1 -1
  89. package/src/components/field-list/examples/screen.tsx +168 -162
  90. package/src/components/field-list/examples/totals.tsx +9 -8
  91. package/src/components/kanban/COMPONENT.md +6 -2
  92. package/src/components/kanban/examples/states.tsx +7 -5
  93. package/src/components/list/COMPONENT.md +21 -11
  94. package/src/components/list/examples/reorder.tsx +42 -7
  95. package/src/components/modal/COMPONENT.md +23 -10
  96. package/src/components/segmented-control/COMPONENT.md +3 -2
  97. package/src/components/sheet/COMPONENT.md +18 -7
  98. package/src/layout/page/COMPONENT.md +5 -12
  99. package/src/layout/page/examples/screen.module.css +1 -1
  100. package/src/layout/page/examples/screen.tsx +93 -104
  101. package/src/layout/page/examples/sections.tsx +2 -15
  102. package/src/styles/theme-dark.css +1 -1
  103. package/src/styles/theme-light.css +1 -1
  104. package/dist/components/data-table/context.d.ts +0 -14
@@ -1,4 +1,4 @@
1
- /** A record editor as a document page, a long form whose title, description and save stand in the content header: a plan's name, its specs and prices in one form; repeating records as FieldList rows with the headings once, a base price pinned above its changes, one value in three currencies in one row — `getLocked`, `compact`. */
1
+ /** A record editor as a document page, a long form whose title, description and save stand in the content header and whose blocks are titled cards one under another: a plan's name, its specs and prices; repeating records as FieldList rows with the headings once, an icon and its color side by side, a base price pinned above its changes, one value in three currencies in one row — `getLocked`, `compact`. */
2
2
  import {
3
3
  Button,
4
4
  Card,
@@ -29,9 +29,9 @@ type Period = { id: string; from: string; price: string; base?: boolean };
29
29
 
30
30
  const SPEC_COLUMNS: FieldListColumn[] = [
31
31
  { label: "Иконка", width: "compact" },
32
+ { label: "Цвет", width: "compact" },
32
33
  { label: "Параметр" },
33
34
  { label: "Значение" },
34
- { label: "Цвет", width: "compact" },
35
35
  ];
36
36
 
37
37
  const SPEC_ICONS: { name: IconName; label: string }[] = [
@@ -97,6 +97,7 @@ const SAVED_MS = 2000;
97
97
 
98
98
  export default function FieldListScreenExample() {
99
99
  const formId = React.useId();
100
+ const specsTitleId = React.useId();
100
101
  const [specs, setSpecs] = React.useState(SPECS);
101
102
  const [periods, setPeriods] = React.useState(PERIODS);
102
103
  const [nameError, setNameError] = React.useState<string>();
@@ -134,182 +135,187 @@ export default function FieldListScreenExample() {
134
135
  </Page.Actions>
135
136
  </Page.Header>
136
137
  <Page.Body>
137
- <Page.Section layout="aside" label="Основное">
138
- <form id={formId} noValidate onSubmit={save}>
139
- <Card.Root variant="panel">
140
- <Card.Body>
141
- <Fieldset disabled={saving} aria-label="Основное">
142
- <Input.Root label="Название" error={nameError}>
138
+ <form id={formId} noValidate onSubmit={save}>
139
+ <Card.Root variant="panel">
140
+ <Card.Header>
141
+ <Card.Title as="h2">Основное</Card.Title>
142
+ </Card.Header>
143
+ <Card.Body>
144
+ <Fieldset disabled={saving} aria-label="Основное">
145
+ <Input.Root label="Название" error={nameError}>
146
+ <Input.Wrapper>
147
+ <Input.Field name="name" defaultValue="Команда" />
148
+ </Input.Wrapper>
149
+ </Input.Root>
150
+ <Textarea.Root
151
+ name="summary"
152
+ label="Описание"
153
+ optional
154
+ rows={2}
155
+ defaultValue="Для отделов до 50 человек: задачи, документы и отчёты в одном месте."
156
+ />
157
+ </Fieldset>
158
+ </Card.Body>
159
+ </Card.Root>
160
+ </form>
161
+ <Card.Root variant="panel">
162
+ <Card.Header>
163
+ <Card.Title as="h2" id={specsTitleId}>
164
+ Характеристики
165
+ </Card.Title>
166
+ <Card.Description>Первые три закреплены: их видно в карточке тарифа.</Card.Description>
167
+ </Card.Header>
168
+ <Card.Body>
169
+ <FieldList
170
+ aria-labelledby={specsTitleId}
171
+ disabled={saving}
172
+ columns={SPEC_COLUMNS}
173
+ reorderable
174
+ value={specs}
175
+ onValueChange={setSpecs}
176
+ getId={(spec) => spec.id}
177
+ getLabel={(spec) => spec.name}
178
+ getLocked={(spec) => spec.pinned ?? false}
179
+ createItem={(): Spec => ({
180
+ id: crypto.randomUUID(),
181
+ icon: "object.globe",
182
+ name: "",
183
+ value: "",
184
+ color: "#14b8a6",
185
+ })}
186
+ addLabel="Добавить характеристику"
187
+ renderItem={(spec, row) => (
188
+ <>
189
+ <Select.Root
190
+ value={spec.icon}
191
+ onValueChange={(icon) => row.update({ icon: icon as IconName })}
192
+ >
193
+ <Select.Trigger>
194
+ <Select.TriggerIcon>
195
+ <Icon name={spec.icon} />
196
+ </Select.TriggerIcon>
197
+ </Select.Trigger>
198
+ <Select.Content>
199
+ {SPEC_ICONS.map((icon) => (
200
+ <Select.Item key={icon.name} value={icon.name}>
201
+ <Select.ItemIcon>
202
+ <Icon name={icon.name} />
203
+ </Select.ItemIcon>
204
+ {icon.label}
205
+ </Select.Item>
206
+ ))}
207
+ </Select.Content>
208
+ </Select.Root>
209
+ <ColorPresets.Root
210
+ value={spec.color}
211
+ onValueChange={(color) => row.update({ color: color ?? spec.color })}
212
+ >
213
+ <ColorPresets.Trigger>
214
+ <ColorPresets.SwatchButton />
215
+ </ColorPresets.Trigger>
216
+ <ColorPresets.Content />
217
+ </ColorPresets.Root>
218
+ <Input.Root>
143
219
  <Input.Wrapper>
144
- <Input.Field name="name" defaultValue="Команда" />
220
+ <Input.Field
221
+ value={spec.name}
222
+ onChange={(event) => row.update({ name: event.target.value })}
223
+ />
145
224
  </Input.Wrapper>
146
225
  </Input.Root>
147
- <Textarea.Root
148
- name="summary"
149
- label="Описание"
150
- optional
151
- rows={2}
152
- defaultValue="Для отделов до 50 человек: задачи, документы и отчёты в одном месте."
153
- />
154
- </Fieldset>
155
- </Card.Body>
156
- </Card.Root>
157
- </form>
158
- </Page.Section>
159
-
160
- <Page.Section layout="aside" label="Характеристики">
161
- <Card.Root variant="panel">
162
- <Card.Body>
226
+ <Input.Root>
227
+ <Input.Wrapper>
228
+ <Input.Field
229
+ value={spec.value}
230
+ onChange={(event) => row.update({ value: event.target.value })}
231
+ />
232
+ </Input.Wrapper>
233
+ </Input.Root>
234
+ </>
235
+ )}
236
+ />
237
+ </Card.Body>
238
+ </Card.Root>
239
+ <Card.Root variant="panel">
240
+ <Card.Header>
241
+ <Card.Title as="h2">Стоимость</Card.Title>
242
+ </Card.Header>
243
+ <Card.Body className={styles.groups}>
244
+ <Fieldset label="За месяц" disabled={saving}>
163
245
  <FieldList
164
- aria-label="Характеристики"
165
- disabled={saving}
166
- columns={SPEC_COLUMNS}
167
- reorderable
168
- value={specs}
169
- onValueChange={setSpecs}
170
- getId={(spec) => spec.id}
171
- getLabel={(spec) => spec.name}
172
- getLocked={(spec) => spec.pinned ?? false}
173
- createItem={(): Spec => ({
174
- id: crypto.randomUUID(),
175
- icon: "object.globe",
176
- name: "",
177
- value: "",
178
- color: "#14b8a6",
179
- })}
180
- addLabel="Добавить характеристику"
181
- renderItem={(spec, row) => (
246
+ columns={PRICE_COLUMNS}
247
+ value={periods}
248
+ onValueChange={setPeriods}
249
+ getId={(period) => period.id}
250
+ getLocked={(period) => period.base ?? false}
251
+ labels={{ lockedReason: "Базовую цену нельзя удалить" }}
252
+ createItem={() => ({ id: crypto.randomUUID(), from: "", price: "" })}
253
+ addLabel="Добавить изменение цены"
254
+ renderItem={(period, row) => (
182
255
  <>
183
- <Select.Root
184
- value={spec.icon}
185
- onValueChange={(icon) => row.update({ icon: icon as IconName })}
186
- >
187
- <Select.Trigger>
188
- <Select.TriggerIcon>
189
- <Icon name={spec.icon} />
190
- </Select.TriggerIcon>
191
- </Select.Trigger>
192
- <Select.Content>
193
- {SPEC_ICONS.map((icon) => (
194
- <Select.Item key={icon.name} value={icon.name}>
195
- <Select.ItemIcon>
196
- <Icon name={icon.name} />
197
- </Select.ItemIcon>
198
- {icon.label}
199
- </Select.Item>
200
- ))}
201
- </Select.Content>
202
- </Select.Root>
203
- <Input.Root>
204
- <Input.Wrapper>
205
- <Input.Field
206
- value={spec.name}
207
- onChange={(event) => row.update({ name: event.target.value })}
208
- />
209
- </Input.Wrapper>
210
- </Input.Root>
211
- <Input.Root>
212
- <Input.Wrapper>
213
- <Input.Field
214
- value={spec.value}
215
- onChange={(event) => row.update({ value: event.target.value })}
216
- />
217
- </Input.Wrapper>
218
- </Input.Root>
219
- <ColorPresets.Root
220
- value={spec.color}
221
- onValueChange={(color) => row.update({ color: color ?? spec.color })}
222
- >
223
- <ColorPresets.Trigger>
224
- <ColorPresets.SwatchButton />
225
- </ColorPresets.Trigger>
226
- <ColorPresets.Content />
227
- </ColorPresets.Root>
228
- </>
229
- )}
230
- />
231
- </Card.Body>
232
- </Card.Root>
233
- </Page.Section>
234
-
235
- <Page.Section layout="aside" label="Стоимость">
236
- <Card.Root variant="panel">
237
- <Card.Body className={styles.groups}>
238
- <Fieldset label="За месяц" disabled={saving}>
239
- <FieldList
240
- columns={PRICE_COLUMNS}
241
- value={periods}
242
- onValueChange={setPeriods}
243
- getId={(period) => period.id}
244
- getLocked={(period) => period.base ?? false}
245
- createItem={() => ({ id: crypto.randomUUID(), from: "", price: "" })}
246
- addLabel="Добавить изменение цены"
247
- renderItem={(period, row) => (
248
- <>
249
- {period.base ? (
250
- <Input.Root>
251
- <Input.Wrapper>
252
- <Input.Field value="Всегда, если нет изменений" readOnly />
253
- </Input.Wrapper>
254
- </Input.Root>
255
- ) : (
256
- <Select.Root
257
- value={period.from}
258
- onValueChange={(from) => row.update({ from })}
259
- placeholder="Выберите месяц"
260
- >
261
- <Select.Trigger>
262
- <Select.Value />
263
- </Select.Trigger>
264
- <Select.Content>
265
- {MONTHS.map((month) => (
266
- <Select.Item key={month.value} value={month.value}>
267
- {month.label}
268
- </Select.Item>
269
- ))}
270
- </Select.Content>
271
- </Select.Root>
272
- )}
256
+ {period.base ? (
273
257
  <Input.Root>
274
258
  <Input.Wrapper>
275
- <Input.Field
276
- value={period.price}
277
- inputMode="decimal"
278
- placeholder="0"
279
- onChange={(event) => row.update({ price: event.target.value })}
280
- />
281
- <Input.InlineAffix side="end">₽</Input.InlineAffix>
259
+ <Input.Field value="Всегда, если нет изменений" readOnly />
282
260
  </Input.Wrapper>
283
261
  </Input.Root>
284
- </>
285
- )}
286
- />
287
- </Fieldset>
288
- <Fieldset
289
- label="Подключение"
290
- description="Разовый платёж; пустая валюта не выставляется."
291
- disabled={saving}
292
- >
293
- <div className={styles.currencies}>
294
- {SETUP_FEE.map((fee) => (
295
- <Input.Root key={fee.name}>
262
+ ) : (
263
+ <Select.Root
264
+ value={period.from}
265
+ onValueChange={(from) => row.update({ from })}
266
+ placeholder="Выберите месяц"
267
+ >
268
+ <Select.Trigger>
269
+ <Select.Value />
270
+ </Select.Trigger>
271
+ <Select.Content>
272
+ {MONTHS.map((month) => (
273
+ <Select.Item key={month.value} value={month.value}>
274
+ {month.label}
275
+ </Select.Item>
276
+ ))}
277
+ </Select.Content>
278
+ </Select.Root>
279
+ )}
280
+ <Input.Root>
296
281
  <Input.Wrapper>
297
282
  <Input.Field
298
- name={fee.name}
299
- aria-label={fee.label}
283
+ value={period.price}
300
284
  inputMode="decimal"
301
285
  placeholder="0"
302
- defaultValue={fee.value}
286
+ onChange={(event) => row.update({ price: event.target.value })}
303
287
  />
304
- <Input.InlineAffix side="end">{fee.currency}</Input.InlineAffix>
288
+ <Input.InlineAffix side="end">₽</Input.InlineAffix>
305
289
  </Input.Wrapper>
306
290
  </Input.Root>
307
- ))}
308
- </div>
309
- </Fieldset>
310
- </Card.Body>
311
- </Card.Root>
312
- </Page.Section>
291
+ </>
292
+ )}
293
+ />
294
+ </Fieldset>
295
+ <Fieldset
296
+ label="Подключение"
297
+ description="Разовый платёж; пустая валюта не выставляется."
298
+ disabled={saving}
299
+ >
300
+ <div className={styles.currencies}>
301
+ {SETUP_FEE.map((fee) => (
302
+ <Input.Root key={fee.name}>
303
+ <Input.Wrapper>
304
+ <Input.Field
305
+ name={fee.name}
306
+ aria-label={fee.label}
307
+ inputMode="decimal"
308
+ placeholder="0"
309
+ defaultValue={fee.value}
310
+ />
311
+ <Input.InlineAffix side="end">{fee.currency}</Input.InlineAffix>
312
+ </Input.Wrapper>
313
+ </Input.Root>
314
+ ))}
315
+ </div>
316
+ </Fieldset>
317
+ </Card.Body>
318
+ </Card.Root>
313
319
  </Page.Body>
314
320
  </Page.Root>
315
321
  );
@@ -1,4 +1,4 @@
1
- /** A deal bonus split between the team: the share takes a narrow column, and the totals row under the lines sums the shares and the amounts — `renderTotals`, `width`. */
1
+ /** A deal bonus split between the team: the share takes a narrow column, and the totals row under the lines sums the shares and the amounts, each ending where its fields end their numbers — `renderTotals`, `width`. */
2
2
  import { FieldList, type FieldListColumn, Input } from "prime-ui-kit";
3
3
  import * as React from "react";
4
4
 
@@ -18,11 +18,7 @@ const SHARES: Share[] = [
18
18
  { id: "kotova", member: "Ирина Котова", percent: "20" },
19
19
  ];
20
20
 
21
- const RUB = new Intl.NumberFormat("ru-RU", {
22
- style: "currency",
23
- currency: "RUB",
24
- maximumFractionDigits: 0,
25
- });
21
+ const AMOUNT = new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 0 });
26
22
 
27
23
  const percentOf = (share: Share) => Number(share.percent.replace(",", ".")) || 0;
28
24
 
@@ -62,7 +58,12 @@ export default function FieldListTotalsExample() {
62
58
  </Input.Root>
63
59
  <Input.Root>
64
60
  <Input.Wrapper>
65
- <Input.Field value={RUB.format((BONUS * percentOf(share)) / 100)} readOnly />
61
+ <Input.Field
62
+ value={AMOUNT.format((BONUS * percentOf(share)) / 100)}
63
+ inputMode="decimal"
64
+ readOnly
65
+ />
66
+ <Input.InlineAffix side="end">₽</Input.InlineAffix>
66
67
  </Input.Wrapper>
67
68
  </Input.Root>
68
69
  </>
@@ -73,7 +74,7 @@ export default function FieldListTotalsExample() {
73
74
  <>
74
75
  {null}
75
76
  {`${percent} %`}
76
- {RUB.format((BONUS * percent) / 100)}
77
+ {`${AMOUNT.format((BONUS * percent) / 100)} ₽`}
77
78
  </>
78
79
  );
79
80
  }}
@@ -31,6 +31,7 @@ import {
31
31
  ## Anatomy
32
32
  ```
33
33
  Kanban.Root board: horizontal strip of columns, its own Dnd.Root if none above
34
+ ├─ placeholder columns while `loading` and `columns` is empty: `loadingColumns` columns of Skeleton title and cards
34
35
  └─ <ul> (Dnd.Sortable, axis x) the columns in `columnOrder`
35
36
  └─ <li> column (from `columns`) no well with a header plate (`plain`) or a header line (`line`), or a `fill-muted` well (`well`)
36
37
  ├─ header title (`columnTitleAs`, h3) · count Badge (the stage color) · renderColumnActions(column) · [grip Dnd.Handle, a lock when `locked`]
@@ -76,6 +77,7 @@ Generic `Kanban.Root<T>`; `ref` → `HTMLDivElement`. The board: a strip of colu
76
77
  | `canDrop` | `(id: string, columnId: string) => boolean \| string` | — | A workflow rule: refuses card `id` in column `columnId` — `false`, or a string that says why (spoken and shown under the carried card). The column turns `danger` before the release. |
77
78
  | `variant` | `"plain" \| "line" \| "well"` | `"plain"` | No wells: `plain` — a filled header plate over the cards, `line` — a bare header over a faint line; cards stand on the host. `well`: every column is a filled well holding its cards (bounded queues). |
78
79
  | `loading` | `boolean` | `false` | Columns show skeleton cards in the card geometry; the cards cross-fade in when it turns off. |
80
+ | `loadingColumns` | `number` | `3` | Placeholder columns a `loading` board draws while `columns` is empty (the stages are still loading); the columns fade in where they stood. |
79
81
  | `disabled` | `boolean` | `false` | Read-only board: no drag, no keyboard moves, no column grips; `onItemOpen` and `getItemHref` still open cards. |
80
82
  | `labels` | `Partial<KanbanLabels>` | — | Built-in strings, see Labels. |
81
83
  | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children" \| "defaultValue">` | — | `className` (the board height), `aria-label` and the other div attributes. |
@@ -144,6 +146,7 @@ Do not wrap a `plain` board's columns in your own boxes, and do not put a `well`
144
146
  | `reorderableColumns` | a ghost grip at the end of every column header, after the actions; the column is carried by it | the user arranges the stages | `false` |
145
147
  | `canDrop` | the column turns `danger` under a refused card; a string reason is spoken and shown under the carried card; a keyboard move is announced as refused | workflow rules (a blocked ticket cannot be closed) | — |
146
148
  | `loading` | skeleton cards in the card geometry, no counts | the first load or a refresh | `false` |
149
+ | `loadingColumns` | placeholder columns (a Skeleton title, Skeleton cards) while `loading` and `columns` is empty | the stages themselves come from the server | `3` |
147
150
  | `disabled` | cards do not drag, Alt + arrows do nothing | read-only boards, no permission | `false` |
148
151
 
149
152
  ## States
@@ -158,12 +161,13 @@ Do not wrap a `plain` board's columns in your own boxes, and do not put a `well`
158
161
  | full | count ≥ `limit` | `data-full` on the column, orange count `count / limit` |
159
162
  | empty | no cards | a compact `EmptyPage` in Dnd's dashed drop slot at the top (`fill-strong` dashed line, no fill): «Нет карточек» and the hint «Перетащите сюда карточку»; a `disabled` board shows the text alone; the whole column stays a drop area |
160
163
  | loading | `loading` | `aria-busy` and `data-loading` on the root; `Skeleton` cards; `Crossfade` swaps them for the cards |
164
+ | loading, no columns yet | `loading` with empty `columns` | an `aria-hidden` row of `loadingColumns` placeholder columns; the columns fade in where they stood (`enterMotion`) |
161
165
  | disabled | `disabled` / `Kanban.Item disabled` | `data-disabled` on the root / card; no `aria-roledescription`, no shortcuts, no column grips |
162
166
  | saving | `onValueChange` / `onColumnOrderChange` returned a pending promise | the card (column) stays in its new place with `aria-busy` and `data-pending`; a card's content quiets to 0.6 under a centred `Spinner`, a column's heading quiets and its grip is busy (Dnd); it does not move again until the answer |
163
167
  | opens | `onItemOpen` / `getItemHref` | `data-clickable` (in place) or `data-link` (a link over the card, its focus ring drawn on the card); a card that opens lifts on hover on a read-only board too |
164
168
  | refused save | the promise rejected | the card glides back to its old column and place (every card it pushed aside glides too), a column glides back in the strip; `labels.moveFailed` / `labels.columnMoveFailed` is announced |
165
169
 
166
- Motion: the hover lift (shadow, the card rises above its neighbours) and the empty slot fade (`fast`, `standard`); a card dropped under the mouse lands straight into its hover lift (`data-dnd-hover`); the swap from skeletons is `Crossfade`; drag motion is Dnd's (lift, gap, glide, landing); a saving card or column quiets (`fast`, both ways) and its spinner fades in by itself; a refused save glides back (`base`, the list's FLIP glide), never jumps. Under `prefers-reduced-motion` all of it is off.
170
+ Motion: the hover lift (shadow, the card rises above its neighbours) and the empty slot fade (`fast`, `standard`); a card dropped under the mouse lands straight into its hover lift (`data-dnd-hover`); the swap from skeletons is `Crossfade`, the columns that replace placeholder columns fade in; drag motion is Dnd's (lift, gap, glide, landing); a saving card or column quiets (`fast`, both ways) and its spinner fades in by itself; a refused save glides back (`base`, the list's FLIP glide), never jumps. Under `prefers-reduced-motion` all of it is off.
167
171
 
168
172
  A card's own `onKeyDown` runs before the board's: `preventDefault()` in it keeps the move from happening. A controlled owner applies the new `value` (or `columnOrder`) once the request succeeds — in the promise chain, before the returned promise resolves — never before the answer.
169
173
 
@@ -231,7 +235,7 @@ A card's own `onKeyDown` runs before the board's: `preventDefault()` in it keeps
231
235
  | [columns.tsx](examples/columns.tsx) | A sales pipeline whose stages move by the grip in their header while the first and the last stay locked; each stage carries its color on the count, a rule and its figures in two lines under the title, and is edited in a popover; the page keeps the order — `reorderableColumns`, `columnOrder`, `onColumnOrderChange`, `locked`, `color`, `description`, `renderColumnMeta`, `renderColumnActions`. |
232
236
  | [saving.tsx](examples/saving.tsx) | Every move is saved on the server: the card waits busy in its new place, and an invoice the server refuses to close glides back to its column — `onValueChange`. |
233
237
  | [item-link.tsx](examples/item-link.tsx) | Tickets that open: each card is a link to its page, so ⌘-click or a middle click opens a new tab, a plain click, Enter or Space opens it in place, and a drag only moves it — `getItemHref`, `onItemOpen`. |
234
- | [states.tsx](examples/states.tsx) | Loading draws skeleton cards in the card geometry; an empty column keeps its drop area and a closed order stays in place while the rest move; a read-only board drags nothing — `loading`, `disabled`. |
238
+ | [states.tsx](examples/states.tsx) | Loading draws skeleton cards in the card geometry, and placeholder columns before the columns arrive; an empty column keeps its drop area and a closed order stays in place while the rest move; a read-only board drags nothing — `loading`, `loadingColumns`, `disabled`. |
235
239
  | [controlled.tsx](examples/controlled.tsx) | The placement lives in the parent: every move arrives with its source, target and position to save, and a button resets the board — `value`, `onValueChange`. |
236
240
  | [narrow.tsx](examples/narrow.tsx) | On a phone-width screen a column takes 85% of the board and snaps into place, so the next one peeks; the strip scrolls inside the board, never the page — `Kanban.Root`, `columns`. |
237
241
  | [screen.tsx](examples/screen.tsx) | A sales pipeline as a work screen: the title with the open deals and their sum, search, filter, the board / list switch and the one primary action in its top bar, a full-height Kanban of deals by stage with totals, moves saved on the server that can be refused, and the deal in a Drawer — `value`, `onValueChange`, `renderColumnMeta`, `onItemOpen`. |
@@ -1,4 +1,4 @@
1
- /** Loading draws skeleton cards in the card geometry; an empty column keeps its drop area and a closed order stays in place while the rest move; a read-only board drags nothing — `loading`, `disabled`. */
1
+ /** Loading draws skeleton cards in the card geometry, and placeholder columns before the columns arrive; an empty column keeps its drop area and a closed order stays in place while the rest move; a read-only board drags nothing — `loading`, `loadingColumns`, `disabled`. */
2
2
  import { Badge, Kanban, Typography } from "prime-ui-kit";
3
3
 
4
4
  type Order = { id: string; title: string; total: string; closed?: boolean };
@@ -24,9 +24,10 @@ const PLACEMENT = {
24
24
  };
25
25
 
26
26
  const STATES = [
27
- { caption: "loading", loading: true, disabled: false },
28
- { caption: "Kanban.Item disabled", loading: false, disabled: false },
29
- { caption: "disabled", loading: false, disabled: true },
27
+ { caption: "loading · columns=[]", columns: [], loading: true, disabled: false },
28
+ { caption: "loading", columns: COLUMNS, loading: true, disabled: false },
29
+ { caption: "Kanban.Item disabled", columns: COLUMNS, loading: false, disabled: false },
30
+ { caption: "disabled", columns: COLUMNS, loading: false, disabled: true },
30
31
  ];
31
32
 
32
33
  export default function KanbanStatesExample() {
@@ -35,7 +36,8 @@ export default function KanbanStatesExample() {
35
36
  <div>
36
37
  <Kanban.Root
37
38
  aria-label={`Заказы склада: ${state.caption}`}
38
- columns={COLUMNS}
39
+ columns={state.columns}
40
+ loadingColumns={COLUMNS.length}
39
41
  items={ORDERS}
40
42
  getId={(order) => order.id}
41
43
  getLabel={(order) => order.id}
@@ -28,13 +28,14 @@ import { List } from "prime-ui-kit";
28
28
  ```
29
29
  List.Root <ul aria-label>; size (else the host tier) → row tier; onReorder → Dnd.Sortable <ul>
30
30
  └─ List.Item <li> with a full-width <button> | <a href> | asChild element; current, disabled
31
- │ value; with onReorder: Dnd.Handle (busy while saving) before the row element
31
+ │ value; with onReorder: Dnd.Handle (busy while saving) at the row's end, after its actions
32
32
  ├─ List.ItemIcon leading Avatar.Root / Icon / Badge.Dot (aria-hidden); after the text — a trailing glyph
33
33
  ├─ List.ItemText primary line, truncates
34
34
  ├─ List.ItemDescription second line, muted, truncates
35
35
  ├─ List.ItemMeta trailing small text (time, date, amount)
36
36
  ├─ List.ItemCount trailing Badge, one tier down
37
- └─ List.ItemActions the row's own controls (Switch, Dropdown): beside the row element, one tier down
37
+ └─ List.ItemActions the row's own controls (Switch, Dropdown): beside the row element, one tier down;
38
+ side="end" after it, side="start" before it (a reorderable row's toggle)
38
39
  ```
39
40
 
40
41
  ## API
@@ -47,7 +48,7 @@ List.Root <ul aria-label>; size (else the host tier) → row t
47
48
  | Prop | Type | Default | Description |
48
49
  |---|---|---|---|
49
50
  | `size` | `"xs" \| "s" \| "m" \| "l" \| "xl"` | host tier, else `"m"` | Row tier: height, text, icon and avatar size; counts take one tier down. Without it the tier of its host, else `m`. |
50
- | `onReorder` | `(id: string, beforeId: string \| null) => void \| Promise<void>` | — | Rows reorder by a leading handle (drag, touch hold, Alt + ↑ / ↓): item `value` `id` goes before `beforeId` (`null` = last). Apply with `moveBefore`; a returned promise keeps the row busy, a rejection moves it back. |
51
+ | `onReorder` | `(id: string, beforeId: string \| null) => void \| Promise<void>` | — | Rows reorder by an end handle (drag, touch hold, Alt + ↑ / ↓): item `value` `id` goes before `beforeId` (`null` = last). Apply with `moveBefore`; a returned promise keeps the row busy, a rejection moves it back. |
51
52
  | `labels` | `Partial<ListLabels>` | — | Built-in strings, see Labels. |
52
53
  | `aria-label` | `string` | — | Name of the list for screen readers («Обращения»), when no visible heading names it. |
53
54
  | `children` | `ReactNode` | — | `List.Item`s. |
@@ -108,10 +109,11 @@ List.Root <ul aria-label>; size (else the host tier) → row t
108
109
  | `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children" \| "color">` | — | `className` and the other `span` attributes. |
109
110
 
110
111
  ### List.ItemActions
111
- `ref` → `HTMLDivElement`. The row's own controls (a `Switch`, a `Dropdown` with its icon `Button`) at its end, beside the row element and never inside it, one tier below the row.
112
+ `ref` → `HTMLDivElement`. The row's own controls (a `Switch`, a `Dropdown` with its icon `Button`) at its end or its start, beside the row element and never inside it, one tier below the row.
112
113
 
113
114
  | Prop | Type | Default | Description |
114
115
  |---|---|---|---|
116
+ | `side` | `"start" \| "end"` | `"end"` | `"end"` after the row element: a plain settings row, label first. `"start"` before it, in the DOM too: the on / off of a reorderable row reads first and the grip keeps the end. |
115
117
  | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children` (the controls, each named on its own), `className` and the other `div` attributes. |
116
118
 
117
119
  ## Variants
@@ -137,6 +139,12 @@ A second line or an avatar makes a row taller than the one-line height; the vert
137
139
  ### List.ItemCount color
138
140
  `gray` (default) for a plain number; a hue for what needs attention (`blue` for unread). Any palette hue.
139
141
 
142
+ ### List.ItemActions side
143
+ | Value | Looks like | Use when | Default |
144
+ |---|---|---|---|
145
+ | `end` | the controls after the row's text and trail | a plain row: the label first, the toggle or ⋯ menu at the end | yes |
146
+ | `start` | the controls before the row's icon and text, first in the DOM and the Tab order | a reorderable row's on / off: the state reads first, the grip keeps the end edge | |
147
+
140
148
  ## States
141
149
  | State | Driven by | DOM |
142
150
  |---|---|---|
@@ -146,9 +154,9 @@ A second line or an avatar makes a row taller than the one-line height; the vert
146
154
  | current | `current` | `aria-current="true"` (button) or `"page"` (link); `fill-muted-hover` fill, primary text |
147
155
  | focus-visible | keyboard | the focus ring inside the row edge |
148
156
  | disabled | `disabled` | `disabled` on a button; `aria-disabled="true"`, no `href` on a link, `tabindex="-1"` on an `asChild` element; `data-disabled="true"`; disabled text; in a reorderable list the handle is disabled and the row holds its place |
149
- | reorderable | `onReorder` | `data-reorderable="true"` on the root (a `Dnd.Sortable` `<ul>`); each `<li>` is a sortable item (`data-dnd-item` = `value`) with a leading `Dnd.Handle`; drag by the handle (a touch hold on touch), Alt + ↑ / ↓ from the handle or the row; a lifted row leaves a dashed gap in the row's radius, its neighbours glide, the copy (handle and row, in the row's padding and radius) follows the pointer and sets down in its new place without a jump |
157
+ | reorderable | `onReorder` | `data-reorderable="true"` on the root (a `Dnd.Sortable` `<ul>`); each `<li>` is a sortable item (`data-dnd-item` = `value`) with a `Dnd.Handle` at its end; drag by the handle (a touch hold on touch), Alt + ↑ / ↓ from the handle or the row; a lifted row leaves a dashed gap in the row's radius, its neighbours glide, the copy (handle and row, in the row's padding and radius) follows the pointer and sets down in its new place without a jump |
150
158
  | saving a move | `onReorder` returned a pending promise | the row stays in its new place, `aria-busy="true"`, the handle is busy (Dnd: a spinner in the disabled grip), the row content dims to 0.6 (`--prime-motion-fade-fast` both ways); a rejection glides the row back |
151
- | own controls | `List.ItemActions` in the item | the row element and the controls stand in one frame that takes the hover, press and current fills for both; the element itself stays clear |
159
+ | own controls | `List.ItemActions` in the item | the row element and the controls stand in one frame that takes the hover, press and current fills for both; the element itself stays clear; `side="start"` controls come before the element in the frame, `data-side` on the actions |
152
160
  | rows come and go | the data adds or takes out a `List.Item` (`value`, else its key) | a new row rises in once (`enterMotion`), a removed one closes up in place (`listPresence`); the first render stays still |
153
161
 
154
162
  Root attributes: `data-size`, `role="list"` (the list style reset would drop the list semantics in WebKit). The fill changes on `background-color` with `--prime-motion-fade-xfast` both ways (a fade: reduced motion keeps it).
@@ -159,8 +167,9 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
159
167
  - Row: leading icon · body (text over description) · trail. Beside two lines the trail stacks — meta on top, count below; beside one line they sit in a row.
160
168
  - Text and description truncate with an ellipsis; give a long `List.ItemText` a `title` with the full text.
161
169
  - On a coarse pointer a row is at least the touch target (44px).
162
- - Reorderable (`onReorder`): an `xs` handle box sits before each row, 4px from it; the row keeps its own padding and fill.
163
- - `List.ItemActions`: the controls one tier below the row, 4px apart, as far from the row's end as from its top and bottom.
170
+ - Reorderable (`onReorder`): an `xs` handle box ends each row, 4px after the row element and its `List.ItemActions` (inline-end: on the left in RTL); the row keeps its own padding and fill, so every row's handle stands on one line.
171
+ - `List.ItemActions`: the controls one tier below the row, 4px apart, as far from the row's edge as from its top and bottom — the end edge, or the start edge with `side="start"`, where the row's own padding then parts the controls from its icon or text.
172
+ - One toggle place per row: a plain row ends with its toggle (label first); a reorderable row leads with it (`side="start"`) and ends with the grip — never a toggle between the label and the grip.
164
173
  - A colour dot in `List.ItemIcon` is the large `Badge.Dot` (8px) centred in an icon-wide box, so the text lines up with the rows that carry icons.
165
174
 
166
175
  ## Accessibility
@@ -181,8 +190,8 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
181
190
  - The open record: `aria-current="true"` on a button (a selection inside the screen), `aria-current="page"` on a link (`href`, `asChild`) — the link points at the current URL. A router link may set it itself.
182
191
  - The row's name is its text, description, meta and count, in that order; the leading icon or avatar is `aria-hidden` — the text names the row.
183
192
  - Arrow keys do not move between rows: rows are ordinary Tab stops, as in the Sidebar.
184
- - `List.ItemActions` stand beside the row element, never inside it: the row's name stays its text, and Tab goes from the row to its controls.
185
- - With `onReorder` each row gets a handle named «Переместить {label}» (`labels.reorderItem`, `{label}` is the row's text line) and `aria-keyshortcuts`; the new place is announced politely («Новый лид: позиция 2 из 5»), a row being saved is `aria-busy`.
193
+ - `List.ItemActions` stand beside the row element, never inside it: the row's name stays its text. Tab follows the row as it reads: the `side="start"` controls, the row, the `side="end"` controls.
194
+ - With `onReorder` each row gets a handle named «Переместить {label}» (`labels.reorderItem`, `{label}` is the row's text line) and `aria-keyshortcuts`, last in the row: Tab reaches the row with its `List.ItemActions`, then the handle; the new place is announced politely («Новый лид: позиция 2 из 5»), a row being saved is `aria-busy`.
186
195
 
187
196
  ### Labels
188
197
  <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
@@ -200,7 +209,7 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
200
209
  | [with-icon.tsx](examples/with-icon.tsx) | Leading avatars of clients, icons of queues and colour dots of projects; an avatar takes the row tier, a dot stands in an icon-wide box — `List.ItemIcon`, `Badge.Dot`. |
201
210
  | [overflow.tsx](examples/overflow.tsx) | A long inbox in a pane of a fixed height: the list scrolls in a ScrollContainer and long names and previews truncate — `List.ItemText`, `List.ItemDescription`. |
202
211
  | [as-link.tsx](examples/as-link.tsx) | Rows as links to the invoice pages: a plain link, or your router link through the slot — `href`, `asChild`. |
203
- | [reorder.tsx](examples/reorder.tsx) | Funnel stages in the team's order: a stage moves by its handle or Alt+arrows and is saved on the server — `onReorder`, `value`. |
212
+ | [reorder.tsx](examples/reorder.tsx) | Funnel stages in the team's order: a stage moves by its handle or Alt+arrows and is saved on the server; its switch leads the row, the grip ends it — `onReorder`, `value`, `side`. |
204
213
  | [row-controls.tsx](examples/row-controls.tsx) | Funnel stages with their own controls: a colour dot names the stage's hue, a switch shows it on the board and a menu edits it, all beside the row that opens the stage — `List.ItemActions`, `Badge.Dot`. |
205
214
 
206
215
  ## Mistakes
@@ -212,6 +221,7 @@ Root attributes: `data-size`, `role="list"` (the list style reset would drop the
212
221
  - Up / down buttons on rows or a hand-made `Dnd.Sortable` of rows to order them → `List.Root onReorder` with a `value` on every item.
213
222
  - `onReorder` that rolls the order back itself on an error → return the promise; a rejection moves the row back.
214
223
  - A Switch or a menu button put inside a row, or a row rebuilt as a `div` to hold one → `List.ItemActions` beside the row element.
224
+ - A Switch at the end of a reorderable row, between its label and the grip → `List.ItemActions side="start"`; the grip keeps the end.
215
225
  - A hollow `Icon` circle or a hand-drawn dot as a stage colour → `Badge.Dot color` in `List.ItemIcon`.
216
226
 
217
227
  ## Related