@charcuterie/ui 3.12.0 → 3.13.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/Avatar/Avatar.d.ts +163 -0
- package/dist/Avatar/Avatar.d.ts.map +1 -0
- package/dist/Avatar/Avatar.js +139 -0
- package/dist/Avatar/Avatar.js.map +1 -0
- package/dist/Board/BoardLaneList.d.ts +29 -0
- package/dist/Board/BoardLaneList.d.ts.map +1 -1
- package/dist/Board/BoardLaneList.js +2 -1
- package/dist/Board/BoardLaneList.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Avatar/Avatar.mdx +92 -0
- package/src/Avatar/Avatar.stories.tsx +254 -0
- package/src/Avatar/Avatar.test.tsx +143 -0
- package/src/Avatar/Avatar.tsx +270 -0
- package/src/Board/Board.mdx +26 -0
- package/src/Board/Board.stories.tsx +47 -21
- package/src/Board/Board.test.tsx +62 -0
- package/src/Board/BoardLaneList.tsx +52 -1
- package/src/index.ts +5 -0
- package/src/sourceRules.test.ts +9 -1
|
@@ -3,6 +3,7 @@ import type { ReactNode } from "react"
|
|
|
3
3
|
import { useState } from "react"
|
|
4
4
|
|
|
5
5
|
import { Alert } from "../Alert/Alert.tsx"
|
|
6
|
+
import { Avatar } from "../Avatar/Avatar.tsx"
|
|
6
7
|
import { Badge } from "../Badge/Badge.tsx"
|
|
7
8
|
import { Button } from "../Button/Button.tsx"
|
|
8
9
|
import {
|
|
@@ -71,26 +72,6 @@ const Mark = ({
|
|
|
71
72
|
</span>
|
|
72
73
|
)
|
|
73
74
|
|
|
74
|
-
/**
|
|
75
|
-
* An assignee. Two initials and a name nobody has to hover to hear —
|
|
76
|
-
* an avatar with no accessible name is a coloured circle, which is
|
|
77
|
-
* the fleet's most-repeated a11y defect after the unnamed icon
|
|
78
|
-
* button.
|
|
79
|
-
*/
|
|
80
|
-
const Avatar = ({
|
|
81
|
-
initials,
|
|
82
|
-
name,
|
|
83
|
-
}: {
|
|
84
|
-
initials: string
|
|
85
|
-
name: string
|
|
86
|
-
}): ReactNode => (
|
|
87
|
-
<span className="grid size-5 shrink-0 place-items-center rounded-full bg-intent-info-surface font-medium text-[0.625rem] text-intent-info-content">
|
|
88
|
-
<span aria-hidden="true">{initials}</span>
|
|
89
|
-
|
|
90
|
-
<VisuallyHidden>{`Assigned to ${name}`}</VisuallyHidden>
|
|
91
|
-
</span>
|
|
92
|
-
)
|
|
93
|
-
|
|
94
75
|
const ProjectChip = ({
|
|
95
76
|
name,
|
|
96
77
|
}: {
|
|
@@ -184,8 +165,20 @@ const toItem = ({
|
|
|
184
165
|
</>
|
|
185
166
|
),
|
|
186
167
|
title,
|
|
168
|
+
/*
|
|
169
|
+
* `Avatar` from this package, not a chip hand-rolled in a story
|
|
170
|
+
* file. This story *had* one — a `size-5` circle painted
|
|
171
|
+
* `intent-info`, with the name in a `VisuallyHidden` — which is
|
|
172
|
+
* exactly the shape three apps were about to write for
|
|
173
|
+
* themselves. The library owns the shape; the story owns the
|
|
174
|
+
* data.
|
|
175
|
+
*/
|
|
187
176
|
trailing: (
|
|
188
|
-
<Avatar
|
|
177
|
+
<Avatar
|
|
178
|
+
initials={assignee[0]}
|
|
179
|
+
name={assignee[1]}
|
|
180
|
+
size="sm"
|
|
181
|
+
/>
|
|
189
182
|
),
|
|
190
183
|
})
|
|
191
184
|
|
|
@@ -705,3 +698,36 @@ export const InBoardScreen: Story = {
|
|
|
705
698
|
</div>
|
|
706
699
|
),
|
|
707
700
|
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* A lane whose heading goes somewhere — the "show me only this
|
|
704
|
+
* column" route.
|
|
705
|
+
*
|
|
706
|
+
* `href` on the lane, not a `ReactNode` label. The heading stays an
|
|
707
|
+
* `<h3>` at the level the caller asked for, keeps its id, and keeps
|
|
708
|
+
* naming the `group`; the anchor sits **inside** it, so the document
|
|
709
|
+
* outline is the same one a board with no links has.
|
|
710
|
+
*
|
|
711
|
+
* It also reads as the same heading: `TextLink` supplies the cursor,
|
|
712
|
+
* the focus ring and the hover underline, and the colour is
|
|
713
|
+
* inherited. Three accent-coloured column titles would compete with
|
|
714
|
+
* the card titles below them, which are what a reader is scanning.
|
|
715
|
+
*/
|
|
716
|
+
export const LinkedLaneHeadings: Story = {
|
|
717
|
+
render: (boardProps) => (
|
|
718
|
+
// The same fixed 72rem the other three-up stories use. Three
|
|
719
|
+
// lanes at once is a measurement of the board's own box, and a
|
|
720
|
+
// story that inherited the canvas width would show three linked
|
|
721
|
+
// headings on one machine and one plus a segmented control on
|
|
722
|
+
// another.
|
|
723
|
+
<Frame inlineSize="72rem">
|
|
724
|
+
<Board
|
|
725
|
+
{...boardProps}
|
|
726
|
+
lanes={LANES.map((lane) => ({
|
|
727
|
+
...lane,
|
|
728
|
+
href: `/board/${lane.key}`,
|
|
729
|
+
}))}
|
|
730
|
+
/>
|
|
731
|
+
</Frame>
|
|
732
|
+
),
|
|
733
|
+
}
|
package/src/Board/Board.test.tsx
CHANGED
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
Default,
|
|
18
18
|
InBoardScreen,
|
|
19
19
|
Interactive,
|
|
20
|
+
LinkedLaneHeadings,
|
|
20
21
|
Responsive,
|
|
21
22
|
} = composeStories(stories)
|
|
22
23
|
|
|
@@ -308,3 +309,64 @@ test("the board sits under a separate attention banner, and the page is clean",
|
|
|
308
309
|
|
|
309
310
|
await expectNoAxeViolations(canvasElement)
|
|
310
311
|
})
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* A linkable column title is a **link inside the heading**, never a
|
|
315
|
+
* heading replaced by a link.
|
|
316
|
+
*
|
|
317
|
+
* The distinction is the whole reason `href` is a prop rather than a
|
|
318
|
+
* widened `label`: swap the `<h3>` for an `<a>` and the lane stops
|
|
319
|
+
* having a heading, the `group` loses the element it is named by,
|
|
320
|
+
* and the document outline gains a hole that no gate but a screen
|
|
321
|
+
* reader would notice.
|
|
322
|
+
*/
|
|
323
|
+
test("a lane heading with an href is a link inside a real heading", async () => {
|
|
324
|
+
const { canvas } = await mountStory(LinkedLaneHeadings)
|
|
325
|
+
|
|
326
|
+
const board = expectAgentDrivable(canvas, {
|
|
327
|
+
name: "Today",
|
|
328
|
+
role: "region",
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
for (const [laneKey, laneLabel] of [
|
|
332
|
+
["todo", "Todo"],
|
|
333
|
+
["in-progress", "In Progress"],
|
|
334
|
+
["review", "Needs Review"],
|
|
335
|
+
]) {
|
|
336
|
+
// Still a named group — the heading did not stop being the
|
|
337
|
+
// thing that names it.
|
|
338
|
+
expectAgentDrivable(canvas, {
|
|
339
|
+
name: laneLabel,
|
|
340
|
+
role: "group",
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
const heading = canvas.getByRole("heading", {
|
|
344
|
+
level: 3,
|
|
345
|
+
name: laneLabel,
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const link = within(heading).getByRole("link", {
|
|
349
|
+
name: laneLabel,
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
await expect(link).toHaveAttribute(
|
|
353
|
+
"href",
|
|
354
|
+
`/board/${laneKey}`,
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
await expectNoAxeViolations(board)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The lane label is still a string everywhere else it is used, and
|
|
363
|
+
* a linked heading must not have changed that. The move menu, the
|
|
364
|
+
* segmented control and the truncation line all interpolate it.
|
|
365
|
+
*/
|
|
366
|
+
test("a linked lane still names itself in the move menu and the overflow line", async () => {
|
|
367
|
+
const { canvas } = await mountStory(LinkedLaneHeadings)
|
|
368
|
+
|
|
369
|
+
await expect(
|
|
370
|
+
canvas.getByText("+ 11 more in Needs Review"),
|
|
371
|
+
).toBeInTheDocument()
|
|
372
|
+
})
|
|
@@ -4,6 +4,7 @@ import type { ReactNode } from "react"
|
|
|
4
4
|
import { Badge } from "../Badge/Badge.tsx"
|
|
5
5
|
import { Button } from "../Button/Button.tsx"
|
|
6
6
|
import { EmptyState } from "../EmptyState/EmptyState.tsx"
|
|
7
|
+
import { TextLink } from "../TextLink/TextLink.tsx"
|
|
7
8
|
import { toClassName } from "../toClassName.ts"
|
|
8
9
|
import type { BoardItem } from "./BoardCard.tsx"
|
|
9
10
|
import { BoardCard } from "./BoardCard.tsx"
|
|
@@ -20,6 +21,23 @@ export type BoardLane = {
|
|
|
20
21
|
* requirement and an empty lane is the one a person sees most.
|
|
21
22
|
*/
|
|
22
23
|
emptyState?: ReactNode
|
|
24
|
+
/**
|
|
25
|
+
* Where the lane's own heading navigates — a route showing this
|
|
26
|
+
* lane and nothing else.
|
|
27
|
+
*
|
|
28
|
+
* **A link inside the heading, never a heading replaced by a
|
|
29
|
+
* link.** The `<h3>` keeps its level, its id and its place in the
|
|
30
|
+
* document outline, so the lane stays a named `group` and the
|
|
31
|
+
* screen-reader outline is unchanged; the anchor sits inside it.
|
|
32
|
+
* Routed through `RouterLinkProvider` like every other `href` in
|
|
33
|
+
* this library, so an app with a client-side router navigates
|
|
34
|
+
* without a full page load and one without it still gets a real
|
|
35
|
+
* `<a href>`.
|
|
36
|
+
*
|
|
37
|
+
* Absent means the heading is plain text, which is what every
|
|
38
|
+
* existing consumer gets.
|
|
39
|
+
*/
|
|
40
|
+
href?: string
|
|
23
41
|
/**
|
|
24
42
|
* The **true** size of the lane, when `items` is a truncated page
|
|
25
43
|
* of it. This is what the count badge shows, and the difference
|
|
@@ -31,6 +49,18 @@ export type BoardLane = {
|
|
|
31
49
|
itemCount?: number
|
|
32
50
|
items: readonly BoardItem[]
|
|
33
51
|
key: string
|
|
52
|
+
/**
|
|
53
|
+
* The lane's name, and it stays a **string** rather than widening
|
|
54
|
+
* to a `ReactNode`.
|
|
55
|
+
*
|
|
56
|
+
* It is not only a heading: the same value names the lane in the
|
|
57
|
+
* move menu on every card, in the Narrow View's segmented control,
|
|
58
|
+
* in the "+ n more in Todo" line, and in the sentence the live
|
|
59
|
+
* region reads out after a move. A `ReactNode` is not something
|
|
60
|
+
* any of those four can put in a string, so widening this would
|
|
61
|
+
* trade one linkable heading for four places that can no longer
|
|
62
|
+
* say which lane they mean. `href` buys the link without that.
|
|
63
|
+
*/
|
|
34
64
|
label: string
|
|
35
65
|
/**
|
|
36
66
|
* What "+ n more in this lane" does. With no handler the line is
|
|
@@ -177,7 +207,28 @@ export const BoardLaneList = ({
|
|
|
177
207
|
className="font-semibold text-content-primary text-sm"
|
|
178
208
|
id={headingId}
|
|
179
209
|
>
|
|
180
|
-
{lane.
|
|
210
|
+
{lane.href ? (
|
|
211
|
+
/*
|
|
212
|
+
* `text-inherit` and `font-semibold` on purpose: a
|
|
213
|
+
* linkable column title should read as the same
|
|
214
|
+
* heading it was, not as a blue word. The link
|
|
215
|
+
* announces itself by being a link — the cursor,
|
|
216
|
+
* the focus ring and the hover underline all come
|
|
217
|
+
* from `TextLink` — and a board whose every lane
|
|
218
|
+
* title is accent-coloured competes with the card
|
|
219
|
+
* titles underneath it, which are the things a
|
|
220
|
+
* reader is actually scanning.
|
|
221
|
+
*/
|
|
222
|
+
<TextLink
|
|
223
|
+
appearance="standalone"
|
|
224
|
+
className="font-semibold text-inherit"
|
|
225
|
+
href={lane.href}
|
|
226
|
+
>
|
|
227
|
+
{lane.label}
|
|
228
|
+
</TextLink>
|
|
229
|
+
) : (
|
|
230
|
+
lane.label
|
|
231
|
+
)}
|
|
181
232
|
</Heading>
|
|
182
233
|
|
|
183
234
|
<Badge
|
package/src/index.ts
CHANGED
|
@@ -63,6 +63,11 @@ export type {
|
|
|
63
63
|
AlertSize,
|
|
64
64
|
} from "./Alert/Alert.tsx"
|
|
65
65
|
export { Alert } from "./Alert/Alert.tsx"
|
|
66
|
+
export type {
|
|
67
|
+
AvatarProps,
|
|
68
|
+
AvatarSize,
|
|
69
|
+
} from "./Avatar/Avatar.tsx"
|
|
70
|
+
export { Avatar } from "./Avatar/Avatar.tsx"
|
|
66
71
|
export type { BadgeProps } from "./Badge/Badge.tsx"
|
|
67
72
|
export { Badge } from "./Badge/Badge.tsx"
|
|
68
73
|
export type { BadgeShapeProps } from "./Badge/useBadgeShape.tsx"
|
package/src/sourceRules.test.ts
CHANGED
|
@@ -465,7 +465,15 @@ test("the barrel is the only place components are re-exported", async () => {
|
|
|
465
465
|
// count by the `<Name>/<Name>.tsx` rule, and it IS exported from
|
|
466
466
|
// the barrel as a function, because a host with its own clipboard
|
|
467
467
|
// bridge needs the fallback chain without the button.
|
|
468
|
-
|
|
468
|
+
//
|
|
469
|
+
// `Avatar` — a person as a coloured chip, so a board stops
|
|
470
|
+
// printing the same name on every card — is +1 -> 55. It has no
|
|
471
|
+
// members: the initials derivation is a private function in
|
|
472
|
+
// `Avatar.tsx`, and the colour it falls back to is
|
|
473
|
+
// `getCategoricalIndex`, which already lives in
|
|
474
|
+
// `@charcuterie/tokens` because a Node service and a Satori render
|
|
475
|
+
// both need to answer the colour question with no React in scope.
|
|
476
|
+
expect(componentNames.length).toBe(55)
|
|
469
477
|
|
|
470
478
|
for (const name of componentNames) {
|
|
471
479
|
expect(barrel).toContain(`export { ${name} }`)
|