@charcuterie/ui 1.1.0 → 2.0.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 (116) hide show
  1. package/dist/Combobox/Combobox.d.ts +54 -0
  2. package/dist/Combobox/Combobox.d.ts.map +1 -0
  3. package/dist/Combobox/Combobox.js +216 -0
  4. package/dist/Combobox/Combobox.js.map +1 -0
  5. package/dist/Combobox/ComboboxOption.d.ts +36 -0
  6. package/dist/Combobox/ComboboxOption.d.ts.map +1 -0
  7. package/dist/Combobox/ComboboxOption.js +32 -0
  8. package/dist/Combobox/ComboboxOption.js.map +1 -0
  9. package/dist/Dialog/Dialog.d.ts +53 -0
  10. package/dist/Dialog/Dialog.d.ts.map +1 -0
  11. package/dist/Dialog/Dialog.js +52 -0
  12. package/dist/Dialog/Dialog.js.map +1 -0
  13. package/dist/Lightbox/Lightbox.d.ts +7 -7
  14. package/dist/Lightbox/Lightbox.js +9 -9
  15. package/dist/Lightbox/Lightbox.js.map +1 -1
  16. package/dist/Listbox/Listbox.d.ts +56 -0
  17. package/dist/Listbox/Listbox.d.ts.map +1 -0
  18. package/dist/Listbox/Listbox.js +151 -0
  19. package/dist/Listbox/Listbox.js.map +1 -0
  20. package/dist/Listbox/ListboxOption.d.ts +36 -0
  21. package/dist/Listbox/ListboxOption.d.ts.map +1 -0
  22. package/dist/Listbox/ListboxOption.js +42 -0
  23. package/dist/Listbox/ListboxOption.js.map +1 -0
  24. package/dist/Menu/Menu.d.ts.map +1 -1
  25. package/dist/Menu/Menu.js +47 -97
  26. package/dist/Menu/Menu.js.map +1 -1
  27. package/dist/Modal/Modal.d.ts +11 -59
  28. package/dist/Modal/Modal.d.ts.map +1 -1
  29. package/dist/Modal/Modal.js +28 -101
  30. package/dist/Modal/Modal.js.map +1 -1
  31. package/dist/Overlay/OverlayPanel.d.ts +50 -0
  32. package/dist/Overlay/OverlayPanel.d.ts.map +1 -0
  33. package/dist/Overlay/OverlayPanel.js +72 -0
  34. package/dist/Overlay/OverlayPanel.js.map +1 -0
  35. package/dist/Overlay/OverlayStack.d.ts +47 -0
  36. package/dist/Overlay/OverlayStack.d.ts.map +1 -0
  37. package/dist/Overlay/OverlayStack.js +46 -0
  38. package/dist/Overlay/OverlayStack.js.map +1 -0
  39. package/dist/Overlay/SharedBackdrop.d.ts +24 -0
  40. package/dist/Overlay/SharedBackdrop.d.ts.map +1 -0
  41. package/dist/Overlay/SharedBackdrop.js +28 -0
  42. package/dist/Overlay/SharedBackdrop.js.map +1 -0
  43. package/dist/Overlay/lockScrollBehind.d.ts.map +1 -0
  44. package/dist/Overlay/lockScrollBehind.js.map +1 -0
  45. package/dist/Overlay/overlayPanelClass.d.ts +17 -0
  46. package/dist/Overlay/overlayPanelClass.d.ts.map +1 -0
  47. package/dist/Overlay/overlayPanelClass.js +17 -0
  48. package/dist/Overlay/overlayPanelClass.js.map +1 -0
  49. package/dist/Overlay/useAnchoredOverlay.d.ts +97 -0
  50. package/dist/Overlay/useAnchoredOverlay.d.ts.map +1 -0
  51. package/dist/Overlay/useAnchoredOverlay.js +81 -0
  52. package/dist/Overlay/useAnchoredOverlay.js.map +1 -0
  53. package/dist/Popover/Popover.d.ts +16 -24
  54. package/dist/Popover/Popover.d.ts.map +1 -1
  55. package/dist/Popover/Popover.js +32 -68
  56. package/dist/Popover/Popover.js.map +1 -1
  57. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  58. package/dist/Tooltip/Tooltip.js +25 -25
  59. package/dist/Tooltip/Tooltip.js.map +1 -1
  60. package/dist/index.d.ts +7 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +3 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/mountStory.testHelpers.d.ts +1 -0
  65. package/dist/mountStory.testHelpers.d.ts.map +1 -1
  66. package/dist/mountStory.testHelpers.js +21 -1
  67. package/dist/mountStory.testHelpers.js.map +1 -1
  68. package/package.json +3 -2
  69. package/src/Combobox/Combobox.mdx +71 -0
  70. package/src/Combobox/Combobox.stories.tsx +245 -0
  71. package/src/Combobox/Combobox.test.tsx +289 -0
  72. package/src/Combobox/Combobox.tsx +513 -0
  73. package/src/Combobox/ComboboxOption.tsx +88 -0
  74. package/src/Dialog/Dialog.mdx +87 -0
  75. package/src/Dialog/Dialog.stories.tsx +397 -0
  76. package/src/Dialog/Dialog.test.tsx +340 -0
  77. package/src/Dialog/Dialog.tsx +133 -0
  78. package/src/Field/Field.test.tsx +6 -4
  79. package/src/Lightbox/Lightbox.mdx +9 -8
  80. package/src/Lightbox/Lightbox.stories.tsx +5 -5
  81. package/src/Lightbox/Lightbox.test.tsx +25 -36
  82. package/src/Lightbox/Lightbox.tsx +10 -10
  83. package/src/Listbox/Listbox.mdx +68 -0
  84. package/src/Listbox/Listbox.stories.tsx +205 -0
  85. package/src/Listbox/Listbox.test.tsx +198 -0
  86. package/src/Listbox/Listbox.tsx +295 -0
  87. package/src/Listbox/ListboxOption.tsx +104 -0
  88. package/src/Menu/Menu.mdx +11 -13
  89. package/src/Menu/Menu.test.tsx +47 -38
  90. package/src/Menu/Menu.tsx +81 -142
  91. package/src/Modal/Modal.mdx +29 -82
  92. package/src/Modal/Modal.stories.tsx +55 -188
  93. package/src/Modal/Modal.test.tsx +44 -177
  94. package/src/Modal/Modal.tsx +54 -204
  95. package/src/Overlay/OverlayPanel.tsx +195 -0
  96. package/src/Overlay/OverlayStack.test.tsx +235 -0
  97. package/src/Overlay/OverlayStack.tsx +132 -0
  98. package/src/Overlay/SharedBackdrop.tsx +40 -0
  99. package/src/Overlay/overlayPanelClass.ts +17 -0
  100. package/src/Overlay/useAnchoredOverlay.ts +197 -0
  101. package/src/Popover/Popover.mdx +17 -20
  102. package/src/Popover/Popover.stories.tsx +33 -0
  103. package/src/Popover/Popover.test.tsx +68 -32
  104. package/src/Popover/Popover.tsx +56 -95
  105. package/src/Tooltip/Tooltip.mdx +12 -9
  106. package/src/Tooltip/Tooltip.test.tsx +17 -19
  107. package/src/Tooltip/Tooltip.tsx +35 -32
  108. package/src/index.ts +13 -4
  109. package/src/mountStory.testHelpers.ts +25 -1
  110. package/src/sourceRules.test.ts +12 -3
  111. package/src/storyControls.test.ts +25 -12
  112. package/dist/Modal/lockScrollBehind.d.ts.map +0 -1
  113. package/dist/Modal/lockScrollBehind.js.map +0 -1
  114. /package/dist/{Modal → Overlay}/lockScrollBehind.d.ts +0 -0
  115. /package/dist/{Modal → Overlay}/lockScrollBehind.js +0 -0
  116. /package/src/{Modal → Overlay}/lockScrollBehind.ts +0 -0
@@ -0,0 +1,397 @@
1
+ import { useVisibility } from "@charcuterie/logic"
2
+ import type { Meta, StoryObj } from "@storybook/react"
3
+ import type { ReactNode } from "react"
4
+
5
+ import { toStoryChoice } from "../argTypes.storyHelpers.ts"
6
+ import { Button } from "../Button/Button.tsx"
7
+ import { StoryRow } from "../board.storyHelpers.tsx"
8
+ import { OverlayStackProvider } from "../Overlay/OverlayStack.tsx"
9
+ import type { DialogSize } from "./Dialog.tsx"
10
+ import { Dialog } from "./Dialog.tsx"
11
+
12
+ const meta = {
13
+ title: "Components/Dialog",
14
+ component: Dialog,
15
+ parameters: { layout: "padded" },
16
+ argTypes: {
17
+ size: toStoryChoice([
18
+ "sm",
19
+ "md",
20
+ "lg",
21
+ "xl",
22
+ "full",
23
+ ] as const),
24
+ },
25
+ args: {
26
+ // `render` supplies the real content; this satisfies the required
27
+ // `children` for the Controls panel's arg type.
28
+ children: null,
29
+ headingLevel: 2,
30
+ isDismissable: true,
31
+ size: "md",
32
+ },
33
+ } satisfies Meta<typeof Dialog>
34
+
35
+ export default meta
36
+
37
+ type Story = StoryObj<typeof meta>
38
+
39
+ /**
40
+ * Every board here is triggers-plus-one-open-dialog rather than a
41
+ * grid, and that is the component rather than the stories being
42
+ * awkward: a dialog covers the viewport, so two side by side is not
43
+ * the real arrangement. What the boards show instead is a page, a
44
+ * scrim, and one dialog over both.
45
+ *
46
+ * `footer` is a function of `hide` because a dialog's own buttons
47
+ * are the normal way out of it, and they need the same `hide` the
48
+ * close button gets. One `useVisibility` per demo, no second source
49
+ * of truth.
50
+ */
51
+ const DialogDemo = ({
52
+ buttonLabel,
53
+ children,
54
+ footer,
55
+ heading,
56
+ isDismissable = true,
57
+ size,
58
+ }: {
59
+ buttonLabel: string
60
+ children?: ReactNode
61
+ footer?: (hide: () => void) => ReactNode
62
+ heading: string
63
+ isDismissable?: boolean
64
+ size?: DialogSize
65
+ }): ReactNode => {
66
+ const { hide, isVisible, show } = useVisibility()
67
+
68
+ return (
69
+ <>
70
+ <Button appearance="soft" onClick={show} size="sm">
71
+ {buttonLabel}
72
+ </Button>
73
+
74
+ <Dialog
75
+ footer={footer?.(hide)}
76
+ heading={heading}
77
+ isDismissable={isDismissable}
78
+ isVisible={isVisible}
79
+ onClose={hide}
80
+ size={size}
81
+ >
82
+ {children ?? (
83
+ <p className="text-content-secondary text-sm">
84
+ MakeMKV reported a read error on title 4. The
85
+ rip can continue with the remaining eight
86
+ titles, or stop here and keep what has
87
+ completed.
88
+ </p>
89
+ )}
90
+ </Dialog>
91
+ </>
92
+ )
93
+ }
94
+
95
+ export const Default: Story = {
96
+ args: {
97
+ heading: "Bay 3",
98
+ isVisible: false,
99
+ onClose: () => {},
100
+ },
101
+ render: () => (
102
+ <DialogDemo
103
+ buttonLabel="Show the read error"
104
+ heading="Read error on title 4"
105
+ />
106
+ ),
107
+ }
108
+
109
+ const SIZES: DialogSize[] = ["sm", "md", "lg", "xl", "full"]
110
+
111
+ export const AllVariants: Story = {
112
+ args: {
113
+ heading: "Bay 3",
114
+ isVisible: false,
115
+ onClose: () => {},
116
+ },
117
+ render: () => (
118
+ <StoryRow>
119
+ {SIZES.map((size) => (
120
+ <DialogDemo
121
+ buttonLabel={`Open ${size}`}
122
+ heading={`Read error — ${size}`}
123
+ key={size}
124
+ size={size}
125
+ />
126
+ ))}
127
+ </StoryRow>
128
+ ),
129
+ }
130
+
131
+ export const AllStates: Story = {
132
+ args: {
133
+ heading: "Bay 3",
134
+ isVisible: false,
135
+ onClose: () => {},
136
+ },
137
+ render: () => (
138
+ <StoryRow>
139
+ <DialogDemo
140
+ buttonLabel="With a footer"
141
+ footer={(hide) => (
142
+ <>
143
+ <Button
144
+ appearance="soft"
145
+ intent="neutral"
146
+ onClick={hide}
147
+ size="sm"
148
+ >
149
+ Keep going
150
+ </Button>
151
+
152
+ <Button intent="danger" size="sm">
153
+ Stop the rip
154
+ </Button>
155
+ </>
156
+ )}
157
+ heading="Stop the rip?"
158
+ />
159
+
160
+ <DialogDemo
161
+ buttonLabel="Not dismissable"
162
+ footer={(hide) => (
163
+ <>
164
+ <Button
165
+ appearance="soft"
166
+ intent="neutral"
167
+ onClick={hide}
168
+ size="sm"
169
+ >
170
+ Keep the titles
171
+ </Button>
172
+
173
+ <Button intent="danger" size="sm">
174
+ Erase and re-rip
175
+ </Button>
176
+ </>
177
+ )}
178
+ heading="This erases eight completed titles"
179
+ isDismissable={false}
180
+ />
181
+
182
+ <DialogDemo
183
+ buttonLabel="Long body"
184
+ heading="MakeMKV log"
185
+ >
186
+ <div className="flex flex-col gap-2">
187
+ {Array.from(
188
+ { length: 24 },
189
+ (_, index) =>
190
+ `14:0${index % 10}:22 title ${
191
+ index + 1
192
+ } AACS handshake complete, reading…`,
193
+ ).map((line) => (
194
+ <p
195
+ className="font-mono text-content-secondary text-xs"
196
+ key={line}
197
+ >
198
+ {line}
199
+ </p>
200
+ ))}
201
+ </div>
202
+ </DialogDemo>
203
+ </StoryRow>
204
+ ),
205
+ }
206
+
207
+ /**
208
+ * Stacking — one dialog opened from inside another. An
209
+ * `OverlayStackProvider` at the app root is the one piece of setup a
210
+ * lone dialog does not need: it renders a **single** shared scrim
211
+ * behind both, keeps the second on top, marks the first `inert`, and
212
+ * closes them top-first on Escape or an outside press. Open the first,
213
+ * then open the second from its footer.
214
+ */
215
+ const StackedDemo = (): ReactNode => {
216
+ const first = useVisibility()
217
+
218
+ const second = useVisibility()
219
+
220
+ return (
221
+ <OverlayStackProvider>
222
+ <Button
223
+ appearance="soft"
224
+ onClick={first.show}
225
+ size="sm"
226
+ >
227
+ Open the first dialog
228
+ </Button>
229
+
230
+ <Dialog
231
+ footer={
232
+ <Button onClick={second.show} size="sm">
233
+ Open a second over it
234
+ </Button>
235
+ }
236
+ heading="First dialog"
237
+ isVisible={first.isVisible}
238
+ onClose={first.hide}
239
+ >
240
+ <p className="text-content-secondary text-sm">
241
+ This one stays open and goes `inert` while the
242
+ second is over it.
243
+ </p>
244
+ </Dialog>
245
+
246
+ <Dialog
247
+ footer={
248
+ <Button
249
+ appearance="soft"
250
+ intent="neutral"
251
+ onClick={second.hide}
252
+ size="sm"
253
+ >
254
+ Close this one
255
+ </Button>
256
+ }
257
+ heading="Second dialog"
258
+ isVisible={second.isVisible}
259
+ onClose={second.hide}
260
+ >
261
+ <p className="text-content-secondary text-sm">
262
+ One scrim behind both. Escape or a backdrop press
263
+ closes this one first.
264
+ </p>
265
+ </Dialog>
266
+ </OverlayStackProvider>
267
+ )
268
+ }
269
+
270
+ export const Stacked: Story = {
271
+ args: {
272
+ heading: "First dialog",
273
+ isVisible: false,
274
+ onClose: () => {},
275
+ },
276
+ render: () => <StackedDemo />,
277
+ }
278
+
279
+ /**
280
+ * A dialog is the one surface in this library that is **not** a
281
+ * container-query citizen, so it gets no `ContainerBoard`. Its size
282
+ * is measured against the viewport by definition — it is the thing
283
+ * covering the viewport — and boarding it at three container widths
284
+ * would be measuring something that has no bearing on it.
285
+ *
286
+ * What does vary is the clamp: every size caps at `85dvh`, and
287
+ * `full` takes the lot. `dvh` rather than `vh` because on a phone
288
+ * the two differ by the address bar, and a dialog whose footer is
289
+ * underneath it is a dialog with no buttons.
290
+ */
291
+ export const Responsive: Story = {
292
+ args: {
293
+ heading: "Bay 3",
294
+ isVisible: false,
295
+ onClose: () => {},
296
+ },
297
+ render: () => (
298
+ <DialogDemo
299
+ buttonLabel="Open full-bleed"
300
+ heading="MakeMKV log — full"
301
+ size="full"
302
+ >
303
+ <p className="text-content-secondary text-sm">
304
+ `full` is the kiosk and phone case: the dialog is
305
+ the screen, so there is nothing behind it worth
306
+ showing.
307
+ </p>
308
+ </DialogDemo>
309
+ ),
310
+ }
311
+
312
+ /**
313
+ * The keyboard contract, driven. `OverlayPanel`'s
314
+ * `FloatingFocusManager` supplies the focus trap and the focus
315
+ * restore, and `useDismiss` routes Escape through `onClose` — which,
316
+ * unlike the old native `<dialog>`, a synthetic `userEvent.keyboard`
317
+ * can now press for real.
318
+ */
319
+ /**
320
+ * A confirm dialog with **no body**: the question is the `heading`
321
+ * (which is the dialog's accessible name) and the answers are the
322
+ * `footer`. `children` is optional precisely for this shape — the old
323
+ * chrome `Modal` extended `<dialog>`'s DOM props, so it allowed it,
324
+ * and image-viewer's delete-confirm renders exactly this.
325
+ */
326
+ const NoBodyDemo = (): ReactNode => {
327
+ const { hide, isVisible, show } = useVisibility()
328
+
329
+ return (
330
+ <>
331
+ <Button appearance="soft" onClick={show} size="sm">
332
+ Delete the file
333
+ </Button>
334
+
335
+ <Dialog
336
+ footer={
337
+ <>
338
+ <Button
339
+ appearance="soft"
340
+ intent="neutral"
341
+ onClick={hide}
342
+ size="sm"
343
+ >
344
+ Keep it
345
+ </Button>
346
+
347
+ <Button intent="danger" size="sm">
348
+ Delete
349
+ </Button>
350
+ </>
351
+ }
352
+ heading="Delete this file? This cannot be undone."
353
+ isVisible={isVisible}
354
+ onClose={hide}
355
+ />
356
+ </>
357
+ )
358
+ }
359
+
360
+ export const NoBody: Story = {
361
+ args: {
362
+ heading: "Delete this file? This cannot be undone.",
363
+ isVisible: false,
364
+ onClose: () => {},
365
+ },
366
+ render: () => <NoBodyDemo />,
367
+ }
368
+
369
+ export const Interactive: Story = {
370
+ args: {
371
+ heading: "Bay 3",
372
+ isVisible: false,
373
+ onClose: () => {},
374
+ },
375
+ render: () => (
376
+ <DialogDemo
377
+ buttonLabel="Stop the rip"
378
+ footer={(hide) => (
379
+ <>
380
+ <Button
381
+ appearance="soft"
382
+ intent="neutral"
383
+ onClick={hide}
384
+ size="sm"
385
+ >
386
+ Keep going
387
+ </Button>
388
+
389
+ <Button intent="danger" size="sm">
390
+ Stop it now
391
+ </Button>
392
+ </>
393
+ )}
394
+ heading="Stop the rip?"
395
+ />
396
+ ),
397
+ }