@seekalfred/primitives 0.7.1 → 0.7.4

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 (147) hide show
  1. package/README.md +255 -5
  2. package/dist/alert/alert.d.ts +49 -4
  3. package/dist/alert/alert.js +66 -38
  4. package/dist/alert/alert.js.map +1 -1
  5. package/dist/alert/index.d.ts +2 -2
  6. package/dist/alert/index.js +2 -2
  7. package/dist/alert-dialog/alert-dialog.d.ts +31 -4
  8. package/dist/alert-dialog/alert-dialog.js +75 -26
  9. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  10. package/dist/announcement-bar/announcement-bar.d.ts +15 -4
  11. package/dist/announcement-bar/announcement-bar.js +31 -6
  12. package/dist/announcement-bar/announcement-bar.js.map +1 -1
  13. package/dist/avatar/avatar.d.ts +7 -0
  14. package/dist/avatar/avatar.js +33 -12
  15. package/dist/avatar/avatar.js.map +1 -1
  16. package/dist/badge/badge.d.ts +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 +1 -1
  25. package/dist/calendar/calendar.d.ts +1 -1
  26. package/dist/card/card.d.ts +22 -1
  27. package/dist/card/card.js +60 -7
  28. package/dist/card/card.js.map +1 -1
  29. package/dist/carousel/carousel.d.ts +21 -0
  30. package/dist/carousel/carousel.js +34 -8
  31. package/dist/carousel/carousel.js.map +1 -1
  32. package/dist/chip-group/chip-group.d.ts +11 -0
  33. package/dist/chip-group/chip-group.js +50 -34
  34. package/dist/chip-group/chip-group.js.map +1 -1
  35. package/dist/chip-group/index.d.ts +2 -2
  36. package/dist/copy-button/copy-button.d.ts +37 -5
  37. package/dist/copy-button/copy-button.js +35 -11
  38. package/dist/copy-button/copy-button.js.map +1 -1
  39. package/dist/donut-chart/donut-chart.d.ts +22 -0
  40. package/dist/donut-chart/donut-chart.js +35 -18
  41. package/dist/donut-chart/donut-chart.js.map +1 -1
  42. package/dist/donut-chart/index.d.ts +2 -2
  43. package/dist/empty-state/empty-state.d.ts +21 -0
  44. package/dist/empty-state/empty-state.js +16 -4
  45. package/dist/empty-state/empty-state.js.map +1 -1
  46. package/dist/empty-state/index.d.ts +2 -2
  47. package/dist/empty-state/index.js +2 -2
  48. package/dist/file-drop-zone/file-drop-zone.d.ts +29 -3
  49. package/dist/file-drop-zone/file-drop-zone.js +26 -8
  50. package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
  51. package/dist/filter-bar/filter-bar.d.ts +4 -1
  52. package/dist/filter-bar/filter-bar.js +53 -9
  53. package/dist/filter-bar/filter-bar.js.map +1 -1
  54. package/dist/gauge/gauge.d.ts +18 -1
  55. package/dist/gauge/gauge.js +35 -21
  56. package/dist/gauge/gauge.js.map +1 -1
  57. package/dist/gauge/index.d.ts +2 -2
  58. package/dist/index.d.ts +15 -14
  59. package/dist/index.js +9 -9
  60. package/dist/input/input.d.ts +7 -0
  61. package/dist/input/input.js +7 -5
  62. package/dist/input/input.js.map +1 -1
  63. package/dist/lib/field-shell.js +6 -3
  64. package/dist/lib/field-shell.js.map +1 -1
  65. package/dist/lib/field-surface.js +22 -1
  66. package/dist/lib/field-surface.js.map +1 -1
  67. package/dist/lib/required-marker.js +20 -0
  68. package/dist/lib/required-marker.js.map +1 -0
  69. package/dist/lib/use-copy-feedback.js +17 -3
  70. package/dist/lib/use-copy-feedback.js.map +1 -1
  71. package/dist/line-chart/index.d.ts +2 -2
  72. package/dist/line-chart/line-chart.d.ts +144 -2
  73. package/dist/line-chart/line-chart.js +314 -46
  74. package/dist/line-chart/line-chart.js.map +1 -1
  75. package/dist/money-input/index.d.ts +2 -2
  76. package/dist/money-input/money-input.d.ts +36 -1
  77. package/dist/money-input/money-input.js +30 -14
  78. package/dist/money-input/money-input.js.map +1 -1
  79. package/dist/morph-select/morph-select.d.ts +26 -1
  80. package/dist/morph-select/morph-select.js +28 -6
  81. package/dist/morph-select/morph-select.js.map +1 -1
  82. package/dist/multi-select/index.d.ts +2 -2
  83. package/dist/multi-select/index.js +2 -2
  84. package/dist/multi-select/multi-select.d.ts +74 -2
  85. package/dist/multi-select/multi-select.js +213 -39
  86. package/dist/multi-select/multi-select.js.map +1 -1
  87. package/dist/password-field/password-field.d.ts +16 -0
  88. package/dist/password-field/password-field.js +37 -8
  89. package/dist/password-field/password-field.js.map +1 -1
  90. package/dist/popover/index.d.ts +2 -2
  91. package/dist/popover/index.js +2 -2
  92. package/dist/popover/popover.d.ts +39 -2
  93. package/dist/popover/popover.js +29 -2
  94. package/dist/popover/popover.js.map +1 -1
  95. package/dist/progress/index.d.ts +2 -2
  96. package/dist/progress/index.js +2 -2
  97. package/dist/progress/progress.d.ts +68 -3
  98. package/dist/progress/progress.js +156 -39
  99. package/dist/progress/progress.js.map +1 -1
  100. package/dist/radio-group/index.d.ts +2 -2
  101. package/dist/radio-group/index.js +2 -2
  102. package/dist/radio-group/radio-group.d.ts +33 -3
  103. package/dist/radio-group/radio-group.js +109 -9
  104. package/dist/radio-group/radio-group.js.map +1 -1
  105. package/dist/scroll-area/scroll-area.js +4 -3
  106. package/dist/scroll-area/scroll-area.js.map +1 -1
  107. package/dist/scrub-input/scrub-input.d.ts +11 -1
  108. package/dist/scrub-input/scrub-input.js +22 -10
  109. package/dist/scrub-input/scrub-input.js.map +1 -1
  110. package/dist/search-field/search-field.d.ts +22 -1
  111. package/dist/search-field/search-field.js +22 -7
  112. package/dist/search-field/search-field.js.map +1 -1
  113. package/dist/select/select.d.ts +10 -1
  114. package/dist/select/select.js +11 -8
  115. package/dist/select/select.js.map +1 -1
  116. package/dist/separator/separator.d.ts +21 -9
  117. package/dist/separator/separator.js +33 -11
  118. package/dist/separator/separator.js.map +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 +65 -1
  125. package/dist/stat-card/stat-card.js +98 -25
  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 +115 -5
  131. package/dist/table/table.js +374 -94
  132. package/dist/table/table.js.map +1 -1
  133. package/dist/text-shimmer/text-shimmer.d.ts +18 -2
  134. package/dist/text-shimmer/text-shimmer.js +5 -2
  135. package/dist/text-shimmer/text-shimmer.js.map +1 -1
  136. package/dist/textarea/textarea.d.ts +7 -0
  137. package/dist/textarea/textarea.js +3 -1
  138. package/dist/textarea/textarea.js.map +1 -1
  139. package/dist/time-picker/time-picker.d.ts +36 -1
  140. package/dist/time-picker/time-picker.js +33 -5
  141. package/dist/time-picker/time-picker.js.map +1 -1
  142. package/dist/toast/toast.d.ts +2 -2
  143. package/dist/usage-meter/index.d.ts +2 -2
  144. package/dist/usage-meter/usage-meter.d.ts +30 -0
  145. package/dist/usage-meter/usage-meter.js +56 -38
  146. package/dist/usage-meter/usage-meter.js.map +1 -1
  147. package/package.json +3 -3
package/README.md CHANGED
@@ -132,6 +132,34 @@ makes `description` **required** — React Aria associates the description
132
132
  for that role, so a confirmation built from a plain `Dialog` loses the
133
133
  sentence explaining what is about to happen.
134
134
 
135
+ ### Rich content: `body`
136
+
137
+ `body` puts rich content between the description and the actions: a plan
138
+ comparison, a before/after diff, a quoted reply, a field to fill in or a choice
139
+ to make (a `RadioGroup`). It shares the description's edge and is in the
140
+ dialog's tab order before the actions. `false`, `true` and `""` (as from
141
+ `cond && <X />`) render no body.
142
+
143
+ ```tsx
144
+ <AlertDialog
145
+ isOpen={open}
146
+ onOpenChange={setOpen}
147
+ title="Cancel subscription?"
148
+ description="You keep Pro until the end of the period, then move to Free."
149
+ body={<PlanSummary plan={plan} />}
150
+ >
151
+ <AlertDialogCancel>Keep plan</AlertDialogCancel>
152
+ <AlertDialogAction onPress={cancel}>Cancel subscription</AlertDialogAction>
153
+ </AlertDialog>
154
+ ```
155
+
156
+ The body is not part of the accessible description: a screen reader announces
157
+ only the `description` sentence with the title, and reads the body in place or
158
+ as focus reaches its controls, each with its own label. So a field or a choice
159
+ belongs in `body`, but the `description` must say everything needed to answer,
160
+ including that something below is required ("Pick what happens to your
161
+ recordings, then confirm.").
162
+
135
163
  ### Migrating from the legacy shadcn AlertDialog
136
164
 
137
165
  | legacy | Alfred | |
@@ -528,6 +556,28 @@ Padding is **block-only on the shell and inline-only on the sections**, which
528
556
  is what lets a divider or a full-bleed image run edge to edge. A single `p-6`
529
557
  on the shell would make that impossible.
530
558
 
559
+ ### Featured and flush cards
560
+
561
+ ```tsx
562
+ <Card highlighted>…</Card> {/* the one featured card in a view */}
563
+ <Card flush>…</Card> {/* a table, list or chart that meets the top and bottom */}
564
+ ```
565
+
566
+ `highlighted` replaces the hairline with a brand gradient edge (secondary to
567
+ tertiary, so it follows the theme) without changing the card's box. To repaint
568
+ the edge, set the `--card-highlight` custom property on the card element itself
569
+ (`className` or `style` on `Card`): the utility declares it on the card, so a
570
+ value set on an ancestor does not reach it. Use it for one card per view and
571
+ say in the content why it is featured: the edge is decoration. `flush` drops
572
+ the shell's block padding and the gap between parts and clips the content to
573
+ the rounded border, keeping the outline, radius, surface and shadow; a
574
+ `className` padding still wins. The header and footer keep a 24px block
575
+ inset, so their text clears the corner curve; the content stays block-flush.
576
+ Every section keeps its inline padding, so text never sits in the corner, and
577
+ a section's fill (`<CardContent className="bg-muted">`) still reaches the side
578
+ edges because background paints the padding. Both default to `false`, and the two
579
+ compose: a flush featured card keeps its gradient edge.
580
+
531
581
  ### Migrating from the legacy shadcn Card
532
582
 
533
583
  | legacy | Alfred | |
@@ -682,6 +732,16 @@ What a list shows when it has nothing to show: one muted line, centred in a
682
732
  bordered box the width of the list. `action` is an optional next step under the
683
733
  text and is omitted from the DOM when absent.
684
734
 
735
+ `image` is an optional illustration above the icon and the title (an `<img>`,
736
+ an inline SVG or any element). It is centred, shrinks to fit a narrow box and
737
+ keeps its aspect ratio; its size is the artwork's. The slot reserves no space,
738
+ so set the artwork's intrinsic `width` and `height`, or the layout jumps when
739
+ it loads. The slot does not hide it from assistive technology, so name it:
740
+ give an `<img>` an `alt` (`alt=""` when the title already says it all), and an
741
+ inline `<svg>` `role="img"` with an `aria-label`, or `aria-hidden="true"` when
742
+ it is decorative. Omitted from the DOM when absent; `false` (from
743
+ `cond && <img />`) counts as absent.
744
+
685
745
  ### Motion (EmptyState)
686
746
 
687
747
  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.
@@ -908,6 +968,18 @@ The wiring, done for you:
908
968
  - The error row has `role="alert"`.
909
969
  - Screen readers get the plain copy. The animated words are `aria-hidden`.
910
970
 
971
+ **`isRequired`** makes the control `required` (announced by screen readers and
972
+ checked by native form validation) and shows the required marker after the
973
+ label: the same red asterisk as `PasswordField` and `Select`, `aria-hidden` so
974
+ the name stays the label's text. Off by default. Without a `label` the control
975
+ is still `required`, with no label to mark. A native `required` on its own still
976
+ only sets the attribute and draws no marker, as before.
977
+
978
+ ```tsx
979
+ <Input label="Email" isRequired />
980
+ <Textarea label="Message" isRequired />
981
+ ```
982
+
911
983
  Limits worth knowing:
912
984
 
913
985
  - **Do not pass `aria-label` together with `label`.** `aria-label` wins, so the
@@ -1044,6 +1116,20 @@ default). Native input props apply to the input, and `className` goes to the
1044
1116
  input; `disabled` also disables the toggle, and `aria-invalid` gives the box its
1045
1117
  invalid border. The box shares the field look of `Input` and `Textarea`.
1046
1118
 
1119
+ `error` is `Input`'s prop: error text under the field, announced as an alert,
1120
+ that sets `aria-invalid` and is named in `aria-describedby`. `isRequired` makes
1121
+ the input `required` and shows a required marker (an asterisk, hidden from
1122
+ assistive technology, which hears the input's own `required`) after the label.
1123
+
1124
+ ```tsx
1125
+ <PasswordField
1126
+ label="API secret"
1127
+ isRequired
1128
+ error={errors.secret?.message}
1129
+ {...register("secret")}
1130
+ />
1131
+ ```
1132
+
1047
1133
  **The toggle's name changes with its state and it is also `aria-pressed`**, so a
1048
1134
  screen reader may announce both ("Hide password, pressed"); this is not tested
1049
1135
  with a screen reader. Reduced motion drops every transition and the press.
@@ -1135,9 +1221,39 @@ and no number. `formatOptions` reaches it too, so a currency or "3 of 7" needs n
1135
1221
  second prop. `trackClassName` and `fillClassName` are escape hatches for a colour
1136
1222
  no tone covers.
1137
1223
 
1224
+ `isIndeterminate` draws a segment 40% of the track wide that sweeps across it
1225
+ from the start edge on a loop (right to left in RTL), and announces no value.
1226
+ With `labelHidden` and no `showValue` nothing is printed above the track, so the
1227
+ 6px header gap collapses and the bar sits at the top of its box.
1228
+
1229
+ `showCompletion={false}` keeps a bar from saying "done" at 100%: the fill keeps
1230
+ its tone and no check lands. Use it for a timer-driven estimate that can reach
1231
+ 100% before the work finishes.
1232
+
1233
+ `target` draws an upright tick where the bar should be by now, on the same
1234
+ scale as `value` (clamped to `minValue`..`maxValue`), standing proud of the
1235
+ track with a 1px halo so it reads over the fill too. The tick is decorative:
1236
+ `targetLabel` (default "Target") and the formatted target ("Expected by now:
1237
+ 40%", formatted like the bar's own value) become the bar's accessible
1238
+ description, kept alongside any `aria-describedby` of yours. `showTargetLabel`
1239
+ also prints that text under the tick, centred on it and kept inside the bar
1240
+ near either end. No `target`, no tick and the same DOM as before.
1241
+
1242
+ `surface="inverse"` is for a bar on the inverted, foreground-coloured surface
1243
+ (a dark strip in the light theme): the track is the inverse text colour mixed
1244
+ into that surface, and the default fill, the tick and the copy take the inverse
1245
+ text colour. Explicit tones keep their colours.
1246
+
1247
+ ```tsx
1248
+ <Progress label="Organic sessions" value={340} maxValue={1000} target={450} targetLabel="Expected by now" labelHidden />
1249
+ <div className="bg-foreground p-4">
1250
+ <Progress label="Plan reaches" value={72} target={100} surface="inverse" labelHidden />
1251
+ </div>
1252
+ ```
1253
+
1138
1254
  ### Motion (Progress)
1139
1255
 
1140
- 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, and a bar that is full at mount shows its check with no motion. A value that is not a percentage (a price) is shown as it is. A changed string label swaps as a whole.
1256
+ 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.
1141
1257
 
1142
1258
  ## RadioGroup
1143
1259
 
@@ -1148,6 +1264,32 @@ The fill follows the value on the smooth spring (it was a 200ms ease) and the pr
1148
1264
  </RadioGroup>
1149
1265
  ```
1150
1266
 
1267
+ ### Card variant
1268
+
1269
+ `variant="card"` draws each choice as a selectable card: the whole card is the
1270
+ target, the title is `children`, and each `Radio` can take a `description` and
1271
+ a decorative `icon`. The group becomes a grid, so the caller sets the columns.
1272
+
1273
+ ```tsx
1274
+ <RadioGroup variant="card" value={mode} onChange={setMode} aria-label="How to run" className="sm:grid-cols-2">
1275
+ <Radio value="draft" icon={<DraftIcon />} description="Save as a draft and review it first.">
1276
+ Draft only
1277
+ </Radio>
1278
+ <Radio value="everywhere" description="Apply to every category now.">
1279
+ Run everywhere
1280
+ </Radio>
1281
+ </RadioGroup>
1282
+ ```
1283
+
1284
+ - The title alone is the option's name; the description is wired as its
1285
+ description (`aria-describedby`), so a screen reader does not run the two
1286
+ together. The icon is `aria-hidden`.
1287
+ - Selected shows twice: a `primary-strong` edge (StatCard's selected pair) and
1288
+ the same dot as the default radio, so it never rests on colour alone.
1289
+ - Keyboard is unchanged: one tab stop, arrow keys move and select. The focus
1290
+ ring is drawn on the card, not the dot.
1291
+ - `description` and `icon` are ignored by the default variant.
1292
+
1151
1293
  ### Migrating from the legacy RadioGroup
1152
1294
 
1153
1295
  | legacy | Alfred | |
@@ -1192,6 +1334,8 @@ New opt-in `fade` (px, default `0` = off, so an area is unchanged) fades the con
1192
1334
 
1193
1335
  ```tsx
1194
1336
  <SearchField label="Search contacts" value={query} onValueChange={setQuery} />
1337
+ <SearchField label="Search tickets" labelHidden size="sm" placeholder="Search tickets"
1338
+ value={query} onValueChange={setQuery} />
1195
1339
  ```
1196
1340
 
1197
1341
  A search input with a clear button. It is controlled: `value` and
@@ -1205,7 +1349,13 @@ animation, and while the button leaves it cannot be pressed or focused. The
1205
1349
  magnifier is muted at rest and takes full contrast while the field has focus or
1206
1350
  a value.
1207
1351
 
1208
- `label` is required and visible; `clearLabel` renames the button (default "Clear
1352
+ `label` is required and visible unless `labelHidden` is set, which keeps it as
1353
+ the input's accessible name (`aria-label`) without printing it — for a toolbar
1354
+ search whose purpose the surrounding UI already shows. That holds inside a
1355
+ React Aria `TextField` too, including one with no `Label` child. `size` (`sm` · `md` ·
1356
+ `lg`, default `md`) takes the same `--density-control-*` height and padding as
1357
+ Input, Select and Button, and `sm` takes Input's 14px text (16px on a touch
1358
+ screen). `clearLabel` renames the button (default "Clear
1209
1359
  search"). Native input props apply to the input, `className` goes to the input,
1210
1360
  and `disabled` also disables the clear button. The browser's own clear button
1211
1361
  and search decoration are hidden. The box shares the field look of `Input`.
@@ -1265,22 +1415,40 @@ only — WCAG 1.4.1. The message is wired into `aria-describedby` for you.
1265
1415
  <Select label="Country" isInvalid errorMessage="Pick a country to continue." />
1266
1416
  ```
1267
1417
 
1418
+ **`isRequired`** is React Aria's prop: the hidden native select is `required`
1419
+ and the root gets `data-required`. It also shows the required marker after the
1420
+ label, the same asterisk as `Input`, `Textarea` and `PasswordField`, hidden from
1421
+ the trigger's accessible name.
1422
+
1423
+ ```tsx
1424
+ <Select label="Country" isRequired>…</Select>
1425
+ ```
1426
+
1268
1427
  ## Separator
1269
1428
 
1270
1429
  ```tsx
1271
1430
  <Separator />
1272
1431
  <Separator orientation="vertical" />
1432
+ <Separator orientation="vertical" decorative />
1273
1433
  ```
1274
1434
 
1275
1435
  React Aria renders an `<hr>` horizontally and a `<div role="separator">`
1276
- vertically.
1436
+ vertically. The line is painted with `--color-separator`, the hairline between
1437
+ rows and groups inside a surface, not `--color-border`, which is the surface's
1438
+ own outline.
1439
+
1440
+ `decorative` draws the same line as a plain `aria-hidden` div, for a rule that
1441
+ only spaces inline items (a metadata row) where announcing "separator" between
1442
+ each pair is noise. Leave it off for a line between two groups of content.
1443
+ Other props (`id`, `data-*`, handlers) reach the div in this mode too; a name
1444
+ (`aria-label`, `aria-labelledby`) is dropped.
1277
1445
 
1278
1446
  ### Migrating from the legacy Separator
1279
1447
 
1280
1448
  | legacy | Alfred | |
1281
1449
  | ------------- | --------------- | ------------------------------------------------------------ |
1282
1450
  | `orientation` | `orientation` | unchanged |
1283
- | `decorative` | *no equivalent* | zero call sites; see below |
1451
+ | `decorative` | `decorative` | default flipped to `false`; see below |
1284
1452
 
1285
1453
  **This changes the semantics of all six existing uses.** The legacy defaulted
1286
1454
  to `decorative: true`, rendering `role="none"` — so every divider in the app
@@ -1404,6 +1572,17 @@ link, `onPress` a button, and `isSelected` with `onPress` a filter toggle with
1404
1572
  use `Badge`, whose `tone` already covers success, warning, danger, info,
1405
1573
  primary and neutral.
1406
1574
 
1575
+ `isLoading` keeps the card's box while its figure loads: the label stays (an
1576
+ empty `label` pulses in its line), the value and the context line (pass `sub`
1577
+ to keep it) become placeholders one line tall, the change pill pulses at its
1578
+ own size, the content is `aria-busy` and the root carries `data-loading`.
1579
+ Screen readers hear `loadingLabel` ("Loading") in place of the figure. A row of
1580
+ loading cards is exactly as tall as the row that replaces it.
1581
+
1582
+ ```tsx
1583
+ <StatCard label={metric?.name ?? ""} value={metric?.value ?? ""} sub="vs previous period" isLoading={!metric} />
1584
+ ```
1585
+
1407
1586
  ## Switch
1408
1587
 
1409
1588
  ```tsx
@@ -1516,6 +1695,12 @@ depends on row *data*, use `rowClassName`.
1516
1695
  | `striped` | zebra-stripes alternate rows |
1517
1696
  | `totalRow` | `(column) => ReactNode`; renders a real `<tfoot>` |
1518
1697
  | `Column.numeric` | tabular numerals, end-aligned by default |
1698
+ | `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) |
1699
+ | `expandedKeys` / `defaultExpandedKeys` / `onExpandedChange` | the open rows, controlled or not; nothing is open by default |
1700
+ | `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 |
1701
+ | `density` | `default` · `compact` — `py-2` rows in small type, the product's tight report tables |
1702
+ | `headerHidden` | hides the header row from sight and keeps it for assistive tech (key/value tables); widths still apply, sorting is off |
1703
+ | `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 |
1519
1704
 
1520
1705
  ### Using it in alfred-ui
1521
1706
 
@@ -1558,7 +1743,7 @@ not worth a palette entry. If you want it exactly, override
1558
1743
  | `rowClassName` | unchanged | |
1559
1744
  | `rowIdField="keywordId"` | `getRowId={(r) => r.keywordId}` | |
1560
1745
 
1561
- Row expansion, sticky headers, loading skeletons, pinned columns, the total
1746
+ Sticky headers, loading skeletons, pinned columns, the total
1562
1747
  row, filtering, column visibility and CSV export are **not ported** — each had
1563
1748
  one consumer or none across the six legacy tables, and they belong to a future
1564
1749
  `DataGrid`.
@@ -1635,6 +1820,28 @@ builds a `Time` at runtime, so the peer dependency is a real import here.
1635
1820
  (1, 5, 10, 15 or 30; a time between steps is added to the list so it is never lost),
1636
1821
  and `hourCycle` chooses 12 or 24 hour times.
1637
1822
 
1823
+ **In a form**: `id` goes on the trigger (so an outside `<label htmlFor>` or a form
1824
+ library reaches it), `name` submits the time as `"HH:MM"` in 24-hour time, as
1825
+ `<input type="time">` does, and `onBlur` fires when focus leaves the trigger.
1826
+ `isInvalid` paints the invalid border and `errorMessage` shows under the control
1827
+ while it is invalid, named in `aria-describedby`, as on `Select`. With
1828
+ react-hook-form, wire them from a `Controller`. Pass `field.value ?? null`, not
1829
+ `field.value`: `undefined` makes the picker uncontrolled, so a form reset to
1830
+ `undefined` would leave the last time on screen, while `null` clears it. With no
1831
+ time chosen, `name` submits an empty string.
1832
+
1833
+ ```tsx
1834
+ <Controller
1835
+ name="syncTime"
1836
+ control={control}
1837
+ render={({ field, fieldState }) => (
1838
+ <TimePicker label="Sync time" id={field.name} name={field.name}
1839
+ value={field.value ?? null} onChange={field.onChange} onBlur={field.onBlur}
1840
+ isInvalid={!!fieldState.error} errorMessage={fieldState.error?.message} />
1841
+ )}
1842
+ />
1843
+ ```
1844
+
1638
1845
  **Motion** (plain CSS and a little plain React, no animation library): the shown
1639
1846
  time rolls like a clock face (a later time rises in from below, an earlier one
1640
1847
  drops in from above, and the old one lifts away); the chevron turns half a turn on
@@ -1707,3 +1914,46 @@ inherits from the document. Set both if you support RTL.
1707
1914
 
1708
1915
  A tooltip rises 3px from its trigger and settles from .97 over 160ms (offset follows the side it opens on), and leaves in 120ms. While any tooltip is open, and for 300ms after the last one closes, the next one only fades in (no travel). The open delay stays 0. Reduced motion fades only. **DOM:** an empty hidden `span` inside the tooltip marks the warm case. Not ported: the crossfade and size spring when a string tooltip's text changes while open.
1709
1916
 
1917
+
1918
+ ## UsageMeter
1919
+
1920
+ ```tsx
1921
+ <UsageMeter label="Workspace storage" segments={segments} limit={10} unit="GB" />
1922
+ ```
1923
+
1924
+ The meter works its state out from the numbers: almost full from `warnAt` (a
1925
+ share of the limit, 0.9 by default), over once the total passes the limit.
1926
+
1927
+ ### A status from the backend
1928
+
1929
+ When the product decides the state, pass `status` and the meter shows that
1930
+ instead of its own reading (badge, `data-status`, the bar's name and the
1931
+ announcement). The bar and legend agree with it:
1932
+
1933
+ - `full` and `exhausted` under the limit mean the allowance is used up, so
1934
+ there is no free share: the bar spans what is used and the free item reads 0.
1935
+ The headline still states the plan size ("7.7 GB of 10 GB used").
1936
+ - Past the limit, `ok` and `warn` give way to the meter's own over state, since
1937
+ the hatch already shows the overage. `full` and `exhausted` keep their words.
1938
+
1939
+ | `status` | meaning | badge | `data-status` |
1940
+ | ----------- | --------------------------- | --------------- | ------------- |
1941
+ | `ok` | under the limit | "N free" | `ok` |
1942
+ | `warn` | almost full | "Almost full" | `near` |
1943
+ | `full` | allowance used up | "Limit reached" | `full` |
1944
+ | `exhausted` | nothing left to draw on | "None left" | `exhausted` |
1945
+
1946
+ `statusLabel` is the product's own words for the status ("Credits paused"). It
1947
+ replaces the badge's words in every mode (including "Unlimited"), the status in
1948
+ the bar's name and the announcement; the tone and icon still follow the status.
1949
+
1950
+ ### Unlimited
1951
+
1952
+ `unlimited` is a plan with no limit. The badge reads "Unlimited" (unless
1953
+ `statusLabel` is set), the bar shows
1954
+ each category's share of what is used, and there is no free share, limit marker
1955
+ or overflow hatch. `limit`, `warnAt` and `status` are ignored.
1956
+
1957
+ ```tsx
1958
+ <UsageMeter label="Keyword slots" segments={segments} limit={0} unlimited />
1959
+ ```
@@ -1,5 +1,5 @@
1
1
  import { VariantProps } from "class-variance-authority";
2
- import { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import { AriaRole, ComponentPropsWithoutRef, ReactNode, SVGProps } from "react";
3
3
  //#region src/alert/alert.d.ts
4
4
  /**
5
5
  * Alert, ported from the legacy shadcn component.
@@ -24,14 +24,42 @@ import { ComponentPropsWithoutRef, ReactNode } from "react";
24
24
  * problems rather than porting them.
25
25
  */
26
26
  export declare const alertVariants: (props?: ({
27
- variant?: "success" | "warning" | "destructive" | "default" | null | undefined;
27
+ variant?: "warning" | "destructive" | "default" | "success" | "info" | null | undefined;
28
28
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
29
+ /**
30
+ * The icon's colour: the state's own hue on every variant. Inside an Alert the
31
+ * destructive red is also the whole alert's text colour, so it changes nothing
32
+ * there; outside one it is what colours the icon.
33
+ *
34
+ * Exported with the status icons so a notice that is not an `Alert` (a row, a
35
+ * banner of the app's own) can show a tone the same way the alert does.
36
+ */
37
+ export declare const alertIconVariants: (props?: ({
38
+ variant?: "warning" | "destructive" | "default" | "success" | "info" | null | undefined;
39
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
40
+ /**
41
+ * The built-in glyph of each variant, drawn with currentColor so the icon
42
+ * wrapper's colour paints it. Each is its own component so a variant change
43
+ * crossfades the icon (see iconKey).
44
+ *
45
+ * Exported as AlertInfoIcon, AlertSuccessIcon, AlertWarningIcon and
46
+ * AlertDestructiveIcon. They are decorative (`aria-hidden`) and `--icon-sm`
47
+ * by default; extra props, such as a `className` with a size, land on the svg.
48
+ * Given an `aria-label` or `aria-labelledby` (with `role="img"`), an icon is
49
+ * no longer hidden, and an explicit `aria-hidden` always wins.
50
+ */
51
+ type StatusIconProps = Omit<SVGProps<SVGSVGElement>, "children">;
52
+ declare function InfoGlyph(props: StatusIconProps): import("react").JSX.Element;
53
+ declare function SuccessGlyph(props: StatusIconProps): import("react").JSX.Element;
54
+ declare function WarningGlyph(props: StatusIconProps): import("react").JSX.Element;
55
+ declare function DestructiveGlyph(props: StatusIconProps): import("react").JSX.Element;
29
56
  /**
30
57
  * An inline status message.
31
58
  *
32
59
  * Announced by assistive technology the moment it renders, via `role="alert"`.
33
- * That makes it the wrong component for content that is present on load and
34
- * merely styled as a notice -- use a plain region for those.
60
+ * That makes it the wrong default for content that is present on load and
61
+ * merely styled as a notice -- pass `role="note"` for those, or
62
+ * `role="status"` for an outcome that should be announced politely.
35
63
  *
36
64
  * @example
37
65
  * <Alert variant="destructive" icon={<WarningIcon aria-hidden />} title="Save failed">
@@ -80,7 +108,24 @@ export interface AlertProps extends Omit<ComponentPropsWithoutRef<"div">, "title
80
108
  onDismiss?: () => void;
81
109
  /** The Dismiss button's accessible name. @default "Dismiss" */
82
110
  dismissLabel?: string;
111
+ /**
112
+ * How assistive technology treats the message.
113
+ *
114
+ * - `alert`: announced assertively the moment it renders. For an error or a
115
+ * warning that appears after the user did something.
116
+ * - `status`: a live region announced politely, after what the user is
117
+ * hearing. For an outcome reported after an action ("Saved").
118
+ * - `note`: not live. For a notice that is simply there when the page loads;
119
+ * an alert there would interrupt the page's own announcement.
120
+ *
121
+ * Typed as any ARIA role, as the inherited `role` was, so existing callers
122
+ * still compile; those three are the ones an inline notice should use.
123
+ *
124
+ * @default "alert"
125
+ */
126
+ role?: "alert" | "status" | "note" | AriaRole;
83
127
  }
84
128
  export declare const Alert: import("react").ForwardRefExoticComponent<AlertProps & import("react").RefAttributes<HTMLDivElement>>;
85
129
  //#endregion
130
+ export { DestructiveGlyph as AlertDestructiveIcon, InfoGlyph as AlertInfoIcon, type StatusIconProps as AlertStatusIconProps, SuccessGlyph as AlertSuccessIcon, WarningGlyph as AlertWarningIcon };
86
131
  //# sourceMappingURL=alert.d.ts.map
@@ -9,6 +9,17 @@ import { Button } from "react-aria-components";
9
9
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
10
10
  //#region src/alert/alert.tsx
11
11
  /**
12
+ * `info` is `default` said by name: a call site that means "information" should
13
+ * not depend on `default` happening to look like it. Every per-variant map below
14
+ * derives `info` here, from its `default`, so the two cannot drift.
15
+ */
16
+ function withInfo(map) {
17
+ return {
18
+ ...map,
19
+ info: map.default
20
+ };
21
+ }
22
+ /**
12
23
  * Alert, ported from the legacy shadcn component.
13
24
  *
14
25
  * There is no React Aria base: RAC ships no static inline alert (its Toast
@@ -35,71 +46,88 @@ const alertVariants = cva([
35
46
  "rounded-panel border-(length:--border-width) p-(--density-control-pad-md)",
36
47
  "font-sans text-sm leading-(--leading-normal)"
37
48
  ], {
38
- variants: { variant: {
49
+ variants: { variant: withInfo({
39
50
  default: "bg-surface text-foreground border-border",
40
51
  success: "bg-surface text-foreground border-border",
41
52
  warning: "bg-surface text-foreground border-border",
42
53
  destructive: "bg-surface text-destructive-strong border-destructive-strong"
43
- } },
54
+ }) },
44
55
  defaultVariants: { variant: "default" }
45
56
  });
46
- /** The icon's colour: the state's own hue, on the status variants. Destructive keeps its whole-alert red. */
57
+ /**
58
+ * The icon's colour: the state's own hue on every variant. Inside an Alert the
59
+ * destructive red is also the whole alert's text colour, so it changes nothing
60
+ * there; outside one it is what colours the icon.
61
+ *
62
+ * Exported with the status icons so a notice that is not an `Alert` (a row, a
63
+ * banner of the app's own) can show a tone the same way the alert does.
64
+ */
47
65
  const alertIconVariants = cva("flex h-lh shrink-0 items-center [&_svg]:block [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-(--icon-sm)", {
48
- variants: { variant: {
66
+ variants: { variant: withInfo({
49
67
  default: "text-info-strong",
50
68
  success: "text-success-strong",
51
69
  warning: "text-warning-strong",
52
- destructive: ""
53
- } },
70
+ destructive: "text-destructive-strong"
71
+ }) },
54
72
  defaultVariants: { variant: "default" }
55
73
  });
56
- /**
57
- * The built-in glyph of each variant, drawn with currentColor so the icon
58
- * wrapper's colour paints it. Each is its own component so a variant change
59
- * crossfades the icon (see iconKey).
60
- */
61
- function StatusGlyph({ children }) {
74
+ function StatusGlyph({ children, className, ...props }) {
75
+ const labelled = props["aria-label"] !== void 0 || props["aria-labelledby"] !== void 0;
62
76
  return /* @__PURE__ */ jsx("svg", {
77
+ "aria-hidden": labelled ? void 0 : "true",
78
+ ...props,
79
+ className: cn("size-(--icon-sm)", className),
63
80
  viewBox: "0 0 16 16",
64
81
  fill: "none",
65
82
  stroke: "currentColor",
66
83
  strokeWidth: "1.5",
67
84
  strokeLinecap: "round",
68
85
  strokeLinejoin: "round",
69
- "aria-hidden": "true",
70
86
  children
71
87
  });
72
88
  }
73
- function InfoGlyph() {
74
- return /* @__PURE__ */ jsxs(StatusGlyph, { children: [/* @__PURE__ */ jsx("circle", {
75
- cx: "8",
76
- cy: "8",
77
- r: "6.25"
78
- }), /* @__PURE__ */ jsx("path", { d: "M8 7.25v3.5M8 5h.01" })] });
89
+ function InfoGlyph(props) {
90
+ return /* @__PURE__ */ jsxs(StatusGlyph, {
91
+ ...props,
92
+ children: [/* @__PURE__ */ jsx("circle", {
93
+ cx: "8",
94
+ cy: "8",
95
+ r: "6.25"
96
+ }), /* @__PURE__ */ jsx("path", { d: "M8 7.25v3.5M8 5h.01" })]
97
+ });
79
98
  }
80
- function SuccessGlyph() {
81
- return /* @__PURE__ */ jsxs(StatusGlyph, { children: [/* @__PURE__ */ jsx("circle", {
82
- cx: "8",
83
- cy: "8",
84
- r: "6.25"
85
- }), /* @__PURE__ */ jsx("path", { d: "m5.25 8.25 1.9 1.9 3.6-3.9" })] });
99
+ function SuccessGlyph(props) {
100
+ return /* @__PURE__ */ jsxs(StatusGlyph, {
101
+ ...props,
102
+ children: [/* @__PURE__ */ jsx("circle", {
103
+ cx: "8",
104
+ cy: "8",
105
+ r: "6.25"
106
+ }), /* @__PURE__ */ jsx("path", { d: "m5.25 8.25 1.9 1.9 3.6-3.9" })]
107
+ });
86
108
  }
87
- function WarningGlyph() {
88
- return /* @__PURE__ */ jsxs(StatusGlyph, { children: [/* @__PURE__ */ jsx("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" }), /* @__PURE__ */ jsx("path", { d: "M8 6.4v2.6M8 11.2h.01" })] });
109
+ function WarningGlyph(props) {
110
+ return /* @__PURE__ */ jsxs(StatusGlyph, {
111
+ ...props,
112
+ children: [/* @__PURE__ */ jsx("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" }), /* @__PURE__ */ jsx("path", { d: "M8 6.4v2.6M8 11.2h.01" })]
113
+ });
89
114
  }
90
- function DestructiveGlyph() {
91
- return /* @__PURE__ */ jsxs(StatusGlyph, { children: [/* @__PURE__ */ jsx("circle", {
92
- cx: "8",
93
- cy: "8",
94
- r: "6.25"
95
- }), /* @__PURE__ */ jsx("path", { d: "m6 6 4 4M10 6l-4 4" })] });
115
+ function DestructiveGlyph(props) {
116
+ return /* @__PURE__ */ jsxs(StatusGlyph, {
117
+ ...props,
118
+ children: [/* @__PURE__ */ jsx("circle", {
119
+ cx: "8",
120
+ cy: "8",
121
+ r: "6.25"
122
+ }), /* @__PURE__ */ jsx("path", { d: "m6 6 4 4M10 6l-4 4" })]
123
+ });
96
124
  }
97
- const statusGlyphs = {
125
+ const statusGlyphs = withInfo({
98
126
  default: /* @__PURE__ */ jsx(InfoGlyph, {}),
99
127
  success: /* @__PURE__ */ jsx(SuccessGlyph, {}),
100
128
  warning: /* @__PURE__ */ jsx(WarningGlyph, {}),
101
129
  destructive: /* @__PURE__ */ jsx(DestructiveGlyph, {})
102
- };
130
+ });
103
131
  /** A string title or description swaps as a whole when its text changes. */
104
132
  function Swappable({ value, children }) {
105
133
  if (typeof value !== "string" && typeof value !== "number") return /* @__PURE__ */ jsx(Fragment$1, { children });
@@ -127,7 +155,7 @@ function DismissGlyph() {
127
155
  children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" })
128
156
  });
129
157
  }
130
- const Alert = forwardRef(function Alert({ className, variant, icon: iconProp, title, headingLevel = 3, children, open, onDismiss, dismissLabel = "Dismiss", ...props }, ref) {
158
+ const Alert = forwardRef(function Alert({ className, variant, icon: iconProp, title, headingLevel = 3, children, open, onDismiss, dismissLabel = "Dismiss", role = "alert", ...props }, ref) {
131
159
  const Title = `h${headingLevel}`;
132
160
  const icon = iconProp === void 0 ? statusGlyphs[variant ?? "default"] : iconProp;
133
161
  const frame = useRef(null);
@@ -163,7 +191,7 @@ const Alert = forwardRef(function Alert({ className, variant, icon: iconProp, ti
163
191
  const alert = /* @__PURE__ */ jsxs("div", {
164
192
  ref,
165
193
  ...props,
166
- role: "alert",
194
+ role,
167
195
  "data-slot": "alert",
168
196
  className: cn(alertVariants({ variant }), className),
169
197
  children: [
@@ -221,6 +249,6 @@ const Alert = forwardRef(function Alert({ className, variant, icon: iconProp, ti
221
249
  });
222
250
  });
223
251
  //#endregion
224
- export { Alert, alertVariants };
252
+ export { Alert, DestructiveGlyph as AlertDestructiveIcon, InfoGlyph as AlertInfoIcon, SuccessGlyph as AlertSuccessIcon, WarningGlyph as AlertWarningIcon, alertIconVariants, alertVariants };
225
253
 
226
254
  //# sourceMappingURL=alert.js.map