@charcuterie/ui 4.25.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/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/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/index.ts +14 -0
- package/src/sourceRules.test.ts +2 -1
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import type { ReactNode } from "react"
|
|
4
|
+
import { useState } from "react"
|
|
5
|
+
|
|
6
|
+
import { controlSizeArgType } from "../argTypes.storyHelpers.ts"
|
|
7
|
+
import { Button } from "../Button/Button.tsx"
|
|
8
|
+
import {
|
|
9
|
+
StoryCell,
|
|
10
|
+
StoryGrid,
|
|
11
|
+
StorySection,
|
|
12
|
+
} from "../board.storyHelpers.tsx"
|
|
13
|
+
import {
|
|
14
|
+
CommandPalette,
|
|
15
|
+
type CommandPaletteProps,
|
|
16
|
+
} from "./CommandPalette.tsx"
|
|
17
|
+
import {
|
|
18
|
+
CommandPalettePanel,
|
|
19
|
+
type CommandPalettePanelProps,
|
|
20
|
+
} from "./CommandPalettePanel.tsx"
|
|
21
|
+
import {
|
|
22
|
+
getWebSearchItem,
|
|
23
|
+
LAUNCHER_ITEMS,
|
|
24
|
+
LAUNCHER_PREFIXES,
|
|
25
|
+
LAUNCHER_SUGGESTIONS,
|
|
26
|
+
PanelFrame,
|
|
27
|
+
} from "./commandPalette.storyHelpers.tsx"
|
|
28
|
+
import type { CommandPaletteItem } from "./commandPaletteSections.ts"
|
|
29
|
+
import { useCommandPalette } from "./useCommandPalette.ts"
|
|
30
|
+
|
|
31
|
+
const meta = {
|
|
32
|
+
title: "Components/Overlays/CommandPalette",
|
|
33
|
+
component: CommandPalette,
|
|
34
|
+
parameters: { layout: "padded" },
|
|
35
|
+
argTypes: { itemSize: controlSizeArgType },
|
|
36
|
+
args: {
|
|
37
|
+
getFallbackItem: getWebSearchItem,
|
|
38
|
+
isVisible: false,
|
|
39
|
+
itemSize: "lg",
|
|
40
|
+
items: LAUNCHER_ITEMS,
|
|
41
|
+
label: "Search apps, bookmarks, and the web",
|
|
42
|
+
maxResultsPerGroup: 6,
|
|
43
|
+
onClose: () => {},
|
|
44
|
+
placeholder: "Search apps, bookmarks, or the web",
|
|
45
|
+
prefixes: LAUNCHER_PREFIXES,
|
|
46
|
+
suggestions: LAUNCHER_SUGGESTIONS,
|
|
47
|
+
},
|
|
48
|
+
} satisfies Meta<typeof CommandPalette>
|
|
49
|
+
|
|
50
|
+
export default meta
|
|
51
|
+
|
|
52
|
+
type Story = StoryObj<typeof meta>
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* What an app writes: `useCommandPalette()` for the open state and the
|
|
56
|
+
* global keys, a button for the pointer, and the palette.
|
|
57
|
+
*/
|
|
58
|
+
const LauncherDemo = ({
|
|
59
|
+
isVisible: _isVisible,
|
|
60
|
+
onClose: _onClose,
|
|
61
|
+
...paletteProps
|
|
62
|
+
}: CommandPaletteProps): ReactNode => {
|
|
63
|
+
const palette = useCommandPalette()
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex flex-col items-start gap-3">
|
|
67
|
+
<Button
|
|
68
|
+
appearance="soft"
|
|
69
|
+
onClick={() => palette.open()}
|
|
70
|
+
size="sm"
|
|
71
|
+
>
|
|
72
|
+
Search
|
|
73
|
+
</Button>
|
|
74
|
+
|
|
75
|
+
<p className="m-0 text-content-secondary text-sm">
|
|
76
|
+
Or press Ctrl K, Ctrl Space, or /, or start typing.
|
|
77
|
+
</p>
|
|
78
|
+
|
|
79
|
+
<CommandPalette
|
|
80
|
+
{...paletteProps}
|
|
81
|
+
{...palette.paletteProps}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const Playground: Story = {
|
|
88
|
+
parameters: playgroundParameters,
|
|
89
|
+
render: (paletteProps) => (
|
|
90
|
+
<LauncherDemo {...paletteProps} />
|
|
91
|
+
),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const panelProps = (
|
|
95
|
+
paletteProps: CommandPaletteProps,
|
|
96
|
+
): CommandPalettePanelProps => {
|
|
97
|
+
const {
|
|
98
|
+
className: _className,
|
|
99
|
+
isVisible: _isVisible,
|
|
100
|
+
onClose: _onClose,
|
|
101
|
+
...rest
|
|
102
|
+
} = paletteProps
|
|
103
|
+
|
|
104
|
+
return rest
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The row's parts, and the three row sizes. Each panel is the
|
|
109
|
+
* palette's contents laid out in place, so the boards can show every
|
|
110
|
+
* case side by side; in an app it is always inside the dialog.
|
|
111
|
+
*/
|
|
112
|
+
export const AllVariants: Story = {
|
|
113
|
+
render: (paletteProps) => (
|
|
114
|
+
<div className="flex flex-col gap-6">
|
|
115
|
+
<StorySection title="Rows: an icon, a highlighted title, a description (the folder), and a trailing hint. Every part but the title is optional.">
|
|
116
|
+
<StoryGrid columns={2}>
|
|
117
|
+
<StoryCell
|
|
118
|
+
align="stretch"
|
|
119
|
+
label="icon, description, hint"
|
|
120
|
+
>
|
|
121
|
+
<PanelFrame>
|
|
122
|
+
<CommandPalettePanel
|
|
123
|
+
{...panelProps(paletteProps)}
|
|
124
|
+
defaultQuery="remote"
|
|
125
|
+
label="Rows with every part"
|
|
126
|
+
/>
|
|
127
|
+
</PanelFrame>
|
|
128
|
+
</StoryCell>
|
|
129
|
+
|
|
130
|
+
<StoryCell
|
|
131
|
+
align="stretch"
|
|
132
|
+
label="title only, and a long title that wraps"
|
|
133
|
+
>
|
|
134
|
+
<PanelFrame>
|
|
135
|
+
<CommandPalettePanel
|
|
136
|
+
{...panelProps(paletteProps)}
|
|
137
|
+
defaultQuery="li"
|
|
138
|
+
items={[
|
|
139
|
+
{
|
|
140
|
+
group: "Notes",
|
|
141
|
+
key: "list",
|
|
142
|
+
title: "Linen closet inventory",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
group: "Notes",
|
|
146
|
+
key: "long",
|
|
147
|
+
title:
|
|
148
|
+
"Living room lamp timer schedule for the winter months, weekdays and weekends",
|
|
149
|
+
},
|
|
150
|
+
]}
|
|
151
|
+
label="Rows with a title only"
|
|
152
|
+
/>
|
|
153
|
+
</PanelFrame>
|
|
154
|
+
</StoryCell>
|
|
155
|
+
</StoryGrid>
|
|
156
|
+
</StorySection>
|
|
157
|
+
|
|
158
|
+
<StorySection title="itemSize sm, md and lg. The search field follows the rows.">
|
|
159
|
+
<StoryGrid columns={3}>
|
|
160
|
+
{(["sm", "md", "lg"] as const).map((itemSize) => (
|
|
161
|
+
<StoryCell
|
|
162
|
+
align="stretch"
|
|
163
|
+
key={itemSize}
|
|
164
|
+
label={`itemSize=${itemSize}`}
|
|
165
|
+
>
|
|
166
|
+
<PanelFrame>
|
|
167
|
+
<CommandPalettePanel
|
|
168
|
+
{...panelProps(paletteProps)}
|
|
169
|
+
footer={null}
|
|
170
|
+
itemSize={itemSize}
|
|
171
|
+
label={`Search at ${itemSize}`}
|
|
172
|
+
/>
|
|
173
|
+
</PanelFrame>
|
|
174
|
+
</StoryCell>
|
|
175
|
+
))}
|
|
176
|
+
</StoryGrid>
|
|
177
|
+
</StorySection>
|
|
178
|
+
</div>
|
|
179
|
+
),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Every state the list can be in. The first is the reason the
|
|
184
|
+
* component exists: it lists something before anything is typed.
|
|
185
|
+
*/
|
|
186
|
+
export const AllStates: Story = {
|
|
187
|
+
render: (paletteProps) => (
|
|
188
|
+
<StoryGrid columns={3}>
|
|
189
|
+
<StoryCell
|
|
190
|
+
align="stretch"
|
|
191
|
+
label="before typing: suggestions"
|
|
192
|
+
>
|
|
193
|
+
<PanelFrame>
|
|
194
|
+
<CommandPalettePanel
|
|
195
|
+
{...panelProps(paletteProps)}
|
|
196
|
+
label="Before typing"
|
|
197
|
+
/>
|
|
198
|
+
</PanelFrame>
|
|
199
|
+
</StoryCell>
|
|
200
|
+
|
|
201
|
+
<StoryCell
|
|
202
|
+
align="stretch"
|
|
203
|
+
label={'"serv": word prefixes, then the fallback'}
|
|
204
|
+
>
|
|
205
|
+
<PanelFrame>
|
|
206
|
+
<CommandPalettePanel
|
|
207
|
+
{...panelProps(paletteProps)}
|
|
208
|
+
defaultQuery="serv"
|
|
209
|
+
label="Word prefixes"
|
|
210
|
+
/>
|
|
211
|
+
</PanelFrame>
|
|
212
|
+
</StoryCell>
|
|
213
|
+
|
|
214
|
+
<StoryCell align="stretch" label={'"ha": initials'}>
|
|
215
|
+
<PanelFrame>
|
|
216
|
+
<CommandPalettePanel
|
|
217
|
+
{...panelProps(paletteProps)}
|
|
218
|
+
defaultQuery="ha"
|
|
219
|
+
label="Initials"
|
|
220
|
+
/>
|
|
221
|
+
</PanelFrame>
|
|
222
|
+
</StoryCell>
|
|
223
|
+
|
|
224
|
+
<StoryCell
|
|
225
|
+
align="stretch"
|
|
226
|
+
label={'"au lego": a prefix command'}
|
|
227
|
+
>
|
|
228
|
+
<PanelFrame>
|
|
229
|
+
<CommandPalettePanel
|
|
230
|
+
{...panelProps(paletteProps)}
|
|
231
|
+
defaultQuery="au lego"
|
|
232
|
+
label="Prefix command"
|
|
233
|
+
/>
|
|
234
|
+
</PanelFrame>
|
|
235
|
+
</StoryCell>
|
|
236
|
+
|
|
237
|
+
<StoryCell
|
|
238
|
+
align="stretch"
|
|
239
|
+
label={'"zebra": only the fallback'}
|
|
240
|
+
>
|
|
241
|
+
<PanelFrame>
|
|
242
|
+
<CommandPalettePanel
|
|
243
|
+
{...panelProps(paletteProps)}
|
|
244
|
+
defaultQuery="zebra"
|
|
245
|
+
label="Only the fallback"
|
|
246
|
+
/>
|
|
247
|
+
</PanelFrame>
|
|
248
|
+
</StoryCell>
|
|
249
|
+
|
|
250
|
+
<StoryCell
|
|
251
|
+
align="stretch"
|
|
252
|
+
label={'"zebra", no fallback: the empty label'}
|
|
253
|
+
>
|
|
254
|
+
<PanelFrame>
|
|
255
|
+
<CommandPalettePanel
|
|
256
|
+
{...panelProps(paletteProps)}
|
|
257
|
+
defaultQuery="zebra"
|
|
258
|
+
getFallbackItem={undefined}
|
|
259
|
+
label="No matches"
|
|
260
|
+
/>
|
|
261
|
+
</PanelFrame>
|
|
262
|
+
</StoryCell>
|
|
263
|
+
</StoryGrid>
|
|
264
|
+
),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Opened over the page, before anything is typed. Like `Dialog`, the
|
|
269
|
+
* palette is measured against the viewport, not a container, so this
|
|
270
|
+
* story has no `ContainerBoard`: it is the window that varies. On a
|
|
271
|
+
* wide window it hangs from the top at up to 45rem wide; in the Narrow
|
|
272
|
+
* View it takes nearly the whole screen, and the key hints in the
|
|
273
|
+
* footer give way to the prefix list.
|
|
274
|
+
*/
|
|
275
|
+
const OpenDemo = ({
|
|
276
|
+
defaultQuery,
|
|
277
|
+
isVisible: _isVisible,
|
|
278
|
+
onClose: _onClose,
|
|
279
|
+
...paletteProps
|
|
280
|
+
}: CommandPaletteProps): ReactNode => {
|
|
281
|
+
const [isVisible, setIsVisible] = useState(true)
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<div className="min-h-screen">
|
|
285
|
+
<Button
|
|
286
|
+
appearance="soft"
|
|
287
|
+
onClick={() => setIsVisible(true)}
|
|
288
|
+
size="sm"
|
|
289
|
+
>
|
|
290
|
+
Search
|
|
291
|
+
</Button>
|
|
292
|
+
|
|
293
|
+
<CommandPalette
|
|
294
|
+
{...paletteProps}
|
|
295
|
+
defaultQuery={defaultQuery}
|
|
296
|
+
isVisible={isVisible}
|
|
297
|
+
onClose={() => setIsVisible(false)}
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export const Responsive: Story = {
|
|
304
|
+
render: (paletteProps) => <OpenDemo {...paletteProps} />,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Open with `serv` typed: matches under their groups, then the fallback row. */
|
|
308
|
+
export const OpenFiltered: Story = {
|
|
309
|
+
args: { defaultQuery: "serv" },
|
|
310
|
+
render: (paletteProps) => <OpenDemo {...paletteProps} />,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Open with `au lego` typed: one row, for the target that owns `au`. */
|
|
314
|
+
export const OpenPrefixCommand: Story = {
|
|
315
|
+
args: { defaultQuery: "au lego" },
|
|
316
|
+
render: (paletteProps) => <OpenDemo {...paletteProps} />,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The whole keyboard path. Ctrl K, Ctrl Space or / open it with an
|
|
321
|
+
* empty field; any other key opens it with that key typed. Arrow
|
|
322
|
+
* keys, Home and End move; Enter opens the row; Ctrl Enter opens a
|
|
323
|
+
* link row in a new tab; Escape closes, and focus goes back to where
|
|
324
|
+
* it was. A clear button appears when the app passes `clearIcon`.
|
|
325
|
+
*/
|
|
326
|
+
const ClearIcon = (): ReactNode => (
|
|
327
|
+
<svg aria-hidden="true" fill="none" viewBox="0 0 24 24">
|
|
328
|
+
<path
|
|
329
|
+
d="m6 6 12 12M18 6 6 18"
|
|
330
|
+
stroke="currentColor"
|
|
331
|
+
strokeLinecap="round"
|
|
332
|
+
strokeWidth="2"
|
|
333
|
+
/>
|
|
334
|
+
</svg>
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Every row runs `onSelect` instead of navigating — the items, the
|
|
339
|
+
* suggestions, the prefix rows and the fallback — so the story can
|
|
340
|
+
* show what was chosen and never leaves the page.
|
|
341
|
+
*/
|
|
342
|
+
const InteractiveDemo = (
|
|
343
|
+
paletteProps: CommandPaletteProps,
|
|
344
|
+
): ReactNode => {
|
|
345
|
+
const [chosen, setChosen] = useState("nothing yet")
|
|
346
|
+
|
|
347
|
+
const toChoice = (item: CommandPaletteItem) => ({
|
|
348
|
+
...item,
|
|
349
|
+
href: undefined,
|
|
350
|
+
onSelect: () => setChosen(item.title),
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
const items = paletteProps.items.map(toChoice)
|
|
354
|
+
|
|
355
|
+
const suggestions = (paletteProps.suggestions ?? []).map(
|
|
356
|
+
(group) => ({
|
|
357
|
+
...group,
|
|
358
|
+
items: group.items.map(toChoice),
|
|
359
|
+
}),
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
const prefixes = (paletteProps.prefixes ?? []).map(
|
|
363
|
+
({ getHref: _getHref, ...prefix }) => ({
|
|
364
|
+
...prefix,
|
|
365
|
+
onSelect: (query: string) =>
|
|
366
|
+
setChosen(`${prefix.label}: ${query}`),
|
|
367
|
+
}),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
const getFallbackItem = (query: string) => {
|
|
371
|
+
const item = paletteProps.getFallbackItem?.(query)
|
|
372
|
+
|
|
373
|
+
return item == null ? item : toChoice(item)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<div className="flex flex-col items-start gap-3">
|
|
378
|
+
<LauncherDemo
|
|
379
|
+
{...paletteProps}
|
|
380
|
+
getFallbackItem={getFallbackItem}
|
|
381
|
+
items={items}
|
|
382
|
+
prefixes={prefixes}
|
|
383
|
+
suggestions={suggestions}
|
|
384
|
+
/>
|
|
385
|
+
|
|
386
|
+
<label className="flex flex-col gap-1 text-content-secondary text-sm">
|
|
387
|
+
A note on the page
|
|
388
|
+
<input
|
|
389
|
+
className="rounded-md border border-border-default bg-surface-raised px-3 py-2 text-content-primary"
|
|
390
|
+
type="text"
|
|
391
|
+
/>
|
|
392
|
+
</label>
|
|
393
|
+
|
|
394
|
+
<p className="m-0 text-content-primary text-sm">
|
|
395
|
+
Last chosen: {chosen}
|
|
396
|
+
</p>
|
|
397
|
+
</div>
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The whole keyboard path. Ctrl K, Ctrl Space or / open it with an
|
|
403
|
+
* empty field; any other key opens it with that key typed. Arrow
|
|
404
|
+
* keys, Home and End move; Enter opens the row; Ctrl Enter opens a
|
|
405
|
+
* link row in a new tab; Escape closes, and focus goes back to where
|
|
406
|
+
* it was. A clear button appears when the app passes `clearIcon`.
|
|
407
|
+
*/
|
|
408
|
+
export const Interactive: Story = {
|
|
409
|
+
args: { clearIcon: <ClearIcon /> },
|
|
410
|
+
render: (paletteProps) => (
|
|
411
|
+
<InteractiveDemo {...paletteProps} />
|
|
412
|
+
),
|
|
413
|
+
}
|