@charcuterie/ui 3.25.0 → 3.26.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/Board/BoardCard.d.ts +7 -0
- package/dist/Board/BoardCard.d.ts.map +1 -1
- package/dist/Board/BoardCard.js +33 -11
- package/dist/Board/BoardCard.js.map +1 -1
- package/package.json +1 -1
- package/src/Board/Board.mdx +5 -0
- package/src/Board/Board.stories.tsx +40 -0
- package/src/Board/Board.test.tsx +41 -0
- package/src/Board/BoardCard.tsx +49 -14
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IntentName } from "@charcuterie/tokens";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import type { CardAccentEdge } from "../Card/cardAccentEdge.ts";
|
|
4
|
+
import type { MenuProps } from "../Menu/Menu.tsx";
|
|
4
5
|
type BoardItemFields = {
|
|
5
6
|
/** What the bar means, in words. "Priority 0", "Movies and Shows". */
|
|
6
7
|
accentLabel?: string;
|
|
@@ -39,6 +40,12 @@ type BoardItemFields = {
|
|
|
39
40
|
title: string;
|
|
40
41
|
/** The trailing cluster: an assignee avatar, an elapsed-time chip. */
|
|
41
42
|
trailing?: ReactNode;
|
|
43
|
+
/**
|
|
44
|
+
* Extra actions in the card's existing menu, after the lane moves.
|
|
45
|
+
* A separator is added when both sets have entries, so a consumer
|
|
46
|
+
* can add a card action without creating a second trigger.
|
|
47
|
+
*/
|
|
48
|
+
menuItems?: MenuProps["items"];
|
|
42
49
|
};
|
|
43
50
|
/**
|
|
44
51
|
* The leading colour bar, in one of two shapes — and the second one
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BoardCard.d.ts","sourceRoot":"","sources":["../../src/Board/BoardCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;
|
|
1
|
+
{"version":3,"file":"BoardCard.d.ts","sourceRoot":"","sources":["../../src/Board/BoardCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAS/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAMjD,KAAK,eAAe,GAAG;IACrB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,sEAAsE;IACtE,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;CAC/B,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,KAAK,eAAe,GAChB;IAAE,UAAU,CAAC,EAAE,KAAK,CAAC;IAAC,YAAY,CAAC,EAAE,UAAU,CAAA;CAAE,GACjD;IAAE,UAAU,EAAE,cAAc,CAAC;IAAC,YAAY,CAAC,EAAE,KAAK,CAAA;CAAE,CAAA;AAExD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,KAAK,kBAAkB,GACnB;IACE,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,YAAY,CAAC,EAAE,KAAK,CAAA;CACrB,GACD;IACE,IAAI,CAAC,EAAE,KAAK,CAAA;IACZ,QAAQ,CAAC,EAAE,KAAK,CAAA;IAChB;;;;;;;;OAQG;IACH,YAAY,EAAE,SAAS,CAAA;CACxB,CAAA;AAEL,MAAM,MAAM,SAAS,GAAG,eAAe,GACrC,eAAe,GACf,kBAAkB,CAAA;AAEpB,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,SAAS,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,0DAA0D;IAC1D,WAAW,EAAE,SAAS;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACtD,8DAA8D;IAC9D,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,WAAW,CAAC,EAAE,CAAC,YAAY,EAAE;QAC3B,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;QACf,aAAa,EAAE,WAAW,GAAG,OAAO,CAAA;QACpC,SAAS,EAAE,MAAM,CAAA;KAClB,KAAK,IAAI,CAAA;CACX,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AACH,eAAO,MAAM,SAAS,GAAI,wEAOvB,cAAc,KAAG,SAiQnB,CAAA"}
|
package/dist/Board/BoardCard.js
CHANGED
|
@@ -79,6 +79,30 @@ import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.js";
|
|
|
79
79
|
*/
|
|
80
80
|
export const BoardCard = ({ item, laneLabel, moveIcon, moveTargets, onMoveToLane, onStartDrag, }) => {
|
|
81
81
|
const [isMenuVisible, setIsMenuVisible] = useState(false);
|
|
82
|
+
const isMovable = onMoveToLane != null && moveTargets.length > 0;
|
|
83
|
+
const hasMenuItems = (item.menuItems?.length ?? 0) > 0;
|
|
84
|
+
const hasMenu = onMoveToLane != null && (isMovable || hasMenuItems);
|
|
85
|
+
const menuItems = [
|
|
86
|
+
...(isMovable
|
|
87
|
+
? moveTargets.map((target) => ({
|
|
88
|
+
key: target.key,
|
|
89
|
+
label: target.label,
|
|
90
|
+
onSelect: () => {
|
|
91
|
+
onMoveToLane?.(target.key);
|
|
92
|
+
},
|
|
93
|
+
}))
|
|
94
|
+
: []),
|
|
95
|
+
...(hasMenuItems && isMovable
|
|
96
|
+
? [
|
|
97
|
+
{
|
|
98
|
+
key: "board-extra-actions",
|
|
99
|
+
type: "separator",
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
: []),
|
|
103
|
+
...(item.menuItems ?? []),
|
|
104
|
+
];
|
|
105
|
+
const menuLabel = isMovable ? "Move" : "More";
|
|
82
106
|
const titleBlock = (_jsx("span", { className: "line-clamp-2 cq-sm:line-clamp-none cq-sm:block cq-sm:truncate", children: item.titleContent ?? item.title }));
|
|
83
107
|
return (_jsxs("li", { className: toClassName("group relative flex gap-2 border-border-subtle border-b py-2 pe-1 last:border-b-0",
|
|
84
108
|
// At `cq-lg` the list stops being a list of rows: the
|
|
@@ -100,13 +124,7 @@ export const BoardCard = ({ item, laneLabel, moveIcon, moveTargets, onMoveToLane
|
|
|
100
124
|
item.accentEdge != null &&
|
|
101
125
|
toClassName("ps-2", getAccentEdgeClassName(item.accentEdge)), item.isDimmed && "opacity-60"), "data-board-card": item.key, ...(item.accentEdge == null
|
|
102
126
|
? {}
|
|
103
|
-
: { style: getAccentEdgeStyle(item.accentEdge) }), children: [item.accentEdge != null && item.accentLabel ? (_jsx(VisuallyHidden, { children: item.accentLabel })) : null, item.accentIntent ? (_jsx("span", { className: toClassName("w-1 shrink-0 self-stretch rounded-full", INTENT_SOLID_FILL_CLASS[item.accentIntent]), children: item.accentLabel ? (_jsx(VisuallyHidden, { children: item.accentLabel })) : null })) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1 cq-sm:flex-row cq-sm:items-center cq-sm:gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-start gap-1.5 text-sm leading-snug", children: [item.marks ? (_jsx("span", { className: "flex shrink-0 items-center gap-1 pt-1", children: item.marks })) : null, item.href ? (_jsx(TextLink, { appearance: "standalone", className: "min-w-0 max-w-full font-normal", href: item.href, intent: "neutral", children: titleBlock })) : item.onSelect ? (_jsx("button", { className: toClassName("min-w-0 max-w-full cursor-pointer rounded-xs text-start text-intent-neutral-content hover:underline", FOCUS_RING_CLASS), onClick: item.onSelect, type: "button", children: titleBlock })) : (titleBlock)] }), item.meta ? (_jsx("div", { className: "flex min-w-0 flex-wrap items-center gap-1 cq-sm:shrink-0 cq-sm:flex-nowrap cq-sm:justify-end", children: item.meta })) : null] }), item.trailing ||
|
|
104
|
-
key: target.key,
|
|
105
|
-
label: target.label,
|
|
106
|
-
onSelect: () => {
|
|
107
|
-
onMoveToLane(target.key);
|
|
108
|
-
},
|
|
109
|
-
})), onDismiss: () => {
|
|
127
|
+
: { style: getAccentEdgeStyle(item.accentEdge) }), children: [item.accentEdge != null && item.accentLabel ? (_jsx(VisuallyHidden, { children: item.accentLabel })) : null, item.accentIntent ? (_jsx("span", { className: toClassName("w-1 shrink-0 self-stretch rounded-full", INTENT_SOLID_FILL_CLASS[item.accentIntent]), children: item.accentLabel ? (_jsx(VisuallyHidden, { children: item.accentLabel })) : null })) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1 cq-sm:flex-row cq-sm:items-center cq-sm:gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-start gap-1.5 text-sm leading-snug", children: [item.marks ? (_jsx("span", { className: "flex shrink-0 items-center gap-1 pt-1", children: item.marks })) : null, item.href ? (_jsx(TextLink, { appearance: "standalone", className: "min-w-0 max-w-full font-normal", href: item.href, intent: "neutral", children: titleBlock })) : item.onSelect ? (_jsx("button", { className: toClassName("min-w-0 max-w-full cursor-pointer rounded-xs text-start text-intent-neutral-content hover:underline", FOCUS_RING_CLASS), onClick: item.onSelect, type: "button", children: titleBlock })) : (titleBlock)] }), item.meta ? (_jsx("div", { className: "flex min-w-0 flex-wrap items-center gap-1 cq-sm:shrink-0 cq-sm:flex-nowrap cq-sm:justify-end", children: item.meta })) : null] }), item.trailing || hasMenu ? (_jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [item.trailing, hasMenu ? (_jsx(Menu, { isVisible: isMenuVisible, items: menuItems, onDismiss: () => {
|
|
110
128
|
setIsMenuVisible(false);
|
|
111
129
|
}, placement: "bottom-end", trigger: _jsxs(Button, { appearance: "ghost", intent: "neutral", className: toClassName(
|
|
112
130
|
// `touch-none` so a touch drag is a drag
|
|
@@ -131,9 +149,13 @@ export const BoardCard = ({ item, laneLabel, moveIcon, moveTargets, onMoveToLane
|
|
|
131
149
|
moveIcon != null &&
|
|
132
150
|
"@min-[48rem]/board:w-(--control-height-sm) @min-[48rem]/board:px-0"), onClick: () => {
|
|
133
151
|
setIsMenuVisible((isVisible) => !isVisible);
|
|
134
|
-
}, onPointerDown:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
152
|
+
}, onPointerDown: isMovable
|
|
153
|
+
? (pointerEvent) => {
|
|
154
|
+
onStartDrag?.(pointerEvent);
|
|
155
|
+
}
|
|
156
|
+
: undefined, size: "sm", sizing: "control", children: [moveIcon == null ? null : (_jsx("span", { "aria-hidden": "true", className: "hidden @min-[48rem]/board:contents", children: moveIcon })), _jsx("span", { "aria-hidden": "true", className: toClassName(moveIcon != null &&
|
|
157
|
+
"@min-[48rem]/board:hidden"), children: menuLabel }), _jsx(VisuallyHidden, { children: isMovable
|
|
158
|
+
? `Move ${item.title}, currently in ${laneLabel}`
|
|
159
|
+
: `More actions for ${item.title}, currently in ${laneLabel}` })] }) })) : null] })) : null] }), item.footer ? (_jsx("div", { className: "min-w-0 text-content-secondary text-xs", children: item.footer })) : null] })] }));
|
|
138
160
|
};
|
|
139
161
|
//# sourceMappingURL=BoardCard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BoardCard.js","sourceRoot":"","sources":["../../src/Board/BoardCard.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAsB,CAAA;AAE7C,OAAO,EACL,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACL,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"BoardCard.js","sourceRoot":"","sources":["../../src/Board/BoardCard.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/Board/Board.mdx
CHANGED
|
@@ -11,6 +11,8 @@ Lanes of cards, sized by their **container**, moved by keyboard or by pointer
|
|
|
11
11
|
|
|
12
12
|
<Canvas of={BoardStories.Default} />
|
|
13
13
|
|
|
14
|
+
<Canvas of={BoardStories.CardMenuItems} />
|
|
15
|
+
|
|
14
16
|
## Props
|
|
15
17
|
|
|
16
18
|
<Controls />
|
|
@@ -84,6 +86,9 @@ The move is one control with two drivers, and they commit through the same `onMo
|
|
|
84
86
|
code path, with `setPointerCapture` so the drag survives the pointer leaving the card.
|
|
85
87
|
Escape abandons it.
|
|
86
88
|
|
|
89
|
+
An item can append actions to that existing menu with `menuItems`. The Board inserts a
|
|
90
|
+
separator after the lane destinations, so a card action does not need a second trigger.
|
|
91
|
+
|
|
87
92
|
<Canvas of={BoardStories.Interactive} />
|
|
88
93
|
|
|
89
94
|
Every move is announced in a `role="status"` region that names the **destination and the
|
|
@@ -750,6 +750,46 @@ export const Interactive: Story = {
|
|
|
750
750
|
render: () => <MovableBoard />,
|
|
751
751
|
}
|
|
752
752
|
|
|
753
|
+
/**
|
|
754
|
+
* A consumer can add a card action to the same menu as the lane
|
|
755
|
+
* moves. The separator is supplied by `BoardCard`, so the action
|
|
756
|
+
* does not need a second trigger beside the existing one.
|
|
757
|
+
*/
|
|
758
|
+
export const CardMenuItems: Story = {
|
|
759
|
+
render: () => (
|
|
760
|
+
<Frame inlineSize="72rem">
|
|
761
|
+
<Board
|
|
762
|
+
label="Today, with card actions"
|
|
763
|
+
lanes={[
|
|
764
|
+
{
|
|
765
|
+
items: [
|
|
766
|
+
{
|
|
767
|
+
...TODO_ITEMS[0],
|
|
768
|
+
menuItems: [
|
|
769
|
+
{
|
|
770
|
+
key: "send-to-backlog",
|
|
771
|
+
label: "Send to Backlog",
|
|
772
|
+
onSelect: () => undefined,
|
|
773
|
+
},
|
|
774
|
+
],
|
|
775
|
+
},
|
|
776
|
+
],
|
|
777
|
+
key: "todo",
|
|
778
|
+
label: "Todo",
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
items: IN_PROGRESS_ITEMS.slice(0, 1),
|
|
782
|
+
key: "in-progress",
|
|
783
|
+
label: "In Progress",
|
|
784
|
+
},
|
|
785
|
+
]}
|
|
786
|
+
moveIcon={<MoreIcon />}
|
|
787
|
+
onMove={() => undefined}
|
|
788
|
+
/>
|
|
789
|
+
</Frame>
|
|
790
|
+
),
|
|
791
|
+
}
|
|
792
|
+
|
|
753
793
|
/**
|
|
754
794
|
* Where it actually goes, and what the consumer still owns.
|
|
755
795
|
*
|
package/src/Board/Board.test.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import * as stories from "./Board.stories.tsx"
|
|
|
15
15
|
const {
|
|
16
16
|
AccentEdge,
|
|
17
17
|
AllStates,
|
|
18
|
+
CardMenuItems,
|
|
18
19
|
Default,
|
|
19
20
|
InBoardScreen,
|
|
20
21
|
Interactive,
|
|
@@ -408,6 +409,46 @@ test("the board adds no link of its own around a rich title", async () => {
|
|
|
408
409
|
)
|
|
409
410
|
})
|
|
410
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Extra card actions share the existing move menu. A second
|
|
414
|
+
* trigger would make the card look busy and would split related
|
|
415
|
+
* actions across two controls, so the component owns the separator
|
|
416
|
+
* between the lane destinations and the consumer's items.
|
|
417
|
+
*/
|
|
418
|
+
test("a card can append an action to its existing menu", async () => {
|
|
419
|
+
const { body, canvas } = await mountStory(CardMenuItems)
|
|
420
|
+
|
|
421
|
+
const todo = expectAgentDrivable(canvas, {
|
|
422
|
+
name: "Todo",
|
|
423
|
+
role: "group",
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
await userEvent.click(
|
|
427
|
+
within(todo).getByRole("button", {
|
|
428
|
+
name: /^Move Retire the second scheduler/,
|
|
429
|
+
}),
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
const menu = await waitFor(() =>
|
|
433
|
+
expectAgentDrivable(body, {
|
|
434
|
+
name: /Move Retire the second scheduler/,
|
|
435
|
+
role: "menu",
|
|
436
|
+
}),
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
expect(
|
|
440
|
+
within(menu).getByRole("menuitem", {
|
|
441
|
+
name: "In Progress",
|
|
442
|
+
}),
|
|
443
|
+
).toBeInTheDocument()
|
|
444
|
+
expect(
|
|
445
|
+
within(menu).getByRole("menuitem", {
|
|
446
|
+
name: "Send to Backlog",
|
|
447
|
+
}),
|
|
448
|
+
).toBeInTheDocument()
|
|
449
|
+
expect(menu.querySelector("hr")).not.toBeNull()
|
|
450
|
+
})
|
|
451
|
+
|
|
411
452
|
/**
|
|
412
453
|
* The edge is drawn on the card's OWN box, which is the whole
|
|
413
454
|
* difference between it and the `accentIntent` pill.
|
package/src/Board/BoardCard.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
FOCUS_RING_CLASS,
|
|
13
13
|
INTENT_SOLID_FILL_CLASS,
|
|
14
14
|
} from "../intentStyles.ts"
|
|
15
|
+
import type { MenuProps } from "../Menu/Menu.tsx"
|
|
15
16
|
import { Menu } from "../Menu/Menu.tsx"
|
|
16
17
|
import { TextLink } from "../TextLink/TextLink.tsx"
|
|
17
18
|
import { toClassName } from "../toClassName.ts"
|
|
@@ -55,6 +56,12 @@ type BoardItemFields = {
|
|
|
55
56
|
title: string
|
|
56
57
|
/** The trailing cluster: an assignee avatar, an elapsed-time chip. */
|
|
57
58
|
trailing?: ReactNode
|
|
59
|
+
/**
|
|
60
|
+
* Extra actions in the card's existing menu, after the lane moves.
|
|
61
|
+
* A separator is added when both sets have entries, so a consumer
|
|
62
|
+
* can add a card action without creating a second trigger.
|
|
63
|
+
*/
|
|
64
|
+
menuItems?: MenuProps["items"]
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
/**
|
|
@@ -251,6 +258,34 @@ export const BoardCard = ({
|
|
|
251
258
|
}: BoardCardProps): ReactNode => {
|
|
252
259
|
const [isMenuVisible, setIsMenuVisible] = useState(false)
|
|
253
260
|
|
|
261
|
+
const isMovable =
|
|
262
|
+
onMoveToLane != null && moveTargets.length > 0
|
|
263
|
+
const hasMenuItems = (item.menuItems?.length ?? 0) > 0
|
|
264
|
+
const hasMenu =
|
|
265
|
+
onMoveToLane != null && (isMovable || hasMenuItems)
|
|
266
|
+
const menuItems: MenuProps["items"] = [
|
|
267
|
+
...(isMovable
|
|
268
|
+
? moveTargets.map((target) => ({
|
|
269
|
+
key: target.key,
|
|
270
|
+
label: target.label,
|
|
271
|
+
onSelect: () => {
|
|
272
|
+
onMoveToLane?.(target.key)
|
|
273
|
+
},
|
|
274
|
+
}))
|
|
275
|
+
: []),
|
|
276
|
+
...(hasMenuItems && isMovable
|
|
277
|
+
? [
|
|
278
|
+
{
|
|
279
|
+
key: "board-extra-actions",
|
|
280
|
+
type: "separator" as const,
|
|
281
|
+
},
|
|
282
|
+
]
|
|
283
|
+
: []),
|
|
284
|
+
...(item.menuItems ?? []),
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
const menuLabel = isMovable ? "Move" : "More"
|
|
288
|
+
|
|
254
289
|
const titleBlock = (
|
|
255
290
|
// `line-clamp-none` before `truncate`, because `line-clamp-2`
|
|
256
291
|
// sets `display: -webkit-box` and `text-overflow` has nothing to
|
|
@@ -359,20 +394,14 @@ export const BoardCard = ({
|
|
|
359
394
|
) : null}
|
|
360
395
|
</div>
|
|
361
396
|
|
|
362
|
-
{item.trailing ||
|
|
397
|
+
{item.trailing || hasMenu ? (
|
|
363
398
|
<div className="flex shrink-0 items-center gap-1">
|
|
364
399
|
{item.trailing}
|
|
365
400
|
|
|
366
|
-
{
|
|
401
|
+
{hasMenu ? (
|
|
367
402
|
<Menu
|
|
368
403
|
isVisible={isMenuVisible}
|
|
369
|
-
items={
|
|
370
|
-
key: target.key,
|
|
371
|
-
label: target.label,
|
|
372
|
-
onSelect: () => {
|
|
373
|
-
onMoveToLane(target.key)
|
|
374
|
-
},
|
|
375
|
-
}))}
|
|
404
|
+
items={menuItems}
|
|
376
405
|
onDismiss={() => {
|
|
377
406
|
setIsMenuVisible(false)
|
|
378
407
|
}}
|
|
@@ -409,9 +438,13 @@ export const BoardCard = ({
|
|
|
409
438
|
(isVisible) => !isVisible,
|
|
410
439
|
)
|
|
411
440
|
}}
|
|
412
|
-
onPointerDown={
|
|
413
|
-
|
|
414
|
-
|
|
441
|
+
onPointerDown={
|
|
442
|
+
isMovable
|
|
443
|
+
? (pointerEvent) => {
|
|
444
|
+
onStartDrag?.(pointerEvent)
|
|
445
|
+
}
|
|
446
|
+
: undefined
|
|
447
|
+
}
|
|
415
448
|
size="sm"
|
|
416
449
|
sizing="control"
|
|
417
450
|
>
|
|
@@ -441,7 +474,7 @@ export const BoardCard = ({
|
|
|
441
474
|
"@min-[48rem]/board:hidden",
|
|
442
475
|
)}
|
|
443
476
|
>
|
|
444
|
-
|
|
477
|
+
{menuLabel}
|
|
445
478
|
</span>
|
|
446
479
|
|
|
447
480
|
{/*
|
|
@@ -459,7 +492,9 @@ export const BoardCard = ({
|
|
|
459
492
|
* X" narrow is one control with two names.
|
|
460
493
|
*/}
|
|
461
494
|
<VisuallyHidden>
|
|
462
|
-
{
|
|
495
|
+
{isMovable
|
|
496
|
+
? `Move ${item.title}, currently in ${laneLabel}`
|
|
497
|
+
: `More actions for ${item.title}, currently in ${laneLabel}`}
|
|
463
498
|
</VisuallyHidden>
|
|
464
499
|
</Button>
|
|
465
500
|
}
|