@charcuterie/ui 3.20.0 → 3.21.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 +42 -18
- package/dist/Board/BoardCard.d.ts.map +1 -1
- package/dist/Board/BoardCard.js +17 -1
- package/dist/Board/BoardCard.js.map +1 -1
- package/package.json +1 -1
- package/src/Board/Board.stories.tsx +100 -0
- package/src/Board/Board.test.tsx +62 -0
- package/src/Board/BoardCard.tsx +71 -18
|
@@ -1,23 +1,8 @@
|
|
|
1
1
|
import type { IntentName } from "@charcuterie/tokens";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
+
import type { CardAccentEdge } from "../Card/cardAccentEdge.ts";
|
|
3
4
|
type BoardItemFields = {
|
|
4
|
-
/**
|
|
5
|
-
* The leading colour bar. Priority, in the consumer this was built
|
|
6
|
-
* for — *"Priority is styled as a leading colour bar, and **never**
|
|
7
|
-
* a coloured word before the title."*
|
|
8
|
-
*
|
|
9
|
-
* Pair it with `accentLabel`. A bar is colour and nothing else, so
|
|
10
|
-
* on its own it fails WCAG 1.4.1 outright and is invisible to
|
|
11
|
-
* every screen reader; the label rides along in a
|
|
12
|
-
* `VisuallyHidden` so the same fact reaches both channels. Docket's
|
|
13
|
-
* first board painted these `--color-danger-9` — a Radix-style
|
|
14
|
-
* scale this token set has never had — so every bar was
|
|
15
|
-
* transparent while every "is it rendered" assertion passed. The
|
|
16
|
-
* families here are `--color-intent-<intent>-<role>`; there is no
|
|
17
|
-
* numeric scale to reach for.
|
|
18
|
-
*/
|
|
19
|
-
accentIntent?: IntentName;
|
|
20
|
-
/** What the bar means, in words. "Priority 0". */
|
|
5
|
+
/** What the bar means, in words. "Priority 0", "Movies and Shows". */
|
|
21
6
|
accentLabel?: string;
|
|
22
7
|
/**
|
|
23
8
|
* Under the title and metadata, spanning the card: a live run
|
|
@@ -55,6 +40,45 @@ type BoardItemFields = {
|
|
|
55
40
|
/** The trailing cluster: an assignee avatar, an elapsed-time chip. */
|
|
56
41
|
trailing?: ReactNode;
|
|
57
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* The leading colour bar, in one of two shapes — and the second one
|
|
45
|
+
* exists because the first cannot follow a corner.
|
|
46
|
+
*
|
|
47
|
+
* `accentIntent` is an inset pill: a `w-1` span inside the card,
|
|
48
|
+
* beside the content. It was built for priority — *"Priority is
|
|
49
|
+
* styled as a leading colour bar, and **never** a coloured word
|
|
50
|
+
* before the title."* An intent is a claim the design system makes
|
|
51
|
+
* (`danger` says what happens if you press the thing), so this arm
|
|
52
|
+
* is for a bar that means a STATE.
|
|
53
|
+
*
|
|
54
|
+
* `accentEdge` is the card's own edge — `Card`'s treatment, the same
|
|
55
|
+
* pseudo-element, so it takes `border-radius: inherit` and wraps the
|
|
56
|
+
* corner at `cq-lg` where this card grows one. It carries a
|
|
57
|
+
* CATEGORICAL colour or a computed one, which is what an identity
|
|
58
|
+
* needs: a project, a repo, a source. The owner asked for exactly
|
|
59
|
+
* this on Docket's board — *"The Board has no colors. That could
|
|
60
|
+
* use project colors on the sides at least."* — after reporting the
|
|
61
|
+
* same treatment missing from the task rows.
|
|
62
|
+
*
|
|
63
|
+
* The two are mutually exclusive by type, not by convention. Both
|
|
64
|
+
* paint a leading bar in the same place, and a card wearing two of
|
|
65
|
+
* them is a card whose leading edge means two things.
|
|
66
|
+
*
|
|
67
|
+
* ⚠️ Either arm pairs with `accentLabel`. A bar is colour and
|
|
68
|
+
* nothing else, so on its own it fails WCAG 1.4.1 outright and is
|
|
69
|
+
* invisible to every screen reader; the label rides along in a
|
|
70
|
+
* `VisuallyHidden` so the same fact reaches both channels. Docket's
|
|
71
|
+
* first board painted its bars `--color-danger-9` — a Radix-style
|
|
72
|
+
* scale this token set has never had — so every bar was transparent
|
|
73
|
+
* while every "is it rendered" assertion passed.
|
|
74
|
+
*/
|
|
75
|
+
type BoardItemAccent = {
|
|
76
|
+
accentEdge?: never;
|
|
77
|
+
accentIntent?: IntentName;
|
|
78
|
+
} | {
|
|
79
|
+
accentEdge: CardAccentEdge;
|
|
80
|
+
accentIntent?: never;
|
|
81
|
+
};
|
|
58
82
|
/**
|
|
59
83
|
* Who owns the card title's link — the board, or the caller.
|
|
60
84
|
*
|
|
@@ -98,7 +122,7 @@ type BoardItemTitleLink = {
|
|
|
98
122
|
*/
|
|
99
123
|
titleContent: ReactNode;
|
|
100
124
|
};
|
|
101
|
-
export type BoardItem = BoardItemFields & BoardItemTitleLink;
|
|
125
|
+
export type BoardItem = BoardItemAccent & BoardItemFields & BoardItemTitleLink;
|
|
102
126
|
export type BoardCardProps = {
|
|
103
127
|
item: BoardItem;
|
|
104
128
|
laneLabel: string;
|
|
@@ -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;
|
|
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;AAc/D,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;CACrB,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;;;;;;;;OAQG;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,SAAS,GAAI,wEAOvB,cAAc,KAAG,SAsLnB,CAAA"}
|
package/dist/Board/BoardCard.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { Button } from "../Button/Button.js";
|
|
4
|
+
import { getAccentEdgeClassName, getAccentEdgeStyle, } from "../Card/cardAccentEdge.js";
|
|
4
5
|
import { FOCUS_RING_CLASS, INTENT_SOLID_FILL_CLASS, } from "../intentStyles.js";
|
|
5
6
|
import { Menu } from "../Menu/Menu.js";
|
|
6
7
|
import { TextLink } from "../TextLink/TextLink.js";
|
|
@@ -52,7 +53,22 @@ export const BoardCard = ({ item, laneLabel, moveIcon, moveTargets, onMoveToLane
|
|
|
52
53
|
// divider goes and each card grows its own box. The *list*
|
|
53
54
|
// cannot do this to itself — it is the container, and a
|
|
54
55
|
// container query never matches its own container.
|
|
55
|
-
"cq-lg:mb-2 cq-lg:rounded-lg cq-lg:border cq-lg:bg-surface-raised cq-lg:p-3 cq-lg:shadow-low",
|
|
56
|
+
"cq-lg:mb-2 cq-lg:rounded-lg cq-lg:border cq-lg:bg-surface-raised cq-lg:p-3 cq-lg:shadow-low",
|
|
57
|
+
// The edge is drawn on THIS box, which is why it needs no
|
|
58
|
+
// shape of its own: the box is a row below `cq-lg` and a
|
|
59
|
+
// rounded card above it, and `border-radius: inherit` gives
|
|
60
|
+
// the right answer at both — a straight stripe down a row
|
|
61
|
+
// that has no corner, and a bar that wraps the card's
|
|
62
|
+
// corner once it grows one.
|
|
63
|
+
//
|
|
64
|
+
// `ps-2` is what keeps the content off it. The bar is 3px
|
|
65
|
+
// and overlays rather than displaces, so without this the
|
|
66
|
+
// first mark sits on top of it; at `cq-lg` the card's own
|
|
67
|
+
// `p-3` is already wider than the bar.
|
|
68
|
+
item.accentEdge != null &&
|
|
69
|
+
toClassName("ps-2", getAccentEdgeClassName(item.accentEdge)), item.isDimmed && "opacity-60"), "data-board-card": item.key, ...(item.accentEdge == null
|
|
70
|
+
? {}
|
|
71
|
+
: { 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 || onMoveToLane ? (_jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [item.trailing, onMoveToLane && moveTargets.length > 0 ? (_jsx(Menu, { isVisible: isMenuVisible, items: moveTargets.map((target) => ({
|
|
56
72
|
key: target.key,
|
|
57
73
|
label: target.label,
|
|
58
74
|
onSelect: () => {
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"BoardCard.js","sourceRoot":"","sources":["../../src/Board/BoardCard.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -188,6 +188,45 @@ const ADA: [string, string] = ["AD", "Ada"]
|
|
|
188
188
|
|
|
189
189
|
const WREN: [string, string] = ["WR", "Wren"]
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* A categorical index per project, and the same four projects the
|
|
193
|
+
* rest of this file uses. The index is what a consumer's user
|
|
194
|
+
* PICKED — it is not derived here, and it is never a hex.
|
|
195
|
+
*/
|
|
196
|
+
const PROJECT_EDGE_ITEMS: BoardItem[] = (
|
|
197
|
+
[
|
|
198
|
+
[
|
|
199
|
+
PROJECTS.signal,
|
|
200
|
+
2,
|
|
201
|
+
"Retire the second scheduler and fold its jobs into the broker",
|
|
202
|
+
],
|
|
203
|
+
[
|
|
204
|
+
PROJECTS.loom,
|
|
205
|
+
5,
|
|
206
|
+
"One composed Storybook for every app in the fleet",
|
|
207
|
+
],
|
|
208
|
+
[
|
|
209
|
+
PROJECTS.atlas,
|
|
210
|
+
8,
|
|
211
|
+
"Fingerprint duplicate uploads before they reach the queue",
|
|
212
|
+
],
|
|
213
|
+
[
|
|
214
|
+
PROJECTS.ferry,
|
|
215
|
+
10,
|
|
216
|
+
"Rewrite the ingest runbook against the new broker",
|
|
217
|
+
],
|
|
218
|
+
] as const
|
|
219
|
+
).map(([project, categorical, title]) => ({
|
|
220
|
+
accentEdge: { categorical },
|
|
221
|
+
// The words the bar cannot say. A colour alone is a WCAG
|
|
222
|
+
// 1.4.1 failure and is silent to a screen reader.
|
|
223
|
+
accentLabel: project,
|
|
224
|
+
href: `#/task/${encodeURIComponent(title)}`,
|
|
225
|
+
key: title,
|
|
226
|
+
meta: <ProjectChip name={project} />,
|
|
227
|
+
title,
|
|
228
|
+
}))
|
|
229
|
+
|
|
191
230
|
const TODO_ITEMS: BoardItem[] = [
|
|
192
231
|
toItem({
|
|
193
232
|
assignee: WREN,
|
|
@@ -565,6 +604,67 @@ export const Responsive: Story = {
|
|
|
565
604
|
),
|
|
566
605
|
}
|
|
567
606
|
|
|
607
|
+
/**
|
|
608
|
+
* **The project on the leading edge**, in both of the shapes a card
|
|
609
|
+
* takes — and the reason `accentEdge` exists beside `accentIntent`.
|
|
610
|
+
*
|
|
611
|
+
* The pill arm is a `w-1` span INSIDE the card, so it is a straight
|
|
612
|
+
* rectangle next to a rounded box. The edge arm is `Card`'s own
|
|
613
|
+
* treatment: a pseudo-element taking `border-radius: inherit`, so it
|
|
614
|
+
* paints a straight stripe down a row that has no corner and wraps
|
|
615
|
+
* the corner once the lane passes `cq-lg` and the row becomes a
|
|
616
|
+
* card. One prop, correct in both shapes, and never a notch.
|
|
617
|
+
*
|
|
618
|
+
* The colours are CATEGORICAL — an identity, not a state. A project,
|
|
619
|
+
* a repo, a source. Priority stays on the pill arm, where an intent
|
|
620
|
+
* belongs.
|
|
621
|
+
*/
|
|
622
|
+
export const AccentEdge: Story = {
|
|
623
|
+
render: () => (
|
|
624
|
+
<div className="flex flex-col gap-8">
|
|
625
|
+
<div className="flex flex-col gap-2">
|
|
626
|
+
<span className="font-mono text-content-muted text-xs">
|
|
627
|
+
a narrow lane — rows, so the edge is a straight
|
|
628
|
+
stripe
|
|
629
|
+
</span>
|
|
630
|
+
|
|
631
|
+
<Frame inlineSize="22rem">
|
|
632
|
+
<Board
|
|
633
|
+
label="Today, by project"
|
|
634
|
+
lanes={[
|
|
635
|
+
{
|
|
636
|
+
items: PROJECT_EDGE_ITEMS,
|
|
637
|
+
key: "todo",
|
|
638
|
+
label: "Todo",
|
|
639
|
+
},
|
|
640
|
+
]}
|
|
641
|
+
/>
|
|
642
|
+
</Frame>
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
<div className="flex flex-col gap-2">
|
|
646
|
+
<span className="font-mono text-content-muted text-xs">
|
|
647
|
+
the same lane past --cq-lg (48rem) — cards, so the
|
|
648
|
+
edge wraps the corner
|
|
649
|
+
</span>
|
|
650
|
+
|
|
651
|
+
<Frame inlineSize="56rem">
|
|
652
|
+
<Board
|
|
653
|
+
label="Today, by project, wide"
|
|
654
|
+
lanes={[
|
|
655
|
+
{
|
|
656
|
+
items: PROJECT_EDGE_ITEMS,
|
|
657
|
+
key: "todo",
|
|
658
|
+
label: "Todo",
|
|
659
|
+
},
|
|
660
|
+
]}
|
|
661
|
+
/>
|
|
662
|
+
</Frame>
|
|
663
|
+
</div>
|
|
664
|
+
</div>
|
|
665
|
+
),
|
|
666
|
+
}
|
|
667
|
+
|
|
568
668
|
/**
|
|
569
669
|
* The complete keyboard path, and it is the **primary** way to move
|
|
570
670
|
* a card rather than an accessible alternative to the real one.
|
package/src/Board/Board.test.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { expectAgentDrivable } from "../testing/index.ts"
|
|
|
13
13
|
import * as stories from "./Board.stories.tsx"
|
|
14
14
|
|
|
15
15
|
const {
|
|
16
|
+
AccentEdge,
|
|
16
17
|
AllStates,
|
|
17
18
|
Default,
|
|
18
19
|
InBoardScreen,
|
|
@@ -405,3 +406,64 @@ test("the board adds no link of its own around a rich title", async () => {
|
|
|
405
406
|
null,
|
|
406
407
|
)
|
|
407
408
|
})
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The edge is drawn on the card's OWN box, which is the whole
|
|
412
|
+
* difference between it and the `accentIntent` pill.
|
|
413
|
+
*
|
|
414
|
+
* A pill is a `w-1` span inside the card: a straight rectangle
|
|
415
|
+
* beside a rounded box, which is the notch three apps grew
|
|
416
|
+
* independently before `Card` took the shape over. The edge is a
|
|
417
|
+
* pseudo-element taking `border-radius: inherit`, so it is correct
|
|
418
|
+
* in both of a board card's shapes without being told which one it
|
|
419
|
+
* is in — a straight stripe down a row that has no corner, and a
|
|
420
|
+
* wrapped bar once the lane passes `cq-lg`.
|
|
421
|
+
*
|
|
422
|
+
* The class is what is asserted rather than a painted pixel: the
|
|
423
|
+
* bar is a `box-shadow` on a `::before`, and the radius it inherits
|
|
424
|
+
* comes from a container query the runner's own width does not
|
|
425
|
+
* decide. `cardAccentEdge.test.ts` owns what the class contains.
|
|
426
|
+
*/
|
|
427
|
+
test("draws a project's colour on the card's own edge", async () => {
|
|
428
|
+
const { canvas } = await mountStory(AccentEdge)
|
|
429
|
+
|
|
430
|
+
const cards = canvas
|
|
431
|
+
.getAllByRole("listitem")
|
|
432
|
+
.filter(
|
|
433
|
+
(item: HTMLElement) => item.dataset.boardCard != null,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
expect(cards.length).toBeGreaterThan(0)
|
|
437
|
+
|
|
438
|
+
for (const card of cards) {
|
|
439
|
+
// The overlay, and the radius it takes from the card.
|
|
440
|
+
expect(card.className).toContain("before:absolute")
|
|
441
|
+
expect(card.className).toContain(
|
|
442
|
+
"before:rounded-[inherit]",
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
// A categorical colour, never an intent — an edge is an
|
|
446
|
+
// identity and an intent is a claim about what happens.
|
|
447
|
+
expect(card.className).toMatch(
|
|
448
|
+
/before:shadow-\[inset_3px_0_0_var\(--color-categorical-\d+-solid\)\]/,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
// And it never eats a press on the card it covers.
|
|
452
|
+
expect(card.className).toContain(
|
|
453
|
+
"before:pointer-events-none",
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// The colour reaches a screen reader as words. Each card names
|
|
458
|
+
// its project through `accentLabel`, so the bar is not the only
|
|
459
|
+
// channel carrying the fact.
|
|
460
|
+
expect(
|
|
461
|
+
canvas.getAllByText("Ferry Docs").length,
|
|
462
|
+
).toBeGreaterThan(1)
|
|
463
|
+
|
|
464
|
+
await expectNoAxeViolations(
|
|
465
|
+
canvas.getByRole("region", {
|
|
466
|
+
name: "Today, by project",
|
|
467
|
+
}),
|
|
468
|
+
)
|
|
469
|
+
})
|
package/src/Board/BoardCard.tsx
CHANGED
|
@@ -3,6 +3,11 @@ import type { ReactNode } from "react"
|
|
|
3
3
|
import { useState } from "react"
|
|
4
4
|
|
|
5
5
|
import { Button } from "../Button/Button.tsx"
|
|
6
|
+
import type { CardAccentEdge } from "../Card/cardAccentEdge.ts"
|
|
7
|
+
import {
|
|
8
|
+
getAccentEdgeClassName,
|
|
9
|
+
getAccentEdgeStyle,
|
|
10
|
+
} from "../Card/cardAccentEdge.ts"
|
|
6
11
|
import {
|
|
7
12
|
FOCUS_RING_CLASS,
|
|
8
13
|
INTENT_SOLID_FILL_CLASS,
|
|
@@ -13,23 +18,7 @@ import { toClassName } from "../toClassName.ts"
|
|
|
13
18
|
import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.tsx"
|
|
14
19
|
|
|
15
20
|
type BoardItemFields = {
|
|
16
|
-
/**
|
|
17
|
-
* The leading colour bar. Priority, in the consumer this was built
|
|
18
|
-
* for — *"Priority is styled as a leading colour bar, and **never**
|
|
19
|
-
* a coloured word before the title."*
|
|
20
|
-
*
|
|
21
|
-
* Pair it with `accentLabel`. A bar is colour and nothing else, so
|
|
22
|
-
* on its own it fails WCAG 1.4.1 outright and is invisible to
|
|
23
|
-
* every screen reader; the label rides along in a
|
|
24
|
-
* `VisuallyHidden` so the same fact reaches both channels. Docket's
|
|
25
|
-
* first board painted these `--color-danger-9` — a Radix-style
|
|
26
|
-
* scale this token set has never had — so every bar was
|
|
27
|
-
* transparent while every "is it rendered" assertion passed. The
|
|
28
|
-
* families here are `--color-intent-<intent>-<role>`; there is no
|
|
29
|
-
* numeric scale to reach for.
|
|
30
|
-
*/
|
|
31
|
-
accentIntent?: IntentName
|
|
32
|
-
/** What the bar means, in words. "Priority 0". */
|
|
21
|
+
/** What the bar means, in words. "Priority 0", "Movies and Shows". */
|
|
33
22
|
accentLabel?: string
|
|
34
23
|
/**
|
|
35
24
|
* Under the title and metadata, spanning the card: a live run
|
|
@@ -68,6 +57,42 @@ type BoardItemFields = {
|
|
|
68
57
|
trailing?: ReactNode
|
|
69
58
|
}
|
|
70
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The leading colour bar, in one of two shapes — and the second one
|
|
62
|
+
* exists because the first cannot follow a corner.
|
|
63
|
+
*
|
|
64
|
+
* `accentIntent` is an inset pill: a `w-1` span inside the card,
|
|
65
|
+
* beside the content. It was built for priority — *"Priority is
|
|
66
|
+
* styled as a leading colour bar, and **never** a coloured word
|
|
67
|
+
* before the title."* An intent is a claim the design system makes
|
|
68
|
+
* (`danger` says what happens if you press the thing), so this arm
|
|
69
|
+
* is for a bar that means a STATE.
|
|
70
|
+
*
|
|
71
|
+
* `accentEdge` is the card's own edge — `Card`'s treatment, the same
|
|
72
|
+
* pseudo-element, so it takes `border-radius: inherit` and wraps the
|
|
73
|
+
* corner at `cq-lg` where this card grows one. It carries a
|
|
74
|
+
* CATEGORICAL colour or a computed one, which is what an identity
|
|
75
|
+
* needs: a project, a repo, a source. The owner asked for exactly
|
|
76
|
+
* this on Docket's board — *"The Board has no colors. That could
|
|
77
|
+
* use project colors on the sides at least."* — after reporting the
|
|
78
|
+
* same treatment missing from the task rows.
|
|
79
|
+
*
|
|
80
|
+
* The two are mutually exclusive by type, not by convention. Both
|
|
81
|
+
* paint a leading bar in the same place, and a card wearing two of
|
|
82
|
+
* them is a card whose leading edge means two things.
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ Either arm pairs with `accentLabel`. A bar is colour and
|
|
85
|
+
* nothing else, so on its own it fails WCAG 1.4.1 outright and is
|
|
86
|
+
* invisible to every screen reader; the label rides along in a
|
|
87
|
+
* `VisuallyHidden` so the same fact reaches both channels. Docket's
|
|
88
|
+
* first board painted its bars `--color-danger-9` — a Radix-style
|
|
89
|
+
* scale this token set has never had — so every bar was transparent
|
|
90
|
+
* while every "is it rendered" assertion passed.
|
|
91
|
+
*/
|
|
92
|
+
type BoardItemAccent =
|
|
93
|
+
| { accentEdge?: never; accentIntent?: IntentName }
|
|
94
|
+
| { accentEdge: CardAccentEdge; accentIntent?: never }
|
|
95
|
+
|
|
71
96
|
/**
|
|
72
97
|
* Who owns the card title's link — the board, or the caller.
|
|
73
98
|
*
|
|
@@ -114,7 +139,9 @@ type BoardItemTitleLink =
|
|
|
114
139
|
titleContent: ReactNode
|
|
115
140
|
}
|
|
116
141
|
|
|
117
|
-
export type BoardItem =
|
|
142
|
+
export type BoardItem = BoardItemAccent &
|
|
143
|
+
BoardItemFields &
|
|
144
|
+
BoardItemTitleLink
|
|
118
145
|
|
|
119
146
|
export type BoardCardProps = {
|
|
120
147
|
item: BoardItem
|
|
@@ -210,10 +237,36 @@ export const BoardCard = ({
|
|
|
210
237
|
// cannot do this to itself — it is the container, and a
|
|
211
238
|
// container query never matches its own container.
|
|
212
239
|
"cq-lg:mb-2 cq-lg:rounded-lg cq-lg:border cq-lg:bg-surface-raised cq-lg:p-3 cq-lg:shadow-low",
|
|
240
|
+
// The edge is drawn on THIS box, which is why it needs no
|
|
241
|
+
// shape of its own: the box is a row below `cq-lg` and a
|
|
242
|
+
// rounded card above it, and `border-radius: inherit` gives
|
|
243
|
+
// the right answer at both — a straight stripe down a row
|
|
244
|
+
// that has no corner, and a bar that wraps the card's
|
|
245
|
+
// corner once it grows one.
|
|
246
|
+
//
|
|
247
|
+
// `ps-2` is what keeps the content off it. The bar is 3px
|
|
248
|
+
// and overlays rather than displaces, so without this the
|
|
249
|
+
// first mark sits on top of it; at `cq-lg` the card's own
|
|
250
|
+
// `p-3` is already wider than the bar.
|
|
251
|
+
item.accentEdge != null &&
|
|
252
|
+
toClassName(
|
|
253
|
+
"ps-2",
|
|
254
|
+
getAccentEdgeClassName(item.accentEdge),
|
|
255
|
+
),
|
|
213
256
|
item.isDimmed && "opacity-60",
|
|
214
257
|
)}
|
|
215
258
|
data-board-card={item.key}
|
|
259
|
+
{...(item.accentEdge == null
|
|
260
|
+
? {}
|
|
261
|
+
: { style: getAccentEdgeStyle(item.accentEdge) })}
|
|
216
262
|
>
|
|
263
|
+
{/* The label the bar cannot say. An edge has no content of
|
|
264
|
+
its own — it is a pseudo-element — so unlike the pill
|
|
265
|
+
arm below there is nowhere inside it to put the words. */}
|
|
266
|
+
{item.accentEdge != null && item.accentLabel ? (
|
|
267
|
+
<VisuallyHidden>{item.accentLabel}</VisuallyHidden>
|
|
268
|
+
) : null}
|
|
269
|
+
|
|
217
270
|
{item.accentIntent ? (
|
|
218
271
|
<span
|
|
219
272
|
className={toClassName(
|