@seekalfred/primitives 0.7.3 → 0.8.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 (149) hide show
  1. package/README.md +315 -4
  2. package/dist/alert/alert.d.ts +2 -2
  3. package/dist/alert/alert.js +1 -1
  4. package/dist/alert/alert.js.map +1 -1
  5. package/dist/alert-dialog/alert-dialog.d.ts +14 -0
  6. package/dist/alert-dialog/alert-dialog.js +52 -27
  7. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  8. package/dist/announcement-bar/announcement-bar.d.ts +15 -4
  9. package/dist/announcement-bar/announcement-bar.js +31 -6
  10. package/dist/announcement-bar/announcement-bar.js.map +1 -1
  11. package/dist/avatar/avatar.d.ts +7 -0
  12. package/dist/avatar/avatar.js +33 -12
  13. package/dist/avatar/avatar.js.map +1 -1
  14. package/dist/badge/badge.d.ts +16 -1
  15. package/dist/badge/badge.js +12 -5
  16. package/dist/badge/badge.js.map +1 -1
  17. package/dist/bar-chart/bar-chart-series.d.ts +75 -0
  18. package/dist/bar-chart/bar-chart-series.js +401 -0
  19. package/dist/bar-chart/bar-chart-series.js.map +1 -0
  20. package/dist/bar-chart/bar-chart.d.ts +25 -1
  21. package/dist/bar-chart/bar-chart.js +26 -1
  22. package/dist/bar-chart/bar-chart.js.map +1 -1
  23. package/dist/bar-chart/index.d.ts +2 -1
  24. package/dist/button/button.d.ts +24 -2
  25. package/dist/button/button.js +62 -13
  26. package/dist/button/button.js.map +1 -1
  27. package/dist/card/card.js +1 -1
  28. package/dist/card/card.js.map +1 -1
  29. package/dist/checkbox/checkbox.d.ts +25 -1
  30. package/dist/checkbox/checkbox.js +103 -28
  31. package/dist/checkbox/checkbox.js.map +1 -1
  32. package/dist/checkbox/index.d.ts +2 -2
  33. package/dist/checkbox/index.js +2 -2
  34. package/dist/chip-group/chip-group.d.ts +11 -0
  35. package/dist/chip-group/chip-group.js +50 -34
  36. package/dist/chip-group/chip-group.js.map +1 -1
  37. package/dist/chip-group/index.d.ts +2 -2
  38. package/dist/code-block/code-block.d.ts +45 -0
  39. package/dist/code-block/code-block.js +74 -0
  40. package/dist/code-block/code-block.js.map +1 -0
  41. package/dist/code-block/index.d.ts +2 -0
  42. package/dist/code-block/index.js +2 -0
  43. package/dist/command-palette/command-palette.d.ts +128 -0
  44. package/dist/command-palette/command-palette.js +371 -0
  45. package/dist/command-palette/command-palette.js.map +1 -0
  46. package/dist/command-palette/index.d.ts +2 -0
  47. package/dist/command-palette/index.js +2 -0
  48. package/dist/copy-button/copy-button.d.ts +19 -3
  49. package/dist/copy-button/copy-button.js +7 -4
  50. package/dist/copy-button/copy-button.js.map +1 -1
  51. package/dist/dialog/dialog.js +1 -1
  52. package/dist/dialog/dialog.js.map +1 -1
  53. package/dist/empty-state/empty-state.d.ts +17 -0
  54. package/dist/empty-state/empty-state.js +9 -7
  55. package/dist/empty-state/empty-state.js.map +1 -1
  56. package/dist/file-drop-zone/file-drop-zone.d.ts +29 -3
  57. package/dist/file-drop-zone/file-drop-zone.js +26 -8
  58. package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
  59. package/dist/gauge/gauge.d.ts +18 -1
  60. package/dist/gauge/gauge.js +35 -21
  61. package/dist/gauge/gauge.js.map +1 -1
  62. package/dist/gauge/index.d.ts +2 -2
  63. package/dist/index.d.ts +14 -9
  64. package/dist/index.js +14 -10
  65. package/dist/info-tooltip/index.d.ts +2 -0
  66. package/dist/info-tooltip/index.js +2 -0
  67. package/dist/info-tooltip/info-tooltip.d.ts +35 -0
  68. package/dist/info-tooltip/info-tooltip.js +49 -0
  69. package/dist/info-tooltip/info-tooltip.js.map +1 -0
  70. package/dist/input/input.d.ts +7 -0
  71. package/dist/input/input.js +4 -2
  72. package/dist/input/input.js.map +1 -1
  73. package/dist/lib/field-shell.js +2 -6
  74. package/dist/lib/field-shell.js.map +1 -1
  75. package/dist/lib/required-marker.js +20 -0
  76. package/dist/lib/required-marker.js.map +1 -0
  77. package/dist/lib/selection-control.js +32 -1
  78. package/dist/lib/selection-control.js.map +1 -1
  79. package/dist/line-chart/index.d.ts +2 -2
  80. package/dist/line-chart/line-chart.d.ts +106 -3
  81. package/dist/line-chart/line-chart.js +185 -46
  82. package/dist/line-chart/line-chart.js.map +1 -1
  83. package/dist/mention-input/mention-input.js +1 -1
  84. package/dist/money-input/index.d.ts +2 -2
  85. package/dist/money-input/money-input.d.ts +14 -0
  86. package/dist/money-input/money-input.js +17 -7
  87. package/dist/money-input/money-input.js.map +1 -1
  88. package/dist/morph-select/morph-select.d.ts +8 -1
  89. package/dist/morph-select/morph-select.js +8 -5
  90. package/dist/morph-select/morph-select.js.map +1 -1
  91. package/dist/multi-select/multi-select.d.ts +31 -0
  92. package/dist/multi-select/multi-select.js +123 -6
  93. package/dist/multi-select/multi-select.js.map +1 -1
  94. package/dist/number-field/number-field.js +1 -1
  95. package/dist/password-field/password-field.js +1 -1
  96. package/dist/password-strength/password-strength.js +1 -1
  97. package/dist/phone-input/phone-input.js +1 -1
  98. package/dist/popover/popover.js +1 -1
  99. package/dist/popover/popover.js.map +1 -1
  100. package/dist/progress/index.d.ts +2 -2
  101. package/dist/progress/index.js +2 -2
  102. package/dist/progress/progress.d.ts +53 -2
  103. package/dist/progress/progress.js +146 -36
  104. package/dist/progress/progress.js.map +1 -1
  105. package/dist/radio-group/radio-group.d.ts +12 -3
  106. package/dist/radio-group/radio-group.js +62 -52
  107. package/dist/radio-group/radio-group.js.map +1 -1
  108. package/dist/rich-text-editor/rich-text-editor.js +1 -1
  109. package/dist/scroll-area/scroll-area.d.ts +1 -1
  110. package/dist/scrub-input/scrub-input.d.ts +11 -1
  111. package/dist/scrub-input/scrub-input.js +22 -10
  112. package/dist/scrub-input/scrub-input.js.map +1 -1
  113. package/dist/search-field/search-field.js +1 -1
  114. package/dist/select/select.d.ts +10 -1
  115. package/dist/select/select.js +4 -3
  116. package/dist/select/select.js.map +1 -1
  117. package/dist/separator/separator.d.ts +1 -1
  118. package/dist/shortcut-recorder/shortcut-recorder.js +1 -1
  119. package/dist/sparkline/sparkline.d.ts +27 -2
  120. package/dist/sparkline/sparkline.js +115 -18
  121. package/dist/sparkline/sparkline.js.map +1 -1
  122. package/dist/stat-card/index.d.ts +2 -2
  123. package/dist/stat-card/index.js +2 -2
  124. package/dist/stat-card/stat-card.d.ts +32 -1
  125. package/dist/stat-card/stat-card.js +52 -9
  126. package/dist/stat-card/stat-card.js.map +1 -1
  127. package/dist/styles.css +1 -1
  128. package/dist/table/index.d.ts +2 -2
  129. package/dist/table/index.js +2 -2
  130. package/dist/table/table.d.ts +211 -3
  131. package/dist/table/table.js +473 -93
  132. package/dist/table/table.js.map +1 -1
  133. package/dist/tabs/tabs.d.ts +4 -2
  134. package/dist/tabs/tabs.js +7 -5
  135. package/dist/tabs/tabs.js.map +1 -1
  136. package/dist/tag-input/tag-input.js +1 -1
  137. package/dist/text-shimmer/text-shimmer.d.ts +18 -2
  138. package/dist/text-shimmer/text-shimmer.js +5 -2
  139. package/dist/text-shimmer/text-shimmer.js.map +1 -1
  140. package/dist/textarea/textarea.d.ts +7 -0
  141. package/dist/textarea/textarea.js +4 -2
  142. package/dist/textarea/textarea.js.map +1 -1
  143. package/dist/toast/toast.d.ts +2 -2
  144. package/dist/toggle-button/index.d.ts +2 -0
  145. package/dist/toggle-button/index.js +2 -0
  146. package/dist/toggle-button/toggle-button.d.ts +93 -0
  147. package/dist/toggle-button/toggle-button.js +199 -0
  148. package/dist/toggle-button/toggle-button.js.map +1 -0
  149. package/package.json +19 -3
package/README.md CHANGED
@@ -292,6 +292,13 @@ darkening it — colour alone disappears under forced-colors.
292
292
 
293
293
  A crumb added after the first render slides in 8px from the left over 240ms and the crumbs that stay slide to their new place; link colour eases over 160ms. The trail present at first render is still. A removed crumb disappears at once (it would need a leaving copy), and the reference's underline fade is not ported: Alfred's links carry no underline at rest by design.
294
294
 
295
+ ### Wrapping (Badge)
296
+
297
+ `wrap` lets the label break onto more lines, for a tag in a table cell or a narrow column. The badge shrinks to its
298
+ container, breaks long unspaced words, starts the text at the leading edge and grows in height. Off by default and then
299
+ a badge is one line that never shrinks, as before. Prefer `shape="rounded"` for long labels: a multi-line `pill` has
300
+ very round ends. The width does not spring to a new label while `wrap` is on.
301
+
295
302
  ## Button
296
303
 
297
304
  | variant | |
@@ -300,6 +307,7 @@ A crumb added after the first render slides in 8px from the left over 240ms and
300
307
  | `secondary` | solid brand periwinkle, ink label |
301
308
  | `outline` | background fill, brand border and label |
302
309
  | `ghost` | transparent until hovered |
310
+ | `ai` | orange gradient, ink label, wand icon; **generative actions only** |
303
311
  | `destructive` | solid `destructive` |
304
312
  | `link` | text only, underlines on hover |
305
313
 
@@ -308,6 +316,13 @@ The primary label is **ink, not white**. Brand orange carries white at only
308
316
  same reason `link` and `outline` use `primary-strong` — a darkened tone of the
309
317
  brand hue — because `primary` itself cannot legally be text or a border.
310
318
 
319
+ **`ai` is for generative actions only** (Generate, Summarise, Seek Alfred) and
320
+ nothing else. Primary stays flat; the gradient belongs to `ai` alone. The button
321
+ brings its own wand: pass `startIcon={false}` for none, or your own node in
322
+ `startIcon` to replace it (never as a child, which adds a second icon). Disabled
323
+ swaps the gradient for the muted fill. On keyboard focus its own border turns
324
+ ink instead of drawing a ring (the outline returns under forced-colors).
325
+
311
326
  Sizes: `sm` (32px) · `md` (36px, default) · `lg` (40px) · `icon` (36px square).
312
327
 
313
328
  `size="icon"` has no visible text, so it **must** carry an `aria-label`. Without
@@ -362,6 +377,18 @@ The `@seekalfred/styles` motion tokens behind it are `--ease-*` and
362
377
  `--duration-*` (`settle`, `enter`, `snappy`, `morph`, `gentle`, `calm`,
363
378
  `smooth`, `quick`, `instant`, …).
364
379
 
380
+ ### Compact icon button and wrapping label (Button)
381
+
382
+ `size="icon-sm"` is a 24px square icon-only button, for a glyph that sits beside text (an info trigger next to a
383
+ heading). 24px is the WCAG 2.5.8 target minimum, so the 14px glyph stays hittable. It is fixed, not on the density
384
+ scale. Same rules as `size="icon"`: the glyph is `children` and the button must carry an `aria-label`.
385
+
386
+ `wrap` lets the label break across lines, for long text that is not ours (a prompt in a list, a name in a cell). The
387
+ button shrinks to its container, breaks even long unspaced words, starts the text at the leading edge, and grows
388
+ taller than `size`, whose height becomes its minimum. Add `className="justify-start"` to start a `link` button at the
389
+ edge of its row. Off by default, and then a button is one line at a fixed height as before. A wrapping button's
390
+ width does not spring when its label changes.
391
+
365
392
  ## Migrating from the legacy shadcn Button
366
393
 
367
394
  The variant and size vocabulary is unchanged, so most call sites move by
@@ -616,6 +643,35 @@ The label is `children`, and that is also how the control gets its accessible
616
643
  name. Pass `aria-label` instead only for a genuinely label-less checkbox, such
617
644
  as a table row selector.
618
645
 
646
+ ### Card variant
647
+
648
+ `variant="card"` draws one option as a selectable card, the counterpart of
649
+ `RadioGroup variant="card"` for a choice where several can be on. The whole card
650
+ is the target, the title is `children`, and it takes a `description`, a
651
+ decorative `icon` and `media` (a thumbnail or preview above the title). The box
652
+ sits at the end of the title row.
653
+
654
+ ```tsx
655
+ <Checkbox variant="card" isSelected={on} onChange={setOn} description="gpt-5 · $1.25 in · $10 out per 1M" icon={<CpuIcon />}>
656
+ GPT-5
657
+ </Checkbox>
658
+ <Checkbox variant="card" media={<img src={thumb} alt="" className="aspect-square w-full rounded-md object-cover" />}>
659
+ hero-banner.png
660
+ </Checkbox>
661
+ ```
662
+
663
+ - The title alone is the option's name; the description is its description
664
+ (`aria-describedby`). The icon is `aria-hidden`; give `media` its own `alt`.
665
+ - Selected shows twice: a `primary-strong` edge and the tick, so it never rests on
666
+ colour alone.
667
+ - **Focus** is the card's own 1px border turning dark, with no ring (as on a text
668
+ field). Forced colours get an outline back, because they override border colours.
669
+ - `description`, `icon` and `media` are ignored by the default variant, which renders
670
+ exactly as before. `checkboxCardVariants` is exported.
671
+ - Card options are for choosing (state that is kept). A tile that does something on
672
+ press, and closes the dialog it is in, is a Button: radio selection follows the
673
+ arrow keys, so a radio card that acts on `onChange` would act on the first arrow press.
674
+
619
675
  ### Migrating from the legacy Checkbox
620
676
 
621
677
  The legacy component had **zero call sites**, so nothing here breaks. It is
@@ -636,6 +692,67 @@ other form controls will take.
636
692
 
637
693
  The chosen colour grows in from small on the snappy spring while it fades in, the mark draws itself, and the check morphs into the dash and back on the morph spring (the `d` property transitions in Chromium; elsewhere it swaps). The square (not the hit area) presses to .95 in 100ms and springs back; its border eases over 160ms and darkens on hover while unchosen. **DOM:** the two glyph svgs are now one `checkbox-mark` (with `data-mark` of `check` or `indeterminate`) and a `checkbox-fill` element carries the colour, so the box's own background no longer turns primary.
638
694
 
695
+ ## CodeBlock
696
+
697
+ ```tsx
698
+ <CodeBlock value="npm install @seekalfred/primitives" onCopied={() => toast("Copied")} />
699
+ <CodeBlock value={longLine} wrap copyAriaLabel="Copy command" />
700
+ ```
701
+
702
+ A snippet of code with a copy button, composed from CopyButton (which owns the copy, its feedback and its announcement).
703
+ `value` is shown and copied; `children` replaces what is shown (highlighted markup) while `value` is still copied. By
704
+ default a long line scrolls sideways and the scroll area takes keyboard focus (named by `codeLabel`); `wrap` breaks
705
+ lines instead. `copyLabel` is the visible button text, `copyAriaLabel` its name (keep the label inside it),
706
+ `iconOnly` drops the text; `onCopied` and `onCopyError` pass through.
707
+
708
+ ## CommandPalette
709
+
710
+ ```tsx
711
+ const [open, setOpen] = useState(false);
712
+
713
+ <CommandPalette
714
+ items={[
715
+ { id: "home", group: "Pages", label: "Home", icon: <HomeIcon aria-hidden />, hint: "Go to" },
716
+ { id: "theme", group: "Actions", label: "Switch theme", keywords: ["dark", "light"], shortcut: "mod+shift+l" },
717
+ ]}
718
+ recent={[{ id: "posts", label: "Published posts", hint: "Go to" }]}
719
+ isOpen={open}
720
+ onOpenChange={setOpen}
721
+ onSelect={(item) => run(item.id)}
722
+ />
723
+ ```
724
+
725
+ A modal search-and-run list, built on Dialog, Input and ShortcutKeys. It listens for **Cmd+K or Ctrl+K**
726
+ on the document (`shortcut`, default `"mod+k"`, where `mod` is either key; `shortcut={false}` removes the
727
+ listener) and toggles itself. The chord's default is prevented, so a browser that binds Ctrl+K does not
728
+ take it. `useCommandPaletteShortcut(onTrigger, { shortcut, enabled })` is the same listener as a hook, for a
729
+ different chord or an app that opens the palette from its own state.
730
+
731
+ It is controlled with `isOpen` and `onOpenChange`, or uncontrolled with `defaultOpen`. `onSelect(item)` runs
732
+ after the palette has asked to close, so an action that opens another dialog finds focus already returned.
733
+
734
+ **Items.** `id` and `label` are required; `group` is the heading (groups appear in the order first seen, items
735
+ kept together), `icon` is decorative, `shortcut` draws key caps (a hint, it binds nothing), `hint` is plain
736
+ trailing text shown when there is no `shortcut`, `keywords` match without being shown, and `isDisabled` lists an
737
+ item that the arrow keys skip and a click ignores. The default filter needs every word of the query to appear in
738
+ the label, group or keywords, case-insensitively; `filter(item, query)` replaces it. `recent` is a group that leads
739
+ while the box is empty and gives way to the search once typing starts.
740
+
741
+ **Keyboard.** Arrow keys move the active row and wrap, Home and End jump to the first and last enabled row, Enter
742
+ chooses, Escape closes. Typing returns the highlight to the first match. Home and End are claimed from the text
743
+ caret on purpose. Hovering a row makes it active.
744
+
745
+ **Accessibility.** The search box is a `combobox` (`aria-expanded`, `aria-controls`, `aria-autocomplete="list"`,
746
+ `aria-haspopup="listbox"`) that keeps DOM focus; `aria-activedescendant` names the active `option` in the
747
+ `listbox`, and groups are `role="group"` labelled by their heading. The dialog is labelled by a visually hidden
748
+ heading (`label`, default "Search and commands"). Focus lands in the box on open, is trapped while open, and returns
749
+ to the element that had it on close. A polite live region announces the result count, and the empty state is a
750
+ `status`. The enter and exit motion is Dialog's and is off under `prefers-reduced-motion`; the row highlight
751
+ fades only under `motion-safe`.
752
+
753
+ **Copy.** Every string is a prop (`label`, `searchLabel`, `placeholder`, `recentLabel`, `emptyMessage`,
754
+ `resultsLabel`, `footer`); `footer={null}` removes the key-hint strip.
755
+
639
756
  ## ConfirmMorph
640
757
 
641
758
  ```tsx
@@ -746,6 +863,17 @@ it is decorative. Omitted from the DOM when absent; `false` (from
746
863
 
747
864
  New optional `icon` settles in once when the state first appears (small and transparent to rest over 480ms); a different icon component later swaps in place. A string message swaps as a whole when it changes and the message column follows its height on the smooth spring. The message now sits in an `empty-state-copy` frame.
748
865
 
866
+ ### Frame and alignment (EmptyState)
867
+
868
+ `frame="dashed"` draws the empty state as a block of its own: a dashed border, rounded corner and the muted surface,
869
+ for a placeholder where a chart or panel will go. `align="start"` puts icon, text and action at the leading edge
870
+ (the default `center` is the page-level empty state); with no icon or image the title also drops its top margin. Both
871
+ are off by default. The description keeps EmptyState's own measure (18rem).
872
+
873
+ ```tsx
874
+ <EmptyState frame="dashed" align="start" title="Connect Search Console" description="Rankings appear here once connected." action={<Button>Connect</Button>} />
875
+ ```
876
+
749
877
  ## ExpandingButtonGroup
750
878
 
751
879
  ```tsx
@@ -848,6 +976,18 @@ interruptible spring, so toggling quickly reverses from wherever the icon is. Th
848
976
  icons are decorative: put them in a button that carries the name and
849
977
  `aria-pressed`. Each is also exported on its own (`MenuIcon`, `HeartIcon`, …).
850
978
 
979
+ ## InfoTooltip
980
+
981
+ ```tsx
982
+ <InfoTooltip label="More about Visibility score" content="Share of answers that mention you." />
983
+ ```
984
+
985
+ A small "i" button that explains something in a tooltip: the compact `icon-sm` Button and a Tooltip. It is a real
986
+ button, so the help is reachable by keyboard as well as hover. `label` is its accessible name (default "More
987
+ information"); say what the help is about, because several triggers on a page otherwise read the same. `placement`,
988
+ `className` (button) and `tooltipClassName` (bubble) are the only other props. It is new to the library: the app's own
989
+ `InfoTooltip` wraps the app's Tooltip with a fixed label.
990
+
851
991
  ## Input and Textarea
852
992
 
853
993
  ```tsx
@@ -968,6 +1108,18 @@ The wiring, done for you:
968
1108
  - The error row has `role="alert"`.
969
1109
  - Screen readers get the plain copy. The animated words are `aria-hidden`.
970
1110
 
1111
+ **`isRequired`** makes the control `required` (announced by screen readers and
1112
+ checked by native form validation) and shows the required marker after the
1113
+ label: the same red asterisk as `PasswordField` and `Select`, `aria-hidden` so
1114
+ the name stays the label's text. Off by default. Without a `label` the control
1115
+ is still `required`, with no label to mark. A native `required` on its own still
1116
+ only sets the attribute and draws no marker, as before.
1117
+
1118
+ ```tsx
1119
+ <Input label="Email" isRequired />
1120
+ <Textarea label="Message" isRequired />
1121
+ ```
1122
+
971
1123
  Limits worth knowing:
972
1124
 
973
1125
  - **Do not pass `aria-label` together with `label`.** `aria-label` wins, so the
@@ -1218,6 +1370,27 @@ With `labelHidden` and no `showValue` nothing is printed above the track, so the
1218
1370
  its tone and no check lands. Use it for a timer-driven estimate that can reach
1219
1371
  100% before the work finishes.
1220
1372
 
1373
+ `target` draws an upright tick where the bar should be by now, on the same
1374
+ scale as `value` (clamped to `minValue`..`maxValue`), standing proud of the
1375
+ track with a 1px halo so it reads over the fill too. The tick is decorative:
1376
+ `targetLabel` (default "Target") and the formatted target ("Expected by now:
1377
+ 40%", formatted like the bar's own value) become the bar's accessible
1378
+ description, kept alongside any `aria-describedby` of yours. `showTargetLabel`
1379
+ also prints that text under the tick, centred on it and kept inside the bar
1380
+ near either end. No `target`, no tick and the same DOM as before.
1381
+
1382
+ `surface="inverse"` is for a bar on the inverted, foreground-coloured surface
1383
+ (a dark strip in the light theme): the track is the inverse text colour mixed
1384
+ into that surface, and the default fill, the tick and the copy take the inverse
1385
+ text colour. Explicit tones keep their colours.
1386
+
1387
+ ```tsx
1388
+ <Progress label="Organic sessions" value={340} maxValue={1000} target={450} targetLabel="Expected by now" labelHidden />
1389
+ <div className="bg-foreground p-4">
1390
+ <Progress label="Plan reaches" value={72} target={100} surface="inverse" labelHidden />
1391
+ </div>
1392
+ ```
1393
+
1221
1394
  ### Motion (Progress)
1222
1395
 
1223
1396
  The fill follows the value on the smooth spring (it was a 200ms ease) and the printed percentage counts to the new number on the same spring, so the number always matches the bar. When a default-toned bar completes it eases to the success colour (after 200ms) and a check lands beside the count; an explicit `tone` is never overridden, `showCompletion={false}` turns both off, and a bar that is full at mount shows its check with no motion. An indeterminate segment sweeps the track every 1.6s; under reduced motion it does not travel, and the fill spans the track and pulses slowly instead. A value that is not a percentage (a price) is shown as it is. A changed string label swaps as a whole.
@@ -1253,9 +1426,27 @@ a decorative `icon`. The group becomes a grid, so the caller sets the columns.
1253
1426
  together. The icon is `aria-hidden`.
1254
1427
  - Selected shows twice: a `primary-strong` edge (StatCard's selected pair) and
1255
1428
  the same dot as the default radio, so it never rests on colour alone.
1256
- - Keyboard is unchanged: one tab stop, arrow keys move and select. The focus
1257
- ring is drawn on the card, not the dot.
1258
- - `description` and `icon` are ignored by the default variant.
1429
+ - Keyboard is unchanged: one tab stop, arrow keys move and select.
1430
+ - **Focus** is the card's own 1px border turning dark, with no ring (the approved
1431
+ style, as on a text field and on `Checkbox variant="card"`). It was an outline ring
1432
+ on the card up to 0.7.4. Forced colours get the outline back.
1433
+ - `media` puts a thumbnail or preview edge to edge above the title (give an `<img>`
1434
+ its own `alt`). `description`, `icon` and `media` are ignored by the other variants.
1435
+
1436
+ ### Framed variant
1437
+
1438
+ `variant="framed"` keeps ordinary radios and draws one bordered frame around the
1439
+ row (or column, by `orientation`). Use it for a small inline picker that should read as one
1440
+ control. **Focus** is the frame's own border turning dark when any radio in it takes
1441
+ keyboard focus; the dot's ring is off in this variant. Radios can be wrapped in a
1442
+ `Tooltip` trigger.
1443
+
1444
+ ```tsx
1445
+ <RadioGroup variant="framed" orientation="horizontal" aria-label="AI provider" value={v} onChange={setV}>
1446
+ <Radio value="platform">Platform</Radio>
1447
+ <Radio value="azure">Azure</Radio>
1448
+ </RadioGroup>
1449
+ ```
1259
1450
 
1260
1451
  ### Migrating from the legacy RadioGroup
1261
1452
 
@@ -1382,6 +1573,15 @@ only — WCAG 1.4.1. The message is wired into `aria-describedby` for you.
1382
1573
  <Select label="Country" isInvalid errorMessage="Pick a country to continue." />
1383
1574
  ```
1384
1575
 
1576
+ **`isRequired`** is React Aria's prop: the hidden native select is `required`
1577
+ and the root gets `data-required`. It also shows the required marker after the
1578
+ label, the same asterisk as `Input`, `Textarea` and `PasswordField`, hidden from
1579
+ the trigger's accessible name.
1580
+
1581
+ ```tsx
1582
+ <Select label="Country" isRequired>…</Select>
1583
+ ```
1584
+
1385
1585
  ## Separator
1386
1586
 
1387
1587
  ```tsx
@@ -1530,6 +1730,17 @@ link, `onPress` a button, and `isSelected` with `onPress` a filter toggle with
1530
1730
  use `Badge`, whose `tone` already covers success, warning, danger, info,
1531
1731
  primary and neutral.
1532
1732
 
1733
+ `isLoading` keeps the card's box while its figure loads: the label stays (an
1734
+ empty `label` pulses in its line), the value and the context line (pass `sub`
1735
+ to keep it) become placeholders one line tall, the change pill pulses at its
1736
+ own size, the content is `aria-busy` and the root carries `data-loading`.
1737
+ Screen readers hear `loadingLabel` ("Loading") in place of the figure. A row of
1738
+ loading cards is exactly as tall as the row that replaces it.
1739
+
1740
+ ```tsx
1741
+ <StatCard label={metric?.name ?? ""} value={metric?.value ?? ""} sub="vs previous period" isLoading={!metric} />
1742
+ ```
1743
+
1533
1744
  ## Switch
1534
1745
 
1535
1746
  ```tsx
@@ -1642,6 +1853,72 @@ depends on row *data*, use `rowClassName`.
1642
1853
  | `striped` | zebra-stripes alternate rows |
1643
1854
  | `totalRow` | `(column) => ReactNode`; renders a real `<tfoot>` |
1644
1855
  | `Column.numeric` | tabular numerals, end-aligned by default |
1856
+ | `getSubRows` | `(row) => T[]`; rows that open to show the rows under them, with a disclosure button in the first cell (`aria-expanded`, named by the cell) |
1857
+ | `expandedKeys` / `defaultExpandedKeys` / `onExpandedChange` | the open rows, controlled or not; nothing is open by default |
1858
+ | `isGroupRow` | `(row) => boolean`; a group header row whose first cell spans the row. It only heads its rows: `onRowAction` does not fire for it and it shows no pointer |
1859
+ | `density` | `default` · `compact` — `py-2` rows in small type, the product's tight report tables |
1860
+ | `headerHidden` | hides the header row from sight and keeps it for assistive tech (key/value tables); widths still apply, sorting is off |
1861
+ | `keyboardModel` | `grid` (default) · `table` — a plain `role="table"` where Tab walks the links and buttons in the cells; no selection or `onRowAction`; `getSubRows` and `isGroupRow` work in it, with the disclosure button in the Tab order |
1862
+ | `Column.colSpan` | `(row) => number`; the cell spans that many columns on the rows that ask, and the covered cells are not drawn. Clamped to the columns left; ignored on a sticky first column and on group rows |
1863
+ | `renderExpandedRow` | `(row) => ReactNode`; a full-width detail row under an open row (nothing for null/undefined/false). Opened with a trigger of your own using `cell`'s `isExpanded` / `toggleExpanded`, or by `expandedKeys`; not selected, sorted or acted on |
1864
+ | `Column.headerAddon` | `ReactNode` beside the header label and outside the sort control: an info icon with a tooltip. Pressing it does not sort. It must name itself and be 24px |
1865
+ | `Column.highlighted` | tints the column's header and cells (`data-highlighted`); say why in words too |
1866
+ | `getRowProps` | `(row) => { "data-*": … }`; extra data attributes on each row (`data-testid`). Library attributes win a clash. No `aria-*` (React Aria drops them on a grid row) and no `onClick` (use `onRowAction`) |
1867
+
1868
+ ### Spans, detail rows, header add-ons, highlighted columns, row attributes
1869
+
1870
+ All five are off until you use them; a table that uses none renders as before.
1871
+
1872
+ ```tsx
1873
+ const columns = [
1874
+ column({ id: "keyword", header: "Keyword", accessor: (r) => r.keyword }),
1875
+ column({
1876
+ id: "clicks",
1877
+ header: "Clicks",
1878
+ accessor: (r) => r.clicks,
1879
+ // One message across clicks and CTR on a row with no impressions.
1880
+ colSpan: (r) => (r.impressions === 0 ? 2 : 1),
1881
+ cell: ({ row, value }) => (row.impressions === 0 ? "No impressions yet" : value),
1882
+ }),
1883
+ column({ id: "ctr", header: "CTR", accessor: (r) => r.ctr }),
1884
+ column({
1885
+ id: "overall",
1886
+ header: "Overall",
1887
+ accessor: (r) => r.overall,
1888
+ sortable: true,
1889
+ headerAddon: (
1890
+ <TooltipTrigger>
1891
+ <AriaButton aria-label="About Overall">i</AriaButton>
1892
+ <Tooltip>The blend of every metric.</Tooltip>
1893
+ </TooltipTrigger>
1894
+ ),
1895
+ highlighted: true,
1896
+ }),
1897
+ ];
1898
+
1899
+ <Table
1900
+ aria-label="Keywords"
1901
+ data={rows}
1902
+ columns={columns}
1903
+ getRowId={(r) => r.id}
1904
+ getRowProps={(r) => ({ "data-testid": `keyword-row-${r.id}` })}
1905
+ renderExpandedRow={(r) => <History keyword={r} />}
1906
+ />;
1907
+ ```
1908
+
1909
+ **`renderExpandedRow` is not `getSubRows`.** Sub-rows are data rows with the
1910
+ table's columns, selection and actions, drawn with a disclosure button. A detail
1911
+ row is one cell spanning every column, holds anything, and is never selected,
1912
+ sorted, counted or acted on. Both read the same `expandedKeys` set, so a row
1913
+ with both opens both: the detail row first, directly under its row, then the
1914
+ sub-rows. There is no built-in button for a detail row, so the first cell's
1915
+ indent does not change: open it from a button in a cell (`aria-expanded`,
1916
+ `toggleExpanded`) or from `expandedKeys`.
1917
+
1918
+ **`Column.colSpan` leaves the header alone.** The header keeps one cell per
1919
+ column, so columns stay named and sortable; only body rows span. In the grid the
1920
+ spans, the checkbox column and the chevron column always add up to the table's
1921
+ columns.
1645
1922
 
1646
1923
  ### Using it in alfred-ui
1647
1924
 
@@ -1682,9 +1959,11 @@ not worth a palette entry. If you want it exactly, override
1682
1959
  | `meta.width` / `<colgroup>` / width classes | `Column.width` | one mechanism instead of three |
1683
1960
  | `onRowClick(row)` | `onRowAction(row)` | also fires on Enter |
1684
1961
  | `rowClassName` | unchanged | |
1962
+ | `renderExpandedRow(row)` (`DataTableBody`) + `row.toggleExpanded()` | `renderExpandedRow(row)` + `cell`'s `toggleExpanded()` / `isExpanded` | the detail row is one full-width cell; the toggle button stays yours |
1963
+ | `rowProps(row)` (`data-*`) | `getRowProps(row)` | `data-*` only |
1685
1964
  | `rowIdField="keywordId"` | `getRowId={(r) => r.keywordId}` | |
1686
1965
 
1687
- Row expansion, sticky headers, loading skeletons, pinned columns, the total
1966
+ Sticky headers, loading skeletons, pinned columns, the total
1688
1967
  row, filtering, column visibility and CSV export are **not ported** — each had
1689
1968
  one consumer or none across the six legacy tables, and they belong to a future
1690
1969
  `DataGrid`.
@@ -1826,6 +2105,38 @@ only, so 38 of 67 calls passed a variant that could not say "this worked".
1826
2105
 
1827
2106
  A toast rises 16px from below and grows from .96 on the morph spring while it fades in over 240ms. Pressing its close control sinks it out (8px, .97, 180ms) before it leaves the queue; the control dips to .96 under a press and springs back. Reduced motion fades only. React Aria 1.21 unmounts a closed toast at once and has no exit of its own, so a toast that leaves by its timeout or by `toastQueue.close` still leaves without the exit. Not ported: swipe-to-dismiss, the status icon.
1828
2107
 
2108
+ ## ToggleButton
2109
+
2110
+ ```tsx
2111
+ <ToggleButton isSelected={unread} onChange={setUnread}>Has new reply</ToggleButton>
2112
+ <ToggleButton iconOnly aria-label="Primary key"><KeyIcon /></ToggleButton>
2113
+
2114
+ <ToggleButtonGroup aria-label="Range" selectionMode="single" disallowEmptySelection defaultSelectedKeys={["7d"]}>
2115
+ <ToggleButton id="7d">7 days</ToggleButton>
2116
+ <ToggleButton id="30d">30 days</ToggleButton>
2117
+ </ToggleButtonGroup>
2118
+ ```
2119
+
2120
+ A button that stays pressed, on React Aria's `ToggleButton`: a real `<button>` with
2121
+ `aria-pressed`. Controlled with `isSelected` and `onChange(boolean)`, or uncontrolled
2122
+ with `defaultSelected`. `size` is `sm` (default) or `md`; `iconOnly` makes a square button
2123
+ that holds a glyph and **must** carry an `aria-label` (name the control, not its state).
2124
+
2125
+ - **Selected is not colour alone.** A tint, a `primary-strong` edge and a check mark drawn in
2126
+ `currentColor`, inside the button's own box in the corner, so it never shifts the layout
2127
+ and is not clipped by a table row. An icon-only toggle carries it too.
2128
+ - **Focus** is the button's own 1px border turning dark, no ring. Forced colours get an outline.
2129
+ - It composes with `TooltipTrigger` and `isDisabled` (a disabled toggle keeps its mark but loses the selected tint and edge).
2130
+ - `ToggleButtonGroup` shares a selection and is always a toolbar of `aria-pressed` buttons:
2131
+ one tab stop, arrow keys move focus, Space or Enter toggles (selection never follows focus).
2132
+ `selectionMode="single"` (default) keeps one button pressed (pressing another moves it; add
2133
+ `disallowEmptySelection` to forbid clearing) and Tab enters on the pressed button;
2134
+ `"multiple"` allows any number. It is not `role="radiogroup"`: React Aria's single mode has
2135
+ radio roles but arrows that do not check, which is not the APG radio pattern. For a real
2136
+ radio group use `RadioGroup`. Children need an `id`, their key in `selectedKeys`.
2137
+ - Not a `Button` with `aria-pressed`, a `ChipGroup` or `FilterBar`: those are collections of
2138
+ options with their own input model. This is one on/off question.
2139
+
1829
2140
  ## Tooltip
1830
2141
 
1831
2142
  ```tsx
@@ -24,7 +24,7 @@ import { AriaRole, ComponentPropsWithoutRef, ReactNode, SVGProps } from "react";
24
24
  * problems rather than porting them.
25
25
  */
26
26
  export declare const alertVariants: (props?: ({
27
- variant?: "destructive" | "success" | "warning" | "default" | "info" | null | undefined;
27
+ variant?: "default" | "success" | "warning" | "info" | "destructive" | null | undefined;
28
28
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
29
29
  /**
30
30
  * The icon's colour: the state's own hue on every variant. Inside an Alert the
@@ -35,7 +35,7 @@ export declare const alertVariants: (props?: ({
35
35
  * banner of the app's own) can show a tone the same way the alert does.
36
36
  */
37
37
  export declare const alertIconVariants: (props?: ({
38
- variant?: "destructive" | "success" | "warning" | "default" | "info" | null | undefined;
38
+ variant?: "default" | "success" | "warning" | "info" | "destructive" | null | undefined;
39
39
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
40
40
  /**
41
41
  * The built-in glyph of each variant, drawn with currentColor so the icon
@@ -213,7 +213,7 @@ const Alert = forwardRef(function Alert({ className, variant, icon: iconProp, ti
213
213
  ref: content,
214
214
  children: [title === void 0 ? null : /* @__PURE__ */ jsx(Title, {
215
215
  "data-slot": "alert-title",
216
- className: "m-0 mb-1 text-sm leading-(--leading-normal) font-medium",
216
+ className: "m-0 mb-1 font-display text-sm leading-(--leading-normal) font-medium",
217
217
  children: /* @__PURE__ */ jsx(Swappable, {
218
218
  value: title,
219
219
  children: title
@@ -1 +1 @@
1
- {"version":3,"file":"alert.js","names":["AriaButton"],"sources":["../../src/alert/alert.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n forwardRef,\n isValidElement,\n useCallback,\n useEffect,\n useRef,\n useState,\n type AriaRole,\n type ComponentPropsWithoutRef,\n type ReactNode,\n type SVGProps,\n} from \"react\";\n\nimport { Button as AriaButton } from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useHeightFollow } from \"../lib/height-follow.js\";\nimport { prefersReducedMotion, useExitBackstop, useIsomorphicLayoutEffect } from \"../lib/motion.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * `info` is `default` said by name: a call site that means \"information\" should\n * not depend on `default` happening to look like it. Every per-variant map below\n * derives `info` here, from its `default`, so the two cannot drift.\n */\nfunction withInfo<T extends { default: unknown }>(map: T): T & { info: T[\"default\"] } {\n return { ...map, info: map.default };\n}\n\n/**\n * Alert, ported from the legacy shadcn component.\n *\n * There is no React Aria base: RAC ships no static inline alert (its Toast\n * API is transient and queue-driven), so this is a styled div with\n * role=\"alert\". Because it is not RAC-backed, its props follow\n * ARCHITECTURE section 7 rule 1 -- VariantProps & ComponentPropsWithoutRef --\n * rather than the Button's Omit-from-React-Aria departure, which exists only\n * to stop RAC-ignored props being advertised.\n *\n * Every string below is a STATIC literal. Tailwind scans source text, so a\n * class assembled at runtime is invisible to it and would never be emitted.\n *\n * THE LAYOUT IS THE BIGGEST CHANGE. The legacy component positioned its icon\n * with `[&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4`, pushed the text over\n * with `[&>svg~*]:pl-7`, and nudged it back with `[&>svg+div]:translate-y-[-3px]`.\n * All three are physical properties or magic numbers: `left` and `pl` do not\n * mirror in RTL, so the icon stayed left while the text indented the wrong\n * side, and `translate-y-[-3px]` is a bracket value that `check:hardcoding`\n * rejects outright. Flex with an explicit `icon` prop deletes all three\n * problems rather than porting them.\n */\nexport const alertVariants = cva(\n [\n // box-border is not boilerplate. Tailwind Preflight sets box-sizing\n // globally and the legacy app had it; this library's reset is OPT-IN, so\n // a consumer may load none and the browser default content-box applies.\n // w-full is width:100%, so the padding and border are then ADDED to it\n // and the alert overflows its container by exactly 46px at comfortable\n // density. Measured, not theorised: 346px inside a 300px parent.\n \"box-border flex w-full items-start gap-(--density-control-gap)\",\n \"rounded-panel border-(length:--border-width) p-(--density-control-pad-md)\",\n // font-sans is load-bearing. The library does not style the consumer's\n // page, so anything that does not set its own face inherits the host's\n // default -- which is a serif in an unstyled document. The accordion\n // panel shipped exactly that way and it was caught by eye, not by a test.\n \"font-sans text-sm leading-(--leading-normal)\",\n ],\n {\n variants: {\n variant: withInfo({\n default: \"bg-surface text-foreground border-border\",\n // The status variants paint only their icon; the card stays neutral\n // (dark title, grey description, light border), like the reference\n // Sync status alert. See alertIconVariants.\n success: \"bg-surface text-foreground border-border\",\n warning: \"bg-surface text-foreground border-border\",\n // destructive-STRONG, not destructive. The fill token is 4.04:1 on\n // the dark page; this role exists so the text and border clear the\n // 4.5 floor in both themes. The legacy `border-destructive/50` was\n // also an opacity modifier, which cannot be contrast-checked at all\n // because the result depends on whatever sits behind it.\n destructive: \"bg-surface text-destructive-strong border-destructive-strong\",\n }),\n },\n defaultVariants: { variant: \"default\" },\n },\n);\n\n/**\n * The icon's colour: the state's own hue on every variant. Inside an Alert the\n * destructive red is also the whole alert's text colour, so it changes nothing\n * there; outside one it is what colours the icon.\n *\n * Exported with the status icons so a notice that is not an `Alert` (a row, a\n * banner of the app's own) can show a tone the same way the alert does.\n */\nexport const alertIconVariants = cva(\n \"flex h-lh shrink-0 items-center [&_svg]:block [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n {\n variants: {\n variant: withInfo({\n default: \"text-info-strong\",\n success: \"text-success-strong\",\n warning: \"text-warning-strong\",\n destructive: \"text-destructive-strong\",\n }),\n },\n defaultVariants: { variant: \"default\" },\n },\n);\n\n/**\n * The built-in glyph of each variant, drawn with currentColor so the icon\n * wrapper's colour paints it. Each is its own component so a variant change\n * crossfades the icon (see iconKey).\n *\n * Exported as AlertInfoIcon, AlertSuccessIcon, AlertWarningIcon and\n * AlertDestructiveIcon. They are decorative (`aria-hidden`) and `--icon-sm`\n * by default; extra props, such as a `className` with a size, land on the svg.\n * Given an `aria-label` or `aria-labelledby` (with `role=\"img\"`), an icon is\n * no longer hidden, and an explicit `aria-hidden` always wins.\n */\ntype StatusIconProps = Omit<SVGProps<SVGSVGElement>, \"children\">;\nfunction StatusGlyph({\n children,\n className,\n ...props\n}: StatusIconProps & { children: ReactNode }) {\n const labelled = props[\"aria-label\"] !== undefined || props[\"aria-labelledby\"] !== undefined;\n return (\n <svg\n // Before the spread, so a caller's own aria-hidden wins; a labelled icon\n // is meant to be read, so it is not hidden by default.\n aria-hidden={labelled ? undefined : \"true\"}\n {...props}\n // Sized on its own, so an exported icon outside an alert is not 0x0; the\n // same --icon-sm the alert's icon slot gives an unsized svg. A caller's\n // size class wins.\n className={cn(\"size-(--icon-sm)\", className)}\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n {children}\n </svg>\n );\n}\nfunction InfoGlyph(props: StatusIconProps) {\n return (\n <StatusGlyph {...props}>\n <circle cx=\"8\" cy=\"8\" r=\"6.25\" />\n <path d=\"M8 7.25v3.5M8 5h.01\" />\n </StatusGlyph>\n );\n}\nfunction SuccessGlyph(props: StatusIconProps) {\n return (\n <StatusGlyph {...props}>\n <circle cx=\"8\" cy=\"8\" r=\"6.25\" />\n <path d=\"m5.25 8.25 1.9 1.9 3.6-3.9\" />\n </StatusGlyph>\n );\n}\nfunction WarningGlyph(props: StatusIconProps) {\n return (\n <StatusGlyph {...props}>\n <path d=\"M6.9 2.6 1.9 11.4a1.2 1.2 0 0 0 1 1.8h10.2a1.2 1.2 0 0 0 1-1.8L9.1 2.6a1.2 1.2 0 0 0-2.2 0Z\" />\n <path d=\"M8 6.4v2.6M8 11.2h.01\" />\n </StatusGlyph>\n );\n}\nfunction DestructiveGlyph(props: StatusIconProps) {\n return (\n <StatusGlyph {...props}>\n <circle cx=\"8\" cy=\"8\" r=\"6.25\" />\n <path d=\"m6 6 4 4M10 6l-4 4\" />\n </StatusGlyph>\n );\n}\nexport {\n InfoGlyph as AlertInfoIcon,\n SuccessGlyph as AlertSuccessIcon,\n WarningGlyph as AlertWarningIcon,\n DestructiveGlyph as AlertDestructiveIcon,\n};\nexport type { StatusIconProps as AlertStatusIconProps };\n\nconst statusGlyphs = withInfo({\n default: <InfoGlyph />,\n success: <SuccessGlyph />,\n warning: <WarningGlyph />,\n destructive: <DestructiveGlyph />,\n});\n\n/**\n * An inline status message.\n *\n * Announced by assistive technology the moment it renders, via `role=\"alert\"`.\n * That makes it the wrong default for content that is present on load and\n * merely styled as a notice -- pass `role=\"note\"` for those, or\n * `role=\"status\"` for an outcome that should be announced politely.\n *\n * @example\n * <Alert variant=\"destructive\" icon={<WarningIcon aria-hidden />} title=\"Save failed\">\n * Your changes were not saved.\n * </Alert>\n */\nexport interface AlertProps\n // The native `title` attribute is omitted so this component can use the\n // name for its heading. That attribute renders a browser tooltip, which is\n // not keyboard reachable and is not announced reliably -- on a role=\"alert\"\n // it is worth nothing, and silently shadowing it with a ReactNode would be\n // a type error waiting for the first consumer who tried both.\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\">, VariantProps<typeof alertVariants> {\n /**\n * Leading glyph. Mark it `aria-hidden` -- the message text is the\n * announcement, and an unhidden graphic makes the alert announce twice.\n *\n * Left out, the alert shows its variant's own icon (info, check, warning\n * triangle, cross), coloured for the state and centred on the first line of\n * text. Pass `null` for no icon, or your own glyph to replace it.\n *\n * A prop rather than a positioned child: the legacy `[&>svg]` selectors\n * only worked when the icon was the first element, and broke in RTL.\n */\n icon?: ReactNode;\n /**\n * Optional heading. Replaces the legacy `AlertTitle` part.\n *\n * Omit it for a single-line alert; most call sites do.\n */\n title?: ReactNode;\n /**\n * Heading level for {@link AlertProps.title}.\n *\n * The legacy `AlertTitle` was always an `<h5>`, which breaks the document\n * outline anywhere the alert is not nested five deep.\n *\n * @default 3\n */\n headingLevel?: 2 | 3 | 4 | 5 | 6;\n /**\n * Whether the alert is shown. Controlled: hiding it collapses its height and\n * fades it out, and showing it again grows it back. An alert that is open on\n * first render does not animate. Leave it out for an alert that is always there.\n */\n open?: boolean;\n /**\n * Adds a Dismiss button. Pressed, a controlled alert (`open` given) only calls\n * this and waits to be told `open={false}`; an uncontrolled alert (no `open`)\n * collapses first and then calls it.\n */\n onDismiss?: () => void;\n /** The Dismiss button's accessible name. @default \"Dismiss\" */\n dismissLabel?: string;\n /**\n * How assistive technology treats the message.\n *\n * - `alert`: announced assertively the moment it renders. For an error or a\n * warning that appears after the user did something.\n * - `status`: a live region announced politely, after what the user is\n * hearing. For an outcome reported after an action (\"Saved\").\n * - `note`: not live. For a notice that is simply there when the page loads;\n * an alert there would interrupt the page's own announcement.\n *\n * Typed as any ARIA role, as the inherited `role` was, so existing callers\n * still compile; those three are the ones an inline notice should use.\n *\n * @default \"alert\"\n */\n role?: \"alert\" | \"status\" | \"note\" | AriaRole;\n}\n\n/** A string title or description swaps as a whole when its text changes. */\nfunction Swappable({ value, children }: { value: ReactNode; children: ReactNode }) {\n if (typeof value !== \"string\" && typeof value !== \"number\") return <>{children}</>;\n return (\n <SwapText swapKey={String(value)} className=\"block\">\n {children}\n </SwapText>\n );\n}\n\n/** The name an icon swaps under: a different icon component crossfades; the same one re-rendering stays still. */\nfunction iconKey(icon: ReactNode) {\n if (!isValidElement(icon)) return \"icon\";\n const type = icon.type as string | { displayName?: string; name?: string };\n return typeof type === \"string\" ? type : (type.displayName ?? type.name ?? \"icon\");\n}\n\nfunction DismissGlyph() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n aria-hidden=\"true\"\n className=\"size-4\"\n >\n <path d=\"M4 4l8 8M12 4l-8 8\" />\n </svg>\n );\n}\n\nexport const Alert = forwardRef<HTMLDivElement, AlertProps>(function Alert(\n {\n className,\n variant,\n icon: iconProp,\n title,\n headingLevel = 3,\n children,\n open,\n onDismiss,\n dismissLabel = \"Dismiss\",\n role = \"alert\",\n ...props\n },\n ref,\n) {\n const Title = `h${headingLevel}` as const;\n const icon = iconProp === undefined ? statusGlyphs[variant ?? \"default\"] : iconProp;\n const frame = useRef<HTMLDivElement>(null);\n const content = useRef<HTMLDivElement>(null);\n const text = typeof children === \"string\" || typeof children === \"number\" ? String(children) : \"\";\n useHeightFollow(\n frame,\n content,\n `${typeof title === \"string\" ? title : \"\"}\\n${children === undefined ? \"\" : \"details\"}\\n${text}`,\n );\n\n // Presence. Without `open` or `onDismiss` the alert is simply there.\n const managed = open !== undefined || onDismiss !== undefined;\n const [dismissed, setDismissed] = useState(false);\n const wanted = open ?? !dismissed;\n const [rendered, setRendered] = useState(wanted);\n const [shown, setShown] = useState(wanted);\n if (wanted && !rendered) setRendered(true);\n if (!wanted && shown) setShown(false);\n useEffect(() => {\n // A newly shown alert mounts collapsed and opens a frame later, so it grows rather than appears.\n if (wanted && rendered && !shown) {\n const frameId = requestAnimationFrame(() => setShown(true));\n return () => cancelAnimationFrame(frameId);\n }\n }, [wanted, rendered, shown]);\n const leaving = rendered && !wanted;\n const finish = useCallback(() => {\n setRendered(false);\n if (open === undefined) onDismiss?.();\n }, [onDismiss, open]);\n useExitBackstop(leaving, finish, 900);\n useIsomorphicLayoutEffect(() => {\n if (leaving && prefersReducedMotion()) finish();\n }, [leaving, finish]);\n if (managed && !rendered) return null;\n\n const alert = (\n <div\n ref={ref}\n {...props}\n role={role}\n data-slot=\"alert\"\n className={cn(alertVariants({ variant }), className)}\n >\n {/*\n The icon is decorative or labelled by the consumer; this wrapper adds\n no name. It keeps the icon from shrinking and sizes a bare svg to the\n icon scale, unless the consumer sized it themselves.\n */}\n {icon === null || icon === false ? null : (\n <span data-slot=\"alert-icon\" className={alertIconVariants({ variant })}>\n <SwapText swapKey={iconKey(icon)} motion=\"glyph\" className=\"grid place-items-center\">\n {icon}\n </SwapText>\n </span>\n )}\n {/* The copy column follows its content's height, so longer or shorter text never snaps the alert. */}\n <div ref={frame} data-slot=\"alert-body\" className=\"height-follow min-w-0 flex-1\">\n <div ref={content}>\n {title === undefined ? null : (\n <Title\n data-slot=\"alert-title\"\n className=\"m-0 mb-1 text-sm leading-(--leading-normal) font-medium\"\n >\n <Swappable value={title}>{title}</Swappable>\n </Title>\n )}\n {children === undefined ? null : (\n <div\n data-slot=\"alert-description\"\n className={title === undefined ? undefined : \"text-xs leading-(--leading-normal) text-text-secondary\"}\n >\n <Swappable value={children}>{children}</Swappable>\n </div>\n )}\n </div>\n </div>\n {onDismiss ? (\n <AriaButton\n data-slot=\"alert-dismiss\"\n aria-label={typeof title === \"string\" ? `${dismissLabel}: ${title}` : dismissLabel}\n onPress={() => (open === undefined ? setDismissed(true) : onDismiss())}\n className=\"calendar-nav-motion -my-1 -me-1.5 grid size-7 shrink-0 cursor-pointer place-items-center rounded-full border-0 bg-transparent text-muted-foreground outline-none data-[hovered]:bg-muted data-[hovered]:text-foreground data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width) data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring)\"\n >\n <DismissGlyph />\n </AriaButton>\n ) : null}\n </div>\n );\n if (!managed) return alert;\n return (\n <div\n data-slot=\"alert-presence\"\n data-shown={shown || undefined}\n onTransitionEnd={(event) => {\n if (leaving && event.target === event.currentTarget && event.propertyName === \"grid-template-rows\")\n finish();\n }}\n className=\"alert-presence\"\n >\n <div>{alert}</div>\n </div>\n );\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"alert.js","names":["AriaButton"],"sources":["../../src/alert/alert.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n forwardRef,\n isValidElement,\n useCallback,\n useEffect,\n useRef,\n useState,\n type AriaRole,\n type ComponentPropsWithoutRef,\n type ReactNode,\n type SVGProps,\n} from \"react\";\n\nimport { Button as AriaButton } from \"react-aria-components\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useHeightFollow } from \"../lib/height-follow.js\";\nimport { prefersReducedMotion, useExitBackstop, useIsomorphicLayoutEffect } from \"../lib/motion.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * `info` is `default` said by name: a call site that means \"information\" should\n * not depend on `default` happening to look like it. Every per-variant map below\n * derives `info` here, from its `default`, so the two cannot drift.\n */\nfunction withInfo<T extends { default: unknown }>(map: T): T & { info: T[\"default\"] } {\n return { ...map, info: map.default };\n}\n\n/**\n * Alert, ported from the legacy shadcn component.\n *\n * There is no React Aria base: RAC ships no static inline alert (its Toast\n * API is transient and queue-driven), so this is a styled div with\n * role=\"alert\". Because it is not RAC-backed, its props follow\n * ARCHITECTURE section 7 rule 1 -- VariantProps & ComponentPropsWithoutRef --\n * rather than the Button's Omit-from-React-Aria departure, which exists only\n * to stop RAC-ignored props being advertised.\n *\n * Every string below is a STATIC literal. Tailwind scans source text, so a\n * class assembled at runtime is invisible to it and would never be emitted.\n *\n * THE LAYOUT IS THE BIGGEST CHANGE. The legacy component positioned its icon\n * with `[&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4`, pushed the text over\n * with `[&>svg~*]:pl-7`, and nudged it back with `[&>svg+div]:translate-y-[-3px]`.\n * All three are physical properties or magic numbers: `left` and `pl` do not\n * mirror in RTL, so the icon stayed left while the text indented the wrong\n * side, and `translate-y-[-3px]` is a bracket value that `check:hardcoding`\n * rejects outright. Flex with an explicit `icon` prop deletes all three\n * problems rather than porting them.\n */\nexport const alertVariants = cva(\n [\n // box-border is not boilerplate. Tailwind Preflight sets box-sizing\n // globally and the legacy app had it; this library's reset is OPT-IN, so\n // a consumer may load none and the browser default content-box applies.\n // w-full is width:100%, so the padding and border are then ADDED to it\n // and the alert overflows its container by exactly 46px at comfortable\n // density. Measured, not theorised: 346px inside a 300px parent.\n \"box-border flex w-full items-start gap-(--density-control-gap)\",\n \"rounded-panel border-(length:--border-width) p-(--density-control-pad-md)\",\n // font-sans is load-bearing. The library does not style the consumer's\n // page, so anything that does not set its own face inherits the host's\n // default -- which is a serif in an unstyled document. The accordion\n // panel shipped exactly that way and it was caught by eye, not by a test.\n \"font-sans text-sm leading-(--leading-normal)\",\n ],\n {\n variants: {\n variant: withInfo({\n default: \"bg-surface text-foreground border-border\",\n // The status variants paint only their icon; the card stays neutral\n // (dark title, grey description, light border), like the reference\n // Sync status alert. See alertIconVariants.\n success: \"bg-surface text-foreground border-border\",\n warning: \"bg-surface text-foreground border-border\",\n // destructive-STRONG, not destructive. The fill token is 4.04:1 on\n // the dark page; this role exists so the text and border clear the\n // 4.5 floor in both themes. The legacy `border-destructive/50` was\n // also an opacity modifier, which cannot be contrast-checked at all\n // because the result depends on whatever sits behind it.\n destructive: \"bg-surface text-destructive-strong border-destructive-strong\",\n }),\n },\n defaultVariants: { variant: \"default\" },\n },\n);\n\n/**\n * The icon's colour: the state's own hue on every variant. Inside an Alert the\n * destructive red is also the whole alert's text colour, so it changes nothing\n * there; outside one it is what colours the icon.\n *\n * Exported with the status icons so a notice that is not an `Alert` (a row, a\n * banner of the app's own) can show a tone the same way the alert does.\n */\nexport const alertIconVariants = cva(\n \"flex h-lh shrink-0 items-center [&_svg]:block [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-(--icon-sm)\",\n {\n variants: {\n variant: withInfo({\n default: \"text-info-strong\",\n success: \"text-success-strong\",\n warning: \"text-warning-strong\",\n destructive: \"text-destructive-strong\",\n }),\n },\n defaultVariants: { variant: \"default\" },\n },\n);\n\n/**\n * The built-in glyph of each variant, drawn with currentColor so the icon\n * wrapper's colour paints it. Each is its own component so a variant change\n * crossfades the icon (see iconKey).\n *\n * Exported as AlertInfoIcon, AlertSuccessIcon, AlertWarningIcon and\n * AlertDestructiveIcon. They are decorative (`aria-hidden`) and `--icon-sm`\n * by default; extra props, such as a `className` with a size, land on the svg.\n * Given an `aria-label` or `aria-labelledby` (with `role=\"img\"`), an icon is\n * no longer hidden, and an explicit `aria-hidden` always wins.\n */\ntype StatusIconProps = Omit<SVGProps<SVGSVGElement>, \"children\">;\nfunction StatusGlyph({\n children,\n className,\n ...props\n}: StatusIconProps & { children: ReactNode }) {\n const labelled = props[\"aria-label\"] !== undefined || props[\"aria-labelledby\"] !== undefined;\n return (\n <svg\n // Before the spread, so a caller's own aria-hidden wins; a labelled icon\n // is meant to be read, so it is not hidden by default.\n aria-hidden={labelled ? undefined : \"true\"}\n {...props}\n // Sized on its own, so an exported icon outside an alert is not 0x0; the\n // same --icon-sm the alert's icon slot gives an unsized svg. A caller's\n // size class wins.\n className={cn(\"size-(--icon-sm)\", className)}\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n {children}\n </svg>\n );\n}\nfunction InfoGlyph(props: StatusIconProps) {\n return (\n <StatusGlyph {...props}>\n <circle cx=\"8\" cy=\"8\" r=\"6.25\" />\n <path d=\"M8 7.25v3.5M8 5h.01\" />\n </StatusGlyph>\n );\n}\nfunction SuccessGlyph(props: StatusIconProps) {\n return (\n <StatusGlyph {...props}>\n <circle cx=\"8\" cy=\"8\" r=\"6.25\" />\n <path d=\"m5.25 8.25 1.9 1.9 3.6-3.9\" />\n </StatusGlyph>\n );\n}\nfunction WarningGlyph(props: StatusIconProps) {\n return (\n <StatusGlyph {...props}>\n <path d=\"M6.9 2.6 1.9 11.4a1.2 1.2 0 0 0 1 1.8h10.2a1.2 1.2 0 0 0 1-1.8L9.1 2.6a1.2 1.2 0 0 0-2.2 0Z\" />\n <path d=\"M8 6.4v2.6M8 11.2h.01\" />\n </StatusGlyph>\n );\n}\nfunction DestructiveGlyph(props: StatusIconProps) {\n return (\n <StatusGlyph {...props}>\n <circle cx=\"8\" cy=\"8\" r=\"6.25\" />\n <path d=\"m6 6 4 4M10 6l-4 4\" />\n </StatusGlyph>\n );\n}\nexport {\n InfoGlyph as AlertInfoIcon,\n SuccessGlyph as AlertSuccessIcon,\n WarningGlyph as AlertWarningIcon,\n DestructiveGlyph as AlertDestructiveIcon,\n};\nexport type { StatusIconProps as AlertStatusIconProps };\n\nconst statusGlyphs = withInfo({\n default: <InfoGlyph />,\n success: <SuccessGlyph />,\n warning: <WarningGlyph />,\n destructive: <DestructiveGlyph />,\n});\n\n/**\n * An inline status message.\n *\n * Announced by assistive technology the moment it renders, via `role=\"alert\"`.\n * That makes it the wrong default for content that is present on load and\n * merely styled as a notice -- pass `role=\"note\"` for those, or\n * `role=\"status\"` for an outcome that should be announced politely.\n *\n * @example\n * <Alert variant=\"destructive\" icon={<WarningIcon aria-hidden />} title=\"Save failed\">\n * Your changes were not saved.\n * </Alert>\n */\nexport interface AlertProps\n // The native `title` attribute is omitted so this component can use the\n // name for its heading. That attribute renders a browser tooltip, which is\n // not keyboard reachable and is not announced reliably -- on a role=\"alert\"\n // it is worth nothing, and silently shadowing it with a ReactNode would be\n // a type error waiting for the first consumer who tried both.\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\">, VariantProps<typeof alertVariants> {\n /**\n * Leading glyph. Mark it `aria-hidden` -- the message text is the\n * announcement, and an unhidden graphic makes the alert announce twice.\n *\n * Left out, the alert shows its variant's own icon (info, check, warning\n * triangle, cross), coloured for the state and centred on the first line of\n * text. Pass `null` for no icon, or your own glyph to replace it.\n *\n * A prop rather than a positioned child: the legacy `[&>svg]` selectors\n * only worked when the icon was the first element, and broke in RTL.\n */\n icon?: ReactNode;\n /**\n * Optional heading. Replaces the legacy `AlertTitle` part.\n *\n * Omit it for a single-line alert; most call sites do.\n */\n title?: ReactNode;\n /**\n * Heading level for {@link AlertProps.title}.\n *\n * The legacy `AlertTitle` was always an `<h5>`, which breaks the document\n * outline anywhere the alert is not nested five deep.\n *\n * @default 3\n */\n headingLevel?: 2 | 3 | 4 | 5 | 6;\n /**\n * Whether the alert is shown. Controlled: hiding it collapses its height and\n * fades it out, and showing it again grows it back. An alert that is open on\n * first render does not animate. Leave it out for an alert that is always there.\n */\n open?: boolean;\n /**\n * Adds a Dismiss button. Pressed, a controlled alert (`open` given) only calls\n * this and waits to be told `open={false}`; an uncontrolled alert (no `open`)\n * collapses first and then calls it.\n */\n onDismiss?: () => void;\n /** The Dismiss button's accessible name. @default \"Dismiss\" */\n dismissLabel?: string;\n /**\n * How assistive technology treats the message.\n *\n * - `alert`: announced assertively the moment it renders. For an error or a\n * warning that appears after the user did something.\n * - `status`: a live region announced politely, after what the user is\n * hearing. For an outcome reported after an action (\"Saved\").\n * - `note`: not live. For a notice that is simply there when the page loads;\n * an alert there would interrupt the page's own announcement.\n *\n * Typed as any ARIA role, as the inherited `role` was, so existing callers\n * still compile; those three are the ones an inline notice should use.\n *\n * @default \"alert\"\n */\n role?: \"alert\" | \"status\" | \"note\" | AriaRole;\n}\n\n/** A string title or description swaps as a whole when its text changes. */\nfunction Swappable({ value, children }: { value: ReactNode; children: ReactNode }) {\n if (typeof value !== \"string\" && typeof value !== \"number\") return <>{children}</>;\n return (\n <SwapText swapKey={String(value)} className=\"block\">\n {children}\n </SwapText>\n );\n}\n\n/** The name an icon swaps under: a different icon component crossfades; the same one re-rendering stays still. */\nfunction iconKey(icon: ReactNode) {\n if (!isValidElement(icon)) return \"icon\";\n const type = icon.type as string | { displayName?: string; name?: string };\n return typeof type === \"string\" ? type : (type.displayName ?? type.name ?? \"icon\");\n}\n\nfunction DismissGlyph() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n aria-hidden=\"true\"\n className=\"size-4\"\n >\n <path d=\"M4 4l8 8M12 4l-8 8\" />\n </svg>\n );\n}\n\nexport const Alert = forwardRef<HTMLDivElement, AlertProps>(function Alert(\n {\n className,\n variant,\n icon: iconProp,\n title,\n headingLevel = 3,\n children,\n open,\n onDismiss,\n dismissLabel = \"Dismiss\",\n role = \"alert\",\n ...props\n },\n ref,\n) {\n const Title = `h${headingLevel}` as const;\n const icon = iconProp === undefined ? statusGlyphs[variant ?? \"default\"] : iconProp;\n const frame = useRef<HTMLDivElement>(null);\n const content = useRef<HTMLDivElement>(null);\n const text = typeof children === \"string\" || typeof children === \"number\" ? String(children) : \"\";\n useHeightFollow(\n frame,\n content,\n `${typeof title === \"string\" ? title : \"\"}\\n${children === undefined ? \"\" : \"details\"}\\n${text}`,\n );\n\n // Presence. Without `open` or `onDismiss` the alert is simply there.\n const managed = open !== undefined || onDismiss !== undefined;\n const [dismissed, setDismissed] = useState(false);\n const wanted = open ?? !dismissed;\n const [rendered, setRendered] = useState(wanted);\n const [shown, setShown] = useState(wanted);\n if (wanted && !rendered) setRendered(true);\n if (!wanted && shown) setShown(false);\n useEffect(() => {\n // A newly shown alert mounts collapsed and opens a frame later, so it grows rather than appears.\n if (wanted && rendered && !shown) {\n const frameId = requestAnimationFrame(() => setShown(true));\n return () => cancelAnimationFrame(frameId);\n }\n }, [wanted, rendered, shown]);\n const leaving = rendered && !wanted;\n const finish = useCallback(() => {\n setRendered(false);\n if (open === undefined) onDismiss?.();\n }, [onDismiss, open]);\n useExitBackstop(leaving, finish, 900);\n useIsomorphicLayoutEffect(() => {\n if (leaving && prefersReducedMotion()) finish();\n }, [leaving, finish]);\n if (managed && !rendered) return null;\n\n const alert = (\n <div\n ref={ref}\n {...props}\n role={role}\n data-slot=\"alert\"\n className={cn(alertVariants({ variant }), className)}\n >\n {/*\n The icon is decorative or labelled by the consumer; this wrapper adds\n no name. It keeps the icon from shrinking and sizes a bare svg to the\n icon scale, unless the consumer sized it themselves.\n */}\n {icon === null || icon === false ? null : (\n <span data-slot=\"alert-icon\" className={alertIconVariants({ variant })}>\n <SwapText swapKey={iconKey(icon)} motion=\"glyph\" className=\"grid place-items-center\">\n {icon}\n </SwapText>\n </span>\n )}\n {/* The copy column follows its content's height, so longer or shorter text never snaps the alert. */}\n <div ref={frame} data-slot=\"alert-body\" className=\"height-follow min-w-0 flex-1\">\n <div ref={content}>\n {title === undefined ? null : (\n <Title\n data-slot=\"alert-title\"\n className=\"m-0 mb-1 font-display text-sm leading-(--leading-normal) font-medium\"\n >\n <Swappable value={title}>{title}</Swappable>\n </Title>\n )}\n {children === undefined ? null : (\n <div\n data-slot=\"alert-description\"\n className={title === undefined ? undefined : \"text-xs leading-(--leading-normal) text-text-secondary\"}\n >\n <Swappable value={children}>{children}</Swappable>\n </div>\n )}\n </div>\n </div>\n {onDismiss ? (\n <AriaButton\n data-slot=\"alert-dismiss\"\n aria-label={typeof title === \"string\" ? `${dismissLabel}: ${title}` : dismissLabel}\n onPress={() => (open === undefined ? setDismissed(true) : onDismiss())}\n className=\"calendar-nav-motion -my-1 -me-1.5 grid size-7 shrink-0 cursor-pointer place-items-center rounded-full border-0 bg-transparent text-muted-foreground outline-none data-[hovered]:bg-muted data-[hovered]:text-foreground data-[focus-visible]:outline-solid data-[focus-visible]:outline-(length:--focus-ring-width) data-[focus-visible]:outline-offset-(--focus-ring-offset) data-[focus-visible]:outline-(--color-ring)\"\n >\n <DismissGlyph />\n </AriaButton>\n ) : null}\n </div>\n );\n if (!managed) return alert;\n return (\n <div\n data-slot=\"alert-presence\"\n data-shown={shown || undefined}\n onTransitionEnd={(event) => {\n if (leaving && event.target === event.currentTarget && event.propertyName === \"grid-template-rows\")\n finish();\n }}\n className=\"alert-presence\"\n >\n <div>{alert}</div>\n </div>\n );\n});\n"],"mappings":"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"}
@@ -66,6 +66,20 @@ export interface AlertDialogProps extends Omit<DialogProps, "role" | "closeButto
66
66
  * `false`, `true` and `""` (as from `cond && <X />`) render no body.
67
67
  */
68
68
  body?: ReactNode;
69
+ /**
70
+ * A decorative icon at the start of the header, in a round tile beside the
71
+ * title: a warning triangle before a wide-reaching switch, a trash can before
72
+ * a delete. Hidden from assistive technology, so the title must still say
73
+ * what is at stake. Leave it out and the header is as before.
74
+ */
75
+ icon?: ReactNode;
76
+ /**
77
+ * The colour of the icon's tile, from the status tokens: `neutral` (the
78
+ * default), `warning`, `destructive`, `info` or `success`. It paints the tile
79
+ * only, never the actions. Without an `icon` it does nothing. Mirrored to
80
+ * `data-tone` on the tile.
81
+ */
82
+ tone?: "neutral" | "warning" | "destructive" | "info" | "success";
69
83
  /** The actions. Give the cancel button `slot="close"`. */
70
84
  children?: ReactNode;
71
85
  }