@charcuterie/ui 4.22.1 → 4.23.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.
@@ -0,0 +1,474 @@
1
+ import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+
4
+ import { Badge } from "../Badge/Badge.tsx"
5
+ import { Button } from "../Button/Button.tsx"
6
+ import {
7
+ ContainerBoard,
8
+ StoryCell,
9
+ StoryGrid,
10
+ } from "../board.storyHelpers.tsx"
11
+ import { EmptyState } from "../EmptyState/EmptyState.tsx"
12
+ import type { GroupedListGroup } from "./GroupedList.tsx"
13
+ import { GroupedList } from "./GroupedList.tsx"
14
+
15
+ /**
16
+ * Fixture data only. A signage app's pages, grouped by the screen
17
+ * that shows them — the shape castkit's page picker draws.
18
+ */
19
+ const openButton = (name: string, prefix = "") => (
20
+ <Button
21
+ appearance="outline"
22
+ aria-label={`Open ${prefix}${name}`}
23
+ size="sm"
24
+ >
25
+ Open
26
+ </Button>
27
+ )
28
+
29
+ const SIGNAGE_GROUPS: GroupedListGroup[] = [
30
+ {
31
+ heading: "Screens",
32
+ items: [
33
+ {
34
+ actions: openButton("Lobby"),
35
+ badges: <Badge size="sm">PIN</Badge>,
36
+ description:
37
+ "3 views: Welcome, Weather, and Schedule",
38
+ key: "lobby",
39
+ label: "Lobby",
40
+ },
41
+ {
42
+ actions: openButton("Break Room"),
43
+ description: "1 view: Menu",
44
+ key: "break-room",
45
+ label: "Break Room",
46
+ },
47
+ ],
48
+ key: "screens",
49
+ },
50
+ {
51
+ heading: "On the Lobby screen",
52
+ items: [
53
+ {
54
+ actions: openButton("Welcome"),
55
+ description: "/views/welcome",
56
+ key: "welcome",
57
+ label: "Welcome",
58
+ },
59
+ {
60
+ actions: openButton("Weather"),
61
+ description: "/views/weather",
62
+ key: "weather",
63
+ label: "Weather",
64
+ },
65
+ {
66
+ actions: openButton("Schedule"),
67
+ description: "/views/schedule",
68
+ key: "schedule",
69
+ label: "Schedule",
70
+ },
71
+ ],
72
+ key: "screen:lobby",
73
+ },
74
+ {
75
+ heading: "Not on a screen",
76
+ items: [
77
+ {
78
+ actions: openButton("Test Pattern"),
79
+ description: "/views/test-pattern",
80
+ key: "test-pattern",
81
+ label: "Test Pattern",
82
+ },
83
+ ],
84
+ key: "views",
85
+ },
86
+ ]
87
+
88
+ /**
89
+ * Fixture data only. Chores grouped by the team that earns them,
90
+ * each with a summary, badges and an Edit button — the shape
91
+ * tally-marks' card list draws.
92
+ */
93
+ const CHORE_GROUPS: GroupedListGroup[] = [
94
+ {
95
+ heading: "Team Orion",
96
+ items: [
97
+ {
98
+ actions: (
99
+ <Button
100
+ appearance="outline"
101
+ aria-label="Edit Water the ferns"
102
+ size="sm"
103
+ >
104
+ Edit
105
+ </Button>
106
+ ),
107
+ badges: (
108
+ <>
109
+ <Badge
110
+ appearance="soft"
111
+ intent="info"
112
+ size="sm"
113
+ >
114
+ Once a day
115
+ </Badge>
116
+
117
+ <Badge
118
+ appearance="soft"
119
+ intent="warning"
120
+ size="sm"
121
+ >
122
+ after 3:00 PM
123
+ </Badge>
124
+ </>
125
+ ),
126
+ content: "Up to 5 pts a day",
127
+ description: "water-ferns",
128
+ key: "water-ferns",
129
+ label: "Water the ferns",
130
+ },
131
+ {
132
+ actions: (
133
+ <Button
134
+ appearance="outline"
135
+ aria-label="Edit Sort the recycling"
136
+ size="sm"
137
+ >
138
+ Edit
139
+ </Button>
140
+ ),
141
+ badges: (
142
+ <Badge appearance="soft" size="sm">
143
+ No tag
144
+ </Badge>
145
+ ),
146
+ content: "Up to 3 scans a day · 6 pts max",
147
+ description: "sort-recycling",
148
+ key: "sort-recycling",
149
+ label: "Sort the recycling",
150
+ },
151
+ ],
152
+ key: "orion",
153
+ summary: "2 cards · up to 11 pts a day",
154
+ },
155
+ {
156
+ heading: "Team Vega",
157
+ items: [
158
+ {
159
+ actions: (
160
+ <Button
161
+ appearance="outline"
162
+ aria-label="Edit Feed the goldfish"
163
+ size="sm"
164
+ >
165
+ Edit
166
+ </Button>
167
+ ),
168
+ badges: (
169
+ <Badge appearance="soft" intent="info" size="sm">
170
+ Once a day
171
+ </Badge>
172
+ ),
173
+ content: "Up to 2 pts a day",
174
+ description: "feed-goldfish",
175
+ key: "feed-goldfish",
176
+ label: "Feed the goldfish",
177
+ },
178
+ ],
179
+ key: "vega",
180
+ summary: "1 card · up to 2 pts a day",
181
+ },
182
+ ]
183
+
184
+ const LONG_GROUPS: GroupedListGroup[] = [
185
+ {
186
+ heading:
187
+ "On the Conference Center North Atrium Wayfinding screen",
188
+ items: [
189
+ {
190
+ actions: openButton(
191
+ "Quarterly All-Hands Countdown and Seating Chart",
192
+ ),
193
+ badges: (
194
+ <>
195
+ <Badge size="sm">PIN</Badge>
196
+
197
+ <Badge
198
+ appearance="soft"
199
+ intent="info"
200
+ size="sm"
201
+ >
202
+ Rotates every 30 s
203
+ </Badge>
204
+ </>
205
+ ),
206
+ description:
207
+ "/views/quarterly-all-hands-countdown-and-seating-chart-for-the-north-atrium",
208
+ key: "all-hands",
209
+ label:
210
+ "Quarterly All-Hands Countdown and Seating Chart",
211
+ },
212
+ {
213
+ actions: openButton("Map"),
214
+ description: "/views/map",
215
+ key: "map",
216
+ label: "Map",
217
+ },
218
+ ],
219
+ key: "atrium",
220
+ },
221
+ ]
222
+
223
+ /**
224
+ * A cap rather than a width, so the long label clips in every window
225
+ * — an ultrawide grid cell would otherwise hold it whole and the
226
+ * truncation half of the board would show nothing.
227
+ */
228
+ const TRUNCATION_CELL_STYLE = { maxInlineSize: "20rem" }
229
+
230
+ const meta = {
231
+ title: "Components/Data/GroupedList",
232
+ component: GroupedList,
233
+ parameters: { layout: "padded" },
234
+ args: {
235
+ groups: SIGNAGE_GROUPS,
236
+ headingLevel: 3,
237
+ isTextTruncated: true,
238
+ size: "md",
239
+ },
240
+ } satisfies Meta<typeof GroupedList>
241
+
242
+ export default meta
243
+
244
+ type Story = StoryObj<typeof meta>
245
+
246
+ /**
247
+ * A signage app's pages under the screen that shows them. Each row is
248
+ * a name, a muted line, an optional badge, and one action named
249
+ * after its row.
250
+ */
251
+ export const Playground: Story = {
252
+ parameters: playgroundParameters,
253
+ }
254
+
255
+ /**
256
+ * `size` and `isTextTruncated` are the two axes. `sm` is a settings
257
+ * pane or a sidebar; `md` is a page. Wrapping is for a list whose
258
+ * rows are read rather than scanned.
259
+ */
260
+ export const AllVariants: Story = {
261
+ render: () => (
262
+ <StoryGrid columns={2}>
263
+ <StoryCell align="stretch" label="md (default)">
264
+ <GroupedList groups={CHORE_GROUPS} />
265
+ </StoryCell>
266
+
267
+ <StoryCell align="stretch" label="sm">
268
+ <GroupedList groups={CHORE_GROUPS} size="sm" />
269
+ </StoryCell>
270
+
271
+ <StoryCell
272
+ align="stretch"
273
+ label="isTextTruncated (default)"
274
+ >
275
+ <div style={TRUNCATION_CELL_STYLE}>
276
+ <GroupedList groups={LONG_GROUPS} size="sm" />
277
+ </div>
278
+ </StoryCell>
279
+
280
+ <StoryCell
281
+ align="stretch"
282
+ label="isTextTruncated={false}"
283
+ >
284
+ <div style={TRUNCATION_CELL_STYLE}>
285
+ <GroupedList
286
+ groups={LONG_GROUPS}
287
+ isTextTruncated={false}
288
+ size="sm"
289
+ />
290
+ </div>
291
+ </StoryCell>
292
+ </StoryGrid>
293
+ ),
294
+ }
295
+
296
+ /**
297
+ * Every optional part of a row, added one at a time — and the two
298
+ * group states: an empty group is not drawn, and a list with no
299
+ * items at all draws its `emptyState`.
300
+ */
301
+ export const AllStates: Story = {
302
+ render: () => (
303
+ <StoryGrid columns={2}>
304
+ <StoryCell align="stretch" label="label only">
305
+ <GroupedList
306
+ groups={[
307
+ {
308
+ heading: "Label only",
309
+ items: [
310
+ { key: "alpha", label: "Alpha" },
311
+ { key: "beta", label: "Beta" },
312
+ ],
313
+ key: "plain",
314
+ },
315
+ ]}
316
+ size="sm"
317
+ />
318
+ </StoryCell>
319
+
320
+ <StoryCell
321
+ align="stretch"
322
+ label="description, badges, content, actions"
323
+ >
324
+ <GroupedList
325
+ groups={[CHORE_GROUPS[1] as GroupedListGroup]}
326
+ size="sm"
327
+ />
328
+ </StoryCell>
329
+
330
+ <StoryCell
331
+ align="stretch"
332
+ label="an empty group is not drawn"
333
+ >
334
+ <GroupedList
335
+ groups={[
336
+ {
337
+ heading: "Never drawn",
338
+ items: [],
339
+ key: "empty",
340
+ },
341
+ {
342
+ heading: "Drawn",
343
+ items: [{ key: "gamma", label: "Gamma" }],
344
+ key: "drawn",
345
+ },
346
+ ]}
347
+ size="sm"
348
+ />
349
+ </StoryCell>
350
+
351
+ <StoryCell
352
+ align="stretch"
353
+ label="every group empty: emptyState"
354
+ >
355
+ <GroupedList
356
+ emptyState={
357
+ <EmptyState
358
+ description="Try fewer words."
359
+ heading="No page matches"
360
+ headingLevel={3}
361
+ size="sm"
362
+ />
363
+ }
364
+ groups={[
365
+ {
366
+ heading: "Screens",
367
+ items: [],
368
+ key: "screens",
369
+ },
370
+ ]}
371
+ size="sm"
372
+ />
373
+ </StoryCell>
374
+ </StoryGrid>
375
+ ),
376
+ }
377
+
378
+ /**
379
+ * The text column asks for 12rem and grows. Below that, the actions
380
+ * drop under the text instead of crushing the label — decided by the
381
+ * content, with no container query and no media query.
382
+ */
383
+ export const Responsive: Story = {
384
+ render: () => (
385
+ <ContainerBoard>
386
+ <GroupedList groups={CHORE_GROUPS} size="sm" />
387
+ </ContainerBoard>
388
+ ),
389
+ }
390
+
391
+ /**
392
+ * Tab walks the actions in reading order, group by group. Nothing
393
+ * else in the list takes focus: a row is not a control, and its
394
+ * action carries its name ("Open Lobby").
395
+ */
396
+ export const Interactive: Story = {}
397
+
398
+ /**
399
+ * A long label and a long path clip to one line, and the full text
400
+ * is the tooltip while — and only while — it is clipped. The short
401
+ * row underneath has no tooltip at all.
402
+ */
403
+ export const LongText: Story = {
404
+ render: () => (
405
+ <div style={{ inlineSize: "28rem" }}>
406
+ <GroupedList groups={LONG_GROUPS} />
407
+ </div>
408
+ ),
409
+ }
410
+
411
+ /**
412
+ * Nothing to list. The app chooses the wording — "nothing yet" and
413
+ * "nothing matches" are different sentences — and passes it as
414
+ * `emptyState`.
415
+ */
416
+ export const Empty: Story = {
417
+ args: {
418
+ emptyState: (
419
+ <EmptyState
420
+ description="Add screens and views in the management page."
421
+ heading="This server has no pages"
422
+ headingLevel={3}
423
+ size="sm"
424
+ />
425
+ ),
426
+ groups: [],
427
+ },
428
+ }
429
+
430
+ /**
431
+ * The Narrow View: a phone-width window. Badges wrap under the
432
+ * label, and the actions wrap under the text.
433
+ */
434
+ export const NarrowView: Story = {
435
+ args: { groups: CHORE_GROUPS, size: "sm" },
436
+ globals: { viewport: { value: "mobile1" } },
437
+ }
438
+
439
+ /**
440
+ * Light and dark side by side, each panel scoped with its own
441
+ * `data-scheme`, so both read at once whatever the toolbar says.
442
+ */
443
+ export const ColorSchemes: Story = {
444
+ render: () => (
445
+ <StoryGrid columns={2}>
446
+ {(["light", "dark"] as const).map((scheme) => (
447
+ <StoryCell
448
+ align="stretch"
449
+ key={scheme}
450
+ label={scheme}
451
+ >
452
+ <div
453
+ className="rounded-lg bg-surface-base p-4 text-content-primary"
454
+ data-scheme={scheme}
455
+ >
456
+ <GroupedList
457
+ groups={SIGNAGE_GROUPS.map((group) => ({
458
+ ...group,
459
+ items: group.items.map((item) => ({
460
+ ...item,
461
+ actions: openButton(
462
+ item.label,
463
+ `${scheme} `,
464
+ ),
465
+ })),
466
+ }))}
467
+ size="sm"
468
+ />
469
+ </div>
470
+ </StoryCell>
471
+ ))}
472
+ </StoryGrid>
473
+ ),
474
+ }