@ceebee/ui 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.css CHANGED
@@ -11,3 +11,284 @@
11
11
  font-size: var(--cb-text-sm);
12
12
  line-height: var(--cb-leading-normal);
13
13
  }
14
+
15
+ /* src/data/board/board.css */
16
+ .cb-board {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: var(--cb-space-3);
20
+ font-family: var(--cb-font-sans);
21
+ }
22
+ .cb-board__surface {
23
+ display: flex;
24
+ align-items: flex-start;
25
+ gap: var(--cb-space-3);
26
+ overflow-x: auto;
27
+ padding-bottom: var(--cb-space-2);
28
+ }
29
+ .cb-board__surface[data-lanes] {
30
+ overflow-x: visible;
31
+ }
32
+ .cb-board__column {
33
+ flex: 0 0 auto;
34
+ inline-size: 17rem;
35
+ max-inline-size: 100%;
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: var(--cb-space-2);
39
+ padding: var(--cb-space-3);
40
+ border: var(--cb-border-width) solid var(--cb-border);
41
+ border-radius: var(--cb-radius-lg);
42
+ background: var(--cb-bg-subtle);
43
+ }
44
+ .cb-board__surface[data-lanes] .cb-board__column {
45
+ flex: 1 1 auto;
46
+ inline-size: 100%;
47
+ }
48
+ .cb-board__column[data-over] {
49
+ border-color: var(--cb-tone-brand);
50
+ background: color-mix(in oklch, var(--cb-tone-brand) 8%, var(--cb-bg-subtle));
51
+ }
52
+ .cb-board__column[data-refuses] {
53
+ background: var(--cb-bg);
54
+ border-style: dashed;
55
+ }
56
+ .cb-board__head {
57
+ display: flex;
58
+ align-items: center;
59
+ justify-content: space-between;
60
+ gap: var(--cb-space-2);
61
+ }
62
+ .cb-board__name {
63
+ font-size: var(--cb-text-sm);
64
+ font-weight: 600;
65
+ color: var(--cb-fg);
66
+ }
67
+ .cb-board__count {
68
+ font-size: var(--cb-text-xs);
69
+ color: var(--cb-fg-muted);
70
+ padding: 0 var(--cb-space-2);
71
+ border-radius: var(--cb-radius-full);
72
+ background: var(--cb-bg);
73
+ border: var(--cb-border-width) solid var(--cb-border);
74
+ }
75
+ .cb-board__count[data-over-limit] {
76
+ color: var(--cb-tone-warning);
77
+ border-color: var(--cb-tone-warning);
78
+ }
79
+ .cb-board__over,
80
+ .cb-board__refusal {
81
+ margin: 0;
82
+ font-size: var(--cb-text-xs);
83
+ color: var(--cb-fg-muted);
84
+ }
85
+ .cb-board__over {
86
+ color: var(--cb-tone-warning);
87
+ }
88
+ .cb-board__list {
89
+ list-style: none;
90
+ margin: 0;
91
+ padding: 0;
92
+ display: flex;
93
+ flex-direction: column;
94
+ gap: var(--cb-space-2);
95
+ min-block-size: var(--cb-space-8);
96
+ }
97
+ .cb-board__card {
98
+ display: flex;
99
+ flex-direction: column;
100
+ gap: var(--cb-space-1);
101
+ padding: var(--cb-space-3);
102
+ border: var(--cb-border-width) solid var(--cb-border);
103
+ border-radius: var(--cb-radius-md);
104
+ background: var(--cb-bg);
105
+ box-shadow: var(--cb-shadow-sm);
106
+ cursor: grab;
107
+ transition: box-shadow var(--cb-duration-fast) var(--cb-ease-standard), transform var(--cb-duration-base) var(--cb-ease-standard);
108
+ }
109
+ .cb-board__card:hover {
110
+ box-shadow: var(--cb-shadow-md);
111
+ }
112
+ .cb-board__card:focus-visible {
113
+ outline: var(--cb-focus-width) solid var(--cb-tone-brand);
114
+ outline-offset: var(--cb-focus-offset);
115
+ }
116
+ .cb-board__card[data-held] {
117
+ box-shadow: var(--cb-shadow-lg);
118
+ border-color: var(--cb-tone-brand);
119
+ }
120
+ .cb-board__card[data-dragging] {
121
+ opacity: 0.4;
122
+ }
123
+ .cb-board__card[data-disabled] {
124
+ cursor: not-allowed;
125
+ color: var(--cb-fg-muted);
126
+ background: var(--cb-bg-subtle);
127
+ box-shadow: var(--cb-shadow-none);
128
+ }
129
+ .cb-board__card--lift {
130
+ cursor: grabbing;
131
+ box-shadow: var(--cb-shadow-lg);
132
+ transform: rotate(1.5deg);
133
+ }
134
+ .cb-board__title {
135
+ font-size: var(--cb-text-sm);
136
+ color: var(--cb-fg);
137
+ line-height: var(--cb-leading-tight);
138
+ display: -webkit-box;
139
+ -webkit-box-orient: vertical;
140
+ -webkit-line-clamp: 2;
141
+ overflow: hidden;
142
+ }
143
+ .cb-board__meta {
144
+ font-size: var(--cb-text-xs);
145
+ color: var(--cb-fg-muted);
146
+ }
147
+ .cb-board__marker {
148
+ block-size: var(--cb-space-1);
149
+ border-radius: var(--cb-radius-full);
150
+ background: var(--cb-tone-brand);
151
+ list-style: none;
152
+ }
153
+ .cb-board__empty {
154
+ padding: var(--cb-space-4) var(--cb-space-3);
155
+ border: var(--cb-border-width) dashed var(--cb-border-strong);
156
+ border-radius: var(--cb-radius-md);
157
+ text-align: center;
158
+ font-size: var(--cb-text-xs);
159
+ color: var(--cb-fg-muted);
160
+ }
161
+ .cb-board__lanes {
162
+ display: flex;
163
+ gap: var(--cb-space-1);
164
+ overflow-x: auto;
165
+ }
166
+ .cb-board__lane {
167
+ display: inline-flex;
168
+ align-items: center;
169
+ gap: var(--cb-space-2);
170
+ min-block-size: var(--cb-control-floor);
171
+ padding: 0 var(--cb-space-3);
172
+ border: var(--cb-border-width) solid var(--cb-border);
173
+ border-radius: var(--cb-radius-full);
174
+ background: var(--cb-bg);
175
+ color: var(--cb-fg-muted);
176
+ font: inherit;
177
+ font-size: var(--cb-text-sm);
178
+ cursor: pointer;
179
+ }
180
+ .cb-board__lane[aria-selected=true] {
181
+ background: color-mix(in oklch, var(--cb-tone-brand) 12%, var(--cb-bg));
182
+ border-color: var(--cb-tone-brand);
183
+ color: var(--cb-fg);
184
+ }
185
+ .cb-board__lane:focus-visible {
186
+ outline: var(--cb-focus-width) solid var(--cb-tone-brand);
187
+ outline-offset: var(--cb-focus-offset);
188
+ }
189
+ .cb-board__undo button {
190
+ min-block-size: var(--cb-control-floor);
191
+ padding: 0 var(--cb-space-3);
192
+ border: var(--cb-border-width) solid var(--cb-border);
193
+ border-radius: var(--cb-radius-md);
194
+ background: var(--cb-bg);
195
+ color: var(--cb-fg-link);
196
+ font: inherit;
197
+ font-size: var(--cb-text-sm);
198
+ cursor: pointer;
199
+ }
200
+ .cb-board__undo button:focus-visible {
201
+ outline: var(--cb-focus-width) solid var(--cb-tone-brand);
202
+ outline-offset: var(--cb-focus-offset);
203
+ }
204
+ .cb-board__live {
205
+ position: absolute;
206
+ inline-size: 1px;
207
+ block-size: 1px;
208
+ margin: -1px;
209
+ padding: 0;
210
+ overflow: hidden;
211
+ clip-path: inset(50%);
212
+ white-space: nowrap;
213
+ }
214
+ .cb-board__ghost,
215
+ .cb-board__ghost-card {
216
+ border-radius: var(--cb-radius-md);
217
+ background: color-mix(in oklch, var(--cb-fg-subtle) 18%, transparent);
218
+ }
219
+ .cb-board__ghost--name {
220
+ inline-size: var(--cb-space-8);
221
+ block-size: var(--cb-space-3);
222
+ }
223
+ .cb-board__ghost--count {
224
+ inline-size: var(--cb-space-4);
225
+ block-size: var(--cb-space-3);
226
+ }
227
+ .cb-board__ghost-card {
228
+ block-size: var(--cb-space-8);
229
+ border: 0;
230
+ box-shadow: var(--cb-shadow-none);
231
+ }
232
+ @media (prefers-reduced-motion: reduce) {
233
+ .cb-board__card {
234
+ transition: none;
235
+ }
236
+ .cb-board__card--lift {
237
+ transform: none;
238
+ }
239
+ }
240
+ .cb-board[data-motion=off] .cb-board__card {
241
+ transition: none;
242
+ }
243
+ .cb-board[data-motion=off] .cb-board__card--lift {
244
+ transform: none;
245
+ }
246
+ @media (forced-colors: active) {
247
+ .cb-board__column,
248
+ .cb-board__card {
249
+ border-color: CanvasText;
250
+ }
251
+ .cb-board__marker {
252
+ background: Highlight;
253
+ }
254
+ .cb-board__card[data-held] {
255
+ outline: var(--cb-focus-width) solid Highlight;
256
+ }
257
+ }
258
+ .cb-board__card[data-handle] {
259
+ cursor: default;
260
+ display: grid;
261
+ grid-template-columns: auto 1fr;
262
+ grid-template-areas: "handle title" ". meta";
263
+ column-gap: var(--cb-space-2);
264
+ align-items: start;
265
+ }
266
+ .cb-board__card[data-handle] .cb-board__title {
267
+ grid-area: title;
268
+ }
269
+ .cb-board__card[data-handle] .cb-board__meta {
270
+ grid-area: meta;
271
+ }
272
+ .cb-board__handle {
273
+ grid-area: handle;
274
+ display: inline-flex;
275
+ align-items: center;
276
+ justify-content: center;
277
+ min-inline-size: var(--cb-space-5);
278
+ min-block-size: var(--cb-space-5);
279
+ padding: 0;
280
+ border: 0;
281
+ border-radius: var(--cb-radius-sm);
282
+ background: transparent;
283
+ color: var(--cb-fg-subtle);
284
+ font: inherit;
285
+ line-height: var(--cb-leading-tight);
286
+ cursor: grab;
287
+ }
288
+ .cb-board__handle:focus-visible {
289
+ outline: var(--cb-focus-width) solid var(--cb-tone-brand);
290
+ outline-offset: var(--cb-focus-offset);
291
+ }
292
+ .cb-board__handle:disabled {
293
+ cursor: not-allowed;
294
+ }
package/dist/client.d.ts CHANGED
@@ -377,6 +377,122 @@ declare const PanZoomCanvas: typeof PanZoomCanvasRoot & {
377
377
  Skeleton: typeof PanZoomCanvasSkeleton;
378
378
  };
379
379
 
380
+ interface BoardSkeletonProps {
381
+ /** How many columns to stand in for — the same shape the real board will draw. */
382
+ columns?: number;
383
+ /** Cards per column. */
384
+ cards?: number;
385
+ label?: string;
386
+ }
387
+ /**
388
+ * The board's placeholder, built from the same geometry as the board itself, so the page does not
389
+ * reflow when the real columns arrive.
390
+ */
391
+ declare function BoardSkeleton({ columns, cards, label }: BoardSkeletonProps): react.JSX.Element;
392
+
393
+ /** One card on the board. `disabled` refuses movement without hiding the card. */
394
+ interface BoardCard {
395
+ id: string;
396
+ title: ReactNode;
397
+ /**
398
+ * The card's name in an announcement and on the drag handle, when `title` is a node rather than a
399
+ * string. Without it a node-titled card is announced by its id, which is a uuid read aloud.
400
+ */
401
+ label?: string;
402
+ /** Whatever a person triages by — an owner, an age, a due date, a blocked marker. */
403
+ meta?: ReactNode;
404
+ /** A card that cannot move: it stays visible, is not a drag source, and is skipped by the keyboard path. */
405
+ disabled?: boolean;
406
+ /** Why it cannot move, announced when a move is attempted on it. */
407
+ disabledReason?: string;
408
+ }
409
+ /** One column: a state of the workflow, not a category of thing. */
410
+ interface BoardColumn {
411
+ id: string;
412
+ name: ReactNode;
413
+ /**
414
+ * The column's accessible name, when `name` is a node rather than a string — a header carrying
415
+ * status tags still has to be nameable. Without it a node-named column has no accessible name.
416
+ */
417
+ label?: string;
418
+ cards: BoardCard[];
419
+ /** A work-in-progress limit, shown beside the count; exceeding it marks the column, never blocks a move. */
420
+ limit?: number;
421
+ /** Shown when the column holds no cards. An empty column must still invite one. */
422
+ empty?: ReactNode;
423
+ /** A column that takes no cards — an end state, say. Dropping onto it is refused with `refusal`. */
424
+ accepts?: boolean;
425
+ /** Why this column refuses a card, announced and shown on an attempted drop. */
426
+ refusal?: string;
427
+ }
428
+ /** Where a card is, or is going. `index` is its position within the column, 0-based. */
429
+ interface BoardPosition {
430
+ columnId: string;
431
+ index: number;
432
+ }
433
+ /** A move the consumer is asked to validate and apply. */
434
+ interface BoardMove {
435
+ cardId: string;
436
+ from: BoardPosition;
437
+ to: BoardPosition;
438
+ }
439
+ /**
440
+ * What the consumer answers with. Resolving accepts the move; rejecting — or resolving with a
441
+ * `refused` reason — rolls the board back and says why.
442
+ */
443
+ type BoardMoveResult = void | {
444
+ refused: string;
445
+ };
446
+ /** The shape the pure logic needs: ids and order, no React. */
447
+ interface BoardShape {
448
+ id: string;
449
+ cards: {
450
+ id: string;
451
+ disabled?: boolean;
452
+ }[];
453
+ accepts?: boolean;
454
+ }
455
+
456
+ interface BoardLabels {
457
+ pickUp: string;
458
+ dropped: (card: string, column: string) => string;
459
+ cancelled: string;
460
+ refused: (reason: string) => string;
461
+ undo: string;
462
+ /** The drag handle's accessible name, when cards carry their own actions. */
463
+ move: (card: string) => string;
464
+ undone: string;
465
+ lanes: string;
466
+ over: (count: number, limit: number) => string;
467
+ }
468
+ interface BoardProps {
469
+ columns: BoardColumn[];
470
+ /**
471
+ * The one path every move takes, from the pointer and from the keyboard alike. Return nothing to
472
+ * accept it; return `{ refused }` or reject to roll the board back and say why.
473
+ */
474
+ onMove: (move: BoardMove) => BoardMoveResult | Promise<BoardMoveResult>;
475
+ /** `auto` switches to lanes on a narrow viewport; force either to prove one in a test. */
476
+ layout?: 'auto' | 'board' | 'lanes';
477
+ /** What `auto` calls narrow. */
478
+ phoneQuery?: string;
479
+ labels?: Partial<BoardLabels>;
480
+ motion?: boolean;
481
+ /**
482
+ * Put the grab on a handle instead of the whole card. Use it whenever a card carries its own
483
+ * buttons: a card that is itself a control has **presentational children**, so anything
484
+ * interactive inside it disappears from the accessibility tree. With a handle the card is plain
485
+ * markup, its buttons stay reachable, and the handle is the one thing that drags and takes the
486
+ * keyboard.
487
+ */
488
+ handle?: boolean;
489
+ 'aria-label'?: string;
490
+ }
491
+ declare function BoardRoot({ columns, onMove, layout, phoneQuery, labels, motion, handle, 'aria-label': ariaLabel, }: BoardProps): react.JSX.Element;
492
+ declare const Board: typeof BoardRoot & {
493
+ Skeleton: typeof BoardSkeleton;
494
+ };
495
+
380
496
  interface DiagramSkeletonProps {
381
497
  label?: string;
382
498
  }
@@ -467,4 +583,4 @@ declare const DiagramEditor: typeof DiagramEditorRoot & {
467
583
  Skeleton: typeof DiagramSkeleton;
468
584
  };
469
585
 
470
- export { type CeebeeAntStyleCache, CeebeeAntStyleProvider, type CeebeeAntStyleProviderProps, Checklist, type ChecklistProps, type ChecklistTask, type Command, CommandPalette, type CommandPaletteProps, DEFAULT_LABELS, Diagram, type DiagramEdge, DiagramEditor, type DiagramEditorProps, type DiagramNode, type DiagramPosition, type DiagramProps, type DiagramRemoval, type DiagramRenameTarget, type DiagramShape, type DiagramSkeletonProps, type DurationToken, type Labels, LabelsProvider, type LabelsProviderProps, Modal, type ModalProps, type MotionHelpers, MotionProvider, type MotionProviderProps, type MotionSettings, type NavItem, type NavSection, type PaletteCommand, PanZoomCanvas, type PanZoomCanvasProps, type PanZoomCanvasSkeletonProps, type RankedCommand, Reveal, type RevealProps, Sidebar, type SidebarProps, type SpringPreset, Stagger, type StaggerProps, StickerGroup, type StickerGroupProps, type StickerGroupSkeletonProps, type StickerItem, ThemeBridge, type ThemeBridgeProps, type ThemeChoice, ThemeProvider, type ThemeProviderProps, type ToastOptions, type ToastPosition, ToastProvider, type ToastProviderProps, TopBar, type TopBarProps, createCeebeeAntStyleCache, extractCeebeeAntStyles, filterCommands, groupCommands, rankCommand, useLabels, useMotionSettings, useTheme, useToast };
586
+ export { Board, type BoardCard, type BoardColumn, type BoardLabels, type BoardMove, type BoardMoveResult, type BoardPosition, type BoardProps, type BoardShape, type BoardSkeletonProps, type CeebeeAntStyleCache, CeebeeAntStyleProvider, type CeebeeAntStyleProviderProps, Checklist, type ChecklistProps, type ChecklistTask, type Command, CommandPalette, type CommandPaletteProps, DEFAULT_LABELS, Diagram, type DiagramEdge, DiagramEditor, type DiagramEditorProps, type DiagramNode, type DiagramPosition, type DiagramProps, type DiagramRemoval, type DiagramRenameTarget, type DiagramShape, type DiagramSkeletonProps, type DurationToken, type Labels, LabelsProvider, type LabelsProviderProps, Modal, type ModalProps, type MotionHelpers, MotionProvider, type MotionProviderProps, type MotionSettings, type NavItem, type NavSection, type PaletteCommand, PanZoomCanvas, type PanZoomCanvasProps, type PanZoomCanvasSkeletonProps, type RankedCommand, Reveal, type RevealProps, Sidebar, type SidebarProps, type SpringPreset, Stagger, type StaggerProps, StickerGroup, type StickerGroupProps, type StickerGroupSkeletonProps, type StickerItem, ThemeBridge, type ThemeBridgeProps, type ThemeChoice, ThemeProvider, type ThemeProviderProps, type ToastOptions, type ToastPosition, ToastProvider, type ToastProviderProps, TopBar, type TopBarProps, createCeebeeAntStyleCache, extractCeebeeAntStyles, filterCommands, groupCommands, rankCommand, useLabels, useMotionSettings, useTheme, useToast };
package/dist/client.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { Modal as Modal$1, Button, theme, ConfigProvider, Progress } from 'antd';
3
3
  export * from 'antd';
4
+ import * as React from 'react';
4
5
  import { createContext, useId, useRef, useCallback, useInsertionEffect, useLayoutEffect, useState, useEffect, useMemo, useContext, Children } from 'react';
5
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
7
  import { StyleProvider, createCache, extractStyle } from '@ant-design/cssinjs';
@@ -13,6 +14,9 @@ import { X, Minus, Plus, RotateCcw, Minimize2, Maximize2, Search, Check, Chevron
13
14
  import { Toast } from '@base-ui/react/toast';
14
15
  import { Popover } from '@base-ui/react/popover';
15
16
  import { AnimatePresence, motion } from 'motion/react';
17
+ import { useSensors, useSensor, PointerSensor, DndContext, closestCorners, DragOverlay, useDroppable } from '@dnd-kit/core';
18
+ import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
19
+ import { CSS } from '@dnd-kit/utilities';
16
20
  import { ReactFlow, ConnectionMode, Background, Controls, applyNodeChanges, MarkerType, Handle, Position } from '@xyflow/react';
17
21
 
18
22
  // src/lib/cn.ts
@@ -1673,6 +1677,364 @@ function PanZoomCanvasRoot({
1673
1677
  ] });
1674
1678
  }
1675
1679
  var PanZoomCanvas = Object.assign(PanZoomCanvasRoot, { Skeleton: PanZoomCanvasSkeleton });
1680
+ function BoardSkeleton({ columns = 3, cards = 3, label = "Loading board" }) {
1681
+ return /* @__PURE__ */ jsx("div", { className: "cb-board", "aria-busy": "true", "aria-label": label, children: /* @__PURE__ */ jsx("div", { className: "cb-board__surface", children: Array.from({ length: columns }, (_, column) => /* @__PURE__ */ jsxs("section", { className: "cb-board__column", children: [
1682
+ /* @__PURE__ */ jsxs("header", { className: "cb-board__head", children: [
1683
+ /* @__PURE__ */ jsx("span", { className: "cb-board__ghost cb-board__ghost cb-board__ghost--name" }),
1684
+ /* @__PURE__ */ jsx("span", { className: "cb-board__ghost cb-board__ghost cb-board__ghost--count" })
1685
+ ] }),
1686
+ /* @__PURE__ */ jsx("ol", { className: "cb-board__list", children: Array.from({ length: cards }, (_2, card) => /* @__PURE__ */ jsx("li", { className: "cb-board__card cb-board__ghost-card" }, card)) })
1687
+ ] }, column)) }) });
1688
+ }
1689
+
1690
+ // src/data/board/board.math.ts
1691
+ function locate(columns, cardId) {
1692
+ for (const column of columns) {
1693
+ const index = column.cards.findIndex((card) => card.id === cardId);
1694
+ if (index !== -1) return { columnId: column.id, index };
1695
+ }
1696
+ return null;
1697
+ }
1698
+ var columnOf = (columns, columnId) => columns.find((c) => c.id === columnId);
1699
+ function canPickUp(columns, cardId) {
1700
+ const at = locate(columns, cardId);
1701
+ if (!at) return false;
1702
+ return !columnOf(columns, at.columnId)?.cards.find((c) => c.id === cardId)?.disabled;
1703
+ }
1704
+ function refusalFor(columns, move) {
1705
+ if (!canPickUp(columns, move.cardId)) return "this card cannot be moved";
1706
+ const target = columnOf(columns, move.to.columnId);
1707
+ if (!target) return "that column is not on the board";
1708
+ if (target.accepts === false) return "that column does not take cards";
1709
+ return null;
1710
+ }
1711
+ function isNoop(move) {
1712
+ return move.from.columnId === move.to.columnId && move.from.index === move.to.index;
1713
+ }
1714
+ function applyMove(columns, move) {
1715
+ const card = columnOf(columns, move.from.columnId)?.cards[move.from.index];
1716
+ if (!card || card.id !== move.cardId) return [...columns];
1717
+ return columns.map((column) => {
1718
+ if (column.id === move.from.columnId && column.id === move.to.columnId) {
1719
+ const rest = column.cards.filter((_, i) => i !== move.from.index);
1720
+ rest.splice(clamp(move.to.index, rest.length), 0, card);
1721
+ return { ...column, cards: rest };
1722
+ }
1723
+ if (column.id === move.from.columnId) {
1724
+ return { ...column, cards: column.cards.filter((_, i) => i !== move.from.index) };
1725
+ }
1726
+ if (column.id === move.to.columnId) {
1727
+ const next = [...column.cards];
1728
+ next.splice(clamp(move.to.index, next.length), 0, card);
1729
+ return { ...column, cards: next };
1730
+ }
1731
+ return column;
1732
+ });
1733
+ }
1734
+ var clamp = (value, max) => Math.max(0, Math.min(value, max));
1735
+ function nextTarget(columns, held, direction) {
1736
+ const at = columns.findIndex((c) => c.id === held.columnId);
1737
+ const current = columns[at];
1738
+ if (!current) return null;
1739
+ if (direction === "up" || direction === "down") {
1740
+ const size = current.cards.length;
1741
+ const index = held.index + (direction === "down" ? 1 : -1);
1742
+ return index < 0 || index > size - 1 ? null : { columnId: held.columnId, index };
1743
+ }
1744
+ const step = direction === "right" ? 1 : -1;
1745
+ for (let i = at + step; i >= 0 && i < columns.length; i += step) {
1746
+ const column = columns[i];
1747
+ if (!column || column.accepts === false) continue;
1748
+ return { columnId: column.id, index: clamp(held.index, column.cards.length) };
1749
+ }
1750
+ return null;
1751
+ }
1752
+ function columnLoad(column) {
1753
+ const count = column.cards.length;
1754
+ return { count, over: typeof column.limit === "number" && count > column.limit };
1755
+ }
1756
+ var DEFAULTS = {
1757
+ pickUp: "Picked up. Use the arrow keys to move it, Space to drop, Escape to cancel.",
1758
+ dropped: (card, column) => `${card} moved to ${column}.`,
1759
+ cancelled: "Move cancelled.",
1760
+ refused: (reason) => `Move refused: ${reason}`,
1761
+ undo: "Undo",
1762
+ move: (card) => `Move ${card}`,
1763
+ undone: "Move undone.",
1764
+ lanes: "Column",
1765
+ over: (count, limit) => `${count} of ${limit}, over the limit`
1766
+ };
1767
+ var textOf = (node, fallback) => typeof node === "string" ? node : fallback;
1768
+ function BoardRoot({
1769
+ columns,
1770
+ onMove,
1771
+ layout = "auto",
1772
+ phoneQuery = "(max-width: 640px)",
1773
+ labels,
1774
+ motion: motion3 = true,
1775
+ handle = false,
1776
+ "aria-label": ariaLabel = "Board"
1777
+ }) {
1778
+ const text = { ...DEFAULTS, ...labels };
1779
+ const [optimistic, setOptimistic] = React.useState(null);
1780
+ const [held, setHeld] = React.useState(null);
1781
+ const [dragging, setDragging] = React.useState(null);
1782
+ const [announcement, setAnnouncement] = React.useState("");
1783
+ const [undoable, setUndoable] = React.useState(null);
1784
+ const [lane, setLane] = React.useState(0);
1785
+ const view = optimistic ?? columns;
1786
+ React.useEffect(() => setOptimistic(null), [columns]);
1787
+ const narrow = useNarrow(layout === "auto" ? phoneQuery : null);
1788
+ const lanes = layout === "lanes" || layout === "auto" && narrow;
1789
+ const nameOf = (columnId) => {
1790
+ const column = view.find((c) => c.id === columnId);
1791
+ return column?.label ?? textOf(column?.name, columnId);
1792
+ };
1793
+ const commit = React.useCallback(
1794
+ async (move, announceAs) => {
1795
+ if (isNoop(move)) return;
1796
+ const refusal = refusalFor(view, move);
1797
+ if (refusal) {
1798
+ setAnnouncement(text.refused(refusal));
1799
+ return;
1800
+ }
1801
+ const before = view;
1802
+ setOptimistic(applyMove(view, move));
1803
+ setAnnouncement(announceAs ?? text.dropped(titleOf(view, move.cardId), nameOf(move.to.columnId)));
1804
+ try {
1805
+ const result = await onMove(move);
1806
+ if (result && "refused" in result) {
1807
+ setOptimistic(before);
1808
+ setAnnouncement(text.refused(result.refused));
1809
+ return;
1810
+ }
1811
+ setUndoable({ cardId: move.cardId, from: move.to, to: move.from });
1812
+ } catch (e) {
1813
+ setOptimistic(before);
1814
+ setAnnouncement(text.refused(e instanceof Error ? e.message : String(e)));
1815
+ }
1816
+ },
1817
+ [view, onMove, text]
1818
+ );
1819
+ const onCardKeyDown = (event, cardId) => {
1820
+ const DIRECTIONS = {
1821
+ ArrowLeft: "left",
1822
+ ArrowRight: "right",
1823
+ ArrowUp: "up",
1824
+ ArrowDown: "down"
1825
+ };
1826
+ if (event.key === "Escape" && held) {
1827
+ event.preventDefault();
1828
+ setHeld(null);
1829
+ setAnnouncement(text.cancelled);
1830
+ return;
1831
+ }
1832
+ if (event.key === " " || event.key === "Enter") {
1833
+ event.preventDefault();
1834
+ if (!held) {
1835
+ if (!canPickUp(view, cardId)) {
1836
+ const card = view.flatMap((c) => c.cards).find((c) => c.id === cardId);
1837
+ setAnnouncement(text.refused(card?.disabledReason ?? "this card cannot be moved"));
1838
+ return;
1839
+ }
1840
+ const at = locate(view, cardId);
1841
+ if (at) {
1842
+ setHeld({ cardId, at });
1843
+ setAnnouncement(text.pickUp);
1844
+ }
1845
+ return;
1846
+ }
1847
+ const from = locate(view, held.cardId);
1848
+ if (from) void commit({ cardId: held.cardId, from, to: held.at });
1849
+ setHeld(null);
1850
+ return;
1851
+ }
1852
+ const direction = DIRECTIONS[event.key];
1853
+ if (!direction || !held) return;
1854
+ event.preventDefault();
1855
+ const target = nextTarget(view, held.at, direction);
1856
+ if (!target) return;
1857
+ setHeld({ ...held, at: target });
1858
+ setAnnouncement(`${nameOf(target.columnId)}, ${target.index + 1}`);
1859
+ };
1860
+ const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
1861
+ const onDragEnd = (event) => {
1862
+ setDragging(null);
1863
+ const cardId = String(event.active.id);
1864
+ const over = event.over ? String(event.over.id) : null;
1865
+ if (!over) return;
1866
+ const from = locate(view, cardId);
1867
+ if (!from) return;
1868
+ const onColumn = view.find((c) => c.id === over);
1869
+ const to = onColumn ? { columnId: onColumn.id, index: onColumn.cards.length } : locate(view, over);
1870
+ if (to) void commit({ cardId, from, to });
1871
+ };
1872
+ const laneColumn = view[Math.min(lane, Math.max(view.length - 1, 0))];
1873
+ const shown = lanes ? laneColumn ? [laneColumn] : [] : view;
1874
+ return /* @__PURE__ */ jsxs("div", { className: "cb-board", "data-motion": motion3 ? void 0 : "off", children: [
1875
+ lanes ? /* @__PURE__ */ jsx("div", { className: "cb-board__lanes", role: "tablist", "aria-label": text.lanes, children: view.map((column, i) => /* @__PURE__ */ jsxs(
1876
+ "button",
1877
+ {
1878
+ type: "button",
1879
+ role: "tab",
1880
+ "aria-selected": i === lane,
1881
+ className: "cb-board__lane",
1882
+ onClick: () => setLane(i),
1883
+ children: [
1884
+ column.name,
1885
+ " ",
1886
+ /* @__PURE__ */ jsx("span", { className: "cb-board__count", children: column.cards.length })
1887
+ ]
1888
+ },
1889
+ column.id
1890
+ )) }) : null,
1891
+ /* @__PURE__ */ jsxs(
1892
+ DndContext,
1893
+ {
1894
+ sensors,
1895
+ collisionDetection: closestCorners,
1896
+ onDragStart: (e) => setDragging(String(e.active.id)),
1897
+ onDragCancel: () => setDragging(null),
1898
+ onDragEnd,
1899
+ children: [
1900
+ /* @__PURE__ */ jsx("div", { className: "cb-board__surface", role: "group", "aria-label": ariaLabel, "data-lanes": lanes ? "" : void 0, children: shown.map((column) => /* @__PURE__ */ jsx(Column, { column, held, onCardKeyDown, labels: text, handle }, column.id)) }),
1901
+ /* @__PURE__ */ jsx(DragOverlay, { children: dragging ? /* @__PURE__ */ jsx("div", { className: "cb-board__card cb-board__card--lift", children: cardTitle(view, dragging) }) : null })
1902
+ ]
1903
+ }
1904
+ ),
1905
+ undoable ? /* @__PURE__ */ jsx("div", { className: "cb-board__undo", children: /* @__PURE__ */ jsx(
1906
+ "button",
1907
+ {
1908
+ type: "button",
1909
+ onClick: () => {
1910
+ const move = undoable;
1911
+ setUndoable(null);
1912
+ void commit(move, text.undone);
1913
+ },
1914
+ children: text.undo
1915
+ }
1916
+ ) }) : null,
1917
+ /* @__PURE__ */ jsx("div", { className: "cb-board__live", role: "status", "aria-live": "polite", children: announcement })
1918
+ ] });
1919
+ }
1920
+ var cardTitle = (columns, cardId) => columns.flatMap((c) => c.cards).find((c) => c.id === cardId)?.title ?? null;
1921
+ var titleOf = (columns, cardId) => {
1922
+ const card = columns.flatMap((c) => c.cards).find((c) => c.id === cardId);
1923
+ return card?.label ?? textOf(card?.title, cardId);
1924
+ };
1925
+ function Column({
1926
+ column,
1927
+ held,
1928
+ onCardKeyDown,
1929
+ labels,
1930
+ handle
1931
+ }) {
1932
+ const { setNodeRef, isOver } = useDroppable({ id: column.id });
1933
+ const load = columnLoad(column);
1934
+ const holding = held?.at.columnId === column.id;
1935
+ return /* @__PURE__ */ jsxs(
1936
+ "section",
1937
+ {
1938
+ ref: setNodeRef,
1939
+ className: "cb-board__column",
1940
+ "data-over": isOver ? "" : void 0,
1941
+ "data-refuses": column.accepts === false ? "" : void 0,
1942
+ "aria-label": column.label ?? (typeof column.name === "string" ? column.name : void 0),
1943
+ children: [
1944
+ /* @__PURE__ */ jsxs("header", { className: "cb-board__head", children: [
1945
+ /* @__PURE__ */ jsx("span", { className: "cb-board__name", children: column.name }),
1946
+ /* @__PURE__ */ jsx("span", { className: "cb-board__count", "data-over-limit": load.over ? "" : void 0, children: typeof column.limit === "number" ? `${load.count}/${column.limit}` : load.count })
1947
+ ] }),
1948
+ /* @__PURE__ */ jsx(SortableContext, { items: column.cards.map((c) => c.id), strategy: verticalListSortingStrategy, children: /* @__PURE__ */ jsxs("ol", { className: "cb-board__list", children: [
1949
+ column.cards.map((card, index) => /* @__PURE__ */ jsx(
1950
+ Card,
1951
+ {
1952
+ card,
1953
+ held: held?.cardId === card.id,
1954
+ marker: holding && held?.at.index === index,
1955
+ onKeyDown: onCardKeyDown,
1956
+ handle,
1957
+ labels
1958
+ },
1959
+ card.id
1960
+ )),
1961
+ holding && held.at.index >= column.cards.length ? /* @__PURE__ */ jsx("li", { className: "cb-board__marker", "aria-hidden": true }) : null
1962
+ ] }) }),
1963
+ column.cards.length === 0 ? /* @__PURE__ */ jsx("div", { className: "cb-board__empty", children: column.empty ?? null }) : null,
1964
+ column.accepts === false && column.refusal ? /* @__PURE__ */ jsx("p", { className: "cb-board__refusal", children: column.refusal }) : null,
1965
+ load.over && typeof column.limit === "number" ? /* @__PURE__ */ jsx("p", { className: "cb-board__over", children: labels.over(load.count, column.limit) }) : null
1966
+ ]
1967
+ }
1968
+ );
1969
+ }
1970
+ function Card({
1971
+ card,
1972
+ held,
1973
+ marker,
1974
+ onKeyDown,
1975
+ handle,
1976
+ labels
1977
+ }) {
1978
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
1979
+ id: card.id,
1980
+ disabled: card.disabled
1981
+ });
1982
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1983
+ marker ? /* @__PURE__ */ jsx("li", { className: "cb-board__marker", "aria-hidden": true }) : null,
1984
+ /* @__PURE__ */ jsxs(
1985
+ "li",
1986
+ {
1987
+ ref: setNodeRef,
1988
+ style: { transform: CSS.Transform.toString(transform), transition },
1989
+ className: "cb-board__card",
1990
+ "data-dragging": isDragging ? "" : void 0,
1991
+ "data-held": held ? "" : void 0,
1992
+ "data-disabled": card.disabled ? "" : void 0,
1993
+ "data-handle": handle ? "" : void 0,
1994
+ ...handle ? {} : {
1995
+ ...attributes,
1996
+ ...card.disabled ? {} : listeners,
1997
+ tabIndex: 0,
1998
+ "aria-roledescription": "draggable card",
1999
+ "aria-disabled": card.disabled || void 0,
2000
+ onKeyDown: (event) => onKeyDown(event, card.id)
2001
+ },
2002
+ children: [
2003
+ handle ? /* @__PURE__ */ jsx(
2004
+ "button",
2005
+ {
2006
+ type: "button",
2007
+ className: "cb-board__handle",
2008
+ ...attributes,
2009
+ ...card.disabled ? {} : listeners,
2010
+ "aria-label": labels.move(card.label ?? (typeof card.title === "string" ? card.title : card.id)),
2011
+ "aria-roledescription": "drag handle",
2012
+ "aria-disabled": card.disabled || void 0,
2013
+ disabled: card.disabled,
2014
+ onKeyDown: (event) => onKeyDown(event, card.id),
2015
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u283F" })
2016
+ }
2017
+ ) : null,
2018
+ /* @__PURE__ */ jsx("div", { className: "cb-board__title", children: card.title }),
2019
+ card.meta ? /* @__PURE__ */ jsx("div", { className: "cb-board__meta", children: card.meta }) : null
2020
+ ]
2021
+ }
2022
+ )
2023
+ ] });
2024
+ }
2025
+ function useNarrow(query) {
2026
+ const [narrow, setNarrow] = React.useState(false);
2027
+ React.useEffect(() => {
2028
+ if (!query || typeof window === "undefined" || !window.matchMedia) return;
2029
+ const mql = window.matchMedia(query);
2030
+ const read = () => setNarrow(mql.matches);
2031
+ read();
2032
+ mql.addEventListener("change", read);
2033
+ return () => mql.removeEventListener("change", read);
2034
+ }, [query]);
2035
+ return narrow;
2036
+ }
2037
+ var Board = Object.assign(BoardRoot, { Skeleton: BoardSkeleton });
1676
2038
  function DiagramNodeView({ data, selected }) {
1677
2039
  return /* @__PURE__ */ jsxs(
1678
2040
  "div",
@@ -1980,4 +2342,4 @@ function DiagramEditorRoot(props) {
1980
2342
  }
1981
2343
  var DiagramEditor = Object.assign(DiagramEditorRoot, { Skeleton: DiagramSkeleton });
1982
2344
 
1983
- export { CeebeeAntStyleProvider, Checklist, CommandPalette, DEFAULT_LABELS, Diagram, DiagramEditor, LabelsProvider, Modal, MotionProvider, PanZoomCanvas, Reveal, Sidebar, Stagger, StickerGroup, ThemeBridge, ThemeProvider, ToastProvider, TopBar, createCeebeeAntStyleCache, extractCeebeeAntStyles, filterCommands, groupCommands, rankCommand, useLabels, useMotionSettings, useTheme, useToast };
2345
+ export { Board, CeebeeAntStyleProvider, Checklist, CommandPalette, DEFAULT_LABELS, Diagram, DiagramEditor, LabelsProvider, Modal, MotionProvider, PanZoomCanvas, Reveal, Sidebar, Stagger, StickerGroup, ThemeBridge, ThemeProvider, ToastProvider, TopBar, createCeebeeAntStyleCache, extractCeebeeAntStyles, filterCommands, groupCommands, rankCommand, useLabels, useMotionSettings, useTheme, useToast };
package/dist/styles.css CHANGED
@@ -1360,6 +1360,326 @@ svg.react-flow__connectionline {
1360
1360
  line-height: var(--cb-leading-normal);
1361
1361
  }
1362
1362
 
1363
+ /* data/board/board.css */
1364
+ /* Board — columns of cards a person moves by pointer or by keyboard.
1365
+ Every value is a Token. The drag lift, the gap animation and the drop marker all have a
1366
+ reduced-motion rendering: transform drops, the marker stays, so a move is never invisible. */
1367
+
1368
+ .cb-board {
1369
+ display: flex;
1370
+ flex-direction: column;
1371
+ gap: var(--cb-space-3);
1372
+ font-family: var(--cb-font-sans);
1373
+ }
1374
+
1375
+ .cb-board__surface {
1376
+ display: flex;
1377
+ align-items: flex-start;
1378
+ gap: var(--cb-space-3);
1379
+ overflow-x: auto;
1380
+ padding-bottom: var(--cb-space-2);
1381
+ }
1382
+
1383
+ .cb-board__surface[data-lanes] {
1384
+ overflow-x: visible;
1385
+ }
1386
+
1387
+ /* ── column ── */
1388
+
1389
+ .cb-board__column {
1390
+ flex: 0 0 auto;
1391
+ inline-size: 17rem;
1392
+ max-inline-size: 100%;
1393
+ display: flex;
1394
+ flex-direction: column;
1395
+ gap: var(--cb-space-2);
1396
+ padding: var(--cb-space-3);
1397
+ border: var(--cb-border-width) solid var(--cb-border);
1398
+ border-radius: var(--cb-radius-lg);
1399
+ background: var(--cb-bg-subtle);
1400
+ }
1401
+
1402
+ .cb-board__surface[data-lanes] .cb-board__column {
1403
+ flex: 1 1 auto;
1404
+ inline-size: 100%;
1405
+ }
1406
+
1407
+ /* The column a pointer is over: named by a ring rather than a fill, so a tinted card keeps its tone. */
1408
+ .cb-board__column[data-over] {
1409
+ border-color: var(--cb-tone-brand);
1410
+ background: color-mix(in oklch, var(--cb-tone-brand) 8%, var(--cb-bg-subtle));
1411
+ }
1412
+
1413
+ /* A column that takes no cards says so before a drop is attempted. */
1414
+ .cb-board__column[data-refuses] {
1415
+ background: var(--cb-bg);
1416
+ border-style: dashed;
1417
+ }
1418
+
1419
+ .cb-board__head {
1420
+ display: flex;
1421
+ align-items: center;
1422
+ justify-content: space-between;
1423
+ gap: var(--cb-space-2);
1424
+ }
1425
+
1426
+ .cb-board__name {
1427
+ font-size: var(--cb-text-sm);
1428
+ font-weight: 600;
1429
+ color: var(--cb-fg);
1430
+ }
1431
+
1432
+ .cb-board__count {
1433
+ font-size: var(--cb-text-xs);
1434
+ color: var(--cb-fg-muted);
1435
+ padding: 0 var(--cb-space-2);
1436
+ border-radius: var(--cb-radius-full);
1437
+ background: var(--cb-bg);
1438
+ border: var(--cb-border-width) solid var(--cb-border);
1439
+ }
1440
+
1441
+ /* Over a WIP limit is marked, never blocked — the board reports, the consumer decides. */
1442
+ .cb-board__count[data-over-limit] {
1443
+ color: var(--cb-tone-warning);
1444
+ border-color: var(--cb-tone-warning);
1445
+ }
1446
+
1447
+ .cb-board__over,
1448
+ .cb-board__refusal {
1449
+ margin: 0;
1450
+ font-size: var(--cb-text-xs);
1451
+ color: var(--cb-fg-muted);
1452
+ }
1453
+
1454
+ .cb-board__over { color: var(--cb-tone-warning); }
1455
+
1456
+ /* ── cards ── */
1457
+
1458
+ .cb-board__list {
1459
+ list-style: none;
1460
+ margin: 0;
1461
+ padding: 0;
1462
+ display: flex;
1463
+ flex-direction: column;
1464
+ gap: var(--cb-space-2);
1465
+ min-block-size: var(--cb-space-8);
1466
+ }
1467
+
1468
+ .cb-board__card {
1469
+ display: flex;
1470
+ flex-direction: column;
1471
+ gap: var(--cb-space-1);
1472
+ padding: var(--cb-space-3);
1473
+ border: var(--cb-border-width) solid var(--cb-border);
1474
+ border-radius: var(--cb-radius-md);
1475
+ background: var(--cb-bg);
1476
+ box-shadow: var(--cb-shadow-sm);
1477
+ cursor: grab;
1478
+ transition: box-shadow var(--cb-duration-fast) var(--cb-ease-standard),
1479
+ transform var(--cb-duration-base) var(--cb-ease-standard);
1480
+ }
1481
+
1482
+ .cb-board__card:hover {
1483
+ box-shadow: var(--cb-shadow-md);
1484
+ }
1485
+
1486
+ .cb-board__card:focus-visible {
1487
+ outline: var(--cb-focus-width) solid var(--cb-tone-brand);
1488
+ outline-offset: var(--cb-focus-offset);
1489
+ }
1490
+
1491
+ /* Held by the keyboard: the same lift a pointer drag gives, so both paths look alike. */
1492
+ .cb-board__card[data-held] {
1493
+ box-shadow: var(--cb-shadow-lg);
1494
+ border-color: var(--cb-tone-brand);
1495
+ }
1496
+
1497
+ .cb-board__card[data-dragging] {
1498
+ opacity: 0.4;
1499
+ }
1500
+
1501
+ .cb-board__card[data-disabled] {
1502
+ cursor: not-allowed;
1503
+ color: var(--cb-fg-muted);
1504
+ background: var(--cb-bg-subtle);
1505
+ box-shadow: var(--cb-shadow-none);
1506
+ }
1507
+
1508
+ /* What follows the pointer. It is out of flow, so it carries the lift on its own. */
1509
+ .cb-board__card--lift {
1510
+ cursor: grabbing;
1511
+ box-shadow: var(--cb-shadow-lg);
1512
+ transform: rotate(1.5deg);
1513
+ }
1514
+
1515
+ .cb-board__title {
1516
+ font-size: var(--cb-text-sm);
1517
+ color: var(--cb-fg);
1518
+ line-height: var(--cb-leading-tight);
1519
+ /* A long title clamps rather than stretching its own card and the cards beside it. The full name
1520
+ belongs in a tooltip the consumer supplies around the title. */
1521
+ display: -webkit-box;
1522
+ -webkit-box-orient: vertical;
1523
+ -webkit-line-clamp: 2;
1524
+ overflow: hidden;
1525
+ }
1526
+
1527
+ .cb-board__meta {
1528
+ font-size: var(--cb-text-xs);
1529
+ color: var(--cb-fg-muted);
1530
+ }
1531
+
1532
+ /* Where the held card would land. It is the one affordance that must survive reduced motion. */
1533
+ .cb-board__marker {
1534
+ block-size: var(--cb-space-1);
1535
+ border-radius: var(--cb-radius-full);
1536
+ background: var(--cb-tone-brand);
1537
+ list-style: none;
1538
+ }
1539
+
1540
+ .cb-board__empty {
1541
+ padding: var(--cb-space-4) var(--cb-space-3);
1542
+ border: var(--cb-border-width) dashed var(--cb-border-strong);
1543
+ border-radius: var(--cb-radius-md);
1544
+ text-align: center;
1545
+ font-size: var(--cb-text-xs);
1546
+ color: var(--cb-fg-muted);
1547
+ }
1548
+
1549
+ /* ── lanes: the phone rendering, chosen rather than a squeezed board ── */
1550
+
1551
+ .cb-board__lanes {
1552
+ display: flex;
1553
+ gap: var(--cb-space-1);
1554
+ overflow-x: auto;
1555
+ }
1556
+
1557
+ .cb-board__lane {
1558
+ display: inline-flex;
1559
+ align-items: center;
1560
+ gap: var(--cb-space-2);
1561
+ min-block-size: var(--cb-control-floor);
1562
+ padding: 0 var(--cb-space-3);
1563
+ border: var(--cb-border-width) solid var(--cb-border);
1564
+ border-radius: var(--cb-radius-full);
1565
+ background: var(--cb-bg);
1566
+ color: var(--cb-fg-muted);
1567
+ font: inherit;
1568
+ font-size: var(--cb-text-sm);
1569
+ cursor: pointer;
1570
+ }
1571
+
1572
+ .cb-board__lane[aria-selected='true'] {
1573
+ background: color-mix(in oklch, var(--cb-tone-brand) 12%, var(--cb-bg));
1574
+ border-color: var(--cb-tone-brand);
1575
+ color: var(--cb-fg);
1576
+ }
1577
+
1578
+ .cb-board__lane:focus-visible {
1579
+ outline: var(--cb-focus-width) solid var(--cb-tone-brand);
1580
+ outline-offset: var(--cb-focus-offset);
1581
+ }
1582
+
1583
+ /* ── undo ── */
1584
+
1585
+ .cb-board__undo button {
1586
+ min-block-size: var(--cb-control-floor);
1587
+ padding: 0 var(--cb-space-3);
1588
+ border: var(--cb-border-width) solid var(--cb-border);
1589
+ border-radius: var(--cb-radius-md);
1590
+ background: var(--cb-bg);
1591
+ color: var(--cb-fg-link);
1592
+ font: inherit;
1593
+ font-size: var(--cb-text-sm);
1594
+ cursor: pointer;
1595
+ }
1596
+
1597
+ .cb-board__undo button:focus-visible {
1598
+ outline: var(--cb-focus-width) solid var(--cb-tone-brand);
1599
+ outline-offset: var(--cb-focus-offset);
1600
+ }
1601
+
1602
+ /* The announcement is for a screen reader, not the page. */
1603
+ .cb-board__live {
1604
+ position: absolute;
1605
+ inline-size: 1px;
1606
+ block-size: 1px;
1607
+ margin: -1px;
1608
+ padding: 0;
1609
+ overflow: hidden;
1610
+ clip-path: inset(50%);
1611
+ white-space: nowrap;
1612
+ }
1613
+
1614
+ /* ── skeleton ── */
1615
+
1616
+ .cb-board__ghost,
1617
+ .cb-board__ghost-card {
1618
+ border-radius: var(--cb-radius-md);
1619
+ background: color-mix(in oklch, var(--cb-fg-subtle) 18%, transparent);
1620
+ }
1621
+
1622
+ .cb-board__ghost--name { inline-size: var(--cb-space-8); block-size: var(--cb-space-3); }
1623
+ .cb-board__ghost--count { inline-size: var(--cb-space-4); block-size: var(--cb-space-3); }
1624
+ .cb-board__ghost-card { block-size: var(--cb-space-8); border: 0; box-shadow: var(--cb-shadow-none); }
1625
+
1626
+ /* ── renderings that are not decoration ── */
1627
+
1628
+ @media (prefers-reduced-motion: reduce) {
1629
+ .cb-board__card { transition: none; }
1630
+ /* The lift's tilt is decoration; the shadow that says "this one is moving" stays. */
1631
+ .cb-board__card--lift { transform: none; }
1632
+ }
1633
+
1634
+ .cb-board[data-motion='off'] .cb-board__card { transition: none; }
1635
+ .cb-board[data-motion='off'] .cb-board__card--lift { transform: none; }
1636
+
1637
+ @media (forced-colors: active) {
1638
+ .cb-board__column,
1639
+ .cb-board__card { border-color: CanvasText; }
1640
+ .cb-board__marker { background: Highlight; }
1641
+ .cb-board__card[data-held] { outline: var(--cb-focus-width) solid Highlight; }
1642
+ }
1643
+
1644
+ /* A card that carries its own buttons puts the grab on a handle instead of the card. A card that is
1645
+ itself a control has presentational children, so anything interactive inside it leaves the
1646
+ accessibility tree — the handle is what keeps those buttons reachable. */
1647
+ .cb-board__card[data-handle] {
1648
+ cursor: default;
1649
+ display: grid;
1650
+ grid-template-columns: auto 1fr;
1651
+ grid-template-areas: 'handle title' '. meta';
1652
+ column-gap: var(--cb-space-2);
1653
+ align-items: start;
1654
+ }
1655
+
1656
+ .cb-board__card[data-handle] .cb-board__title { grid-area: title; }
1657
+ .cb-board__card[data-handle] .cb-board__meta { grid-area: meta; }
1658
+
1659
+ .cb-board__handle {
1660
+ grid-area: handle;
1661
+ display: inline-flex;
1662
+ align-items: center;
1663
+ justify-content: center;
1664
+ min-inline-size: var(--cb-space-5);
1665
+ min-block-size: var(--cb-space-5);
1666
+ padding: 0;
1667
+ border: 0;
1668
+ border-radius: var(--cb-radius-sm);
1669
+ background: transparent;
1670
+ color: var(--cb-fg-subtle);
1671
+ font: inherit;
1672
+ line-height: var(--cb-leading-tight);
1673
+ cursor: grab;
1674
+ }
1675
+
1676
+ .cb-board__handle:focus-visible {
1677
+ outline: var(--cb-focus-width) solid var(--cb-tone-brand);
1678
+ outline-offset: var(--cb-focus-offset);
1679
+ }
1680
+
1681
+ .cb-board__handle:disabled { cursor: not-allowed; }
1682
+
1363
1683
  /* data/diagram/diagram.css */
1364
1684
  /* Diagram and DiagramEditor run on React Flow. Its base stylesheet (bundled before this file) owns layout and
1365
1685
  reads its colours from `--xy-*` custom properties; here those are pointed at Tokens, and the node React
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceebee/ui",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Ceebee's design system: tokens, themed primitives, motion, and onboarding.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -55,6 +55,9 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@ant-design/cssinjs": "2.1.2",
58
+ "@dnd-kit/core": "6.3.1",
59
+ "@dnd-kit/sortable": "10.0.0",
60
+ "@dnd-kit/utilities": "3.2.2",
58
61
  "@xyflow/react": "12.11.6",
59
62
  "antd": "6.6.1",
60
63
  "dayjs": "1.11.18"