@uniflowed/ui 0.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,1012 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/ui`: components you own, with the behaviour you would get wrong.
4
+ //
5
+ // The premise is the one shadcn established and it is the right one: a
6
+ // component library that ships styles is a library you fight, so these ship
7
+ // none. Every part takes `className` and every other DOM prop and passes it
8
+ // through; what they contribute is the part that is genuinely hard and
9
+ // genuinely invisible when it is missing.
10
+ //
11
+ // That part is behaviour, and specifically keyboard and screen-reader
12
+ // behaviour: a roving `tabindex` so a twelve-tab list does not take twelve Tab
13
+ // presses to get past, a focus trap that actually cannot be escaped, focus
14
+ // restored to whatever opened a dialog, typeahead in a menu, an
15
+ // `aria-activedescendant` that names an option still in the document. None of
16
+ // it is visible in a screenshot and all of it is what separates a component
17
+ // from a `div` that looks like one.
18
+ //
19
+ // Each primitive implements the WAI-ARIA authoring practices pattern for it —
20
+ // the roles, the `aria-*` wiring, the focus management and the whole keyboard
21
+ // map — and each module's header says which interaction it exists to get right
22
+ // and what a naive version breaks.
23
+ //
24
+ // # Composition is type-checked
25
+ //
26
+ // This is where Flow says something no other type system can. `Tabs.List`
27
+ // declares `renders* Tabs.Tab`, so a `<button>` in a tab list is a *type
28
+ // error* — not a review comment, not a runtime warning, not a screen reader
29
+ // announcing "button" where the reader expected "tab, 2 of 5". `Menu.Body` and
30
+ // `Combobox.List` state the same constraint about what may appear inside a
31
+ // menu and a listbox, which ARIA also requires and which nothing else checks.
32
+ // A library written in TypeScript can document those constraints; it cannot
33
+ // state them.
34
+ //
35
+ // # Styling is a default, not a dependency
36
+ //
37
+ // Nothing here imports StyleX, and nothing here has a StyleX-shaped type. A
38
+ // consumer styling with plain CSS, CSS Modules or anything else gets exactly
39
+ // the same components with exactly the same behaviour; the design-system layer
40
+ // that adds uf's default styles is built *on* these, not into them.
41
+ //
42
+ // # What these components promise React
43
+ //
44
+ // Nothing here mutates during a render, reads a ref during a render, or depends
45
+ // on a render happening exactly once — so React Compiler's memoization and
46
+ // ordinary `memo` are both safe, and none of it needs an escape hatch. The
47
+ // refs that exist (`triggerRef`, `pendingFocus`, the typeahead buffer) are
48
+ // written only from event handlers and effects, and nothing renders them.
49
+ //
50
+ // Where a component has to learn something the DOM knows — how many options a
51
+ // caller filtered down to, which item the arrow key should move to — it reads
52
+ // the document in an effect or an event handler and, if a render depends on
53
+ // the answer, puts it in state. That is deliberately *not*
54
+ // `useSyncExternalStore`: the DOM is not a store whose value a render may
55
+ // read, and reading layout during a render is the thing that API exists to
56
+ // prevent.
57
+ //
58
+ // One component does use it, and it is the case the API is actually for.
59
+ // `toast("Saved")` is called from an event handler or a `catch`, so the queue
60
+ // of notifications lives outside React — a store at module scope in
61
+ // `toast.js`, read through `useSyncExternalStore` with the cached immutable
62
+ // snapshots and the consistent server snapshot that requires. A queue is a
63
+ // store; the DOM is not.
64
+ //
65
+ // That store should be an atom in `@uniflowed/state`, and was one. It is
66
+ // written out by hand in `toast.js` because `@uniflowed/ui` is published to
67
+ // npm and `@uniflowed/state` is not: its name has never been bound, and
68
+ // binding it takes a person with an `npm login` session and a 2FA prompt —
69
+ // ubugeeei-prod/uf#210. A published package whose dependency is missing
70
+ // installs as nothing, `ETARGET` on the first thing a user types, so this
71
+ // package cannot declare that dependency until the name exists. The queue
72
+ // moves back to `@uniflowed/state` when #210 binds it. The local store is the
73
+ // shippable design, not the better one.
74
+ //
75
+ // # Server and client
76
+ //
77
+ // Every module that manages focus, listens to the document or holds state
78
+ // declares `"use client"`, because each of those needs a browser. That is a
79
+ // property of the components, not of the application: an RSC page may import
80
+ // this package from a Server Component, and only the parts that need the client
81
+ // join the client bundle.
82
+ //
83
+ // # How the package is laid out
84
+ //
85
+ // One root module per primitive, each with its own `exports` subpath, each
86
+ // named after the thing it implements:
87
+ //
88
+ // - `dialog.js` — the focus trap, focus restore, scroll lock and inert page,
89
+ // and the three props the four components below it differ from it by.
90
+ // - `alert-dialog.js`, `sheet.js`, `drawer.js` and `sidebar.js` — the four
91
+ // built on that one. An alert dialog is modal and cannot be dismissed by
92
+ // pressing beside it; a sheet is a dialog with an edge; a drawer is a sheet
93
+ // with a gesture, and therefore with WCAG 2.5.7's keyboard equivalent of it;
94
+ // a sidebar is most often neither modal nor a dialog, and becomes both on a
95
+ // narrow viewport.
96
+ // - `carousel.js`, `scroll-area.js` and `input-otp.js` — the three that replace
97
+ // something the browser already does, and so the three that have to be better
98
+ // than what they replaced. Each module's header says what it gives that the
99
+ // plain element does not; if it ever stops being true, the component should
100
+ // be deleted rather than fixed.
101
+ // - `menu.js` — the arrow keys, typeahead, submenus and `Escape` stacking.
102
+ // - `combobox.js` — `aria-activedescendant` over a filtered list, and the
103
+ // count a screen reader is told.
104
+ // - `select.js` — the other half of the combobox pattern: the select-only one,
105
+ // with typeahead, option groups and a value a form can submit.
106
+ // - `tabs.js` — the roving `tabindex`, and automatic versus manual activation.
107
+ // - `toast.js` — the live region that was watching before there was anything
108
+ // to announce, and the countdown that stops.
109
+ // - `field.js` — the label, description, error and `aria-invalid` wiring.
110
+ // - `switch.js`, `checkbox.js` and `toggle.js` — the three two-state controls,
111
+ // apart because a reader is told something different by each, and because the
112
+ // third state and the `Enter` key genuinely differ between them.
113
+ // - `radio-group.js` — one answer out of several, and the tab stop an
114
+ // unanswered group would otherwise not have.
115
+ // - `toggle-group.js` — a row of toggle buttons as one control, whose `single`
116
+ // mode is a radio group and is rendered by `radio-group.js` rather than
117
+ // written a second time.
118
+ // - `collapsible.js`, `accordion.js` and `navigation-menu.js` — the disclosure
119
+ // pattern on its own, stacked, and applied to a site's navigation. The third
120
+ // of those exists as much to prevent `role="menu"` from being used for a list
121
+ // of links as to provide anything.
122
+ // - `slider.js`, `resizable.js` and `progress.js` — the three that report a
123
+ // number in a range. A window splitter is a slider wearing a separator's
124
+ // role, which is why it is beside one rather than with the layout.
125
+ // - `table.js` and `pagination.js` — the sort that is announced, the selection
126
+ // that can be mixed, and the rows a page is not showing.
127
+ // - `popover.js`, `tooltip.js` and `hover-card.js` — the three anchored
128
+ // overlays, which are one component seen from three distances: one you
129
+ // click, one you hover, and one you hover and then read. They are three
130
+ // modules because what a reader is told differs in every one — a popover is
131
+ // a dialog that is not modal, a tooltip describes its trigger and may never
132
+ // take focus, a hover card is neither and holds links — and because a flag
133
+ // selecting between them would be one flag every behaviour had to read.
134
+ //
135
+ // Every name below is exported from one of those, so a consumer may import
136
+ // `@uniflowed/ui` or `@uniflowed/ui/dialog` and get the same thing. The split
137
+ // is by primitive because that is the unit a reader looks for, the unit a
138
+ // bundler drops, and the unit the WAI-ARIA practices are written in.
139
+ //
140
+ // `internal/` holds nine modules and nothing else, each a rule the primitives
141
+ // must apply identically and a consumer must not be able to apply differently:
142
+ // `merge-props.js` (the caller's props go on first, the component's semantics
143
+ // last), `controlled-state.js` (what "controlled" means here),
144
+ // `roving-focus.js` (how a set of items is found and moved between),
145
+ // `disclosure.js` (how a button says whether a region is showing, and how a
146
+ // closed region stays findable), `form-value.js` (what a `<form>` submits for a
147
+ // control the browser has never heard of), `range.js` (the arithmetic that
148
+ // keeps `aria-valuemin`, `aria-valuemax` and `aria-valuenow` true about each
149
+ // other), `anchor.js` (where an overlay goes, and what it does when it does not
150
+ // fit where it was asked to go), `focus.js` (which elements a reader can reach,
151
+ // which a focus trap and a popover want opposite things from), and
152
+ // `hover-intent.js` (what WCAG requires of content shown on hover or focus,
153
+ // which is three clauses and one mechanism). Each says in its own header why it
154
+ // is unreachable rather than exported. There is no `internal/props.js`-shaped
155
+ // bag of helpers: a module that cannot say what it is about does not belong in
156
+ // this package.
157
+
158
+ import {
159
+ AccordionContent,
160
+ AccordionHeader,
161
+ AccordionItem,
162
+ AccordionRoot,
163
+ AccordionTrigger,
164
+ } from "./accordion.js";
165
+ import {
166
+ AlertDialogAction,
167
+ AlertDialogBody,
168
+ AlertDialogCancel,
169
+ AlertDialogDescription,
170
+ AlertDialogFooter,
171
+ AlertDialogHeader,
172
+ AlertDialogOverlay,
173
+ AlertDialogRoot,
174
+ AlertDialogTitle,
175
+ AlertDialogTrigger,
176
+ } from "./alert-dialog.js";
177
+ import {
178
+ CarouselContent,
179
+ CarouselItem,
180
+ CarouselNext,
181
+ CarouselPause,
182
+ CarouselPrevious,
183
+ CarouselRoot,
184
+ } from "./carousel.js";
185
+ import { Checkbox } from "./checkbox.js";
186
+ import { CollapsibleContent, CollapsibleRoot, CollapsibleTrigger } from "./collapsible.js";
187
+ import {
188
+ ComboboxEmpty,
189
+ ComboboxInput,
190
+ ComboboxLabel,
191
+ ComboboxList,
192
+ ComboboxOption,
193
+ ComboboxRoot,
194
+ ComboboxStatus,
195
+ } from "./combobox.js";
196
+ import {
197
+ DialogBody,
198
+ DialogClose,
199
+ DialogDescription,
200
+ DialogFooter,
201
+ DialogHeader,
202
+ DialogOverlay,
203
+ DialogRoot,
204
+ DialogTitle,
205
+ DialogTrigger,
206
+ } from "./dialog.js";
207
+ import {
208
+ DrawerBody,
209
+ DrawerClose,
210
+ DrawerDescription,
211
+ DrawerFooter,
212
+ DrawerHandle,
213
+ DrawerHeader,
214
+ DrawerOverlay,
215
+ DrawerRoot,
216
+ DrawerTitle,
217
+ DrawerTrigger,
218
+ } from "./drawer.js";
219
+ import { FieldControl, FieldDescription, FieldError, FieldLabel, FieldRoot } from "./field.js";
220
+ import { HoverCardBody, HoverCardRoot, HoverCardTrigger } from "./hover-card.js";
221
+ import { InputOtpGroup, InputOtpRoot, InputOtpSeparator, InputOtpSlot } from "./input-otp.js";
222
+ import {
223
+ MenuBody,
224
+ MenuGroup,
225
+ MenuItem,
226
+ MenuLabel,
227
+ MenuRoot,
228
+ MenuSeparator,
229
+ MenuSub,
230
+ MenuSubTrigger,
231
+ MenuTrigger,
232
+ } from "./menu.js";
233
+ import {
234
+ NavigationMenuBody,
235
+ NavigationMenuItem,
236
+ NavigationMenuLink,
237
+ NavigationMenuList,
238
+ NavigationMenuRoot,
239
+ NavigationMenuTrigger,
240
+ } from "./navigation-menu.js";
241
+ import {
242
+ PaginationContent,
243
+ PaginationItem,
244
+ PaginationNext,
245
+ PaginationPrevious,
246
+ PaginationRoot,
247
+ } from "./pagination.js";
248
+ import { PopoverBody, PopoverRoot, PopoverTrigger } from "./popover.js";
249
+ import { Progress } from "./progress.js";
250
+ import { RadioGroupIndicator, RadioGroupItem, RadioGroupRoot } from "./radio-group.js";
251
+ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./resizable.js";
252
+ import { ScrollAreaRoot, ScrollAreaScrollbar, ScrollAreaViewport } from "./scroll-area.js";
253
+ import {
254
+ SelectGroup,
255
+ SelectGroupLabel,
256
+ SelectLabel,
257
+ SelectList,
258
+ SelectOption,
259
+ SelectRoot,
260
+ SelectSeparator,
261
+ SelectTrigger,
262
+ SelectValue,
263
+ } from "./select.js";
264
+ import {
265
+ SheetBody,
266
+ SheetClose,
267
+ SheetDescription,
268
+ SheetFooter,
269
+ SheetHeader,
270
+ SheetOverlay,
271
+ SheetRoot,
272
+ SheetTitle,
273
+ SheetTrigger,
274
+ } from "./sheet.js";
275
+ import {
276
+ SidebarBody,
277
+ SidebarFooter,
278
+ SidebarHeader,
279
+ SidebarItem,
280
+ SidebarRoot,
281
+ SidebarTrigger,
282
+ } from "./sidebar.js";
283
+ import { SliderRange, SliderRoot, SliderThumb, SliderTrack } from "./slider.js";
284
+ import { Switch } from "./switch.js";
285
+ import {
286
+ TableBody,
287
+ TableCaption,
288
+ TableCell,
289
+ TableHead,
290
+ TableHeader,
291
+ TableRoot,
292
+ TableRow,
293
+ TableRowHeader,
294
+ TableRowSelect,
295
+ TableSelectAll,
296
+ } from "./table.js";
297
+ import { TabsList, TabsPanel, TabsRoot, TabsTab } from "./tabs.js";
298
+ import {
299
+ ToastAction,
300
+ ToastClose,
301
+ ToastDescription,
302
+ ToastRegion,
303
+ ToastRoot,
304
+ ToastTitle,
305
+ dismissAllToasts,
306
+ dismissToast,
307
+ toast,
308
+ updateToast,
309
+ } from "./toast.js";
310
+ import { Toggle } from "./toggle.js";
311
+ import { ToggleGroupItem, ToggleGroupRoot } from "./toggle-group.js";
312
+ import { TooltipBody, TooltipProvider, TooltipRoot, TooltipTrigger } from "./tooltip.js";
313
+
314
+ export type { AccordionType } from "./accordion.js";
315
+ export type { ActivationMode } from "./tabs.js";
316
+ // What a modal announces itself as, for a caller who holds one in a variable.
317
+ // Two members, not a string: see `dialog.js`.
318
+ export type { DialogRole } from "./dialog.js";
319
+ // Which edge of the viewport a sheet or a drawer is attached to. `Sidebar` has
320
+ // its own two-member union, because a sidebar is never on the top or bottom.
321
+ export type { Edge } from "./sheet.js";
322
+ export type { InputOtpKind } from "./input-otp.js";
323
+ export type { SidebarSide } from "./sidebar.js";
324
+ // Where an anchored overlay opens, for a caller who holds one in a variable or
325
+ // a prop of their own. Unions rather than strings, so `side="botom"` is a type
326
+ // error at the call rather than an overlay that quietly opens somewhere else.
327
+ export type { Align, Side } from "./popover.js";
328
+ export type { Sort } from "./table.js";
329
+ export type { Notification, ToastChanges, ToastOptions, Urgency } from "./toast.js";
330
+ export type { ToggleGroupType } from "./toggle-group.js";
331
+
332
+ export { Checkbox, Progress, Switch, Toggle };
333
+
334
+ /**
335
+ * Queueing a notification, from anywhere.
336
+ *
337
+ * Functions rather than a hook, because the places a notification comes from —
338
+ * an event handler, a `catch`, a Server Action's error path — do not have a
339
+ * component to hold state in. `Toast.Region` is what displays them.
340
+ *
341
+ * const id = toast("Uploading…", { duration: null });
342
+ * updateToast(id, { content: "Uploaded", duration: 4000 });
343
+ * toast("Could not save", { urgency: "assertive" });
344
+ */
345
+ export { dismissAllToasts, dismissToast, toast, updateToast };
346
+
347
+ /**
348
+ * An accessible form field.
349
+ *
350
+ * <Field.Root invalid={error != null}>
351
+ * <Field.Label>Email</Field.Label>
352
+ * <Field.Control render={(props) => <input type="email" {...props} />} />
353
+ * <Field.Description>We will not share it.</Field.Description>
354
+ * <Field.Error>{error}</Field.Error>
355
+ * </Field.Root>
356
+ */
357
+ export const Field = {
358
+ Root: FieldRoot,
359
+ Label: FieldLabel,
360
+ Control: FieldControl,
361
+ Description: FieldDescription,
362
+ Error: FieldError,
363
+ };
364
+
365
+ /**
366
+ * Tabs, with the arrow-key behaviour the pattern requires.
367
+ *
368
+ * `activationMode="manual"` moves focus without selecting, for panels that cost
369
+ * something to show.
370
+ *
371
+ * <Tabs.Root defaultValue="one">
372
+ * <Tabs.List aria-label="Sections">
373
+ * <Tabs.Tab value="one">One</Tabs.Tab>
374
+ * <Tabs.Tab value="two">Two</Tabs.Tab>
375
+ * </Tabs.List>
376
+ * <Tabs.Panel value="one">…</Tabs.Panel>
377
+ * <Tabs.Panel value="two">…</Tabs.Panel>
378
+ * </Tabs.Root>
379
+ */
380
+ export const Tabs = {
381
+ Root: TabsRoot,
382
+ List: TabsList,
383
+ Tab: TabsTab,
384
+ Panel: TabsPanel,
385
+ };
386
+
387
+ /**
388
+ * A button and the region it shows, with the three attributes that say so.
389
+ *
390
+ * The content stays in the document while it is closed, so the browser's
391
+ * find-in-page can still reach the text in it.
392
+ *
393
+ * <Collapsible.Root>
394
+ * <Collapsible.Trigger>Details</Collapsible.Trigger>
395
+ * <Collapsible.Content>…</Collapsible.Content>
396
+ * </Collapsible.Root>
397
+ */
398
+ export const Collapsible = {
399
+ Root: CollapsibleRoot,
400
+ Trigger: CollapsibleTrigger,
401
+ Content: CollapsibleContent,
402
+ };
403
+
404
+ /**
405
+ * A stack of disclosures that know about each other.
406
+ *
407
+ * `Accordion.Header` takes the heading `level`, because which heading an
408
+ * accordion's sections are depends on where the accordion sits. Each panel is a
409
+ * region named after the header that opens it.
410
+ *
411
+ * <Accordion.Root type="single">
412
+ * <Accordion.Item value="shipping">
413
+ * <Accordion.Header level={3}>
414
+ * <Accordion.Trigger>Shipping</Accordion.Trigger>
415
+ * </Accordion.Header>
416
+ * <Accordion.Content>…</Accordion.Content>
417
+ * </Accordion.Item>
418
+ * </Accordion.Root>
419
+ */
420
+ export const Accordion = {
421
+ Root: AccordionRoot,
422
+ Item: AccordionItem,
423
+ Header: AccordionHeader,
424
+ Trigger: AccordionTrigger,
425
+ Content: AccordionContent,
426
+ };
427
+
428
+ /**
429
+ * Site navigation: a list of links behind buttons, and not a `menu`.
430
+ *
431
+ * <NavigationMenu.Root aria-label="Main">
432
+ * <NavigationMenu.List>
433
+ * <NavigationMenu.Item value="docs">
434
+ * <NavigationMenu.Trigger>Docs</NavigationMenu.Trigger>
435
+ * <NavigationMenu.Body>
436
+ * <NavigationMenu.Link href="/guide">Guide</NavigationMenu.Link>
437
+ * </NavigationMenu.Body>
438
+ * </NavigationMenu.Item>
439
+ * </NavigationMenu.List>
440
+ * </NavigationMenu.Root>
441
+ */
442
+ export const NavigationMenu = {
443
+ Root: NavigationMenuRoot,
444
+ List: NavigationMenuList,
445
+ Item: NavigationMenuItem,
446
+ Trigger: NavigationMenuTrigger,
447
+ Body: NavigationMenuBody,
448
+ Link: NavigationMenuLink,
449
+ };
450
+
451
+ /**
452
+ * One answer out of several, with the arrow keys that check as they move.
453
+ *
454
+ * `Tab` reaches the chosen answer, or the first one while there is none, and
455
+ * leaves the whole group in one press. `name` puts the answer where a form can
456
+ * submit it.
457
+ *
458
+ * <Field.Root>
459
+ * <Field.Label>Plan</Field.Label>
460
+ * <Field.Control
461
+ * render={(props) => (
462
+ * <RadioGroup.Root {...props} defaultValue="free" name="plan">
463
+ * <RadioGroup.Item value="free">
464
+ * Free <RadioGroup.Indicator>●</RadioGroup.Indicator>
465
+ * </RadioGroup.Item>
466
+ * <RadioGroup.Item value="pro">Pro</RadioGroup.Item>
467
+ * </RadioGroup.Root>
468
+ * )}
469
+ * />
470
+ * </Field.Root>
471
+ */
472
+ export const RadioGroup = {
473
+ Root: RadioGroupRoot,
474
+ Item: RadioGroupItem,
475
+ Indicator: RadioGroupIndicator,
476
+ };
477
+
478
+ /**
479
+ * A row of toggle buttons that behaves as one control.
480
+ *
481
+ * `type="multiple"` is a group of toggle buttons, any number of them pressed.
482
+ * `type="single"` is a radio group drawn as segments, and is rendered by
483
+ * `RadioGroup` rather than written a second time.
484
+ *
485
+ * <ToggleGroup.Root aria-label="Formatting" type="multiple">
486
+ * <ToggleGroup.Item value="bold">B</ToggleGroup.Item>
487
+ * <ToggleGroup.Item value="italic">I</ToggleGroup.Item>
488
+ * </ToggleGroup.Root>
489
+ */
490
+ export const ToggleGroup = {
491
+ Root: ToggleGroupRoot,
492
+ Item: ToggleGroupItem,
493
+ };
494
+
495
+ /**
496
+ * A modal dialog: focus moved in, kept in, and given back.
497
+ *
498
+ * <Dialog.Root>
499
+ * <Dialog.Trigger>Delete</Dialog.Trigger>
500
+ * <Dialog.Overlay />
501
+ * <Dialog.Body>
502
+ * <Dialog.Header>
503
+ * <Dialog.Title>Delete this project?</Dialog.Title>
504
+ * <Dialog.Description>This cannot be undone.</Dialog.Description>
505
+ * </Dialog.Header>
506
+ * <Dialog.Footer>
507
+ * <Dialog.Close>Cancel</Dialog.Close>
508
+ * </Dialog.Footer>
509
+ * </Dialog.Body>
510
+ * </Dialog.Root>
511
+ */
512
+ export const Dialog = {
513
+ Root: DialogRoot,
514
+ Trigger: DialogTrigger,
515
+ Overlay: DialogOverlay,
516
+ Body: DialogBody,
517
+ Header: DialogHeader,
518
+ Footer: DialogFooter,
519
+ Title: DialogTitle,
520
+ Description: DialogDescription,
521
+ Close: DialogClose,
522
+ };
523
+
524
+ /**
525
+ * The confirmation: modal, announced as an alert, and not dismissible by a
526
+ * press beside it.
527
+ *
528
+ * Focus lands on `Cancel` rather than on the first thing in the dialog, and the
529
+ * description is required — `role="alertdialog"` exists to announce one, so an
530
+ * alert dialog without it interrupts the reader to say nothing.
531
+ *
532
+ * <AlertDialog.Root>
533
+ * <AlertDialog.Trigger>Delete</AlertDialog.Trigger>
534
+ * <AlertDialog.Overlay />
535
+ * <AlertDialog.Body>
536
+ * <AlertDialog.Header>
537
+ * <AlertDialog.Title>Delete this project?</AlertDialog.Title>
538
+ * <AlertDialog.Description>This cannot be undone.</AlertDialog.Description>
539
+ * </AlertDialog.Header>
540
+ * <AlertDialog.Footer>
541
+ * <AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
542
+ * <AlertDialog.Action onClick={remove}>Delete</AlertDialog.Action>
543
+ * </AlertDialog.Footer>
544
+ * </AlertDialog.Body>
545
+ * </AlertDialog.Root>
546
+ */
547
+ export const AlertDialog = {
548
+ Root: AlertDialogRoot,
549
+ Trigger: AlertDialogTrigger,
550
+ Overlay: AlertDialogOverlay,
551
+ Body: AlertDialogBody,
552
+ Header: AlertDialogHeader,
553
+ Footer: AlertDialogFooter,
554
+ Title: AlertDialogTitle,
555
+ Description: AlertDialogDescription,
556
+ Action: AlertDialogAction,
557
+ Cancel: AlertDialogCancel,
558
+ };
559
+
560
+ /**
561
+ * A modal dialog attached to an edge of the viewport.
562
+ *
563
+ * `side` is a union rather than a class name, and every part reports it as
564
+ * `data-side` — the same attribute `Popover.Body` writes, so one stylesheet
565
+ * rule covers every overlay in this package.
566
+ *
567
+ * <Sheet.Root side="left">
568
+ * <Sheet.Trigger>Filters</Sheet.Trigger>
569
+ * <Sheet.Overlay />
570
+ * <Sheet.Body>
571
+ * <Sheet.Title>Filters</Sheet.Title>
572
+ * <Sheet.Close>Done</Sheet.Close>
573
+ * </Sheet.Body>
574
+ * </Sheet.Root>
575
+ */
576
+ export const Sheet = {
577
+ Root: SheetRoot,
578
+ Trigger: SheetTrigger,
579
+ Overlay: SheetOverlay,
580
+ Body: SheetBody,
581
+ Header: SheetHeader,
582
+ Footer: SheetFooter,
583
+ Title: SheetTitle,
584
+ Description: SheetDescription,
585
+ Close: SheetClose,
586
+ };
587
+
588
+ /**
589
+ * The sheet you can drag away, with the keyboard that can do everything the
590
+ * drag can.
591
+ *
592
+ * `Drawer.Handle` is a `role="slider"` over the snap points: the arrow keys
593
+ * move between them, `Home` and `End` go to the ends, and the closing key at
594
+ * the smallest snap point closes it. WCAG 2.5.7 also wants a single-pointer
595
+ * alternative, so a drawer with a handle and no `Drawer.Close` raises.
596
+ *
597
+ * <Drawer.Root side="bottom" snapPoints={[0.4, 1]}>
598
+ * <Drawer.Trigger>Details</Drawer.Trigger>
599
+ * <Drawer.Overlay />
600
+ * <Drawer.Body>
601
+ * <Drawer.Handle label="Resize the details" />
602
+ * <Drawer.Title>Details</Drawer.Title>
603
+ * <Drawer.Close>Close</Drawer.Close>
604
+ * </Drawer.Body>
605
+ * </Drawer.Root>
606
+ */
607
+ export const Drawer = {
608
+ Root: DrawerRoot,
609
+ Trigger: DrawerTrigger,
610
+ Overlay: DrawerOverlay,
611
+ Body: DrawerBody,
612
+ Handle: DrawerHandle,
613
+ Header: DrawerHeader,
614
+ Footer: DrawerFooter,
615
+ Title: DrawerTitle,
616
+ Description: DrawerDescription,
617
+ Close: DrawerClose,
618
+ };
619
+
620
+ /**
621
+ * Navigation beside the page, which becomes a modal sheet on a narrow one.
622
+ *
623
+ * `Sidebar.Item` takes a `label` and keeps it as the button's accessible name
624
+ * the moment the sidebar collapses to icons — which is the whole reason a
625
+ * collapsing sidebar is a component rather than a class.
626
+ *
627
+ * <Sidebar.Root defaultOpen={fromCookie}>
628
+ * <Sidebar.Trigger>Menu</Sidebar.Trigger>
629
+ * <Sidebar.Body label="Main">
630
+ * <Sidebar.Item label="Settings">
631
+ * <Gear /> Settings
632
+ * </Sidebar.Item>
633
+ * </Sidebar.Body>
634
+ * </Sidebar.Root>
635
+ */
636
+ export const Sidebar = {
637
+ Root: SidebarRoot,
638
+ Trigger: SidebarTrigger,
639
+ Header: SidebarHeader,
640
+ Body: SidebarBody,
641
+ Footer: SidebarFooter,
642
+ Item: SidebarItem,
643
+ };
644
+
645
+ /**
646
+ * Slides, one at a time, that a reader can stop and cannot fall into.
647
+ *
648
+ * `Carousel.Pause` is WCAG 2.2.2's mechanism and must be the first focusable
649
+ * thing inside the carousel; the slides that are not showing are `inert`, so
650
+ * `Tab` cannot reach a link nobody can see.
651
+ *
652
+ * <Carousel.Root autoplay={5000} count={3} label="Featured">
653
+ * <Carousel.Pause />
654
+ * <Carousel.Content>
655
+ * <Carousel.Item index={0}>…</Carousel.Item>
656
+ * <Carousel.Item index={1}>…</Carousel.Item>
657
+ * <Carousel.Item index={2}>…</Carousel.Item>
658
+ * </Carousel.Content>
659
+ * <Carousel.Previous />
660
+ * <Carousel.Next />
661
+ * </Carousel.Root>
662
+ */
663
+ export const Carousel = {
664
+ Root: CarouselRoot,
665
+ Content: CarouselContent,
666
+ Item: CarouselItem,
667
+ Pause: CarouselPause,
668
+ Previous: CarouselPrevious,
669
+ Next: CarouselNext,
670
+ };
671
+
672
+ /**
673
+ * An overflow container a keyboard can actually scroll.
674
+ *
675
+ * `role="region"`, a name and `tabindex="0"`, because a scroll container is not
676
+ * focusable in every browser and one that is not is one a keyboard reader can
677
+ * see the top of and nothing else.
678
+ *
679
+ * <ScrollArea.Root label="Release notes">
680
+ * <ScrollArea.Viewport>…</ScrollArea.Viewport>
681
+ * <ScrollArea.Scrollbar orientation="vertical" />
682
+ * </ScrollArea.Root>
683
+ */
684
+ export const ScrollArea = {
685
+ Root: ScrollAreaRoot,
686
+ Viewport: ScrollAreaViewport,
687
+ Scrollbar: ScrollAreaScrollbar,
688
+ };
689
+
690
+ /**
691
+ * A one-time code: six boxes drawn over one real `<input>`.
692
+ *
693
+ * One input, so `autocomplete="one-time-code"` works, a paste fills every box,
694
+ * and a form submits one value under one name.
695
+ *
696
+ * <InputOtp.Root label="One-time code" length={6} name="code">
697
+ * <InputOtp.Group>
698
+ * <InputOtp.Slot index={0} />
699
+ * <InputOtp.Slot index={1} />
700
+ * <InputOtp.Slot index={2} />
701
+ * </InputOtp.Group>
702
+ * <InputOtp.Separator>-</InputOtp.Separator>
703
+ * <InputOtp.Group>
704
+ * <InputOtp.Slot index={3} />
705
+ * <InputOtp.Slot index={4} />
706
+ * <InputOtp.Slot index={5} />
707
+ * </InputOtp.Group>
708
+ * </InputOtp.Root>
709
+ */
710
+ export const InputOtp = {
711
+ Root: InputOtpRoot,
712
+ Group: InputOtpGroup,
713
+ Slot: InputOtpSlot,
714
+ Separator: InputOtpSeparator,
715
+ };
716
+
717
+ /**
718
+ * A menu, with the keyboard map every native menu has had for thirty years.
719
+ *
720
+ * <Menu.Root>
721
+ * <Menu.Trigger>File</Menu.Trigger>
722
+ * <Menu.Body>
723
+ * <Menu.Group>
724
+ * <Menu.Label>Recent</Menu.Label>
725
+ * <Menu.Item onSelect={open}>Open…</Menu.Item>
726
+ * </Menu.Group>
727
+ * <Menu.Separator />
728
+ * <Menu.Sub>
729
+ * <Menu.SubTrigger>Export</Menu.SubTrigger>
730
+ * <Menu.Body>
731
+ * <Menu.Item onSelect={png}>PNG</Menu.Item>
732
+ * </Menu.Body>
733
+ * </Menu.Sub>
734
+ * </Menu.Body>
735
+ * </Menu.Root>
736
+ */
737
+ export const Menu = {
738
+ Root: MenuRoot,
739
+ Trigger: MenuTrigger,
740
+ Body: MenuBody,
741
+ Item: MenuItem,
742
+ Separator: MenuSeparator,
743
+ Group: MenuGroup,
744
+ Label: MenuLabel,
745
+ Sub: MenuSub,
746
+ SubTrigger: MenuSubTrigger,
747
+ };
748
+
749
+ /**
750
+ * A text field with a list of options, navigated without leaving the field.
751
+ *
752
+ * The caller filters; the component keeps the ARIA wiring true while they do.
753
+ *
754
+ * <Combobox.Root inputValue={query} onInputValueChange={setQuery}>
755
+ * <Combobox.Label>Country</Combobox.Label>
756
+ * <Combobox.Input />
757
+ * <Combobox.List>
758
+ * {matches.map((each) => (
759
+ * <Combobox.Option key={each} value={each}>{each}</Combobox.Option>
760
+ * ))}
761
+ * </Combobox.List>
762
+ * <Combobox.Empty>No matches.</Combobox.Empty>
763
+ * <Combobox.Status />
764
+ * </Combobox.Root>
765
+ */
766
+ export const Combobox = {
767
+ Root: ComboboxRoot,
768
+ Label: ComboboxLabel,
769
+ Input: ComboboxInput,
770
+ List: ComboboxList,
771
+ Option: ComboboxOption,
772
+ Empty: ComboboxEmpty,
773
+ Status: ComboboxStatus,
774
+ };
775
+
776
+ /**
777
+ * The other half of the combobox pattern: a button, a list, and no typing.
778
+ *
779
+ * Use a native `<select>` when a native `<select>` will do — `select.js` says
780
+ * so first and means it. This is for the popup a `<select>` cannot draw.
781
+ *
782
+ * <Select.Root defaultValue="GB" name="country">
783
+ * <Select.Label>Country</Select.Label>
784
+ * <Select.Trigger>
785
+ * <Select.Value placeholder="Choose one" />
786
+ * </Select.Trigger>
787
+ * <Select.List>
788
+ * <Select.Group>
789
+ * <Select.GroupLabel>Europe</Select.GroupLabel>
790
+ * <Select.Option value="GB">United Kingdom</Select.Option>
791
+ * <Select.Option value="FR">France</Select.Option>
792
+ * </Select.Group>
793
+ * <Select.Separator />
794
+ * <Select.Option value="JP">Japan</Select.Option>
795
+ * </Select.List>
796
+ * </Select.Root>
797
+ *
798
+ * `Select.Label` names the field and `Select.GroupLabel` names a group of
799
+ * options. shadcn has one `SelectLabel` and it is the second of those; a select
800
+ * needs both, so they are two parts here.
801
+ */
802
+ export const Select = {
803
+ Root: SelectRoot,
804
+ Label: SelectLabel,
805
+ Trigger: SelectTrigger,
806
+ Value: SelectValue,
807
+ List: SelectList,
808
+ Option: SelectOption,
809
+ Group: SelectGroup,
810
+ GroupLabel: SelectGroupLabel,
811
+ Separator: SelectSeparator,
812
+ };
813
+
814
+ /**
815
+ * A dialog that is not modal, anchored to the button that opened it.
816
+ *
817
+ * Focus moves in, `Escape` closes it and gives focus back, and `Tab` *leaves* —
818
+ * the page behind a popover is still there, still scrollable and still
819
+ * tabbable, which is every way in which it is not a `Dialog`.
820
+ *
821
+ * <Popover.Root>
822
+ * <Popover.Trigger>Filters</Popover.Trigger>
823
+ * <Popover.Body align="start" side="bottom" sideOffset={8}>
824
+ * <label>
825
+ * Only mine <input type="checkbox" />
826
+ * </label>
827
+ * </Popover.Body>
828
+ * </Popover.Root>
829
+ *
830
+ * `Popover.Body` reports where it ended up as `data-side` and `data-align`, and
831
+ * writes the trigger's width and the room it had as custom properties, so a
832
+ * stylesheet can point an arrow and cap a height without measuring anything.
833
+ */
834
+ export const Popover = {
835
+ Root: PopoverRoot,
836
+ Trigger: PopoverTrigger,
837
+ Body: PopoverBody,
838
+ };
839
+
840
+ /**
841
+ * A phrase about a control, on hover and on focus, that WCAG would accept.
842
+ *
843
+ * Dismissible with `Escape`, hoverable — the pointer can travel onto it — and
844
+ * never focusable. It does not open on touch, deliberately, so the trigger must
845
+ * carry its own name for a reader holding a phone.
846
+ *
847
+ * <Tooltip.Provider delayDuration={700} skipDelayDuration={300}>
848
+ * <Tooltip.Root>
849
+ * <Tooltip.Trigger aria-label="Bold">B</Tooltip.Trigger>
850
+ * <Tooltip.Body>Bold (⌘B)</Tooltip.Body>
851
+ * </Tooltip.Root>
852
+ * <Tooltip.Root>
853
+ * <Tooltip.Trigger aria-label="Italic">I</Tooltip.Trigger>
854
+ * <Tooltip.Body>Italic (⌘I)</Tooltip.Body>
855
+ * </Tooltip.Root>
856
+ * </Tooltip.Provider>
857
+ *
858
+ * `Tooltip.Provider` is what makes the second icon in that toolbar answer at
859
+ * once instead of making the reader wait the delay again. A tooltip outside one
860
+ * is a complete tooltip with a delay of its own.
861
+ */
862
+ export const Tooltip = {
863
+ Provider: TooltipProvider,
864
+ Root: TooltipRoot,
865
+ Trigger: TooltipTrigger,
866
+ Body: TooltipBody,
867
+ };
868
+
869
+ /**
870
+ * The preview a name expands into: hovered, focused, and full of links.
871
+ *
872
+ * Not a tooltip — its contents are reachable, by pointer and by `Tab` — and not
873
+ * a dialog, because nothing about it is modal.
874
+ *
875
+ * <HoverCard.Root>
876
+ * <HoverCard.Trigger render={(props) => <a href="/ada" {...props}>@ada</a>} />
877
+ * <HoverCard.Body>
878
+ * <p>Ada Lovelace</p>
879
+ * <a href="/ada/notes">Notes</a>
880
+ * </HoverCard.Body>
881
+ * </HoverCard.Root>
882
+ */
883
+ export const HoverCard = {
884
+ Root: HoverCardRoot,
885
+ Trigger: HoverCardTrigger,
886
+ Body: HoverCardBody,
887
+ };
888
+
889
+ /**
890
+ * Notifications, in a live region that was watching before them.
891
+ *
892
+ * Render `Toast.Region` once, in the layout; `toast()` from anywhere.
893
+ *
894
+ * <Toast.Region>
895
+ * {(each) => (
896
+ * <Toast.Root>
897
+ * <Toast.Title>{each.content}</Toast.Title>
898
+ * <Toast.Action onClick={undo}>Undo</Toast.Action>
899
+ * <Toast.Close />
900
+ * </Toast.Root>
901
+ * )}
902
+ * </Toast.Region>
903
+ */
904
+ export const Toast = {
905
+ Region: ToastRegion,
906
+ Root: ToastRoot,
907
+ Title: ToastTitle,
908
+ Description: ToastDescription,
909
+ Action: ToastAction,
910
+ Close: ToastClose,
911
+ };
912
+
913
+ /**
914
+ * A value in a range, with `role="slider"` on the thumb where it belongs.
915
+ *
916
+ * One thumb or two; a range is the same component with a second one, each
917
+ * bounded by its neighbour and each needing its own name.
918
+ *
919
+ * <Slider.Root defaultValue={[20, 60]} valueText={(each) => `£${each}`}>
920
+ * <Slider.Track>
921
+ * <Slider.Range />
922
+ * </Slider.Track>
923
+ * <Slider.Thumb aria-label="Minimum" index={0} />
924
+ * <Slider.Thumb aria-label="Maximum" index={1} />
925
+ * </Slider.Root>
926
+ */
927
+ export const Slider = {
928
+ Root: SliderRoot,
929
+ Track: SliderTrack,
930
+ Range: SliderRange,
931
+ Thumb: SliderThumb,
932
+ };
933
+
934
+ /**
935
+ * Two panes and the splitter between them, operable from the keyboard.
936
+ *
937
+ * <Resizable.PanelGroup defaultValue={30}>
938
+ * <Resizable.Panel primary>Files</Resizable.Panel>
939
+ * <Resizable.Handle label="Resize the file list" />
940
+ * <Resizable.Panel>Editor</Resizable.Panel>
941
+ * </Resizable.PanelGroup>
942
+ */
943
+ export const Resizable = {
944
+ PanelGroup: ResizablePanelGroup,
945
+ Panel: ResizablePanel,
946
+ Handle: ResizableHandle,
947
+ };
948
+
949
+ /**
950
+ * A table, with the four things about one nobody gets right by hand.
951
+ *
952
+ * A real `<table>`, deliberately not a `role="grid"` — `table.js` says why —
953
+ * and its own live region, so a re-sort is something a reader is told about
954
+ * rather than something that happens silently behind them.
955
+ *
956
+ * <Table.Root onSortChange={setSort} rowCount={500} rowOffset={90} sort={sort}>
957
+ * <Table.Caption>People</Table.Caption>
958
+ * <Table.Header>
959
+ * <Table.Row>
960
+ * <Table.Head>
961
+ * <Table.SelectAll checked={all} onCheckedChange={setAll} />
962
+ * </Table.Head>
963
+ * <Table.Head column="name">Name</Table.Head>
964
+ * </Table.Row>
965
+ * </Table.Header>
966
+ * <Table.Body>
967
+ * {page.map((person, at) => (
968
+ * <Table.Row index={at} key={person.id}>
969
+ * <Table.Cell>
970
+ * <Table.RowSelect
971
+ * checked={chosen.has(person.id)}
972
+ * label={`Select ${person.name}`}
973
+ * onCheckedChange={(on) => choose(person.id, on)}
974
+ * />
975
+ * </Table.Cell>
976
+ * <Table.RowHeader>{person.name}</Table.RowHeader>
977
+ * </Table.Row>
978
+ * ))}
979
+ * </Table.Body>
980
+ * </Table.Root>
981
+ */
982
+ export const Table = {
983
+ Root: TableRoot,
984
+ Caption: TableCaption,
985
+ Header: TableHeader,
986
+ Body: TableBody,
987
+ Row: TableRow,
988
+ Head: TableHead,
989
+ RowHeader: TableRowHeader,
990
+ Cell: TableCell,
991
+ SelectAll: TableSelectAll,
992
+ RowSelect: TableRowSelect,
993
+ };
994
+
995
+ /**
996
+ * The navigation a paginated table needs, and the sentence that says it moved.
997
+ *
998
+ * <Pagination.Root page={4} pageCount={25}>
999
+ * <Pagination.Content>
1000
+ * <Pagination.Previous disabled={page === 1} href={hrefFor(page - 1)}>‹</Pagination.Previous>
1001
+ * <Pagination.Item current href={hrefFor(4)}>4</Pagination.Item>
1002
+ * <Pagination.Next href={hrefFor(page + 1)}>›</Pagination.Next>
1003
+ * </Pagination.Content>
1004
+ * </Pagination.Root>
1005
+ */
1006
+ export const Pagination = {
1007
+ Root: PaginationRoot,
1008
+ Content: PaginationContent,
1009
+ Item: PaginationItem,
1010
+ Previous: PaginationPrevious,
1011
+ Next: PaginationNext,
1012
+ };