pixelize-design-library 2.4.2-beta.59 → 2.4.2-beta.60

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 (138) hide show
  1. package/CHANGELOG.md +78 -9
  2. package/COMPONENT-GOTCHAS.md +118 -17
  3. package/DESIGN-GUIDE.md +17 -6
  4. package/dist/Components/AudioRecorder/AudioRecorder.d.ts +8 -0
  5. package/dist/Components/AudioRecorder/AudioRecorder.js +169 -0
  6. package/dist/Components/AudioRecorder/AudioRecorderProps.d.ts +20 -0
  7. package/dist/Components/AudioRecorder/AudioRecorderProps.js +2 -0
  8. package/dist/Components/BulkSelectionBar/BulkSelectionBar.d.ts +3 -0
  9. package/dist/Components/BulkSelectionBar/BulkSelectionBar.js +43 -0
  10. package/dist/Components/BulkSelectionBar/BulkSelectionBarProps.d.ts +35 -0
  11. package/dist/Components/BulkSelectionBar/BulkSelectionBarProps.js +2 -0
  12. package/dist/Components/BulkSelectionBar/index.d.ts +2 -0
  13. package/dist/Components/BulkSelectionBar/index.js +8 -0
  14. package/dist/Components/CalendarGrid/CalendarGrid.d.ts +3 -0
  15. package/dist/Components/CalendarGrid/CalendarGrid.js +173 -0
  16. package/dist/Components/CalendarGrid/CalendarGridProps.d.ts +54 -0
  17. package/dist/Components/CalendarGrid/CalendarGridProps.js +2 -0
  18. package/dist/Components/CalendarGrid/calendarMath.d.ts +49 -0
  19. package/dist/Components/CalendarGrid/calendarMath.js +178 -0
  20. package/dist/Components/CircularProgress/CircularProgress.d.ts +4 -0
  21. package/dist/Components/CircularProgress/CircularProgress.js +39 -0
  22. package/dist/Components/CircularProgress/CircularProgressProps.d.ts +22 -0
  23. package/dist/Components/CircularProgress/CircularProgressProps.js +2 -0
  24. package/dist/Components/CircularProgress/arc.d.ts +12 -0
  25. package/dist/Components/CircularProgress/arc.js +16 -0
  26. package/dist/Components/Common/fieldStyles.d.ts +1 -0
  27. package/dist/Components/Common/fieldStyles.js +2 -1
  28. package/dist/Components/Drawer/Drawer.d.ts +1 -1
  29. package/dist/Components/Drawer/Drawer.js +4 -2
  30. package/dist/Components/Drawer/DrawerProps.d.ts +3 -1
  31. package/dist/Components/EmptyState/EmptyState.js +2 -1
  32. package/dist/Components/EmptyState/EmptyStateProps.d.ts +2 -2
  33. package/dist/Components/FormDrawer/FormDrawer.d.ts +2 -0
  34. package/dist/Components/FormDrawer/FormDrawer.js +97 -0
  35. package/dist/Components/FormDrawer/FormDrawerProps.d.ts +55 -0
  36. package/dist/Components/FormDrawer/FormDrawerProps.js +2 -0
  37. package/dist/Components/LauncherTrigger/LauncherTrigger.d.ts +1 -1
  38. package/dist/Components/LauncherTrigger/LauncherTrigger.js +8 -1
  39. package/dist/Components/LauncherTrigger/LauncherTriggerProps.d.ts +2 -0
  40. package/dist/Components/Modal/Modal.d.ts +1 -1
  41. package/dist/Components/Modal/Modal.js +4 -2
  42. package/dist/Components/Modal/ModalProps.d.ts +3 -1
  43. package/dist/Components/Modal/warnDroppedSlotChildren.d.ts +2 -0
  44. package/dist/Components/Modal/warnDroppedSlotChildren.js +45 -0
  45. package/dist/Components/NavLauncher/NavLauncher.js +83 -26
  46. package/dist/Components/NavLauncher/NavLauncherProps.d.ts +6 -6
  47. package/dist/Components/NavLauncher/launcherParts.d.ts +60 -1
  48. package/dist/Components/NavLauncher/launcherParts.js +93 -11
  49. package/dist/Components/ProfilePhotoViewer/ProfilePhotoViewer.js +5 -0
  50. package/dist/Components/ProfilePhotoViewer/ProfilePhotoViewerProps.d.ts +1 -1
  51. package/dist/Components/QueryState/QueryStateBoundary.d.ts +2 -2
  52. package/dist/Components/QueryState/QueryStateBoundary.js +9 -3
  53. package/dist/Components/QueryState/QueryStateProps.d.ts +19 -2
  54. package/dist/Components/SectionRail/SectionRail.d.ts +3 -0
  55. package/dist/Components/SectionRail/SectionRail.js +49 -0
  56. package/dist/Components/SectionRail/SectionRailProps.d.ts +23 -0
  57. package/dist/Components/SectionRail/SectionRailProps.js +2 -0
  58. package/dist/Components/SectionRail/index.d.ts +2 -0
  59. package/dist/Components/SectionRail/index.js +8 -0
  60. package/dist/Components/SegmentedControl/SegmentedControl.js +9 -14
  61. package/dist/Components/SegmentedControl/segmentedInk.d.ts +0 -2
  62. package/dist/Components/SegmentedControl/segmentedInk.js +1 -5
  63. package/dist/Components/StatCard/StatCard.d.ts +1 -1
  64. package/dist/Components/StatCard/StatCard.js +6 -2
  65. package/dist/Components/StatCard/StatCardProps.d.ts +3 -0
  66. package/dist/Components/StatusTag/StatusTag.d.ts +4 -0
  67. package/dist/Components/StatusTag/StatusTag.js +24 -0
  68. package/dist/Components/StatusTag/StatusTagProps.d.ts +20 -0
  69. package/dist/Components/StatusTag/StatusTagProps.js +2 -0
  70. package/dist/Components/Table/Table.d.ts +1 -1
  71. package/dist/Components/Table/Table.js +6 -2
  72. package/dist/Components/Table/TableProps.d.ts +20 -0
  73. package/dist/Components/Table/components/TableBody.d.ts +1 -1
  74. package/dist/Components/Table/components/TableBody.js +43 -18
  75. package/dist/Components/Table/components/TableHeader.d.ts +1 -1
  76. package/dist/Components/Table/components/TableHeader.js +7 -5
  77. package/dist/Components/Table/components/rowDetail.d.ts +32 -0
  78. package/dist/Components/Table/components/rowDetail.js +20 -0
  79. package/dist/Components/Table/hooks/useRowExpansion.d.ts +7 -0
  80. package/dist/Components/Table/hooks/useRowExpansion.js +27 -0
  81. package/dist/Components/Tabs/Tabs.d.ts +1 -1
  82. package/dist/Components/Tabs/Tabs.js +15 -2
  83. package/dist/Components/Tabs/TabsProps.d.ts +6 -2
  84. package/dist/Components/Tabs/components/Tab.js +4 -33
  85. package/dist/Components/Tabs/components/TabLinkList.d.ts +7 -0
  86. package/dist/Components/Tabs/components/TabLinkList.js +34 -0
  87. package/dist/Components/Tabs/useTabAppearance.d.ts +17 -0
  88. package/dist/Components/Tabs/useTabAppearance.js +67 -0
  89. package/dist/Components/Tag/Tag.js +11 -1
  90. package/dist/Components/Tag/TagProps.d.ts +4 -0
  91. package/dist/Components/Tag/tagDot.d.ts +3 -0
  92. package/dist/Components/Tag/tagDot.js +21 -0
  93. package/dist/esm/Components/AudioRecorder/AudioRecorder.js +161 -0
  94. package/dist/esm/Components/AudioRecorder/AudioRecorderProps.js +1 -0
  95. package/dist/esm/Components/BulkSelectionBar/BulkSelectionBar.js +38 -0
  96. package/dist/esm/Components/BulkSelectionBar/BulkSelectionBarProps.js +1 -0
  97. package/dist/esm/Components/BulkSelectionBar/index.js +1 -0
  98. package/dist/esm/Components/CalendarGrid/CalendarGrid.js +168 -0
  99. package/dist/esm/Components/CalendarGrid/CalendarGridProps.js +1 -0
  100. package/dist/esm/Components/CalendarGrid/calendarMath.js +155 -0
  101. package/dist/esm/Components/CircularProgress/CircularProgress.js +37 -0
  102. package/dist/esm/Components/CircularProgress/CircularProgressProps.js +1 -0
  103. package/dist/esm/Components/CircularProgress/arc.js +11 -0
  104. package/dist/esm/Components/Common/fieldStyles.js +2 -1
  105. package/dist/esm/Components/Drawer/Drawer.js +4 -2
  106. package/dist/esm/Components/EmptyState/EmptyState.js +2 -1
  107. package/dist/esm/Components/FormDrawer/FormDrawer.js +58 -0
  108. package/dist/esm/Components/FormDrawer/FormDrawerProps.js +1 -0
  109. package/dist/esm/Components/LauncherTrigger/LauncherTrigger.js +5 -1
  110. package/dist/esm/Components/Modal/Modal.js +4 -2
  111. package/dist/esm/Components/Modal/warnDroppedSlotChildren.js +38 -0
  112. package/dist/esm/Components/NavLauncher/NavLauncher.js +85 -28
  113. package/dist/esm/Components/NavLauncher/launcherParts.js +88 -11
  114. package/dist/esm/Components/ProfilePhotoViewer/ProfilePhotoViewer.js +5 -0
  115. package/dist/esm/Components/QueryState/QueryStateBoundary.js +10 -4
  116. package/dist/esm/Components/SectionRail/SectionRail.js +44 -0
  117. package/dist/esm/Components/SectionRail/SectionRailProps.js +1 -0
  118. package/dist/esm/Components/SectionRail/index.js +1 -0
  119. package/dist/esm/Components/SegmentedControl/SegmentedControl.js +11 -16
  120. package/dist/esm/Components/SegmentedControl/segmentedInk.js +0 -3
  121. package/dist/esm/Components/StatCard/StatCard.js +6 -2
  122. package/dist/esm/Components/StatusTag/StatusTag.js +19 -0
  123. package/dist/esm/Components/StatusTag/StatusTagProps.js +1 -0
  124. package/dist/esm/Components/Table/Table.js +7 -3
  125. package/dist/esm/Components/Table/components/TableBody.js +43 -18
  126. package/dist/esm/Components/Table/components/TableHeader.js +7 -5
  127. package/dist/esm/Components/Table/components/rowDetail.js +14 -0
  128. package/dist/esm/Components/Table/hooks/useRowExpansion.js +23 -0
  129. package/dist/esm/Components/Tabs/Tabs.js +15 -2
  130. package/dist/esm/Components/Tabs/components/Tab.js +4 -33
  131. package/dist/esm/Components/Tabs/components/TabLinkList.js +29 -0
  132. package/dist/esm/Components/Tabs/useTabAppearance.js +63 -0
  133. package/dist/esm/Components/Tag/Tag.js +12 -2
  134. package/dist/esm/Components/Tag/tagDot.js +17 -0
  135. package/dist/esm/index.js +7 -0
  136. package/dist/index.d.ts +16 -2
  137. package/dist/index.js +18 -4
  138. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -26,15 +26,21 @@ next major.
26
26
  screens, keyword-only hits get a `· keyword` hint) and `flattenScreens`. The pin control is a
27
27
  star, shown with the ↗ link on row hover/focus only (always on touch); pinned rail items are tiles. It binds no global chord. `LauncherTrigger`
28
28
  is the chip (10px radius, 36px, accent ink, one ⌘K hint) that opens an app-owned launcher (`aria-haspopup="dialog"`, `aria-expanded`,
29
- `aria-keyshortcuts`). Types: `NavLauncherProps`, `LauncherScreen`, `LauncherModule`,
29
+ `aria-keyshortcuts`); `display="icon"` is a 36px icon-only variant for a nav bar's `moreIcon`
30
+ (label is the accessible name, tooltip "Menu (⌘K)"). Types: `NavLauncherProps`, `LauncherScreen`, `LauncherModule`,
30
31
  `LauncherSection`, `LauncherSearchItem`, `LauncherLabels` and friends, `LauncherTriggerProps`.
31
32
  `NavLauncher` follows canvas `Launcher.dc.html`: stacked sections with "(n)" counts and a 1/2/3-column
32
- grid, a 300px "Quick access" rail (Pinned tiles, Frequently visited, Recent), "Pins follow your
33
- account" footer. New optional props `frequentLimit`, `suggestions` (no-results chips), `searchHint`,
33
+ grid, a "Quick access" rail of `clamp(20rem, 30%, 26rem)` in a panel whose `maxWidth` defaults to
34
+ 1080px (Pinned tiles, Frequently visited, Recent), "Pins follow your account" footer. The footer's
35
+ key hints are key caps (ENTER, ⌘ ENTER / CTRL ENTER, ESC, four arrows) and are hidden on touch devices. New optional props `frequentLimit`, `suggestions` (no-results chips), `searchHint`,
34
36
  `LauncherModule.isNew`, and labels `quickAccess`, `newBadge`, `screens`, `pinShort`, `footerNote`,
35
37
  `suggestionsHint`, `found`, `pinTip`, `unpinTip`. The rail landmark is named "Quick access".
36
38
  Keyboard: ←/→ and ↑/↓ are grid-aware (row-major, ↑/↓ cross sections), and `P` pins or unpins the
37
- focused item (never while typing). New optional `openShortcutHint` (e.g. "⌘K or /", app-owned),
39
+ focused item (never while typing); ArrowUp from the first item returns to the search field and
40
+ pinned tiles are a 2-column grid. The keyboard-active row highlights as a whole (soft fill plus
41
+ one 1px inset ring, at least 3:1), the whole row is the link target (Cmd/middle-click opens a new
42
+ tab), and truncated titles, rail rows, pinned tiles, app names and search rows show a tooltip with
43
+ the full text. New optional `openShortcutHint` (e.g. "⌘K or /", app-owned),
38
44
  labels `hintOpenMenu` and `hintPin`; hint label defaults are lowercase and follow the canvas order.
39
45
  - `Kbd` / `KbdShortcut` — a key cap and a chord (`mod+k` renders ⌘ K or Ctrl K), in the
40
46
  `AppSwitcher` notation. `Search` and `AppSwitcher` render their shortcut hints with `Kbd`.
@@ -68,6 +74,48 @@ next major.
68
74
  `navigator.pdfViewerEnabled`. `PdfViewerProps` / `PdfViewerLabels` are now exported types.
69
75
  - Page examples: **App launcher** (`/examples/launcher`), **Wizard** (`/examples/wizard`),
70
76
  **Template editor** (`/examples/template-editor`).
77
+ - `Tag` `display="dot"` — an 8px tone dot plus plain label for quiet rows (`variant` and icons
78
+ are ignored; the dot ink is lifted to 3:1 on both canvases). `StatusTag` (+ `StatusTagProps`,
79
+ `StatusAppearance`) maps an app's status union to a `Tag` tone once; an unknown status renders
80
+ neutral and warns once in development. (HRMS #13)
81
+ - `Drawer` / `Modal` `portalChildren` — rendered **inside** the dialog content after the slots (not
82
+ a sibling in the portal, which Chakra would `aria-hide`), for nested dialogs and live regions;
83
+ a nested `AlertDialog` still portals out, stacks above and returns focus. (HRMS #20)
84
+ - `FormDrawer` (+ `FormDrawerProps`, `FormDrawerSection`, `FormDrawerLabels`) — a sectioned
85
+ create/edit form in a `Drawer`: `role="alert"` error summary focused after a failed submit,
86
+ sticky footer with a real `<form>` submit, discard-changes confirm on every close path while
87
+ `isDirty`, all close paths blocked while `isSubmitting`. (HRMS #14)
88
+ - `Table` row detail expander — `renderRowDetail(row)` (null = no chevron), `expandedRows` /
89
+ `onExpandedRowsChange` (uncontrolled when `expandedRows` is omitted). Chevron column after the
90
+ checkbox, full-width detail row; works with pagination, virtualisation, frozen columns and
91
+ grouped mode. The chevron is a button with `aria-expanded` / `aria-controls`. (HRMS #17)
92
+ - `EmptyState` `description` accepts a `ReactNode` — a string keeps today's styling, a node sits in
93
+ the same muted block above `action`. (HRMS #18)
94
+ - `QueryStateBoundary` `isForbidden` (boolean or `(error) => boolean`), `forbidden`
95
+ (`{ title, description, icon, action }`) and `hasData`; type `QueryStateForbiddenProps`. Order is
96
+ now loading (only when `!hasData`) → forbidden → error → empty → children; forbidden is a lock
97
+ `EmptyState` with `role="status"`. Defaults unchanged. (HRMS #21)
98
+ - `Tabs` link mode — `TabItem.href` and `TabsProps.linkAs`: when every item has an `href`, `Tabs`
99
+ renders a `<nav aria-label>` of links (same variants and sizes, no panels, not a tablist) with
100
+ `aria-current="page"` on `value`; modifier and middle clicks are never intercepted. (HRMS #19)
101
+ - `BulkSelectionBar` (+ `BulkAction`, `BulkActionTone`, `BulkSelectionBarProps`) — selection bar for
102
+ beside a `Table`: "{n} selected", facts about the whole selection, tone-ranked actions, clear,
103
+ `inline` or `sticky`, polite count announcement; renders nothing at 0. (HRMS #15)
104
+ - `SectionRail` (+ `RailSection`, `SectionRailProps`) — section navigation for long settings
105
+ screens: a vertical `nav` with `aria-current` and a dirty dot per section, collapsing to a
106
+ `Select` below `collapseBelow` (default `md`). (HRMS #16)
107
+ - `StatCard` `iconTone` (neutral / info / success / warning / error) paints the icon chip with the
108
+ soft tone; unset keeps today's brand-primary chip. (HRMS #23)
109
+ - `CircularProgress` (+ `CircularProgressProps`, `CircularProgressSize`) — SVG ring, sm/md/lg =
110
+ 32/48/72px, `thickness`, `tone`, centred `label`, `role="progressbar"`. (HRMS #24)
111
+ - `CalendarGrid` (+ `CalendarEvent`, `CalendarGridProps`, `CalendarGridView`, `CalendarGridLabels`) —
112
+ a controlled month/week/day grid of date-granularity events: multi-day spans, "+N more" opens the
113
+ day, `role="grid"` with arrow/Home/End/Page keys, link or button chips in `Tag` tones, `locale`,
114
+ `weekStartsOn`, `isLoading`. (HRMS #25)
115
+ - `AudioRecorder` (+ `AudioRecorderProps`, `AudioRecorderLabels`) — microphone recorder: asks for
116
+ the mic on Start, mm:ss timer, `maxDurationSec` auto-stop, hands a `File` named for the recorded
117
+ mime to `onRecorded`, permission-denied and unsupported states, releases the mic on unmount.
118
+ (HRMS #26)
71
119
 
72
120
  - **Commerce kit** — the components for a shop front, designed mobile-first (Pebble principles:
73
121
  chromeless tonal cards, one loud action per view, selection never by colour alone). Product:
@@ -152,6 +200,16 @@ next major.
152
200
  - `Editor` binds Jodit's `change` / `blur` itself on every Jodit instance and renders Jodit only once
153
201
  its popup root exists (no throwaway first instance). Behaviour for existing props is unchanged.
154
202
  - `BreadcrumbItem.path` is optional (type widening).
203
+ - **`Drawer` / `Modal` warn when they drop a child.** A child that is not a Header/Body/Footer slot
204
+ still renders nothing, but now logs a dev-only `console.warn` (once per child type) pointing at
205
+ `portalChildren`. (HRMS #20)
206
+ - **`Table` `studio` variant on touch devices:** under `@media (pointer: coarse)` the row checkbox is
207
+ always visible and the row number is hidden, instead of a hover-only reveal. (HRMS #17)
208
+ - **`ProfilePhotoViewer` re-syncs when `photoUrl` changes** (it used to seed on mount only), so
209
+ consumers can drop `key={photoUrl}`. A locally picked preview is replaced by the new URL — update
210
+ `photoUrl` only once an upload has finished. (HRMS #22)
211
+ - **Page examples:** `records.tsx` uses `BulkSelectionBar` instead of the `Table`'s `headerActions`;
212
+ `settings.tsx` uses `SectionRail` instead of a hand-built section nav.
155
213
 
156
214
  - **`ProductCard` and `ProductDetails` are rewritten** — see Breaking below. `ProductCard` is a
157
215
  chromeless 4:5 card (grid / list / rail, swatch dots that swap the photo, quick-add sheet or
@@ -192,6 +250,15 @@ next major.
192
250
 
193
251
  ### Fixed
194
252
 
253
+ - **`NavLauncher` keyboard and link fixes.** Arrow keys, typing and Backspace were dead on rail and
254
+ apps rows while a search was active; Enter could throw when results shrank; Enter during IME
255
+ composition opened a result; removing the focused row (e.g. an app dropped from `apps`) dropped
256
+ focus to the page body (it now moves to the neighbouring row, or the field).
257
+ - **`Search` / `TextInput` / `Select` / `Textarea` / `NumberInput` / `FileUpload` / `PincodeChecker`
258
+ focus glow was transitioned in**, so a screenshot or accessibility check taken the instant a field
259
+ gained focus saw no ring. `FIELD_TRANSITION` no longer animates `box-shadow`; border-colour and
260
+ background transitions are unchanged and still honour reduced motion.
261
+
195
262
  - **`SideBar` rail controls now show a `:focus-visible` ring** (rows, submenu and flyout rows,
196
263
  `SecondaryBar`, collapse toggle, Add New, user footer, Collapse/Hide/Show, switch-app cells,
197
264
  overlay close) — WCAG 2.4.7 / 2.4.11.
@@ -370,8 +437,8 @@ next major.
370
437
  which is deep in light and pale in dark, so the bar inverted and the whole comparison was
371
438
  unreadable. It now derives the bar from `resolveFilledSurface(...).activeFill` and **measures**
372
439
  both contrast ratios live against the active brand and mode, instead of printing two numbers that
373
- were only ever true in light mode. `segmentedTrackSurface`, `segmentedRestInk` and
374
- `segmentedFocusShadow` are now one shared module, so the demo, the component and the contrast
440
+ were only ever true in light mode. `segmentedTrackSurface` and `segmentedRestInk`
441
+ are now one shared module, so the demo, the component and the contrast
375
442
  gate cannot drift apart.
376
443
  - **`SegmentedControl` on a brand-filled bar was unreadable mud.** `brandSlide`'s track is an 8%
377
444
  brand tint, which on a brand bar composites straight back into the bar — the control lost its
@@ -387,9 +454,11 @@ next major.
387
454
  the ring's surface-toned halo painted a white band and both bands landed outside the pill — a
388
455
  thick double border that overflowed the track. A radiogroup is also a single tab stop, so ringing
389
456
  one segment says nothing the selection does not already say. The **group** now carries the
390
- indicator, in the same language as a focused field: a 1px brand hairline inset on the track plus
391
- `focusGlowShadow`. `underlineSlide` has no track chrome, so there the segment wears it.
392
- `ThemeSwitcher`'s mode segments follow the same rule through their own border.
457
+ indicator: Button's own two-tone ring (halo plus brand band, at least 3:1 on any fill, light and
458
+ dark) on the track while a segment has keyboard focus (never after a mouse click).
459
+ `underlineSlide` has no track chrome, so there the
460
+ segment wears `focusVisibleStyles`. `ThemeSwitcher`'s mode segments follow the same rule through
461
+ their own border.
393
462
  - **`Switch` clipped long labels.** Its 12.5rem width was fixed, so labels wider than the track
394
463
  overflowed the background; it is now a minimum. `SegmentedControl`'s track carries
395
464
  `minW="max-content"` for the same reason.
@@ -177,6 +177,15 @@ is now the library's own themed component, with a real dual-thumb mode and a `ma
177
177
 
178
178
  **Apply:** "on vs off" is always `ToggleSwitch`; 2+ named options is always `SegmentedControl`.
179
179
 
180
+ **`SegmentedControl`'s focus ring sits on the track, not the segment** (Button's two-tone ring). It is
181
+ driven by a `:focus-visible` check in `onFocus`, so a mouse click stays ringless. `underlineSlide` has no
182
+ track chrome, so its focused segment wears the ring instead.
183
+
184
+ **A focus indicator must never be in a `transition` list.** The 0ms computed style is the transparent
185
+ start value, so a screenshot or axe check taken on focus misses it. Field styles go through
186
+ `FIELD_TRANSITION` (`Common/fieldStyles.ts`), which animates `border-color` and `background-color`
187
+ only; keep `box-shadow` out of any transition you add around a field.
188
+
180
189
  **`SegmentedControl` and `Tabs` look near-identical and are not interchangeable.** They share the
181
190
  size scale and tokens, so `SegmentedControl variant="underlineSlide"` and `Tabs variant="line"`
182
191
  render almost the same picture, as do `variant="default"` and `Tabs variant="subtle"`. The
@@ -304,7 +313,7 @@ not enough when the overlay sits inside a transition wrapper — the wrapper kee
304
313
  | --- | --- | --- |
305
314
  | "Nothing here yet" with an optional action | `EmptyState` | Neutral placeholder |
306
315
  | A failed load with retry | `ErrorState` | `role="alert"`, `onRetry`/`isRetrying` built in |
307
- | One fetch that can be loading, failed, empty or ready | `QueryStateBoundary` | Picks for you: loading > error > empty > `children` |
316
+ | One fetch that can be loading, forbidden, failed, empty or ready | `QueryStateBoundary` | Picks for you: loading > forbidden > error > empty > `children` |
308
317
 
309
318
  ---
310
319
 
@@ -421,6 +430,12 @@ renders a plain tag with no way to remove it.
421
430
  removable chip. `icon` alone (no `onIconClick`) is the decorative case — a plain right-side icon
422
431
  with no click affordance, aria-hidden.
423
432
 
433
+ ### `Tag display="dot"` ignores `variant` and icons; `StatusTag` renders an unknown status neutral
434
+
435
+ `display="dot"` ignores `variant`, `icon`, `leadingIcon` and `onIconClick`. `StatusTag`'s `map` is
436
+ keyed by your status union, but a runtime value outside it (API drift) renders a neutral chip and
437
+ only warns in development — guard upstream if a silent neutral would mislead.
438
+
424
439
  ### A rounded box with `overflow: hidden` clips glyphs at the corners — never round the clipping element
425
440
 
426
441
  `border-radius` reshapes the overflow clip, so a pill-shaped element with `overflow: hidden`
@@ -457,20 +472,33 @@ popovers and menus now paint `colors.surfaceRaised` — identical `#ffffff` in l
457
472
  step up in dark. A custom panel, popover or `ModalContent bg=` in an app should read
458
473
  `surfaceRaised` too, and a divider inside it `raisedRule(colors)`.
459
474
 
460
- ### `Drawer` / `Modal` / `ProfileCard` drop any child that isn't a named slot
475
+ ### `Drawer` / `Modal` / `ProfileCard` drop any child that isn't a named slot — use `portalChildren`
461
476
 
462
477
  All three match children by exact type (`child.type === XHeader/XBody/XFooter`) and render only
463
478
  those matches — nothing else in `children` is ever read. A sibling among the slots (a nested
464
- `Modal`, `AlertDialog`, a confirm dialog, a portal after the footer) silently never mounts. All
465
- three now unwrap **one** `<>...</>` level around a slot (so `<>{cond && <DrawerFooter>...}</>`
466
- still renders) and spread styling props (`padding`, `borderBottom`, `sx`, …) from the matched slot
467
- onto the real header/body/footer — a second nested fragment, or any other wrapper element, still
468
- drops the slot silently.
469
-
470
- **Apply:** put a confirm dialog **outside** the `Drawer`/`Modal`/`ProfileCard`, as a sibling in a
471
- wrapping fragment — never among the header/body/footer slots. When a ternary picks between bodies,
472
- branch into a variable rendered inside one body slot, rather than returning fragment-wrapped slots
473
- per branch. `Drawer`'s misspelled `vaiant` is `@deprecated` — use `variant`.
479
+ `Modal`, `AlertDialog`, a confirm dialog, a live region) silently never mounts; `Drawer` and `Modal`
480
+ now warn once per dropped child type in development, `ProfileCard` still says nothing. All three
481
+ unwrap **one** `<>...</>` level around a slot (so `<>{cond && <DrawerFooter>...}</>` still renders)
482
+ and spread styling props (`padding`, `borderBottom`, `sx`, …) from the matched slot onto the real
483
+ header/body/footer — a second nested fragment, or any other wrapper element, still drops the slot
484
+ silently.
485
+
486
+ **Apply:** pass a nested confirm dialog, toast host or live region to `Drawer` / `Modal` as
487
+ `portalChildren`. It renders **inside** the dialog content after the slots (a sibling in the portal
488
+ would be `aria-hidden` by Chakra, so a live region there is never announced); a nested `AlertDialog`
489
+ portals out on its own, stacks above and returns focus. It unmounts with the dialog, so reset its
490
+ open state when the parent closes. `ProfileCard` has no such prop — keep its confirm dialog outside,
491
+ as a sibling in a wrapping fragment. When a ternary picks between bodies, branch into a variable
492
+ rendered inside one body slot, rather than returning fragment-wrapped slots per branch. `Drawer`'s
493
+ misspelled `vaiant` is `@deprecated` — use `variant`.
494
+
495
+ ### `FormDrawer` has no browser validation, and its submit button sits outside the `<form>`
496
+
497
+ It sets `noValidate`: validate in `onSubmit` and pass messages via `errors`. Error-summary focus
498
+ fires only after a submit; if validation is async, set `isSubmitting` synchronously in `onSubmit` so
499
+ focus waits for the result, otherwise errors that arrive later are not focused. The footer submit
500
+ button is tied to the form with the `form` attribute — Enter-to-submit works in browsers, but
501
+ jsdom / user-event does not simulate it, so use `fireEvent.submit(form)` in tests.
474
502
 
475
503
  ### `QueryStateBoundary` evaluates JSX `children` even while loading
476
504
 
@@ -478,11 +506,23 @@ JSX children are built before the boundary decides what to show, so `data.items.
478
506
  while `data` is still `undefined`. **Apply:** pass `children` as a function — `{() => <List
479
507
  items={data.items} />}` — whenever it reads data that may not exist yet.
480
508
 
509
+ ### `QueryStateBoundary` `isForbidden` and `hasData`
510
+
511
+ An `isForbidden` function is only called when `error` is set (never with `undefined`); `isForbidden:
512
+ true` shows the panel even with no error. `isLoading` + `hasData` keeps `children` visible during a
513
+ refetch, so pass `hasData` only when data is genuinely loaded — otherwise a first load shows a blank
514
+ region.
515
+
481
516
  ### `StatCard`'s delta colour follows `trend` vs `goodDirection`, not `trend` alone
482
517
 
483
518
  Green when `trend === goodDirection`, red otherwise — a falling cost with `goodDirection="down"` is
484
519
  green. Pass `onClick` for a clickable card (it renders its own `<button>`); never wrap it in one.
485
520
 
521
+ ### `CircularProgress` `tone="info"` is the brand primary, not blue
522
+
523
+ Unlike `Tag` and `StatCard`, where `info` is blue, the default `tone="info"` here paints the brand
524
+ primary scale. `aria-label` is required — the ring has no visible caption.
525
+
486
526
  ### A clickable `TextInput` icon is a real button — give it a name
487
527
 
488
528
  With `onRightIconclick`/`onLeftIconClick` the icon renders as a `<button>`. Without
@@ -562,6 +602,14 @@ accepted, typed, never called.
562
602
  **Apply:** leave at least one column without a `node`, or put an explicit control inside a `node`
563
603
  renderer instead of relying on row click.
564
604
 
605
+ ### `Table.renderRowDetail` runs for every row on every render, and `expandedRows` ids match strictly
606
+
607
+ It decides whether each row gets a chevron, so an inline function re-renders every row (defeating
608
+ `TableRow`'s memo) — wrap it in `useCallback` / `useMemo`. `expandedRows` ids compare with `===`, so
609
+ `1` and `"1"` are different: keep the id type the same as `row.id`. It is not the older
610
+ `row.content` + `isContent` expander (chevron before the checkbox, index-based); both can coexist,
611
+ but only `row.content` still disables grouped virtualisation.
612
+
565
613
  ### `error` without `errorMessage` renders the literal word "Error" — and erases `helperText`
566
614
 
567
615
  Every field that shows validation errors (`TextInput`, `InputTextArea`, `Select`, `MultiSelect`,
@@ -1246,9 +1294,22 @@ the `Search` listeners see it — otherwise the first mounted `Search` takes Ctr
1246
1294
  - **`NavLauncher` `suggestions` empty falls back to `noResultsHint`** instead of showing chips.
1247
1295
  - **`PinToggle` / `NewTabLink` are opacity 0 until row hover or focus-within**, so a consumer's own row must carry `data-group` (or `role="group"`) to reveal them.
1248
1296
  - **`LauncherLabels.screensGroup` is unused** by search now (sections replaced the flat group); kept so label objects still type-check.
1297
+ - **`apps` is data-driven.** Gate by plan by changing the array or setting `locked` / `lockedReason`;
1298
+ keep `id`s stable (hues and `key` derive from them), and pass `color` on an app whose mark must not
1299
+ shift when siblings are added or removed. Locked rows are not links, not focusable, `aria-disabled`,
1300
+ tooltip = `lockedReason`. Pass `appsVisibility="always"` to show them above `md`.
1301
+ - **Never use `:has()` in an emotion style.** jsdom's (nwsapi) `getComputedStyle` can throw ("not a valid selector") and takes every consumer test that opens a
1302
+ Drawer down with it. The launcher marks row focus with a `data-row-focus` attribute instead. If you
1303
+ hit that error in your own tests on a newer jsdom, report it with the selector.
1304
+ - **Launcher rows are CSS containers** (`container-type: inline-size`, name `launcher-row`); jsdom
1305
+ drops `@container` rules, so tests assert the style objects (`titleInset`, `WIDE_ROW`), not the
1306
+ stylesheet.
1307
+ - **A search row without `href` (an action with `run`) cannot open in a new tab:** any click, with or
1308
+ without Cmd/Ctrl, runs it and closes the launcher, same as Enter.
1249
1309
  - **`LauncherTrigger` does not bind its shortcut**; `shortcut={null}` removes the hint and
1250
1310
  `aria-keyshortcuts`. An explicit `aria-label` hides the visible label from assistive tech, so keep
1251
- the visible text inside the label (WCAG 2.5.3).
1311
+ the visible text inside the label (WCAG 2.5.3). `display="icon"` has no visible text: the label is
1312
+ its accessible name and tooltip.
1252
1313
 
1253
1314
  ### `Header`'s desktop submenus open on hover only — no keyboard or click trigger
1254
1315
 
@@ -1316,16 +1377,56 @@ error, no warning, `scrollTop` on a non-scrolling parent just stays `0` forever.
1316
1377
  **Apply:** `ScrollToTop`'s JSX parent must be the actual scroll container, or pass `useWindow` when
1317
1378
  the page itself scrolls.
1318
1379
 
1319
- ### `ProfilePhotoViewer`'s PDF detection breaks the moment a file is picked, and `photoUrl` is read once
1380
+ ### `ProfilePhotoViewer`'s PDF detection breaks the moment a file is picked, and a new `photoUrl` discards the preview
1320
1381
 
1321
1382
  PDF detection is a literal `.endsWith(".pdf")` check — picking a file through the built-in editor
1322
1383
  replaces the value with a base64 data URI that never ends in `.pdf`, so a freshly-picked PDF renders
1323
1384
  as a broken image instead of the intended file badge (only the original URL prop is detected
1324
- correctly). Separately, `photoUrl` only seeds state at mount — a later re-render with a new
1325
- `photoUrl` (list virtualization, route change without remount) keeps showing the old photo.
1385
+ correctly). Separately, a changed `photoUrl` re-syncs the component and replaces any locally picked
1386
+ preview (a same-URL re-render keeps it), so a parent that updates `photoUrl` before the upload has
1387
+ finished makes the preview vanish mid-upload.
1326
1388
 
1327
1389
  **Apply:** don't use the built-in edit flow for PDF documents; detect kind from `File.type` in your
1328
- own handler instead. Use `key={photoUrl}` to force a remount when the photo identity changes.
1390
+ own handler instead. Update `photoUrl` only once the upload has finished. `key={photoUrl}` is no
1391
+ longer needed.
1392
+
1393
+ ### `Tabs` link mode needs `href` on every item, and has no default current item
1394
+
1395
+ One item without `href` makes the whole thing a normal tablist and `href` is ignored everywhere (a
1396
+ dev-only `console.warn` is the only signal). Button tabs select the first enabled item when `value`
1397
+ is missing; link tabs mark nothing `aria-current` until you pass `value`. A plain left-click calls
1398
+ `onChange` but never `preventDefault`s; modifier and middle clicks only open the link. `isDisabled`
1399
+ link items render as a non-link `span`.
1400
+
1401
+ **Apply:** `Tabs items=[{ id, label, href }] linkAs={RouterLink} value={currentId}` with `value`
1402
+ derived from the route, and never hand-build `Box as={Link}` tabs.
1403
+
1404
+ ### `BulkSelectionBar` renders nothing at `selectedCount` 0, and `sticky` needs a scroll ancestor
1405
+
1406
+ With no persistent live region it announces count changes itself (the node mounts empty and fills in
1407
+ an effect), so don't also announce from your own region. `placement="sticky"` pins only inside an
1408
+ ancestor scroll container — an ancestor with `overflow: hidden` silently disables it. Combining it
1409
+ with `Table`'s `headerActions` gives two action sets for one selection.
1410
+
1411
+ ### `SectionRail` chooses rail vs `Select` with `matchMedia` at render time
1412
+
1413
+ It renders the rail when `matchMedia` is missing (SSR). In the `Select` branch a dirty section's
1414
+ option reads `"Label (dirtyHint)"` — a native `<option>` cannot hold a dot.
1415
+
1416
+ ### `CalendarGrid` is fully controlled, ignores time of day, and `renderDayCell` turns multi-day bars off
1417
+
1418
+ Without `onDateChange` / `onViewChange` the toolbar's Today/Prev/Next and the view switcher are
1419
+ hidden and arrow keys stop at the edge of the visible range, so the grid looks static — wire both.
1420
+ `start` / `end` are read as local calendar dates (`end` inclusive): a UTC-midnight `Date` shows the
1421
+ previous day in a negative-offset zone, so build date-only values with `new Date(y, m, d)`.
1422
+ `renderDayCell` replaces each cell's chips wholesale: a 3-day event is passed to it for all 3 days
1423
+ but is no longer drawn as one bar, and `onEventClick` never fires from your own content.
1424
+
1425
+ ### `AudioRecorder` needs a secure context, and `onRecorded` is not called after unmount
1426
+
1427
+ `getUserMedia` is undefined on plain http (except localhost), so the component shows its
1428
+ "unsupported" state there, not "permission denied". A parent that navigates away mid-recording gets
1429
+ no `onRecorded` call (the mic is released instead), and an empty recording never calls it either.
1329
1430
 
1330
1431
  ### `FilePreview` infers file kind from `fileName`, not `fileUrl`, and never resets zoom/rotation on its own
1331
1432
 
package/DESIGN-GUIDE.md CHANGED
@@ -45,7 +45,7 @@ Borders are the one px value: `borderWidth="1px"`. Fixed widths (side panel, lis
45
45
  | Section / card title | `Heading as="h2" fontSize="md" fontWeight={600} color={colors.header[900]}`, in `Card`'s `header` prop |
46
46
  | Body | `Text fontSize="sm" color={colors.text[800]}` |
47
47
  | Label / meta / helper | `Text fontSize="xs" color={colors.textMuted}` |
48
- | KPI tile | `StatCard` — pass `trend` and `goodDirection`; the delta is green when the change is *good* (a shorter sales cycle is green) |
48
+ | KPI tile | `StatCard` — pass `trend` and `goodDirection`; the delta is green when the change is *good* (a shorter sales cycle is green); `iconTone` tints the icon chip (unset = brand primary) |
49
49
  | Numbers in tables/columns | `sx={{ fontVariantNumeric: "tabular-nums" }}` so digits align; `Table` columns take `align: "end"` |
50
50
  | Overline (rare) | `fontSize="xs" fontWeight={600} textTransform="uppercase" letterSpacing="0.06em"` |
51
51
 
@@ -69,7 +69,7 @@ stay light in dark mode.
69
69
  | Secondary text, icons | `colors.textMuted` |
70
70
  | Brand-colored text/link | `colors.accentText` — never `primary[500]` |
71
71
  | Status text/icon | `colors.semanticText.success \| error \| warning \| info` |
72
- | Status chip | `<Tag label="Won" colorScheme="green" />` (subtle, `sm` by default) — never a painted `Box`. Tone → `colorScheme`: success `green`, warning `orange`, error `red`, info `blue`, neutral `gray` |
72
+ | Status chip | `<Tag label="Won" colorScheme="green" />` (subtle, `sm` by default) — never a painted `Box`. Tone → `colorScheme`: success `green`, warning `orange`, error `red`, info `blue`, neutral `gray`. Quiet rows: `<Tag tone="success" display="dot" label=… />`; map an app's status vocabulary once with `<StatusTag status map label display />` |
73
73
  | Selectable list row (inbox, picker) | `listRowBackgrounds(colors)` for hover/selected + `sx={focusVisibleStyles(colors)}`; pad the list `p={2}` so the ring isn't clipped |
74
74
  | Any filled surface you paint | `resolveFilledSurface(colors.<scale>, colors)` → `{ fill, color }` |
75
75
  | Icon tint chip, avatar tint | `bg={colors.primary.opacity[8]}` + `color={colors.accentText}` (`opacity` exists on `primary`/`secondary`/`tertiary` only) |
@@ -154,6 +154,12 @@ sections are `Card` with the title in `header`.
154
154
  | A person: avatar + name (+ role/email, presence, remove) | `PersonChip` (`orientation="vertical"` for a stacked profile block) |
155
155
  | An avatar on its own, or a stack of them | `Avatar` / `AvatarGroup` — never pick the fill, it is derived from `name` |
156
156
  | Unsaved-changes bar | `SaveBar isOpen={isDirty} onSave onDiscard isSaving` |
157
+ | Create / edit a record in a side panel | `FormDrawer` (`sections`, `errors`, `isDirty`, `isSubmitting`) — don't hand-assemble Drawer + footer + discard confirm. A nested confirm dialog in any `Drawer` / `Modal` goes in `portalChildren` |
158
+ | Detail or sub-table under a table row | `Table renderRowDetail` (+ optional `expandedRows`) — don't put an Accordion outside the Table |
159
+ | Tabs that navigate between routes | `Tabs items=[{ id, label, href }] linkAs={RouterLink} value={currentId}` — never hand-build `Box as={Link}` tabs |
160
+ | Completion / capacity ring | `CircularProgress` (`ProgressBar` stays for linear) |
161
+ | Per-day activity (leave, tasks, shifts) | `CalendarGrid` with `events` mapped from your data — don't hand-roll a month grid |
162
+ | Voice note capture | `AudioRecorder`; `MediaAudioPlayer` for playback |
157
163
  | Previewing an attachment (image, video, audio, PDF, Office, text, archive) | `MediaViewer` (`items`, `isOpen`, `onClose`) — omit `index` and it owns navigation itself |
158
164
  | Notification tray | `NotificationBell` (badge, needs `aria-label`) opening a `NotificationPanel` (zones, filters, all four states) |
159
165
  | Confirm that needs the name typed to arm it | `ConfirmDialog confirmationText="delete-me"` — `onConfirm` does **not** close; call `onClose` yourself |
@@ -178,14 +184,19 @@ scrim `Box`. `Loading` is the older API and stays on the heavier house scrim by
178
184
  - Pins live in the app (server or local). Pass them to `NavLauncher` (`pinnedIds`, `onTogglePin`) *and*
179
185
  to `SideBar pinnedItems` + `onUnpin` so the rail shows what the launcher pinned. Persist collapse with
180
186
  `useSidebarPrefs`, and pass `autoCollapse={false}` if the app, not the rail, decides.
187
+ - Replacing a 9-dot app switcher in a nav bar: `<LauncherTrigger display="icon" />` (36px) in `moreIcon`;
188
+ keep the chip variant (`leading`) where there is room.
189
+ - Show the suite's apps with `appsVisibility="always"` and drive plan gating by editing `apps` /
190
+ `locked`.
181
191
  - Show shortcut hints with `KbdShortcut`, never a hand-styled `<kbd>`.
182
192
 
183
193
  ## 7. Every page has four states
184
194
 
185
195
  Design all four, not just the happy path.
186
196
 
187
- `QueryStateBoundary` picks one per content region (loading > error > empty > data). Pass `children`
188
- as a function when it reads data that may still be undefined.
197
+ `QueryStateBoundary` picks one per content region (loading > no access > error > empty > data). Pass
198
+ `children` as a function when it reads data that may still be undefined. `isForbidden` shows the
199
+ no-access panel between loading and error; `hasData` keeps content on screen during a refetch.
189
200
 
190
201
  | State | Render |
191
202
  | --- | --- |
@@ -228,9 +239,9 @@ not shipped in this package). Start from the closest one:
228
239
  | Screen you're building | Reference | Shows |
229
240
  | --- | --- | --- |
230
241
  | Dashboard / overview | `dashboard.tsx` | `StatCard` row, chart `Card`, stage list with `ProgressBar`, task list, `QueryStateBoundary` |
231
- | List of records | `records.tsx` | saved views, `FilterBar`, `Table` (`isGlobalSearch={false}`, `Tag` cells), bulk actions, `AlertDialog tone="danger"`, create `Drawer` |
242
+ | List of records | `records.tsx` | saved views, `FilterBar`, `Table` (`isGlobalSearch={false}`, `Tag` cells, row selection), `BulkSelectionBar` (count, facts, ranked actions, clear), `AlertDialog tone="danger"`, create `Drawer` |
232
243
  | One record | `recordDetail.tsx` | `PageHeader` with breadcrumbs/avatar/meta, `Tabs` (declarative `items` with `count` badges), `DescriptionList` (grid + inline side panel), `Timeline` (per-event `icon`/`description`), `ErrorState` + back |
233
- | Settings / long form | `settings.tsx` | section nav, `SettingsSection` + `SettingRow`, `SaveBar`, danger zone |
244
+ | Settings / long form | `settings.tsx` | `SectionRail` (rail from `lg`, `Select` below, dirty dots), `SettingsSection` + `SettingRow`, `SaveBar`, danger zone |
234
245
  | Board | `pipeline.tsx` | `KanbanBoard` with custom cards and column totals (`height`/`maxH` to size it) |
235
246
  | Inbox / master–detail | `inbox.tsx` | selectable list rows, conversation pane, composer, mobile list↔detail |
236
247
  | App launcher (browse, pins, recents, search with actions) | `launcher.tsx` (`/examples/launcher`) | `NavLauncher` + `LauncherTrigger` in `NavigationBar leading`, app-owned registry and permission filter, pinned / frequent / recent ids, `linkAs`, remote results via `extraResults`, `isLoading`, `SideBar pinnedItems` |
@@ -0,0 +1,8 @@
1
+ import type { AudioRecorderProps } from "./AudioRecorderProps";
2
+ export declare const formatElapsed: (ms: number) => string;
3
+ export declare const recordingFileName: (mime: string, now: number) => {
4
+ type: string;
5
+ name: string;
6
+ };
7
+ declare const AudioRecorder: ({ onRecorded, maxDurationSec, isDisabled, labels }: AudioRecorderProps) => import("react/jsx-runtime").JSX.Element;
8
+ export default AudioRecorder;
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.recordingFileName = exports.formatElapsed = void 0;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
9
+ const react_2 = require("@chakra-ui/react");
10
+ const Button_1 = __importDefault(require("../Button/Button"));
11
+ const useCustomTheme_1 = require("../../Theme/useCustomTheme");
12
+ const DEFAULT_LABELS = {
13
+ start: "Start recording",
14
+ stop: "Stop recording",
15
+ recording: "Recording",
16
+ recorded: "Recorded",
17
+ permissionDenied: "Microphone access was denied. Allow it in your browser settings and try again.",
18
+ unsupported: "Audio recording is not supported in this browser.",
19
+ };
20
+ const PREFERRED_MIME = "audio/webm";
21
+ const TICK_MS = 250;
22
+ const EXTENSIONS = {
23
+ "audio/webm": "webm",
24
+ "audio/wav": "wav",
25
+ "audio/wave": "wav",
26
+ "audio/x-wav": "wav",
27
+ "audio/ogg": "ogg",
28
+ "audio/mp4": "m4a",
29
+ "audio/mpeg": "mp3",
30
+ };
31
+ const formatElapsed = (ms) => {
32
+ const total = Math.floor(ms / 1000);
33
+ return `${String(Math.floor(total / 60)).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}`;
34
+ };
35
+ exports.formatElapsed = formatElapsed;
36
+ const recordingFileName = (mime, now) => {
37
+ var _a, _b;
38
+ const type = mime.split(";")[0].trim().toLowerCase() || PREFERRED_MIME;
39
+ return { type, name: `recording-${now}.${(_a = EXTENSIONS[type]) !== null && _a !== void 0 ? _a : (((_b = type.split("/")[1]) === null || _b === void 0 ? void 0 : _b.replace(/^x-/, "")) || "webm")}` };
40
+ };
41
+ exports.recordingFileName = recordingFileName;
42
+ const isSupported = () => { var _a; return typeof navigator !== "undefined" && !!((_a = navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.getUserMedia) && typeof MediaRecorder !== "undefined"; };
43
+ const AudioRecorder = ({ onRecorded, maxDurationSec, isDisabled = false, labels }) => {
44
+ const { colors } = (0, useCustomTheme_1.useCustomTheme)();
45
+ const text = { ...DEFAULT_LABELS, ...labels };
46
+ const [status, setStatus] = (0, react_1.useState)("idle");
47
+ const [elapsedMs, setElapsedMs] = (0, react_1.useState)(0);
48
+ const [announcement, setAnnouncement] = (0, react_1.useState)("");
49
+ const recorderRef = (0, react_1.useRef)(null);
50
+ const streamRef = (0, react_1.useRef)(null);
51
+ const chunksRef = (0, react_1.useRef)([]);
52
+ const timerRef = (0, react_1.useRef)(null);
53
+ const startedAtRef = (0, react_1.useRef)(0);
54
+ const mountedRef = (0, react_1.useRef)(true);
55
+ const startingRef = (0, react_1.useRef)(false);
56
+ const onRecordedRef = (0, react_1.useRef)(onRecorded);
57
+ const textRef = (0, react_1.useRef)(text);
58
+ onRecordedRef.current = onRecorded;
59
+ textRef.current = text;
60
+ const release = (0, react_1.useCallback)(() => {
61
+ var _a;
62
+ if (timerRef.current !== null)
63
+ clearInterval(timerRef.current);
64
+ timerRef.current = null;
65
+ (_a = streamRef.current) === null || _a === void 0 ? void 0 : _a.getTracks().forEach((track) => track.stop());
66
+ streamRef.current = null;
67
+ recorderRef.current = null;
68
+ }, []);
69
+ (0, react_1.useEffect)(() => {
70
+ mountedRef.current = true;
71
+ if (!isSupported())
72
+ setStatus("unsupported");
73
+ return () => {
74
+ mountedRef.current = false;
75
+ const recorder = recorderRef.current;
76
+ if (recorder) {
77
+ recorder.ondataavailable = null;
78
+ recorder.onstop = null;
79
+ recorder.onerror = null;
80
+ if (recorder.state !== "inactive") {
81
+ try {
82
+ recorder.stop();
83
+ }
84
+ catch {
85
+ // NOTE: a recorder the browser already tore down throws on stop.
86
+ }
87
+ }
88
+ }
89
+ release();
90
+ };
91
+ }, [release]);
92
+ const stop = (0, react_1.useCallback)(() => {
93
+ const recorder = recorderRef.current;
94
+ if (recorder && recorder.state !== "inactive")
95
+ recorder.stop();
96
+ }, []);
97
+ const start = (0, react_1.useCallback)(async () => {
98
+ var _a, _b;
99
+ if (startingRef.current)
100
+ return;
101
+ if (!isSupported()) {
102
+ setStatus("unsupported");
103
+ return;
104
+ }
105
+ startingRef.current = true;
106
+ try {
107
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
108
+ if (!mountedRef.current) {
109
+ stream.getTracks().forEach((track) => track.stop());
110
+ return;
111
+ }
112
+ streamRef.current = stream;
113
+ chunksRef.current = [];
114
+ const useWebm = (_b = (_a = MediaRecorder.isTypeSupported) === null || _a === void 0 ? void 0 : _a.call(MediaRecorder, PREFERRED_MIME)) !== null && _b !== void 0 ? _b : false;
115
+ const recorder = new MediaRecorder(stream, useWebm ? { mimeType: PREFERRED_MIME } : undefined);
116
+ recorderRef.current = recorder;
117
+ recorder.ondataavailable = (event) => {
118
+ if (event.data.size > 0)
119
+ chunksRef.current.push(event.data);
120
+ };
121
+ recorder.onerror = () => {
122
+ release();
123
+ setStatus("idle");
124
+ };
125
+ recorder.onstop = () => {
126
+ const duration = Date.now() - startedAtRef.current;
127
+ const { type, name } = (0, exports.recordingFileName)(recorder.mimeType || PREFERRED_MIME, Date.now());
128
+ const file = new File(chunksRef.current, name, { type });
129
+ release();
130
+ setStatus("idle");
131
+ setElapsedMs(duration);
132
+ setAnnouncement(`${textRef.current.recorded} ${(0, exports.formatElapsed)(duration)}`);
133
+ if (file.size > 0)
134
+ onRecordedRef.current(file);
135
+ };
136
+ recorder.start();
137
+ startedAtRef.current = Date.now();
138
+ setElapsedMs(0);
139
+ setStatus("recording");
140
+ setAnnouncement(textRef.current.recording);
141
+ timerRef.current = setInterval(() => {
142
+ const elapsed = Date.now() - startedAtRef.current;
143
+ if (maxDurationSec !== undefined && elapsed >= maxDurationSec * 1000) {
144
+ setElapsedMs(maxDurationSec * 1000);
145
+ stop();
146
+ return;
147
+ }
148
+ setElapsedMs(elapsed);
149
+ }, TICK_MS);
150
+ }
151
+ catch (error) {
152
+ release();
153
+ if (!mountedRef.current)
154
+ return;
155
+ const name = error === null || error === void 0 ? void 0 : error.name;
156
+ setStatus(name === "NotFoundError" ? "unsupported" : "denied");
157
+ }
158
+ finally {
159
+ startingRef.current = false;
160
+ }
161
+ }, [maxDurationSec, release, stop]);
162
+ const isRecording = status === "recording";
163
+ const blocked = status === "unsupported";
164
+ return ((0, jsx_runtime_1.jsxs)(react_2.Box, { children: [(0, jsx_runtime_1.jsxs)(react_2.Flex, { align: "center", gap: 3, children: [(0, jsx_runtime_1.jsx)(Button_1.default, { label: isRecording ? text.stop : text.start, size: "sm", colorScheme: isRecording ? "red" : undefined, variant: isRecording ? "solid" : "outline", "aria-pressed": isRecording, isDisabled: isRecording ? false : isDisabled || blocked, onClick: isRecording ? stop : () => void start() }), isRecording ? ((0, jsx_runtime_1.jsxs)(react_2.Flex, { align: "center", gap: 2, children: [(0, jsx_runtime_1.jsx)(react_2.Box, { "data-testid": "audio-recorder-dot", boxSize: "0.5rem", borderRadius: "full", bg: colors.semanticText.error, "aria-hidden": true, sx: {
165
+ "@media (prefers-reduced-motion: no-preference)": { animation: "audio-recorder-pulse 1.2s ease-in-out infinite" },
166
+ "@keyframes audio-recorder-pulse": { "0%, 100%": { opacity: 1 }, "50%": { opacity: 0.35 } },
167
+ } }), (0, jsx_runtime_1.jsx)(react_2.Text, { as: "span", role: "timer", fontSize: "0.875rem", color: colors.header[900], sx: { fontVariantNumeric: "tabular-nums" }, children: (0, exports.formatElapsed)(elapsedMs) })] })) : null] }), (0, jsx_runtime_1.jsx)(react_2.VisuallyHidden, { role: "status", "aria-live": "polite", children: announcement }), status === "denied" || status === "unsupported" ? ((0, jsx_runtime_1.jsx)(react_2.Text, { role: "alert", mt: 1, fontSize: "0.75rem", color: colors.semanticText.error, children: status === "denied" ? text.permissionDenied : text.unsupported })) : null] }));
168
+ };
169
+ exports.default = AudioRecorder;
@@ -0,0 +1,20 @@
1
+ /** Visible text and announcements; every key falls back to English. */
2
+ export type AudioRecorderLabels = {
3
+ start: string;
4
+ stop: string;
5
+ recording: string;
6
+ /** Announced when a recording finishes, followed by its duration. */
7
+ recorded: string;
8
+ permissionDenied: string;
9
+ unsupported: string;
10
+ };
11
+ /** Props for the microphone recorder: one toggle button, a timer, and a `File` on stop. */
12
+ export interface AudioRecorderProps {
13
+ /** Receives the finished recording; never called for an empty one or after unmount. */
14
+ onRecorded: (file: File) => void;
15
+ /** Stops the recording by itself after this many seconds. No limit when omitted. */
16
+ maxDurationSec?: number;
17
+ /** Blocks Start only; a recording already in progress can always be stopped. */
18
+ isDisabled?: boolean;
19
+ labels?: Partial<AudioRecorderLabels>;
20
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ import type { BulkSelectionBarProps } from "./BulkSelectionBarProps";
2
+ declare const BulkSelectionBar: ({ selectedCount, facts, actions, onClearSelection, clearLabel, countLabel, placement, "aria-label": ariaLabel, "data-testid": testId, }: BulkSelectionBarProps) => import("react/jsx-runtime").JSX.Element | null;
3
+ export default BulkSelectionBar;