prime-ui-kit 0.9.1 → 0.9.2

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 (107) hide show
  1. package/README.md +22 -8
  2. package/dist/chunks/{chunk-KCQFOORX.js → chunk-IQELQNMR.js} +1 -1
  3. package/dist/chunks/{chunk-DB2Y3XXA.js → chunk-LXGRKN34.js} +2323 -1500
  4. package/dist/chunks/{chunk-NHOWNIAR.js → chunk-ZEFCDEEQ.js} +790 -536
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/data-table/DataTable.d.ts +6 -1
  7. package/dist/components/data-table/types.d.ts +5 -0
  8. package/dist/components/dnd/Dnd.d.ts +1 -1
  9. package/dist/components/dnd/dragSession.d.ts +9 -0
  10. package/dist/components/dnd/useDropTarget.d.ts +5 -0
  11. package/dist/components/dnd/useFlipList.d.ts +3 -3
  12. package/dist/components/drawer/Drawer.d.ts +8 -5
  13. package/dist/components/dropdown/Dropdown.d.ts +17 -0
  14. package/dist/components/index.d.ts +5 -1
  15. package/dist/components/kanban/Kanban.d.ts +129 -0
  16. package/dist/components/modal/Modal.d.ts +5 -2
  17. package/dist/components/page-toolbar/PageToolbar.d.ts +53 -0
  18. package/dist/components/segmented-control/SegmentedControl.d.ts +5 -2
  19. package/dist/components/tabs/Tabs.d.ts +5 -2
  20. package/dist/hooks/useEdgeOverflow.d.ts +12 -0
  21. package/dist/hooks/useMediaQuery.d.ts +7 -0
  22. package/dist/hooks/useSwipeDismiss.d.ts +34 -0
  23. package/dist/index.css +2271 -1163
  24. package/dist/index.js +10 -2
  25. package/dist/internal/overlay/FloatingPanel.d.ts +3 -1
  26. package/dist/internal/overlay/useFloatingLayer.d.ts +11 -1
  27. package/dist/internal/touchTarget.d.ts +4 -0
  28. package/dist/layout/app-header/AppHeader.d.ts +110 -0
  29. package/dist/layout/app-shell/AppShell.d.ts +14 -9
  30. package/dist/layout/bottom-nav/BottomNav.d.ts +80 -0
  31. package/dist/layout/index.d.ts +5 -1
  32. package/dist/tokens/primitives.d.ts +4 -0
  33. package/dist/tokens/semantic.d.ts +56 -0
  34. package/dist/tokens/themes/dark.d.ts +4 -0
  35. package/package.json +3 -2
  36. package/src/components/accordion/COMPONENT.md +1 -1
  37. package/src/components/badge/COMPONENT.md +2 -2
  38. package/src/components/banner/COMPONENT.md +1 -1
  39. package/src/components/checkbox/examples/examples.module.css +4 -2
  40. package/src/components/crossfade/COMPONENT.md +1 -1
  41. package/src/components/data-table/COMPONENT.md +19 -6
  42. package/src/components/data-table/examples/columns-visibility.tsx +167 -0
  43. package/src/components/digit-input/COMPONENT.md +10 -10
  44. package/src/components/digit-input/examples/examples.module.css +8 -0
  45. package/src/components/digit-input/examples/in-form.tsx +2 -5
  46. package/src/components/digit-input/examples/sizes.tsx +1 -1
  47. package/src/components/divider/COMPONENT.md +1 -1
  48. package/src/components/dnd/COMPONENT.md +3 -3
  49. package/src/components/drawer/COMPONENT.md +17 -9
  50. package/src/components/drawer/examples/date-sheet.tsx +41 -0
  51. package/src/components/drawer/examples/examples.module.css +5 -0
  52. package/src/components/drawer/examples/placement.tsx +4 -2
  53. package/src/components/dropdown/COMPONENT.md +19 -5
  54. package/src/components/dropdown/examples/checkbox-items.tsx +48 -0
  55. package/src/components/empty-page/COMPONENT.md +1 -1
  56. package/src/components/kanban/COMPONENT.md +181 -0
  57. package/src/components/kanban/examples/controlled.tsx +78 -0
  58. package/src/components/kanban/examples/examples.module.css +31 -0
  59. package/src/components/kanban/examples/limits.tsx +58 -0
  60. package/src/components/kanban/examples/narrow.tsx +76 -0
  61. package/src/components/kanban/examples/overview.tsx +130 -0
  62. package/src/components/kanban/examples/states.tsx +75 -0
  63. package/src/components/kanban/examples/structure.tsx +76 -0
  64. package/src/components/login-form/COMPONENT.md +1 -1
  65. package/src/components/modal/COMPONENT.md +9 -6
  66. package/src/components/notification/COMPONENT.md +2 -2
  67. package/src/components/page-content/COMPONENT.md +1 -1
  68. package/src/components/page-toolbar/COMPONENT.md +142 -0
  69. package/src/components/page-toolbar/examples/examples.module.css +16 -0
  70. package/src/components/page-toolbar/examples/narrow.tsx +103 -0
  71. package/src/components/page-toolbar/examples/overview.tsx +113 -0
  72. package/src/components/page-toolbar/examples/save-changes.tsx +40 -0
  73. package/src/components/page-toolbar/examples/without-search.tsx +60 -0
  74. package/src/components/popover/COMPONENT.md +4 -3
  75. package/src/components/progress-bar/COMPONENT.md +1 -1
  76. package/src/components/progress-circle/COMPONENT.md +1 -1
  77. package/src/components/scroll-container/COMPONENT.md +1 -1
  78. package/src/components/segmented-control/COMPONENT.md +8 -8
  79. package/src/components/segmented-control/examples/full-width.tsx +1 -1
  80. package/src/components/select/COMPONENT.md +2 -1
  81. package/src/components/skeleton/COMPONENT.md +1 -1
  82. package/src/components/smart-filter/COMPONENT.md +2 -0
  83. package/src/components/spinner/COMPONENT.md +1 -1
  84. package/src/components/tabs/COMPONENT.md +11 -8
  85. package/src/components/tabs/examples/closable.tsx +1 -1
  86. package/src/layout/app-header/COMPONENT.md +178 -0
  87. package/src/layout/app-header/examples/breadcrumbs.tsx +36 -0
  88. package/src/layout/app-header/examples/examples.module.css +47 -0
  89. package/src/layout/app-header/examples/in-app-shell.tsx +70 -0
  90. package/src/layout/app-header/examples/narrow.tsx +29 -0
  91. package/src/layout/app-header/examples/overview.tsx +36 -0
  92. package/src/layout/app-shell/COMPONENT.md +27 -21
  93. package/src/layout/app-shell/examples/examples.module.css +15 -0
  94. package/src/layout/app-shell/examples/footer.tsx +59 -0
  95. package/src/layout/app-shell/examples/overview.tsx +19 -8
  96. package/src/layout/bottom-nav/COMPONENT.md +153 -0
  97. package/src/layout/bottom-nav/examples/examples.module.css +39 -0
  98. package/src/layout/bottom-nav/examples/floating.tsx +89 -0
  99. package/src/layout/bottom-nav/examples/icon-only.tsx +41 -0
  100. package/src/layout/bottom-nav/examples/in-app-shell.tsx +64 -0
  101. package/src/layout/bottom-nav/examples/overview.tsx +44 -0
  102. package/src/layout/bottom-nav/examples/router.tsx +31 -0
  103. package/src/layout/bottom-nav/examples/structure.tsx +47 -0
  104. package/src/layout/sidebar/COMPONENT.md +11 -10
  105. package/src/styles/theme-dark.css +3 -0
  106. package/src/styles/theme-light.css +24 -0
  107. package/src/styles/tokens.css +2 -0
@@ -35,6 +35,7 @@ Dropdown.Root state and dismiss policy (no DOM)
35
35
  │ └── Dropdown.Item <button role="menuitem">
36
36
  │ ├── Dropdown.ItemIcon leading glyph (aria-hidden)
37
37
  │ └── Dropdown.ItemShortcut Kbd at the end
38
+ ├── Dropdown.CheckboxItem <button role="menuitemcheckbox">: label + check mark at the end while checked; stays open
38
39
  └── Dropdown.Item
39
40
  ```
40
41
 
@@ -62,7 +63,7 @@ No DOM: clones the single child, merges `ref` and `onClick` (toggles), sets `ari
62
63
  | `children` | `ReactElement` | — (required) | One element, usually a Button. |
63
64
 
64
65
  ### Dropdown.Content
65
- `ref` → `HTMLDivElement`. Portal + `role="menu"` on the floating surface (a ScrollContainer), named by the trigger; renders while open and during its exit animation. Focus moves to the first item; Tab closes the menu and returns focus to the trigger.
66
+ `ref` → `HTMLDivElement`. Portal + `role="menu"` on the floating surface (a ScrollContainer), named by the trigger; renders while open and during its exit animation. Focus moves to the first item; Tab closes the menu and returns focus to the trigger. Below 640px of viewport the menu is a bottom sheet: a scrim, a grab handle, swipe down to close (with `closeOnOutsideClick`), page scroll locked.
66
67
 
67
68
  | Prop | Type | Default | Description |
68
69
  |---|---|---|---|
@@ -82,6 +83,17 @@ No DOM: clones the single child, merges `ref` and `onClick` (toggles), sets `ari
82
83
  | `tone` | `"neutral" \| "danger"` | `"neutral"` | `danger`: destructive action — danger text, icon and hover fill. |
83
84
  | `children` | `ReactNode` | — | `Dropdown.ItemIcon`, the label, `Dropdown.ItemShortcut`. |
84
85
 
86
+ ### Dropdown.CheckboxItem
87
+ `ref` → `HTMLButtonElement`. A `<button role="menuitemcheckbox">` with `aria-checked`: the label (and an `ItemIcon`) on the left like any item, an accent check mark at the end while checked and nothing while not; a click, Enter or Space toggles it and the menu stays open. + native button props.
88
+
89
+ | Prop | Type | Default | Description |
90
+ |---|---|---|---|
91
+ | `checked` | `boolean` | — | Checked state (controlled). |
92
+ | `defaultChecked` | `boolean` | `false` | Initial state (uncontrolled). |
93
+ | `onCheckedChange` | `(checked: boolean) => void` | — | Called with the new state. |
94
+ | `disabled` | `boolean` | `false` | Muted, `aria-disabled`, skipped by the arrow keys, does not toggle. |
95
+ | `children` | `ReactNode` | — | The label (optionally after a `Dropdown.ItemIcon`). |
96
+
85
97
  ### Dropdown.ItemIcon · Dropdown.ItemShortcut
86
98
  `ref` → `HTMLSpanElement` / `HTMLElement` (the `<kbd>`). An `aria-hidden` `<span>` holding the leading glyph at the menu icon size (a kit `Icon` follows it) / a `Kbd` one tier below the menu, pushed to the end of the item — a hint, not a handler. + native props.
87
99
 
@@ -145,9 +157,10 @@ The panel is the shared floating surface: `bg-raised`, `--prime-panel-radius` (1
145
157
  | open / closed | `open` / `defaultOpen` / `onOpenChange` | `data-state` on the menu (closed while the exit animation plays) and on the trigger, `aria-expanded` on the trigger |
146
158
  | side | `side` and the room next to the trigger | `data-side` (resolved) |
147
159
  | size | `size` on Content | `data-size` |
148
- | item hover / focus | pointer, arrow keys | subtle fill (danger-soft for `danger`), no movement |
160
+ | item hover / focus | a hovering pointer, arrow keys | subtle fill (danger-soft for `danger`), no movement; a tap leaves no hover fill |
149
161
  | item disabled | `disabled` | `aria-disabled="true"`, `data-disabled="true"` |
150
162
  | item tone | `tone` | `data-tone` |
163
+ | sheet | viewport below 640px | a scrim and a full-width bottom sheet with an `aria-hidden` grab handle around the menu; the sheet carries `data-state`, the menu keeps its role and ref; page scroll locked; a swipe down from the handle closes it (with `closeOnOutsideClick`) |
151
164
 
152
165
  ## Layout & spacing
153
166
  - Width is `max-content` between `--prime-panel-min-width` and twice that; items never wrap.
@@ -160,14 +173,14 @@ The panel is the shared floating surface: `bg-raised`, `--prime-panel-radius` (1
160
173
  ### Keyboard
161
174
  | Key | Action |
162
175
  |---|---|
163
- | `Enter` · `Space` | On the trigger opens the menu (focus on the first item); on an item runs it and returns focus to the trigger. |
176
+ | `Enter` · `Space` | On the trigger opens the menu (focus on the first item); on an item runs it and returns focus to the trigger; on a checkbox item toggles it and the menu stays open. |
164
177
  | `ArrowDown` · `ArrowUp` | Move focus to the next / previous enabled item, wrapping. |
165
178
  | `Home` · `End` | First / last enabled item. |
166
179
  | `Escape` | Closes the menu (`closeOnEscape`); focus returns to the trigger. |
167
180
  | `Tab` · `Shift+Tab` | Closes the menu; focus returns to the trigger (Tab then goes on to the next stop). |
168
181
 
169
182
  ### ARIA
170
- - The menu is `role="menu"` named by the trigger; items are `role="menuitem"`, disabled ones `aria-disabled`.
183
+ - The menu is `role="menu"` named by the trigger; items are `role="menuitem"`, checkbox items `role="menuitemcheckbox"` with `aria-checked`, disabled ones `aria-disabled`.
171
184
  - The trigger gets `aria-haspopup="menu"`, `aria-expanded` and `aria-controls`.
172
185
  - `Dropdown.Group` is `role="group"` named by its `label`.
173
186
  - `Dropdown.ItemIcon` is hidden from screen readers; a key hint is text only — the app handles the shortcut.
@@ -187,6 +200,7 @@ No `labels`.
187
200
  | [placement.tsx](examples/placement.tsx) | Every side and alignment relative to the trigger; near the viewport edge the menu flips and shifts — `side`, `align`. |
188
201
  | [states.tsx](examples/states.tsx) | A regular, a disabled and a destructive item; arrow keys skip the disabled one — `disabled`, `tone`. |
189
202
  | [match-trigger-width.tsx](examples/match-trigger-width.tsx) | Under a full-width button the menu is at least as wide as the trigger — `matchTriggerWidth`. |
203
+ | [checkbox-items.tsx](examples/checkbox-items.tsx) | A column chooser: toggles in the menu keep it open, the key column cannot be hidden — `Dropdown.CheckboxItem`, `checked`, `onCheckedChange`. |
190
204
  | [long-list.tsx](examples/long-list.tsx) | More projects than fit: the grouped list scrolls inside the panel, capped by the room next to the trigger — `Dropdown.Group`. |
191
205
  | [dismiss.tsx](examples/dismiss.tsx) | An onboarding step keeps the menu open on an outside press and on Escape until an item is picked — `closeOnOutsideClick`, `closeOnEscape`. |
192
206
  | [controlled-open.tsx](examples/controlled-open.tsx) | The parent owns the open state and opens the menu from another button; the picked step updates the trigger — `open`, `onOpenChange`. |
@@ -199,5 +213,5 @@ No `labels`.
199
213
  - A destructive item without `tone="danger"`, or in the middle of the list → mark it and put it last.
200
214
 
201
215
  ## Related
202
- - **Built from:** [ScrollContainer](../scroll-container/COMPONENT.md) (the menu), [Kbd](../kbd/COMPONENT.md) (`ItemShortcut`), [Divider](../divider/COMPONENT.md) (`Separator`)
216
+ - **Built from:** [ScrollContainer](../scroll-container/COMPONENT.md) (the menu), [Kbd](../kbd/COMPONENT.md) (`ItemShortcut`), [Divider](../divider/COMPONENT.md) (`Separator`), Icon (`action.check` of `CheckboxItem`)
203
217
  - **See also:** [Select](../select/COMPONENT.md), [CommandMenu](../command-menu/COMPONENT.md), [Popover](../popover/COMPONENT.md), [Button](../button/COMPONENT.md)
@@ -0,0 +1,48 @@
1
+ /** A column chooser: toggles in the menu keep it open, the key column cannot be hidden — `Dropdown.CheckboxItem`, `checked`, `onCheckedChange`. */
2
+ import { Button, Dropdown, Icon } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ const COLUMNS = [
6
+ { id: "number", label: "Номер", locked: true },
7
+ { id: "client", label: "Клиент", locked: false },
8
+ { id: "status", label: "Статус", locked: false },
9
+ { id: "amount", label: "Сумма", locked: false },
10
+ { id: "due", label: "Срок оплаты", locked: false },
11
+ ];
12
+
13
+ export default function DropdownCheckboxItemsExample() {
14
+ const [hidden, setHidden] = React.useState<string[]>(["due"]);
15
+ const shown = COLUMNS.length - hidden.length;
16
+
17
+ const toggle = (id: string, visible: boolean) =>
18
+ setHidden((list) => (visible ? list.filter((item) => item !== id) : [...list, id]));
19
+
20
+ return (
21
+ <Dropdown.Root>
22
+ <Dropdown.Trigger>
23
+ <Button.Root variant="soft" tone="neutral">
24
+ <Button.Icon>
25
+ <Icon name="action.settings" />
26
+ </Button.Icon>
27
+ Колонки: {shown} из {COLUMNS.length}
28
+ </Button.Root>
29
+ </Dropdown.Trigger>
30
+ <Dropdown.Content>
31
+ <Dropdown.Group label="Показывать">
32
+ {COLUMNS.map((column) => (
33
+ <Dropdown.CheckboxItem
34
+ key={column.id}
35
+ checked={!hidden.includes(column.id)}
36
+ disabled={column.locked}
37
+ onCheckedChange={(visible) => toggle(column.id, visible)}
38
+ >
39
+ {column.label}
40
+ </Dropdown.CheckboxItem>
41
+ ))}
42
+ </Dropdown.Group>
43
+ <Dropdown.Separator />
44
+ <Dropdown.Item onSelect={() => setHidden([])}>Показать все</Dropdown.Item>
45
+ </Dropdown.Content>
46
+ </Dropdown.Root>
47
+ );
48
+ }
@@ -1,6 +1,6 @@
1
1
  # EmptyPage
2
2
 
3
- **Category:** feedback
3
+ **Category:** status
4
4
  **Kind:** composite
5
5
 
6
6
  > Empty state of a page, a block or a menu: icon, title, explanation and an action.
@@ -0,0 +1,181 @@
1
+ # Kanban
2
+
3
+ **Category:** data-display
4
+ **Kind:** composite
5
+
6
+ > A board of status columns: cards reorder within a column and move between columns by pointer, touch hold and keyboard, with announcements, WIP limits, loading and empty states and a layout that scrolls inside itself at any width.
7
+
8
+ ## When to use
9
+ - Work items that flow through statuses and are moved by hand: tasks, tickets, orders, invoices, deals.
10
+ - A team needs to see the load of every status at once (counts, WIP limits).
11
+ - The status change is the main action on the screen, and its order inside a status matters.
12
+
13
+ ## When not to use
14
+ - Records with many attributes to compare, sort or filter → use [DataTable](../data-table/COMPONENT.md).
15
+ - One list to reorder, or items carried onto folders or assignees → use [Dnd](../dnd/COMPONENT.md) directly.
16
+ - A linear process the user walks through once → use [Stepper](../stepper/COMPONENT.md).
17
+ - A history of events → use [Timeline](../timeline/COMPONENT.md).
18
+
19
+ ## Import
20
+ ```tsx
21
+ import { Kanban, type KanbanColumn, type KanbanMove, type KanbanValue } from "prime-ui-kit";
22
+ ```
23
+
24
+ ## Anatomy
25
+ ```
26
+ Kanban.Root board: horizontal strip of columns, its own Dnd.Root if none above
27
+ └─ column (from `columns`) `fill-faint` well; header + own vertical scroll
28
+ ├─ header title (h3) · count Badge · renderColumnActions(column)
29
+ └─ <ul> (Dnd.Sortable) drop area; Skeleton cards while loading; EmptyPage when empty
30
+ └─ Kanban.Item <li> card from renderItem(item)
31
+ ├─ Kanban.ItemTitle title, up to three lines
32
+ ├─ Kanban.ItemDescription muted meta line: id, due date
33
+ ├─ Kanban.ItemBadges wrapping row of Badge
34
+ └─ Kanban.ItemFooter row: Kanban.ItemCount … at the start, the assignee at the end
35
+ └─ Kanban.ItemCount icon + number
36
+ ```
37
+ Columns and cards are application data (`columns`, `items`); the board state is where each card stands: `value` maps a column id to its card ids in order. A card in flight is drawn by Dnd as a lifted clone; the column under it opens a gap where it lands.
38
+
39
+ ## API
40
+
41
+ <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
42
+
43
+ ### Kanban.Root
44
+ Generic `Kanban.Root<T>`; `ref` → `HTMLDivElement`. The board: a strip of columns that scrolls sideways inside itself, each column a labelled `<ul>` of cards under a header with the title, a count `Badge` and actions. Mounts its own `Dnd.Root` unless one is already above it. Sets `aria-busy` while loading, `data-loading`, `data-disabled`.
45
+
46
+ | Prop | Type | Default | Description |
47
+ |---|---|---|---|
48
+ | `columns` | `readonly KanbanColumn[]` | — (required) | Columns in their order: `{ id, title, limit? }`; `limit` is a WIP limit — a full column refuses cards from other columns and its count turns orange (`count / limit`). |
49
+ | `items` | `readonly T[]` | — (required) | Every card the board can show; `value` decides where each one stands. |
50
+ | `getId` | `(item: T) => string` | — (required) | Stable unique id of a card, the one used in `value`. |
51
+ | `getLabel` | `(item: T) => string` | `the id` | Spoken by the live region. |
52
+ | `value` | `KanbanValue` | — | Card ids per column (controlled): `{ [columnId]: id[] }`. Ids missing from `items` are skipped. |
53
+ | `defaultValue` | `KanbanValue` | `{}` | Initial card ids per column (uncontrolled). |
54
+ | `onValueChange` | `(value: KanbanValue, move: KanbanMove) => void` | — | A card moved by pointer, touch or keyboard: the new placement, then `{ id, from, to, index }` to save. |
55
+ | `renderItem` | `(item: T) => ReactNode` | — (required) | Renders one card; return a `Kanban.Item`. |
56
+ | `renderColumnActions` | `(column: KanbanColumn) => ReactNode` | — | Trailing actions of a column header (add a card, a menu); kit controls inside take the `s` tier. |
57
+ | `canDrop` | `(id: string, columnId: string) => boolean` | — | A workflow rule: refuses card `id` in column `columnId`; the column turns `danger` before the release and a keyboard move is announced as refused. |
58
+ | `loading` | `boolean` | `false` | Columns show skeleton cards in the card geometry; the cards cross-fade in when it turns off. |
59
+ | `disabled` | `boolean` | `false` | Read-only board: no drag, no keyboard moves; clicks on cards still work. |
60
+ | `labels` | `Partial<KanbanLabels>` | — | Built-in strings, see Labels. |
61
+ | `…rest` | `Omit<HTMLAttributes<HTMLDivElement>, "children" \| "defaultValue">` | — | `className` (the board height), `aria-label` and the other div attributes. |
62
+
63
+ ### Kanban.Item
64
+ `ref` → `HTMLLIElement`. One card: an `<li>` on the raised fill (`bg-raised`) with the card shadow; return it from `renderItem` (it takes its id from there). Draggable by the whole card, focusable, `aria-keyshortcuts` for Alt + arrows. Presses on buttons, fields and links inside never start a drag.
65
+
66
+ | Prop | Type | Default | Description |
67
+ |---|---|---|---|
68
+ | `disabled` | `boolean` | `false` | This card cannot be dragged or moved. |
69
+ | `…rest` | `Omit<HTMLAttributes<HTMLElement>, "id">` | — | `children` (the card parts), `className`, `onClick` and the other attributes; `onKeyDown` runs first, `preventDefault()` keeps the move from happening. |
70
+
71
+ ### Kanban.ItemTitle · Kanban.ItemDescription
72
+ `ref` → `HTMLSpanElement`. The card title (medium body text, up to three lines) and the muted meta line under it (id, due date; one line, tabular numbers).
73
+
74
+ | Prop | Type | Default | Description |
75
+ |---|---|---|---|
76
+ | `…rest` | `HTMLAttributes<HTMLSpanElement>` | — | `children`, `className` and the other span attributes. |
77
+
78
+ ### Kanban.ItemBadges
79
+ `ref` → `HTMLDivElement`. A wrapping row of `Badge`s: labels, priority.
80
+
81
+ | Prop | Type | Default | Description |
82
+ |---|---|---|---|
83
+ | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children`, `className` and the other div attributes. |
84
+
85
+ ### Kanban.ItemFooter
86
+ `ref` → `HTMLDivElement`. The bottom row: `Kanban.ItemCount`s at the start; the last child, unless a count, sits at the end (the assignee `Avatar`).
87
+
88
+ | Prop | Type | Default | Description |
89
+ |---|---|---|---|
90
+ | `…rest` | `HTMLAttributes<HTMLDivElement>` | — | `children`, `className` and the other div attributes. |
91
+
92
+ ### Kanban.ItemCount
93
+ `ref` → `HTMLSpanElement`. A small muted counter with a 14 px icon: comments, attachments, subtasks.
94
+
95
+ | Prop | Type | Default | Description |
96
+ |---|---|---|---|
97
+ | `…rest` | `HTMLAttributes<HTMLSpanElement>` | — | `children`, `className` and the other span attributes. |
98
+
99
+ ## Variants
100
+
101
+ The board has no visual variants: one look, fills over lines.
102
+
103
+ ### Flags
104
+ | Value | Looks like | Use when | Default |
105
+ |---|---|---|---|
106
+ | `limit` (column) | count reads `count / limit`, orange when full; a full column refuses cards from others | a WIP limit per status | — |
107
+ | `canDrop` | the column turns `danger` under a refused card; a keyboard move is announced as refused | workflow rules (a blocked ticket cannot be closed) | — |
108
+ | `loading` | skeleton cards in the card geometry, no counts | the first load or a refresh | `false` |
109
+ | `disabled` | cards do not drag, Alt + arrows do nothing | read-only boards, no permission | `false` |
110
+
111
+ ## States
112
+ | State | Driven by | DOM |
113
+ |---|---|---|
114
+ | idle | — | cards show `cursor: grab`; under a mouse the card gets a `fill-subtle` wash (`@media (hover: hover)` only) |
115
+ | carried | a drag | Dnd: the clone lifts on `--prime-shadow-modal`, a dashed accent gap opens where the card lands, cards glide aside |
116
+ | over a column | pointer over another column | the gap opens there; an empty column's well fades out |
117
+ | refused | `limit` reached, `canDrop` returns `false` | `data-dnd-reject` on the `<ul>`: `danger-soft` fill |
118
+ | full | count ≥ `limit` | `data-full` on the column, orange count `count / limit` |
119
+ | empty | no cards | a compact `EmptyPage` («Нет карточек») in an 80 px well at the top; the whole column stays a drop area |
120
+ | loading | `loading` | `aria-busy` and `data-loading` on the root; `Skeleton` cards; `Crossfade` swaps them for the cards |
121
+ | disabled | `disabled` / `Kanban.Item disabled` | `data-disabled` on the root / card; no `aria-roledescription`, no shortcuts |
122
+
123
+ Motion: the hover wash and the empty well fade on opacity (`fast`, `standard`); the swap from skeletons is `Crossfade`; drag motion is Dnd's (lift, gap, glide, landing). Under `prefers-reduced-motion` all of it is off.
124
+
125
+ ## Layout & spacing
126
+ - Give the root a height (`className`): columns span it and every column body scrolls on its own; without one the columns take the height of the tallest.
127
+ - The board is a size container. From 40rem of its own width columns keep a fixed 18rem; below it a column takes 85% of the board and snaps (`scroll-snap-type: x mandatory`), so the next column peeks. Snapping is off while a drag auto-scrolls the strip.
128
+ - The strip scrolls sideways inside the board, never the page. Cards sit at the top of their column; the drop area runs to the bottom of the column, so a card can be released anywhere in it.
129
+ - Column gap `--prime-space-3`, card gap `--prime-space-2`, card padding `--prime-space-3`, column radius `l`, card radius `m`.
130
+ - Header actions get the `s` tier; inside a card, kit controls take the `m` host tier (a `Badge` is `s`). Pick a small `Avatar` (`size="s"`).
131
+
132
+ ## Accessibility
133
+
134
+ ### Keyboard
135
+ | Key | Action |
136
+ |---|---|
137
+ | `Tab` | Moves through the cards, column by column. |
138
+ | `Alt + ↑` · `Alt + ↓` | Moves the card one place inside its column. |
139
+ | `Alt + ←` · `Alt + →` | Moves the card to the neighbouring column at the same row (or its end); focus follows it. The direction follows the writing direction (`dir="rtl"`: ← is the next column). |
140
+ | `Escape` | Cancels a drag in flight. |
141
+
142
+ ### ARIA
143
+ - Every column is a `<ul>` labelled by its heading (`aria-labelledby`); cards are `<li>` with `aria-roledescription` and `aria-keyshortcuts`.
144
+ - A live region (`role="status"`) announces grabbed, dropped, «position N of M», a move to another column and a refusal (`labels`).
145
+ - The count is spoken as text («Карточек: 3 из 5»), not only by colour; a full column also reads `count / limit`.
146
+ - Touch: a drag starts after a hold; a finger that moves first scrolls the board.
147
+ - While loading the root has `aria-busy`; skeletons are hidden from assistive tech.
148
+
149
+ ### Labels
150
+ <!-- Generated from api.ts by `bun run docs:build`. Edit api.ts, not this section. -->
151
+
152
+ | Key | Default | Used for |
153
+ |---|---|---|
154
+ | `count` | `"Карточек: {count}"` | Spoken name of the count badge. |
155
+ | `countLimit` | `"Карточек: {count} из {limit}"` | Spoken name of the count badge of a column with a `limit`. |
156
+ | `empty` | `"Нет карточек"` | Text of an empty column. |
157
+ | `moved` | `"{label}: «{column}», позиция {position} из {total}"` | Live region: a card moved to another column with the keyboard. |
158
+ | `refused` | `"{label}: колонка «{column}» не принимает карточку"` | Live region: the next column refused the card (full or `canDrop`). |
159
+
160
+ ## Examples
161
+ | Example | Shows |
162
+ |---|---|
163
+ | [overview.tsx](examples/overview.tsx) | A release board: cards move within and between columns by drag, a touch hold or Alt + arrows; a card shows its meta line, labels, comments and assignee — `Kanban.Root`, `Kanban.Item`, `renderItem`, `renderColumnActions`. |
164
+ | [structure.tsx](examples/structure.tsx) | Cards take only the parts they need: a title alone, a meta line, labels, a footer with counts and an assignee — `Kanban.ItemTitle`, `Kanban.ItemDescription`, `Kanban.ItemBadges`, `Kanban.ItemFooter`, `Kanban.ItemCount`. |
165
+ | [limits.tsx](examples/limits.tsx) | A support queue with a work-in-progress limit and a workflow rule: a full column and a blocked ticket are refused before the release, from the keyboard too — `limit`, `canDrop`. |
166
+ | [states.tsx](examples/states.tsx) | Loading draws skeleton cards in the card geometry and cross-fades the cards in; an empty column keeps its drop area; a read-only board drags nothing — `loading`, `disabled`. |
167
+ | [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`. |
168
+ | [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. |
169
+
170
+ ## Mistakes
171
+ - Returning something other than `Kanban.Item` from `renderItem` → the card does not drag and Alt + arrows do nothing.
172
+ - Using the array index as the card id → ids must survive a move.
173
+ - Keeping the status only on the item and no `value` → the board draws nothing; build `defaultValue` (or `value`) from your data: `{ [status]: ids }`.
174
+ - No height on the root inside a fixed region → the page scrolls instead of the column.
175
+ - A hover-only action on a card → touch has no hover; put the action in the card or open the card on click.
176
+ - Styling a card from an ancestor (`.board .card`) → the flying clone is portalled to `<body>` and loses it; put the class on `Kanban.Item`.
177
+ - Drag as the only way to change a status → keep a status field or a menu in the card details too.
178
+
179
+ ## Related
180
+ - **Built from:** [Dnd](../dnd/COMPONENT.md), [Badge](../badge/COMPONENT.md), [ScrollContainer](../scroll-container/COMPONENT.md), [Skeleton](../skeleton/COMPONENT.md), [Crossfade](../crossfade/COMPONENT.md), [EmptyPage](../empty-page/COMPONENT.md)
181
+ - **See also:** [DataTable](../data-table/COMPONENT.md), [Card](../card/COMPONENT.md), [Avatar](../avatar/COMPONENT.md), [Timeline](../timeline/COMPONENT.md)
@@ -0,0 +1,78 @@
1
+ /** 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`. */
2
+ import { Button, Kanban, type KanbanMove, type KanbanValue, Typography } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ type Invoice = { id: string; client: string; amount: string };
8
+
9
+ const COLUMNS = [
10
+ { id: "draft", title: "Черновики" },
11
+ { id: "sent", title: "Отправлены" },
12
+ { id: "paid", title: "Оплачены" },
13
+ ];
14
+
15
+ const INVOICES: Invoice[] = [
16
+ { id: "INV-2041", client: "ООО «Север»", amount: "48 300 ₽" },
17
+ { id: "INV-2042", client: "АО «Гранит»", amount: "212 000 ₽" },
18
+ { id: "INV-2038", client: "ИП Лебедев", amount: "7 950 ₽" },
19
+ { id: "INV-2035", client: "ООО «Вектор»", amount: "95 400 ₽" },
20
+ ];
21
+
22
+ const INITIAL: KanbanValue = {
23
+ draft: ["INV-2041", "INV-2042"],
24
+ sent: ["INV-2038"],
25
+ paid: ["INV-2035"],
26
+ };
27
+
28
+ const TITLES = new Map(COLUMNS.map((column) => [column.id, column.title]));
29
+
30
+ export default function KanbanControlledExample() {
31
+ const [placement, setPlacement] = React.useState(INITIAL);
32
+ const [lastMove, setLastMove] = React.useState<KanbanMove | null>(null);
33
+
34
+ return (
35
+ <div className={styles.stage}>
36
+ <div className={styles.toolbar}>
37
+ <Typography as="span" variant="body-s" tone="muted" aria-live="polite">
38
+ {lastMove
39
+ ? `${lastMove.id}: ${TITLES.get(lastMove.from)} → ${TITLES.get(lastMove.to)}, позиция ${lastMove.index + 1}`
40
+ : "Перенесите счёт — изменение уйдёт на сервер"}
41
+ </Typography>
42
+ <div className={styles.spacer} />
43
+ <Button.Root
44
+ variant="ghost"
45
+ tone="neutral"
46
+ disabled={placement === INITIAL}
47
+ onClick={() => {
48
+ setPlacement(INITIAL);
49
+ setLastMove(null);
50
+ }}
51
+ >
52
+ Сбросить
53
+ </Button.Root>
54
+ </div>
55
+ <Kanban.Root
56
+ aria-label="Счета за октябрь"
57
+ className={styles.board}
58
+ columns={COLUMNS}
59
+ items={INVOICES}
60
+ getId={(invoice) => invoice.id}
61
+ getLabel={(invoice) => invoice.id}
62
+ value={placement}
63
+ onValueChange={(next, move) => {
64
+ setPlacement(next);
65
+ setLastMove(move);
66
+ }}
67
+ renderItem={(invoice) => (
68
+ <Kanban.Item>
69
+ <Kanban.ItemTitle>{invoice.client}</Kanban.ItemTitle>
70
+ <Kanban.ItemDescription>
71
+ {invoice.id} · {invoice.amount}
72
+ </Kanban.ItemDescription>
73
+ </Kanban.Item>
74
+ )}
75
+ />
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,31 @@
1
+ /* Layout of the Kanban scenarios: semantic tokens only. */
2
+
3
+ /* A board gets its height from the page region it fills; tall columns scroll inside it. */
4
+ .board {
5
+ block-size: calc(var(--prime-space-24) * 5);
6
+ }
7
+
8
+ /* A board that stands below its own toolbar. */
9
+ .stage {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: var(--prime-space-3);
13
+ inline-size: 100%;
14
+ }
15
+
16
+ .toolbar {
17
+ display: flex;
18
+ flex-wrap: wrap;
19
+ align-items: center;
20
+ gap: var(--prime-space-3);
21
+ }
22
+
23
+ .spacer {
24
+ flex: 1;
25
+ }
26
+
27
+ /* A phone-width screen. */
28
+ .phone {
29
+ inline-size: min(100%, calc(var(--prime-space-20) * 4.5));
30
+ block-size: calc(var(--prime-space-24) * 5);
31
+ }
@@ -0,0 +1,58 @@
1
+ /** A support queue with a work-in-progress limit and a workflow rule: a full column and a blocked ticket are refused before the release, from the keyboard too — `limit`, `canDrop`. */
2
+ import { Badge, Kanban } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ type Ticket = { id: string; title: string; client: string; blocked?: boolean };
7
+
8
+ const COLUMNS = [
9
+ { id: "new", title: "Новые" },
10
+ { id: "progress", title: "В работе", limit: 3 },
11
+ { id: "solved", title: "Решены" },
12
+ ];
13
+
14
+ const TICKETS: Ticket[] = [
15
+ { id: "SUP-1042", title: "Не приходит письмо со счётом", client: "ООО «Север»" },
16
+ { id: "SUP-1045", title: "Ошибка оплаты картой", client: "ИП Лебедев", blocked: true },
17
+ { id: "SUP-1038", title: "Сменить владельца аккаунта", client: "ООО «Вектор»" },
18
+ { id: "SUP-1036", title: "Дубли в отчёте по заказам", client: "АО «Гранит»" },
19
+ { id: "SUP-1031", title: "Нет доступа к API-ключам", client: "ООО «Логистик»" },
20
+ { id: "SUP-1027", title: "Перенести проект в другую команду", client: "ООО «Север»" },
21
+ ];
22
+
23
+ const BLOCKED = new Set(TICKETS.filter((ticket) => ticket.blocked).map((ticket) => ticket.id));
24
+
25
+ const PLACEMENT = {
26
+ new: ["SUP-1042", "SUP-1045"],
27
+ progress: ["SUP-1038", "SUP-1036", "SUP-1031"],
28
+ solved: ["SUP-1027"],
29
+ };
30
+
31
+ export default function KanbanLimitsExample() {
32
+ return (
33
+ <Kanban.Root
34
+ aria-label="Очередь поддержки"
35
+ className={styles.board}
36
+ columns={COLUMNS}
37
+ items={TICKETS}
38
+ getId={(ticket) => ticket.id}
39
+ getLabel={(ticket) => ticket.title}
40
+ defaultValue={PLACEMENT}
41
+ // A ticket waiting on the client cannot be closed.
42
+ canDrop={(id, columnId) => columnId !== "solved" || !BLOCKED.has(id)}
43
+ renderItem={(ticket) => (
44
+ <Kanban.Item>
45
+ <Kanban.ItemTitle>{ticket.title}</Kanban.ItemTitle>
46
+ <Kanban.ItemDescription>
47
+ {ticket.id} · {ticket.client}
48
+ </Kanban.ItemDescription>
49
+ {ticket.blocked ? (
50
+ <Kanban.ItemBadges>
51
+ <Badge.Root color="orange">Ждёт клиента</Badge.Root>
52
+ </Kanban.ItemBadges>
53
+ ) : null}
54
+ </Kanban.Item>
55
+ )}
56
+ />
57
+ );
58
+ }
@@ -0,0 +1,76 @@
1
+ /** 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. */
2
+ import { Avatar, Kanban } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ type Task = { id: string; title: string; due: string; initials: string; name: string };
7
+
8
+ const COLUMNS = [
9
+ { id: "todo", title: "К выполнению" },
10
+ { id: "progress", title: "В работе" },
11
+ { id: "done", title: "Готово" },
12
+ ];
13
+
14
+ const TASKS: Task[] = [
15
+ {
16
+ id: "PRJ-301",
17
+ title: "Подготовить отчёт по продажам",
18
+ due: "до 14 окт",
19
+ initials: "АК",
20
+ name: "Анна Котова",
21
+ },
22
+ {
23
+ id: "PRJ-305",
24
+ title: "Созвон с ООО «Вектор»",
25
+ due: "до 9 окт",
26
+ initials: "ИС",
27
+ name: "Илья Смирнов",
28
+ },
29
+ {
30
+ id: "PRJ-297",
31
+ title: "Обновить прайс-лист",
32
+ due: "до 11 окт",
33
+ initials: "МВ",
34
+ name: "Мария Волкова",
35
+ },
36
+ {
37
+ id: "PRJ-290",
38
+ title: "Закрыть акты за сентябрь",
39
+ due: "2 окт",
40
+ initials: "АК",
41
+ name: "Анна Котова",
42
+ },
43
+ ];
44
+
45
+ const PLACEMENT = {
46
+ todo: ["PRJ-301", "PRJ-305"],
47
+ progress: ["PRJ-297"],
48
+ done: ["PRJ-290"],
49
+ };
50
+
51
+ export default function KanbanNarrowExample() {
52
+ return (
53
+ <Kanban.Root
54
+ aria-label="Мои задачи"
55
+ className={styles.phone}
56
+ columns={COLUMNS}
57
+ items={TASKS}
58
+ getId={(task) => task.id}
59
+ getLabel={(task) => task.title}
60
+ defaultValue={PLACEMENT}
61
+ renderItem={(task) => (
62
+ <Kanban.Item>
63
+ <Kanban.ItemTitle>{task.title}</Kanban.ItemTitle>
64
+ <Kanban.ItemFooter>
65
+ <Kanban.ItemDescription>
66
+ {task.id} · {task.due}
67
+ </Kanban.ItemDescription>
68
+ <Avatar.Root size="s" aria-label={task.name}>
69
+ <Avatar.Fallback>{task.initials}</Avatar.Fallback>
70
+ </Avatar.Root>
71
+ </Kanban.ItemFooter>
72
+ </Kanban.Item>
73
+ )}
74
+ />
75
+ );
76
+ }