prime-ui-kit 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +1 -1
  2. package/dist/chunks/{chunk-OAXJWF3L.js → chunk-3PY52JP3.js} +1764 -1764
  3. package/dist/chunks/{chunk-X3JT5CLR.js → chunk-4KONMO7Z.js} +1 -1
  4. package/dist/chunks/{chunk-MB4CXSLL.js → chunk-W6EGTNG6.js} +3772 -2888
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/data-table/ColumnsMenu.d.ts +39 -17
  7. package/dist/components/data-table/DataTable.d.ts +41 -12
  8. package/dist/components/data-table/DataTableColumnsMenu.d.ts +45 -0
  9. package/dist/components/data-table/Head.d.ts +18 -5
  10. package/dist/components/data-table/Row.d.ts +10 -4
  11. package/dist/components/data-table/StateRows.d.ts +5 -3
  12. package/dist/components/data-table/columnOrder.d.ts +14 -0
  13. package/dist/components/data-table/reorder.d.ts +15 -5
  14. package/dist/components/data-table/types.d.ts +5 -0
  15. package/dist/components/data-table/useFrozenColumns.d.ts +6 -2
  16. package/dist/components/datepicker/DatepickerPanel.d.ts +1 -1
  17. package/dist/components/dnd/Dnd.d.ts +9 -5
  18. package/dist/components/dnd/inPlaceRow.d.ts +7 -0
  19. package/dist/components/dnd/useDragSource.d.ts +2 -0
  20. package/dist/components/dnd/useSortableList.d.ts +5 -0
  21. package/dist/components/field-list/FieldList.d.ts +37 -15
  22. package/dist/components/gantt/Gantt.d.ts +1 -1
  23. package/dist/components/index.d.ts +3 -1
  24. package/dist/components/kanban/Kanban.d.ts +4 -0
  25. package/dist/components/list/List.d.ts +9 -4
  26. package/dist/components/scroll-container/ScrollContainer.d.ts +1 -1
  27. package/dist/index.css +1 -1
  28. package/dist/index.js +4 -2
  29. package/dist/internal/ToolRow.d.ts +23 -0
  30. package/dist/internal/leaveGuard.d.ts +6 -0
  31. package/dist/internal/overlay/DialogGuard.d.ts +3 -2
  32. package/dist/internal/overlay/DialogParts.d.ts +14 -5
  33. package/dist/layout/index.d.ts +1 -1
  34. package/dist/layout/page/Page.d.ts +8 -14
  35. package/dist/skill/anti-slop.md +55 -1
  36. package/dist/skill/api-contract.md +2 -2
  37. package/dist/skill/cheatsheet.md +12 -4
  38. package/dist/skill/checklist.md +7 -0
  39. package/dist/skill/choosing.md +1 -0
  40. package/dist/skill/components.md +1 -1
  41. package/dist/skill/composition.md +196 -21
  42. package/dist/skill/layouts.md +1 -1
  43. package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -1
  44. package/dist/skill/reference/components/data-table/COMPONENT.md +110 -49
  45. package/dist/skill/reference/components/data-table/examples/actions-column.tsx +154 -0
  46. package/dist/skill/reference/components/data-table/examples/column-order.tsx +152 -0
  47. package/dist/skill/reference/components/data-table/examples/columns-visibility.tsx +18 -18
  48. package/dist/skill/reference/components/data-table/examples/controlled.tsx +32 -17
  49. package/dist/skill/reference/components/data-table/examples/examples.module.css +14 -36
  50. package/dist/skill/reference/components/data-table/examples/narrow.tsx +25 -19
  51. package/dist/skill/reference/components/data-table/examples/reorder.tsx +24 -2
  52. package/dist/skill/reference/components/data-table/examples/row-link.tsx +18 -22
  53. package/dist/skill/reference/components/data-table/examples/screen.tsx +12 -1
  54. package/dist/skill/reference/components/data-table/examples/selection.tsx +13 -18
  55. package/dist/skill/reference/components/data-table/examples/server.tsx +45 -31
  56. package/dist/skill/reference/components/data-table/examples/toolbar.tsx +22 -35
  57. package/dist/skill/reference/components/datepicker/COMPONENT.md +2 -2
  58. package/dist/skill/reference/components/dnd/COMPONENT.md +13 -7
  59. package/dist/skill/reference/components/dnd/examples/examples.module.css +4 -3
  60. package/dist/skill/reference/components/dnd/examples/handle.tsx +5 -5
  61. package/dist/skill/reference/components/dnd/examples/locked.tsx +1 -1
  62. package/dist/skill/reference/components/dnd/examples/table-columns.tsx +75 -0
  63. package/dist/skill/reference/components/dnd/examples/table.tsx +6 -6
  64. package/dist/skill/reference/components/drawer/COMPONENT.md +26 -10
  65. package/dist/skill/reference/components/drawer/examples/header-actions.tsx +1 -1
  66. package/dist/skill/reference/components/drawer/examples/loading.tsx +58 -0
  67. package/dist/skill/reference/components/field-list/COMPONENT.md +78 -47
  68. package/dist/skill/reference/components/field-list/examples/base-row.tsx +2 -1
  69. package/dist/skill/reference/components/field-list/examples/locked.tsx +11 -11
  70. package/dist/skill/reference/components/field-list/examples/narrow.tsx +108 -33
  71. package/dist/skill/reference/components/field-list/examples/reorder.tsx +1 -1
  72. package/dist/skill/reference/components/field-list/examples/row-menu.tsx +71 -0
  73. package/dist/skill/reference/components/field-list/examples/screen.module.css +1 -1
  74. package/dist/skill/reference/components/field-list/examples/screen.tsx +169 -162
  75. package/dist/skill/reference/components/field-list/examples/totals.tsx +9 -8
  76. package/dist/skill/reference/components/field-list/examples/variants.tsx +51 -0
  77. package/dist/skill/reference/components/gantt/COMPONENT.md +4 -4
  78. package/dist/skill/reference/components/kanban/COMPONENT.md +6 -2
  79. package/dist/skill/reference/components/kanban/examples/states.tsx +7 -5
  80. package/dist/skill/reference/components/list/COMPONENT.md +21 -11
  81. package/dist/skill/reference/components/list/examples/reorder.tsx +42 -7
  82. package/dist/skill/reference/components/modal/COMPONENT.md +23 -10
  83. package/dist/skill/reference/components/scroll-container/COMPONENT.md +1 -1
  84. package/dist/skill/reference/components/segmented-control/COMPONENT.md +3 -2
  85. package/dist/skill/reference/components/sheet/COMPONENT.md +18 -7
  86. package/dist/skill/reference/components/smart-filter/COMPONENT.md +2 -2
  87. package/dist/skill/reference/layout/page/COMPONENT.md +13 -17
  88. package/dist/skill/reference/layout/page/examples/screen.module.css +1 -1
  89. package/dist/skill/reference/layout/page/examples/screen.tsx +93 -104
  90. package/dist/skill/reference/layout/page/examples/sections.tsx +2 -15
  91. package/dist/skill/responsive.md +14 -10
  92. package/dist/tokens/semantic.d.ts +2 -2
  93. package/dist/tokens/themes/dark.d.ts +1 -1
  94. package/package.json +1 -1
  95. package/src/components/confirm-dialog/COMPONENT.md +1 -1
  96. package/src/components/data-table/COMPONENT.md +110 -49
  97. package/src/components/data-table/examples/actions-column.tsx +154 -0
  98. package/src/components/data-table/examples/column-order.tsx +152 -0
  99. package/src/components/data-table/examples/columns-visibility.tsx +18 -18
  100. package/src/components/data-table/examples/controlled.tsx +32 -17
  101. package/src/components/data-table/examples/examples.module.css +14 -36
  102. package/src/components/data-table/examples/narrow.tsx +25 -19
  103. package/src/components/data-table/examples/reorder.tsx +24 -2
  104. package/src/components/data-table/examples/row-link.tsx +18 -22
  105. package/src/components/data-table/examples/screen.tsx +12 -1
  106. package/src/components/data-table/examples/selection.tsx +13 -18
  107. package/src/components/data-table/examples/server.tsx +45 -31
  108. package/src/components/data-table/examples/toolbar.tsx +22 -35
  109. package/src/components/datepicker/COMPONENT.md +2 -2
  110. package/src/components/dnd/COMPONENT.md +13 -7
  111. package/src/components/dnd/examples/examples.module.css +4 -3
  112. package/src/components/dnd/examples/handle.tsx +5 -5
  113. package/src/components/dnd/examples/locked.tsx +1 -1
  114. package/src/components/dnd/examples/table-columns.tsx +75 -0
  115. package/src/components/dnd/examples/table.tsx +6 -6
  116. package/src/components/drawer/COMPONENT.md +26 -10
  117. package/src/components/drawer/examples/header-actions.tsx +1 -1
  118. package/src/components/drawer/examples/loading.tsx +58 -0
  119. package/src/components/field-list/COMPONENT.md +78 -47
  120. package/src/components/field-list/examples/base-row.tsx +2 -1
  121. package/src/components/field-list/examples/locked.tsx +11 -11
  122. package/src/components/field-list/examples/narrow.tsx +108 -33
  123. package/src/components/field-list/examples/reorder.tsx +1 -1
  124. package/src/components/field-list/examples/row-menu.tsx +71 -0
  125. package/src/components/field-list/examples/screen.module.css +1 -1
  126. package/src/components/field-list/examples/screen.tsx +169 -162
  127. package/src/components/field-list/examples/totals.tsx +9 -8
  128. package/src/components/field-list/examples/variants.tsx +51 -0
  129. package/src/components/gantt/COMPONENT.md +4 -4
  130. package/src/components/kanban/COMPONENT.md +6 -2
  131. package/src/components/kanban/examples/states.tsx +7 -5
  132. package/src/components/list/COMPONENT.md +21 -11
  133. package/src/components/list/examples/reorder.tsx +42 -7
  134. package/src/components/modal/COMPONENT.md +23 -10
  135. package/src/components/scroll-container/COMPONENT.md +1 -1
  136. package/src/components/segmented-control/COMPONENT.md +3 -2
  137. package/src/components/sheet/COMPONENT.md +18 -7
  138. package/src/components/smart-filter/COMPONENT.md +2 -2
  139. package/src/layout/page/COMPONENT.md +13 -17
  140. package/src/layout/page/examples/screen.module.css +1 -1
  141. package/src/layout/page/examples/screen.tsx +93 -104
  142. package/src/layout/page/examples/sections.tsx +2 -15
  143. package/src/styles/theme-dark.css +1 -1
  144. package/src/styles/theme-light.css +1 -1
  145. package/dist/components/data-table/context.d.ts +0 -14
@@ -0,0 +1,71 @@
1
+ /** Payment stages of a project in dense rows: one more-button per row holds move up, move down and remove, the moves are off at the ends and next to the pinned advance, and Alt+arrows on the button move the row too — `rowActions`, `reorderable`, `getLocked`. */
2
+ import { FieldList, type FieldListColumn, Input } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ type Stage = { id: string; name: string; days: string; percent: string; pinned?: boolean };
6
+
7
+ const COLUMNS: FieldListColumn[] = [
8
+ { label: "Этап" },
9
+ { label: "Срок", width: "s" },
10
+ { label: "Доля", width: "xs" },
11
+ ];
12
+
13
+ const STAGES: Stage[] = [
14
+ { id: "advance", name: "Аванс", days: "0", percent: "30", pinned: true },
15
+ { id: "design", name: "Приёмка дизайна", days: "21", percent: "20" },
16
+ { id: "beta", name: "Запуск бета-версии", days: "45", percent: "30" },
17
+ { id: "release", name: "Подписание акта", days: "60", percent: "20" },
18
+ ];
19
+
20
+ export default function FieldListRowMenuExample() {
21
+ const [stages, setStages] = React.useState(STAGES);
22
+
23
+ return (
24
+ <FieldList
25
+ label="Этапы оплаты"
26
+ columns={COLUMNS}
27
+ rowActions="menu"
28
+ reorderable
29
+ value={stages}
30
+ onValueChange={setStages}
31
+ getId={(stage) => stage.id}
32
+ getLabel={(stage) => stage.name}
33
+ getLocked={(stage) => stage.pinned ?? false}
34
+ labels={{ lockedReason: "Аванс всегда первый" }}
35
+ createItem={() => ({ id: crypto.randomUUID(), name: "", days: "", percent: "" })}
36
+ addLabel="Добавить этап"
37
+ renderItem={(stage, row) => (
38
+ <>
39
+ <Input.Root>
40
+ <Input.Wrapper>
41
+ <Input.Field
42
+ value={stage.name}
43
+ onChange={(event) => row.update({ name: event.target.value })}
44
+ />
45
+ </Input.Wrapper>
46
+ </Input.Root>
47
+ <Input.Root>
48
+ <Input.Wrapper>
49
+ <Input.Field
50
+ value={stage.days}
51
+ inputMode="numeric"
52
+ onChange={(event) => row.update({ days: event.target.value })}
53
+ />
54
+ <Input.InlineAffix side="end">дн.</Input.InlineAffix>
55
+ </Input.Wrapper>
56
+ </Input.Root>
57
+ <Input.Root>
58
+ <Input.Wrapper>
59
+ <Input.Field
60
+ value={stage.percent}
61
+ inputMode="decimal"
62
+ onChange={(event) => row.update({ percent: event.target.value })}
63
+ />
64
+ <Input.InlineAffix side="end">%</Input.InlineAffix>
65
+ </Input.Wrapper>
66
+ </Input.Root>
67
+ </>
68
+ )}
69
+ />
70
+ );
71
+ }
@@ -1,4 +1,4 @@
1
- /* Record editor: the sections are Page.Section layout="aside"; here only the panels' insides. */
1
+ /* Record editor: the blocks are titled cards in the page flow; here only the cards' insides. */
2
2
 
3
3
  /* Two groups in one panel, a group apart. */
4
4
  .groups {
@@ -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, the price list on its own fill among the fields of its card, one value in three currencies in one row — `getLocked`, `compact`, `variant`. */
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,188 @@ 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
+ variant="surface"
247
+ columns={PRICE_COLUMNS}
248
+ value={periods}
249
+ onValueChange={setPeriods}
250
+ getId={(period) => period.id}
251
+ getLocked={(period) => period.base ?? false}
252
+ labels={{ lockedReason: "Базовую цену нельзя удалить" }}
253
+ createItem={() => ({ id: crypto.randomUUID(), from: "", price: "" })}
254
+ addLabel="Добавить изменение цены"
255
+ renderItem={(period, row) => (
182
256
  <>
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
- )}
257
+ {period.base ? (
273
258
  <Input.Root>
274
259
  <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>
260
+ <Input.Field value="Всегда, если нет изменений" readOnly />
282
261
  </Input.Wrapper>
283
262
  </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}>
263
+ ) : (
264
+ <Select.Root
265
+ value={period.from}
266
+ onValueChange={(from) => row.update({ from })}
267
+ placeholder="Выберите месяц"
268
+ >
269
+ <Select.Trigger>
270
+ <Select.Value />
271
+ </Select.Trigger>
272
+ <Select.Content>
273
+ {MONTHS.map((month) => (
274
+ <Select.Item key={month.value} value={month.value}>
275
+ {month.label}
276
+ </Select.Item>
277
+ ))}
278
+ </Select.Content>
279
+ </Select.Root>
280
+ )}
281
+ <Input.Root>
296
282
  <Input.Wrapper>
297
283
  <Input.Field
298
- name={fee.name}
299
- aria-label={fee.label}
284
+ value={period.price}
300
285
  inputMode="decimal"
301
286
  placeholder="0"
302
- defaultValue={fee.value}
287
+ onChange={(event) => row.update({ price: event.target.value })}
303
288
  />
304
- <Input.InlineAffix side="end">{fee.currency}</Input.InlineAffix>
289
+ <Input.InlineAffix side="end">₽</Input.InlineAffix>
305
290
  </Input.Wrapper>
306
291
  </Input.Root>
307
- ))}
308
- </div>
309
- </Fieldset>
310
- </Card.Body>
311
- </Card.Root>
312
- </Page.Section>
292
+ </>
293
+ )}
294
+ />
295
+ </Fieldset>
296
+ <Fieldset
297
+ label="Подключение"
298
+ description="Разовый платёж; пустая валюта не выставляется."
299
+ disabled={saving}
300
+ >
301
+ <div className={styles.currencies}>
302
+ {SETUP_FEE.map((fee) => (
303
+ <Input.Root key={fee.name}>
304
+ <Input.Wrapper>
305
+ <Input.Field
306
+ name={fee.name}
307
+ aria-label={fee.label}
308
+ inputMode="decimal"
309
+ placeholder="0"
310
+ defaultValue={fee.value}
311
+ />
312
+ <Input.InlineAffix side="end">{fee.currency}</Input.InlineAffix>
313
+ </Input.Wrapper>
314
+ </Input.Root>
315
+ ))}
316
+ </div>
317
+ </Fieldset>
318
+ </Card.Body>
319
+ </Card.Root>
313
320
  </Page.Body>
314
321
  </Page.Root>
315
322
  );
@@ -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
  }}
@@ -0,0 +1,51 @@
1
+ /** The two looks of the same list: the rows on the host surface, or on a fill of their own with side padding — `variant`. */
2
+ import { FieldList, type FieldListColumn, Input, Typography } from "prime-ui-kit";
3
+
4
+ type Contact = { id: string; name: string; phone: string };
5
+
6
+ const VARIANTS = ["plain", "surface"] as const;
7
+
8
+ const COLUMNS: FieldListColumn[] = [{ label: "Контакт" }, { label: "Телефон", width: "m" }];
9
+
10
+ const CONTACTS: Contact[] = [
11
+ { id: "buyer", name: "Анна Лебедева", phone: "+7 912 404-18-20" },
12
+ { id: "accountant", name: "Олег Демин", phone: "+7 903 118-42-07" },
13
+ ];
14
+
15
+ export default function FieldListVariantsExample() {
16
+ return (
17
+ <div>
18
+ {VARIANTS.map((variant) => (
19
+ <div key={variant}>
20
+ <div>
21
+ <FieldList
22
+ variant={variant}
23
+ label="Контакты покупателя"
24
+ columns={COLUMNS}
25
+ defaultValue={CONTACTS}
26
+ getId={(contact) => contact.id}
27
+ getLabel={(contact) => contact.name}
28
+ renderItem={(contact) => (
29
+ <>
30
+ <Input.Root>
31
+ <Input.Wrapper>
32
+ <Input.Field defaultValue={contact.name} />
33
+ </Input.Wrapper>
34
+ </Input.Root>
35
+ <Input.Root>
36
+ <Input.Wrapper>
37
+ <Input.Field type="tel" defaultValue={contact.phone} />
38
+ </Input.Wrapper>
39
+ </Input.Root>
40
+ </>
41
+ )}
42
+ />
43
+ <Typography as="span" variant="caption" tone="muted">
44
+ {variant}
45
+ </Typography>
46
+ </div>
47
+ </div>
48
+ ))}
49
+ </div>
50
+ );
51
+ }
@@ -25,7 +25,7 @@ import { Gantt, type GanttBar, type GanttDates, type GanttScale } from "prime-ui
25
25
  ## Anatomy
26
26
  ```
27
27
  Gantt surface (layer of the ladder), size container
28
- ├─ toolbar your `toolbar` content · «Сегодня» · zoom (SegmentedControl)
28
+ ├─ toolbar the kit's tool row: your `toolbar` content (the free room) · «Сегодня» · zoom (SegmentedControl)
29
29
  └─ viewport ScrollContainer, both axes, own scroll only
30
30
  └─ role="grid"
31
31
  ├─ head row (sticky) corner (sticky) · axis: top tier (months / years) over bottom tier (days / weeks / months), today chip
@@ -62,7 +62,7 @@ Gantt surface (layer of the ladder), size container
62
62
  | `readOnly` | `boolean` | `false` | No moving, resizing or creating; bars still select and show their dates. |
63
63
  | `loading` | `boolean` | `false` | Without rows — skeleton rows in the chart's geometry and a status for screen readers; with rows they stay and the grid is `aria-busy`. |
64
64
  | `empty` | `ReactNode` | — | Empty-state content when `rows` is empty; a string goes into a compact `EmptyPage`. Defaults to `labels.empty`. |
65
- | `toolbar` | `ReactNode` | — | Content at the start of the toolbar (title, filters, a primary action); the zoom and «Сегодня» sit at its end. |
65
+ | `toolbar` | `ReactNode` | — | Content at the start of the toolbar (title, a legend, filters), kept together in the free room; «Сегодня» and the zoom keep their width at its end, and on a narrow chart they take the next line, the zoom filling it. |
66
66
  | `scrollHeight` | `number \| "fill"` | — | Max height of the viewport in px, so the header sticks while rows scroll; `"fill"` — the chart takes its flex / grid parent's height. |
67
67
  | `onItemOpen` | `(bar: Bar, row: Row) => void` | — | A bar was activated (click, Enter, Space): open its record. A drag never opens it. |
68
68
  | `onBarChange` | `(dates: GanttDates, bar: Bar, row: Row) => void \| Promise<void>` | — | New dates of a bar after a drag or an editing key — update your data. A returned Promise keeps the bar busy at the new dates; a rejection glides it back. Without it bars cannot be edited. |
@@ -151,7 +151,7 @@ A `tone` wins over `color`. Say the status in the label too, never only by color
151
151
  - Editing precision: a move, a resize and a drawing snap to whole days at every zoom (at the month zoom a day is 4 px, so the snap holds a day until the pointer clearly leaves it); hold Alt to snap to the zoom's week or month. A bar is at least one day long: a resize past the opposite edge stops there and never flips. On touch a finger held on a bar picks it up, then it drags like a pointer.
152
152
  - Creating: on empty space the cursor is a crosshair and, on pointer devices, the cell under it is washed with a «+». Drag across a row to draw a range in days, double-click (or long-press on touch) for one unit of the zoom; a drag after the long press draws days. While drawing, a ghost with a dates and duration tooltip follows the pointer. Without `onBarCreate` (or with `readOnly`) nothing can be created and empty space shows no create cursor. Near the edges of the axis a drag, a resize and a drawing scroll the chart.
153
153
  - Tooltip: the bar's title, then its dates · duration · progress; `renderBarTooltip` adds lines under them (a client, planned hours) and never replaces them — the dates are what the bar means on the axis, and the same tooltip shows the new dates while dragging.
154
- - A legend of bar colours goes in `toolbar`: a row of `Badge.Root color` + `Badge.Dot` with the category name, one per hue the bars use; the toolbar wraps it. Say the category in the bar label or the tooltip too, never only by colour.
154
+ - A legend of bar colours goes in `toolbar`: a row of `Badge.Root color` + `Badge.Dot` with the category name, one per hue the bars use; the toolbar keeps it together in the free room and wraps it, «Сегодня» and the zoom at the end (on a narrow chart on the next line, the zoom filling it). Say the category in the bar label or the tooltip too, never only by colour.
155
155
  - The default axis covers every bar and today with a unit of room on both sides; pass `range` for a fixed period.
156
156
  - The chart opens with today a third into the view; «Сегодня» and the arrow keys scroll only the chart's scroller, never the page.
157
157
  - A row grows by a lane for every overlapping bar: lanes are packed greedily by start date.
@@ -237,5 +237,5 @@ A `tone` wins over `color`. Say the status in the label too, never only by color
237
237
  - Expecting the page to scroll to today → «Сегодня» scrolls only the chart.
238
238
 
239
239
  ## Related
240
- - **Built from:** [ScrollContainer](../scroll-container/COMPONENT.md), [Tooltip](../tooltip/COMPONENT.md), [SegmentedControl](../segmented-control/COMPONENT.md), [Button](../button/COMPONENT.md), [Skeleton](../skeleton/COMPONENT.md), [EmptyPage](../empty-page/COMPONENT.md), [Spinner](../spinner/COMPONENT.md), Icon (`action.add` in the create cue), [VisuallyHidden](../visually-hidden/COMPONENT.md)
240
+ - **Built from:** [ScrollContainer](../scroll-container/COMPONENT.md), [Tooltip](../tooltip/COMPONENT.md), [SegmentedControl](../segmented-control/COMPONENT.md), [Button](../button/COMPONENT.md), [Skeleton](../skeleton/COMPONENT.md), [EmptyPage](../empty-page/COMPONENT.md), [Spinner](../spinner/COMPONENT.md), Icon (`action.add` in the create cue), [VisuallyHidden](../visually-hidden/COMPONENT.md); `ToolRow` (the toolbar, as the DataTable `toolbar`) with [SmartFilter](../smart-filter/COMPONENT.md), [Input](../input/COMPONENT.md) and [Typography](../typography/COMPONENT.md) (what grows in the tool row), [SegmentedControl](../segmented-control/COMPONENT.md), [Datepicker](../datepicker/COMPONENT.md) and [Select](../select/COMPONENT.md) (what fills a short line; the first two get `fullWidth`)
241
241
  - **See also:** [Timeline](../timeline/COMPONENT.md), [DataTable](../data-table/COMPONENT.md), [Kanban](../kanban/COMPONENT.md), [Datepicker](../datepicker/COMPONENT.md), [Avatar](../avatar/COMPONENT.md)
@@ -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`. |