@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.
- package/README.md +13 -0
- package/dist/GroupedList/GroupedList.d.ts +105 -0
- package/dist/GroupedList/GroupedList.d.ts.map +1 -0
- package/dist/GroupedList/GroupedList.js +63 -0
- package/dist/GroupedList/GroupedList.js.map +1 -0
- package/dist/GroupedList/GroupedListRow.d.ts +29 -0
- package/dist/GroupedList/GroupedListRow.d.ts.map +1 -0
- package/dist/GroupedList/GroupedListRow.js +38 -0
- package/dist/GroupedList/GroupedListRow.js.map +1 -0
- 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/GroupedList/GroupedList.mdx +67 -0
- package/src/GroupedList/GroupedList.stories.tsx +474 -0
- package/src/GroupedList/GroupedList.test.tsx +271 -0
- package/src/GroupedList/GroupedList.tsx +200 -0
- package/src/GroupedList/GroupedListRow.tsx +117 -0
- package/src/index.ts +8 -0
- package/src/sourceRules.test.ts +4 -1
- package/src/Button/Button.test.tsx +0 -114
|
@@ -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
|
+
}
|