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.
- package/README.md +30 -12
- package/dist/chunks/{chunk-NHOWNIAR.js → chunk-6JTZOA5I.js} +1472 -671
- package/dist/chunks/{chunk-DB2Y3XXA.js → chunk-OAE4IQX2.js} +2845 -1732
- package/dist/chunks/{chunk-KCQFOORX.js → chunk-U5SR2LZR.js} +17 -4
- package/dist/color-picker.js +2 -2
- package/dist/components/button/Button.d.ts +19 -2
- package/dist/components/card/Card.d.ts +1 -1
- package/dist/components/code-block/CodeBlock.d.ts +1 -1
- package/dist/components/color-picker/ColorPicker.d.ts +1 -1
- package/dist/components/data-table/DataTable.d.ts +6 -1
- package/dist/components/data-table/Row.d.ts +3 -1
- package/dist/components/data-table/rows.d.ts +4 -0
- package/dist/components/data-table/types.d.ts +5 -0
- package/dist/components/digit-input/DigitInput.d.ts +6 -1
- package/dist/components/dnd/Dnd.d.ts +1 -1
- package/dist/components/dnd/dragSession.d.ts +9 -0
- package/dist/components/dnd/useDropTarget.d.ts +5 -0
- package/dist/components/dnd/useFlipList.d.ts +3 -3
- package/dist/components/drawer/Drawer.d.ts +8 -5
- package/dist/components/dropdown/Dropdown.d.ts +17 -0
- package/dist/components/index.d.ts +9 -2
- package/dist/components/input/Input.d.ts +16 -1
- package/dist/components/input/passwordStrength.d.ts +9 -0
- package/dist/components/kanban/Kanban.d.ts +129 -0
- package/dist/components/modal/Modal.d.ts +5 -2
- package/dist/components/page-toolbar/PageToolbar.d.ts +53 -0
- package/dist/components/progress-bar/ProgressBar.d.ts +16 -1
- package/dist/components/segmented-control/SegmentedControl.d.ts +5 -2
- package/dist/components/sparkline/Sparkline.d.ts +26 -0
- package/dist/components/tabs/Tabs.d.ts +5 -2
- package/dist/hooks/useEdgeOverflow.d.ts +16 -0
- package/dist/hooks/useMediaQuery.d.ts +7 -0
- package/dist/hooks/useSwipeDismiss.d.ts +34 -0
- package/dist/icons/registry.d.ts +2 -0
- package/dist/index.css +2977 -1306
- package/dist/index.d.ts +1 -0
- package/dist/index.js +17 -3
- package/dist/internal/CheckMark.d.ts +12 -0
- package/dist/internal/MorphText.d.ts +12 -0
- package/dist/internal/RollingNumber.d.ts +14 -0
- package/dist/internal/overlay/FloatingPanel.d.ts +3 -1
- package/dist/internal/overlay/useFloatingLayer.d.ts +11 -1
- package/dist/internal/surfaceDepth.d.ts +25 -0
- package/dist/internal/swatch.d.ts +4 -1
- package/dist/internal/touchTarget.d.ts +4 -0
- package/dist/layout/app-header/AppHeader.d.ts +110 -0
- package/dist/layout/app-shell/AppShell.d.ts +16 -11
- package/dist/layout/bottom-nav/BottomNav.d.ts +80 -0
- package/dist/layout/index.d.ts +5 -1
- package/dist/motion/celebrate.d.ts +18 -0
- package/dist/tokens/layers.d.ts +37 -0
- package/dist/tokens/naming.d.ts +1 -1
- package/dist/tokens/primitives.d.ts +4 -0
- package/dist/tokens/semantic.d.ts +122 -56
- package/dist/tokens/themes/dark.d.ts +10 -14
- package/package.json +3 -2
- package/src/components/accordion/COMPONENT.md +5 -5
- package/src/components/avatar/COMPONENT.md +1 -1
- package/src/components/badge/COMPONENT.md +2 -2
- package/src/components/banner/COMPONENT.md +1 -1
- package/src/components/button/COMPONENT.md +26 -5
- package/src/components/button/examples/download.tsx +45 -0
- package/src/components/button/examples/hold-to-confirm.tsx +23 -0
- package/src/components/button/examples/label-morph.tsx +33 -0
- package/src/components/card/COMPONENT.md +4 -2
- package/src/components/card/examples/narrow.tsx +2 -2
- package/src/components/card/examples/panel-chart.tsx +1 -1
- package/src/components/card/examples/variants.tsx +1 -1
- package/src/components/checkbox/examples/examples.module.css +5 -3
- package/src/components/code-block/COMPONENT.md +6 -6
- package/src/components/code-block/examples/overview.tsx +1 -1
- package/src/components/code-block/examples/variants.tsx +1 -1
- package/src/components/color-picker/COMPONENT.md +1 -1
- package/src/components/color-swatches/COMPONENT.md +5 -5
- package/src/components/command-menu/COMPONENT.md +1 -1
- package/src/components/crossfade/COMPONENT.md +1 -1
- package/src/components/crossfade/examples/overview.tsx +3 -3
- package/src/components/data-table/COMPONENT.md +24 -10
- package/src/components/data-table/examples/columns-visibility.tsx +167 -0
- package/src/components/digit-input/COMPONENT.md +15 -13
- package/src/components/digit-input/examples/examples.module.css +8 -0
- package/src/components/digit-input/examples/in-form.tsx +2 -5
- package/src/components/digit-input/examples/on-complete.tsx +7 -6
- package/src/components/digit-input/examples/sizes.tsx +1 -1
- package/src/components/divider/COMPONENT.md +1 -1
- package/src/components/dnd/COMPONENT.md +3 -3
- package/src/components/dnd/examples/examples.module.css +4 -4
- package/src/components/drawer/COMPONENT.md +17 -9
- package/src/components/drawer/examples/date-sheet.tsx +41 -0
- package/src/components/drawer/examples/examples.module.css +5 -0
- package/src/components/drawer/examples/placement.tsx +4 -2
- package/src/components/dropdown/COMPONENT.md +20 -6
- package/src/components/dropdown/examples/checkbox-items.tsx +48 -0
- package/src/components/dropdown/examples/examples.module.css +1 -1
- package/src/components/empty-page/COMPONENT.md +1 -1
- package/src/components/empty-page/examples/examples.module.css +1 -1
- package/src/components/example-frame/COMPONENT.md +1 -1
- package/src/components/input/COMPONENT.md +10 -1
- package/src/components/input/examples/password-strength.tsx +12 -0
- package/src/components/kanban/COMPONENT.md +181 -0
- package/src/components/kanban/examples/controlled.tsx +78 -0
- package/src/components/kanban/examples/examples.module.css +31 -0
- package/src/components/kanban/examples/limits.tsx +58 -0
- package/src/components/kanban/examples/narrow.tsx +76 -0
- package/src/components/kanban/examples/overview.tsx +130 -0
- package/src/components/kanban/examples/states.tsx +75 -0
- package/src/components/kanban/examples/structure.tsx +76 -0
- package/src/components/login-form/COMPONENT.md +5 -5
- package/src/components/login-form/examples/register.tsx +2 -2
- package/src/components/login-form/examples/reset-password.tsx +2 -2
- package/src/components/modal/COMPONENT.md +11 -8
- package/src/components/modal/examples/examples.module.css +2 -2
- package/src/components/notification/COMPONENT.md +2 -2
- package/src/components/page-content/COMPONENT.md +1 -1
- package/src/components/page-content/examples/examples.module.css +1 -1
- package/src/components/page-toolbar/COMPONENT.md +142 -0
- package/src/components/page-toolbar/examples/examples.module.css +16 -0
- package/src/components/page-toolbar/examples/narrow.tsx +103 -0
- package/src/components/page-toolbar/examples/overview.tsx +113 -0
- package/src/components/page-toolbar/examples/save-changes.tsx +40 -0
- package/src/components/page-toolbar/examples/without-search.tsx +60 -0
- package/src/components/popover/COMPONENT.md +5 -4
- package/src/components/progress-bar/COMPONENT.md +6 -2
- package/src/components/progress-bar/examples/examples.module.css +7 -0
- package/src/components/progress-bar/examples/milestone.tsx +43 -0
- package/src/components/progress-bar/examples/steps.tsx +35 -0
- package/src/components/progress-circle/COMPONENT.md +1 -1
- package/src/components/scroll-container/COMPONENT.md +1 -1
- package/src/components/segmented-control/COMPONENT.md +8 -8
- package/src/components/segmented-control/examples/full-width.tsx +1 -1
- package/src/components/select/COMPONENT.md +2 -1
- package/src/components/skeleton/COMPONENT.md +1 -1
- package/src/components/skeleton/examples/with-crossfade.tsx +3 -3
- package/src/components/smart-filter/COMPONENT.md +2 -0
- package/src/components/sparkline/COMPONENT.md +98 -0
- package/src/components/sparkline/examples/examples.module.css +13 -0
- package/src/components/sparkline/examples/overview.tsx +27 -0
- package/src/components/sparkline/examples/period.tsx +46 -0
- package/src/components/spinner/COMPONENT.md +1 -1
- package/src/components/tabs/COMPONENT.md +24 -22
- package/src/components/tabs/examples/closable.tsx +1 -1
- package/src/components/tabs/examples/examples.module.css +14 -0
- package/src/components/tabs/examples/orientation.tsx +87 -33
- package/src/components/tooltip/COMPONENT.md +8 -6
- package/src/components/tooltip/examples/examples.module.css +2 -2
- package/src/layout/app-header/COMPONENT.md +178 -0
- package/src/layout/app-header/examples/breadcrumbs.tsx +36 -0
- package/src/layout/app-header/examples/examples.module.css +47 -0
- package/src/layout/app-header/examples/in-app-shell.tsx +70 -0
- package/src/layout/app-header/examples/narrow.tsx +29 -0
- package/src/layout/app-header/examples/overview.tsx +36 -0
- package/src/layout/app-shell/COMPONENT.md +35 -29
- package/src/layout/app-shell/examples/examples.module.css +17 -2
- package/src/layout/app-shell/examples/footer.tsx +59 -0
- package/src/layout/app-shell/examples/overview.tsx +19 -8
- package/src/layout/bottom-nav/COMPONENT.md +153 -0
- package/src/layout/bottom-nav/examples/examples.module.css +39 -0
- package/src/layout/bottom-nav/examples/floating.tsx +89 -0
- package/src/layout/bottom-nav/examples/icon-only.tsx +41 -0
- package/src/layout/bottom-nav/examples/in-app-shell.tsx +64 -0
- package/src/layout/bottom-nav/examples/overview.tsx +44 -0
- package/src/layout/bottom-nav/examples/router.tsx +31 -0
- package/src/layout/bottom-nav/examples/structure.tsx +47 -0
- package/src/layout/sidebar/COMPONENT.md +14 -13
- package/src/layout/sidebar/examples/examples.module.css +5 -5
- package/src/styles/globals.css +125 -2
- package/src/styles/theme-dark.css +39 -13
- package/src/styles/theme-light.css +84 -16
- 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
|
+
}
|