@charcuterie/ui 4.23.0 → 4.25.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/GroupedList/GroupedList.d.ts +104 -2
- package/dist/GroupedList/GroupedList.d.ts.map +1 -1
- package/dist/GroupedList/GroupedList.js +24 -2
- package/dist/GroupedList/GroupedList.js.map +1 -1
- package/dist/GroupedList/GroupedListRow.d.ts +17 -6
- package/dist/GroupedList/GroupedListRow.d.ts.map +1 -1
- package/dist/GroupedList/GroupedListRow.js +75 -8
- package/dist/GroupedList/GroupedListRow.js.map +1 -1
- package/dist/GroupedList/groupedListStickyHeading.d.ts +20 -0
- package/dist/GroupedList/groupedListStickyHeading.d.ts.map +1 -0
- package/dist/GroupedList/groupedListStickyHeading.js +20 -0
- package/dist/GroupedList/groupedListStickyHeading.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/styles.css +36 -4
- package/package.json +1 -1
- package/src/GroupedList/GroupedList.mdx +70 -1
- package/src/GroupedList/GroupedList.stories.tsx +339 -1
- package/src/GroupedList/GroupedList.test.tsx +510 -0
- package/src/GroupedList/GroupedList.tsx +115 -3
- package/src/GroupedList/GroupedListRow.tsx +135 -10
- package/src/GroupedList/groupedListStickyHeading.ts +24 -0
- package/src/index.ts +4 -0
- package/src/styles.css +36 -4
|
@@ -7,7 +7,9 @@ import * as GroupedListStories from "./GroupedList.stories";
|
|
|
7
7
|
# GroupedList
|
|
8
8
|
|
|
9
9
|
An always-open list whose rows sit under group headings. Each row is a label, an optional
|
|
10
|
-
muted description, optional badges, optional richer content,
|
|
10
|
+
muted description, optional badges, optional richer content, an optional figure beside the
|
|
11
|
+
text, and optional trailing actions. A row can also be the target itself, muted, or
|
|
12
|
+
highlighted.
|
|
11
13
|
|
|
12
14
|
Three apps drew this shape by hand: castkit's page picker (pages under the screen that shows
|
|
13
15
|
them), tally-marks' cards (cards under the team that earns them) and whereabouts' nested
|
|
@@ -56,6 +58,73 @@ There is no container query and no media query: the content decides.
|
|
|
56
58
|
|
|
57
59
|
<Canvas of={GroupedListStories.Responsive} />
|
|
58
60
|
|
|
61
|
+
## Sticky headings
|
|
62
|
+
|
|
63
|
+
`isHeadingSticky` pins each group's heading to the top of the nearest scrolling ancestor while
|
|
64
|
+
that group's rows scroll under it, and the next group's heading pushes it away. Use it for a
|
|
65
|
+
list inside a scrolling pane, where the heading is the only thing that says which screen or
|
|
66
|
+
team a row belongs to once the row scrolls out of sight. It is off by default.
|
|
67
|
+
|
|
68
|
+
A stuck heading needs an opaque background, or the rows show through it. The component cannot
|
|
69
|
+
know which surface it sits on — a page is `surface-base`, a settings pane is
|
|
70
|
+
`surface-raised` — so the host says so with a custom property:
|
|
71
|
+
|
|
72
|
+
| Property | Default | Set it to |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| `--charcuterie-grouped-list-heading-surface` | `var(--color-surface-base)` | The background of the pane the list sits on. |
|
|
75
|
+
| `--charcuterie-grouped-list-heading-offset` | `0px` | The height of anything already stuck to the top of the same scroll container, such as a sticky `Header`. |
|
|
76
|
+
|
|
77
|
+
Set either one on any ancestor of the list. The names are exported as
|
|
78
|
+
`GROUPED_LIST_HEADING_SURFACE_PROPERTY` and `GROUPED_LIST_HEADING_OFFSET_PROPERTY` for a host
|
|
79
|
+
that sets them through a `style` object.
|
|
80
|
+
|
|
81
|
+
```css
|
|
82
|
+
.settings-panel {
|
|
83
|
+
--charcuterie-grouped-list-heading-surface: var(--color-surface-raised);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
<Canvas of={GroupedListStories.StickyHeadings} />
|
|
88
|
+
|
|
89
|
+
## A row can be the target
|
|
90
|
+
|
|
91
|
+
Give an item `onSelect` and the whole row becomes one native `<button>` — "open this card"
|
|
92
|
+
for a list whose primary action is the row itself. `selectLabel` names it (it defaults to
|
|
93
|
+
`label`); name the verb, as with any repeated action: `selectLabel="Edit Water the ferns"`.
|
|
94
|
+
|
|
95
|
+
- **Every state is drawn.** Hover tints the row and rings its edge (only on devices that
|
|
96
|
+
hover, so a tap does not leave a row lit). A press sinks the row slightly, with motion only
|
|
97
|
+
when the user has not asked the OS for less. Keyboard focus draws the focus ring inside the
|
|
98
|
+
row's edge, where the list's rounded corners cannot clip it, and one ring-width in from it,
|
|
99
|
+
so a highlighted row that has focus shows both rings.
|
|
100
|
+
- **The actions stay their own targets.** The button is drawn under the row's text and the
|
|
101
|
+
text lets the pointer through to it; `actions` sit above it. Pressing "Delete" never opens
|
|
102
|
+
the row, and Tab reaches the row, then its actions, then the next row.
|
|
103
|
+
- **Nothing is nested in the button.** It is empty and named by `aria-label`, so `badges`,
|
|
104
|
+
`content` and `aside` must not hold controls — a control belongs in `actions`.
|
|
105
|
+
|
|
106
|
+
`aside` is a figure that belongs **beside** the text — a payout, a total. It asks for at
|
|
107
|
+
least 11rem next to the text column's 12rem, so it sits between the text and the actions in a
|
|
108
|
+
wide list and drops under the text in a narrow one.
|
|
109
|
+
|
|
110
|
+
<Canvas of={GroupedListStories.SelectableRows} />
|
|
111
|
+
|
|
112
|
+
## Muted and highlighted rows
|
|
113
|
+
|
|
114
|
+
- `isMuted` recedes a row that exists but is switched off: a sunken surface and a quieter
|
|
115
|
+
label. It stays fully operable, and it is a surface and a text role rather than an opacity,
|
|
116
|
+
because a faded row would drop below the AA contrast floor and only a disabled control may.
|
|
117
|
+
Say why in a badge as well ("off").
|
|
118
|
+
- `isHighlighted` rings the row and pulses the ring twice — "this one just changed", after a
|
|
119
|
+
scan or a save. `highlightIntent` picks the colour (`accent` by default). The ring stays
|
|
120
|
+
while the flag does, so the app decides how long it lasts, usually with a timer. **To pulse
|
|
121
|
+
an already-highlighted row again, change its `key`**: a new key is a new row, and a new row
|
|
122
|
+
starts the pulse from the beginning. Under `prefers-reduced-motion` and on ePaper the ring
|
|
123
|
+
does not pulse; it is simply there. The pulse lives in `@charcuterie/ui/styles.css`, which
|
|
124
|
+
every app already imports.
|
|
125
|
+
|
|
126
|
+
<Canvas of={GroupedListStories.RowStates} />
|
|
127
|
+
|
|
59
128
|
## Name a repeated action after its row
|
|
60
129
|
|
|
61
130
|
Ten rows of "Open" are ten buttons an agent cannot tell apart. Give each one a name that
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { type CSSProperties, useState } from "react"
|
|
3
4
|
|
|
4
5
|
import { Badge } from "../Badge/Badge.tsx"
|
|
5
6
|
import { Button } from "../Button/Button.tsx"
|
|
@@ -9,8 +10,13 @@ import {
|
|
|
9
10
|
StoryGrid,
|
|
10
11
|
} from "../board.storyHelpers.tsx"
|
|
11
12
|
import { EmptyState } from "../EmptyState/EmptyState.tsx"
|
|
12
|
-
import
|
|
13
|
+
import { ValuePairs } from "../ValuePairs/ValuePairs.tsx"
|
|
14
|
+
import type {
|
|
15
|
+
GroupedListGroup,
|
|
16
|
+
GroupedListItem,
|
|
17
|
+
} from "./GroupedList.tsx"
|
|
13
18
|
import { GroupedList } from "./GroupedList.tsx"
|
|
19
|
+
import { GROUPED_LIST_HEADING_SURFACE_PROPERTY } from "./groupedListStickyHeading.ts"
|
|
14
20
|
|
|
15
21
|
/**
|
|
16
22
|
* Fixture data only. A signage app's pages, grouped by the screen
|
|
@@ -181,6 +187,142 @@ const CHORE_GROUPS: GroupedListGroup[] = [
|
|
|
181
187
|
},
|
|
182
188
|
]
|
|
183
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Fixture data only. A chore card whose whole row opens its editor:
|
|
192
|
+
* the shape tally-marks' Rows view draws, with the payout beside the
|
|
193
|
+
* text and the daily cap under it.
|
|
194
|
+
*/
|
|
195
|
+
type ChoreCard = {
|
|
196
|
+
isActive: boolean
|
|
197
|
+
isHighlighted?: boolean
|
|
198
|
+
key: string
|
|
199
|
+
label: string
|
|
200
|
+
maxPerDay: number
|
|
201
|
+
points: number
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const CHORE_CARDS: ChoreCard[] = [
|
|
205
|
+
{
|
|
206
|
+
isActive: true,
|
|
207
|
+
key: "water-ferns",
|
|
208
|
+
label: "Water the ferns",
|
|
209
|
+
maxPerDay: 5,
|
|
210
|
+
points: 5,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
isActive: true,
|
|
214
|
+
isHighlighted: true,
|
|
215
|
+
key: "sort-recycling",
|
|
216
|
+
label: "Sort the recycling",
|
|
217
|
+
maxPerDay: 6,
|
|
218
|
+
points: 2,
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
isActive: false,
|
|
222
|
+
key: "feed-goldfish",
|
|
223
|
+
label: "Feed the goldfish",
|
|
224
|
+
maxPerDay: 2,
|
|
225
|
+
points: 2,
|
|
226
|
+
},
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* One chore card as a row. `onSelect` opens it; the action beside it
|
|
231
|
+
* is a separate target. A switched-off card is muted and says so in
|
|
232
|
+
* a badge.
|
|
233
|
+
*/
|
|
234
|
+
const toChoreRow = (
|
|
235
|
+
card: ChoreCard,
|
|
236
|
+
{
|
|
237
|
+
onDelete,
|
|
238
|
+
onSelect,
|
|
239
|
+
}: {
|
|
240
|
+
onDelete?: (label: string) => void
|
|
241
|
+
onSelect?: (label: string) => void
|
|
242
|
+
} = {},
|
|
243
|
+
): GroupedListItem => ({
|
|
244
|
+
actions: (
|
|
245
|
+
<Button
|
|
246
|
+
appearance="ghost"
|
|
247
|
+
aria-label={`Delete ${card.label}`}
|
|
248
|
+
intent="danger"
|
|
249
|
+
onClick={() => {
|
|
250
|
+
onDelete?.(card.label)
|
|
251
|
+
}}
|
|
252
|
+
size="sm"
|
|
253
|
+
>
|
|
254
|
+
Delete
|
|
255
|
+
</Button>
|
|
256
|
+
),
|
|
257
|
+
aside: (
|
|
258
|
+
<ValuePairs
|
|
259
|
+
items={[
|
|
260
|
+
{
|
|
261
|
+
id: "scan",
|
|
262
|
+
label: "Per scan",
|
|
263
|
+
value: `${card.points} pts`,
|
|
264
|
+
},
|
|
265
|
+
]}
|
|
266
|
+
label="Payout"
|
|
267
|
+
/>
|
|
268
|
+
),
|
|
269
|
+
badges: card.isActive ? undefined : (
|
|
270
|
+
<Badge appearance="soft" size="sm">
|
|
271
|
+
off
|
|
272
|
+
</Badge>
|
|
273
|
+
),
|
|
274
|
+
content: `Up to ${card.maxPerDay} pts a day`,
|
|
275
|
+
description: card.key,
|
|
276
|
+
highlightIntent: "success",
|
|
277
|
+
isHighlighted: card.isHighlighted,
|
|
278
|
+
isMuted: !card.isActive,
|
|
279
|
+
key: card.key,
|
|
280
|
+
label: card.label,
|
|
281
|
+
onSelect: () => {
|
|
282
|
+
onSelect?.(card.label)
|
|
283
|
+
},
|
|
284
|
+
selectLabel: `Edit ${card.label}`,
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
const toChoreGroups = (
|
|
288
|
+
handlers?: Parameters<typeof toChoreRow>[1],
|
|
289
|
+
): GroupedListGroup[] => [
|
|
290
|
+
{
|
|
291
|
+
heading: "Team Orion",
|
|
292
|
+
items: CHORE_CARDS.map((card) =>
|
|
293
|
+
toChoreRow(card, handlers),
|
|
294
|
+
),
|
|
295
|
+
key: "orion",
|
|
296
|
+
summary: "3 cards · up to 11 pts a day",
|
|
297
|
+
},
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Fixture data only. The same plain row in each highlight intent.
|
|
302
|
+
*/
|
|
303
|
+
const HIGHLIGHT_INTENT_GROUPS: GroupedListGroup[] = [
|
|
304
|
+
{
|
|
305
|
+
heading: "Highlight intents",
|
|
306
|
+
items: (
|
|
307
|
+
[
|
|
308
|
+
"accent",
|
|
309
|
+
"success",
|
|
310
|
+
"info",
|
|
311
|
+
"warning",
|
|
312
|
+
"danger",
|
|
313
|
+
"neutral",
|
|
314
|
+
] as const
|
|
315
|
+
).map((intent) => ({
|
|
316
|
+
description: `highlightIntent="${intent}"`,
|
|
317
|
+
highlightIntent: intent,
|
|
318
|
+
isHighlighted: true,
|
|
319
|
+
key: intent,
|
|
320
|
+
label: `Highlighted ${intent}`,
|
|
321
|
+
})),
|
|
322
|
+
key: "intents",
|
|
323
|
+
},
|
|
324
|
+
]
|
|
325
|
+
|
|
184
326
|
const LONG_GROUPS: GroupedListGroup[] = [
|
|
185
327
|
{
|
|
186
328
|
heading:
|
|
@@ -220,6 +362,45 @@ const LONG_GROUPS: GroupedListGroup[] = [
|
|
|
220
362
|
},
|
|
221
363
|
]
|
|
222
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Fixture data only. Enough screens that the pane has to scroll, so
|
|
367
|
+
* a stuck heading has rows to sit over.
|
|
368
|
+
*/
|
|
369
|
+
const MANY_SCREEN_GROUPS: GroupedListGroup[] = [
|
|
370
|
+
"Lobby",
|
|
371
|
+
"Break Room",
|
|
372
|
+
"Loading Dock",
|
|
373
|
+
].map((screen) => ({
|
|
374
|
+
heading: `On the ${screen} screen`,
|
|
375
|
+
items: ["Welcome", "Weather", "Schedule", "Menu"].map(
|
|
376
|
+
(view) => ({
|
|
377
|
+
actions: openButton(`${view} on ${screen}`),
|
|
378
|
+
description: `/views/${view.toLowerCase()}`,
|
|
379
|
+
key: view,
|
|
380
|
+
label: view,
|
|
381
|
+
}),
|
|
382
|
+
),
|
|
383
|
+
key: screen,
|
|
384
|
+
}))
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* A fixed block size is what makes the pane scroll; the heading
|
|
388
|
+
* sticks to the pane, the nearest scrolling ancestor.
|
|
389
|
+
*/
|
|
390
|
+
const SCROLL_PANE_STYLE: CSSProperties = {
|
|
391
|
+
blockSize: "16rem",
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The host says which surface the list sits on. The pane is raised,
|
|
396
|
+
* so the stuck heading is raised too.
|
|
397
|
+
*/
|
|
398
|
+
const RAISED_PANE_STYLE = {
|
|
399
|
+
...SCROLL_PANE_STYLE,
|
|
400
|
+
[GROUPED_LIST_HEADING_SURFACE_PROPERTY]:
|
|
401
|
+
"var(--color-surface-raised)",
|
|
402
|
+
} as CSSProperties
|
|
403
|
+
|
|
223
404
|
/**
|
|
224
405
|
* A cap rather than a width, so the long label clips in every window
|
|
225
406
|
* — an ultrawide grid cell would otherwise hold it whole and the
|
|
@@ -436,6 +617,163 @@ export const NarrowView: Story = {
|
|
|
436
617
|
globals: { viewport: { value: "mobile1" } },
|
|
437
618
|
}
|
|
438
619
|
|
|
620
|
+
/**
|
|
621
|
+
* `isHeadingSticky` pins each heading to the top of the scrolling
|
|
622
|
+
* pane while its rows scroll under it. On the left the pane is the
|
|
623
|
+
* page surface, which is the default heading background. On the
|
|
624
|
+
* right the pane is raised, and it sets
|
|
625
|
+
* `--charcuterie-grouped-list-heading-surface` to match.
|
|
626
|
+
*/
|
|
627
|
+
export const StickyHeadings: Story = {
|
|
628
|
+
render: () => (
|
|
629
|
+
<StoryGrid columns={2}>
|
|
630
|
+
<StoryCell
|
|
631
|
+
align="stretch"
|
|
632
|
+
label="default: surface-base"
|
|
633
|
+
>
|
|
634
|
+
<section
|
|
635
|
+
aria-label="Pages on a base surface"
|
|
636
|
+
className="overflow-y-auto rounded-lg border border-border-subtle bg-surface-base px-3"
|
|
637
|
+
style={SCROLL_PANE_STYLE}
|
|
638
|
+
>
|
|
639
|
+
<GroupedList
|
|
640
|
+
groups={MANY_SCREEN_GROUPS}
|
|
641
|
+
isHeadingSticky
|
|
642
|
+
size="sm"
|
|
643
|
+
/>
|
|
644
|
+
</section>
|
|
645
|
+
</StoryCell>
|
|
646
|
+
|
|
647
|
+
<StoryCell
|
|
648
|
+
align="stretch"
|
|
649
|
+
label="host sets the surface: surface-raised"
|
|
650
|
+
>
|
|
651
|
+
<section
|
|
652
|
+
aria-label="Pages on a raised surface"
|
|
653
|
+
className="overflow-y-auto rounded-lg border border-border-subtle bg-surface-raised px-3"
|
|
654
|
+
style={RAISED_PANE_STYLE}
|
|
655
|
+
>
|
|
656
|
+
<GroupedList
|
|
657
|
+
groups={MANY_SCREEN_GROUPS}
|
|
658
|
+
isHeadingSticky
|
|
659
|
+
size="sm"
|
|
660
|
+
/>
|
|
661
|
+
</section>
|
|
662
|
+
</StoryCell>
|
|
663
|
+
</StoryGrid>
|
|
664
|
+
),
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* A row whose whole surface is its primary action. `onSelect` draws
|
|
669
|
+
* one native button under the row's text, named by `selectLabel`;
|
|
670
|
+
* the Delete action sits above it as its own target. The status
|
|
671
|
+
* line under the list says which one a click reached.
|
|
672
|
+
*
|
|
673
|
+
* The second card is highlighted (a scan just paid it), and the
|
|
674
|
+
* third is switched off, so it is muted.
|
|
675
|
+
*/
|
|
676
|
+
export const SelectableRows: Story = {
|
|
677
|
+
render: function SelectableRowsStory() {
|
|
678
|
+
const [lastEvent, setLastEvent] = useState(
|
|
679
|
+
"Nothing chosen yet",
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
return (
|
|
683
|
+
<div className="flex flex-col gap-3">
|
|
684
|
+
<GroupedList
|
|
685
|
+
groups={toChoreGroups({
|
|
686
|
+
onDelete: (label) => {
|
|
687
|
+
setLastEvent(`Deleted ${label}`)
|
|
688
|
+
},
|
|
689
|
+
onSelect: (label) => {
|
|
690
|
+
setLastEvent(`Opened ${label}`)
|
|
691
|
+
},
|
|
692
|
+
})}
|
|
693
|
+
/>
|
|
694
|
+
|
|
695
|
+
<p
|
|
696
|
+
className="m-0 text-content-secondary text-sm"
|
|
697
|
+
role="status"
|
|
698
|
+
>
|
|
699
|
+
{lastEvent}
|
|
700
|
+
</p>
|
|
701
|
+
</div>
|
|
702
|
+
)
|
|
703
|
+
},
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Every per-row state at once: hover, press and keyboard focus on
|
|
708
|
+
* the whole-row button (forced by the pseudo-states addon, the only
|
|
709
|
+
* way to review them in a static board), a muted row, and a
|
|
710
|
+
* highlight in each intent. The highlight's pulse is frozen here, so
|
|
711
|
+
* this is also its static, reduced-motion look.
|
|
712
|
+
*/
|
|
713
|
+
export const RowStates: Story = {
|
|
714
|
+
parameters: {
|
|
715
|
+
pseudo: {
|
|
716
|
+
active: ["#active-row li:first-child > button"],
|
|
717
|
+
focusVisible: ["#focus-row li:first-child > button"],
|
|
718
|
+
hover: ["#hover-row li:first-child > button"],
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
render: () => (
|
|
722
|
+
<StoryGrid columns={2}>
|
|
723
|
+
{(
|
|
724
|
+
[
|
|
725
|
+
["resting", "resting-row"],
|
|
726
|
+
["hover (forced)", "hover-row"],
|
|
727
|
+
["active (forced)", "active-row"],
|
|
728
|
+
["focus-visible (forced)", "focus-row"],
|
|
729
|
+
] as const
|
|
730
|
+
).map(([label, id]) => (
|
|
731
|
+
<StoryCell align="stretch" key={id} label={label}>
|
|
732
|
+
<div id={id}>
|
|
733
|
+
<GroupedList
|
|
734
|
+
groups={[
|
|
735
|
+
{
|
|
736
|
+
heading: `Team Orion, ${label}`,
|
|
737
|
+
items: CHORE_CARDS.slice(0, 1).map(
|
|
738
|
+
(card) => toChoreRow(card),
|
|
739
|
+
),
|
|
740
|
+
key: id,
|
|
741
|
+
},
|
|
742
|
+
]}
|
|
743
|
+
size="sm"
|
|
744
|
+
/>
|
|
745
|
+
</div>
|
|
746
|
+
</StoryCell>
|
|
747
|
+
))}
|
|
748
|
+
|
|
749
|
+
<StoryCell align="stretch" label="isMuted">
|
|
750
|
+
<GroupedList
|
|
751
|
+
groups={[
|
|
752
|
+
{
|
|
753
|
+
heading: "Switched off",
|
|
754
|
+
items: [
|
|
755
|
+
toChoreRow(CHORE_CARDS[2] as ChoreCard),
|
|
756
|
+
],
|
|
757
|
+
key: "muted",
|
|
758
|
+
},
|
|
759
|
+
]}
|
|
760
|
+
size="sm"
|
|
761
|
+
/>
|
|
762
|
+
</StoryCell>
|
|
763
|
+
|
|
764
|
+
<StoryCell
|
|
765
|
+
align="stretch"
|
|
766
|
+
label="isHighlighted, each highlightIntent"
|
|
767
|
+
>
|
|
768
|
+
<GroupedList
|
|
769
|
+
groups={HIGHLIGHT_INTENT_GROUPS}
|
|
770
|
+
size="sm"
|
|
771
|
+
/>
|
|
772
|
+
</StoryCell>
|
|
773
|
+
</StoryGrid>
|
|
774
|
+
),
|
|
775
|
+
}
|
|
776
|
+
|
|
439
777
|
/**
|
|
440
778
|
* Light and dark side by side, each panel scoped with its own
|
|
441
779
|
* `data-scheme`, so both read at once whatever the toolbar says.
|