@charcuterie/ui 3.0.0 → 3.2.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/README.md +10 -0
- package/dist/Badge/Badge.d.ts +33 -3
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +20 -2
- package/dist/Badge/Badge.js.map +1 -1
- package/dist/Board/Board.d.ts +119 -0
- package/dist/Board/Board.d.ts.map +1 -0
- package/dist/Board/Board.js +165 -0
- package/dist/Board/Board.js.map +1 -0
- package/dist/Board/BoardCard.d.ts +120 -0
- package/dist/Board/BoardCard.d.ts.map +1 -0
- package/dist/Board/BoardCard.js +77 -0
- package/dist/Board/BoardCard.js.map +1 -0
- package/dist/Board/BoardLaneList.d.ts +103 -0
- package/dist/Board/BoardLaneList.d.ts.map +1 -0
- package/dist/Board/BoardLaneList.js +69 -0
- package/dist/Board/BoardLaneList.js.map +1 -0
- package/dist/Board/boardMove.d.ts +116 -0
- package/dist/Board/boardMove.d.ts.map +1 -0
- package/dist/Board/boardMove.js +116 -0
- package/dist/Board/boardMove.js.map +1 -0
- package/dist/Board/useBoardDrag.d.ts +44 -0
- package/dist/Board/useBoardDrag.d.ts.map +1 -0
- package/dist/Board/useBoardDrag.js +237 -0
- package/dist/Board/useBoardDrag.js.map +1 -0
- package/dist/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/Checkbox/Checkbox.js +15 -4
- package/dist/Checkbox/Checkbox.js.map +1 -1
- package/dist/DataTable/DataTable.d.ts +159 -0
- package/dist/DataTable/DataTable.d.ts.map +1 -0
- package/dist/DataTable/DataTable.js +197 -0
- package/dist/DataTable/DataTable.js.map +1 -0
- package/dist/DatePicker/DateGrid.d.ts +79 -0
- package/dist/DatePicker/DateGrid.d.ts.map +1 -0
- package/dist/DatePicker/DateGrid.js +220 -0
- package/dist/DatePicker/DateGrid.js.map +1 -0
- package/dist/DatePicker/DatePicker.d.ts +138 -0
- package/dist/DatePicker/DatePicker.d.ts.map +1 -0
- package/dist/DatePicker/DatePicker.js +384 -0
- package/dist/DatePicker/DatePicker.js.map +1 -0
- package/dist/DatePicker/datePresets.d.ts +29 -0
- package/dist/DatePicker/datePresets.d.ts.map +1 -0
- package/dist/DatePicker/datePresets.js +28 -0
- package/dist/DatePicker/datePresets.js.map +1 -0
- package/dist/DatePicker/parseDateInput.d.ts +106 -0
- package/dist/DatePicker/parseDateInput.d.ts.map +1 -0
- package/dist/DatePicker/parseDateInput.js +419 -0
- package/dist/DatePicker/parseDateInput.js.map +1 -0
- package/dist/DatePicker/plainDate.d.ts +167 -0
- package/dist/DatePicker/plainDate.d.ts.map +1 -0
- package/dist/DatePicker/plainDate.js +272 -0
- package/dist/DatePicker/plainDate.js.map +1 -0
- package/dist/Dialog/Dialog.d.ts +12 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +2 -2
- package/dist/Dialog/Dialog.js.map +1 -1
- package/dist/MarkdownEditor/MarkdownEditor.d.ts +147 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.js +492 -0
- package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts +202 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.js +379 -0
- package/dist/MarkdownEditor/markdownCommands.js.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts +92 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.js +303 -0
- package/dist/MarkdownEditor/markdownSpans.js.map +1 -0
- package/dist/Modal/Modal.d.ts +6 -2
- package/dist/Modal/Modal.d.ts.map +1 -1
- package/dist/Modal/Modal.js +1 -1
- package/dist/Modal/Modal.js.map +1 -1
- package/dist/Overlay/OverlayPanel.d.ts +13 -2
- package/dist/Overlay/OverlayPanel.d.ts.map +1 -1
- package/dist/Overlay/OverlayPanel.js +5 -1
- package/dist/Overlay/OverlayPanel.js.map +1 -1
- package/dist/categoricalStyles.d.ts +53 -0
- package/dist/categoricalStyles.d.ts.map +1 -0
- package/dist/categoricalStyles.js +195 -0
- package/dist/categoricalStyles.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Badge/Badge.mdx +46 -0
- package/src/Badge/Badge.stories.tsx +98 -4
- package/src/Badge/Badge.test.tsx +157 -2
- package/src/Badge/Badge.tsx +49 -3
- package/src/Board/Board.mdx +199 -0
- package/src/Board/Board.stories.tsx +707 -0
- package/src/Board/Board.test.tsx +310 -0
- package/src/Board/Board.tsx +333 -0
- package/src/Board/BoardCard.tsx +290 -0
- package/src/Board/BoardLaneList.tsx +275 -0
- package/src/Board/boardMove.test.ts +213 -0
- package/src/Board/boardMove.ts +225 -0
- package/src/Board/useBoardDrag.ts +413 -0
- package/src/Checkbox/Checkbox.tsx +34 -3
- package/src/DataTable/DataTable.mdx +123 -0
- package/src/DataTable/DataTable.stories.tsx +411 -0
- package/src/DataTable/DataTable.test.tsx +386 -0
- package/src/DataTable/DataTable.tsx +497 -0
- package/src/DatePicker/DateGrid.tsx +467 -0
- package/src/DatePicker/DatePicker.mdx +170 -0
- package/src/DatePicker/DatePicker.stories.tsx +268 -0
- package/src/DatePicker/DatePicker.test.tsx +455 -0
- package/src/DatePicker/DatePicker.tsx +852 -0
- package/src/DatePicker/datePresets.ts +34 -0
- package/src/DatePicker/parseDateInput.test.ts +187 -0
- package/src/DatePicker/parseDateInput.ts +629 -0
- package/src/DatePicker/plainDate.test.ts +375 -0
- package/src/DatePicker/plainDate.ts +465 -0
- package/src/Dialog/Dialog.stories.tsx +48 -0
- package/src/Dialog/Dialog.test.tsx +30 -0
- package/src/Dialog/Dialog.tsx +13 -1
- package/src/MarkdownEditor/MarkdownEditor.mdx +189 -0
- package/src/MarkdownEditor/MarkdownEditor.stories.tsx +267 -0
- package/src/MarkdownEditor/MarkdownEditor.test.tsx +627 -0
- package/src/MarkdownEditor/MarkdownEditor.tsx +862 -0
- package/src/MarkdownEditor/markdownCommands.test.ts +384 -0
- package/src/MarkdownEditor/markdownCommands.ts +566 -0
- package/src/MarkdownEditor/markdownSpans.test.ts +217 -0
- package/src/MarkdownEditor/markdownSpans.ts +458 -0
- package/src/Modal/Modal.tsx +7 -1
- package/src/Overlay/OverlayPanel.tsx +17 -1
- package/src/argTypes.storyHelpers.ts +39 -0
- package/src/categoricalStyles.ts +244 -0
- package/src/index.ts +89 -0
- package/src/sourceRules.test.ts +52 -1
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
|
|
5
|
+
import { Alert } from "../Alert/Alert.tsx"
|
|
6
|
+
import { Badge } from "../Badge/Badge.tsx"
|
|
7
|
+
import { Button } from "../Button/Button.tsx"
|
|
8
|
+
import {
|
|
9
|
+
ContainerBoard,
|
|
10
|
+
StoryCell,
|
|
11
|
+
StoryGrid,
|
|
12
|
+
} from "../board.storyHelpers.tsx"
|
|
13
|
+
import { EmptyState } from "../EmptyState/EmptyState.tsx"
|
|
14
|
+
import { MoreIcon } from "../icons.storyHelpers.tsx"
|
|
15
|
+
import { ProgressBar } from "../ProgressBar/ProgressBar.tsx"
|
|
16
|
+
import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.tsx"
|
|
17
|
+
import type { BoardMove } from "./Board.tsx"
|
|
18
|
+
import { Board } from "./Board.tsx"
|
|
19
|
+
import type { BoardItem } from "./BoardCard.tsx"
|
|
20
|
+
import type { BoardLane } from "./BoardLaneList.tsx"
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* **Invented, all of it.** The board this component was built for
|
|
24
|
+
* tracks a real household, and its prototype's fixture file is real
|
|
25
|
+
* data — which is exactly the thing that must never reach a
|
|
26
|
+
* published repo, and doubly never a committed screenshot, because a
|
|
27
|
+
* PNG is opaque to every grep and no text scrub can redact it.
|
|
28
|
+
*
|
|
29
|
+
* So this is a fictional studio's tooling backlog: long enough
|
|
30
|
+
* titles to make truncation happen, enough rows to make the "+ n
|
|
31
|
+
* more" line honest, and a lane with a live agent run in it.
|
|
32
|
+
*/
|
|
33
|
+
const PROJECTS = {
|
|
34
|
+
atlas: "Atlas Ingest",
|
|
35
|
+
ferry: "Ferry Docs",
|
|
36
|
+
loom: "Loom Fleet",
|
|
37
|
+
signal: "Signal Kitchen",
|
|
38
|
+
} as const
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Priority as an intent, written out once. There is no numeric
|
|
42
|
+
* colour scale in this token set — the families are
|
|
43
|
+
* `--color-intent-<intent>-<role>` — so a `p0`/`p1` map has to land
|
|
44
|
+
* on an intent name or it lands on nothing at all and paints
|
|
45
|
+
* transparent.
|
|
46
|
+
*/
|
|
47
|
+
const PRIORITY = {
|
|
48
|
+
p0: { intent: "danger", label: "Priority 0" },
|
|
49
|
+
p1: { intent: "warning", label: "Priority 1" },
|
|
50
|
+
p2: { intent: "accent", label: "Priority 2" },
|
|
51
|
+
p3: { intent: "neutral", label: "Priority 3" },
|
|
52
|
+
} as const
|
|
53
|
+
|
|
54
|
+
const Mark = ({
|
|
55
|
+
intent,
|
|
56
|
+
name,
|
|
57
|
+
}: {
|
|
58
|
+
intent: "danger" | "success" | "warning"
|
|
59
|
+
name: string
|
|
60
|
+
}): ReactNode => (
|
|
61
|
+
<span
|
|
62
|
+
className={
|
|
63
|
+
intent === "success"
|
|
64
|
+
? "block size-2 rounded-full bg-intent-success-solid"
|
|
65
|
+
: intent === "warning"
|
|
66
|
+
? "block size-2 rounded-full bg-intent-warning-solid"
|
|
67
|
+
: "block size-2 rounded-full bg-intent-danger-solid"
|
|
68
|
+
}
|
|
69
|
+
>
|
|
70
|
+
<VisuallyHidden>{name}</VisuallyHidden>
|
|
71
|
+
</span>
|
|
72
|
+
)
|
|
73
|
+
|
|
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
|
+
const ProjectChip = ({
|
|
95
|
+
name,
|
|
96
|
+
}: {
|
|
97
|
+
name: string
|
|
98
|
+
}): ReactNode => (
|
|
99
|
+
<Badge appearance="outline" intent="neutral" size="sm">
|
|
100
|
+
{name}
|
|
101
|
+
</Badge>
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const LabelChip = ({
|
|
105
|
+
name,
|
|
106
|
+
}: {
|
|
107
|
+
name: string
|
|
108
|
+
}): ReactNode => (
|
|
109
|
+
<Badge appearance="soft" intent="success" size="sm">
|
|
110
|
+
{name}
|
|
111
|
+
</Badge>
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The live-run line. `ProgressBar` with its label hidden, because
|
|
116
|
+
* the phase text beside it already says what is happening — a
|
|
117
|
+
* visible "Adopting the new toolbar" twice is a screen-reader
|
|
118
|
+
* duplicate for no gain.
|
|
119
|
+
*/
|
|
120
|
+
const RunLine = ({
|
|
121
|
+
elapsed,
|
|
122
|
+
percent,
|
|
123
|
+
phase,
|
|
124
|
+
}: {
|
|
125
|
+
elapsed: string
|
|
126
|
+
percent: number
|
|
127
|
+
phase: string
|
|
128
|
+
}): ReactNode => (
|
|
129
|
+
<span className="flex items-center gap-2">
|
|
130
|
+
<span className="min-w-0 flex-1 truncate text-intent-success-content">
|
|
131
|
+
{phase}
|
|
132
|
+
</span>
|
|
133
|
+
|
|
134
|
+
<ProgressBar
|
|
135
|
+
className="w-16 shrink-0"
|
|
136
|
+
intent="success"
|
|
137
|
+
label={`${phase} progress`}
|
|
138
|
+
size="sm"
|
|
139
|
+
value={percent}
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
<span className="shrink-0 tabular-nums">{elapsed}</span>
|
|
143
|
+
</span>
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const FailedLine = ({
|
|
147
|
+
reason,
|
|
148
|
+
}: {
|
|
149
|
+
reason: string
|
|
150
|
+
}): ReactNode => (
|
|
151
|
+
<span className="text-intent-danger-content">
|
|
152
|
+
{reason}
|
|
153
|
+
</span>
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const toItem = ({
|
|
157
|
+
assignee,
|
|
158
|
+
footer,
|
|
159
|
+
label = "agent-runnable",
|
|
160
|
+
marks,
|
|
161
|
+
priority,
|
|
162
|
+
project,
|
|
163
|
+
title,
|
|
164
|
+
}: {
|
|
165
|
+
assignee: [string, string]
|
|
166
|
+
footer?: ReactNode
|
|
167
|
+
label?: string
|
|
168
|
+
marks?: ReactNode
|
|
169
|
+
priority: keyof typeof PRIORITY
|
|
170
|
+
project: string
|
|
171
|
+
title: string
|
|
172
|
+
}): BoardItem => ({
|
|
173
|
+
accentIntent: PRIORITY[priority].intent,
|
|
174
|
+
accentLabel: PRIORITY[priority].label,
|
|
175
|
+
footer,
|
|
176
|
+
href: `#/task/${encodeURIComponent(title)}`,
|
|
177
|
+
key: title,
|
|
178
|
+
marks,
|
|
179
|
+
meta: (
|
|
180
|
+
<>
|
|
181
|
+
<ProjectChip name={project} />
|
|
182
|
+
|
|
183
|
+
<LabelChip name={label} />
|
|
184
|
+
</>
|
|
185
|
+
),
|
|
186
|
+
title,
|
|
187
|
+
trailing: (
|
|
188
|
+
<Avatar initials={assignee[0]} name={assignee[1]} />
|
|
189
|
+
),
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const ADA: [string, string] = ["AD", "Ada"]
|
|
193
|
+
|
|
194
|
+
const WREN: [string, string] = ["WR", "Wren"]
|
|
195
|
+
|
|
196
|
+
const TODO_ITEMS: BoardItem[] = [
|
|
197
|
+
toItem({
|
|
198
|
+
assignee: WREN,
|
|
199
|
+
priority: "p0",
|
|
200
|
+
project: PROJECTS.signal,
|
|
201
|
+
title:
|
|
202
|
+
"Retire the second scheduler and fold its jobs into the broker",
|
|
203
|
+
}),
|
|
204
|
+
toItem({
|
|
205
|
+
assignee: ADA,
|
|
206
|
+
priority: "p1",
|
|
207
|
+
project: PROJECTS.loom,
|
|
208
|
+
title:
|
|
209
|
+
"One composed Storybook for every app in the fleet",
|
|
210
|
+
}),
|
|
211
|
+
toItem({
|
|
212
|
+
assignee: ADA,
|
|
213
|
+
marks: <Mark intent="warning" name="Stale" />,
|
|
214
|
+
priority: "p1",
|
|
215
|
+
project: PROJECTS.atlas,
|
|
216
|
+
title:
|
|
217
|
+
"Fingerprint duplicate uploads before they reach the queue",
|
|
218
|
+
}),
|
|
219
|
+
toItem({
|
|
220
|
+
assignee: WREN,
|
|
221
|
+
label: "physical",
|
|
222
|
+
priority: "p2",
|
|
223
|
+
project: PROJECTS.ferry,
|
|
224
|
+
title:
|
|
225
|
+
"Photograph the rack layout for the wiring appendix",
|
|
226
|
+
}),
|
|
227
|
+
toItem({
|
|
228
|
+
assignee: ADA,
|
|
229
|
+
priority: "p2",
|
|
230
|
+
project: PROJECTS.signal,
|
|
231
|
+
title:
|
|
232
|
+
"Move the nightly digest onto the shared template",
|
|
233
|
+
}),
|
|
234
|
+
toItem({
|
|
235
|
+
assignee: ADA,
|
|
236
|
+
priority: "p3",
|
|
237
|
+
project: PROJECTS.loom,
|
|
238
|
+
title: "Drop the four local overflow implementations",
|
|
239
|
+
}),
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
const IN_PROGRESS_ITEMS: BoardItem[] = [
|
|
243
|
+
toItem({
|
|
244
|
+
assignee: ADA,
|
|
245
|
+
footer: (
|
|
246
|
+
<RunLine
|
|
247
|
+
elapsed="41m"
|
|
248
|
+
percent={64}
|
|
249
|
+
phase="Adopting the toolbar in 3 of 4 repos"
|
|
250
|
+
/>
|
|
251
|
+
),
|
|
252
|
+
marks: <Mark intent="success" name="Agent running" />,
|
|
253
|
+
priority: "p0",
|
|
254
|
+
project: PROJECTS.loom,
|
|
255
|
+
title: "Unify the page chrome across the fleet",
|
|
256
|
+
}),
|
|
257
|
+
toItem({
|
|
258
|
+
assignee: ADA,
|
|
259
|
+
footer: (
|
|
260
|
+
<FailedLine reason="Failed: the ingest token expired after 3 retries" />
|
|
261
|
+
),
|
|
262
|
+
marks: <Mark intent="danger" name="Run failed" />,
|
|
263
|
+
priority: "p0",
|
|
264
|
+
project: PROJECTS.atlas,
|
|
265
|
+
title: "Link queue for the weekly digest",
|
|
266
|
+
}),
|
|
267
|
+
toItem({
|
|
268
|
+
assignee: WREN,
|
|
269
|
+
priority: "p1",
|
|
270
|
+
project: PROJECTS.signal,
|
|
271
|
+
title: "Wire the pantry sensors onto the new bridge",
|
|
272
|
+
}),
|
|
273
|
+
toItem({
|
|
274
|
+
assignee: ADA,
|
|
275
|
+
footer: (
|
|
276
|
+
<RunLine
|
|
277
|
+
elapsed="2h 06m"
|
|
278
|
+
percent={34}
|
|
279
|
+
phase="Fingerprinting batch 118 of 342"
|
|
280
|
+
/>
|
|
281
|
+
),
|
|
282
|
+
marks: <Mark intent="success" name="Agent running" />,
|
|
283
|
+
priority: "p1",
|
|
284
|
+
project: PROJECTS.atlas,
|
|
285
|
+
title: "Deduplicate the archive by content hash",
|
|
286
|
+
}),
|
|
287
|
+
toItem({
|
|
288
|
+
assignee: ADA,
|
|
289
|
+
marks: <Mark intent="danger" name="Blocked" />,
|
|
290
|
+
priority: "p2",
|
|
291
|
+
project: PROJECTS.ferry,
|
|
292
|
+
title: "Print farm queue — waiting on filament",
|
|
293
|
+
}),
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
const REVIEW_ITEMS: BoardItem[] = [
|
|
297
|
+
toItem({
|
|
298
|
+
assignee: ADA,
|
|
299
|
+
priority: "p1",
|
|
300
|
+
project: PROJECTS.loom,
|
|
301
|
+
title:
|
|
302
|
+
"Merge manager rename, split-out and add-games flow",
|
|
303
|
+
}),
|
|
304
|
+
toItem({
|
|
305
|
+
assignee: ADA,
|
|
306
|
+
priority: "p1",
|
|
307
|
+
project: PROJECTS.signal,
|
|
308
|
+
title: "Usage rollup across every provider, one table",
|
|
309
|
+
}),
|
|
310
|
+
toItem({
|
|
311
|
+
assignee: WREN,
|
|
312
|
+
priority: "p2",
|
|
313
|
+
project: PROJECTS.atlas,
|
|
314
|
+
title: "Gallery downloader keeps its own store now",
|
|
315
|
+
}),
|
|
316
|
+
toItem({
|
|
317
|
+
assignee: ADA,
|
|
318
|
+
priority: "p2",
|
|
319
|
+
project: PROJECTS.loom,
|
|
320
|
+
title:
|
|
321
|
+
"Push-transport kit for the shared broker package",
|
|
322
|
+
}),
|
|
323
|
+
toItem({
|
|
324
|
+
assignee: ADA,
|
|
325
|
+
priority: "p2",
|
|
326
|
+
project: PROJECTS.ferry,
|
|
327
|
+
title:
|
|
328
|
+
"Un-pin the docs generator from the old renderer",
|
|
329
|
+
}),
|
|
330
|
+
toItem({
|
|
331
|
+
assignee: WREN,
|
|
332
|
+
priority: "p3",
|
|
333
|
+
project: PROJECTS.loom,
|
|
334
|
+
title: "Brace expansion no longer melts the search box",
|
|
335
|
+
}),
|
|
336
|
+
toItem({
|
|
337
|
+
assignee: ADA,
|
|
338
|
+
priority: "p3",
|
|
339
|
+
project: PROJECTS.signal,
|
|
340
|
+
title: "Generalise the app updater beyond one registry",
|
|
341
|
+
}),
|
|
342
|
+
toItem({
|
|
343
|
+
assignee: ADA,
|
|
344
|
+
priority: "p3",
|
|
345
|
+
project: PROJECTS.atlas,
|
|
346
|
+
title: "Container image auto-update, with a rollback",
|
|
347
|
+
}),
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
const LANES: BoardLane[] = [
|
|
351
|
+
{ items: TODO_ITEMS, key: "todo", label: "Todo" },
|
|
352
|
+
{
|
|
353
|
+
items: IN_PROGRESS_ITEMS,
|
|
354
|
+
key: "in-progress",
|
|
355
|
+
label: "In Progress",
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
actions: (
|
|
359
|
+
<Button appearance="ghost" intent="accent" size="sm">
|
|
360
|
+
Accept all
|
|
361
|
+
</Button>
|
|
362
|
+
),
|
|
363
|
+
// Eight rendered, nineteen real — which is what makes the
|
|
364
|
+
// "+ 11 more" line true rather than decorative.
|
|
365
|
+
itemCount: 19,
|
|
366
|
+
items: REVIEW_ITEMS,
|
|
367
|
+
key: "review",
|
|
368
|
+
label: "Needs Review",
|
|
369
|
+
onShowMore: () => undefined,
|
|
370
|
+
},
|
|
371
|
+
]
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* A **fixed** inline size, because the three-up layout is a
|
|
375
|
+
* measurement of the board's own box. A story that inherited the
|
|
376
|
+
* canvas width would show three lanes on one machine and one lane
|
|
377
|
+
* plus a picker on another, and the test asserting it would be
|
|
378
|
+
* asserting the window.
|
|
379
|
+
*/
|
|
380
|
+
const Frame = ({
|
|
381
|
+
children,
|
|
382
|
+
inlineSize,
|
|
383
|
+
}: {
|
|
384
|
+
children: ReactNode
|
|
385
|
+
inlineSize: string
|
|
386
|
+
}): ReactNode => (
|
|
387
|
+
<div style={{ inlineSize }}>{children}</div>
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
const meta = {
|
|
391
|
+
title: "Components/Board",
|
|
392
|
+
component: Board,
|
|
393
|
+
parameters: { layout: "padded" },
|
|
394
|
+
args: {
|
|
395
|
+
label: "Today",
|
|
396
|
+
lanes: LANES,
|
|
397
|
+
},
|
|
398
|
+
} satisfies Meta<typeof Board>
|
|
399
|
+
|
|
400
|
+
export default meta
|
|
401
|
+
|
|
402
|
+
type Story = StoryObj<typeof meta>
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Three lanes at the density a real board reaches: priority bars,
|
|
406
|
+
* project and label chips, assignee avatars, live run lines with
|
|
407
|
+
* elapsed times, a failed run, and an honest overflow.
|
|
408
|
+
*
|
|
409
|
+
* Read-only — there is no `onMove`, so there are no handles. That is
|
|
410
|
+
* a real mode rather than a story convenience: a board on a wall
|
|
411
|
+
* display should not be advertising affordances nobody can use.
|
|
412
|
+
*/
|
|
413
|
+
export const Default: Story = {}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Read-only against movable, side by side.
|
|
417
|
+
*
|
|
418
|
+
* The difference is one prop, and it is deliberately all-or-nothing:
|
|
419
|
+
* a per-card `isMovable` would put the decision in the data, where a
|
|
420
|
+
* consumer has to remember to set it, and where a card that cannot
|
|
421
|
+
* move looks identical to one nobody has thought about.
|
|
422
|
+
*/
|
|
423
|
+
export const AllVariants: Story = {
|
|
424
|
+
render: (boardProps) => (
|
|
425
|
+
<StoryGrid columns={1}>
|
|
426
|
+
<StoryCell
|
|
427
|
+
align="stretch"
|
|
428
|
+
label="read-only — no `onMove`, so no handles"
|
|
429
|
+
>
|
|
430
|
+
<Board {...boardProps} label="Today, read-only" />
|
|
431
|
+
</StoryCell>
|
|
432
|
+
|
|
433
|
+
<StoryCell
|
|
434
|
+
align="stretch"
|
|
435
|
+
label="movable — every card carries a Move handle"
|
|
436
|
+
>
|
|
437
|
+
<Board
|
|
438
|
+
{...boardProps}
|
|
439
|
+
label="Today, movable"
|
|
440
|
+
onMove={() => undefined}
|
|
441
|
+
/>
|
|
442
|
+
</StoryCell>
|
|
443
|
+
|
|
444
|
+
<StoryCell
|
|
445
|
+
align="stretch"
|
|
446
|
+
label="movable, with the app's own `moveIcon` — ~55px a row cheaper"
|
|
447
|
+
>
|
|
448
|
+
<Board
|
|
449
|
+
{...boardProps}
|
|
450
|
+
label="Today, movable with an icon"
|
|
451
|
+
moveIcon={<MoreIcon />}
|
|
452
|
+
onMove={() => undefined}
|
|
453
|
+
/>
|
|
454
|
+
</StoryCell>
|
|
455
|
+
</StoryGrid>
|
|
456
|
+
),
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* The states a board is actually in, and the two nobody screenshots:
|
|
461
|
+
* a lane with nothing in it, and a lane truncated with more behind
|
|
462
|
+
* it.
|
|
463
|
+
*
|
|
464
|
+
* The empty lane is a real `EmptyState` with its own wording, not a
|
|
465
|
+
* grey "Empty". *"Real empty states"* was a stated requirement,
|
|
466
|
+
* because the empty lane is the one a person looks at most on a good
|
|
467
|
+
* day.
|
|
468
|
+
*/
|
|
469
|
+
export const AllStates: Story = {
|
|
470
|
+
render: (boardProps) => (
|
|
471
|
+
<StoryGrid columns={1}>
|
|
472
|
+
<StoryCell
|
|
473
|
+
align="stretch"
|
|
474
|
+
label="72rem — wide enough for three-up; an empty lane, a full one, and a truncated one"
|
|
475
|
+
>
|
|
476
|
+
<Frame inlineSize="72rem">
|
|
477
|
+
<Board
|
|
478
|
+
{...boardProps}
|
|
479
|
+
label="Today, mixed lanes"
|
|
480
|
+
lanes={[
|
|
481
|
+
{
|
|
482
|
+
emptyState: (
|
|
483
|
+
<EmptyState
|
|
484
|
+
description="Pull something over from the backlog."
|
|
485
|
+
heading="Nothing chosen yet"
|
|
486
|
+
headingLevel={4}
|
|
487
|
+
size="sm"
|
|
488
|
+
/>
|
|
489
|
+
),
|
|
490
|
+
items: [],
|
|
491
|
+
key: "todo",
|
|
492
|
+
label: "Todo",
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
items: IN_PROGRESS_ITEMS,
|
|
496
|
+
key: "in-progress",
|
|
497
|
+
label: "In Progress",
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
itemCount: 19,
|
|
501
|
+
items: REVIEW_ITEMS,
|
|
502
|
+
key: "review",
|
|
503
|
+
label: "Needs Review",
|
|
504
|
+
onShowMore: () => undefined,
|
|
505
|
+
},
|
|
506
|
+
]}
|
|
507
|
+
/>
|
|
508
|
+
</Frame>
|
|
509
|
+
</StoryCell>
|
|
510
|
+
</StoryGrid>
|
|
511
|
+
),
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* **The story that proves the whole design**, and the only honest
|
|
516
|
+
* way to show it: three fixed-width panels inside one unchanged
|
|
517
|
+
* browser window.
|
|
518
|
+
*
|
|
519
|
+
* The narrow panel is not a phone. It is the same board in a
|
|
520
|
+
* sidebar, in a split window, or on a display the owner has zoomed
|
|
521
|
+
* to 175% — and a media query cannot tell any of those from a
|
|
522
|
+
* maximised 4K window. Resizing the viewport would prove nothing
|
|
523
|
+
* here, because the thing that changed is the component's container.
|
|
524
|
+
*
|
|
525
|
+
* Watch two things flip independently. The board's own box decides
|
|
526
|
+
* whether the lanes are three-up or one-at-a-time behind a segmented
|
|
527
|
+
* control; each **lane's** box decides whether a card is two lines,
|
|
528
|
+
* one line, or a card of its own.
|
|
529
|
+
*/
|
|
530
|
+
export const Responsive: Story = {
|
|
531
|
+
render: (boardProps) => (
|
|
532
|
+
<div className="flex flex-col gap-8">
|
|
533
|
+
<ContainerBoard>
|
|
534
|
+
{(width) => (
|
|
535
|
+
<Board
|
|
536
|
+
{...boardProps}
|
|
537
|
+
label={`Today at ${width}`}
|
|
538
|
+
/>
|
|
539
|
+
)}
|
|
540
|
+
</ContainerBoard>
|
|
541
|
+
|
|
542
|
+
<div className="flex flex-col gap-2">
|
|
543
|
+
<span className="font-mono text-content-muted text-xs">
|
|
544
|
+
a lane past --cq-lg (48rem) — the rows become
|
|
545
|
+
cards
|
|
546
|
+
</span>
|
|
547
|
+
|
|
548
|
+
{/*
|
|
549
|
+
* A **lane** wider than 48rem: the third row shape, and the
|
|
550
|
+
* one it is easy to believe is unreachable. It is not. A
|
|
551
|
+
* board with one or two lanes gives each of them most of the
|
|
552
|
+
* board's width, and three lanes clear it on a 4K display —
|
|
553
|
+
* the frame here is 56rem, an ordinary content column.
|
|
554
|
+
*/}
|
|
555
|
+
<Frame inlineSize="56rem">
|
|
556
|
+
<Board
|
|
557
|
+
label="Needs Review, one lane"
|
|
558
|
+
lanes={[
|
|
559
|
+
{
|
|
560
|
+
itemCount: 19,
|
|
561
|
+
items: REVIEW_ITEMS.slice(0, 4),
|
|
562
|
+
key: "review",
|
|
563
|
+
label: "Needs Review",
|
|
564
|
+
},
|
|
565
|
+
]}
|
|
566
|
+
/>
|
|
567
|
+
</Frame>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
),
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* The complete keyboard path, and it is the **primary** way to move
|
|
575
|
+
* a card rather than an accessible alternative to the real one.
|
|
576
|
+
*
|
|
577
|
+
* Tab to a card's `Move` handle, press Enter, arrow to a lane, press
|
|
578
|
+
* Enter again. The card moves, and a `role="status"` region says
|
|
579
|
+
* where it went and what position it landed in — the one thing a
|
|
580
|
+
* sighted user gets free from watching it land, and the thing every
|
|
581
|
+
* drag implementation forgets to say out loud.
|
|
582
|
+
*
|
|
583
|
+
* Dragging the same handle with a mouse, a finger or a pen does the
|
|
584
|
+
* same thing through the same callback. Escape abandons a drag in
|
|
585
|
+
* flight.
|
|
586
|
+
*/
|
|
587
|
+
const MovableBoard = (): ReactNode => {
|
|
588
|
+
const [lanes, setLanes] = useState<BoardLane[]>([
|
|
589
|
+
{ items: TODO_ITEMS, key: "todo", label: "Todo" },
|
|
590
|
+
{
|
|
591
|
+
items: IN_PROGRESS_ITEMS,
|
|
592
|
+
key: "in-progress",
|
|
593
|
+
label: "In Progress",
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
items: REVIEW_ITEMS.slice(0, 4),
|
|
597
|
+
key: "review",
|
|
598
|
+
label: "Needs Review",
|
|
599
|
+
},
|
|
600
|
+
])
|
|
601
|
+
|
|
602
|
+
const applyMove = (move: BoardMove) => {
|
|
603
|
+
setLanes((current) => {
|
|
604
|
+
const moved = current
|
|
605
|
+
.find((lane) => lane.key === move.fromLaneKey)
|
|
606
|
+
?.items.find((item) => item.key === move.itemKey)
|
|
607
|
+
|
|
608
|
+
if (!moved) {
|
|
609
|
+
return current
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// `toIndex` already accounts for the removal, so this really
|
|
613
|
+
// is a splice out and a splice in with no arithmetic.
|
|
614
|
+
return current.map((lane) => {
|
|
615
|
+
const kept =
|
|
616
|
+
lane.key === move.fromLaneKey
|
|
617
|
+
? lane.items.filter(
|
|
618
|
+
(item) => item.key !== move.itemKey,
|
|
619
|
+
)
|
|
620
|
+
: lane.items
|
|
621
|
+
|
|
622
|
+
if (lane.key !== move.toLaneKey) {
|
|
623
|
+
return { ...lane, items: kept }
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return {
|
|
627
|
+
...lane,
|
|
628
|
+
items: [
|
|
629
|
+
...kept.slice(0, move.toIndex),
|
|
630
|
+
moved,
|
|
631
|
+
...kept.slice(move.toIndex),
|
|
632
|
+
],
|
|
633
|
+
}
|
|
634
|
+
})
|
|
635
|
+
})
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return (
|
|
639
|
+
// Framed wide on purpose: the keyboard path is identical at
|
|
640
|
+
// every width, but a reader who moves a card wants to watch it
|
|
641
|
+
// land in the lane it went to rather than take the segmented
|
|
642
|
+
// control's word for it.
|
|
643
|
+
<Frame inlineSize="72rem">
|
|
644
|
+
<Board
|
|
645
|
+
label="Today, movable"
|
|
646
|
+
lanes={lanes}
|
|
647
|
+
moveIcon={<MoreIcon />}
|
|
648
|
+
onMove={applyMove}
|
|
649
|
+
/>
|
|
650
|
+
</Frame>
|
|
651
|
+
)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export const Interactive: Story = {
|
|
655
|
+
render: () => <MovableBoard />,
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Where it actually goes, and what the consumer still owns.
|
|
660
|
+
*
|
|
661
|
+
* The **"needs attention"** banner is not part of `Board`, and that
|
|
662
|
+
* is a decision rather than an omission: it reaches across every
|
|
663
|
+
* lane, so it is not a lane; it is absent when empty, so it is not
|
|
664
|
+
* furniture the board can reserve room for; and it is already
|
|
665
|
+
* `Alert` — the component this library shipped for exactly this
|
|
666
|
+
* shape. Composing it here rather than absorbing it keeps `Board`
|
|
667
|
+
* about lanes and cards, and keeps the banner reusable above a list
|
|
668
|
+
* that is not a board at all.
|
|
669
|
+
*/
|
|
670
|
+
export const InBoardScreen: Story = {
|
|
671
|
+
render: (boardProps) => (
|
|
672
|
+
<div className="flex flex-col gap-5">
|
|
673
|
+
<div className="flex flex-col gap-1">
|
|
674
|
+
<h1 className="font-display font-semibold text-2xl text-content-primary">
|
|
675
|
+
Today
|
|
676
|
+
</h1>
|
|
677
|
+
|
|
678
|
+
<p className="text-content-secondary text-sm">
|
|
679
|
+
Across every project — 6 chosen, 5 running, 19
|
|
680
|
+
waiting on you
|
|
681
|
+
</p>
|
|
682
|
+
</div>
|
|
683
|
+
|
|
684
|
+
<Alert
|
|
685
|
+
description="Three things are stuck. Nothing below moves until they are dealt with."
|
|
686
|
+
details={[
|
|
687
|
+
"Run failed — Link queue for the weekly digest: the ingest token expired after 3 retries",
|
|
688
|
+
"Blocked — Print farm queue: waiting on filament",
|
|
689
|
+
"Stale — Fingerprint duplicate uploads: in Todo 19 days",
|
|
690
|
+
]}
|
|
691
|
+
heading="Needs attention"
|
|
692
|
+
intent="danger"
|
|
693
|
+
label="Needs attention"
|
|
694
|
+
/>
|
|
695
|
+
|
|
696
|
+
<Board
|
|
697
|
+
{...boardProps}
|
|
698
|
+
// The page's own `<h1>` is above it, so the lanes are `<h2>`
|
|
699
|
+
// — an `<h3>` here is an axe `heading-order` failure and a
|
|
700
|
+
// hole in the screen-reader outline.
|
|
701
|
+
headingLevel={2}
|
|
702
|
+
moveIcon={<MoreIcon />}
|
|
703
|
+
onMove={() => undefined}
|
|
704
|
+
/>
|
|
705
|
+
</div>
|
|
706
|
+
),
|
|
707
|
+
}
|