@charcuterie/ui 4.5.0 → 4.5.1

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 (207) hide show
  1. package/README.md +7 -2
  2. package/dist/Card/Card.d.ts +5 -2
  3. package/dist/Card/Card.d.ts.map +1 -1
  4. package/dist/Card/Card.js +23 -3
  5. package/dist/Card/Card.js.map +1 -1
  6. package/package.json +2 -1
  7. package/src/Accordion/Accordion.mdx +1 -1
  8. package/src/Accordion/Accordion.stories.tsx +3 -1
  9. package/src/Accordion/Accordion.test.tsx +4 -4
  10. package/src/ActionTiles/ActionTiles.mdx +1 -1
  11. package/src/ActionTiles/ActionTiles.stories.tsx +3 -1
  12. package/src/ActionTiles/ActionTiles.test.tsx +3 -3
  13. package/src/AdaptiveGrid/AdaptiveGrid.mdx +1 -1
  14. package/src/AdaptiveGrid/AdaptiveGrid.stories.tsx +3 -1
  15. package/src/AdaptiveGrid/AdaptiveGrid.test.tsx +5 -5
  16. package/src/Alert/Alert.mdx +1 -1
  17. package/src/Alert/Alert.stories.tsx +3 -1
  18. package/src/Alert/Alert.test.tsx +2 -2
  19. package/src/Avatar/Avatar.mdx +1 -1
  20. package/src/Avatar/Avatar.stories.tsx +4 -1
  21. package/src/Avatar/Avatar.test.tsx +2 -2
  22. package/src/Badge/Badge.mdx +1 -1
  23. package/src/Badge/Badge.stories.tsx +3 -1
  24. package/src/Badge/Badge.test.tsx +3 -3
  25. package/src/BadgeButton/BadgeButton.mdx +1 -1
  26. package/src/BadgeButton/BadgeButton.stories.tsx +3 -1
  27. package/src/BadgeButton/BadgeButton.test.tsx +2 -2
  28. package/src/Board/Board.mdx +1 -1
  29. package/src/Board/Board.stories.tsx +4 -1
  30. package/src/Board/Board.test.tsx +2 -2
  31. package/src/Button/Button.mdx +1 -1
  32. package/src/Button/Button.stories.tsx +3 -1
  33. package/src/Button/Button.test.tsx +2 -2
  34. package/src/ButtonLink/ButtonLink.mdx +1 -1
  35. package/src/ButtonLink/ButtonLink.stories.tsx +3 -1
  36. package/src/ButtonLink/ButtonLink.test.tsx +3 -3
  37. package/src/Card/Card.mdx +21 -1
  38. package/src/Card/Card.stories.tsx +91 -1
  39. package/src/Card/Card.test.tsx +60 -3
  40. package/src/Card/Card.tsx +24 -2
  41. package/src/Checkbox/Checkbox.mdx +1 -1
  42. package/src/Checkbox/Checkbox.stories.tsx +3 -1
  43. package/src/Checkbox/Checkbox.test.tsx +4 -4
  44. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.mdx +1 -1
  45. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.stories.tsx +3 -1
  46. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.test.tsx +5 -5
  47. package/src/ColorSchemeToggle/ColorSchemeToggle.mdx +1 -1
  48. package/src/ColorSchemeToggle/ColorSchemeToggle.stories.tsx +3 -1
  49. package/src/ColorSchemeToggle/ColorSchemeToggle.test.tsx +5 -5
  50. package/src/Combobox/Combobox.mdx +1 -1
  51. package/src/Combobox/Combobox.stories.tsx +3 -1
  52. package/src/Combobox/Combobox.test.tsx +2 -2
  53. package/src/CopyButton/CopyButton.mdx +1 -1
  54. package/src/CopyButton/CopyButton.stories.tsx +4 -1
  55. package/src/CopyButton/CopyButton.test.tsx +3 -3
  56. package/src/DataTable/DataTable.mdx +1 -1
  57. package/src/DataTable/DataTable.stories.tsx +3 -1
  58. package/src/DataTable/DataTable.test.tsx +3 -3
  59. package/src/DatePicker/DatePicker.mdx +1 -1
  60. package/src/DatePicker/DatePicker.stories.tsx +3 -1
  61. package/src/DatePicker/DatePicker.test.tsx +3 -3
  62. package/src/DatePicker/plainDate.test.ts +21 -3
  63. package/src/Dialog/Dialog.mdx +1 -1
  64. package/src/Dialog/Dialog.stories.tsx +3 -1
  65. package/src/Dialog/Dialog.test.tsx +2 -2
  66. package/src/DropRail/DropRail.mdx +1 -1
  67. package/src/DropRail/DropRail.stories.tsx +4 -1
  68. package/src/DropRail/DropRail.test.tsx +4 -4
  69. package/src/EmptyState/EmptyState.mdx +1 -1
  70. package/src/EmptyState/EmptyState.stories.tsx +3 -1
  71. package/src/EmptyState/EmptyState.test.tsx +2 -2
  72. package/src/Field/Field.mdx +1 -1
  73. package/src/Field/Field.stories.tsx +3 -1
  74. package/src/Field/Field.test.tsx +4 -4
  75. package/src/FileDropZone/FileDropZone.mdx +1 -1
  76. package/src/FileDropZone/FileDropZone.stories.tsx +3 -1
  77. package/src/FileDropZone/FileDropZone.test.tsx +3 -3
  78. package/src/Header/Header.mdx +1 -1
  79. package/src/Header/Header.stories.tsx +3 -1
  80. package/src/IconButton/IconButton.mdx +1 -1
  81. package/src/IconButton/IconButton.stories.tsx +3 -1
  82. package/src/IconButton/IconButton.test.tsx +2 -2
  83. package/src/Lightbox/Lightbox.mdx +1 -1
  84. package/src/Lightbox/Lightbox.stories.tsx +3 -1
  85. package/src/Lightbox/Lightbox.test.tsx +2 -2
  86. package/src/Listbox/Listbox.mdx +1 -1
  87. package/src/Listbox/Listbox.stories.tsx +3 -1
  88. package/src/Listbox/Listbox.test.tsx +5 -5
  89. package/src/LiveStatusIndicator/LiveStatusIndicator.mdx +1 -1
  90. package/src/LiveStatusIndicator/LiveStatusIndicator.stories.tsx +3 -1
  91. package/src/LiveStatusIndicator/LiveStatusIndicator.test.tsx +2 -2
  92. package/src/LogViewer/LogViewer.mdx +1 -1
  93. package/src/LogViewer/LogViewer.stories.tsx +3 -1
  94. package/src/LogViewer/LogViewer.test.tsx +2 -2
  95. package/src/Main/Main.mdx +1 -1
  96. package/src/Main/Main.stories.tsx +3 -1
  97. package/src/Main/Main.test.tsx +3 -3
  98. package/src/MarkdownEditor/MarkdownEditor.mdx +1 -1
  99. package/src/MarkdownEditor/MarkdownEditor.stories.tsx +3 -1
  100. package/src/MarkdownEditor/MarkdownEditor.test.tsx +4 -4
  101. package/src/MarkdownLine/MarkdownLine.mdx +1 -1
  102. package/src/MarkdownLine/MarkdownLine.stories.tsx +4 -1
  103. package/src/MarkdownLine/MarkdownLine.test.tsx +3 -3
  104. package/src/MediaTile/MediaTile.stories.tsx +3 -1
  105. package/src/MediaTile/MediaTile.test.tsx +2 -2
  106. package/src/Menu/Menu.mdx +1 -1
  107. package/src/Menu/Menu.stories.tsx +3 -1
  108. package/src/Modal/Modal.mdx +1 -1
  109. package/src/Modal/Modal.stories.tsx +3 -1
  110. package/src/Modal/Modal.test.tsx +3 -3
  111. package/src/Nav/Nav.mdx +1 -1
  112. package/src/Nav/Nav.stories.tsx +3 -1
  113. package/src/Nav/Nav.test.tsx +6 -6
  114. package/src/NavBar/NavBar.mdx +1 -1
  115. package/src/NavBar/NavBar.stories.tsx +3 -1
  116. package/src/NavBar/NavBar.test.tsx +3 -3
  117. package/src/Picker/Picker.mdx +1 -1
  118. package/src/Picker/Picker.stories.tsx +3 -1
  119. package/src/Picker/Picker.test.tsx +2 -2
  120. package/src/Popover/Popover.mdx +1 -1
  121. package/src/Popover/Popover.stories.tsx +3 -1
  122. package/src/Popover/Popover.test.tsx +2 -2
  123. package/src/PortraitTiles/PortraitTiles.mdx +1 -1
  124. package/src/PortraitTiles/PortraitTiles.stories.tsx +3 -1
  125. package/src/PortraitTiles/PortraitTiles.test.tsx +5 -5
  126. package/src/ProgressBar/ProgressBar.mdx +1 -1
  127. package/src/ProgressBar/ProgressBar.stories.tsx +3 -1
  128. package/src/ProgressBar/ProgressBar.test.tsx +2 -2
  129. package/src/QueryBuilder/QueryBuilder.mdx +1 -1
  130. package/src/QueryBuilder/QueryBuilder.stories.tsx +3 -1
  131. package/src/QueryBuilder/QueryBuilder.test.tsx +2 -2
  132. package/src/RadioGroup/RadioGroup.mdx +1 -1
  133. package/src/RadioGroup/RadioGroup.stories.tsx +3 -1
  134. package/src/RadioGroup/RadioGroup.test.tsx +2 -2
  135. package/src/Rail/Rail.mdx +1 -1
  136. package/src/Rail/Rail.stories.tsx +3 -1
  137. package/src/Rail/Rail.test.tsx +2 -2
  138. package/src/RangeSlider/RangeSlider.mdx +1 -1
  139. package/src/RangeSlider/RangeSlider.stories.tsx +3 -1
  140. package/src/RangeSlider/RangeSlider.test.tsx +7 -3
  141. package/src/ReorderList/ReorderList.mdx +1 -1
  142. package/src/ReorderList/ReorderList.stories.tsx +3 -1
  143. package/src/ReorderList/ReorderList.test.tsx +7 -7
  144. package/src/RouterLink/UnstyledLink.stories.tsx +4 -1
  145. package/src/RouterLink/UnstyledLink.test.tsx +2 -2
  146. package/src/SearchInput/SearchInput.mdx +1 -1
  147. package/src/SearchInput/SearchInput.stories.tsx +3 -1
  148. package/src/SegmentedControl/SegmentedControl.mdx +1 -1
  149. package/src/SegmentedControl/SegmentedControl.stories.tsx +3 -1
  150. package/src/SegmentedControl/SegmentedControl.test.tsx +2 -2
  151. package/src/Select/Select.mdx +1 -1
  152. package/src/Select/Select.stories.tsx +3 -1
  153. package/src/Select/Select.test.tsx +4 -4
  154. package/src/Shell/Shell.mdx +1 -1
  155. package/src/Shell/Shell.stories.tsx +3 -1
  156. package/src/Shell/Shell.test.tsx +2 -2
  157. package/src/Skeleton/Skeleton.mdx +1 -1
  158. package/src/Skeleton/Skeleton.stories.tsx +3 -1
  159. package/src/Skeleton/Skeleton.test.tsx +2 -2
  160. package/src/Slider/Slider.mdx +1 -1
  161. package/src/Slider/Slider.stories.tsx +3 -1
  162. package/src/Slider/Slider.test.tsx +2 -2
  163. package/src/SortableTableHeader/SortableTableHeader.mdx +1 -1
  164. package/src/SortableTableHeader/SortableTableHeader.stories.tsx +3 -1
  165. package/src/SortableTableHeader/SortableTableHeader.test.tsx +3 -3
  166. package/src/Spinner/Spinner.mdx +1 -1
  167. package/src/Spinner/Spinner.stories.tsx +3 -1
  168. package/src/Spinner/Spinner.test.tsx +2 -2
  169. package/src/Stepper/Stepper.mdx +1 -1
  170. package/src/Stepper/Stepper.stories.tsx +4 -1
  171. package/src/Stepper/Stepper.test.tsx +7 -7
  172. package/src/Swatch/Swatch.mdx +1 -1
  173. package/src/Swatch/Swatch.stories.tsx +4 -1
  174. package/src/Swatch/Swatch.test.tsx +3 -3
  175. package/src/Switch/Switch.mdx +1 -1
  176. package/src/Switch/Switch.stories.tsx +3 -1
  177. package/src/Switch/Switch.test.tsx +2 -2
  178. package/src/Tabs/Tabs.mdx +1 -1
  179. package/src/Tabs/Tabs.stories.tsx +3 -1
  180. package/src/Tabs/Tabs.test.tsx +3 -3
  181. package/src/TextLink/TextLink.mdx +1 -1
  182. package/src/TextLink/TextLink.stories.tsx +3 -1
  183. package/src/TextLink/TextLink.test.tsx +4 -4
  184. package/src/TimecodeInput/TimecodeInput.mdx +1 -1
  185. package/src/TimecodeInput/TimecodeInput.stories.tsx +3 -1
  186. package/src/Toast/Toast.mdx +1 -1
  187. package/src/Toast/Toast.stories.tsx +3 -1
  188. package/src/Toast/Toast.test.tsx +2 -2
  189. package/src/Toolbar/Toolbar.mdx +1 -1
  190. package/src/Toolbar/Toolbar.stories.tsx +3 -1
  191. package/src/Toolbar/Toolbar.test.tsx +5 -5
  192. package/src/Tooltip/Tooltip.mdx +1 -1
  193. package/src/Tooltip/Tooltip.stories.tsx +3 -1
  194. package/src/Tooltip/Tooltip.test.tsx +2 -2
  195. package/src/VirtualizedGrid/VirtualizedGrid.mdx +1 -1
  196. package/src/VirtualizedGrid/VirtualizedGrid.stories.tsx +4 -1
  197. package/src/VirtualizedGrid/VirtualizedGrid.test.tsx +5 -5
  198. package/src/VisuallyHidden/VisuallyHidden.mdx +1 -1
  199. package/src/VisuallyHidden/VisuallyHidden.stories.tsx +3 -1
  200. package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.mdx +1 -1
  201. package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.stories.tsx +3 -1
  202. package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.test.tsx +3 -3
  203. package/src/markdownEditorCodeMirror/MarkdownView.mdx +1 -1
  204. package/src/markdownEditorCodeMirror/MarkdownView.stories.tsx +4 -1
  205. package/src/markdownEditorCodeMirror/MarkdownView.test.tsx +44 -20
  206. package/src/reactRouter/ReactRouterAdapter.stories.tsx +3 -1
  207. package/src/reactRouter/ReactRouterAdapter.test.tsx +2 -2
package/src/Card/Card.mdx CHANGED
@@ -11,7 +11,7 @@ a **landmark**, so `getByRole("region", { name: "Bay 3" })` scopes every query i
11
11
  Without that, an agent driving a 16-bay tower sees sixteen identical "Start" buttons and no
12
12
  way to say which one it means. The `Interactive` story asserts exactly that.
13
13
 
14
- <Canvas of={CardStories.Default} />
14
+ <Canvas of={CardStories.Playground} />
15
15
 
16
16
  ## Props
17
17
 
@@ -35,6 +35,26 @@ all, deliberately — it has nothing to be named.
35
35
  `raised` means *more separated from base*, not *lighter*, which is why light mode's base is
36
36
  `#F5F7FA` and its raised is white.
37
37
 
38
+ ### A card the app names itself
39
+
40
+ Pass `aria-labelledby` and no `heading` when the app draws its own title row — a name beside
41
+ a count is one line that `heading` cannot express, so mail-sifter's tiles render it and point
42
+ at it. The card keeps that pointer.
43
+
44
+ > **It has to be a fallback, not a default.** The attribute is written after the caller's
45
+ > props are spread, so the obvious `heading ? headingId : undefined` **overwrites** the call
46
+ > site's pointer rather than leaving it alone. React then drops the attribute, the card's name
47
+ > falls back to its entire subtree, and nothing reports it: the markup is valid, the types are
48
+ > satisfied, and axe is content with a name made of everything. Both of mail-sifter's
49
+ > `role="button"` tiles announced their whole contents — "Recent Mail 6 40 arrived in the last
50
+ > 24 hours, 34 done" for a tile called "Recent Mail".
51
+
52
+ `heading` still wins where both are given, because the heading is text the reader can see and
53
+ this component renders it. `aria-label` needs no fallback at all — `Card` never writes one, so
54
+ the spread carries the caller's through.
55
+
56
+ <Canvas of={CardStories.CallerNamed} />
57
+
38
58
  <Canvas of={CardStories.AllVariants} />
39
59
 
40
60
  ## Container queries, not media queries
@@ -1,3 +1,4 @@
1
+ import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
1
2
  import type { Meta, StoryObj } from "@storybook/react"
2
3
 
3
4
  import { toStoryChoice } from "../argTypes.storyHelpers.ts"
@@ -43,7 +44,8 @@ export default meta
43
44
 
44
45
  type Story = StoryObj<typeof meta>
45
46
 
46
- export const Default: Story = {
47
+ export const Playground: Story = {
48
+ parameters: playgroundParameters,
47
49
  args: {
48
50
  children: (
49
51
  <p className="text-content-secondary text-sm">
@@ -296,3 +298,91 @@ export const AccentEdge: Story = {
296
298
  </StorySection>
297
299
  ),
298
300
  }
301
+
302
+ /**
303
+ * Ids for the two cells that point at their own text. Written out
304
+ * rather than generated, because a story is a static demo and the
305
+ * docs page mounts every cell at once — two cells sharing a
306
+ * generated id would be a duplicate-id violation nobody could see.
307
+ */
308
+ const CALLER_HEADING_ID = "card-caller-heading"
309
+ const SHADOWED_HEADING_ID = "card-shadowed-heading"
310
+
311
+ /**
312
+ * The card is named by a heading the **app** draws, not by
313
+ * `heading`.
314
+ *
315
+ * mail-sifter's tiles put the name and its count on one row, so they
316
+ * render the title themselves and pass `aria-labelledby` at the call
317
+ * site. `Card` writes that attribute after the caller's spread, so
318
+ * it has to fall back to the caller's pointer: without the fallback
319
+ * the attribute is overwritten with `undefined`, React drops it, and
320
+ * the tile's name becomes its whole subtree — "Recent Mail 6 40
321
+ * arrived in the last 24 hours, 34 done" instead of "Recent Mail".
322
+ * Nothing reports that: it renders, it typechecks, and axe is happy
323
+ * with a name made of everything.
324
+ *
325
+ * `heading` still wins where it is given, and `aria-label` needs no
326
+ * fallback at all — the component never writes one, so the spread
327
+ * carries it through.
328
+ */
329
+ export const CallerNamed: Story = {
330
+ args: { children: "Content" },
331
+ render: () => (
332
+ <StoryGrid columns={3}>
333
+ <StoryCell
334
+ align="stretch"
335
+ label="the call site's own pointer"
336
+ >
337
+ <Card aria-labelledby={CALLER_HEADING_ID}>
338
+ <div className="flex items-baseline justify-between gap-2">
339
+ <h2
340
+ className="font-semibold text-md leading-tight"
341
+ id={CALLER_HEADING_ID}
342
+ >
343
+ Recent Mail
344
+ </h2>
345
+
346
+ <Badge intent="info" size="sm">
347
+ 6
348
+ </Badge>
349
+ </div>
350
+
351
+ <p className="text-content-secondary text-sm">
352
+ 40 arrived in the last 24 hours, 34 done.
353
+ </p>
354
+ </Card>
355
+ </StoryCell>
356
+
357
+ <StoryCell
358
+ align="stretch"
359
+ label="a heading beats the pointer"
360
+ >
361
+ <Card
362
+ aria-labelledby={SHADOWED_HEADING_ID}
363
+ heading="Bay 5"
364
+ >
365
+ <p
366
+ className="text-content-secondary text-sm"
367
+ id={SHADOWED_HEADING_ID}
368
+ >
369
+ Pointed at, and deliberately not the name — the
370
+ heading is the text the reader can see.
371
+ </p>
372
+ </Card>
373
+ </StoryCell>
374
+
375
+ <StoryCell
376
+ align="stretch"
377
+ label="an aria-label passes through"
378
+ >
379
+ <Card aria-label="Unread mail">
380
+ <p className="text-content-secondary text-sm">
381
+ No heading and no pointer, so the card is named
382
+ by the label the call site set.
383
+ </p>
384
+ </Card>
385
+ </StoryCell>
386
+ </StoryGrid>
387
+ ),
388
+ }
@@ -6,11 +6,16 @@ import { mountStory } from "../mountStory.testHelpers.ts"
6
6
  import { expectAgentDrivable } from "../testing/index.ts"
7
7
  import * as stories from "./Card.stories.tsx"
8
8
 
9
- const { AccentEdge, AllStates, Default, Interactive } =
10
- composeStories(stories)
9
+ const {
10
+ AccentEdge,
11
+ AllStates,
12
+ CallerNamed,
13
+ Playground,
14
+ Interactive,
15
+ } = composeStories(stories)
11
16
 
12
17
  test("a heading turns the card into a named landmark", async () => {
13
- const { canvas } = await mountStory(Default)
18
+ const { canvas } = await mountStory(Playground)
14
19
 
15
20
  expectAgentDrivable(canvas, {
16
21
  name: "Bay 3",
@@ -29,6 +34,58 @@ test("a card with no heading is not a landmark", async () => {
29
34
  )
30
35
  })
31
36
 
37
+ /**
38
+ * The name the call site set is the fallback, and it is the half the
39
+ * component used to throw away.
40
+ *
41
+ * `aria-labelledby` is written after the caller's spread, so the old
42
+ * `heading ? headingId : undefined` overwrote the caller's pointer
43
+ * instead of deferring to it. A tile naming itself from a heading it
44
+ * draws lost its name and picked up its whole subtree — and this is
45
+ * the only gate that can see it, because the markup is valid, the
46
+ * types are satisfied, and a card named by everything is still a
47
+ * card with a name.
48
+ */
49
+ test("a card with no heading keeps the caller's own name", async () => {
50
+ const { canvas } = await mountStory(CallerNamed)
51
+
52
+ // Exactly "Recent Mail". Were the subtree naming it, this would be
53
+ // "Recent Mail 6 40 arrived in the last 24 hours, 34 done" and the
54
+ // query would match nothing.
55
+ expectAgentDrivable(canvas, {
56
+ name: "Recent Mail",
57
+ role: "region",
58
+ })
59
+ })
60
+
61
+ /**
62
+ * The fallback is a fallback, not a swap: a `heading` is text the
63
+ * reader can see and the component renders it, so it still wins.
64
+ */
65
+ test("a heading still names the card over the caller's pointer", async () => {
66
+ const { canvas } = await mountStory(CallerNamed)
67
+
68
+ expectAgentDrivable(canvas, {
69
+ name: "Bay 5",
70
+ role: "region",
71
+ })
72
+ })
73
+
74
+ /**
75
+ * `aria-label` needs no fallback — the component never writes one,
76
+ * so the spread carries the caller's through. Asserted anyway, so
77
+ * that a future "fix" which starts writing `aria-label` here cannot
78
+ * take the caller's with it the way this one took `aria-labelledby`.
79
+ */
80
+ test("a caller's aria-label reaches the card untouched", async () => {
81
+ const { canvas } = await mountStory(CallerNamed)
82
+
83
+ expectAgentDrivable(canvas, {
84
+ name: "Unread mail",
85
+ role: "region",
86
+ })
87
+ })
88
+
32
89
  /**
33
90
  * Two identical "Start rip" buttons on one page, disambiguated only
34
91
  * by the region each sits in.
package/src/Card/Card.tsx CHANGED
@@ -83,7 +83,10 @@ const PADDING_CLASS: Record<CardPadding, string> = {
83
83
  *
84
84
  * `aria-labelledby` rather than `aria-label`, because the heading is
85
85
  * on screen already and stating the name twice is how the two drift
86
- * apart.
86
+ * apart. A card with **no** `heading` keeps the `aria-labelledby`
87
+ * the call site passed, for the app that draws its own title row
88
+ * inside the card — the name still points at text on screen, which
89
+ * is the same rule one level out.
87
90
  *
88
91
  * **Density is not a prop here.** mux-magic's inventory turned up a
89
92
  * `CardDensityProvider` in the withdrawn bambuddy evidence, and it
@@ -94,6 +97,7 @@ const PADDING_CLASS: Record<CardPadding, string> = {
94
97
  * a layout fact rather than a density one.
95
98
  */
96
99
  export const Card = ({
100
+ "aria-labelledby": ariaLabelledBy,
97
101
  accentEdge,
98
102
  actions,
99
103
  children,
@@ -122,12 +126,30 @@ export const Card = ({
122
126
  ? style
123
127
  : { ...accentEdgeStyle, ...style }
124
128
 
129
+ // The caller's own pointer is the FALLBACK, not the loser. This
130
+ // attribute is written *after* the spread, so `undefined` here
131
+ // does not mean "leave whatever the call site passed": it
132
+ // overwrites it, React then drops the attribute entirely, and the
133
+ // card's accessible name falls back to its whole subtree with
134
+ // nothing failing — not typecheck, not axe, not a render. A
135
+ // `role="button"` tile in mail-sifter naming itself
136
+ // `aria-labelledby="recent-tile-heading"` announced as "Recent
137
+ // Mail 6 40 arrived in the last 24 hours, 34 done".
138
+ //
139
+ // `heading` still wins when it is given, because that is the name
140
+ // the reader can see and this component renders it. Do not
141
+ // "simplify" this back to a ternary ending in `undefined`.
142
+ //
143
+ // `aria-label` needs no equivalent: the component never writes
144
+ // one, so the spread carries the caller's through untouched.
145
+ const labelledById = heading ? headingId : ariaLabelledBy
146
+
125
147
  const Heading = `h${headingLevel}` as const
126
148
 
127
149
  return (
128
150
  <section
129
151
  {...sectionProps}
130
- aria-labelledby={heading ? headingId : undefined}
152
+ aria-labelledby={labelledById}
131
153
  className={toClassName(
132
154
  // `@container` here, not on a wrapper, is what lets the
133
155
  // header stack at narrow widths *inside a wide viewport* —
@@ -16,7 +16,7 @@ Here the box is `bg-surface-sunken`, its border `border-border-strong`, and the
16
16
  the kiosk with no per-app override. `BooleanField` becomes a thin wrapper that binds this
17
17
  to a `CommandField`.
18
18
 
19
- <Canvas of={CheckboxStories.Default} />
19
+ <Canvas of={CheckboxStories.Playground} />
20
20
 
21
21
  ## Props
22
22
 
@@ -1,3 +1,4 @@
1
+ import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
1
2
  import type { Meta, StoryObj } from "@storybook/react"
2
3
 
3
4
  import { controlSizeArgType } from "../argTypes.storyHelpers.ts"
@@ -29,7 +30,8 @@ export default meta
29
30
 
30
31
  type Story = StoryObj<typeof meta>
31
32
 
32
- export const Default: Story = {
33
+ export const Playground: Story = {
34
+ parameters: playgroundParameters,
33
35
  args: { label: "Delete originals after import" },
34
36
  }
35
37
 
@@ -9,7 +9,7 @@ import * as stories from "./Checkbox.stories.tsx"
9
9
 
10
10
  const {
11
11
  AllStates,
12
- Default,
12
+ Playground,
13
13
  Interactive,
14
14
  WithDescriptions,
15
15
  WithValues,
@@ -17,7 +17,7 @@ const {
17
17
 
18
18
  test("it is a checkbox a screen reader can name", async () => {
19
19
  const { canvas, canvasElement } =
20
- await mountStory(Default)
20
+ await mountStory(Playground)
21
21
 
22
22
  const checkbox = expectAgentDrivable(canvas, {
23
23
  name: "Delete originals after import",
@@ -151,7 +151,7 @@ test("value names the member, and a group reads back the ticked ones", async ()
151
151
  })
152
152
 
153
153
  test("a box with no value stays out of the way", async () => {
154
- const { canvas } = await mountStory(Default)
154
+ const { canvas } = await mountStory(Playground)
155
155
 
156
156
  // `value` is optional and a lone boolean should omit it. React drops
157
157
  // an `undefined` attribute entirely rather than writing an empty
@@ -252,7 +252,7 @@ test("each box owns its own description, and the group owns the group's", async
252
252
  })
253
253
 
254
254
  test("a box with no description writes no aria-describedby", async () => {
255
- const { canvas } = await mountStory(Default)
255
+ const { canvas } = await mountStory(Playground)
256
256
 
257
257
  // The slot is opt-in. React drops an `undefined` attribute rather
258
258
  // than writing an empty string, so a box without a hint points at
@@ -12,7 +12,7 @@ the [`useColorScheme`](/) hook to the three defaults from `@charcuterie/logic/br
12
12
  [`ColorSchemeToggle`](?path=/docs/components-colorschemetoggle--docs). Drop it in and the app
13
13
  follows the OS, remembers the user's choice, and re-themes with zero React re-render.
14
14
 
15
- <Canvas of={ColorSchemeSwitcherStories.Default} />
15
+ <Canvas of={ColorSchemeSwitcherStories.Playground} />
16
16
 
17
17
  ## Props
18
18
 
@@ -3,6 +3,7 @@ import type {
3
3
  ColorSchemeResolver,
4
4
  ResolvedColorScheme,
5
5
  } from "@charcuterie/logic"
6
+ import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
6
7
  import type { Meta, StoryObj } from "@storybook/react"
7
8
  import { useRef, useState } from "react"
8
9
  import type { ColorSchemeIcons } from "../ColorSchemeToggle/ColorSchemeToggle.tsx"
@@ -113,6 +114,7 @@ type Story = StoryObj<typeof meta>
113
114
  * scoped to the demo panel, which is exactly how a non-browser host
114
115
  * (Electron `nativeTheme`) wires it — by replacing those seams.
115
116
  */
116
- export const Default: Story = {
117
+ export const Playground: Story = {
118
+ parameters: playgroundParameters,
117
119
  render: () => <ScopedSwitcherDemo />,
118
120
  }
@@ -7,7 +7,7 @@ import { mountStory } from "../mountStory.testHelpers.ts"
7
7
  import { expectAgentDrivable } from "../testing/index.ts"
8
8
  import * as stories from "./ColorSchemeSwitcher.stories.tsx"
9
9
 
10
- const { Default } = composeStories(stories)
10
+ const { Playground } = composeStories(stories)
11
11
 
12
12
  /**
13
13
  * The connected switcher is agent-drivable and cycles like the
@@ -15,7 +15,7 @@ const { Default } = composeStories(stories)
15
15
  * reaches the applier: the demo panel's `data-scheme` tracks it.
16
16
  */
17
17
  test("it renders a named, drivable button", async () => {
18
- const { canvas } = await mountStory(Default)
18
+ const { canvas } = await mountStory(Playground)
19
19
 
20
20
  expectAgentDrivable(canvas, {
21
21
  name: /colour scheme/i,
@@ -24,7 +24,7 @@ test("it renders a named, drivable button", async () => {
24
24
  })
25
25
 
26
26
  test("system resolves through the injected resolver to the panel", async () => {
27
- const { canvasElement } = await mountStory(Default)
27
+ const { canvasElement } = await mountStory(Playground)
28
28
 
29
29
  const panel = canvasElement.querySelector("[data-scheme]")
30
30
 
@@ -33,7 +33,7 @@ test("system resolves through the injected resolver to the panel", async () => {
33
33
  })
34
34
 
35
35
  test("cycling to a concrete mode re-applies the scheme", async () => {
36
- const { canvasElement } = await mountStory(Default)
36
+ const { canvasElement } = await mountStory(Playground)
37
37
 
38
38
  const canvas = within(canvasElement)
39
39
 
@@ -53,7 +53,7 @@ test("cycling to a concrete mode re-applies the scheme", async () => {
53
53
  })
54
54
 
55
55
  test("the connected switcher forwards the neutral intent by default", async () => {
56
- const { canvasElement } = await mountStory(Default)
56
+ const { canvasElement } = await mountStory(Playground)
57
57
 
58
58
  const button = canvasElement.querySelector("button")
59
59
 
@@ -16,7 +16,7 @@ That split is deliberate. The browser dependency lives one layer up in
16
16
  runs unchanged in any runtime — a plain React app wires `onCycle` to `useState`, an Electron
17
17
  or React-Native host wires it to `useColorScheme` with its own resolver.
18
18
 
19
- <Canvas of={ColorSchemeToggleStories.Default} />
19
+ <Canvas of={ColorSchemeToggleStories.Playground} />
20
20
 
21
21
  ## Props
22
22
 
@@ -1,4 +1,5 @@
1
1
  import type { ColorSchemeMode } from "@charcuterie/logic"
2
+ import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
2
3
  import type { Meta, StoryObj } from "@storybook/react"
3
4
  import { useState } from "react"
4
5
 
@@ -71,7 +72,8 @@ type Story = StoryObj<typeof meta>
71
72
  * The accessible name states the current mode and the next one, so
72
73
  * it changes as the button cycles — that is what the test asserts.
73
74
  */
74
- export const Default: Story = {
75
+ export const Playground: Story = {
76
+ parameters: playgroundParameters,
75
77
  render: (toggleProps) => {
76
78
  const [mode, setMode] =
77
79
  useState<ColorSchemeMode>("system")
@@ -10,7 +10,7 @@ import { mountStory } from "../mountStory.testHelpers.ts"
10
10
  import { expectAgentDrivable } from "../testing/index.ts"
11
11
  import meta, * as stories from "./ColorSchemeToggle.stories.tsx"
12
12
 
13
- const { AllModes, Default } = composeStories(stories)
13
+ const { AllModes, Playground } = composeStories(stories)
14
14
 
15
15
  /**
16
16
  * The name states the current mode — a screen reader that lands on
@@ -18,7 +18,7 @@ const { AllModes, Default } = composeStories(stories)
18
18
  * and an agent can find it.
19
19
  */
20
20
  test("the toggle is named for its current mode", async () => {
21
- const { canvas } = await mountStory(Default)
21
+ const { canvas } = await mountStory(Playground)
22
22
 
23
23
  expectAgentDrivable(canvas, {
24
24
  name: /colour scheme: system/i,
@@ -27,7 +27,7 @@ test("the toggle is named for its current mode", async () => {
27
27
  })
28
28
 
29
29
  test("a press cycles the mode and updates the name", async () => {
30
- const { canvas } = await mountStory(Default)
30
+ const { canvas } = await mountStory(Playground)
31
31
 
32
32
  const button = expectAgentDrivable(canvas, {
33
33
  name: /colour scheme: system/i,
@@ -53,7 +53,7 @@ test("a press cycles the mode and updates the name", async () => {
53
53
 
54
54
  test("the icon is hidden from assistive tech; the name carries it", async () => {
55
55
  const { canvas, canvasElement } =
56
- await mountStory(Default)
56
+ await mountStory(Playground)
57
57
 
58
58
  expectAgentDrivable(canvas, {
59
59
  name: /colour scheme/i,
@@ -90,7 +90,7 @@ test("each mode renders its own control", async () => {
90
90
  * action on real app chrome.
91
91
  */
92
92
  test("defaults to the neutral intent, not accent", async () => {
93
- const { canvasElement } = await mountStory(Default)
93
+ const { canvasElement } = await mountStory(Playground)
94
94
 
95
95
  const button = canvasElement.querySelector("button")
96
96
 
@@ -13,7 +13,7 @@ Use it when the list is long enough to search or filter; reach for
13
13
  [`Listbox`](?path=/docs/components-listbox--docs) for a short rich-option list, and the
14
14
  native [`Select`](?path=/docs/components-select--docs) for a plain one.
15
15
 
16
- <Canvas of={ComboboxStories.Default} />
16
+ <Canvas of={ComboboxStories.Playground} />
17
17
 
18
18
  ## Props
19
19
 
@@ -1,4 +1,5 @@
1
1
  import { useVisibility } from "@charcuterie/logic"
2
+ import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
2
3
  import type { ControlSize } from "@charcuterie/tokens"
3
4
  import type { Meta, StoryObj } from "@storybook/react"
4
5
  import type { ReactNode } from "react"
@@ -150,7 +151,8 @@ const ComboboxHarness = ({
150
151
  )
151
152
  }
152
153
 
153
- export const Default: Story = {
154
+ export const Playground: Story = {
155
+ parameters: playgroundParameters,
154
156
  args: {
155
157
  isVisible: false,
156
158
  onDismiss: () => {},
@@ -11,7 +11,7 @@ const {
11
11
  AllVariants,
12
12
  AttachedInputDrillDown,
13
13
  ChosenValueOnOpen,
14
- Default,
14
+ Playground,
15
15
  DisabledFirstOption,
16
16
  Interactive,
17
17
  LongFooter,
@@ -42,7 +42,7 @@ const openMulti = async () => {
42
42
  }
43
43
 
44
44
  const openDefault = async () => {
45
- const mounted = await mountStory(Default)
45
+ const mounted = await mountStory(Playground)
46
46
 
47
47
  await userEvent.click(
48
48
  expectAgentDrivable(mounted.canvas, {
@@ -9,7 +9,7 @@ import * as CopyButtonStories from "./CopyButton.stories";
9
9
  **A button that puts a string on the clipboard and says so.** Press it, the label turns to
10
10
  `Copied`, and the caller is told whether that is true.
11
11
 
12
- <Canvas of={CopyButtonStories.Default} />
12
+ <Canvas of={CopyButtonStories.Playground} />
13
13
 
14
14
  ## Props
15
15
 
@@ -1,3 +1,4 @@
1
+ import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
1
2
  import type { Meta, StoryObj } from "@storybook/react"
2
3
  import { userEvent, within } from "storybook/test"
3
4
 
@@ -29,7 +30,9 @@ export default meta
29
30
 
30
31
  type Story = StoryObj<typeof meta>
31
32
 
32
- export const Default: Story = {}
33
+ export const Playground: Story = {
34
+ parameters: playgroundParameters,
35
+ }
33
36
 
34
37
  /**
35
38
  * The confirmation, held open. `confirmDuration` is enormous here so
@@ -16,11 +16,11 @@ import { expectAgentDrivable } from "../testing/index.ts"
16
16
  import meta, * as stories from "./CopyButton.stories.tsx"
17
17
  import { copyText } from "./copyText.ts"
18
18
 
19
- const { Confirmed, Default, Refused } =
19
+ const { Confirmed, Playground, Refused } =
20
20
  composeStories(stories)
21
21
 
22
22
  test("is a button an agent can find by role and name", async () => {
23
- const { canvas } = await mountStory(Default)
23
+ const { canvas } = await mountStory(Playground)
24
24
 
25
25
  const button = expectAgentDrivable(canvas, {
26
26
  name: "Copy",
@@ -170,7 +170,7 @@ test("a prevented click copies nothing", async () => {
170
170
  })
171
171
 
172
172
  test("has no axe violations", async () => {
173
- const { canvasElement } = await mountStory(Default)
173
+ const { canvasElement } = await mountStory(Playground)
174
174
 
175
175
  await expectNoAxeViolations(canvasElement)
176
176
  })
@@ -10,7 +10,7 @@ import * as DataTableStories from "./DataTable.stories";
10
10
  optional selection column, and the sort *announcement* — the data itself stays the
11
11
  consumer's.
12
12
 
13
- <Canvas of={DataTableStories.Default} />
13
+ <Canvas of={DataTableStories.Playground} />
14
14
 
15
15
  ## Props
16
16
 
@@ -1,3 +1,4 @@
1
+ import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
1
2
  import type { Meta, StoryObj } from "@storybook/react"
2
3
  import type { ReactNode } from "react"
3
4
  import { useState } from "react"
@@ -266,7 +267,8 @@ type Story = StoryObj<typeof meta>
266
267
  * of the split: the component says *which* column is sorted, the
267
268
  * consumer says what that means for a priority enum.
268
269
  */
269
- export const Default: Story = {
270
+ export const Playground: Story = {
271
+ parameters: playgroundParameters,
270
272
  render: () => (
271
273
  <TaskTable
272
274
  initialSort={{
@@ -11,7 +11,7 @@ import {
11
11
  } from "../viewport.testHelpers.ts"
12
12
  import * as stories from "./DataTable.stories.tsx"
13
13
 
14
- const { AllVariants, Default, Interactive, Responsive } =
14
+ const { AllVariants, Playground, Interactive, Responsive } =
15
15
  composeStories(stories)
16
16
 
17
17
  /**
@@ -23,7 +23,7 @@ const { AllVariants, Default, Interactive, Responsive } =
23
23
  */
24
24
  test("the table is drivable by role and name", async () => {
25
25
  const { canvas, canvasElement } =
26
- await mountStory(Default)
26
+ await mountStory(Playground)
27
27
 
28
28
  const table = expectAgentDrivable(canvas, {
29
29
  name: "Tasks",
@@ -43,7 +43,7 @@ test("the table is drivable by role and name", async () => {
43
43
  * cannot be sorted", where `"none"` would say the opposite.
44
44
  */
45
45
  test("sortable columns announce, unsortable ones stay silent", async () => {
46
- const { canvas } = await mountStory(Default)
46
+ const { canvas } = await mountStory(Playground)
47
47
 
48
48
  const updated = expectAgentDrivable(canvas, {
49
49
  name: "Updated",
@@ -11,7 +11,7 @@ it. The consumer that shaped it is **Docket** — due dates, scheduled dates, ph
11
11
  ranges, and a staleness threshold counted in days
12
12
  ([decision](../../../docs/decisions/2026-08-19-a-date-is-a-calendar-date-not-an-instant.md)).
13
13
 
14
- <Canvas of={DatePickerStories.Default} />
14
+ <Canvas of={DatePickerStories.Playground} />
15
15
 
16
16
  ## Props
17
17
 
@@ -1,3 +1,4 @@
1
+ import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters"
1
2
  import type { Meta, StoryObj } from "@storybook/react"
2
3
  import type { ReactNode } from "react"
3
4
  import { useState } from "react"
@@ -99,7 +100,8 @@ const DatePickerHarness = ({
99
100
  * `19 aug`, `2026-08-19` and a bare `19` all resolve, and the line
100
101
  * underneath shows what they resolved to before anything commits.
101
102
  */
102
- export const Default: Story = {
103
+ export const Playground: Story = {
104
+ parameters: playgroundParameters,
103
105
  args: { label: "Due date" },
104
106
  render: () => <DatePickerHarness label="Due date" />,
105
107
  }