@charcuterie/ui 2.4.1 → 2.6.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.
Files changed (56) hide show
  1. package/dist/Checkbox/Checkbox.d.ts +68 -0
  2. package/dist/Checkbox/Checkbox.d.ts.map +1 -0
  3. package/dist/Checkbox/Checkbox.js +108 -0
  4. package/dist/Checkbox/Checkbox.js.map +1 -0
  5. package/dist/Field/Field.d.ts.map +1 -1
  6. package/dist/Field/Field.js +12 -1
  7. package/dist/Field/Field.js.map +1 -1
  8. package/dist/Menu/Menu.d.ts +60 -2
  9. package/dist/Menu/Menu.d.ts.map +1 -1
  10. package/dist/Menu/Menu.js +58 -11
  11. package/dist/Menu/Menu.js.map +1 -1
  12. package/dist/RadioGroup/RadioGroup.d.ts +54 -0
  13. package/dist/RadioGroup/RadioGroup.d.ts.map +1 -0
  14. package/dist/RadioGroup/RadioGroup.js +121 -0
  15. package/dist/RadioGroup/RadioGroup.js.map +1 -0
  16. package/dist/RadioGroup/RadioGroupOption.d.ts +27 -0
  17. package/dist/RadioGroup/RadioGroupOption.d.ts.map +1 -0
  18. package/dist/RadioGroup/RadioGroupOption.js +75 -0
  19. package/dist/RadioGroup/RadioGroupOption.js.map +1 -0
  20. package/dist/Switch/Switch.d.ts +60 -0
  21. package/dist/Switch/Switch.d.ts.map +1 -0
  22. package/dist/Switch/Switch.js +106 -0
  23. package/dist/Switch/Switch.js.map +1 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/slotWiring.d.ts +9 -7
  29. package/dist/slotWiring.d.ts.map +1 -1
  30. package/dist/slotWiring.js +10 -70
  31. package/dist/slotWiring.js.map +1 -1
  32. package/package.json +2 -2
  33. package/src/Checkbox/Checkbox.mdx +71 -0
  34. package/src/Checkbox/Checkbox.stories.tsx +122 -0
  35. package/src/Checkbox/Checkbox.test.tsx +109 -0
  36. package/src/Checkbox/Checkbox.tsx +205 -0
  37. package/src/Combobox/Combobox.stories.tsx +19 -4
  38. package/src/Field/Field.stories.tsx +19 -0
  39. package/src/Field/Field.test.tsx +30 -2
  40. package/src/Field/Field.tsx +13 -1
  41. package/src/Menu/Menu.stories.tsx +80 -2
  42. package/src/Menu/Menu.test.tsx +102 -0
  43. package/src/Menu/Menu.tsx +156 -17
  44. package/src/RadioGroup/RadioGroup.mdx +67 -0
  45. package/src/RadioGroup/RadioGroup.stories.tsx +140 -0
  46. package/src/RadioGroup/RadioGroup.test.tsx +232 -0
  47. package/src/RadioGroup/RadioGroup.tsx +223 -0
  48. package/src/RadioGroup/RadioGroupOption.tsx +148 -0
  49. package/src/Switch/Switch.mdx +55 -0
  50. package/src/Switch/Switch.stories.tsx +116 -0
  51. package/src/Switch/Switch.test.tsx +115 -0
  52. package/src/Switch/Switch.tsx +183 -0
  53. package/src/index.ts +9 -0
  54. package/src/slotWiring.ts +15 -107
  55. package/src/sourceRules.test.ts +11 -1
  56. package/src/tailwindCandidates.test.ts +9 -7
@@ -10,6 +10,8 @@ import * as stories from "./Menu.stories.tsx"
10
10
  const {
11
11
  AllStates,
12
12
  AllVariants,
13
+ Empty,
14
+ Grouped,
13
15
  Interactive,
14
16
  SharedTrigger,
15
17
  } = composeStories(stories)
@@ -320,3 +322,103 @@ test("the tooltip sharing that trigger is anchored to it too", async () => {
320
322
  )
321
323
  })
322
324
  })
325
+
326
+ test("a group is a named group and a separator is a separator", async () => {
327
+ const { body } = await mountStory(Grouped)
328
+
329
+ const menu = expectAgentDrivable(body, {
330
+ name: "Bay 6",
331
+ role: "menu",
332
+ })
333
+
334
+ // Each group is a `role="group"` named by its heading, so a screen
335
+ // reader announces "Danger, group" before its members.
336
+ expectAgentDrivable(body, {
337
+ name: "Disc",
338
+ role: "group",
339
+ })
340
+
341
+ expectAgentDrivable(body, {
342
+ name: "Danger",
343
+ role: "group",
344
+ })
345
+
346
+ // A native `<hr>`, which carries the implicit `role="separator"`.
347
+ await expect(menu.querySelectorAll("hr")).toHaveLength(1)
348
+
349
+ await expect(body.getAllByRole("separator")).toHaveLength(
350
+ 1,
351
+ )
352
+
353
+ await expectNoAxeViolations(menu)
354
+ })
355
+
356
+ test("arrow keys skip group headings and separators", async () => {
357
+ const { body } = await mountStory(Grouped)
358
+
359
+ const retry = expectAgentDrivable(body, {
360
+ name: "Retry title",
361
+ role: "menuitem",
362
+ })
363
+
364
+ const skip = expectAgentDrivable(body, {
365
+ name: "Skip title",
366
+ role: "menuitem",
367
+ })
368
+
369
+ const eject = expectAgentDrivable(body, {
370
+ name: "Eject disc",
371
+ role: "menuitem",
372
+ })
373
+
374
+ // Opening lands on the first item, and arrow-down walks the items
375
+ // in DOM order across the group boundary — the heading and the
376
+ // separator register nothing, so there is nothing between Skip
377
+ // (end of group one) and Eject (start of group two) to stop on.
378
+ await waitFor(() => {
379
+ expect(retry).toHaveFocus()
380
+ })
381
+
382
+ await userEvent.keyboard("{ArrowDown}")
383
+
384
+ await waitFor(() => {
385
+ expect(skip).toHaveFocus()
386
+ })
387
+
388
+ await userEvent.keyboard("{ArrowDown}")
389
+
390
+ await waitFor(() => {
391
+ expect(eject).toHaveFocus()
392
+ })
393
+ })
394
+
395
+ test("an empty menu shows a disabled note, not an actionable item", async () => {
396
+ const { body } = await mountStory(Empty)
397
+
398
+ const menu = expectAgentDrivable(body, {
399
+ name: "Bay 7",
400
+ role: "menu",
401
+ })
402
+
403
+ await expect(menu).toHaveTextContent(
404
+ "No actions available",
405
+ )
406
+
407
+ // The note is the menu's one `menuitem` — a `role="menu"` must own
408
+ // one — but it is `aria-disabled` and carries no tab stop, so a
409
+ // keyboard user is told the menu is empty and cannot land on it.
410
+ const items = menu.querySelectorAll<HTMLElement>(
411
+ '[role="menuitem"]',
412
+ )
413
+
414
+ await expect(items).toHaveLength(1)
415
+
416
+ await expect(items[0]).toHaveAttribute(
417
+ "aria-disabled",
418
+ "true",
419
+ )
420
+
421
+ await expect(items[0]?.tabIndex).toBe(-1)
422
+
423
+ await expectNoAxeViolations(menu)
424
+ })
package/src/Menu/Menu.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  selectTabIndex,
3
3
  useRovingFocus,
4
+ useUniqueId,
4
5
  } from "@charcuterie/logic"
5
6
  import type { Placement } from "@floating-ui/react"
6
7
  import {
@@ -21,12 +22,57 @@ export type MenuItem = {
21
22
  key: string
22
23
  label: ReactNode
23
24
  onSelect: () => void
25
+ /**
26
+ * The default kind, and optional so the common `{ key, label,
27
+ * onSelect }` still type-checks. Present only to discriminate this
28
+ * from a `separator` or a `group`.
29
+ */
30
+ type?: "item"
31
+ }
32
+
33
+ /**
34
+ * A rule between groups of items — `role="separator"`, and nothing a
35
+ * keyboard user lands on. It never registers with the roving group,
36
+ * so the arrow keys pass straight over it, the same mechanism a
37
+ * disabled item uses.
38
+ */
39
+ export type MenuSeparator = {
40
+ key: string
41
+ type: "separator"
24
42
  }
25
43
 
44
+ /**
45
+ * A named set of items — `role="group"` with the `label` as its
46
+ * accessible name (a screen reader announces "Danger, group" before
47
+ * its members). The heading itself is not focusable; only the items
48
+ * inside it register.
49
+ */
50
+ export type MenuGroup = {
51
+ items: MenuItem[]
52
+ key: string
53
+ label: ReactNode
54
+ type: "group"
55
+ }
56
+
57
+ /**
58
+ * What a menu is made of. A bare `MenuItem` is still valid on its
59
+ * own, so existing `items` arrays need no change; a `separator` or a
60
+ * `group` opts in by its `type`.
61
+ */
62
+ export type MenuEntry = MenuGroup | MenuItem | MenuSeparator
63
+
26
64
  export type MenuProps = {
27
65
  className?: string
66
+ /**
67
+ * Shown when there is no item to show — a `group` with no members
68
+ * and a bare separator list both count as empty. Rendered as a
69
+ * **disabled** `menuitem` (a `role="menu"` must own one), so it is
70
+ * announced but never focused. Without it an empty menu renders
71
+ * nothing.
72
+ */
73
+ emptyState?: ReactNode
28
74
  isVisible: boolean
29
- items: MenuItem[]
75
+ items: MenuEntry[]
30
76
  /** Outside press, Escape, and choosing an item all land here. */
31
77
  onDismiss: () => void
32
78
  placement?: Placement
@@ -34,6 +80,16 @@ export type MenuProps = {
34
80
  trigger: ReactElement
35
81
  }
36
82
 
83
+ const isMenuItem = (entry: MenuEntry): entry is MenuItem =>
84
+ entry.type === undefined || entry.type === "item"
85
+
86
+ const hasAnyItem = (items: MenuEntry[]): boolean =>
87
+ items.some(
88
+ (entry) =>
89
+ isMenuItem(entry) ||
90
+ (entry.type === "group" && entry.items.length > 0),
91
+ )
92
+
37
93
  /**
38
94
  * A menu is **not** a listbox, and mux-magic's `TypePicker` is the
39
95
  * fleet's one attempt at telling them apart — it renders
@@ -86,6 +142,22 @@ export type MenuProps = {
86
142
  * focus is whichever one `RovingFocus` says is active, and that is
87
143
  * not known until the items have registered.
88
144
  *
145
+ * ### Separators, groups, and an empty state
146
+ *
147
+ * `items` is a union, not a flat `MenuItem[]`: a `separator`
148
+ * (`role="separator"`) and a `group` (`role="group"`, named by its
149
+ * `label`) opt in by their `type`, and a bare item still type-checks
150
+ * unchanged. The keyboard model needs no new code for them — a
151
+ * separator and a group heading register nothing, so the roving
152
+ * group never sees them and the arrow keys skip straight over, the
153
+ * same mechanism a disabled item already used. A `group`'s items
154
+ * register normally, so focus moves through them in DOM order as if
155
+ * the heading were not there. When there is nothing to show, the
156
+ * `emptyState` renders as a **disabled** `menuitem` — a `role="menu"`
157
+ * with no `menuitem` child fails `aria-required-children`, so the
158
+ * note has to be one, `aria-disabled` and out of the roving group:
159
+ * announced as "No actions available, dimmed", focusable by nothing.
160
+ *
89
161
  * ### No type-ahead, deliberately
90
162
  *
91
163
  * The APG lists type-ahead as optional for a menu and required for a
@@ -96,12 +168,15 @@ export type MenuProps = {
96
168
  */
97
169
  export const Menu = ({
98
170
  className,
171
+ emptyState,
99
172
  isVisible,
100
173
  items,
101
174
  onDismiss,
102
175
  placement = "bottom-start",
103
176
  trigger,
104
177
  }: MenuProps): ReactNode => {
178
+ const menuId = useUniqueId()
179
+
105
180
  const itemElements = useRef(
106
181
  new Map<string, HTMLButtonElement>(),
107
182
  )
@@ -160,6 +235,83 @@ export const Menu = ({
160
235
  itemElements.current.get(activeValue)?.focus()
161
236
  }, [activeValue, isVisible])
162
237
 
238
+ // One member of the roving group, whether it sits at the top level
239
+ // or inside a `group`. The registration lives in `MenuAction`'s
240
+ // effect, so an item nested in a group joins the same arrow-key
241
+ // sequence in DOM order — the heading above it registers nothing.
242
+ const renderItem = (item: MenuItem) => (
243
+ <MenuAction
244
+ item={item}
245
+ key={item.key}
246
+ onDismiss={onDismiss}
247
+ register={focus.register}
248
+ tabIndex={selectTabIndex(focus, item.key)}
249
+ trackElement={(key, element) => {
250
+ if (element) {
251
+ itemElements.current.set(key, element)
252
+ } else {
253
+ itemElements.current.delete(key)
254
+ }
255
+ }}
256
+ />
257
+ )
258
+
259
+ const renderEntry = (entry: MenuEntry) => {
260
+ if (entry.type === "separator") {
261
+ // A native `<hr>` — implicit `role="separator"`, and the one
262
+ // that does *not* need `aria-valuenow` (that is the focusable
263
+ // splitter kind, not a static rule between menu groups).
264
+ return (
265
+ <hr
266
+ className="-mx-1 my-1 h-px border-0 bg-border-subtle"
267
+ key={entry.key}
268
+ />
269
+ )
270
+ }
271
+
272
+ if (entry.type === "group") {
273
+ const headingId = `${menuId}-${entry.key}`
274
+
275
+ return (
276
+ // biome-ignore lint/a11y/useSemanticElements: a menu group is `role="group"` (ARIA APG), not a form `<fieldset>` — a fieldset is invalid content inside `role="menu"` and drags legend/border/reset semantics onto it.
277
+ <div
278
+ aria-labelledby={headingId}
279
+ key={entry.key}
280
+ role="group"
281
+ >
282
+ <div
283
+ className="px-2 pt-1.5 pb-0.5 font-medium text-content-secondary text-xs"
284
+ id={headingId}
285
+ >
286
+ {entry.label}
287
+ </div>
288
+
289
+ {entry.items.map(renderItem)}
290
+ </div>
291
+ )
292
+ }
293
+
294
+ return renderItem(entry)
295
+ }
296
+
297
+ // A **disabled** `menuitem`, not inert text: a `role="menu"` with
298
+ // no `menuitem` child fails `aria-required-children`, so the empty
299
+ // note has to be one — `aria-disabled` and `tabindex="-1"`, so it
300
+ // is announced ("No actions available, dimmed") but takes no tab
301
+ // stop and never joins the roving group. `null` when the consumer
302
+ // gave no `emptyState`, so an empty menu renders nothing.
303
+ const emptyElement =
304
+ emptyState === undefined ? null : (
305
+ <div
306
+ aria-disabled="true"
307
+ className="px-2 py-1.5 text-content-disabled text-sm"
308
+ role="menuitem"
309
+ tabIndex={-1}
310
+ >
311
+ {emptyState}
312
+ </div>
313
+ )
314
+
163
315
  return (
164
316
  <>
165
317
  {clonedTrigger}
@@ -214,22 +366,9 @@ export const Menu = ({
214
366
  role="menu"
215
367
  style={floatingStyles}
216
368
  >
217
- {items.map((item) => (
218
- <MenuAction
219
- item={item}
220
- key={item.key}
221
- onDismiss={onDismiss}
222
- register={focus.register}
223
- tabIndex={selectTabIndex(focus, item.key)}
224
- trackElement={(key, element) => {
225
- if (element) {
226
- itemElements.current.set(key, element)
227
- } else {
228
- itemElements.current.delete(key)
229
- }
230
- }}
231
- />
232
- ))}
369
+ {hasAnyItem(items)
370
+ ? items.map(renderEntry)
371
+ : emptyElement}
233
372
  </div>
234
373
  </FloatingFocusManager>
235
374
  </FloatingPortal>
@@ -0,0 +1,67 @@
1
+ import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ import * as RadioGroupStories from "./RadioGroup.stories";
4
+
5
+ <Meta of={RadioGroupStories} />
6
+
7
+ # RadioGroup
8
+
9
+ One choice out of many, each on its own row with a label the size of prose.
10
+
11
+ `RadioGroup` and [`SegmentedControl`](/docs/components-segmentedcontrol--docs) are the
12
+ **same composition** — `SinglePicker` + `RovingFocus`, a `radiogroup` of `radio`s where
13
+ selection follows focus — and a different shape. The segmented strip is compact, for a
14
+ handful of short options that share a control row; the radio group is form-length, where
15
+ each option carries a real sentence and an in-line strip would wrap. Choosing between them
16
+ is layout, not behaviour, which is why the keyboard model is identical and lives in the same
17
+ two hooks.
18
+
19
+ <Canvas of={RadioGroupStories.Default} />
20
+
21
+ ## Props
22
+
23
+ <Controls />
24
+
25
+ ## `radiogroup`, not a stack of checkboxes
26
+
27
+ A column of `<input type="checkbox">` is the **independent toggles** pattern: nothing in it
28
+ says the options are mutually exclusive, so a screen reader announces each as its own
29
+ on/off with no "1 of 4" and no clue that checking one unchecks the rest. A single choice
30
+ from a named set is a radio group, and ARIA has a pattern for it — which is the same
31
+ correction `SegmentedControl` makes to rip-deck's row of `aria-pressed` buttons.
32
+
33
+ <Canvas of={RadioGroupStories.AllVariants} />
34
+
35
+ ## Selection follows focus
36
+
37
+ APG gives a radio group one activation mode: the arrow keys both move focus and check.
38
+ There is nothing behind an option here to be expensive — no panel, no request — so unlike
39
+ `Tabs` there is no `manual` mode and no prop, because a `manual` radio group is one the
40
+ arrow keys appear to do nothing in. Tab enters the group once (the roving-tabindex rule),
41
+ the arrows move and check together, Home and End jump to the ends, and the ends wrap.
42
+
43
+ A disabled option is simply **not registered** with the roving group, so the arrows skip it
44
+ without any command having to know what "disabled" means. It stays registered with the
45
+ picker — it is still one of the options, and still the one a saved `selectedValue` may point
46
+ at.
47
+
48
+ <Canvas of={RadioGroupStories.AllStates} />
49
+
50
+ ## A read-only group shows the choice without moving it
51
+
52
+ `isReadOnly` severs the one link that defines this component — selection follows focus — so
53
+ the arrow keys and clicks still travel the options for reading but no longer move the
54
+ choice. The group announces `aria-readonly`, and the chosen option shows at full contrast
55
+ in the **neutral** intent: a value you may read here and change elsewhere. That is distinct
56
+ from a single **disabled** option, which is dimmed with `opacity-60` and dropped from the
57
+ focus group but stays selectable via `selectedValue`.
58
+
59
+ ## `selectedValue` is the initial value, not a controlled one
60
+
61
+ It decides the first render and nothing after — Charcuterie owns the choice from then on,
62
+ and `onChange` reports it outward. Members register from an effect, so on the first paint
63
+ the value lives in the picker's `pendingValue` and the group paints already showing it
64
+ rather than flashing empty for a frame — a flash on a desktop, a visible blank on a kiosk
65
+ Pi.
66
+
67
+ <Canvas of={RadioGroupStories.Interactive} />
@@ -0,0 +1,140 @@
1
+ import type { Meta, StoryObj } from "@storybook/react"
2
+
3
+ import { controlSizeArgType } from "../argTypes.storyHelpers.ts"
4
+ import {
5
+ StoryCell,
6
+ StoryGrid,
7
+ StorySection,
8
+ } from "../board.storyHelpers.tsx"
9
+ import type { RadioItem } from "./RadioGroup.tsx"
10
+ import { RadioGroup } from "./RadioGroup.tsx"
11
+
12
+ /**
13
+ * Options carrying a real sentence each — the case an in-line
14
+ * `SegmentedControl` strip would wrap, and the reason to stack.
15
+ */
16
+ const NAMING_ITEMS: RadioItem[] = [
17
+ { label: "Match the Plex agent", value: "plex" },
18
+ { label: "Match AniDB titles", value: "anidb" },
19
+ {
20
+ label: "Keep the original filenames",
21
+ value: "original",
22
+ },
23
+ { label: "Use a custom pattern", value: "custom" },
24
+ ]
25
+
26
+ const meta = {
27
+ title: "Components/RadioGroup",
28
+ component: RadioGroup,
29
+ parameters: { layout: "padded" },
30
+ argTypes: { size: controlSizeArgType },
31
+ // The component's own defaults, restated — Storybook does not seed
32
+ // `args` from docgen, so an unstated default shows in the props
33
+ // table with nothing selected in its control.
34
+ args: { isReadOnly: false, size: "md" },
35
+ } satisfies Meta<typeof RadioGroup>
36
+
37
+ export default meta
38
+
39
+ type Story = StoryObj<typeof meta>
40
+
41
+ export const Default: Story = {
42
+ args: { items: NAMING_ITEMS, label: "Naming scheme" },
43
+ }
44
+
45
+ export const AllVariants: Story = {
46
+ args: { items: NAMING_ITEMS, label: "Naming scheme" },
47
+ render: (controlProps) => (
48
+ <StorySection title="Three control heights, all reading the density axis rather than hardcoding a length.">
49
+ <StoryGrid columns={3}>
50
+ {(["sm", "md", "lg"] as const).map((size) => (
51
+ <StoryCell key={size} label={size}>
52
+ <RadioGroup
53
+ {...controlProps}
54
+ label={`Naming scheme at ${size}`}
55
+ size={size}
56
+ />
57
+ </StoryCell>
58
+ ))}
59
+ </StoryGrid>
60
+ </StorySection>
61
+ ),
62
+ }
63
+
64
+ const STATE_ITEMS: RadioItem[] = [
65
+ { label: "Match the Plex agent", value: "plex" },
66
+ { label: "Match AniDB titles", value: "anidb" },
67
+ {
68
+ isDisabled: true,
69
+ label: "Keep the original filenames",
70
+ value: "original",
71
+ },
72
+ { label: "Use a custom pattern", value: "custom" },
73
+ ]
74
+
75
+ /**
76
+ * A disabled option is out of the *focus* group and still one of the
77
+ * *options* — it can even be the one a consumer names as the initial
78
+ * `selectedValue`, which is what a saved preference pointing at a
79
+ * scheme the source no longer supports looks like.
80
+ */
81
+ export const AllStates: Story = {
82
+ args: { items: STATE_ITEMS, label: "Naming scheme" },
83
+ render: (controlProps) => (
84
+ <StorySection title="What varies is which option is checked and which are reachable.">
85
+ <StoryGrid columns={2}>
86
+ <StoryCell label="first option checked">
87
+ <RadioGroup
88
+ {...controlProps}
89
+ label="Naming scheme starting on Plex"
90
+ />
91
+ </StoryCell>
92
+
93
+ <StoryCell label="starts on the fourth option">
94
+ <RadioGroup
95
+ {...controlProps}
96
+ label="Naming scheme starting on custom"
97
+ selectedValue="custom"
98
+ />
99
+ </StoryCell>
100
+
101
+ <StoryCell label="a disabled option">
102
+ <RadioGroup
103
+ {...controlProps}
104
+ label="Naming scheme with original unavailable"
105
+ />
106
+ </StoryCell>
107
+
108
+ <StoryCell label="two options">
109
+ <RadioGroup
110
+ {...controlProps}
111
+ items={[
112
+ { label: "Move the files", value: "move" },
113
+ { label: "Copy the files", value: "copy" },
114
+ ]}
115
+ label="On import"
116
+ />
117
+ </StoryCell>
118
+
119
+ <StoryCell label="read-only">
120
+ <RadioGroup
121
+ {...controlProps}
122
+ isReadOnly
123
+ items={NAMING_ITEMS}
124
+ label="Naming scheme, read-only"
125
+ selectedValue="anidb"
126
+ />
127
+ </StoryCell>
128
+ </StoryGrid>
129
+ </StorySection>
130
+ ),
131
+ }
132
+
133
+ /**
134
+ * The complete keyboard path. Tab enters the group **once** — the
135
+ * roving-tabindex rule — then the arrow keys move and check
136
+ * together, the same activation model as `SegmentedControl`.
137
+ */
138
+ export const Interactive: Story = {
139
+ args: { items: NAMING_ITEMS, label: "Naming scheme" },
140
+ }