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.
- package/dist/chunks/{chunk-OAXJWF3L.js → chunk-RZNGLELR.js} +120 -114
- package/dist/chunks/{chunk-MB4CXSLL.js → chunk-WYCTXD63.js} +832 -619
- package/dist/chunks/{chunk-X3JT5CLR.js → chunk-ZTZ2ML2B.js} +1 -1
- package/dist/color-picker.js +2 -2
- package/dist/components/data-table/ColumnsMenu.d.ts +16 -20
- package/dist/components/data-table/DataTable.d.ts +14 -11
- package/dist/components/data-table/Head.d.ts +8 -1
- package/dist/components/data-table/Row.d.ts +7 -1
- package/dist/components/data-table/StateRows.d.ts +3 -1
- package/dist/components/field-list/FieldList.d.ts +8 -8
- package/dist/components/index.d.ts +1 -1
- package/dist/components/kanban/Kanban.d.ts +4 -0
- package/dist/components/list/List.d.ts +9 -4
- package/dist/index.css +1 -1
- package/dist/index.js +2 -2
- package/dist/internal/fieldCell.d.ts +6 -0
- package/dist/internal/leaveGuard.d.ts +6 -0
- package/dist/internal/overlay/DialogGuard.d.ts +3 -2
- package/dist/internal/overlay/DialogParts.d.ts +14 -5
- package/dist/layout/index.d.ts +1 -1
- package/dist/layout/page/Page.d.ts +1 -7
- package/dist/skill/anti-slop.md +23 -0
- package/dist/skill/api-contract.md +1 -1
- package/dist/skill/cheatsheet.md +7 -3
- package/dist/skill/checklist.md +4 -0
- package/dist/skill/composition.md +70 -12
- package/dist/skill/layouts.md +1 -1
- package/dist/skill/reference/components/confirm-dialog/COMPONENT.md +1 -1
- package/dist/skill/reference/components/data-table/COMPONENT.md +40 -40
- package/dist/skill/reference/components/data-table/examples/actions-column.tsx +154 -0
- package/dist/skill/reference/components/data-table/examples/columns-visibility.tsx +3 -12
- package/dist/skill/reference/components/data-table/examples/controlled.tsx +1 -1
- package/dist/skill/reference/components/data-table/examples/narrow.tsx +4 -11
- package/dist/skill/reference/components/data-table/examples/row-link.tsx +18 -22
- package/dist/skill/reference/components/data-table/examples/screen.tsx +12 -1
- package/dist/skill/reference/components/data-table/examples/toolbar.tsx +26 -32
- package/dist/skill/reference/components/dnd/COMPONENT.md +4 -3
- package/dist/skill/reference/components/dnd/examples/examples.module.css +4 -3
- package/dist/skill/reference/components/dnd/examples/handle.tsx +5 -5
- package/dist/skill/reference/components/dnd/examples/locked.tsx +1 -1
- package/dist/skill/reference/components/dnd/examples/table.tsx +6 -6
- package/dist/skill/reference/components/drawer/COMPONENT.md +26 -10
- package/dist/skill/reference/components/drawer/examples/header-actions.tsx +1 -1
- package/dist/skill/reference/components/drawer/examples/loading.tsx +58 -0
- package/dist/skill/reference/components/field-list/COMPONENT.md +29 -25
- package/dist/skill/reference/components/field-list/examples/base-row.tsx +2 -1
- package/dist/skill/reference/components/field-list/examples/locked.tsx +11 -11
- package/dist/skill/reference/components/field-list/examples/reorder.tsx +1 -1
- package/dist/skill/reference/components/field-list/examples/screen.module.css +1 -1
- package/dist/skill/reference/components/field-list/examples/screen.tsx +168 -162
- package/dist/skill/reference/components/field-list/examples/totals.tsx +9 -8
- package/dist/skill/reference/components/kanban/COMPONENT.md +6 -2
- package/dist/skill/reference/components/kanban/examples/states.tsx +7 -5
- package/dist/skill/reference/components/list/COMPONENT.md +21 -11
- package/dist/skill/reference/components/list/examples/reorder.tsx +42 -7
- package/dist/skill/reference/components/modal/COMPONENT.md +23 -10
- package/dist/skill/reference/components/segmented-control/COMPONENT.md +3 -2
- package/dist/skill/reference/components/sheet/COMPONENT.md +18 -7
- package/dist/skill/reference/layout/page/COMPONENT.md +5 -12
- package/dist/skill/reference/layout/page/examples/screen.module.css +1 -1
- package/dist/skill/reference/layout/page/examples/screen.tsx +93 -104
- package/dist/skill/reference/layout/page/examples/sections.tsx +2 -15
- package/dist/skill/responsive.md +7 -5
- package/dist/tokens/semantic.d.ts +1 -1
- package/dist/tokens/themes/dark.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/confirm-dialog/COMPONENT.md +1 -1
- package/src/components/data-table/COMPONENT.md +40 -40
- package/src/components/data-table/examples/actions-column.tsx +154 -0
- package/src/components/data-table/examples/columns-visibility.tsx +3 -12
- package/src/components/data-table/examples/controlled.tsx +1 -1
- package/src/components/data-table/examples/narrow.tsx +4 -11
- package/src/components/data-table/examples/row-link.tsx +18 -22
- package/src/components/data-table/examples/screen.tsx +12 -1
- package/src/components/data-table/examples/toolbar.tsx +26 -32
- package/src/components/dnd/COMPONENT.md +4 -3
- package/src/components/dnd/examples/examples.module.css +4 -3
- package/src/components/dnd/examples/handle.tsx +5 -5
- package/src/components/dnd/examples/locked.tsx +1 -1
- package/src/components/dnd/examples/table.tsx +6 -6
- package/src/components/drawer/COMPONENT.md +26 -10
- package/src/components/drawer/examples/header-actions.tsx +1 -1
- package/src/components/drawer/examples/loading.tsx +58 -0
- package/src/components/field-list/COMPONENT.md +29 -25
- package/src/components/field-list/examples/base-row.tsx +2 -1
- package/src/components/field-list/examples/locked.tsx +11 -11
- package/src/components/field-list/examples/reorder.tsx +1 -1
- package/src/components/field-list/examples/screen.module.css +1 -1
- package/src/components/field-list/examples/screen.tsx +168 -162
- package/src/components/field-list/examples/totals.tsx +9 -8
- package/src/components/kanban/COMPONENT.md +6 -2
- package/src/components/kanban/examples/states.tsx +7 -5
- package/src/components/list/COMPONENT.md +21 -11
- package/src/components/list/examples/reorder.tsx +42 -7
- package/src/components/modal/COMPONENT.md +23 -10
- package/src/components/segmented-control/COMPONENT.md +3 -2
- package/src/components/sheet/COMPONENT.md +18 -7
- package/src/layout/page/COMPONENT.md +5 -12
- package/src/layout/page/examples/screen.module.css +1 -1
- package/src/layout/page/examples/screen.tsx +93 -104
- package/src/layout/page/examples/sections.tsx +2 -15
- package/src/styles/theme-dark.css +1 -1
- package/src/styles/theme-light.css +1 -1
- package/dist/components/data-table/context.d.ts +0 -14
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
/** A campaign
|
|
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`. */
|
|
2
2
|
import {
|
|
3
3
|
Badge,
|
|
4
|
-
Button,
|
|
5
4
|
DataTable,
|
|
6
5
|
type DataTableColumn,
|
|
7
6
|
Icon,
|
|
@@ -87,36 +86,31 @@ export default function DataTableToolbarExample() {
|
|
|
87
86
|
pageSize={5}
|
|
88
87
|
empty="Ничего не найдено — измените запрос или фильтр"
|
|
89
88
|
toolbar={
|
|
90
|
-
<div className={styles.
|
|
91
|
-
<
|
|
92
|
-
<Input.
|
|
93
|
-
<Input.
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
</SegmentedControl.Root>
|
|
116
|
-
</div>
|
|
117
|
-
<Button.Root variant="outline" tone="neutral" size="s">
|
|
118
|
-
Экспорт
|
|
119
|
-
</Button.Root>
|
|
89
|
+
<div className={styles.filters}>
|
|
90
|
+
<Input.Root size="s" className={styles.search}>
|
|
91
|
+
<Input.Wrapper>
|
|
92
|
+
<Input.Icon side="start">
|
|
93
|
+
<Icon name="action.search" />
|
|
94
|
+
</Input.Icon>
|
|
95
|
+
<Input.Field
|
|
96
|
+
type="search"
|
|
97
|
+
placeholder="Поиск кампаний"
|
|
98
|
+
aria-label="Поиск кампаний"
|
|
99
|
+
value={query}
|
|
100
|
+
onValueChange={setQuery}
|
|
101
|
+
/>
|
|
102
|
+
</Input.Wrapper>
|
|
103
|
+
</Input.Root>
|
|
104
|
+
<SegmentedControl.Root
|
|
105
|
+
size="s"
|
|
106
|
+
aria-label="Статус"
|
|
107
|
+
value={status}
|
|
108
|
+
onValueChange={setStatus}
|
|
109
|
+
>
|
|
110
|
+
<SegmentedControl.Item value="all">Все</SegmentedControl.Item>
|
|
111
|
+
<SegmentedControl.Item value="active">Активные</SegmentedControl.Item>
|
|
112
|
+
<SegmentedControl.Item value="paused">На паузе</SegmentedControl.Item>
|
|
113
|
+
</SegmentedControl.Root>
|
|
120
114
|
</div>
|
|
121
115
|
}
|
|
122
116
|
/>
|
|
@@ -30,7 +30,7 @@ Dnd.Root session owner: overlay (portal), live region, l
|
|
|
30
30
|
├─ Dnd.Sortable list container and drop target; renders items in the drawn order
|
|
31
31
|
│ ├─ header optional content before the items that never moves (a group's heading row)
|
|
32
32
|
│ ├─ Dnd.SortableItem one item (li in ul/ol, div otherwise); the gap is drawn before it
|
|
33
|
-
│ │ └─ Dnd.Handle optional grip: a ghost icon-only Button (with `handle` on the list); a lock in a `locked` item; busy while the item's move is saved
|
|
33
|
+
│ │ └─ Dnd.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
34
|
│ ├─ empty the `empty` content while the list has no items
|
|
35
35
|
│ └─ children optional content after the items that never moves (an add tile)
|
|
36
36
|
├─ Dnd.Draggable an item to carry elsewhere; stays in place, faded, until the drop
|
|
@@ -85,7 +85,7 @@ Generic `Dnd.Sortable<T>`; `ref` → `HTMLElement`. The list and its drop target
|
|
|
85
85
|
| `…rest` | `Omit<HTMLAttributes<HTMLElement>, "id">` | — | `aria-labelledby` / `aria-describedby` (the visible title and description) and the other attributes. |
|
|
86
86
|
|
|
87
87
|
### Dnd.Handle
|
|
88
|
-
`ref` → `HTMLButtonElement` (`HTMLSpanElement` in a `locked` item). The grip: a ghost icon-only `xs` Button that starts the drag and takes the item's keys; busy while
|
|
88
|
+
`ref` → `HTMLButtonElement` (`HTMLSpanElement` in a `locked` item). The grip: a ghost icon-only `xs` Button that starts the drag and takes the item's keys; busy while saving. In a `locked` item it is a lock. Place it last in the item, after its controls.
|
|
89
89
|
|
|
90
90
|
| Prop | Type | Default | Description |
|
|
91
91
|
|---|---|---|---|
|
|
@@ -201,6 +201,7 @@ An item's own `onPointerDown` / `onKeyDown` run before the kit's: `preventDefaul
|
|
|
201
201
|
## Layout & spacing
|
|
202
202
|
- The distance between items is `--prime-space-2`; a host sets `--dnd-sortable-gap` on the list for another one.
|
|
203
203
|
- 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
|
+
- A `Dnd.Handle` ends its item: the last child, after the item's own trailing controls (a remove button), at the inline end (on the left in RTL); a `locked` item's lock takes the same place, so the grips of every row stand on one line. In a table it is the last cell. An on / off switch leads the item (its first child), so its state reads first and nothing stands between the text and the grip.
|
|
204
205
|
- The reason chip stands `--prime-tooltip-offset` under the carried copy, start-aligned, at most `--prime-tooltip-max-width` wide.
|
|
205
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.
|
|
206
207
|
- A scrollable ancestor under the pointer auto-scrolls within 64 px of its edge (a `ScrollContainer` strip works).
|
|
@@ -251,7 +252,7 @@ An item's own `onPointerDown` / `onKeyDown` run before the kit's: `preventDefaul
|
|
|
251
252
|
| Example | Shows |
|
|
252
253
|
|---|---|
|
|
253
254
|
| [overview.tsx](examples/overview.tsx) | Project tasks reordered by dragging the whole row; one root above, the move applied with a helper — `Dnd.Root`, `Dnd.Sortable`, `onReorder`, `moveBefore`. |
|
|
254
|
-
| [handle.tsx](examples/handle.tsx) | Notification channels
|
|
255
|
+
| [handle.tsx](examples/handle.tsx) | Notification channels, each switch leading its row and the grip ending it: only the grip starts a drag, and the async save can roll the order back — `handle`, `Dnd.Handle`, `onReorder`. |
|
|
255
256
|
| [grid.tsx](examples/grid.tsx) | Product photos in a wrapping grid: a photo carried across rows lands in reading order, the gap takes a cell, Alt + ← / → steps it one place — `axis`. |
|
|
256
257
|
| [locked.tsx](examples/locked.tsx) | Sections of a workspace menu: the home section is pinned first with a lock in place of its grip, the rest reorder around nothing past it; each row is named by its visible title — `locked`, `handle`, `aria-labelledby`. |
|
|
257
258
|
| [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`. |
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
box-shadow: var(--prime-shadow-raised);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/* The grip sits closer to the edge:
|
|
21
|
+
/* The grip ends the row and sits closer to the edge: a square button with its own inner room. */
|
|
22
22
|
.handleRow {
|
|
23
|
-
padding-inline-
|
|
23
|
+
padding-inline-end: var(--prime-space-2);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
.rowText {
|
|
@@ -134,9 +134,10 @@
|
|
|
134
134
|
text-align: end;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/* The grip column ends the row. */
|
|
137
138
|
.grip {
|
|
138
139
|
inline-size: var(--prime-space-10);
|
|
139
|
-
padding-inline-
|
|
140
|
+
padding-inline-start: 0;
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
/* A stage's heading row: a fill marks where a new stage starts. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Notification channels
|
|
1
|
+
/** Notification channels, each switch leading its row and the grip ending it: only the grip starts a drag, and the async save can roll the order back — `handle`, `Dnd.Handle`, `onReorder`. */
|
|
2
2
|
import { Dnd, moveBefore, Switch, Typography } from "prime-ui-kit";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
@@ -37,10 +37,6 @@ export default function DndHandleExample() {
|
|
|
37
37
|
}}
|
|
38
38
|
renderItem={(channel) => (
|
|
39
39
|
<Dnd.SortableItem id={channel.id} className={styles.handleRow}>
|
|
40
|
-
<Dnd.Handle />
|
|
41
|
-
<Typography as="span" variant="body-m" className={styles.rowText}>
|
|
42
|
-
{channel.title}
|
|
43
|
-
</Typography>
|
|
44
40
|
<Switch.Root
|
|
45
41
|
aria-label={channel.title}
|
|
46
42
|
checked={channel.enabled}
|
|
@@ -50,6 +46,10 @@ export default function DndHandleExample() {
|
|
|
50
46
|
)
|
|
51
47
|
}
|
|
52
48
|
/>
|
|
49
|
+
<Typography as="span" variant="body-m" className={styles.rowText}>
|
|
50
|
+
{channel.title}
|
|
51
|
+
</Typography>
|
|
52
|
+
<Dnd.Handle />
|
|
53
53
|
</Dnd.SortableItem>
|
|
54
54
|
)}
|
|
55
55
|
/>
|
|
@@ -40,7 +40,6 @@ export default function DndLockedExample() {
|
|
|
40
40
|
aria-labelledby={`${uid}-${section.id}`}
|
|
41
41
|
aria-describedby={`${uid}-${section.id}-note`}
|
|
42
42
|
>
|
|
43
|
-
<Dnd.Handle aria-label={`Переместить раздел «${section.title}»`} />
|
|
44
43
|
<span className={styles.rowText}>
|
|
45
44
|
<Typography as="span" variant="body-m" id={`${uid}-${section.id}`}>
|
|
46
45
|
{section.title}
|
|
@@ -49,6 +48,7 @@ export default function DndLockedExample() {
|
|
|
49
48
|
{section.description}
|
|
50
49
|
</Typography>
|
|
51
50
|
</span>
|
|
51
|
+
<Dnd.Handle aria-label={`Переместить раздел «${section.title}»`} />
|
|
52
52
|
</Dnd.SortableItem>
|
|
53
53
|
)}
|
|
54
54
|
/>
|
|
@@ -51,9 +51,6 @@ export default function DndTableExample() {
|
|
|
51
51
|
</caption>
|
|
52
52
|
<thead>
|
|
53
53
|
<tr>
|
|
54
|
-
<th scope="col" className={styles.grip}>
|
|
55
|
-
<VisuallyHidden>Порядок</VisuallyHidden>
|
|
56
|
-
</th>
|
|
57
54
|
<th scope="col" className={styles.cell}>
|
|
58
55
|
<Typography as="span" variant="caption" tone="muted">
|
|
59
56
|
Работа
|
|
@@ -64,6 +61,9 @@ export default function DndTableExample() {
|
|
|
64
61
|
Часы
|
|
65
62
|
</Typography>
|
|
66
63
|
</th>
|
|
64
|
+
<th scope="col" className={styles.grip}>
|
|
65
|
+
<VisuallyHidden>Порядок</VisuallyHidden>
|
|
66
|
+
</th>
|
|
67
67
|
</tr>
|
|
68
68
|
</thead>
|
|
69
69
|
{stages.map((stage) => (
|
|
@@ -90,9 +90,6 @@ export default function DndTableExample() {
|
|
|
90
90
|
}
|
|
91
91
|
renderItem={(line) => (
|
|
92
92
|
<Dnd.SortableItem id={line.id}>
|
|
93
|
-
<td className={styles.grip}>
|
|
94
|
-
<Dnd.Handle />
|
|
95
|
-
</td>
|
|
96
93
|
<td className={styles.cell}>
|
|
97
94
|
<Typography as="span" variant="body-m">
|
|
98
95
|
{line.title}
|
|
@@ -103,6 +100,9 @@ export default function DndTableExample() {
|
|
|
103
100
|
{line.hours}
|
|
104
101
|
</Typography>
|
|
105
102
|
</td>
|
|
103
|
+
<td className={styles.grip}>
|
|
104
|
+
<Dnd.Handle />
|
|
105
|
+
</td>
|
|
106
106
|
</Dnd.SortableItem>
|
|
107
107
|
)}
|
|
108
108
|
/>
|
|
@@ -81,7 +81,7 @@ No DOM, no ref. State and dismiss policy.
|
|
|
81
81
|
| `…rest` | `HTMLAttributes<HTMLElement>` | — | `className` and the other attributes of the `<header>`. |
|
|
82
82
|
|
|
83
83
|
### Drawer.Actions
|
|
84
|
-
`ref` → `HTMLDivElement`. A row of controls at the end of the header, before the close button: a status Badge, a ⋯ Dropdown
|
|
84
|
+
`ref` → `HTMLDivElement`. A row of controls at the end of the header, before the close button: a status Badge, a ⋯ Dropdown; under the title when it would get less than 12rem; or at the end of a `Drawer.Section` heading. Controls inside take tier `s`.
|
|
85
85
|
|
|
86
86
|
| Prop | Type | Default | Description |
|
|
87
87
|
|---|---|---|---|
|
|
@@ -96,12 +96,20 @@ No DOM, no ref. State and dismiss policy.
|
|
|
96
96
|
| `children` | `ReactNode` | — (required) | An `Icon` (sized to the m icon), or the record's picture: an `Avatar.Root` (sized to the tile) or a `Thumbnail.Root`. |
|
|
97
97
|
| `…rest` | `Omit<HTMLAttributes<HTMLSpanElement>, "children">` | — | `className` and the other attributes of the tile. |
|
|
98
98
|
|
|
99
|
-
### Drawer.Title
|
|
100
|
-
`ref` → `HTMLHeadingElement
|
|
99
|
+
### Drawer.Title
|
|
100
|
+
`ref` → `HTMLHeadingElement`. `<h2>` (title-m); its id names the dialog.
|
|
101
101
|
|
|
102
102
|
| Prop | Type | Default | Description |
|
|
103
103
|
|---|---|---|---|
|
|
104
|
-
| `…rest` | `Omit<HTMLAttributes<HTMLHeadingElement
|
|
104
|
+
| `…rest` | `Omit<HTMLAttributes<HTMLHeadingElement>, "id">` | — | `className`, `children` and the other attributes except `id` (the dialog owns it). |
|
|
105
|
+
|
|
106
|
+
### Drawer.Description
|
|
107
|
+
`ref` → `HTMLParagraphElement`. `<p>` (body-s, muted) under the title; its id describes the dialog.
|
|
108
|
+
|
|
109
|
+
| Prop | Type | Default | Description |
|
|
110
|
+
|---|---|---|---|
|
|
111
|
+
| `loading` | `boolean` | `false` | Its data loads: a `Skeleton` holds the line's place and `aria-busy` is set, so the header never moves; the data fades in. |
|
|
112
|
+
| `…rest` | `Omit<HTMLAttributes<HTMLParagraphElement>, "id">` | — | `className`, `children` and the other attributes except `id` (the dialog owns it). |
|
|
105
113
|
|
|
106
114
|
### Drawer.Body
|
|
107
115
|
`ref` → `HTMLDivElement`. The only scrolling zone (a ScrollContainer), 16 gap between blocks.
|
|
@@ -111,7 +119,7 @@ No DOM, no ref. State and dismiss policy.
|
|
|
111
119
|
| `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `className`, `children` and the other attributes of the zone. |
|
|
112
120
|
|
|
113
121
|
### Drawer.Section
|
|
114
|
-
`ref` → `HTMLElement`. A titled `<section>` of the body: an `<h3>` (title-s, under the `<h2>` title), an optional description, a `Drawer.Actions` at the end of the heading row; the content 16 below, sections 32 apart. Fields group in a `Fieldset`.
|
|
122
|
+
`ref` → `HTMLElement`. A titled `<section>` of the body: an `<h3>` (title-s, under the `<h2>` title), an optional description, a `Drawer.Actions` at the end of the heading row; the content 16 below, sections 32 apart, in any wrapper. Fields group in a `Fieldset`.
|
|
115
123
|
|
|
116
124
|
| Prop | Type | Default | Description |
|
|
117
125
|
|---|---|---|---|
|
|
@@ -124,7 +132,7 @@ No DOM, no ref. State and dismiss policy.
|
|
|
124
132
|
|
|
125
133
|
| Prop | Type | Default | Description |
|
|
126
134
|
|---|---|---|---|
|
|
127
|
-
| `layout` | `"fill" \| "end"` | `"end"` | `fill`: equal-width buttons in one row; `end`: auto width, at the end. Stacked on phones (viewport below 640 px) and in a dialog narrower than 360 px. |
|
|
135
|
+
| `layout` | `"fill" \| "end"` | `"end"` | `fill`: equal-width buttons in one row, wrapping rather than cutting a label; `end`: auto width, at the end. Stacked on phones (viewport below 640 px) and in a dialog narrower than 360 px. |
|
|
128
136
|
| `…rest` | `HTMLAttributes<HTMLElement>` | — | `className`, `children` and the other attributes of the `<footer>`. |
|
|
129
137
|
|
|
130
138
|
### Drawer.Meta
|
|
@@ -135,7 +143,7 @@ No DOM, no ref. State and dismiss policy.
|
|
|
135
143
|
| `…rest` | `HTMLAttributes<HTMLParagraphElement>` | — | `className`, `children` and the other attributes of the `<p>`. |
|
|
136
144
|
|
|
137
145
|
### Drawer.Guard
|
|
138
|
-
No DOM, no ref. Holds the dialog from anywhere inside Content, where the form state lives; renders only the built-in «discard changes?» confirm. Closing from code is never held.
|
|
146
|
+
No DOM, no ref. Holds the dialog from anywhere inside Content, where the form state lives; renders only the built-in «discard changes?» confirm. While `busy` or `dirty` the browser also asks before the tab closes or reloads. Closing from code is never held.
|
|
139
147
|
|
|
140
148
|
| Prop | Type | Default | Description |
|
|
141
149
|
|---|---|---|---|
|
|
@@ -186,6 +194,9 @@ Below 640px of viewport every size is full width and loses its corners.
|
|
|
186
194
|
| closing | `open` turns false | the panel keeps showing the children it had open while it slides out |
|
|
187
195
|
| busy | `Drawer.Guard busy` | Escape, the scrim, the close button and `Drawer.Close` do nothing; the close button and the `Close` child are `disabled` in place |
|
|
188
196
|
| dirty | `Drawer.Guard dirty` | a dismiss opens the «Не сохранять изменения?» ConfirmDialog (`labels.discard*`); its confirm closes the drawer |
|
|
197
|
+
| leaving the tab | `Drawer.Guard busy` or `dirty` | the browser asks before the tab closes or reloads (`beforeunload`), as with `Page.Guard` |
|
|
198
|
+
| description loading | `Drawer.Description loading` | a `Skeleton` holds the line, `aria-busy` on the `<p>`; the data fades in under the same element |
|
|
199
|
+
| actions below | the title would get less than 12rem beside `Drawer.Actions` | `data-actions-below` on the header: the actions move under the title and the description |
|
|
189
200
|
|
|
190
201
|
Motion: the scrim fades, the panel slides from its side over `slow` and leaves over `base` (`overlayMotion`); under `prefers-reduced-motion` it unmounts at once. The drawer has no touch gestures: it is not dragged or swiped away — it closes by its buttons, Escape and the scrim, with the same slide-out.
|
|
191
202
|
|
|
@@ -199,8 +210,10 @@ Motion: the scrim fades, the panel slides from its side over `slow` and leaves o
|
|
|
199
210
|
- Full viewport height; only the body scrolls, with its own side padding.
|
|
200
211
|
- Tabs in the header: a `Tabs.List` inside `Drawer.Header` (with `Tabs.Root` around the header and the body) spans the header's last row. The header becomes the tab strip, edge to edge and without the hairline; the active tab is the body rising into it, the panels sit in the body.
|
|
201
212
|
- Fields inside get the surface field fill.
|
|
202
|
-
- Titled groups of the body (a record's contacts, its deals, its history): `Drawer.Section label` — an `<h3>` under the drawer's `<h2>` title, an optional `description`, a `Drawer.Actions` at the end of its heading row; content 16 below, sections 32 apart. A group of fields is a `Fieldset`; a page section is `Page.Section`.
|
|
213
|
+
- Titled groups of the body (a record's contacts, its deals, its history): `Drawer.Section label` — an `<h3>` under the drawer's `<h2>` title, an optional `description`, a `Drawer.Actions` at the end of its heading row; content 16 below, sections 32 apart — directly in the body, in a `Tabs.Panel` or in a form, with no spacing wrapper of your own. A group of fields is a `Fieldset`; a page section is `Page.Section`.
|
|
203
214
|
- With a `Drawer.Meta` the buttons stay one row: when the footer runs short the status goes above them first.
|
|
215
|
+
- `Drawer.Actions` keep their place beside the title while it has 12rem; on a phone, where a status and a ⋯ beside the × would fold the title into letters, they move under it by themselves.
|
|
216
|
+
- A record whose data is still loading: the title from the list row, `Drawer.Description loading` and a `Skeleton` body in the data's geometry under a `Crossfade` — not a skeleton of your own in the header.
|
|
204
217
|
|
|
205
218
|
## Accessibility
|
|
206
219
|
|
|
@@ -240,8 +253,9 @@ Motion: the scrim fades, the panel slides from its side over `slow` and leaves o
|
|
|
240
253
|
| [structure.tsx](examples/structure.tsx) | Optional parts: an icon tile in the header and one full-width action in the footer — `Drawer.Icon`, `Drawer.Footer`, `layout`. |
|
|
241
254
|
| [sizes.tsx](examples/sizes.tsx) | Every panel width, 360 to 800 px; below 640 px of viewport the panel takes the full width — `size`. |
|
|
242
255
|
| [placement.tsx](examples/placement.tsx) | The panel slides from the right for details and from the left for filters, rounded only on the edge facing the page — `side`. |
|
|
243
|
-
| [header-actions.tsx](examples/header-actions.tsx) | A record card: its status and a ⋯ menu sit in the header before the close button, one tier smaller — `Drawer.Actions`. |
|
|
256
|
+
| [header-actions.tsx](examples/header-actions.tsx) | A record card: its status and a ⋯ menu sit in the header before the close button, one tier smaller; on a phone, where the title has no room beside them, they move under it — `Drawer.Actions`. |
|
|
244
257
|
| [client-card.tsx](examples/client-card.tsx) | A client card from the list: the contact's avatar heads the panel in place of an icon tile, then the details in titled sections, one with its own action — `Drawer.Icon`, `Avatar.Root`, `Drawer.Section`, `Drawer.Actions`. |
|
|
258
|
+
| [loading.tsx](examples/loading.tsx) | A record card that opens before its data arrives: the title comes from the list row, the line under it and the body hold their place in skeletons, then the data fades in — `Drawer.Description`, `loading`. |
|
|
245
259
|
| [guard.tsx](examples/guard.tsx) | A form that holds the drawer from inside: after an edit Escape, the scrim and Cancel ask before discarding, the footer says so, and saving locks every way out — `Drawer.Guard`, `Drawer.Meta`. |
|
|
246
260
|
| [long-content.tsx](examples/long-content.tsx) | A long body scrolls on its own while the header and the footer stay in place — `Drawer.Body`. |
|
|
247
261
|
| [dismiss.tsx](examples/dismiss.tsx) | An import closes only from its buttons, and not at all while it runs — `closeOnOutsideClick`, `closeOnEscape`. |
|
|
@@ -254,6 +268,8 @@ Motion: the scrim fades, the panel slides from its side over `slow` and leaves o
|
|
|
254
268
|
- Save or Cancel buttons at the end of the body, or in the header → they go in `Drawer.Footer`, which stays in place while the body scrolls.
|
|
255
269
|
- A delete confirmation in a Drawer → use ConfirmDialog.
|
|
256
270
|
- A hand-made heading row (`Typography as="h3"` + buttons) over a group in the body → `Drawer.Section label` with `Drawer.Actions`.
|
|
271
|
+
- A wrapper with its own gap around `Drawer.Section`s inside a `Tabs.Panel` or a form → not needed: sections keep 32 between them in any wrapper.
|
|
272
|
+
- A `Skeleton` placed by hand in `Drawer.Description` while the record loads → `Drawer.Description loading`.
|
|
257
273
|
- Busy or unsaved state lifted out of the form into Root props → put `Drawer.Guard` next to the form, inside Content.
|
|
258
274
|
- Clearing the record together with `open` and rendering a skeleton for «no record» → not needed: the panel keeps its last content while it slides out.
|
|
259
275
|
- A Badge or a menu as a custom child of the header text column → put them in `Drawer.Actions`.
|
|
@@ -263,5 +279,5 @@ Motion: the scrim fades, the panel slides from its side over `slow` and leaves o
|
|
|
263
279
|
- Padding on a wrapper inside Body → Body already pads and scrolls.
|
|
264
280
|
|
|
265
281
|
## Related
|
|
266
|
-
- **Built from:** the dialog shared with Modal and Sheet (`internal/overlay/DialogParts`), [Button](../button/COMPONENT.md) (close button), [ScrollContainer](../scroll-container/COMPONENT.md) (`Drawer.Body`), [Avatar](../avatar/COMPONENT.md) and [Thumbnail](../thumbnail/COMPONENT.md) (a record's picture in `Drawer.Icon`), [ConfirmDialog](../confirm-dialog/COMPONENT.md) (the `Drawer.Guard` discard confirm), `Icon` (the close glyph)
|
|
282
|
+
- **Built from:** the dialog shared with Modal and Sheet (`internal/overlay/DialogParts`), [Button](../button/COMPONENT.md) (close button), [ScrollContainer](../scroll-container/COMPONENT.md) (`Drawer.Body`), [Avatar](../avatar/COMPONENT.md) and [Thumbnail](../thumbnail/COMPONENT.md) (a record's picture in `Drawer.Icon`), [Skeleton](../skeleton/COMPONENT.md) (`Drawer.Description loading`), [ConfirmDialog](../confirm-dialog/COMPONENT.md) (the `Drawer.Guard` discard confirm), `Icon` (the close glyph)
|
|
267
283
|
- **See also:** [Modal](../modal/COMPONENT.md) (the same header, body, footer and `Guard` in the centre), [Sheet](../sheet/COMPONENT.md) (the same parts raised from the bottom edge for a short task), [ConfirmDialog](../confirm-dialog/COMPONENT.md) (a destructive confirm), [Sidebar](../../layout/sidebar/COMPONENT.md), [Popover](../popover/COMPONENT.md), [Avatar](../avatar/COMPONENT.md) (a person's picture in `Drawer.Icon`)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** A record card: its status and a ⋯ menu sit in the header before the close button, one tier smaller — `Drawer.Actions`. */
|
|
1
|
+
/** A record card: its status and a ⋯ menu sit in the header before the close button, one tier smaller; on a phone, where the title has no room beside them, they move under it — `Drawer.Actions`. */
|
|
2
2
|
import { Badge, Button, DescriptionList, Drawer, Dropdown, Icon } from "prime-ui-kit";
|
|
3
3
|
|
|
4
4
|
const DEAL = {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** A record card that opens before its data arrives: the title comes from the list row, the line under it and the body hold their place in skeletons, then the data fades in — `Drawer.Description`, `loading`. */
|
|
2
|
+
import { Button, Crossfade, DescriptionList, Drawer, Skeleton } from "prime-ui-kit";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
const BIKE = {
|
|
6
|
+
title: "Велосипед №214",
|
|
7
|
+
summary: "Тур 2 · на линии с 6 октября",
|
|
8
|
+
facts: [
|
|
9
|
+
["Модель", "Tour 2, рама M"],
|
|
10
|
+
["Пробег", "1 840 км"],
|
|
11
|
+
["Последнее ТО", "28 сентября"],
|
|
12
|
+
["Арендатор", "Илья Морозов"],
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function DrawerLoadingExample() {
|
|
17
|
+
const [loading, setLoading] = React.useState(true);
|
|
18
|
+
|
|
19
|
+
const openChange = (open: boolean) => {
|
|
20
|
+
if (!open) return;
|
|
21
|
+
setLoading(true);
|
|
22
|
+
window.setTimeout(() => setLoading(false), 1200);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Drawer.Root onOpenChange={openChange}>
|
|
27
|
+
<Drawer.Trigger>
|
|
28
|
+
<Button.Root variant="soft" tone="neutral">
|
|
29
|
+
Открыть велосипед
|
|
30
|
+
</Button.Root>
|
|
31
|
+
</Drawer.Trigger>
|
|
32
|
+
<Drawer.Content>
|
|
33
|
+
<Drawer.Header>
|
|
34
|
+
<Drawer.Title>{BIKE.title}</Drawer.Title>
|
|
35
|
+
<Drawer.Description loading={loading}>{BIKE.summary}</Drawer.Description>
|
|
36
|
+
</Drawer.Header>
|
|
37
|
+
<Drawer.Body>
|
|
38
|
+
<Crossfade state={loading ? "loading" : "data"} aria-busy={loading}>
|
|
39
|
+
<DescriptionList.Root>
|
|
40
|
+
{BIKE.facts.map(([term, value]) => (
|
|
41
|
+
<DescriptionList.Item key={term} label={term}>
|
|
42
|
+
{loading ? <Skeleton /> : value}
|
|
43
|
+
</DescriptionList.Item>
|
|
44
|
+
))}
|
|
45
|
+
</DescriptionList.Root>
|
|
46
|
+
</Crossfade>
|
|
47
|
+
</Drawer.Body>
|
|
48
|
+
<Drawer.Footer>
|
|
49
|
+
<Drawer.Close>
|
|
50
|
+
<Button.Root variant="outline" tone="neutral">
|
|
51
|
+
Закрыть
|
|
52
|
+
</Button.Root>
|
|
53
|
+
</Drawer.Close>
|
|
54
|
+
</Drawer.Footer>
|
|
55
|
+
</Drawer.Content>
|
|
56
|
+
</Drawer.Root>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
## When to use
|
|
9
9
|
- A form part where a person adds, edits and removes several records of the same fields: invoice lines, contacts of a client, specs of a plan, recipients.
|
|
10
10
|
- A base value with overrides (a price and its changes by month): the base is the first row, pinned with `getLocked`, each override is a row.
|
|
11
|
-
- A list of short texts in the order a person sets (selling points, checklist items): one column, `reorderable
|
|
11
|
+
- A list of short texts in the order a person sets (selling points, checklist items): one column, `reorderable`; each row's remove button is the last segment of its field.
|
|
12
12
|
- Small attributes of a record beside its text (an icon, a color): `width="compact"` columns with an icon-only `Select` (`Select.TriggerIcon` and no `Select.Value`) and a `ColorPresets` swatch.
|
|
13
13
|
|
|
14
14
|
## When not to use
|
|
@@ -28,12 +28,13 @@ import { FieldList, type FieldListColumn } from "prime-ui-kit";
|
|
|
28
28
|
FieldList field frame: label · the list · hint / error
|
|
29
29
|
├─ heading row the column headings, once (aria-hidden; each field carries its heading as its label)
|
|
30
30
|
├─ <ol> the rows (Dnd.Sortable with `reorderable`)
|
|
31
|
-
│ └─ <li> row [
|
|
31
|
+
│ └─ <li> row [a cell per column] [end: remove] [grip: handle | lock]
|
|
32
32
|
│ └─ cell one kit field without `label`, named by its column heading
|
|
33
|
+
│ (one column: no end slot, the remove is the field's last segment)
|
|
33
34
|
├─ totals row `renderTotals`: a group «Итого», a total per column on the same tracks
|
|
34
35
|
└─ add row Button «addLabel» (+ `labels.maxReached` at `max`)
|
|
35
36
|
```
|
|
36
|
-
A leaf: rows come from `value` through `renderItem`, which returns one element per column (a fragment). Every framed kit field (Input, Select, Datepicker, NativeSelect, ColorField…) in a cell takes its column heading as its label: read by screen readers, not shown while the heading row is on screen, shown above the field once the rows stack in a narrow container. `ColorPresets` takes it as the name of its trigger.
|
|
37
|
+
A leaf: rows come from `value` through `renderItem`, which returns one element per column (a fragment). Every framed kit field (Input, Select, Datepicker, NativeSelect, ColorField…) in a cell takes its column heading as its label: read by screen readers, not shown while the heading row is on screen, shown above the field once the rows stack in a narrow container. `ColorPresets` takes it as the name of its trigger. In a one-column list the row's remove button goes into the field as its last action segment (`Input.Wrapper`, `Textarea`, the same segment as `Input.Action`); a field without an action edge (a `Select`) keeps it in the end slot.
|
|
37
38
|
|
|
38
39
|
## API
|
|
39
40
|
|
|
@@ -45,8 +46,8 @@ Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editabl
|
|
|
45
46
|
| Prop | Type | Default | Description |
|
|
46
47
|
|---|---|---|---|
|
|
47
48
|
| `columns` | `readonly FieldListColumn[]` | — (required) | The columns in cell order: `label` (the heading; it names the field of the column in every row), `width` (`fill` \| `compact` \| `xs` \| `s` \| `m`), `required` (a `*` after the heading). |
|
|
48
|
-
| `renderItem` | `(item: T, row: FieldListRow<T>) => ReactNode` | — (required) | The cells of a row, one element per column (a fragment): kit fields without `label`. `row` has `index`, `update(patch)` and `remove()`. |
|
|
49
|
-
| `renderTotals` | `(items: readonly T[]) => ReactNode` | — | The totals row under the rows: one node per column (a fragment), `null` where
|
|
49
|
+
| `renderItem` | `(item: T, row: FieldListRow<T>) => ReactNode` | — (required) | The cells of a row, one element per column (a fragment): kit fields without `label`. `row` has `index`, `update(patch)` and `remove()`. In one column the remove is the field's last segment. |
|
|
50
|
+
| `renderTotals` | `(items: readonly T[]) => ReactNode` | — | The totals row under the rows: one node per column (a fragment), `null` where none; each aligned as its column's field text. The first cell without a total reads `labels.totals`. |
|
|
50
51
|
| `getId` | `(item: T) => string` | — (required) | Stable unique id of an item: the row key and the drag id. |
|
|
51
52
|
| `value` | `readonly T[]` | — | The rows (controlled). |
|
|
52
53
|
| `defaultValue` | `readonly T[]` | `[]` | The first rows (uncontrolled). |
|
|
@@ -54,8 +55,8 @@ Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editabl
|
|
|
54
55
|
| `createItem` | `() => T` | — | A new empty item; with it the add button shows under the rows and focus goes to the new row's first field. |
|
|
55
56
|
| `addLabel` | `string` | — | Text of the add button: what is added («Добавить позицию»). |
|
|
56
57
|
| `getLabel` | `(item: T) => string` | — | The row's own text (its title field) for the remove button, the handle and drag announcements; else the row number. |
|
|
57
|
-
| `getLocked` | `(item: T) => boolean` | — | Pinned rows:
|
|
58
|
-
| `reorderable` | `boolean` | `false` | Rows move by a
|
|
58
|
+
| `getLocked` | `(item: T) => boolean` | — | Pinned rows: the remove button stays in its place, off, its tooltip says why (`labels.lockedReason`); with `reorderable` a lock takes the handle's place and the row holds it while others move. |
|
|
59
|
+
| `reorderable` | `boolean` | `false` | Rows move by a handle at the row's end, after the remove button: drag, a touch hold, Alt + ↑ / ↓. |
|
|
59
60
|
| `min` | `number` | `0` | The fewest rows: at this count the remove buttons are disabled. |
|
|
60
61
|
| `max` | `number` | — | The most rows: at this count the add button is disabled with `labels.maxReached` beside it. |
|
|
61
62
|
| `label` | `ReactNode` | — | Heading of the list; names the `<ol>`. Omit it inside a `Fieldset` whose legend names the list. |
|
|
@@ -74,7 +75,7 @@ Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editabl
|
|
|
74
75
|
| Value | Looks like | Use when | Default |
|
|
75
76
|
|---|---|---|---|
|
|
76
77
|
| `fill` | shares the free width with the other `fill` columns | names, descriptions, any text | yes |
|
|
77
|
-
| `compact` |
|
|
78
|
+
| `compact` | as wide as the widest of its controls, heading and total — the gap after it is the column gap like every other | an icon-only `Select`, a `ColorPresets` swatch, a checkbox | |
|
|
78
79
|
| `xs` | 96 px | a number of a few digits: a share, a percent, a count | |
|
|
79
80
|
| `s` | 144 px | a quantity, a code, a number of days | |
|
|
80
81
|
| `m` | 192 px | an amount with its unit, a date, a phone | |
|
|
@@ -82,11 +83,11 @@ Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editabl
|
|
|
82
83
|
### Flags
|
|
83
84
|
| Value | Looks like | Use when | Default |
|
|
84
85
|
|---|---|---|---|
|
|
85
|
-
| `reorderable` | a grip
|
|
86
|
-
| `getLocked` | a lock where the grip is
|
|
86
|
+
| `reorderable` | a grip at the end of every row, after the remove button; Alt + ↑ / ↓ on it | the order of the records matters (a pricing page, a checklist) | `false` |
|
|
87
|
+
| `getLocked` | the remove button in its place, off, with a tooltip saying why (`labels.lockedReason`); with `reorderable` a lock where the grip is and the row holds its place | a base value, system records a person cannot drop | — |
|
|
87
88
|
| `createItem` | the add button under the rows, aligned with the first column | the person grows the list | — |
|
|
88
89
|
| `min` · `max` | remove buttons off at `min`; the add button off with «Не больше {max}» at `max` | the record count is bounded | `0` · — |
|
|
89
|
-
| `renderTotals` | a row of totals under the rows over a faint line, each on its column; «Итого» in the first cell without a total | the rows add up (shares, amounts, hours) | — |
|
|
90
|
+
| `renderTotals` | a row of totals under the rows over a faint line, each on its column, aligned as the column's field text; «Итого» in the first cell without a total | the rows add up (shares, amounts, hours) | — |
|
|
90
91
|
|
|
91
92
|
**Combinations**
|
|
92
93
|
- Recommended: `label` on the list (or a `Fieldset` legend around it), one heading per column, a `getLabel` from the title field, an `addLabel` that says what is added.
|
|
@@ -100,7 +101,7 @@ Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editabl
|
|
|
100
101
|
| empty | `value` with no items | no heading row; only the add button (with `createItem`) |
|
|
101
102
|
| at min | `value.length <= min` | remove buttons `disabled` |
|
|
102
103
|
| at max | `value.length >= max` | add button `disabled`, `labels.maxReached` beside it |
|
|
103
|
-
| pinned row | `getLocked(item)` | `data-locked="true"` on the `<li>`; a
|
|
104
|
+
| pinned row | `getLocked(item)` | `data-locked="true"` on the `<li>`; the remove button stays in its slot, off (`disabled` in the end slot behind a focusable Tooltip host, `aria-disabled="true"` as a field segment), its tooltip `labels.lockedReason`; with `reorderable` Dnd's lock in the grip box |
|
|
104
105
|
| invalid | `error` (the list), a field's own `error` | the list's message under the add row; a field's under the field in its row |
|
|
105
106
|
| disabled | `disabled` (or a disabled Fieldset) | `data-disabled="true"` on the frame; fields, handles and buttons disabled through the kit's disabled context |
|
|
106
107
|
| stacked | container narrower than 30rem | heading row hidden, each row's fields in one column with their labels shown; each total on its own line after its column name |
|
|
@@ -109,9 +110,10 @@ Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editabl
|
|
|
109
110
|
|
|
110
111
|
## Layout & spacing
|
|
111
112
|
- Rows 12 apart (`--prime-space-3`); columns 12 apart; the heading row sits at the tier's label gap above the first row.
|
|
112
|
-
- Every row and the
|
|
113
|
-
-
|
|
114
|
-
- The
|
|
113
|
+
- Every row, the heading row and the totals row share one track list: `[columns] [end] [grip]`; the end fits a remove button of the tier, the grip (with `reorderable`) is one `xs` control wide (the handle or the lock) and ends the row on the inline-end side (on the left in RTL). Every row fills the same slots: a pinned row shows its remove off, never a hole. A one-column list has no end track: the remove is inside the field.
|
|
114
|
+
- A `compact` column is measured: its track is the widest of its controls, its heading and its total, so the gaps between columns are all 12.
|
|
115
|
+
- The remove button, the handle and the lock stand on the middle of the first line of fields, so a field's error under it grows the row downwards only.
|
|
116
|
+
- The totals row stands 12 under the last row over a faint line, 12 more above its totals; a total's text sits where its column's field text does: at the start, or at the end for a field that ends its number there (an `Input` with `inputMode="decimal"` and an end `Input.InlineAffix`). Stacked, every total ends its line.
|
|
115
117
|
- The add button lines up with the first column of fields, 12 under the last row.
|
|
116
118
|
- Under 30rem of its own width (a container query): rows stack, fields 16 apart inside a row, rows 24 apart.
|
|
117
119
|
|
|
@@ -122,7 +124,7 @@ Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editabl
|
|
|
122
124
|
|
|
123
125
|
| Key | Action |
|
|
124
126
|
|---|---|
|
|
125
|
-
| `Tab` | Moves through the fields of a row, then to its remove button and on. |
|
|
127
|
+
| `Tab` | Moves through the fields of a row, then to its remove button (inside the field in a one-column list), its handle and on; a pinned row's remove takes focus to show why it is off. |
|
|
126
128
|
| `Enter` · `Space` | On the add button adds a row and focuses its first field; on a remove button removes the row and focuses the next row's remove button. |
|
|
127
129
|
| `Alt + ↑` · `Alt + ↓` | With `reorderable`, moves the row up or down (from its handle). |
|
|
128
130
|
|
|
@@ -135,7 +137,7 @@ Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editabl
|
|
|
135
137
|
- In a narrow container the heading row goes and each field's label shows above it.
|
|
136
138
|
- The totals row (`renderTotals`) is a `group` named by `labels.totals`; each total is read after its column heading.
|
|
137
139
|
- The remove button is «Удалить {row}», the handle «Переместить {row}»: the row's name from `getLabel`, else its number.
|
|
138
|
-
- A pinned row
|
|
140
|
+
- A pinned row keeps its remove button: `aria-disabled`, described by `labels.lockedReason` (a tooltip on hover and focus); with `reorderable` Dnd's lock stands in place of the handle.
|
|
139
141
|
|
|
140
142
|
### Labels
|
|
141
143
|
<!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
|
|
@@ -146,7 +148,7 @@ Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editabl
|
|
|
146
148
|
| `reorder` | `"Переместить {row}"` | `aria-label` of a row's handle (`reorderable`); `{row}` is the row's name. |
|
|
147
149
|
| `row` | `"строку {index}"` | Name of a row without `getLabel` text; `{index}` counts from 1. |
|
|
148
150
|
| `namedRow` | `"«{label}»"` | Name of a row with `getLabel` text; `{label}` is that text. |
|
|
149
|
-
| `
|
|
151
|
+
| `lockedReason` | `"Закреплённую строку нельзя удалить"` | Why the remove button of a pinned row (`getLocked`) is off: its tooltip and description. |
|
|
150
152
|
| `maxReached` | `"Не больше {max}"` | Text beside the disabled add button once the list holds `max` rows. |
|
|
151
153
|
| `totals` | `"Итого"` | Name of the totals row (`renderTotals`), shown in its first cell when that has no total. |
|
|
152
154
|
|
|
@@ -155,17 +157,17 @@ Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editabl
|
|
|
155
157
|
|---|---|
|
|
156
158
|
| [overview.tsx](examples/overview.tsx) | Lines of an invoice: the column headings stand once above the rows, each row has a remove button and the add button sits under them — `columns`, `renderItem`, `createItem`, `addLabel`. |
|
|
157
159
|
| [sizes.tsx](examples/sizes.tsx) | Every tier: the headings, the fields, the handle and the buttons follow the list's size — `size`. |
|
|
158
|
-
| [reorder.tsx](examples/reorder.tsx) | Selling points of a plan in the order of the pricing page: a one-column list has no headings and no numbers, a point moves by its handle or Alt+arrows — `reorderable`, `getLabel`. |
|
|
159
|
-
| [locked.tsx](examples/locked.tsx) | Specs of a plan on one line each: an icon picker and
|
|
160
|
-
| [base-row.tsx](examples/base-row.tsx) | A plan price and its changes by month in one table: the base price is the first, pinned row, each change is a row with its month and its price, and the unit sits in the field, not in a heading — `getLocked`, `width`. |
|
|
160
|
+
| [reorder.tsx](examples/reorder.tsx) | Selling points of a plan in the order of the pricing page: a one-column list has no headings and no numbers, the remove button is the last segment of each field, a point moves by its handle or Alt+arrows — `reorderable`, `getLabel`. |
|
|
161
|
+
| [locked.tsx](examples/locked.tsx) | Specs of a plan on one line each: an icon picker and its color side by side in compact columns, the first three specs are pinned with a lock in the handle's place and a remove button that is off and says why, the rest reorder below them — `getLocked`, `width`, `reorderable`. |
|
|
162
|
+
| [base-row.tsx](examples/base-row.tsx) | A plan price and its changes by month in one table: the base price is the first, pinned row whose remove button is off and says why, each change is a row with its month and its price, and the unit sits in the field, not in a heading — `getLocked`, `labels`, `width`. |
|
|
161
163
|
| [limits.tsx](examples/limits.tsx) | Recipients of invoice reminders, from one to three: the last row cannot be removed and the add button says why it is off once the list is full — `min`, `max`, `hint`. |
|
|
162
164
|
| [errors.tsx](examples/errors.tsx) | Contacts of a client checked on save: a field's problem shows under that field in its row, a problem of the whole list under the list, and focus goes to the first wrong field; a `*` after a heading marks a required column — `error`, `required`. |
|
|
163
|
-
| [totals.tsx](examples/totals.tsx) | 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`. |
|
|
165
|
+
| [totals.tsx](examples/totals.tsx) | 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`. |
|
|
164
166
|
| [saving.tsx](examples/saving.tsx) | Removing a recipient is saved on the server: the row leaves at once and comes back with the reason if the server refuses — `onValueChange`, `error`. |
|
|
165
167
|
| [states.tsx](examples/states.tsx) | The same list editable and disabled while the request saves it: fields, handles and buttons turn off together — `disabled`. |
|
|
166
168
|
| [controlled.tsx](examples/controlled.tsx) | The parent owns the lines: it sums them under the list and puts the saved lines back on reset — `value`, `onValueChange`. |
|
|
167
169
|
| [narrow.tsx](examples/narrow.tsx) | The invoice lines in a 320 px column: the headings give way, each row stacks its fields with their own labels and rows stand a group apart — `columns`. |
|
|
168
|
-
| [screen.tsx](examples/screen.tsx) | 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
|
|
170
|
+
| [screen.tsx](examples/screen.tsx) | 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`. |
|
|
169
171
|
|
|
170
172
|
## Mistakes
|
|
171
173
|
- A `label` above every field of every row («Иконка», «Параметр», «Значение» four times) → a FieldList: the headings stand once, the fields take them as hidden labels.
|
|
@@ -173,11 +175,13 @@ Generic `FieldList<T>`; `ref` → `HTMLDivElement` (the field frame). An editabl
|
|
|
173
175
|
- One record split over two lines of half-width fields → one row; small attributes go to `compact` columns (icon-only `Select`, `ColorPresets`).
|
|
174
176
|
- A base value as a separate field above its overrides, in another geometry → the first row, pinned with `getLocked`.
|
|
175
177
|
- The unit in every heading («Цена, ₽ / день») and again in the field → the unit once, in `Input.InlineAffix`.
|
|
176
|
-
- A hint that explains what the lock already shows («Первые три всегда первыми») → no hint; the lock
|
|
178
|
+
- A hint that explains what the lock already shows («Первые три всегда первыми») → no hint; the lock and the remove button's reason say it.
|
|
179
|
+
- A pinned row with an empty place where the others have a remove button → none to fix by hand: FieldList draws it off with `labels.lockedReason`; say the domain reason there («Базовую цену нельзя удалить»).
|
|
180
|
+
- A one-field row with a remove button in a column beside the field → FieldList puts it inside the field; no `Input.Action` of your own for it.
|
|
177
181
|
- `aria-label` written on every field in a cell → none: the column heading names the field.
|
|
178
182
|
- A grip, a remove button and an add button drawn by hand around a `.map()` of fields → FieldList.
|
|
179
183
|
- A total under the list typed by hand in a line of text, or a DataTable of fields to get a totals row → `renderTotals`.
|
|
180
184
|
|
|
181
185
|
## Related
|
|
182
|
-
- **Built from:** [Button](../button/COMPONENT.md) (add, remove), [Dnd](../dnd/COMPONENT.md) (`reorderable`: `Dnd.Sortable`, `Dnd.Handle`; the lock of a pinned row), [Label](../label/COMPONENT.md) (headings), the field frame (label, [Hint](../hint/COMPONENT.md) for the hint and error), [VisuallyHidden](../visually-hidden/COMPONENT.md)
|
|
186
|
+
- **Built from:** [Button](../button/COMPONENT.md) (add, remove), [Dnd](../dnd/COMPONENT.md) (`reorderable`: `Dnd.Sortable`, `Dnd.Handle`; the lock of a pinned row), [Label](../label/COMPONENT.md) (headings), [Tooltip](../tooltip/COMPONENT.md) (why a pinned row's remove is off), the field action segment (the remove inside the field of a one-column list), the field frame (label, [Hint](../hint/COMPONENT.md) for the hint and error), [VisuallyHidden](../visually-hidden/COMPONENT.md)
|
|
183
187
|
- **See also:** [Input](../input/COMPONENT.md), [Select](../select/COMPONENT.md) (icon-only trigger), [ColorPicker](../color-picker/COMPONENT.md) (`ColorPresets`), [Fieldset](../fieldset/COMPONENT.md), [DataTable](../data-table/COMPONENT.md)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** A plan price and its changes by month in one table: the base price is the first, pinned row, each change is a row with its month and its price, and the unit sits in the field, not in a heading — `getLocked`, `width`. */
|
|
1
|
+
/** A plan price and its changes by month in one table: the base price is the first, pinned row whose remove button is off and says why, each change is a row with its month and its price, and the unit sits in the field, not in a heading — `getLocked`, `labels`, `width`. */
|
|
2
2
|
import { FieldList, type FieldListColumn, Input, Select } from "prime-ui-kit";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
|
|
@@ -30,6 +30,7 @@ export default function FieldListBaseRowExample() {
|
|
|
30
30
|
onValueChange={setPeriods}
|
|
31
31
|
getId={(period) => period.id}
|
|
32
32
|
getLocked={(period) => period.base ?? false}
|
|
33
|
+
labels={{ lockedReason: "Базовую цену нельзя удалить" }}
|
|
33
34
|
createItem={() => ({ id: crypto.randomUUID(), from: "", price: "" })}
|
|
34
35
|
addLabel="Добавить изменение цены"
|
|
35
36
|
renderItem={(period, row) => (
|