gridsmith-ui 0.29.1 → 0.30.1

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 (108) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +5 -1
  3. package/dist/DropdownMenu-DqT7ZQ0D.d.ts +39 -0
  4. package/dist/charts.js +24 -9
  5. package/dist/charts.js.map +1 -1
  6. package/dist/chat.js +7 -7
  7. package/dist/{chunk-MFAWJSMJ.js → chunk-6C2IUMY7.js} +4 -23
  8. package/dist/chunk-6C2IUMY7.js.map +1 -0
  9. package/dist/{chunk-OYNUU346.js → chunk-7QLXYRPD.js} +3 -12
  10. package/dist/chunk-7QLXYRPD.js.map +1 -0
  11. package/dist/{chunk-FD4YED7O.js → chunk-DBHVJ7Z3.js} +5 -49
  12. package/dist/chunk-DBHVJ7Z3.js.map +1 -0
  13. package/dist/chunk-IELIF4PJ.js +110 -0
  14. package/dist/chunk-IELIF4PJ.js.map +1 -0
  15. package/dist/chunk-ISONVOA7.js +316 -0
  16. package/dist/chunk-ISONVOA7.js.map +1 -0
  17. package/dist/{chunk-4TUJXDRU.js → chunk-KMKBOTFR.js} +93 -32
  18. package/dist/chunk-KMKBOTFR.js.map +1 -0
  19. package/dist/{chunk-MKFWHWBK.js → chunk-L6HWACK7.js} +7 -7
  20. package/dist/chunk-L6HWACK7.js.map +1 -0
  21. package/dist/chunk-LJYSVZMI.js +25 -0
  22. package/dist/chunk-LJYSVZMI.js.map +1 -0
  23. package/dist/{chunk-6A5RHW2W.js → chunk-NEUWZUK6.js} +4 -5
  24. package/dist/chunk-NEUWZUK6.js.map +1 -0
  25. package/dist/chunk-NKUCS67T.js +13 -0
  26. package/dist/chunk-NKUCS67T.js.map +1 -0
  27. package/dist/{chunk-RECCCA7Z.js → chunk-OVHBQT3Z.js} +6 -5
  28. package/dist/chunk-OVHBQT3Z.js.map +1 -0
  29. package/dist/{chunk-I3MTQ3CS.js → chunk-OX3NNMWG.js} +6 -6
  30. package/dist/{chunk-I3MTQ3CS.js.map → chunk-OX3NNMWG.js.map} +1 -1
  31. package/dist/{chunk-ZI3OQDET.js → chunk-R5QXMNW7.js} +297 -15
  32. package/dist/chunk-R5QXMNW7.js.map +1 -0
  33. package/dist/chunk-SJZWEBHL.js +58 -0
  34. package/dist/chunk-SJZWEBHL.js.map +1 -0
  35. package/dist/{chunk-FUKSLQ77.js → chunk-SLRTSBCJ.js} +91 -51
  36. package/dist/chunk-SLRTSBCJ.js.map +1 -0
  37. package/dist/chunk-T52LAFKG.js +1350 -0
  38. package/dist/chunk-T52LAFKG.js.map +1 -0
  39. package/dist/chunk-TYNDXMBV.js +105 -0
  40. package/dist/chunk-TYNDXMBV.js.map +1 -0
  41. package/dist/{chunk-37WOC7F7.js → chunk-UDJOIVKQ.js} +31 -113
  42. package/dist/chunk-UDJOIVKQ.js.map +1 -0
  43. package/dist/{chunk-RJEUQ7RL.js → chunk-ULCACQSA.js} +47 -90
  44. package/dist/chunk-ULCACQSA.js.map +1 -0
  45. package/dist/{chunk-HP7WUZPK.js → chunk-WIUGPZOH.js} +10 -3
  46. package/dist/chunk-WIUGPZOH.js.map +1 -0
  47. package/dist/{chunk-JTU7OBYJ.js → chunk-YL5KD5RC.js} +4 -3
  48. package/dist/chunk-YL5KD5RC.js.map +1 -0
  49. package/dist/data-grid.d.ts +25 -6
  50. package/dist/data-grid.js +180 -58
  51. package/dist/data-grid.js.map +1 -1
  52. package/dist/editor-mentions.js +4 -5
  53. package/dist/editor-mentions.js.map +1 -1
  54. package/dist/editor.js +11 -8
  55. package/dist/editor.js.map +1 -1
  56. package/dist/{feather-FNBQ74RR.js → feather-XFFEFLTV.js} +5 -3
  57. package/dist/feather-XFFEFLTV.js.map +1 -0
  58. package/dist/{heroicons-DODCSYWD.js → heroicons-L6DOHBEA.js} +5 -3
  59. package/dist/heroicons-L6DOHBEA.js.map +1 -0
  60. package/dist/icons.d.ts +1 -1
  61. package/dist/icons.js +1 -1
  62. package/dist/index.d.ts +138 -39
  63. package/dist/index.js +846 -1667
  64. package/dist/index.js.map +1 -1
  65. package/dist/kanban.d.ts +26 -8
  66. package/dist/kanban.js +204 -79
  67. package/dist/kanban.js.map +1 -1
  68. package/dist/{lucide-ZWE3EPGP.js → lucide-JOORDTO6.js} +5 -3
  69. package/dist/lucide-JOORDTO6.js.map +1 -0
  70. package/dist/{phosphor-DIHPA4AJ.js → phosphor-TKGDYNGR.js} +5 -3
  71. package/dist/phosphor-TKGDYNGR.js.map +1 -0
  72. package/dist/resizable.js +47 -16
  73. package/dist/resizable.js.map +1 -1
  74. package/dist/roots.css +12 -8
  75. package/dist/{table-utils-CxnpFHRF.d.ts → table-utils-BXkP-2Hx.d.ts} +3 -1
  76. package/dist/{tabler-D3VYTSGI.js → tabler-YGETLYRU.js} +5 -3
  77. package/dist/tabler-YGETLYRU.js.map +1 -0
  78. package/dist/tokens.css +14 -0
  79. package/dist/utils.js.map +1 -1
  80. package/dist/virtualized.d.ts +6 -1
  81. package/dist/virtualized.js +17 -13
  82. package/dist/virtualized.js.map +1 -1
  83. package/dist-static/tokens.css +14 -0
  84. package/package.json +1 -1
  85. package/dist/chunk-37WOC7F7.js.map +0 -1
  86. package/dist/chunk-4TUJXDRU.js.map +0 -1
  87. package/dist/chunk-6A5RHW2W.js.map +0 -1
  88. package/dist/chunk-FD4YED7O.js.map +0 -1
  89. package/dist/chunk-FUKSLQ77.js.map +0 -1
  90. package/dist/chunk-HP7WUZPK.js.map +0 -1
  91. package/dist/chunk-JTU7OBYJ.js.map +0 -1
  92. package/dist/chunk-MFAWJSMJ.js.map +0 -1
  93. package/dist/chunk-MKFWHWBK.js.map +0 -1
  94. package/dist/chunk-OYNUU346.js.map +0 -1
  95. package/dist/chunk-QM4SJKWV.js +0 -51
  96. package/dist/chunk-QM4SJKWV.js.map +0 -1
  97. package/dist/chunk-QNHVPE2B.js +0 -117
  98. package/dist/chunk-QNHVPE2B.js.map +0 -1
  99. package/dist/chunk-RECCCA7Z.js.map +0 -1
  100. package/dist/chunk-RJEUQ7RL.js.map +0 -1
  101. package/dist/chunk-WH2FUFAC.js +0 -15
  102. package/dist/chunk-WH2FUFAC.js.map +0 -1
  103. package/dist/chunk-ZI3OQDET.js.map +0 -1
  104. package/dist/feather-FNBQ74RR.js.map +0 -1
  105. package/dist/heroicons-DODCSYWD.js.map +0 -1
  106. package/dist/lucide-ZWE3EPGP.js.map +0 -1
  107. package/dist/phosphor-DIHPA4AJ.js.map +0 -1
  108. package/dist/tabler-D3VYTSGI.js.map +0 -1
package/dist/kanban.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
+ import { D as DropdownMenuSubmenu, a as DropdownMenuEntry } from './DropdownMenu-DqT7ZQ0D.js';
4
+ import './icons.js';
3
5
 
4
6
  interface KanbanCard {
5
7
  id: string;
@@ -13,6 +15,12 @@ interface KanbanColumn {
13
15
  title: string;
14
16
  cards: KanbanCard[];
15
17
  color?: string;
18
+ /**
19
+ * The column's work-in-progress limit, in cards: the header shows its count against it ("4/3"), and a column holding
20
+ * more is marked with a warning icon, its count's words saying so to a screen reader. A limit informs and never refuses
21
+ * a drop; a page that must refuse a move still can, in onCardMove. Default: no limit, the count alone.
22
+ */
23
+ limit?: number;
16
24
  }
17
25
  interface KanbanBoardProps {
18
26
  /**
@@ -28,19 +36,22 @@ interface KanbanBoardProps {
28
36
  */
29
37
  onColumnsChange?: (columns: KanbanColumn[]) => unknown;
30
38
  /**
31
- * A card was dropped at a new place: its column and its index there, once on drop — a move between columns, into an
32
- * empty one, or a reorder within one. Save the move here (or in onColumnsChange); a board with neither saves nothing.
33
- * The index counts the cards the board shows: when `columns` is filtered, it is the place among the cards passed.
39
+ * A card was moved to a new place: its column and its index there, once on drop, or on a choice from the card's Move
40
+ * menu (the button beside each card that moves it without dragging: to the top of another column, or a step up or
41
+ * down) — a move between columns, into an empty one, or a reorder within one. Save the move here (or in
42
+ * onColumnsChange); a board with neither saves nothing. The index counts the cards the board shows: when `columns` is
43
+ * filtered, it is the place among the cards passed.
34
44
  *
35
45
  * Return the save's promise (an async function, or a mutation's mutateAsync) and the card stays where it was dropped
36
46
  * while it saves, and while an earlier move of the same card still saves, whatever `columns` arrive meanwhile; when
37
47
  * it rejects, the card goes back to where `columns` has it, a screen reader hears so, and the app says why (a toast
38
48
  * inside the app shell). The save must settle (give a request a timeout): one that never does holds the card. A
39
- * handler that throws refuses a move the same way (a column that is full). An app that updates `columns` at once,
40
- * then saves, puts the card back itself when its save fails; the board still says the move was not saved.
49
+ * handler that throws refuses a move the same way (one the person isn't allowed to make). An app that updates
50
+ * `columns` at once, then saves, puts the card back itself when its save fails; the board still says the move was not
51
+ * saved.
41
52
  */
42
53
  onCardMove?: (card: KanbanCard, toColumnId: string, index: number) => unknown;
43
- /** A board to look at, not rearrange: no dragging and no grab cursor; cards still open with onCardClick. */
54
+ /** A board to look at, not rearrange: no dragging, no grab cursor and no Move menu; cards still open with onCardClick. */
44
55
  readOnly?: boolean;
45
56
  renderCard?: (card: KanbanCard) => ReactNode;
46
57
  /**
@@ -49,8 +60,15 @@ interface KanbanBoardProps {
49
60
  * renderCard — a role="button" div fights the drag listeners and the judge cites it (Sprint 25.4 K-1).
50
61
  */
51
62
  onCardClick?: (card: KanbanCard) => void;
63
+ /**
64
+ * A card's own actions (open, edit its labels, copy a link, delete…), for its ⋯ menu. `move` is the board's Move entry,
65
+ * a submenu of the columns the card can go to and its steps up and down: put it where it belongs among the actions;
66
+ * left out, the board adds it at the end, since it is the way to move a card without dragging (WCAG 2.5.7). Without
67
+ * cardActions the menu is the move choices alone. On a read-only board the menu holds the actions, with no Move.
68
+ */
69
+ cardActions?: (card: KanbanCard, move: DropdownMenuSubmenu) => DropdownMenuEntry[];
52
70
  className?: string;
53
71
  }
54
- declare function KanbanBoard({ columns: columnsProp, onColumnsChange, onCardMove, readOnly, renderCard, onCardClick, className, }: KanbanBoardProps): react_jsx_runtime.JSX.Element;
72
+ declare function KanbanBoard({ columns: columnsProp, onColumnsChange, onCardMove, readOnly, renderCard, onCardClick, cardActions, className, }: KanbanBoardProps): react_jsx_runtime.JSX.Element;
55
73
 
56
- export { KanbanBoard };
74
+ export { KanbanBoard, type KanbanBoardProps, type KanbanCard, type KanbanColumn };
package/dist/kanban.js CHANGED
@@ -1,26 +1,52 @@
1
1
  "use client";
2
- import { useFillHeight, FillContext, useKeyboardScroll } from './chunk-FD4YED7O.js';
3
- import { Badge } from './chunk-RECCCA7Z.js';
4
- import { useFramesItself } from './chunk-QM4SJKWV.js';
2
+ import { DropdownMenu } from './chunk-ISONVOA7.js';
3
+ import { useFillHeight, FillContext } from './chunk-DBHVJ7Z3.js';
4
+ import { useKeyboardScroll } from './chunk-SJZWEBHL.js';
5
+ import { Badge } from './chunk-OVHBQT3Z.js';
6
+ import { useFramesItself, Button } from './chunk-ULCACQSA.js';
7
+ import './chunk-NKUCS67T.js';
8
+ import './chunk-ZHUVLBCC.js';
5
9
  import { useRevealFocus } from './chunk-HTXP5BMM.js';
6
- import './chunk-ZI3OQDET.js';
7
- import { useTextOverflow, AutoTooltip } from './chunk-37WOC7F7.js';
10
+ import './chunk-R5QXMNW7.js';
11
+ import { useTextOverflow } from './chunk-IELIF4PJ.js';
12
+ import { AutoTooltip } from './chunk-UDJOIVKQ.js';
13
+ import { Icon } from './chunk-L6HWACK7.js';
8
14
  import './chunk-7Q46JXSK.js';
9
- import { Icon } from './chunk-MKFWHWBK.js';
10
- import './chunk-HP7WUZPK.js';
11
- import './chunk-WH2FUFAC.js';
12
- import { cn, FOCUS_INSET, FOCUS_RING } from './chunk-JTU7OBYJ.js';
15
+ import './chunk-WIUGPZOH.js';
16
+ import { cn, FOCUS_INSET, FOCUS_RING } from './chunk-YL5KD5RC.js';
13
17
  import { useState, useRef, useLayoutEffect, useMemo, useCallback, useContext } from 'react';
14
18
  import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCorners, DragOverlay, useDroppable } from '@dnd-kit/core';
15
19
  import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from '@dnd-kit/sortable';
16
20
  import { CSS } from '@dnd-kit/utilities';
17
21
  import { jsxs, jsx } from 'react/jsx-runtime';
18
22
 
23
+ var MOVE_ROOM_TITLE = "pr-[var(--ds-control-height-sm)]";
24
+ var MOVE_ROOM_CARD = "pr-[calc(var(--ds-space-3)+var(--ds-control-height-sm))]";
25
+ function cardMenuOf(card, moveEntries, cardActions) {
26
+ if (!cardActions) return moveEntries;
27
+ const move = { label: "Move", items: moveEntries };
28
+ const own = cardActions(card, move);
29
+ const isMove = (e) => e === move || "items" in e && e.items === move.items;
30
+ const divider = { type: "divider" };
31
+ const entries = moveEntries.length === 0 ? own.filter((e) => !isMove(e)) : own.some(isMove) ? own : [...own, divider, move];
32
+ const isDivider = (e) => !!e && "type" in e && e.type === "divider";
33
+ return entries.reduce((acc, e) => isDivider(e) && (acc.length === 0 || isDivider(acc[acc.length - 1])) ? acc : [...acc, e], []).filter((e, i, all) => !(isDivider(e) && i === all.length - 1));
34
+ }
35
+ function menuOf(moves, choose) {
36
+ const entry = (m) => ({ label: m.label, onClick: () => choose(m) });
37
+ const others = moves.filter((m) => m.step === 0).map(entry);
38
+ const steps = moves.filter((m) => m.step !== 0).map(entry);
39
+ return others.length && steps.length ? [...others, { type: "divider" }, ...steps] : [...others, ...steps];
40
+ }
41
+ var MOVE_PLACE = "absolute top-ds-1.5 right-ds-1.5";
19
42
  function SortableCard({
20
43
  card,
21
44
  renderCard,
22
45
  onCardClick,
23
- readOnly
46
+ readOnly,
47
+ moves,
48
+ onMove,
49
+ cardActions
24
50
  }) {
25
51
  const {
26
52
  attributes,
@@ -30,9 +56,34 @@ function SortableCard({
30
56
  transition,
31
57
  isDragging
32
58
  } = useSortable({ id: card.id, disabled: readOnly });
59
+ const entries = cardMenuOf(card, readOnly ? [] : menuOf(moves, (m) => onMove(card.id, m.to, m.step)), cardActions);
60
+ const renderMenu = (style2, faded) => /* @__PURE__ */ jsx(
61
+ DropdownMenu,
62
+ {
63
+ items: entries,
64
+ trigger: (
65
+ // the library's icon button: its name says what its menu is for, and it shows that name as its one tooltip;
66
+ // shown at all times, since a phone has no hover; quiet, as a Card's ⋯ is
67
+ /* @__PURE__ */ jsx(
68
+ Button,
69
+ {
70
+ type: "button",
71
+ variant: "ghost",
72
+ size: "icon-sm",
73
+ "aria-label": cardActions ? `Actions for ${card.title}` : `Move ${card.title}`,
74
+ "data-kanban-move": card.id,
75
+ style: style2,
76
+ className: cn(MOVE_PLACE, "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)]", faded && "opacity-30"),
77
+ children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: 14 })
78
+ }
79
+ )
80
+ )
81
+ }
82
+ );
83
+ const menu = entries.length > 0 ? renderMenu : null;
33
84
  if (readOnly) {
34
85
  const open = onCardClick ? () => onCardClick(card) : void 0;
35
- return /* @__PURE__ */ jsx(
86
+ const surface = /* @__PURE__ */ jsx(
36
87
  "div",
37
88
  {
38
89
  ref: setNodeRef,
@@ -48,47 +99,63 @@ function SortableCard({
48
99
  className: cn(
49
100
  "rounded-[var(--ds-radius-md)] border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
50
101
  "bg-[var(--ds-bg-surface)] p-ds-3 shadow-[var(--ds-shadow-button)]",
102
+ // a card with its actions' ⋯ keeps the button's room clear, as a card that moves does
103
+ menu && renderCard && MOVE_ROOM_CARD,
51
104
  open ? "cursor-pointer" : "cursor-default",
52
105
  FOCUS_RING
53
106
  ),
54
- children: renderCard ? renderCard(card) : /* @__PURE__ */ jsx(DefaultCard, { card })
107
+ children: renderCard ? renderCard(card) : /* @__PURE__ */ jsx(DefaultCard, { card, moveRoom: !!menu })
55
108
  }
56
109
  );
110
+ return menu ? /* @__PURE__ */ jsxs("div", { className: "relative", "data-kanban-item": card.id, children: [
111
+ surface,
112
+ menu(void 0, false)
113
+ ] }) : surface;
57
114
  }
58
115
  const style = {
59
116
  transform: CSS.Transform.toString(transform),
60
117
  transition
61
118
  };
62
- return /* @__PURE__ */ jsx(
63
- "div",
64
- {
65
- ref: setNodeRef,
66
- style,
67
- ...attributes,
68
- ...listeners,
69
- onClick: onCardClick ? () => onCardClick(card) : void 0,
70
- onKeyDown: (e) => {
71
- if (onCardClick && e.key === "Enter") {
72
- e.preventDefault();
73
- onCardClick(card);
74
- return;
119
+ const movable = !!menu;
120
+ return (
121
+ // Sprint 25.8.7 item 3 (WCAG 2.5.7): the card's slot holds the card and, beside it, its Move button, never one
122
+ // inside the other (a control inside a control: the card is the drag's button). The card stays the node the drag
123
+ // measures and moves; the button takes the same transform, so it travels with the card as the list sorts.
124
+ /* @__PURE__ */ jsxs("div", { className: "relative", "data-kanban-item": card.id, children: [
125
+ /* @__PURE__ */ jsx(
126
+ "div",
127
+ {
128
+ ref: setNodeRef,
129
+ style,
130
+ ...attributes,
131
+ ...listeners,
132
+ onClick: onCardClick ? () => onCardClick(card) : void 0,
133
+ onKeyDown: (e) => {
134
+ if (onCardClick && e.key === "Enter") {
135
+ e.preventDefault();
136
+ onCardClick(card);
137
+ return;
138
+ }
139
+ listeners?.onKeyDown?.(e);
140
+ },
141
+ "data-kanban-card": card.id,
142
+ className: cn(
143
+ "rounded-[var(--ds-radius-md)] border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
144
+ "bg-[var(--ds-bg-surface)] p-ds-3 active:cursor-grabbing",
145
+ movable && renderCard && MOVE_ROOM_CARD,
146
+ onCardClick ? "cursor-pointer" : "cursor-grab",
147
+ "shadow-[var(--ds-shadow-button)]",
148
+ "transition-shadow duration-[var(--ds-transition-speed)]",
149
+ FOCUS_RING,
150
+ // no scale on the card left behind: the drag overlay takes its size from it, and 2% wider made the cards
151
+ // below count as "to the right" of it, so a keyboard drag's arrow keys landed on them (Sprint 25.7, C2)
152
+ isDragging && "opacity-30"
153
+ ),
154
+ children: renderCard ? renderCard(card) : /* @__PURE__ */ jsx(DefaultCard, { card, moveRoom: movable })
75
155
  }
76
- listeners?.onKeyDown?.(e);
77
- },
78
- "data-kanban-card": card.id,
79
- className: cn(
80
- "rounded-[var(--ds-radius-md)] border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
81
- "bg-[var(--ds-bg-surface)] p-ds-3 active:cursor-grabbing",
82
- onCardClick ? "cursor-pointer" : "cursor-grab",
83
- "shadow-[var(--ds-shadow-button)]",
84
- "transition-shadow duration-[var(--ds-transition-speed)]",
85
- FOCUS_RING,
86
- // no scale on the card left behind: the drag overlay takes its size from it, and 2% wider made the cards
87
- // below count as "to the right" of it, so a keyboard drag's arrow keys landed on them (Sprint 25.7, C2)
88
- isDragging && "opacity-30"
89
156
  ),
90
- children: renderCard ? renderCard(card) : /* @__PURE__ */ jsx(DefaultCard, { card })
91
- }
157
+ menu && menu(style, isDragging)
158
+ ] })
92
159
  );
93
160
  }
94
161
  function ClampedDescription({ children }) {
@@ -97,31 +164,36 @@ function ClampedDescription({ children }) {
97
164
  if (!isTruncated) return el;
98
165
  return /* @__PURE__ */ jsx(AutoTooltip, { content: children, children: el });
99
166
  }
100
- function TruncTitle({ children }) {
167
+ function TruncTitle({ children, className }) {
101
168
  const { ref, isTruncated } = useTextOverflow({ lines: 1 });
102
- const el = /* @__PURE__ */ jsx("p", { ref, className: "text-sm font-medium text-[var(--ds-text-primary)] truncate", children });
169
+ const el = /* @__PURE__ */ jsx("p", { ref, className: cn("text-sm font-medium text-[var(--ds-text-primary)] truncate", className), children });
103
170
  if (!isTruncated) return el;
104
171
  return /* @__PURE__ */ jsx(AutoTooltip, { content: children, children: el });
105
172
  }
106
- function DefaultCard({ card }) {
173
+ function DefaultCard({ card, moveRoom }) {
107
174
  return /* @__PURE__ */ jsxs("div", { className: "space-y-ds-2", children: [
108
- /* @__PURE__ */ jsx(TruncTitle, { children: card.title }),
175
+ /* @__PURE__ */ jsx(TruncTitle, { className: moveRoom ? MOVE_ROOM_TITLE : void 0, children: card.title }),
109
176
  card.description && /* @__PURE__ */ jsx(ClampedDescription, { children: card.description }),
110
177
  card.tags && card.tags.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-ds-1", children: card.tags.map((tag) => /* @__PURE__ */ jsx(Badge, { variant: "subtle", size: "sm", children: tag }, tag)) })
111
178
  ] });
112
179
  }
113
180
  function OverlayCard({
114
181
  card,
115
- renderCard
182
+ renderCard,
183
+ moveRoom
116
184
  }) {
117
- return /* @__PURE__ */ jsx(
185
+ return /* @__PURE__ */ jsxs(
118
186
  "div",
119
187
  {
120
188
  className: cn(
121
- "rounded-[var(--ds-radius-md)] border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
122
- "bg-[var(--ds-bg-surface)] p-ds-3 shadow-[var(--ds-shadow-card)]"
189
+ "relative rounded-[var(--ds-radius-md)] border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
190
+ "bg-[var(--ds-bg-surface)] p-ds-3 shadow-[var(--ds-shadow-card)]",
191
+ moveRoom && renderCard && MOVE_ROOM_CARD
123
192
  ),
124
- children: renderCard ? renderCard(card) : /* @__PURE__ */ jsx(DefaultCard, { card })
193
+ children: [
194
+ renderCard ? renderCard(card) : /* @__PURE__ */ jsx(DefaultCard, { card, moveRoom }),
195
+ moveRoom && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn(MOVE_PLACE, "inline-flex items-center justify-center h-[var(--ds-control-height-sm)] w-[var(--ds-control-height-sm)] text-[var(--ds-text-muted)]"), children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: 14 }) })
196
+ ]
125
197
  }
126
198
  );
127
199
  }
@@ -194,6 +266,21 @@ function keptHolds(holds, cols) {
194
266
  var replayHolds = (cols, holds) => holds.reduce(place, cols);
195
267
  var isThenable = (value) => value != null && typeof value.then === "function";
196
268
  var placeWords = (at) => `${at.col.title}, position ${at.index + 1} of ${at.col.cards.length}`;
269
+ var overLimit = (col) => col.limit != null && col.cards.length > col.limit;
270
+ var overLimitWords = (col) => overLimit(col) ? ` ${col.title} is over its limit of ${col.limit}.` : "";
271
+ var countWords = (count, limit) => `${count} ${count === 1 ? "card" : "cards"}, ${count > limit ? `over the limit of ${limit}` : `limit ${limit}`}`;
272
+ function ColumnCount({ count, limit }) {
273
+ if (limit == null) return /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--ds-text-muted)] tabular-nums", children: count });
274
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-ds-1 text-xs text-[var(--ds-text-muted)] tabular-nums", children: [
275
+ /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", children: [
276
+ count,
277
+ "/",
278
+ limit
279
+ ] }),
280
+ count > limit && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", "data-kanban-over-limit": "", className: "inline-flex shrink-0 text-[var(--ds-status-warning)]", children: /* @__PURE__ */ jsx(Icon, { name: "warning", size: 14 }) }),
281
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: countWords(count, limit) })
282
+ ] });
283
+ }
197
284
  var MOVE_INSTRUCTIONS = "Press Space to pick up the card. Use the Left and Right arrow keys to move it between columns, and the Up and Down arrow keys to move it within a column. Press Space again to drop it, or Escape to cancel.";
198
285
  function BoardColumn({
199
286
  col,
@@ -228,7 +315,7 @@ function BoardColumn({
228
315
  }
229
316
  ),
230
317
  /* @__PURE__ */ jsx("span", { className: "text-sm font-[var(--ds-weight-heading)] text-[var(--ds-text-primary)]", children: col.title }),
231
- /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--ds-text-muted)] tabular-nums", children: col.cards.length })
318
+ /* @__PURE__ */ jsx(ColumnCount, { count: col.cards.length, limit: col.limit })
232
319
  ] }),
233
320
  /* @__PURE__ */ jsx(Icon, { name: "plus", size: 14, className: "text-[var(--ds-text-muted)]" })
234
321
  ] }),
@@ -266,6 +353,7 @@ function KanbanBoard({
266
353
  readOnly,
267
354
  renderCard,
268
355
  onCardClick,
356
+ cardActions,
269
357
  className
270
358
  }) {
271
359
  useFramesItself();
@@ -329,13 +417,13 @@ function KanbanBoard({
329
417
  const fill = useFillHeight(rootRef);
330
418
  const inPane = useContext(FillContext);
331
419
  const filled = fill != null || inPane;
420
+ const cardPart = (attr, id) => [...rootRef.current?.querySelectorAll(`[${attr}]`) ?? []].find((el) => el.getAttribute(attr) === id);
332
421
  const refocus = useRef(null);
333
422
  useLayoutEffect(() => {
334
- const id = refocus.current;
335
- if (id == null) return;
423
+ const target = refocus.current;
424
+ if (target == null) return;
336
425
  refocus.current = null;
337
- const cards = rootRef.current?.querySelectorAll("[data-kanban-card]") ?? [];
338
- [...cards].find((el) => el.getAttribute("data-kanban-card") === id)?.focus();
426
+ cardPart(target.part === "move" ? "data-kanban-move" : "data-kanban-card", target.id)?.focus();
339
427
  });
340
428
  const holdKey = useRef(0);
341
429
  const sensors = useSensors(
@@ -389,8 +477,11 @@ function KanbanBoard({
389
477
  const back = locate(shown, hold.cardId);
390
478
  const card = back?.col.cards[back.index];
391
479
  const focused = document.activeElement;
392
- if (focused && rootRef.current?.contains(focused) && focused.closest?.("[data-kanban-card]")?.getAttribute("data-kanban-card") === hold.cardId) {
393
- refocus.current = hold.cardId;
480
+ const inSlot = !!focused && !!rootRef.current?.contains(focused) && focused.closest?.("[data-kanban-item]")?.getAttribute("data-kanban-item") === hold.cardId;
481
+ const menuOpen = cardPart("data-kanban-move", hold.cardId)?.getAttribute("aria-expanded") === "true";
482
+ if (inSlot) refocus.current = { id: hold.cardId, part: focused?.hasAttribute("data-kanban-move") ? "move" : "card" };
483
+ else if (menuOpen && locate(replayHolds(columnsRef.current, holdsRef.current), hold.cardId)?.col.id !== back?.col.id) {
484
+ refocus.current = { id: hold.cardId, part: "move" };
394
485
  }
395
486
  setHolds(rest);
396
487
  if (previewRef.current && activeRef.current) setPreview(rebase(shown, previewRef.current, activeRef.current));
@@ -406,32 +497,20 @@ function KanbanBoard({
406
497
  const next = holdsRef.current.map((h) => h === hold ? { ...h, inFlight, layout: inFlight > 0 ? h.layout : layoutOf(columnsRef.current) } : h);
407
498
  setHolds(inFlight > 0 ? next : keptHolds(next, columnsRef.current));
408
499
  };
409
- const handleDragEnd = (event) => {
410
- const { active, over } = event;
411
- const activeId = String(active.id);
412
- const cols = previewRef.current ?? committed;
413
- activeRef.current = null;
414
- setActiveCard(null);
415
- setPreview(null);
416
- const final = over ? landing(cols, activeId, String(over.id)) : committed;
417
- const before = locate(committed, activeId);
418
- const after = locate(final, activeId);
419
- if (!before || !after || before.col.id === after.col.id && before.index === after.index) {
420
- if (before) said.current = `Dropped ${before.col.cards[before.index].title} back in ${placeWords(before)}.`;
421
- return;
422
- }
423
- said.current = `Dropped ${after.col.cards[after.index].title} in ${placeWords(after)}.`;
500
+ const commit = (final, cardId) => {
501
+ const after = locate(final, cardId);
502
+ if (!after) return;
424
503
  const key = ++holdKey.current;
425
- const prev = holdsRef.current.find((h) => h.cardId === activeId);
504
+ const prev = holdsRef.current.find((h) => h.cardId === cardId);
426
505
  const hold = {
427
- ...placementIn(final, activeId),
506
+ ...placementIn(final, cardId),
428
507
  key,
429
508
  inFlight: prev?.inFlight ?? 0,
430
- origin: prev?.origin ?? placementIn(columnsProp, activeId) ?? placementIn(committed, activeId),
509
+ origin: prev?.origin ?? placementIn(columnsProp, cardId) ?? placementIn(committed, cardId),
431
510
  earlier: prev ? [...prev.earlier, { cardId: prev.cardId, toColumnId: prev.toColumnId, beforeId: prev.beforeId, index: prev.index }] : [],
432
511
  layout: layoutOf(columnsProp)
433
512
  };
434
- setHolds([...holdsRef.current.filter((h) => h.cardId !== activeId), hold]);
513
+ setHolds([...holdsRef.current.filter((h) => h.cardId !== cardId), hold]);
435
514
  const saves = [];
436
515
  try {
437
516
  for (const result of [onColumnsChange?.(final), onCardMove?.(after.col.cards[after.index], after.col.id, after.index)]) {
@@ -443,8 +522,51 @@ function KanbanBoard({
443
522
  }
444
523
  if (!saves.length) return;
445
524
  setHolds(holdsRef.current.map((h) => h.key === key ? { ...h, inFlight: h.inFlight + 1 } : h));
446
- Promise.allSettled(saves).then((results) => settle(activeId, key, results.every((r) => r.status === "fulfilled")));
525
+ Promise.allSettled(saves).then((results) => settle(cardId, key, results.every((r) => r.status === "fulfilled")));
526
+ };
527
+ const handleDragEnd = (event) => {
528
+ const { active, over } = event;
529
+ const activeId = String(active.id);
530
+ const cols = previewRef.current ?? committed;
531
+ activeRef.current = null;
532
+ setActiveCard(null);
533
+ setPreview(null);
534
+ const final = over ? landing(cols, activeId, String(over.id)) : committed;
535
+ const before = locate(committed, activeId);
536
+ const after = locate(final, activeId);
537
+ if (!before || !after || before.col.id === after.col.id && before.index === after.index) {
538
+ if (before) said.current = `Dropped ${before.col.cards[before.index].title} back in ${placeWords(before)}.`;
539
+ return;
540
+ }
541
+ said.current = `Dropped ${after.col.cards[after.index].title} in ${placeWords(after)}.${overLimitWords(after.col)}`;
542
+ commit(final, activeId);
543
+ };
544
+ const moveFromMenu = (cardId, toColumnId, step = 0) => {
545
+ const at = locate(committed, cardId);
546
+ const to = committed.find((col) => col.id === toColumnId);
547
+ if (!at || !to) return;
548
+ const index = to.id === at.col.id ? at.index + step : 0;
549
+ const beforeId = to.cards.filter((c) => c.id !== cardId)[index]?.id ?? null;
550
+ const final = place(committed, { cardId, toColumnId, beforeId, index });
551
+ const after = locate(final, cardId);
552
+ if (!after || after.col.id === at.col.id && after.index === at.index) return;
553
+ noticeParts.current = [];
554
+ setNotice(`Moved ${at.col.cards[at.index].title} to ${placeWords(after)}.${overLimitWords(after.col)}`);
555
+ commit(final, cardId);
556
+ refocus.current = { id: cardId, part: "move" };
557
+ };
558
+ const movesFor = (col, index) => {
559
+ if (readOnly) return [];
560
+ const others = columns.filter((c) => c.id !== col.id).map((c) => ({ label: `Move to ${c.title}`, to: c.id, step: 0 }));
561
+ const steps = [
562
+ ...index > 0 ? [{ label: "Move up", to: col.id, step: -1 }] : [],
563
+ ...index < col.cards.length - 1 ? [{ label: "Move down", to: col.id, step: 1 }] : []
564
+ ];
565
+ return [...others, ...steps];
447
566
  };
567
+ const activeAt = activeCard ? locate(columns, activeCard.id) : null;
568
+ const activeMovable = !!activeAt && cardMenuOf(activeAt.col.cards[activeAt.index], menuOf(movesFor(activeAt.col, activeAt.index), () => {
569
+ }), cardActions).length > 0;
448
570
  const handleDragCancel = (event) => {
449
571
  activeRef.current = null;
450
572
  setActiveCard(null);
@@ -464,19 +586,22 @@ function KanbanBoard({
464
586
  onDragCancel: handleDragCancel,
465
587
  children: [
466
588
  /* @__PURE__ */ jsxs("div", { ref: setRoot, "data-fill": fill != null ? "" : void 0, className: cn("relative flex gap-ds-4 overflow-x-auto pb-ds-2 scroll-px-[4px] snap-x snap-mandatory sm:snap-none", inPane && "h-full min-h-0", className), style: fill != null ? { height: fill } : void 0, children: [
467
- columns.map((col) => /* @__PURE__ */ jsx(BoardColumn, { col, readOnly, filled, children: col.cards.map((card) => /* @__PURE__ */ jsx(
589
+ columns.map((col) => /* @__PURE__ */ jsx(BoardColumn, { col, readOnly, filled, children: col.cards.map((card, index) => /* @__PURE__ */ jsx(
468
590
  SortableCard,
469
591
  {
470
592
  card,
471
593
  renderCard,
472
594
  onCardClick,
473
- readOnly
595
+ readOnly,
596
+ moves: movesFor(col, index),
597
+ onMove: moveFromMenu,
598
+ cardActions
474
599
  },
475
600
  card.id
476
601
  )) }, col.id)),
477
602
  /* @__PURE__ */ jsx("div", { role: "status", className: "sr-only", children: notice })
478
603
  ] }),
479
- /* @__PURE__ */ jsx(DragOverlay, { children: activeCard && /* @__PURE__ */ jsx(OverlayCard, { card: activeCard, renderCard }) })
604
+ /* @__PURE__ */ jsx(DragOverlay, { children: activeCard && /* @__PURE__ */ jsx(OverlayCard, { card: activeCard, renderCard, moveRoom: activeMovable }) })
480
605
  ]
481
606
  }
482
607
  );