gridsmith-ui 0.29.1 → 0.30.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +5 -1
  3. package/dist/DropdownMenu-DqT7ZQ0D.d.ts +39 -0
  4. package/dist/charts.js +4 -2
  5. package/dist/charts.js.map +1 -1
  6. package/dist/chat.js +4 -4
  7. package/dist/{chunk-OYNUU346.js → chunk-7QLXYRPD.js} +3 -12
  8. package/dist/chunk-7QLXYRPD.js.map +1 -0
  9. package/dist/chunk-F2BNJSON.js +1348 -0
  10. package/dist/chunk-F2BNJSON.js.map +1 -0
  11. package/dist/{chunk-37WOC7F7.js → chunk-GQVBZ4GW.js} +29 -8
  12. package/dist/chunk-GQVBZ4GW.js.map +1 -0
  13. package/dist/{chunk-MFAWJSMJ.js → chunk-INGF2CPW.js} +3 -22
  14. package/dist/chunk-INGF2CPW.js.map +1 -0
  15. package/dist/{chunk-I3MTQ3CS.js → chunk-KTPGMJUK.js} +5 -5
  16. package/dist/{chunk-I3MTQ3CS.js.map → chunk-KTPGMJUK.js.map} +1 -1
  17. package/dist/{chunk-MKFWHWBK.js → chunk-L6HWACK7.js} +7 -7
  18. package/dist/chunk-L6HWACK7.js.map +1 -0
  19. package/dist/chunk-LJYSVZMI.js +25 -0
  20. package/dist/chunk-LJYSVZMI.js.map +1 -0
  21. package/dist/{chunk-FUKSLQ77.js → chunk-NI5542ZM.js} +39 -35
  22. package/dist/chunk-NI5542ZM.js.map +1 -0
  23. package/dist/chunk-NKUCS67T.js +13 -0
  24. package/dist/chunk-NKUCS67T.js.map +1 -0
  25. package/dist/{chunk-RECCCA7Z.js → chunk-NLXQ323J.js} +4 -4
  26. package/dist/{chunk-RECCCA7Z.js.map → chunk-NLXQ323J.js.map} +1 -1
  27. package/dist/{chunk-ZI3OQDET.js → chunk-OIOII5L6.js} +93 -4
  28. package/dist/chunk-OIOII5L6.js.map +1 -0
  29. package/dist/{chunk-RJEUQ7RL.js → chunk-Q36JVJX4.js} +46 -89
  30. package/dist/chunk-Q36JVJX4.js.map +1 -0
  31. package/dist/{chunk-FD4YED7O.js → chunk-RHWTMTU5.js} +3 -3
  32. package/dist/{chunk-FD4YED7O.js.map → chunk-RHWTMTU5.js.map} +1 -1
  33. package/dist/chunk-TYNDXMBV.js +105 -0
  34. package/dist/chunk-TYNDXMBV.js.map +1 -0
  35. package/dist/{chunk-4TUJXDRU.js → chunk-UNS4TGGK.js} +20 -11
  36. package/dist/chunk-UNS4TGGK.js.map +1 -0
  37. package/dist/chunk-Z4MCD73B.js +316 -0
  38. package/dist/chunk-Z4MCD73B.js.map +1 -0
  39. package/dist/data-grid.d.ts +25 -6
  40. package/dist/data-grid.js +177 -55
  41. package/dist/data-grid.js.map +1 -1
  42. package/dist/editor-mentions.js +1 -1
  43. package/dist/editor.js +3 -3
  44. package/dist/{feather-FNBQ74RR.js → feather-XFFEFLTV.js} +5 -3
  45. package/dist/feather-XFFEFLTV.js.map +1 -0
  46. package/dist/{heroicons-DODCSYWD.js → heroicons-L6DOHBEA.js} +5 -3
  47. package/dist/heroicons-L6DOHBEA.js.map +1 -0
  48. package/dist/icons.d.ts +1 -1
  49. package/dist/icons.js +1 -1
  50. package/dist/index.d.ts +138 -39
  51. package/dist/index.js +660 -1568
  52. package/dist/index.js.map +1 -1
  53. package/dist/kanban.d.ts +26 -8
  54. package/dist/kanban.js +200 -76
  55. package/dist/kanban.js.map +1 -1
  56. package/dist/{lucide-ZWE3EPGP.js → lucide-JOORDTO6.js} +5 -3
  57. package/dist/lucide-JOORDTO6.js.map +1 -0
  58. package/dist/{phosphor-DIHPA4AJ.js → phosphor-TKGDYNGR.js} +5 -3
  59. package/dist/phosphor-TKGDYNGR.js.map +1 -0
  60. package/dist/resizable.js +10 -9
  61. package/dist/resizable.js.map +1 -1
  62. package/dist/roots.css +12 -8
  63. package/dist/{table-utils-CxnpFHRF.d.ts → table-utils-BXkP-2Hx.d.ts} +3 -1
  64. package/dist/{tabler-D3VYTSGI.js → tabler-YGETLYRU.js} +5 -3
  65. package/dist/tabler-YGETLYRU.js.map +1 -0
  66. package/dist/tokens.css +14 -0
  67. package/dist/utils.js.map +1 -1
  68. package/dist/virtualized.js +8 -7
  69. package/dist/virtualized.js.map +1 -1
  70. package/dist-static/tokens.css +14 -0
  71. package/package.json +1 -1
  72. package/dist/chunk-37WOC7F7.js.map +0 -1
  73. package/dist/chunk-4TUJXDRU.js.map +0 -1
  74. package/dist/chunk-FUKSLQ77.js.map +0 -1
  75. package/dist/chunk-MFAWJSMJ.js.map +0 -1
  76. package/dist/chunk-MKFWHWBK.js.map +0 -1
  77. package/dist/chunk-OYNUU346.js.map +0 -1
  78. package/dist/chunk-QM4SJKWV.js +0 -51
  79. package/dist/chunk-QM4SJKWV.js.map +0 -1
  80. package/dist/chunk-QNHVPE2B.js +0 -117
  81. package/dist/chunk-QNHVPE2B.js.map +0 -1
  82. package/dist/chunk-RJEUQ7RL.js.map +0 -1
  83. package/dist/chunk-ZI3OQDET.js.map +0 -1
  84. package/dist/feather-FNBQ74RR.js.map +0 -1
  85. package/dist/heroicons-DODCSYWD.js.map +0 -1
  86. package/dist/lucide-ZWE3EPGP.js.map +0 -1
  87. package/dist/phosphor-DIHPA4AJ.js.map +0 -1
  88. 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,12 +1,15 @@
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-Z4MCD73B.js';
3
+ import { useFillHeight, FillContext, useKeyboardScroll } from './chunk-RHWTMTU5.js';
4
+ import { Badge } from './chunk-NLXQ323J.js';
5
+ import { useFramesItself, Button } from './chunk-Q36JVJX4.js';
6
+ import './chunk-NKUCS67T.js';
7
+ import './chunk-ZHUVLBCC.js';
5
8
  import { useRevealFocus } from './chunk-HTXP5BMM.js';
6
- import './chunk-ZI3OQDET.js';
7
- import { useTextOverflow, AutoTooltip } from './chunk-37WOC7F7.js';
9
+ import './chunk-OIOII5L6.js';
10
+ import { useTextOverflow, AutoTooltip } from './chunk-GQVBZ4GW.js';
8
11
  import './chunk-7Q46JXSK.js';
9
- import { Icon } from './chunk-MKFWHWBK.js';
12
+ import { Icon } from './chunk-L6HWACK7.js';
10
13
  import './chunk-HP7WUZPK.js';
11
14
  import './chunk-WH2FUFAC.js';
12
15
  import { cn, FOCUS_INSET, FOCUS_RING } from './chunk-JTU7OBYJ.js';
@@ -16,11 +19,33 @@ import { sortableKeyboardCoordinates, SortableContext, verticalListSortingStrate
16
19
  import { CSS } from '@dnd-kit/utilities';
17
20
  import { jsxs, jsx } from 'react/jsx-runtime';
18
21
 
22
+ var MOVE_ROOM_TITLE = "pr-[var(--ds-control-height-sm)]";
23
+ var MOVE_ROOM_CARD = "pr-[calc(var(--ds-space-3)+var(--ds-control-height-sm))]";
24
+ function cardMenuOf(card, moveEntries, cardActions) {
25
+ if (!cardActions) return moveEntries;
26
+ const move = { label: "Move", items: moveEntries };
27
+ const own = cardActions(card, move);
28
+ const isMove = (e) => e === move || "items" in e && e.items === move.items;
29
+ const divider = { type: "divider" };
30
+ const entries = moveEntries.length === 0 ? own.filter((e) => !isMove(e)) : own.some(isMove) ? own : [...own, divider, move];
31
+ const isDivider = (e) => !!e && "type" in e && e.type === "divider";
32
+ 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));
33
+ }
34
+ function menuOf(moves, choose) {
35
+ const entry = (m) => ({ label: m.label, onClick: () => choose(m) });
36
+ const others = moves.filter((m) => m.step === 0).map(entry);
37
+ const steps = moves.filter((m) => m.step !== 0).map(entry);
38
+ return others.length && steps.length ? [...others, { type: "divider" }, ...steps] : [...others, ...steps];
39
+ }
40
+ var MOVE_PLACE = "absolute top-ds-1.5 right-ds-1.5";
19
41
  function SortableCard({
20
42
  card,
21
43
  renderCard,
22
44
  onCardClick,
23
- readOnly
45
+ readOnly,
46
+ moves,
47
+ onMove,
48
+ cardActions
24
49
  }) {
25
50
  const {
26
51
  attributes,
@@ -30,9 +55,34 @@ function SortableCard({
30
55
  transition,
31
56
  isDragging
32
57
  } = useSortable({ id: card.id, disabled: readOnly });
58
+ const entries = cardMenuOf(card, readOnly ? [] : menuOf(moves, (m) => onMove(card.id, m.to, m.step)), cardActions);
59
+ const renderMenu = (style2, faded) => /* @__PURE__ */ jsx(
60
+ DropdownMenu,
61
+ {
62
+ items: entries,
63
+ trigger: (
64
+ // the library's icon button: its name says what its menu is for, and it shows that name as its one tooltip;
65
+ // shown at all times, since a phone has no hover; quiet, as a Card's ⋯ is
66
+ /* @__PURE__ */ jsx(
67
+ Button,
68
+ {
69
+ type: "button",
70
+ variant: "ghost",
71
+ size: "icon-sm",
72
+ "aria-label": cardActions ? `Actions for ${card.title}` : `Move ${card.title}`,
73
+ "data-kanban-move": card.id,
74
+ style: style2,
75
+ className: cn(MOVE_PLACE, "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)]", faded && "opacity-30"),
76
+ children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: 14 })
77
+ }
78
+ )
79
+ )
80
+ }
81
+ );
82
+ const menu = entries.length > 0 ? renderMenu : null;
33
83
  if (readOnly) {
34
84
  const open = onCardClick ? () => onCardClick(card) : void 0;
35
- return /* @__PURE__ */ jsx(
85
+ const surface = /* @__PURE__ */ jsx(
36
86
  "div",
37
87
  {
38
88
  ref: setNodeRef,
@@ -48,47 +98,63 @@ function SortableCard({
48
98
  className: cn(
49
99
  "rounded-[var(--ds-radius-md)] border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
50
100
  "bg-[var(--ds-bg-surface)] p-ds-3 shadow-[var(--ds-shadow-button)]",
101
+ // a card with its actions' ⋯ keeps the button's room clear, as a card that moves does
102
+ menu && renderCard && MOVE_ROOM_CARD,
51
103
  open ? "cursor-pointer" : "cursor-default",
52
104
  FOCUS_RING
53
105
  ),
54
- children: renderCard ? renderCard(card) : /* @__PURE__ */ jsx(DefaultCard, { card })
106
+ children: renderCard ? renderCard(card) : /* @__PURE__ */ jsx(DefaultCard, { card, moveRoom: !!menu })
55
107
  }
56
108
  );
109
+ return menu ? /* @__PURE__ */ jsxs("div", { className: "relative", "data-kanban-item": card.id, children: [
110
+ surface,
111
+ menu(void 0, false)
112
+ ] }) : surface;
57
113
  }
58
114
  const style = {
59
115
  transform: CSS.Transform.toString(transform),
60
116
  transition
61
117
  };
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;
118
+ const movable = !!menu;
119
+ return (
120
+ // 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
121
+ // inside the other (a control inside a control: the card is the drag's button). The card stays the node the drag
122
+ // measures and moves; the button takes the same transform, so it travels with the card as the list sorts.
123
+ /* @__PURE__ */ jsxs("div", { className: "relative", "data-kanban-item": card.id, children: [
124
+ /* @__PURE__ */ jsx(
125
+ "div",
126
+ {
127
+ ref: setNodeRef,
128
+ style,
129
+ ...attributes,
130
+ ...listeners,
131
+ onClick: onCardClick ? () => onCardClick(card) : void 0,
132
+ onKeyDown: (e) => {
133
+ if (onCardClick && e.key === "Enter") {
134
+ e.preventDefault();
135
+ onCardClick(card);
136
+ return;
137
+ }
138
+ listeners?.onKeyDown?.(e);
139
+ },
140
+ "data-kanban-card": card.id,
141
+ className: cn(
142
+ "rounded-[var(--ds-radius-md)] border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
143
+ "bg-[var(--ds-bg-surface)] p-ds-3 active:cursor-grabbing",
144
+ movable && renderCard && MOVE_ROOM_CARD,
145
+ onCardClick ? "cursor-pointer" : "cursor-grab",
146
+ "shadow-[var(--ds-shadow-button)]",
147
+ "transition-shadow duration-[var(--ds-transition-speed)]",
148
+ FOCUS_RING,
149
+ // no scale on the card left behind: the drag overlay takes its size from it, and 2% wider made the cards
150
+ // below count as "to the right" of it, so a keyboard drag's arrow keys landed on them (Sprint 25.7, C2)
151
+ isDragging && "opacity-30"
152
+ ),
153
+ children: renderCard ? renderCard(card) : /* @__PURE__ */ jsx(DefaultCard, { card, moveRoom: movable })
75
154
  }
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
155
  ),
90
- children: renderCard ? renderCard(card) : /* @__PURE__ */ jsx(DefaultCard, { card })
91
- }
156
+ menu && menu(style, isDragging)
157
+ ] })
92
158
  );
93
159
  }
94
160
  function ClampedDescription({ children }) {
@@ -97,31 +163,36 @@ function ClampedDescription({ children }) {
97
163
  if (!isTruncated) return el;
98
164
  return /* @__PURE__ */ jsx(AutoTooltip, { content: children, children: el });
99
165
  }
100
- function TruncTitle({ children }) {
166
+ function TruncTitle({ children, className }) {
101
167
  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 });
168
+ const el = /* @__PURE__ */ jsx("p", { ref, className: cn("text-sm font-medium text-[var(--ds-text-primary)] truncate", className), children });
103
169
  if (!isTruncated) return el;
104
170
  return /* @__PURE__ */ jsx(AutoTooltip, { content: children, children: el });
105
171
  }
106
- function DefaultCard({ card }) {
172
+ function DefaultCard({ card, moveRoom }) {
107
173
  return /* @__PURE__ */ jsxs("div", { className: "space-y-ds-2", children: [
108
- /* @__PURE__ */ jsx(TruncTitle, { children: card.title }),
174
+ /* @__PURE__ */ jsx(TruncTitle, { className: moveRoom ? MOVE_ROOM_TITLE : void 0, children: card.title }),
109
175
  card.description && /* @__PURE__ */ jsx(ClampedDescription, { children: card.description }),
110
176
  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
177
  ] });
112
178
  }
113
179
  function OverlayCard({
114
180
  card,
115
- renderCard
181
+ renderCard,
182
+ moveRoom
116
183
  }) {
117
- return /* @__PURE__ */ jsx(
184
+ return /* @__PURE__ */ jsxs(
118
185
  "div",
119
186
  {
120
187
  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)]"
188
+ "relative rounded-[var(--ds-radius-md)] border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
189
+ "bg-[var(--ds-bg-surface)] p-ds-3 shadow-[var(--ds-shadow-card)]",
190
+ moveRoom && renderCard && MOVE_ROOM_CARD
123
191
  ),
124
- children: renderCard ? renderCard(card) : /* @__PURE__ */ jsx(DefaultCard, { card })
192
+ children: [
193
+ renderCard ? renderCard(card) : /* @__PURE__ */ jsx(DefaultCard, { card, moveRoom }),
194
+ 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 }) })
195
+ ]
125
196
  }
126
197
  );
127
198
  }
@@ -194,6 +265,21 @@ function keptHolds(holds, cols) {
194
265
  var replayHolds = (cols, holds) => holds.reduce(place, cols);
195
266
  var isThenable = (value) => value != null && typeof value.then === "function";
196
267
  var placeWords = (at) => `${at.col.title}, position ${at.index + 1} of ${at.col.cards.length}`;
268
+ var overLimit = (col) => col.limit != null && col.cards.length > col.limit;
269
+ var overLimitWords = (col) => overLimit(col) ? ` ${col.title} is over its limit of ${col.limit}.` : "";
270
+ var countWords = (count, limit) => `${count} ${count === 1 ? "card" : "cards"}, ${count > limit ? `over the limit of ${limit}` : `limit ${limit}`}`;
271
+ function ColumnCount({ count, limit }) {
272
+ if (limit == null) return /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--ds-text-muted)] tabular-nums", children: count });
273
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-ds-1 text-xs text-[var(--ds-text-muted)] tabular-nums", children: [
274
+ /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", children: [
275
+ count,
276
+ "/",
277
+ limit
278
+ ] }),
279
+ 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 }) }),
280
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: countWords(count, limit) })
281
+ ] });
282
+ }
197
283
  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
284
  function BoardColumn({
199
285
  col,
@@ -228,7 +314,7 @@ function BoardColumn({
228
314
  }
229
315
  ),
230
316
  /* @__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 })
317
+ /* @__PURE__ */ jsx(ColumnCount, { count: col.cards.length, limit: col.limit })
232
318
  ] }),
233
319
  /* @__PURE__ */ jsx(Icon, { name: "plus", size: 14, className: "text-[var(--ds-text-muted)]" })
234
320
  ] }),
@@ -266,6 +352,7 @@ function KanbanBoard({
266
352
  readOnly,
267
353
  renderCard,
268
354
  onCardClick,
355
+ cardActions,
269
356
  className
270
357
  }) {
271
358
  useFramesItself();
@@ -329,13 +416,13 @@ function KanbanBoard({
329
416
  const fill = useFillHeight(rootRef);
330
417
  const inPane = useContext(FillContext);
331
418
  const filled = fill != null || inPane;
419
+ const cardPart = (attr, id) => [...rootRef.current?.querySelectorAll(`[${attr}]`) ?? []].find((el) => el.getAttribute(attr) === id);
332
420
  const refocus = useRef(null);
333
421
  useLayoutEffect(() => {
334
- const id = refocus.current;
335
- if (id == null) return;
422
+ const target = refocus.current;
423
+ if (target == null) return;
336
424
  refocus.current = null;
337
- const cards = rootRef.current?.querySelectorAll("[data-kanban-card]") ?? [];
338
- [...cards].find((el) => el.getAttribute("data-kanban-card") === id)?.focus();
425
+ cardPart(target.part === "move" ? "data-kanban-move" : "data-kanban-card", target.id)?.focus();
339
426
  });
340
427
  const holdKey = useRef(0);
341
428
  const sensors = useSensors(
@@ -389,8 +476,11 @@ function KanbanBoard({
389
476
  const back = locate(shown, hold.cardId);
390
477
  const card = back?.col.cards[back.index];
391
478
  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;
479
+ const inSlot = !!focused && !!rootRef.current?.contains(focused) && focused.closest?.("[data-kanban-item]")?.getAttribute("data-kanban-item") === hold.cardId;
480
+ const menuOpen = cardPart("data-kanban-move", hold.cardId)?.getAttribute("aria-expanded") === "true";
481
+ if (inSlot) refocus.current = { id: hold.cardId, part: focused?.hasAttribute("data-kanban-move") ? "move" : "card" };
482
+ else if (menuOpen && locate(replayHolds(columnsRef.current, holdsRef.current), hold.cardId)?.col.id !== back?.col.id) {
483
+ refocus.current = { id: hold.cardId, part: "move" };
394
484
  }
395
485
  setHolds(rest);
396
486
  if (previewRef.current && activeRef.current) setPreview(rebase(shown, previewRef.current, activeRef.current));
@@ -406,32 +496,20 @@ function KanbanBoard({
406
496
  const next = holdsRef.current.map((h) => h === hold ? { ...h, inFlight, layout: inFlight > 0 ? h.layout : layoutOf(columnsRef.current) } : h);
407
497
  setHolds(inFlight > 0 ? next : keptHolds(next, columnsRef.current));
408
498
  };
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)}.`;
499
+ const commit = (final, cardId) => {
500
+ const after = locate(final, cardId);
501
+ if (!after) return;
424
502
  const key = ++holdKey.current;
425
- const prev = holdsRef.current.find((h) => h.cardId === activeId);
503
+ const prev = holdsRef.current.find((h) => h.cardId === cardId);
426
504
  const hold = {
427
- ...placementIn(final, activeId),
505
+ ...placementIn(final, cardId),
428
506
  key,
429
507
  inFlight: prev?.inFlight ?? 0,
430
- origin: prev?.origin ?? placementIn(columnsProp, activeId) ?? placementIn(committed, activeId),
508
+ origin: prev?.origin ?? placementIn(columnsProp, cardId) ?? placementIn(committed, cardId),
431
509
  earlier: prev ? [...prev.earlier, { cardId: prev.cardId, toColumnId: prev.toColumnId, beforeId: prev.beforeId, index: prev.index }] : [],
432
510
  layout: layoutOf(columnsProp)
433
511
  };
434
- setHolds([...holdsRef.current.filter((h) => h.cardId !== activeId), hold]);
512
+ setHolds([...holdsRef.current.filter((h) => h.cardId !== cardId), hold]);
435
513
  const saves = [];
436
514
  try {
437
515
  for (const result of [onColumnsChange?.(final), onCardMove?.(after.col.cards[after.index], after.col.id, after.index)]) {
@@ -443,8 +521,51 @@ function KanbanBoard({
443
521
  }
444
522
  if (!saves.length) return;
445
523
  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")));
524
+ Promise.allSettled(saves).then((results) => settle(cardId, key, results.every((r) => r.status === "fulfilled")));
525
+ };
526
+ const handleDragEnd = (event) => {
527
+ const { active, over } = event;
528
+ const activeId = String(active.id);
529
+ const cols = previewRef.current ?? committed;
530
+ activeRef.current = null;
531
+ setActiveCard(null);
532
+ setPreview(null);
533
+ const final = over ? landing(cols, activeId, String(over.id)) : committed;
534
+ const before = locate(committed, activeId);
535
+ const after = locate(final, activeId);
536
+ if (!before || !after || before.col.id === after.col.id && before.index === after.index) {
537
+ if (before) said.current = `Dropped ${before.col.cards[before.index].title} back in ${placeWords(before)}.`;
538
+ return;
539
+ }
540
+ said.current = `Dropped ${after.col.cards[after.index].title} in ${placeWords(after)}.${overLimitWords(after.col)}`;
541
+ commit(final, activeId);
542
+ };
543
+ const moveFromMenu = (cardId, toColumnId, step = 0) => {
544
+ const at = locate(committed, cardId);
545
+ const to = committed.find((col) => col.id === toColumnId);
546
+ if (!at || !to) return;
547
+ const index = to.id === at.col.id ? at.index + step : 0;
548
+ const beforeId = to.cards.filter((c) => c.id !== cardId)[index]?.id ?? null;
549
+ const final = place(committed, { cardId, toColumnId, beforeId, index });
550
+ const after = locate(final, cardId);
551
+ if (!after || after.col.id === at.col.id && after.index === at.index) return;
552
+ noticeParts.current = [];
553
+ setNotice(`Moved ${at.col.cards[at.index].title} to ${placeWords(after)}.${overLimitWords(after.col)}`);
554
+ commit(final, cardId);
555
+ refocus.current = { id: cardId, part: "card" };
556
+ };
557
+ const movesFor = (col, index) => {
558
+ if (readOnly) return [];
559
+ const others = columns.filter((c) => c.id !== col.id).map((c) => ({ label: `Move to ${c.title}`, to: c.id, step: 0 }));
560
+ const steps = [
561
+ ...index > 0 ? [{ label: "Move up", to: col.id, step: -1 }] : [],
562
+ ...index < col.cards.length - 1 ? [{ label: "Move down", to: col.id, step: 1 }] : []
563
+ ];
564
+ return [...others, ...steps];
447
565
  };
566
+ const activeAt = activeCard ? locate(columns, activeCard.id) : null;
567
+ const activeMovable = !!activeAt && cardMenuOf(activeAt.col.cards[activeAt.index], menuOf(movesFor(activeAt.col, activeAt.index), () => {
568
+ }), cardActions).length > 0;
448
569
  const handleDragCancel = (event) => {
449
570
  activeRef.current = null;
450
571
  setActiveCard(null);
@@ -464,19 +585,22 @@ function KanbanBoard({
464
585
  onDragCancel: handleDragCancel,
465
586
  children: [
466
587
  /* @__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(
588
+ columns.map((col) => /* @__PURE__ */ jsx(BoardColumn, { col, readOnly, filled, children: col.cards.map((card, index) => /* @__PURE__ */ jsx(
468
589
  SortableCard,
469
590
  {
470
591
  card,
471
592
  renderCard,
472
593
  onCardClick,
473
- readOnly
594
+ readOnly,
595
+ moves: movesFor(col, index),
596
+ onMove: moveFromMenu,
597
+ cardActions
474
598
  },
475
599
  card.id
476
600
  )) }, col.id)),
477
601
  /* @__PURE__ */ jsx("div", { role: "status", className: "sr-only", children: notice })
478
602
  ] }),
479
- /* @__PURE__ */ jsx(DragOverlay, { children: activeCard && /* @__PURE__ */ jsx(OverlayCard, { card: activeCard, renderCard }) })
603
+ /* @__PURE__ */ jsx(DragOverlay, { children: activeCard && /* @__PURE__ */ jsx(OverlayCard, { card: activeCard, renderCard, moveRoom: activeMovable }) })
480
604
  ]
481
605
  }
482
606
  );