prime-ui-kit 0.9.1 → 0.9.3

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 (169) hide show
  1. package/README.md +30 -12
  2. package/dist/chunks/{chunk-NHOWNIAR.js → chunk-6JTZOA5I.js} +1472 -671
  3. package/dist/chunks/{chunk-DB2Y3XXA.js → chunk-OAE4IQX2.js} +2845 -1732
  4. package/dist/chunks/{chunk-KCQFOORX.js → chunk-U5SR2LZR.js} +17 -4
  5. package/dist/color-picker.js +2 -2
  6. package/dist/components/button/Button.d.ts +19 -2
  7. package/dist/components/card/Card.d.ts +1 -1
  8. package/dist/components/code-block/CodeBlock.d.ts +1 -1
  9. package/dist/components/color-picker/ColorPicker.d.ts +1 -1
  10. package/dist/components/data-table/DataTable.d.ts +6 -1
  11. package/dist/components/data-table/Row.d.ts +3 -1
  12. package/dist/components/data-table/rows.d.ts +4 -0
  13. package/dist/components/data-table/types.d.ts +5 -0
  14. package/dist/components/digit-input/DigitInput.d.ts +6 -1
  15. package/dist/components/dnd/Dnd.d.ts +1 -1
  16. package/dist/components/dnd/dragSession.d.ts +9 -0
  17. package/dist/components/dnd/useDropTarget.d.ts +5 -0
  18. package/dist/components/dnd/useFlipList.d.ts +3 -3
  19. package/dist/components/drawer/Drawer.d.ts +8 -5
  20. package/dist/components/dropdown/Dropdown.d.ts +17 -0
  21. package/dist/components/index.d.ts +9 -2
  22. package/dist/components/input/Input.d.ts +16 -1
  23. package/dist/components/input/passwordStrength.d.ts +9 -0
  24. package/dist/components/kanban/Kanban.d.ts +129 -0
  25. package/dist/components/modal/Modal.d.ts +5 -2
  26. package/dist/components/page-toolbar/PageToolbar.d.ts +53 -0
  27. package/dist/components/progress-bar/ProgressBar.d.ts +16 -1
  28. package/dist/components/segmented-control/SegmentedControl.d.ts +5 -2
  29. package/dist/components/sparkline/Sparkline.d.ts +26 -0
  30. package/dist/components/tabs/Tabs.d.ts +5 -2
  31. package/dist/hooks/useEdgeOverflow.d.ts +16 -0
  32. package/dist/hooks/useMediaQuery.d.ts +7 -0
  33. package/dist/hooks/useSwipeDismiss.d.ts +34 -0
  34. package/dist/icons/registry.d.ts +2 -0
  35. package/dist/index.css +2977 -1306
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +17 -3
  38. package/dist/internal/CheckMark.d.ts +12 -0
  39. package/dist/internal/MorphText.d.ts +12 -0
  40. package/dist/internal/RollingNumber.d.ts +14 -0
  41. package/dist/internal/overlay/FloatingPanel.d.ts +3 -1
  42. package/dist/internal/overlay/useFloatingLayer.d.ts +11 -1
  43. package/dist/internal/surfaceDepth.d.ts +25 -0
  44. package/dist/internal/swatch.d.ts +4 -1
  45. package/dist/internal/touchTarget.d.ts +4 -0
  46. package/dist/layout/app-header/AppHeader.d.ts +110 -0
  47. package/dist/layout/app-shell/AppShell.d.ts +16 -11
  48. package/dist/layout/bottom-nav/BottomNav.d.ts +80 -0
  49. package/dist/layout/index.d.ts +5 -1
  50. package/dist/motion/celebrate.d.ts +18 -0
  51. package/dist/tokens/layers.d.ts +37 -0
  52. package/dist/tokens/naming.d.ts +1 -1
  53. package/dist/tokens/primitives.d.ts +4 -0
  54. package/dist/tokens/semantic.d.ts +122 -56
  55. package/dist/tokens/themes/dark.d.ts +10 -14
  56. package/package.json +3 -2
  57. package/src/components/accordion/COMPONENT.md +5 -5
  58. package/src/components/avatar/COMPONENT.md +1 -1
  59. package/src/components/badge/COMPONENT.md +2 -2
  60. package/src/components/banner/COMPONENT.md +1 -1
  61. package/src/components/button/COMPONENT.md +26 -5
  62. package/src/components/button/examples/download.tsx +45 -0
  63. package/src/components/button/examples/hold-to-confirm.tsx +23 -0
  64. package/src/components/button/examples/label-morph.tsx +33 -0
  65. package/src/components/card/COMPONENT.md +4 -2
  66. package/src/components/card/examples/narrow.tsx +2 -2
  67. package/src/components/card/examples/panel-chart.tsx +1 -1
  68. package/src/components/card/examples/variants.tsx +1 -1
  69. package/src/components/checkbox/examples/examples.module.css +5 -3
  70. package/src/components/code-block/COMPONENT.md +6 -6
  71. package/src/components/code-block/examples/overview.tsx +1 -1
  72. package/src/components/code-block/examples/variants.tsx +1 -1
  73. package/src/components/color-picker/COMPONENT.md +1 -1
  74. package/src/components/color-swatches/COMPONENT.md +5 -5
  75. package/src/components/command-menu/COMPONENT.md +1 -1
  76. package/src/components/crossfade/COMPONENT.md +1 -1
  77. package/src/components/crossfade/examples/overview.tsx +3 -3
  78. package/src/components/data-table/COMPONENT.md +24 -10
  79. package/src/components/data-table/examples/columns-visibility.tsx +167 -0
  80. package/src/components/digit-input/COMPONENT.md +15 -13
  81. package/src/components/digit-input/examples/examples.module.css +8 -0
  82. package/src/components/digit-input/examples/in-form.tsx +2 -5
  83. package/src/components/digit-input/examples/on-complete.tsx +7 -6
  84. package/src/components/digit-input/examples/sizes.tsx +1 -1
  85. package/src/components/divider/COMPONENT.md +1 -1
  86. package/src/components/dnd/COMPONENT.md +3 -3
  87. package/src/components/dnd/examples/examples.module.css +4 -4
  88. package/src/components/drawer/COMPONENT.md +17 -9
  89. package/src/components/drawer/examples/date-sheet.tsx +41 -0
  90. package/src/components/drawer/examples/examples.module.css +5 -0
  91. package/src/components/drawer/examples/placement.tsx +4 -2
  92. package/src/components/dropdown/COMPONENT.md +20 -6
  93. package/src/components/dropdown/examples/checkbox-items.tsx +48 -0
  94. package/src/components/dropdown/examples/examples.module.css +1 -1
  95. package/src/components/empty-page/COMPONENT.md +1 -1
  96. package/src/components/empty-page/examples/examples.module.css +1 -1
  97. package/src/components/example-frame/COMPONENT.md +1 -1
  98. package/src/components/input/COMPONENT.md +10 -1
  99. package/src/components/input/examples/password-strength.tsx +12 -0
  100. package/src/components/kanban/COMPONENT.md +181 -0
  101. package/src/components/kanban/examples/controlled.tsx +78 -0
  102. package/src/components/kanban/examples/examples.module.css +31 -0
  103. package/src/components/kanban/examples/limits.tsx +58 -0
  104. package/src/components/kanban/examples/narrow.tsx +76 -0
  105. package/src/components/kanban/examples/overview.tsx +130 -0
  106. package/src/components/kanban/examples/states.tsx +75 -0
  107. package/src/components/kanban/examples/structure.tsx +76 -0
  108. package/src/components/login-form/COMPONENT.md +5 -5
  109. package/src/components/login-form/examples/register.tsx +2 -2
  110. package/src/components/login-form/examples/reset-password.tsx +2 -2
  111. package/src/components/modal/COMPONENT.md +11 -8
  112. package/src/components/modal/examples/examples.module.css +2 -2
  113. package/src/components/notification/COMPONENT.md +2 -2
  114. package/src/components/page-content/COMPONENT.md +1 -1
  115. package/src/components/page-content/examples/examples.module.css +1 -1
  116. package/src/components/page-toolbar/COMPONENT.md +142 -0
  117. package/src/components/page-toolbar/examples/examples.module.css +16 -0
  118. package/src/components/page-toolbar/examples/narrow.tsx +103 -0
  119. package/src/components/page-toolbar/examples/overview.tsx +113 -0
  120. package/src/components/page-toolbar/examples/save-changes.tsx +40 -0
  121. package/src/components/page-toolbar/examples/without-search.tsx +60 -0
  122. package/src/components/popover/COMPONENT.md +5 -4
  123. package/src/components/progress-bar/COMPONENT.md +6 -2
  124. package/src/components/progress-bar/examples/examples.module.css +7 -0
  125. package/src/components/progress-bar/examples/milestone.tsx +43 -0
  126. package/src/components/progress-bar/examples/steps.tsx +35 -0
  127. package/src/components/progress-circle/COMPONENT.md +1 -1
  128. package/src/components/scroll-container/COMPONENT.md +1 -1
  129. package/src/components/segmented-control/COMPONENT.md +8 -8
  130. package/src/components/segmented-control/examples/full-width.tsx +1 -1
  131. package/src/components/select/COMPONENT.md +2 -1
  132. package/src/components/skeleton/COMPONENT.md +1 -1
  133. package/src/components/skeleton/examples/with-crossfade.tsx +3 -3
  134. package/src/components/smart-filter/COMPONENT.md +2 -0
  135. package/src/components/sparkline/COMPONENT.md +98 -0
  136. package/src/components/sparkline/examples/examples.module.css +13 -0
  137. package/src/components/sparkline/examples/overview.tsx +27 -0
  138. package/src/components/sparkline/examples/period.tsx +46 -0
  139. package/src/components/spinner/COMPONENT.md +1 -1
  140. package/src/components/tabs/COMPONENT.md +24 -22
  141. package/src/components/tabs/examples/closable.tsx +1 -1
  142. package/src/components/tabs/examples/examples.module.css +14 -0
  143. package/src/components/tabs/examples/orientation.tsx +87 -33
  144. package/src/components/tooltip/COMPONENT.md +8 -6
  145. package/src/components/tooltip/examples/examples.module.css +2 -2
  146. package/src/layout/app-header/COMPONENT.md +178 -0
  147. package/src/layout/app-header/examples/breadcrumbs.tsx +36 -0
  148. package/src/layout/app-header/examples/examples.module.css +47 -0
  149. package/src/layout/app-header/examples/in-app-shell.tsx +70 -0
  150. package/src/layout/app-header/examples/narrow.tsx +29 -0
  151. package/src/layout/app-header/examples/overview.tsx +36 -0
  152. package/src/layout/app-shell/COMPONENT.md +35 -29
  153. package/src/layout/app-shell/examples/examples.module.css +17 -2
  154. package/src/layout/app-shell/examples/footer.tsx +59 -0
  155. package/src/layout/app-shell/examples/overview.tsx +19 -8
  156. package/src/layout/bottom-nav/COMPONENT.md +153 -0
  157. package/src/layout/bottom-nav/examples/examples.module.css +39 -0
  158. package/src/layout/bottom-nav/examples/floating.tsx +89 -0
  159. package/src/layout/bottom-nav/examples/icon-only.tsx +41 -0
  160. package/src/layout/bottom-nav/examples/in-app-shell.tsx +64 -0
  161. package/src/layout/bottom-nav/examples/overview.tsx +44 -0
  162. package/src/layout/bottom-nav/examples/router.tsx +31 -0
  163. package/src/layout/bottom-nav/examples/structure.tsx +47 -0
  164. package/src/layout/sidebar/COMPONENT.md +14 -13
  165. package/src/layout/sidebar/examples/examples.module.css +5 -5
  166. package/src/styles/globals.css +125 -2
  167. package/src/styles/theme-dark.css +39 -13
  168. package/src/styles/theme-light.css +84 -16
  169. package/src/styles/tokens.css +2 -0
@@ -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-muted` well (one ladder step off the board's surface); 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>` two ladder steps lighter than its column (`control-selected`: white in light) with the raised shadow, lifted to the overlay shadow on hover; 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 lifts to `--prime-shadow-overlay` (`@media (hover: hover)` only) — no wash: a wash is one ladder step, the column's own color |
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 lift (shadow, the card rises above its neighbours) and the empty well fade (`fast`, `standard`); a card dropped under the mouse lands straight into its hover lift (`data-dnd-hover`); the swap from skeletons is `Crossfade`; drag motion is Dnd's (lift, gap, glide, landing). 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
+ }
@@ -0,0 +1,130 @@
1
+ /** 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`. */
2
+ import { Avatar, Badge, Button, Icon, Kanban, type PaletteColor } from "prime-ui-kit";
3
+
4
+ import styles from "./examples.module.css";
5
+
6
+ type Task = {
7
+ id: string;
8
+ title: string;
9
+ due: string;
10
+ labels: { text: string; color: PaletteColor }[];
11
+ comments: number;
12
+ assignee: { initials: string; name: string; color: PaletteColor };
13
+ };
14
+
15
+ const COLUMNS = [
16
+ { id: "backlog", title: "Бэклог" },
17
+ { id: "progress", title: "В работе" },
18
+ { id: "review", title: "На проверке" },
19
+ { id: "done", title: "Готово" },
20
+ ];
21
+
22
+ const ANNA = { initials: "АК", name: "Анна Котова", color: "purple" } as const;
23
+ const ILYA = { initials: "ИС", name: "Илья Смирнов", color: "teal" } as const;
24
+ const MARIA = { initials: "МВ", name: "Мария Волкова", color: "orange" } as const;
25
+
26
+ const TASKS: Task[] = [
27
+ {
28
+ id: "PRJ-214",
29
+ title: "Экспорт счетов в CSV для бухгалтерии",
30
+ due: "14 окт",
31
+ labels: [{ text: "Биллинг", color: "blue" }],
32
+ comments: 3,
33
+ assignee: ANNA,
34
+ },
35
+ {
36
+ id: "PRJ-221",
37
+ title: "Фильтр заказов по складу",
38
+ due: "17 окт",
39
+ labels: [{ text: "Заказы", color: "green" }],
40
+ comments: 0,
41
+ assignee: ILYA,
42
+ },
43
+ {
44
+ id: "PRJ-198",
45
+ title: "Повторная отправка приглашений в команду",
46
+ due: "10 окт",
47
+ labels: [
48
+ { text: "Команда", color: "purple" },
49
+ { text: "Срочно", color: "red" },
50
+ ],
51
+ comments: 5,
52
+ assignee: MARIA,
53
+ },
54
+ {
55
+ id: "PRJ-205",
56
+ title: "Тёмная тема для отчётов",
57
+ due: "12 окт",
58
+ labels: [{ text: "Дизайн", color: "pink" }],
59
+ comments: 2,
60
+ assignee: ANNA,
61
+ },
62
+ {
63
+ id: "PRJ-187",
64
+ title: "Лимиты API для партнёров",
65
+ due: "9 окт",
66
+ labels: [{ text: "API", color: "sky" }],
67
+ comments: 8,
68
+ assignee: ILYA,
69
+ },
70
+ {
71
+ id: "PRJ-176",
72
+ title: "Онбординг нового проекта",
73
+ due: "3 окт",
74
+ labels: [{ text: "Проекты", color: "yellow" }],
75
+ comments: 1,
76
+ assignee: MARIA,
77
+ },
78
+ ];
79
+
80
+ const PLACEMENT = {
81
+ backlog: ["PRJ-214", "PRJ-221"],
82
+ progress: ["PRJ-198", "PRJ-205"],
83
+ review: ["PRJ-187"],
84
+ done: ["PRJ-176"],
85
+ };
86
+
87
+ export default function KanbanOverviewExample() {
88
+ return (
89
+ <Kanban.Root
90
+ aria-label="Задачи релиза 2.4"
91
+ className={styles.board}
92
+ columns={COLUMNS}
93
+ items={TASKS}
94
+ getId={(task) => task.id}
95
+ getLabel={(task) => task.title}
96
+ defaultValue={PLACEMENT}
97
+ renderColumnActions={(column) => (
98
+ <Button.Root variant="ghost" tone="neutral" aria-label={`Добавить в «${column.title}»`}>
99
+ <Button.Icon>
100
+ <Icon name="action.add" />
101
+ </Button.Icon>
102
+ </Button.Root>
103
+ )}
104
+ renderItem={(task) => (
105
+ <Kanban.Item>
106
+ <Kanban.ItemTitle>{task.title}</Kanban.ItemTitle>
107
+ <Kanban.ItemDescription>
108
+ {task.id} · до {task.due}
109
+ </Kanban.ItemDescription>
110
+ <Kanban.ItemBadges>
111
+ {task.labels.map((label) => (
112
+ <Badge.Root key={label.text} color={label.color}>
113
+ {label.text}
114
+ </Badge.Root>
115
+ ))}
116
+ </Kanban.ItemBadges>
117
+ <Kanban.ItemFooter>
118
+ <Kanban.ItemCount>
119
+ <Icon name="object.message" />
120
+ {task.comments}
121
+ </Kanban.ItemCount>
122
+ <Avatar.Root size="s" color={task.assignee.color} aria-label={task.assignee.name}>
123
+ <Avatar.Fallback>{task.assignee.initials}</Avatar.Fallback>
124
+ </Avatar.Root>
125
+ </Kanban.ItemFooter>
126
+ </Kanban.Item>
127
+ )}
128
+ />
129
+ );
130
+ }
@@ -0,0 +1,75 @@
1
+ /** 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`. */
2
+ import { Button, Icon, Kanban, Switch } from "prime-ui-kit";
3
+ import * as React from "react";
4
+
5
+ import styles from "./examples.module.css";
6
+
7
+ type Order = { id: string; title: string; total: string };
8
+
9
+ const COLUMNS = [
10
+ { id: "new", title: "Новые" },
11
+ { id: "packing", title: "Сборка" },
12
+ { id: "shipped", title: "Отгружены" },
13
+ ];
14
+
15
+ const ORDERS: Order[] = [
16
+ { id: "ORD-5531", title: "ООО «Север» · 12 позиций", total: "48 300 ₽" },
17
+ { id: "ORD-5529", title: "ИП Лебедев · 3 позиции", total: "7 950 ₽" },
18
+ { id: "ORD-5524", title: "АО «Гранит» · 40 позиций", total: "212 000 ₽" },
19
+ ];
20
+
21
+ const PLACEMENT = { new: ["ORD-5531", "ORD-5529"], packing: [], shipped: ["ORD-5524"] };
22
+
23
+ const LOAD_MS = 1200;
24
+
25
+ export default function KanbanStatesExample() {
26
+ const [loading, setLoading] = React.useState(true);
27
+ const [readOnly, setReadOnly] = React.useState(false);
28
+
29
+ React.useEffect(() => {
30
+ if (!loading) return;
31
+ const timer = window.setTimeout(() => setLoading(false), LOAD_MS);
32
+ return () => window.clearTimeout(timer);
33
+ }, [loading]);
34
+
35
+ return (
36
+ <div className={styles.stage}>
37
+ <div className={styles.toolbar}>
38
+ <Switch.Root checked={readOnly} onCheckedChange={setReadOnly}>
39
+ <Switch.Label>Только просмотр</Switch.Label>
40
+ </Switch.Root>
41
+ <div className={styles.spacer} />
42
+ <Button.Root
43
+ variant="soft"
44
+ tone="neutral"
45
+ loading={loading}
46
+ onClick={() => setLoading(true)}
47
+ >
48
+ <Button.Icon>
49
+ <Icon name="action.refresh" />
50
+ </Button.Icon>
51
+ Обновить
52
+ </Button.Root>
53
+ </div>
54
+ <Kanban.Root
55
+ aria-label="Заказы склада"
56
+ className={styles.board}
57
+ columns={COLUMNS}
58
+ items={ORDERS}
59
+ getId={(order) => order.id}
60
+ getLabel={(order) => order.id}
61
+ defaultValue={PLACEMENT}
62
+ loading={loading}
63
+ disabled={readOnly}
64
+ renderItem={(order) => (
65
+ <Kanban.Item>
66
+ <Kanban.ItemTitle>{order.id}</Kanban.ItemTitle>
67
+ <Kanban.ItemDescription>
68
+ {order.title} · {order.total}
69
+ </Kanban.ItemDescription>
70
+ </Kanban.Item>
71
+ )}
72
+ />
73
+ </div>
74
+ );
75
+ }