@charcuterie/ui 4.24.0 → 4.26.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/CommandPalette/CommandPalette.d.ts +43 -0
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -0
- package/dist/CommandPalette/CommandPalette.js +53 -0
- package/dist/CommandPalette/CommandPalette.js.map +1 -0
- package/dist/CommandPalette/CommandPaletteFooter.d.ts +16 -0
- package/dist/CommandPalette/CommandPaletteFooter.d.ts.map +1 -0
- package/dist/CommandPalette/CommandPaletteFooter.js +14 -0
- package/dist/CommandPalette/CommandPaletteFooter.js.map +1 -0
- package/dist/CommandPalette/CommandPaletteOption.d.ts +44 -0
- package/dist/CommandPalette/CommandPaletteOption.d.ts.map +1 -0
- package/dist/CommandPalette/CommandPaletteOption.js +72 -0
- package/dist/CommandPalette/CommandPaletteOption.js.map +1 -0
- package/dist/CommandPalette/CommandPalettePanel.d.ts +52 -0
- package/dist/CommandPalette/CommandPalettePanel.d.ts.map +1 -0
- package/dist/CommandPalette/CommandPalettePanel.js +168 -0
- package/dist/CommandPalette/CommandPalettePanel.js.map +1 -0
- package/dist/CommandPalette/commandPaletteSections.d.ts +93 -0
- package/dist/CommandPalette/commandPaletteSections.d.ts.map +1 -0
- package/dist/CommandPalette/commandPaletteSections.js +94 -0
- package/dist/CommandPalette/commandPaletteSections.js.map +1 -0
- package/dist/CommandPalette/useCommandPalette.d.ts +32 -0
- package/dist/CommandPalette/useCommandPalette.d.ts.map +1 -0
- package/dist/CommandPalette/useCommandPalette.js +40 -0
- package/dist/CommandPalette/useCommandPalette.js.map +1 -0
- package/dist/CommandPalette/useCommandPaletteHotkeys.d.ts +35 -0
- package/dist/CommandPalette/useCommandPaletteHotkeys.d.ts.map +1 -0
- package/dist/CommandPalette/useCommandPaletteHotkeys.js +114 -0
- package/dist/CommandPalette/useCommandPaletteHotkeys.js.map +1 -0
- package/dist/GroupedList/GroupedList.d.ts +78 -1
- package/dist/GroupedList/GroupedList.d.ts.map +1 -1
- package/dist/GroupedList/GroupedList.js +14 -0
- 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/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/styles.css +36 -4
- package/package.json +2 -2
- package/src/CommandPalette/CommandPalette.mdx +129 -0
- package/src/CommandPalette/CommandPalette.stories.tsx +413 -0
- package/src/CommandPalette/CommandPalette.test.tsx +553 -0
- package/src/CommandPalette/CommandPalette.tsx +91 -0
- package/src/CommandPalette/CommandPaletteFooter.tsx +45 -0
- package/src/CommandPalette/CommandPaletteOption.tsx +200 -0
- package/src/CommandPalette/CommandPalettePanel.tsx +411 -0
- package/src/CommandPalette/commandPalette.storyHelpers.tsx +248 -0
- package/src/CommandPalette/commandPaletteSections.test.ts +156 -0
- package/src/CommandPalette/commandPaletteSections.ts +219 -0
- package/src/CommandPalette/useCommandPalette.ts +75 -0
- package/src/CommandPalette/useCommandPaletteHotkeys.ts +158 -0
- package/src/GroupedList/GroupedList.mdx +42 -1
- package/src/GroupedList/GroupedList.stories.tsx +252 -2
- package/src/GroupedList/GroupedList.test.tsx +391 -0
- package/src/GroupedList/GroupedList.tsx +78 -1
- package/src/GroupedList/GroupedListRow.tsx +135 -10
- package/src/index.ts +14 -0
- package/src/sourceRules.test.ts +2 -1
- package/src/styles.css +36 -4
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { useLatestRef } from "@charcuterie/logic"
|
|
2
|
+
import { useEffect } from "react"
|
|
3
|
+
|
|
4
|
+
export type CommandPaletteHotkeysOptions = {
|
|
5
|
+
/** Off while the palette is open, or on a page that should not take the keys. Default `true`. */
|
|
6
|
+
isEnabled?: boolean
|
|
7
|
+
/**
|
|
8
|
+
* A printable key typed outside an editable field opens the palette
|
|
9
|
+
* with that character as the query. Default `true`.
|
|
10
|
+
*/
|
|
11
|
+
isOpenedByTyping?: boolean
|
|
12
|
+
/** Receives the text the palette should open with: `""`, or the character typed. */
|
|
13
|
+
onOpen: (initialQuery: string) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Elements that keep their keys: anything you type into, and the
|
|
18
|
+
* widgets with their own type-to-find (a focused `Listbox` or `Menu`
|
|
19
|
+
* jumps to the option you type, and stealing that key breaks it).
|
|
20
|
+
*/
|
|
21
|
+
const TYPEAHEAD_OWNER_SELECTOR = [
|
|
22
|
+
"input",
|
|
23
|
+
"textarea",
|
|
24
|
+
"select",
|
|
25
|
+
"[contenteditable]:not([contenteditable='false'])",
|
|
26
|
+
"[role='combobox']",
|
|
27
|
+
"[role='grid']",
|
|
28
|
+
"[role='listbox']",
|
|
29
|
+
"[role='menu']",
|
|
30
|
+
"[role='menubar']",
|
|
31
|
+
"[role='searchbox']",
|
|
32
|
+
"[role='textbox']",
|
|
33
|
+
"[role='tree']",
|
|
34
|
+
"[role='treegrid']",
|
|
35
|
+
].join(",")
|
|
36
|
+
|
|
37
|
+
/** Input types that take no text, so a letter typed on one is free to open the palette. */
|
|
38
|
+
const NON_TEXT_INPUT_TYPES = new Set([
|
|
39
|
+
"button",
|
|
40
|
+
"checkbox",
|
|
41
|
+
"color",
|
|
42
|
+
"file",
|
|
43
|
+
"image",
|
|
44
|
+
"radio",
|
|
45
|
+
"range",
|
|
46
|
+
"reset",
|
|
47
|
+
"submit",
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
export const getIsTypingTarget = (
|
|
51
|
+
target: EventTarget | null,
|
|
52
|
+
): boolean => {
|
|
53
|
+
if (!(target instanceof Element)) {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
target instanceof HTMLInputElement &&
|
|
59
|
+
NON_TEXT_INPUT_TYPES.has(target.type)
|
|
60
|
+
) {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
(target instanceof HTMLElement &&
|
|
66
|
+
target.isContentEditable) ||
|
|
67
|
+
target.closest(TYPEAHEAD_OWNER_SELECTOR) !== null
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The global keys that open a command palette, as an opt-in hook.
|
|
73
|
+
*
|
|
74
|
+
* - **`Ctrl K`, `Ctrl Space`** (or `⌘` on a Mac) open it from
|
|
75
|
+
* anywhere, a text field included — the chord types nothing.
|
|
76
|
+
* - **`/`** opens it when focus is not somewhere that types, and
|
|
77
|
+
* the slash itself is not typed.
|
|
78
|
+
* - **Any printable key** does the same and becomes the query, so
|
|
79
|
+
* the page is a search box without a click (`isOpenedByTyping`).
|
|
80
|
+
*
|
|
81
|
+
* It is a hook, not something `CommandPalette` does on mount,
|
|
82
|
+
* because a document-wide key listener is a page-level decision: an
|
|
83
|
+
* app with two palettes, or a page where letters already mean
|
|
84
|
+
* something (a board with its own shortcuts), must be able to say
|
|
85
|
+
* no. See
|
|
86
|
+
* `docs/decisions/2026-10-16-command-palette-hotkeys-are-an-opt-in-hook.md`.
|
|
87
|
+
*
|
|
88
|
+
* A key another handler already claimed (`defaultPrevented`), a key
|
|
89
|
+
* pressed during IME composition, and a key with `Alt` held are all
|
|
90
|
+
* left alone.
|
|
91
|
+
*/
|
|
92
|
+
export const useCommandPaletteHotkeys = ({
|
|
93
|
+
isEnabled = true,
|
|
94
|
+
isOpenedByTyping = true,
|
|
95
|
+
onOpen,
|
|
96
|
+
}: CommandPaletteHotkeysOptions): void => {
|
|
97
|
+
const onOpenRef = useLatestRef(onOpen)
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!isEnabled) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
105
|
+
if (
|
|
106
|
+
event.defaultPrevented ||
|
|
107
|
+
event.isComposing ||
|
|
108
|
+
event.altKey
|
|
109
|
+
) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isChord = event.ctrlKey || event.metaKey
|
|
114
|
+
|
|
115
|
+
if (isChord) {
|
|
116
|
+
if (
|
|
117
|
+
!event.shiftKey &&
|
|
118
|
+
(event.key.toLowerCase() === "k" ||
|
|
119
|
+
event.key === " ")
|
|
120
|
+
) {
|
|
121
|
+
event.preventDefault()
|
|
122
|
+
onOpenRef.current("")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (getIsTypingTarget(event.target)) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (event.key === "/") {
|
|
133
|
+
event.preventDefault()
|
|
134
|
+
onOpenRef.current("")
|
|
135
|
+
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// `key` is the character for a printable key and a name
|
|
140
|
+
// ("Enter", "ArrowDown") for everything else; a space is
|
|
141
|
+
// printable but presses the focused button.
|
|
142
|
+
if (
|
|
143
|
+
isOpenedByTyping &&
|
|
144
|
+
[...event.key].length === 1 &&
|
|
145
|
+
/\S/u.test(event.key)
|
|
146
|
+
) {
|
|
147
|
+
event.preventDefault()
|
|
148
|
+
onOpenRef.current(event.key)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
document.addEventListener("keydown", handleKeyDown)
|
|
153
|
+
|
|
154
|
+
return () => {
|
|
155
|
+
document.removeEventListener("keydown", handleKeyDown)
|
|
156
|
+
}
|
|
157
|
+
}, [isEnabled, isOpenedByTyping, onOpenRef])
|
|
158
|
+
}
|
|
@@ -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
|
|
@@ -84,6 +86,45 @@ that sets them through a `style` object.
|
|
|
84
86
|
|
|
85
87
|
<Canvas of={GroupedListStories.StickyHeadings} />
|
|
86
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
|
+
|
|
87
128
|
## Name a repeated action after its row
|
|
88
129
|
|
|
89
130
|
Ten rows of "Open" are ten buttons an agent cannot tell apart. Give each one a name that
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
-
import type
|
|
3
|
+
import { type CSSProperties, useState } from "react"
|
|
4
4
|
|
|
5
5
|
import { Badge } from "../Badge/Badge.tsx"
|
|
6
6
|
import { Button } from "../Button/Button.tsx"
|
|
@@ -10,7 +10,11 @@ import {
|
|
|
10
10
|
StoryGrid,
|
|
11
11
|
} from "../board.storyHelpers.tsx"
|
|
12
12
|
import { EmptyState } from "../EmptyState/EmptyState.tsx"
|
|
13
|
-
import
|
|
13
|
+
import { ValuePairs } from "../ValuePairs/ValuePairs.tsx"
|
|
14
|
+
import type {
|
|
15
|
+
GroupedListGroup,
|
|
16
|
+
GroupedListItem,
|
|
17
|
+
} from "./GroupedList.tsx"
|
|
14
18
|
import { GroupedList } from "./GroupedList.tsx"
|
|
15
19
|
import { GROUPED_LIST_HEADING_SURFACE_PROPERTY } from "./groupedListStickyHeading.ts"
|
|
16
20
|
|
|
@@ -183,6 +187,142 @@ const CHORE_GROUPS: GroupedListGroup[] = [
|
|
|
183
187
|
},
|
|
184
188
|
]
|
|
185
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
|
+
|
|
186
326
|
const LONG_GROUPS: GroupedListGroup[] = [
|
|
187
327
|
{
|
|
188
328
|
heading:
|
|
@@ -524,6 +664,116 @@ export const StickyHeadings: Story = {
|
|
|
524
664
|
),
|
|
525
665
|
}
|
|
526
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
|
+
|
|
527
777
|
/**
|
|
528
778
|
* Light and dark side by side, each panel scoped with its own
|
|
529
779
|
* `data-scheme`, so both read at once whatever the toolbar says.
|