@charcuterie/ui 1.0.1 → 2.0.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 (134) hide show
  1. package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.d.ts +35 -0
  2. package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.d.ts.map +1 -0
  3. package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.js +33 -0
  4. package/dist/ColorSchemeSwitcher/ColorSchemeSwitcher.js.map +1 -0
  5. package/dist/ColorSchemeToggle/ColorSchemeToggle.d.ts +54 -0
  6. package/dist/ColorSchemeToggle/ColorSchemeToggle.d.ts.map +1 -0
  7. package/dist/ColorSchemeToggle/ColorSchemeToggle.js +30 -0
  8. package/dist/ColorSchemeToggle/ColorSchemeToggle.js.map +1 -0
  9. package/dist/Combobox/Combobox.d.ts +54 -0
  10. package/dist/Combobox/Combobox.d.ts.map +1 -0
  11. package/dist/Combobox/Combobox.js +216 -0
  12. package/dist/Combobox/Combobox.js.map +1 -0
  13. package/dist/Combobox/ComboboxOption.d.ts +36 -0
  14. package/dist/Combobox/ComboboxOption.d.ts.map +1 -0
  15. package/dist/Combobox/ComboboxOption.js +32 -0
  16. package/dist/Combobox/ComboboxOption.js.map +1 -0
  17. package/dist/Dialog/Dialog.d.ts +47 -0
  18. package/dist/Dialog/Dialog.d.ts.map +1 -0
  19. package/dist/Dialog/Dialog.js +52 -0
  20. package/dist/Dialog/Dialog.js.map +1 -0
  21. package/dist/Lightbox/Lightbox.d.ts +57 -0
  22. package/dist/Lightbox/Lightbox.d.ts.map +1 -0
  23. package/dist/Lightbox/Lightbox.js +50 -0
  24. package/dist/Lightbox/Lightbox.js.map +1 -0
  25. package/dist/Listbox/Listbox.d.ts +56 -0
  26. package/dist/Listbox/Listbox.d.ts.map +1 -0
  27. package/dist/Listbox/Listbox.js +151 -0
  28. package/dist/Listbox/Listbox.js.map +1 -0
  29. package/dist/Listbox/ListboxOption.d.ts +36 -0
  30. package/dist/Listbox/ListboxOption.d.ts.map +1 -0
  31. package/dist/Listbox/ListboxOption.js +42 -0
  32. package/dist/Listbox/ListboxOption.js.map +1 -0
  33. package/dist/Menu/Menu.d.ts.map +1 -1
  34. package/dist/Menu/Menu.js +47 -97
  35. package/dist/Menu/Menu.js.map +1 -1
  36. package/dist/Modal/Modal.d.ts +11 -59
  37. package/dist/Modal/Modal.d.ts.map +1 -1
  38. package/dist/Modal/Modal.js +28 -101
  39. package/dist/Modal/Modal.js.map +1 -1
  40. package/dist/Overlay/OverlayPanel.d.ts +50 -0
  41. package/dist/Overlay/OverlayPanel.d.ts.map +1 -0
  42. package/dist/Overlay/OverlayPanel.js +72 -0
  43. package/dist/Overlay/OverlayPanel.js.map +1 -0
  44. package/dist/Overlay/OverlayStack.d.ts +47 -0
  45. package/dist/Overlay/OverlayStack.d.ts.map +1 -0
  46. package/dist/Overlay/OverlayStack.js +46 -0
  47. package/dist/Overlay/OverlayStack.js.map +1 -0
  48. package/dist/Overlay/SharedBackdrop.d.ts +24 -0
  49. package/dist/Overlay/SharedBackdrop.d.ts.map +1 -0
  50. package/dist/Overlay/SharedBackdrop.js +28 -0
  51. package/dist/Overlay/SharedBackdrop.js.map +1 -0
  52. package/dist/Overlay/lockScrollBehind.d.ts.map +1 -0
  53. package/dist/Overlay/lockScrollBehind.js.map +1 -0
  54. package/dist/Overlay/overlayPanelClass.d.ts +17 -0
  55. package/dist/Overlay/overlayPanelClass.d.ts.map +1 -0
  56. package/dist/Overlay/overlayPanelClass.js +17 -0
  57. package/dist/Overlay/overlayPanelClass.js.map +1 -0
  58. package/dist/Overlay/useAnchoredOverlay.d.ts +97 -0
  59. package/dist/Overlay/useAnchoredOverlay.d.ts.map +1 -0
  60. package/dist/Overlay/useAnchoredOverlay.js +81 -0
  61. package/dist/Overlay/useAnchoredOverlay.js.map +1 -0
  62. package/dist/Popover/Popover.d.ts +16 -24
  63. package/dist/Popover/Popover.d.ts.map +1 -1
  64. package/dist/Popover/Popover.js +32 -68
  65. package/dist/Popover/Popover.js.map +1 -1
  66. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  67. package/dist/Tooltip/Tooltip.js +25 -25
  68. package/dist/Tooltip/Tooltip.js.map +1 -1
  69. package/dist/index.d.ts +13 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +6 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/mountStory.testHelpers.d.ts +1 -0
  74. package/dist/mountStory.testHelpers.d.ts.map +1 -1
  75. package/dist/mountStory.testHelpers.js +21 -1
  76. package/dist/mountStory.testHelpers.js.map +1 -1
  77. package/package.json +5 -4
  78. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.mdx +70 -0
  79. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.stories.tsx +118 -0
  80. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.test.tsx +53 -0
  81. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.tsx +90 -0
  82. package/src/ColorSchemeToggle/ColorSchemeToggle.mdx +48 -0
  83. package/src/ColorSchemeToggle/ColorSchemeToggle.stories.tsx +161 -0
  84. package/src/ColorSchemeToggle/ColorSchemeToggle.test.tsx +80 -0
  85. package/src/ColorSchemeToggle/ColorSchemeToggle.tsx +107 -0
  86. package/src/Combobox/Combobox.mdx +71 -0
  87. package/src/Combobox/Combobox.stories.tsx +245 -0
  88. package/src/Combobox/Combobox.test.tsx +289 -0
  89. package/src/Combobox/Combobox.tsx +513 -0
  90. package/src/Combobox/ComboboxOption.tsx +88 -0
  91. package/src/Dialog/Dialog.mdx +87 -0
  92. package/src/Dialog/Dialog.stories.tsx +347 -0
  93. package/src/Dialog/Dialog.test.tsx +312 -0
  94. package/src/Dialog/Dialog.tsx +127 -0
  95. package/src/Field/Field.test.tsx +6 -4
  96. package/src/Lightbox/Lightbox.mdx +63 -0
  97. package/src/Lightbox/Lightbox.stories.tsx +133 -0
  98. package/src/Lightbox/Lightbox.test.tsx +124 -0
  99. package/src/Lightbox/Lightbox.tsx +143 -0
  100. package/src/Listbox/Listbox.mdx +68 -0
  101. package/src/Listbox/Listbox.stories.tsx +205 -0
  102. package/src/Listbox/Listbox.test.tsx +198 -0
  103. package/src/Listbox/Listbox.tsx +295 -0
  104. package/src/Listbox/ListboxOption.tsx +104 -0
  105. package/src/Menu/Menu.mdx +11 -13
  106. package/src/Menu/Menu.test.tsx +47 -38
  107. package/src/Menu/Menu.tsx +81 -142
  108. package/src/Modal/Modal.mdx +29 -82
  109. package/src/Modal/Modal.stories.tsx +55 -188
  110. package/src/Modal/Modal.test.tsx +44 -177
  111. package/src/Modal/Modal.tsx +54 -204
  112. package/src/Overlay/OverlayPanel.tsx +195 -0
  113. package/src/Overlay/OverlayStack.test.tsx +235 -0
  114. package/src/Overlay/OverlayStack.tsx +132 -0
  115. package/src/Overlay/SharedBackdrop.tsx +40 -0
  116. package/src/Overlay/overlayPanelClass.ts +17 -0
  117. package/src/Overlay/useAnchoredOverlay.ts +197 -0
  118. package/src/Popover/Popover.mdx +17 -20
  119. package/src/Popover/Popover.stories.tsx +33 -0
  120. package/src/Popover/Popover.test.tsx +68 -32
  121. package/src/Popover/Popover.tsx +56 -95
  122. package/src/Tooltip/Tooltip.mdx +12 -9
  123. package/src/Tooltip/Tooltip.test.tsx +17 -19
  124. package/src/Tooltip/Tooltip.tsx +35 -32
  125. package/src/icons.storyHelpers.tsx +22 -0
  126. package/src/index.ts +22 -4
  127. package/src/mountStory.testHelpers.ts +25 -1
  128. package/src/sourceRules.test.ts +26 -1
  129. package/src/storyControls.test.ts +25 -12
  130. package/dist/Modal/lockScrollBehind.d.ts.map +0 -1
  131. package/dist/Modal/lockScrollBehind.js.map +0 -1
  132. /package/dist/{Modal → Overlay}/lockScrollBehind.d.ts +0 -0
  133. /package/dist/{Modal → Overlay}/lockScrollBehind.js +0 -0
  134. /package/src/{Modal → Overlay}/lockScrollBehind.ts +0 -0
@@ -6,10 +6,11 @@ import * as ModalStories from "./Modal.stories";
6
6
 
7
7
  # Modal
8
8
 
9
- Four repos, and **plex-channels and ripdeck arrived at native `<dialog>` independently**
10
- before this library existed. That is the design, not a shortcut: `showModal()` gives a
11
- focus trap, Escape, `::backdrop`, `inert` on everything behind it, and the top layer —
12
- from the platform, correctly, in every browser the fleet runs on.
9
+ The **base layer**: a backdrop, a focus trap, Escape, and an outside press — and no
10
+ chrome. A dialog is one kind of modal; so is a confirmation, a media lightbox, and
11
+ anything else that wants a scrim over the page. `Modal` is that foundation, and
12
+ [`Dialog`](?path=/docs/components-dialog--docs) is `Modal` plus the
13
+ heading/Close/footer/size chrome the component shipped with before the M8 rebuild.
13
14
 
14
15
  <Canvas of={ModalStories.Default} />
15
16
 
@@ -17,97 +18,43 @@ from the platform, correctly, in every browser the fleet runs on.
17
18
 
18
19
  <Controls />
19
20
 
20
- ## The one thing a component has to add
21
+ ## Portalled, not the top layer
21
22
 
22
- A native `<dialog>` is a **self-owning** control: call `showModal()` and it decides it is
23
- open. That is the same conflict Radix has, and it gets the same answer as everywhere else
24
- here — `isVisible` is the truth, and an effect makes the element agree with it. Nothing
25
- reads `dialogElement.open` as state; it is read only to avoid calling a method that would
26
- throw.
23
+ `Modal` renders an `OverlayPanel`, which portals to `document.body`. The old `Modal` rode
24
+ the platform top layer (`<dialog>.showModal()`); this one supersedes that
25
+ ([decision](../../../docs/decisions/2026-08-03-overlays-portal-to-the-body-not-the-top-layer.md)),
26
+ because the top layer cannot escape an `overflow: hidden` ancestor's clipping in every
27
+ case the fleet hit, and a portal does. The objection a portal used to raise — that it
28
+ moves the node out of a scoped query — is answered by `useRole` writing `aria-controls` /
29
+ `aria-labelledby` across the boundary, and by stories scoping panel queries to
30
+ `within(document.body)`.
27
31
 
28
32
  ```tsx
29
33
  const { hide, isVisible, show } = useVisibility()
30
34
 
31
- <Button onClick={show}>Stop the rip</Button>
35
+ <Button onClick={show}>Open</Button>
32
36
 
33
- <Modal heading="Stop the rip?" isVisible={isVisible} onClose={hide}>
34
- …
37
+ <Modal aria-label="Read error on title 4" isVisible={isVisible} onClose={hide}>
38
+ …your own markup…
35
39
  </Modal>
36
40
  ```
37
41
 
38
- `onClose` is every route out — the close button, Escape, a backdrop press — so there is
39
- one place to wire and no way to end up with the element shut and the state still open.
42
+ `onClose` is every route out — Escape, a backdrop press — so there is one place to wire
43
+ and no way to end up with the element shut and the state still open.
40
44
 
41
- ## No portal, and that is load-bearing
45
+ ## One accessible name, required
42
46
 
43
- The top layer is a **paint** concept. A `<dialog>` renders above everything while staying
44
- exactly where it is in the DOM, so it needs no portal — and that matters well past
45
- tidiness:
47
+ A `role="dialog"` takes no name from its content, so pass exactly one of `aria-label` or
48
+ `aria-labelledby`. Missing both is a dev-time warning, not a throw.
46
49
 
47
- > A portal moves the node to `document.body`, **out of whatever element a test or an agent
48
- > scoped its queries to.** `canvas.getByRole("dialog")` finds this modal. It would not find
49
- > a portalled one, and every story here would have to reach for `screen` and give up its
50
- > scoping.
50
+ <Canvas of={ModalStories.Alert} />
51
51
 
52
- The same reasoning is why `Popover` uses `popover="manual"` rather than a portal.
52
+ ## Stacking is zero-config, until it is not
53
53
 
54
- ## `open:flex`, not `flex`
55
-
56
- A closed `<dialog>` is hidden by a UA rule, and any `display: flex` of ours beats it — so
57
- a modal laid out with a plain `flex` renders permanently, in the page flow, with no error
58
- anywhere. The layout is therefore behind Tailwind's `open:` variant, keyed on the `[open]`
59
- attribute the browser manages.
60
-
61
- <Canvas of={ModalStories.AllVariants} />
62
-
63
- ## What a story cannot press
64
-
65
- `play`'s `userEvent` is `@testing-library/user-event`, which dispatches **untrusted**
66
- events, and a browser runs a default action only for trusted input. So a synthetic Escape
67
- does not make a native `<dialog>` fire `cancel` — it does nothing at all, and a story
68
- asserting "Escape did not close the non-dismissable dialog" would pass for the wrong
69
- reason forever.
70
-
71
- The stories dispatch the close request where the browser would raise it instead, which
72
- splits the contract honestly: keystroke → `cancel` is the platform's and is not ours to
73
- test; everything downstream of `cancel` is ours and is asserted. `AllStates` pairs it with
74
- a differential — the dialog that refuses the close request is then closed by its own
75
- button — because "still open" is also true of a dialog nothing can close at all.
76
-
77
- <Canvas of={ModalStories.AllStates} />
78
-
79
- ## `isDismissable={false}` cancels, it does not ignore
80
-
81
- Escape is `preventDefault`ed rather than dropped. Letting the browser close a dialog the
82
- app said was not dismissable would leave `isVisible` true and the element shut — the exact
83
- desync this component exists to prevent.
84
-
85
- ## Scroll behind it is locked, and the count is why
86
-
87
- `showModal()` makes the page `inert` and then lets you scroll it anyway. The lock is a
88
- counted module-level one rather than a hook, because nested dialogs are legal and a naive
89
- lock/unlock pair has the inner dialog's cleanup unlock the page while the outer is still
90
- open. The release is idempotent, because StrictMode runs an effect's cleanup twice.
91
-
92
- ## `dvh`, and the scrim
93
-
94
- Every size clamps at `85dvh`; `full` takes the lot. `dvh` rather than `vh` because on a
95
- phone the two differ by the address bar, and a dialog whose footer is under it is a dialog
96
- with no buttons.
97
-
98
- `::backdrop` reads `--color-scrim`, which is a **variant** token: `legible` dims at 0.84
99
- and `daylight` at 0.66, because how hard a direction separates a dialog from the page is
100
- exactly the sort of thing a visual direction decides. ePaper's is `transparent` — a panel
101
- with no opacity cannot dim, so it says so instead of faking it. The `Responsive` story
102
- asserts the computed `::backdrop` colour is really there, since a `var()` that failed to
103
- inherit would just look like a design choice.
104
-
105
- <Canvas of={ModalStories.Responsive} />
106
-
107
- ## A modal is not a container-query citizen
108
-
109
- It gets no `ContainerBoard`. Its size is measured against the viewport by definition — it
110
- is the thing covering the viewport — so boarding it at three container widths would be
111
- measuring something with no bearing on it.
54
+ A lone `Modal` renders its own scrim with no setup. Only when modals **stack** — one
55
+ opened from inside another — does the app need an `OverlayStackProvider` at its root, so
56
+ the N of them share **one** scrim, keep the top live, mark the rest `inert`, and dismiss
57
+ top-first. The scroll lock is ref-counted through the stack, so the page frees only when
58
+ the last modal closes.
112
59
 
113
60
  <Canvas of={ModalStories.Interactive} />
@@ -2,29 +2,16 @@ import { useVisibility } from "@charcuterie/logic"
2
2
  import type { Meta, StoryObj } from "@storybook/react"
3
3
  import type { ReactNode } from "react"
4
4
 
5
- import { toStoryChoice } from "../argTypes.storyHelpers.ts"
6
5
  import { Button } from "../Button/Button.tsx"
7
- import { StoryRow } from "../board.storyHelpers.tsx"
8
- import type { ModalSize } from "./Modal.tsx"
9
6
  import { Modal } from "./Modal.tsx"
10
7
 
11
8
  const meta = {
12
9
  title: "Components/Modal",
13
10
  component: Modal,
14
11
  parameters: { layout: "padded" },
15
- argTypes: {
16
- size: toStoryChoice([
17
- "sm",
18
- "md",
19
- "lg",
20
- "xl",
21
- "full",
22
- ] as const),
23
- },
24
12
  args: {
25
- headingLevel: 2,
26
13
  isDismissable: true,
27
- size: "md",
14
+ role: "dialog",
28
15
  },
29
16
  } satisfies Meta<typeof Modal>
30
17
 
@@ -33,32 +20,21 @@ export default meta
33
20
  type Story = StoryObj<typeof meta>
34
21
 
35
22
  /**
36
- * Every board here is triggers-plus-one-open-dialog rather than a
37
- * grid, and that is the component rather than the stories being
38
- * awkward: `showModal()` puts a dialog in the **top layer**, so two
39
- * of them side by side is not a thing the platform can do. What the
40
- * boards show instead is the real arrangement — a page, a scrim,
41
- * and one dialog over both.
42
- *
43
- * `footer` is a function of `hide` because a dialog's own buttons
44
- * are the normal way out of it, and they need the same `hide` the
45
- * close button gets. One `useVisibility` per demo, no second source
46
- * of truth.
23
+ * The base layer has no chrome, so a demo supplies its own — the
24
+ * point being that `Modal` is the backdrop-plus-dismiss-plus-portal
25
+ * foundation and the content is entirely the caller's. `Dialog` is
26
+ * the batteries-included version of exactly this.
47
27
  */
48
28
  const ModalDemo = ({
49
29
  buttonLabel,
50
30
  children,
51
- footer,
52
- heading,
53
- isDismissable = true,
54
- size,
31
+ label,
32
+ role,
55
33
  }: {
56
34
  buttonLabel: string
57
35
  children?: ReactNode
58
- footer?: (hide: () => void) => ReactNode
59
- heading: string
60
- isDismissable?: boolean
61
- size?: ModalSize
36
+ label: string
37
+ role?: "alertdialog" | "dialog"
62
38
  }): ReactNode => {
63
39
  const { hide, isVisible, show } = useVisibility()
64
40
 
@@ -69,21 +45,29 @@ const ModalDemo = ({
69
45
  </Button>
70
46
 
71
47
  <Modal
72
- footer={footer?.(hide)}
73
- heading={heading}
74
- isDismissable={isDismissable}
48
+ aria-label={label}
75
49
  isVisible={isVisible}
76
50
  onClose={hide}
77
- size={size}
51
+ role={role}
78
52
  >
79
- {children ?? (
80
- <p className="text-content-secondary text-sm">
81
- MakeMKV reported a read error on title 4. The
82
- rip can continue with the remaining eight
83
- titles, or stop here and keep what has
84
- completed.
85
- </p>
86
- )}
53
+ <div className="flex flex-col gap-3 p-4">
54
+ {children ?? (
55
+ <p className="text-content-secondary text-sm">
56
+ A bare `Modal`: a scrim, a focus trap, Escape,
57
+ and an outside press. Everything you see
58
+ inside is the caller's own markup.
59
+ </p>
60
+ )}
61
+
62
+ <Button
63
+ appearance="soft"
64
+ intent="neutral"
65
+ onClick={hide}
66
+ size="sm"
67
+ >
68
+ Close
69
+ </Button>
70
+ </div>
87
71
  </Modal>
88
72
  </>
89
73
  )
@@ -91,182 +75,65 @@ const ModalDemo = ({
91
75
 
92
76
  export const Default: Story = {
93
77
  args: {
94
- heading: "Bay 3",
78
+ "aria-label": "Read error on title 4",
79
+ children: null,
95
80
  isVisible: false,
96
81
  onClose: () => {},
97
82
  },
98
83
  render: () => (
99
84
  <ModalDemo
100
- buttonLabel="Show the read error"
101
- heading="Read error on title 4"
85
+ buttonLabel="Open the base modal"
86
+ label="Read error on title 4"
102
87
  />
103
88
  ),
104
89
  }
105
90
 
106
- const SIZES: ModalSize[] = ["sm", "md", "lg", "xl", "full"]
107
-
108
- export const AllVariants: Story = {
109
- args: {
110
- heading: "Bay 3",
111
- isVisible: false,
112
- onClose: () => {},
113
- },
114
- render: () => (
115
- <StoryRow>
116
- {SIZES.map((size) => (
117
- <ModalDemo
118
- buttonLabel={`Open ${size}`}
119
- heading={`Read error — ${size}`}
120
- key={size}
121
- size={size}
122
- />
123
- ))}
124
- </StoryRow>
125
- ),
126
- }
127
-
128
- export const AllStates: Story = {
129
- args: {
130
- heading: "Bay 3",
131
- isVisible: false,
132
- onClose: () => {},
133
- },
134
- render: () => (
135
- <StoryRow>
136
- <ModalDemo
137
- buttonLabel="With a footer"
138
- footer={(hide) => (
139
- <>
140
- <Button
141
- appearance="soft"
142
- intent="neutral"
143
- onClick={hide}
144
- size="sm"
145
- >
146
- Keep going
147
- </Button>
148
-
149
- <Button intent="danger" size="sm">
150
- Stop the rip
151
- </Button>
152
- </>
153
- )}
154
- heading="Stop the rip?"
155
- />
156
-
157
- <ModalDemo
158
- buttonLabel="Not dismissable"
159
- footer={(hide) => (
160
- <>
161
- <Button
162
- appearance="soft"
163
- intent="neutral"
164
- onClick={hide}
165
- size="sm"
166
- >
167
- Keep the titles
168
- </Button>
169
-
170
- <Button intent="danger" size="sm">
171
- Erase and re-rip
172
- </Button>
173
- </>
174
- )}
175
- heading="This erases eight completed titles"
176
- isDismissable={false}
177
- />
178
-
179
- <ModalDemo
180
- buttonLabel="Long body"
181
- heading="MakeMKV log"
182
- >
183
- <div className="flex flex-col gap-2">
184
- {Array.from(
185
- { length: 24 },
186
- (_, index) =>
187
- `14:0${index % 10}:22 title ${
188
- index + 1
189
- } AACS handshake complete, reading…`,
190
- ).map((line) => (
191
- <p
192
- className="font-mono text-content-secondary text-xs"
193
- key={line}
194
- >
195
- {line}
196
- </p>
197
- ))}
198
- </div>
199
- </ModalDemo>
200
- </StoryRow>
201
- ),
202
- }
203
-
204
91
  /**
205
- * A modal is the one surface in this library that is **not** a
206
- * container-query citizen, so it gets no `ContainerBoard`. Its size
207
- * is measured against the viewport by definition — it is the thing
208
- * covering the viewport — and boarding it at three container widths
209
- * would be measuring something that has no bearing on it.
210
- *
211
- * What does vary is the clamp: every size caps at `85dvh`, and
212
- * `full` takes the lot. `dvh` rather than `vh` because on a phone
213
- * the two differ by the address bar, and a dialog whose footer is
214
- * underneath it is a dialog with no buttons.
92
+ * `role="alertdialog"` is the one configuration the base exposes that
93
+ * `Dialog` does not surface directly — an interruption the user has
94
+ * to answer, announced as an alert rather than a plain dialog.
215
95
  */
216
- export const Responsive: Story = {
96
+ export const Alert: Story = {
217
97
  args: {
218
- heading: "Bay 3",
98
+ "aria-label": "Confirm erase",
99
+ children: null,
219
100
  isVisible: false,
220
101
  onClose: () => {},
221
102
  },
222
103
  render: () => (
223
104
  <ModalDemo
224
- buttonLabel="Open full-bleed"
225
- heading="MakeMKV log — full"
226
- size="full"
105
+ buttonLabel="Open an alertdialog"
106
+ label="Confirm erase"
107
+ role="alertdialog"
227
108
  >
228
109
  <p className="text-content-secondary text-sm">
229
- `full` is the kiosk and phone case: the dialog is
230
- the screen, so there is nothing behind it worth
231
- showing.
110
+ This erases eight completed titles.
232
111
  </p>
233
112
  </ModalDemo>
234
113
  ),
235
114
  }
236
115
 
237
116
  /**
238
- * The keyboard contract, driven. `showModal()` supplies the focus
239
- * trap and the focus restore; what this asserts is that the state
240
- * layer never disagrees with the element — Escape routes through
241
- * `onCancel` → `hide()` → the effect → `close()`, rather than the
242
- * browser closing the dialog behind the app's back.
117
+ * The keyboard contract, driven. `OverlayPanel`'s
118
+ * `FloatingFocusManager` traps focus and restores it to the trigger;
119
+ * `useDismiss` routes Escape and an outside press through `onClose`.
243
120
  */
244
121
  export const Interactive: Story = {
245
122
  args: {
246
- heading: "Bay 3",
123
+ "aria-label": "Stop the rip?",
124
+ children: null,
247
125
  isVisible: false,
248
126
  onClose: () => {},
249
127
  },
250
128
  render: () => (
251
129
  <ModalDemo
252
130
  buttonLabel="Stop the rip"
253
- footer={(hide) => (
254
- <>
255
- <Button
256
- appearance="soft"
257
- intent="neutral"
258
- onClick={hide}
259
- size="sm"
260
- >
261
- Keep going
262
- </Button>
263
-
264
- <Button intent="danger" size="sm">
265
- Stop it now
266
- </Button>
267
- </>
268
- )}
269
- heading="Stop the rip?"
270
- />
131
+ label="Stop the rip?"
132
+ >
133
+ <p className="text-content-secondary text-sm">
134
+ Escape, an outside press, or the button below all
135
+ land on the same `onClose`.
136
+ </p>
137
+ </ModalDemo>
271
138
  ),
272
139
  }