@tensor_1/react 2.47.0 → 2.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +470 -1
  2. package/dist/{SchedulingCalendar-B6TakO2h.d.ts → SchedulingCalendar-WWyFL3Eo.d.ts} +51 -2
  3. package/dist/announce.d.ts +10 -0
  4. package/dist/announce.js +30 -0
  5. package/dist/chart-analytic.cjs +33 -25
  6. package/dist/chart-preset.cjs +23 -15
  7. package/dist/chart.cjs +23 -15
  8. package/dist/combobox-model.d.ts +10 -1
  9. package/dist/combobox-model.js +15 -0
  10. package/dist/command-palette-model.d.ts +44 -0
  11. package/dist/command-palette-model.js +36 -0
  12. package/dist/components/AccountMenu.d.ts +44 -0
  13. package/dist/components/AccountMenu.js +43 -0
  14. package/dist/components/AppShell.d.ts +45 -2
  15. package/dist/components/AppShell.js +116 -5
  16. package/dist/components/AuditLog.d.ts +128 -0
  17. package/dist/components/AuditLog.js +196 -0
  18. package/dist/components/BrandProfiles.js +4 -3
  19. package/dist/components/Breadcrumbs.d.ts +9 -2
  20. package/dist/components/Breadcrumbs.js +3 -2
  21. package/dist/components/Button.d.ts +12 -5
  22. package/dist/components/Button.js +7 -3
  23. package/dist/components/Card.d.ts +18 -1
  24. package/dist/components/Card.js +5 -1
  25. package/dist/components/ChangeHighlight.d.ts +30 -0
  26. package/dist/components/ChangeHighlight.js +21 -0
  27. package/dist/components/ChartCore.js +2 -1
  28. package/dist/components/Checkbox.d.ts +27 -1
  29. package/dist/components/Checkbox.js +31 -2
  30. package/dist/components/CodeDisplay.d.ts +65 -0
  31. package/dist/components/CodeDisplay.js +124 -0
  32. package/dist/components/Combobox.d.ts +27 -6
  33. package/dist/components/Combobox.js +195 -58
  34. package/dist/components/CommandPalette.d.ts +36 -1
  35. package/dist/components/CommandPalette.js +254 -2
  36. package/dist/components/ContactCard.d.ts +66 -0
  37. package/dist/components/ContactCard.js +74 -0
  38. package/dist/components/Count.d.ts +24 -0
  39. package/dist/components/Count.js +32 -0
  40. package/dist/components/DataWorkspace.d.ts +167 -0
  41. package/dist/components/DataWorkspace.js +383 -0
  42. package/dist/components/DateFieldParts.d.ts +2 -1
  43. package/dist/components/DateFieldParts.js +7 -0
  44. package/dist/components/DatePicker.d.ts +9 -2
  45. package/dist/components/DatePicker.js +67 -28
  46. package/dist/components/DateRangePicker.d.ts +12 -2
  47. package/dist/components/DateRangePicker.js +13 -12
  48. package/dist/components/DetailPage.d.ts +7 -1
  49. package/dist/components/DetailPage.js +2 -1
  50. package/dist/components/Dialogs.d.ts +71 -10
  51. package/dist/components/Dialogs.js +130 -30
  52. package/dist/components/DiffViewer.d.ts +23 -3
  53. package/dist/components/DiffViewer.js +26 -12
  54. package/dist/components/DiscardPrompt.js +16 -0
  55. package/dist/components/DisplayPreferences.d.ts +83 -0
  56. package/dist/components/DisplayPreferences.js +185 -0
  57. package/dist/components/EditorialHome.d.ts +23 -3
  58. package/dist/components/EditorialHome.js +16 -11
  59. package/dist/components/EntityPicker.d.ts +20 -3
  60. package/dist/components/EntityPicker.js +32 -13
  61. package/dist/components/FilterEditor.d.ts +18 -4
  62. package/dist/components/FilterEditor.js +141 -19
  63. package/dist/components/InfoTip.js +11 -12
  64. package/dist/components/InlineEdit.d.ts +87 -20
  65. package/dist/components/InlineEdit.js +197 -54
  66. package/dist/components/Input.d.ts +12 -2
  67. package/dist/components/Input.js +5 -3
  68. package/dist/components/Kbd.d.ts +6 -1
  69. package/dist/components/Kbd.js +6 -6
  70. package/dist/components/Lightbox.d.ts +41 -5
  71. package/dist/components/Lightbox.js +95 -9
  72. package/dist/components/MatchReview.d.ts +40 -1
  73. package/dist/components/MatchReview.js +130 -4
  74. package/dist/components/MetricInput.js +3 -2
  75. package/dist/components/Modal.d.ts +2 -1
  76. package/dist/components/Modal.js +13 -4
  77. package/dist/components/NavFavourites.d.ts +5 -0
  78. package/dist/components/NavFavourites.js +7 -7
  79. package/dist/components/OtpInput.d.ts +33 -0
  80. package/dist/components/OtpInput.js +123 -0
  81. package/dist/components/PageHeader.d.ts +7 -1
  82. package/dist/components/PageHeader.js +2 -2
  83. package/dist/components/Pagination.js +6 -5
  84. package/dist/components/PasswordInput.d.ts +34 -0
  85. package/dist/components/PasswordInput.js +101 -0
  86. package/dist/components/Peek.js +3 -3
  87. package/dist/components/Popover.js +2 -0
  88. package/dist/components/PrintLayout.d.ts +63 -0
  89. package/dist/components/PrintLayout.js +114 -0
  90. package/dist/components/QrCode.d.ts +47 -0
  91. package/dist/components/QrCode.js +55 -0
  92. package/dist/components/QrScanner.d.ts +95 -0
  93. package/dist/components/QrScanner.js +266 -0
  94. package/dist/components/RelationshipGraph.d.ts +32 -1
  95. package/dist/components/RelationshipGraph.js +18 -5
  96. package/dist/components/RelationshipGraphLayered.d.ts +1 -1
  97. package/dist/components/RelationshipGraphLayered.js +190 -115
  98. package/dist/components/RelationshipGraphList.js +33 -6
  99. package/dist/components/RuleBuilder.js +39 -7
  100. package/dist/components/SchedulingAgenda.d.ts +3 -1
  101. package/dist/components/SchedulingCalendar.d.ts +3 -1
  102. package/dist/components/SchedulingCalendar.js +43 -4
  103. package/dist/components/SchedulingResourceDay.d.ts +57 -0
  104. package/dist/components/SchedulingResourceDay.js +287 -0
  105. package/dist/components/SchedulingTimeGrid.d.ts +3 -1
  106. package/dist/components/SchedulingTimeline.d.ts +3 -1
  107. package/dist/components/SearchSuggest.d.ts +23 -3
  108. package/dist/components/SearchSuggest.js +48 -17
  109. package/dist/components/Segmented.d.ts +39 -4
  110. package/dist/components/Segmented.js +31 -3
  111. package/dist/components/Select.d.ts +3 -2
  112. package/dist/components/Select.js +1 -1
  113. package/dist/components/ShortcutHint.d.ts +11 -10
  114. package/dist/components/ShortcutHint.js +44 -38
  115. package/dist/components/Sidebar.d.ts +36 -4
  116. package/dist/components/Sidebar.js +37 -12
  117. package/dist/components/SortableList.d.ts +45 -0
  118. package/dist/components/SortableList.js +134 -0
  119. package/dist/components/Stat.d.ts +28 -3
  120. package/dist/components/Stat.js +13 -3
  121. package/dist/components/StatusPill.d.ts +14 -3
  122. package/dist/components/StatusPill.js +11 -3
  123. package/dist/components/Steps.js +2 -1
  124. package/dist/components/Table.d.ts +9 -2
  125. package/dist/components/Table.js +4 -2
  126. package/dist/components/TableControls.js +18 -58
  127. package/dist/components/TableOverflowProbe.d.ts +12 -0
  128. package/dist/components/TableOverflowProbe.js +27 -0
  129. package/dist/components/Tabs.d.ts +2 -1
  130. package/dist/components/Tabs.js +1 -0
  131. package/dist/components/TagInput.d.ts +67 -0
  132. package/dist/components/TagInput.js +342 -0
  133. package/dist/components/Textarea.d.ts +5 -0
  134. package/dist/components/Textarea.js +2 -2
  135. package/dist/components/TimePicker.d.ts +80 -0
  136. package/dist/components/TimePicker.js +347 -0
  137. package/dist/components/Toast.js +3 -2
  138. package/dist/components/Tooltip.d.ts +8 -4
  139. package/dist/components/Tooltip.js +24 -21
  140. package/dist/components/Tree.d.ts +8 -0
  141. package/dist/components/Tree.js +11 -5
  142. package/dist/display-preferences-model.d.ts +53 -0
  143. package/dist/display-preferences-model.js +64 -0
  144. package/dist/emoji.cjs +28 -35
  145. package/dist/filter-model.d.ts +59 -3
  146. package/dist/filter-model.js +67 -2
  147. package/dist/hooks/useChangeHighlight.d.ts +33 -0
  148. package/dist/hooks/useChangeHighlight.js +44 -0
  149. package/dist/hooks/useDialog.d.ts +5 -0
  150. package/dist/hooks/useDialog.js +3 -2
  151. package/dist/hooks/useOptimisticValue.d.ts +51 -0
  152. package/dist/hooks/useOptimisticValue.js +61 -0
  153. package/dist/hooks/useReducedMotion.d.ts +4 -1
  154. package/dist/hooks/useReducedMotion.js +7 -1
  155. package/dist/hooks/useResolvedFilterLabels.d.ts +15 -0
  156. package/dist/hooks/useResolvedFilterLabels.js +34 -0
  157. package/dist/hooks/useVirtualRows.d.ts +21 -2
  158. package/dist/hooks/useVirtualRows.js +30 -3
  159. package/dist/icons/Icon.d.ts +1 -1
  160. package/dist/index.cjs +18644 -13210
  161. package/dist/index.d.cts +3003 -699
  162. package/dist/index.d.ts +42 -16
  163. package/dist/index.js +139 -7
  164. package/dist/labels-context.d.ts +90 -3
  165. package/dist/overlay-stack.d.ts +6 -0
  166. package/dist/overlay-stack.js +13 -0
  167. package/dist/qr-model.d.ts +65 -0
  168. package/dist/qr-model.js +323 -0
  169. package/dist/relationship-graph-model.d.ts +78 -3
  170. package/dist/relationship-graph-model.js +112 -5
  171. package/dist/reorder.d.ts +30 -0
  172. package/dist/reorder.js +74 -0
  173. package/dist/rich-text.cjs +4 -2
  174. package/dist/scheduling-resource-model.d.ts +76 -0
  175. package/dist/scheduling-resource-model.js +74 -0
  176. package/dist/search-suggest-model.js +3 -1
  177. package/dist/shell-context.d.ts +11 -0
  178. package/dist/shell-context.js +7 -0
  179. package/dist/time-field-model.d.ts +46 -0
  180. package/dist/time-field-model.js +152 -0
  181. package/dist/toast-store.d.ts +17 -1
  182. package/dist/toast-store.js +23 -7
  183. package/package.json +2 -1
package/README.md CHANGED
@@ -256,10 +256,39 @@ const showSpinner = useDelayedPending(upload.isPending);
256
256
  {showSpinner && <Spinner size="sm" label={t('uploading')} />}
257
257
  ```
258
258
 
259
+ ### What changed: highlight, count, optimistic values (motion standard §2.5, §2.6, §2.8)
260
+
261
+ - **`ChangeHighlight value={…}`** (or `useChangeHighlight(value)` on your own element, or
262
+ `.uix-changed` on plain markup) tints the element when `value` changes after mount and fades
263
+ the tint; never on the first render; a second change restarts it. `as="tr"` tints the row's
264
+ cells; `variant="ring"` rings something with a colour of its own (a status pill, a star).
265
+ Under reduced motion or forced colours it is a steady outline for the same time. Give it
266
+ words: `announce="Status changed to Resolved by A. Novak"` speaks through a polite region
267
+ (inside the open dialog when there is one). `flashChanged(el)` and `announcePolite(text)`
268
+ are the same pieces for changes that do not flow through a React value.
269
+ - **`Count value={n}`** for a badge or stat value: one 4 px tick in the change's direction with
270
+ a fade, tabular figures, `format` for "99+". Never a loop, never a count-up.
271
+ - **`useOptimisticValue(serverValue, commit, { onRollback })`** only for reversible, personal
272
+ actions (star, pin, reaction, mark read, display preference, own saved-view order). It shows
273
+ the change at once; if `commit` rejects, the last confirmed value comes back, `rollbacks`
274
+ counts up (flash it with `ChangeHighlight value={rollbacks}`) and `onRollback` lets you say
275
+ what was restored. State transitions, assignment, approvals and priority stay pessimistic.
276
+
277
+ ```tsx
278
+ <ChangeHighlight as="tr" value={row.updatedAt} announce={t('rowUpdated', { id: row.id, actor })}>…</ChangeHighlight>
279
+ <Td><ChangeHighlight value={row.status} variant="ring"><StatusPill tone={tone}>{label}</StatusPill></ChangeHighlight></Td>
280
+ <span className="uix-navitem__badge"><Count value={unread} format={(n) => (n > 99 ? '99+' : String(n))} /></span>
281
+
282
+ const star = useOptimisticValue(view.starred, (next) => api.star(view.id, next), { onRollback: () => toast.error(t('starNotSaved')) });
283
+ <ChangeHighlight value={star.rollbacks} variant="ring" announce={star.rollback ? t('starRestored') : undefined}>
284
+ <StarButton pinned={star.value} label={view.name} onToggle={(next) => star.set(next)} />
285
+ </ChangeHighlight>
286
+ ```
287
+
259
288
  ### Scrolling tabs, toned stat tiles and copying
260
289
 
261
290
  - **`Tabs overflow="scroll"`** keeps a long tab row on one line that scrolls sideways. The selected tab is always scrolled into view, and edge buttons appear only on the side that has hidden tabs. The buttons are pointer-only: keyboard users move with Arrow, Home and End. Combine it with `TabPanel keepMounted` on a record page with many sections.
262
- - **`Stat tone`** (`warning`, `danger`) draws a toned outline and value; the label stays muted, so the tone never replaces words. `size="compact"` fits dense fact bands.
291
+ - **`Stat tone`** (`warning`, `danger`) colours the value and leads it with a dot of that tone; the border stays neutral and the label muted, so the tone never replaces words (a toned outline before 2.48). `size="compact"` fits dense fact bands.
263
292
  - **`Stat onActivate`** makes the whole tile a button that opens an editor (`aria-haspopup="dialog"`). Pass `activateLabel` (e.g. "change") so the name reads "Priority: P1, change", and `expanded` while the editor is open. The ref points at the button, for anchoring a popover.
264
293
  - **`Stat pending`** is for a tile whose action is still running (a save, a state change). The button gets `aria-busy="true"` and `aria-disabled="true"`, is dimmed with a progress cursor, and calls neither `onActivate` nor your `onClick` / `onKeyDown` until `pending` is false again. It is never natively `disabled`, so it keeps focus and a screen reader can still read it. Only a tile with `onActivate` has the state; a read-only tile and a link tile ignore the prop.
265
294
  - **Tiles in one row line up.** A grid or flex row stretches its tiles to the tallest one. Read-only, button and link tiles all start their label and value at the same offset from the top edge, whatever the height (before 2.38.0 a button tile centred its rows).
@@ -1166,3 +1195,443 @@ mask; an avatar with a dot no longer clips it.
1166
1195
  `entityPicker`. `SearchSuggest` has the two slots these use: `idle` and `note`.
1167
1196
  - `CommandPalette` announces its result count through `UixLabelsProvider` `commandPalette`
1168
1197
  (`resultsOne`, `resultsMany`, `{count}`) or a `resultsLabel(count)` prop; English is unchanged.
1198
+
1199
+ ### Codes, contacts and paper (batch 5)
1200
+
1201
+ #### QrCode
1202
+
1203
+ `QrCode` draws `value` as a scannable SVG QR code with the standard four-module quiet zone.
1204
+ Name it for where it leads (`label`), pick `errorCorrection` (`M` by default; `Q` or `H` for a
1205
+ poster that may be creased or partly covered) and a `size`: CSS pixels, or a length such as
1206
+ `'32mm'` on paper. Keep at least 2 px per module on a screen and 0.4 mm per module on paper.
1207
+ The symbol is ink on paper in light, dark, print and forced colours, because scanners read dark
1208
+ on light. The encoder is the kit's own (`encodeQr`, `qrPath`), with no runtime dependency.
1209
+
1210
+ ```tsx
1211
+ <QrCode value={campaignUrl} label={t('qrOpensCampaign')} errorCorrection="Q" size="40mm" />
1212
+ ```
1213
+
1214
+ #### CodeDisplay
1215
+
1216
+ `CodeDisplay` shows a one-time code large enough to read across a counter: monospace groups
1217
+ (`groupSize`, or the code's own spaces and hyphens), a visible `label`, and a screen-reader
1218
+ spelling character by character (`spell` for a spelling alphabet or to tell O from 0).
1219
+ `expiresAt` adds a countdown that is never a live region: a screen reader hears "{time} left"
1220
+ once at `warnBefore` seconds (60) and "expired" once, and `onExpire` runs once. An expired code
1221
+ is struck through and says so in words; `copyable` adds a `CopyButton` while the code is live.
1222
+ The product makes the code and enforces its expiry.
1223
+
1224
+ ```tsx
1225
+ <CodeDisplay code={pairing.code} label={t('pairingCode')} expiresAt={pairing.expiresAt} copyable onExpire={refresh} />
1226
+ ```
1227
+
1228
+ #### ContactCard
1229
+
1230
+ `ContactCard` renders `.uix-contact` in a card (`bare` for a peek, a popover or a card body that
1231
+ already has padding): avatar, `name` (a quiet link with `href` / `renderLink`), `role`, the
1232
+ `email` and `phone` as selectable text, and Email / Call as `mailto:` / `tel:` `ButtonLink`s
1233
+ named for the person ("Email Amra Hadžić"), then your own `actions` and `stats`.
1234
+ `state="purged"` (the person's data was erased) shows a neutral placeholder and nothing that
1235
+ reaches anyone, even when stale props still carry an address; `state="unavailable"` keeps the
1236
+ name and says the details are unavailable. Words: `labels` or `UixLabelsProvider` `contactCard`.
1237
+
1238
+ ```tsx
1239
+ <ContactCard name={p.name} role={p.role} avatar={<Avatar size="lg" src={p.photo} alt="" />} email={p.email} phone={p.phone}
1240
+ state={p.erasedAt ? 'purged' : 'active'} href={`/people/${p.id}`} renderLink={(props) => <Link {...props} />} />
1241
+ ```
1242
+
1243
+ #### EventRow slots
1244
+
1245
+ `EventRow` takes `weekday`, `year` and `time` in its date tile (the tile widens to its longest
1246
+ line), `dateTime` to make the tile a `<time>` with a machine-readable date, and `media` for a
1247
+ thumbnail at the row's end: an `<img>` drawn 4:3 and cropped to fill, moved under the text on a
1248
+ phone. Without them the markup is unchanged.
1249
+
1250
+ ```tsx
1251
+ <EventRow weekday="Wed" month="Oct" day="22" year="2026" time="18:00" dateTime="2026-10-22T18:00+02:00"
1252
+ title={<Link href="/program/42">Book launch</Link>} meta="National library" media={<img src={cover} alt="" />} />
1253
+ ```
1254
+
1255
+ #### PrintLayout
1256
+
1257
+ `PrintLayout` is a print document frame for the browser's own print (`window.print()`): `size`
1258
+ `A4` (default) or `A5`, `orientation`, a `header` and a `footer` that repeat on every printed page
1259
+ (it renders a layout `<table>`: Chromium repeats a real `<thead>` / `<tfoot>`, not a CSS table of
1260
+ divs), "page / pages" in the bottom-right margin (`pageNumbers`), and ink-on-paper tokens for
1261
+ everything inside, also as a paper preview on screen in the dark theme. While it is on the page,
1262
+ printing shows only the documents (`isolate`, default): the app shell, toolbars and toasts are
1263
+ left out and scroll boxes cannot cut the document. `singlePage` keeps a poster or a one-page
1264
+ readout in one piece; when its content does not fit, the preview is outlined and
1265
+ `onOverflowChange(true)` is called. `PrintSignature` is a signature line with place and date and
1266
+ an optional stamp box (never split across pages); `PrintPageBreak` starts a new page. Utilities:
1267
+ `.uix-print-keep`, `.uix-print-hide`, `.uix-print-only`.
1268
+
1269
+ ```tsx
1270
+ <PrintLayout header={<PracticeHeader />} footer={<DoctorFooter />}>
1271
+ <h1>{t('findings')}</h1>
1272
+ <DescriptionList items={patient} />
1273
+ <PrintSignature placeAndDate={placeAndDate} name={doctor} stamp />
1274
+ </PrintLayout>
1275
+ ```
1276
+
1277
+ #### QrScanner
1278
+
1279
+ `QrScanner` is a camera viewfinder for a decoder you supply: `decode(frame)` gets
1280
+ `{ imageData, video }` (frames at most `maxFrameSize` px on the long side, every `interval` ms)
1281
+ and returns the text it read; the kit ships no decoder and makes no network request.
1282
+ `onResult(value)` is called once per new text (the same text again within `repeatAfter` ms is
1283
+ not repeated), and a status message says it once. Before the browser's prompt the scanner shows
1284
+ your `explainer` and a start button (with `autoStart`, a camera the browser already allows starts
1285
+ at once); after a refusal it says how to allow the camera again; it says so when the browser has
1286
+ no camera API, no camera is found, or the camera sends no picture. `manualEntry` is rendered in
1287
+ every state. A hint appears after `hintAfter` ms of frames without a code; a torch toggle appears
1288
+ when the camera has one. The camera stops on Stop, on unmount and while the page is hidden.
1289
+
1290
+ ```tsx
1291
+ <QrScanner decode={({ imageData }) => wasmDecoder.read(imageData)} onResult={redeem}
1292
+ explainer={<p>{t('cameraWhy')}</p>} manualEntry={<VoucherCodeField />} />
1293
+ ```
1294
+
1295
+ A scan trail (what was read, what the product did with it, what to do next) is the product's:
1296
+ keep `onResult` and `onStatusChange` in state and show the last few as a `Timeline` under the
1297
+ scanner, newest first, with the outcome in words ("Not changed: already lent").
1298
+
1299
+ #### SchedulingCalendar: the day by resource
1300
+
1301
+ `view="day"` with `resources` draws one column per resource (a practitioner, a room) on a slot
1302
+ axis in `timeZone`: `resourceSpans` are the free, booked and off stretches the server composed;
1303
+ a free span is cut into bookable slots of `slotMinutes` (15), a booking and time off stay whole,
1304
+ and a time no span covers is hatched "no schedule" (never free; nothing is inferred). `dayStart`
1305
+ / `dayEnd` set the axis (07:00–19:00). `onSelectSlot` gets `{ resourceId, start, end, kind, span }`
1306
+ for a click, Enter or Space on a slot or a booking; `selectedSlot` marks one. The view is one tab
1307
+ stop: arrows move between slots and resources, Home / End to a column's ends. The gutter and the
1308
+ heads stay in view while the grid scrolls. `loading` shows skeletons on the first load and dims
1309
+ what is on screen on a refetch. Below `resourceListBelow` (480 px of its own width) the day is a
1310
+ list, one resource after another (`resourceLayout` fixes either).
1311
+
1312
+ ```tsx
1313
+ <SchedulingCalendar view="day" anchorDate={day} timeZone={tenantZone} entries={[]}
1314
+ resources={practitioners} resourceSpans={spans} slotMinutes={15} dayStart="07:30" dayEnd="17:00"
1315
+ onSelectSlot={openSlotPopover} selectedSlot={open && { resourceId: open.resourceId, start: open.start }} loading={isFetching} />
1316
+ ```
1317
+
1318
+ #### RelationshipGraph: gaps on the map
1319
+
1320
+ For a map of what is expected and what is there (TENSOR's K3 gaps): an edge takes
1321
+ `variant: 'dashed'` (an expected relation that is missing), a `tone` and a `description` that
1322
+ both nodes say to a screen reader ("covers, from Control K3.2 (expected, missing)"); a node takes
1323
+ a `tone`; a legend item takes the same `tone`, a `shape` (`edge` or `node`) and a `variant`, so
1324
+ it matches what it explains. Say a tone's meaning in words too (`description`, the node's text).
1325
+ `revealId` scrolls a node into view without moving focus (a table row beside the map), and with
1326
+ reduced motion it jumps. Without `rootId` no node gets the root border (`markRoot` forces it
1327
+ either way). `controls="none"` hides the toolbar in the radial layout as well. Edge labels that
1328
+ would overlap in a dense column are moved apart, or left out when they would drift too far (the
1329
+ relationship stays in the nodes' descriptions and the list). An edge across more than one column
1330
+ is a solid "skip" edge in a channel under the columns it crosses, no longer dashed or flagged as a
1331
+ cycle; `LayeredEdgeKind` gains `skip` and `back`.
1332
+
1333
+ `RelationshipGraph layout="layered"` was measured at CMDB topology sizes (HAR-1387; 300 and 600
1334
+ items): no items overlap; a column that takes many forward edges says their common relation once
1335
+ in its header and only edges that differ keep a label; a busy node fans out along one bus; a
1336
+ node's description names five relationships and points to the list, whose rows list at most eight
1337
+ neighbours until "Show all N" (`connectedHeading`, `showAllRelations`, `showFewerRelations`,
1338
+ `moreRelations`); a pan or a zoom only moves the stage, so dragging stays at the display's frame
1339
+ rate. A force layout was not needed.
1340
+
1341
+ ### Form fields (HAR-1812, HAR-1799, …)
1342
+
1343
+ #### CheckboxGroup
1344
+
1345
+ ```tsx
1346
+ <CheckboxGroup label={t('tags')} hint={t('tagsHint')} error={errors.tags} columns={3} required>
1347
+ {tags.map((tag) => (
1348
+ <Checkbox key={tag.id} name="tags" value={tag.id} label={tag.name}
1349
+ checked={chosen.includes(tag.id)} onChange={() => toggle(tag.id)} />
1350
+ ))}
1351
+ </CheckboxGroup>
1352
+ ```
1353
+
1354
+ The checkbox counterpart of `<RadioGroup label>`: a `<fieldset>` whose `<legend>` names the group,
1355
+ so it is announced with each box, and the field's message slot below (`hint`, or `error` with
1356
+ `role="alert"`, tied to the group with `aria-describedby`). `columns` is a ceiling: no column is
1357
+ narrower than 10rem, so a phone gets one. `disabled` disables every box; `required` draws the
1358
+ marker (which boxes must be ticked is your rule).
1359
+
1360
+ #### PasswordInput and OtpInput
1361
+
1362
+ ```tsx
1363
+ <Field label={t('password')} error={errors.password}>
1364
+ <PasswordInput name="password" autoComplete="current-password" />
1365
+ </Field>
1366
+ <Field label={t('code')} hint={t('codeHint')} error={errors.code}>
1367
+ <OtpInput name="code" length={6} groupSize={3} onComplete={(code) => verify(code)} />
1368
+ </Field>
1369
+ ```
1370
+
1371
+ `PasswordInput` is an `Input type="password"` with a reveal toggle inside its end: a button with
1372
+ `aria-pressed` whose name stays "Show password" (`labels.show`, or `UixLabelsProvider`
1373
+ `passwordInput`). A pointer press keeps the caret in the field; Tab reaches the toggle. When the
1374
+ form submits, a revealed password is hidden first, so the browser still offers to save it.
1375
+ `revealed` / `defaultRevealed` / `onRevealedChange`, `size`, and every `Input` prop pass through.
1376
+
1377
+ `OtpInput` is ONE input (`inputMode="numeric"`, `autoComplete="one-time-code"`, `pattern`;
1378
+ no `maxLength`, so an autofilled "ABCD-EFGH" is cleaned before it is cut) drawn as `length` cells, so the phone's code autofill, password managers and screen
1379
+ readers see one field. Typing or pasting "123 456" keeps the characters; `onComplete(code)` runs
1380
+ once each time the code fills. `mode="alphanumeric"` takes letters (upper-cased); `groupSize`
1381
+ draws a gap; `aria-invalid` (from `Field error`) turns the cells red.
1382
+
1383
+ `InputGroup` gains `trailingAction`: a real control inside the field's end (class
1384
+ `uix-input-group__action`, square, the field's height less 4 px a side), which both use.
1385
+
1386
+ #### TimePicker
1387
+
1388
+ ```tsx
1389
+ <Field label={t('opensAt')} hint={t('businessHours')}>
1390
+ <TimePicker name="opens" value={opens} onValueChange={setOpens} locale={locale} minuteStep={30} min="08:00" max="18:00" />
1391
+ </Field>
1392
+ ```
1393
+
1394
+ A time of day beside `DatePicker`; the value is `HH:mm` (what `<input type="time">` holds) or
1395
+ `null`. The field shows the locale's clock (14:30 in `de`, 2:30 PM in `en-US`; `hour12` overrides)
1396
+ and reads a typed time on Enter or blur: `1430`, `14.30`, `9`, `2:30 pm`. ArrowUp / ArrowDown step
1397
+ the part under the caret (the hour, or the minutes to the next `minuteStep`), PageUp / PageDown the
1398
+ hour, Alt+ArrowDown opens the list. The clock button lists the times at `minuteStep` between `min`
1399
+ and `max`; the list takes focus on the chosen time, Enter picks, Escape closes and focus goes back
1400
+ to the field. Text that is not a time, or one before `min` / after `max`, marks the field invalid and
1401
+ says why. `name` submits `HH:mm` through a hidden input. Words: `labels` or `UixLabelsProvider`
1402
+ `timePicker`. `DateTimePicker` now uses it for its time (no native `type="time"` is left);
1403
+ `minuteStep` defaults to 15 there and only steps the keys and the list, any typed minute is kept.
1404
+
1405
+ #### Date fields without call-site workarounds (HAR-2432)
1406
+
1407
+ ```tsx
1408
+ <UixLabelsProvider labels={{
1409
+ dateFormat: { locale: 'de', formatDate: toDDMMYYYY, parseDate: fromDDMMYYYY, placeholder: 'TT.MM.JJJJ' },
1410
+ dateRangePicker: { label: t('chooseRange'), invalidMessage: t('rangeInvalid') },
1411
+ }}>
1412
+ ```
1413
+
1414
+ - `dateFormat` sets `locale`, `formatDate`, `parseDate` and `placeholder` for every `DatePicker`,
1415
+ `DateTimePicker`, `DateRangePicker` (and the `locale` of `TimePicker`) below the provider; a prop
1416
+ on one field still wins.
1417
+ - `FilterPopover type="date"` renders the kit `DatePicker`; the value stays `YYYY-MM-DD` (`''` for
1418
+ none).
1419
+ - `DateRangePickerLabels` has `label` and `invalidMessage`, so no English default is left outside
1420
+ the labels group; the props still win.
1421
+ - `DateTimePicker` names its time from the label's accessible text (an `aria-hidden` "*" is left
1422
+ out) and follows the label when its text changes after mount.
1423
+
1424
+ #### Combobox at parity with Select (HAR-1588)
1425
+
1426
+ ```tsx
1427
+ <span id="cat-label" className="uix-field__label">{t('category')}</span>
1428
+ <Combobox aria-labelledby="cat-label" label={t('category')} emptyLabel={t('noMatch')} value={category} onValueChange={setCategory}
1429
+ options={[{ label: t('hardware'), options: hardware }, { label: t('software'), options: software }, other]} />
1430
+ ```
1431
+
1432
+ The list is a top-layer popover placed with `useAnchoredPosition` (like `Select`), so a Drawer, a
1433
+ Popover or an `overflow: hidden` cell never clips it. ↑ ↓ PageUp PageDown move (Home End too while
1434
+ an option is active), disabled options are skipped and the list does not wrap; Enter chooses;
1435
+ Escape closes it and puts the chosen option's text back, as leaving the field does. `options` take
1436
+ groups (`{ label, options, disabled }`, rendered as `role="group"`); the no-match row is outside the
1437
+ listbox; the chosen option has a check mark. New: `aria-labelledby` (replaces `label` as the name),
1438
+ `aria-describedby`, `aria-invalid` and `inputId` land on the input; `placement`;
1439
+ `filterComboboxItems`.
1440
+
1441
+ #### TagInput
1442
+
1443
+ ```tsx
1444
+ <Field label={t('labels')} hint={t('labelsHint')}>
1445
+ <TagInput name="labels" value={labels} onValueChange={setLabels} suggestions={known} maxTags={10}
1446
+ validate={(tag) => (tag.length > 40 ? t('tooLong') : null)} />
1447
+ </Field>
1448
+ ```
1449
+
1450
+ Type and press Enter or a comma to add a chip; a paste of "a, b, c" adds three. A duplicate (ignoring
1451
+ case and accents), a `validate` refusal or one past `maxTags` keeps the text and says why under the
1452
+ field. Suggestions (those not added yet) open in a top-layer list once the person types or presses
1453
+ ArrowDown; Enter adds the active one. Backspace in the empty field reaches the last chip; ArrowLeft /
1454
+ ArrowRight move between chips and back; Backspace or Delete removes the focused chip and focus stays
1455
+ with the tags; the field is the only tab stop. Each change is announced politely. Inside `Field` the
1456
+ text input gets the label, hint and error; `name` submits one value per tag. Words: `labels` or
1457
+ `UixLabelsProvider` `tagInput`.
1458
+
1459
+ #### SearchSuggest freeText (HAR-2465)
1460
+
1461
+ ```tsx
1462
+ <label className="uix-field__label" htmlFor="maker">{t('manufacturer')}</label>
1463
+ <SearchSuggest freeText inputId="maker" name="manufacturer" label={t('manufacturer')}
1464
+ value={maker} onValueChange={setMaker} options={spellings} loading={loading} />
1465
+ ```
1466
+
1467
+ A text field with suggestions: the value is the text. Enter keeps what was typed and never submits
1468
+ the surrounding form (also while `loading`); no row is picked unless the person moves to it
1469
+ (ArrowDown / ArrowUp, then Enter) or clicks it, and then its title replaces the text through
1470
+ `onValueChange`, again without submitting. Blur, Tab and Escape keep the text (a second Escape does
1471
+ not clear it). No magnifier is drawn. `onSelect` is optional. Without `freeText` the search mode is
1472
+ unchanged. Decision: `Docs/adr/0004-search-suggest-free-text.md`.
1473
+
1474
+ #### EntityPicker and SearchSuggest gaps (HAR-1834)
1475
+
1476
+ - A failure Retry cannot fix: reject `onSearch` with `{ retryable: false, message? }`, or pass
1477
+ `isRetryable(error)`. The list shows the message (or `labels.forbidden`) and no Retry row.
1478
+ - `SearchSuggest` takes `disabled` and `invalid`; `EntityPicker` forwards them, so an empty disabled
1479
+ picker cannot be typed in and an empty required one with an error is announced invalid.
1480
+ - Clear starts a new search: the old query and rows are gone and the field has focus.
1481
+ - A visible label names the field: with a `<label for>` (a `Field`) or `aria-labelledby`
1482
+ (`EntityPicker`) / `inputLabelledBy` (`SearchSuggest`), no `aria-label` overrides it.
1483
+ - While the next query loads the last rows stay in view (`aria-busy` at once), and the spinner waits
1484
+ 300 ms (`useDelayedPending`), so a fast answer shows none; Enter during a search picks nothing and
1485
+ submits nothing.
1486
+ ### Calm status colour, the app shell and display preferences (2.48)
1487
+
1488
+ #### StatusPill treatments, Stat sentiment, quiet Alert (HAR-1563)
1489
+
1490
+ ```tsx
1491
+ <StatusPill tone="danger" treatment="dot">Breached</StatusPill> // a table cell
1492
+ <StatusPill tone="progress">In progress</StatusPill> // the record's header (soft, the default)
1493
+ <StatusPill tone="warning" treatment="outline">Due in 2h</StatusPill> // a flag among other chrome
1494
+ <Stat label="Breached SLAs" value={7} trend={{ direction: 'up', label: '2', sentiment: 'negative' }} />
1495
+ ```
1496
+
1497
+ - `StatusPill treatment`: `'soft'` (default, the tinted wash; `'filled'` is its old name and still works), `'dot'`
1498
+ (no fill, a neutral label, the tone only in the dot: for tables) and `'outline'` (a neutral hairline, a hushed
1499
+ label and the dot; only `critical` keeps a toned border). `dot` and `outline` always draw the dot. **Changed:**
1500
+ `outline` used to colour the label and a 40 % border in the tone's hue.
1501
+ - `StatusPill tone="progress"`: in flight, the same blue as `info`.
1502
+ - `Stat trend.sentiment`: `'positive'` (green), `'negative'` (red) or `'neutral'` (grey, the default). **Changed:** the
1503
+ chip used to be green for `up` and red for `down`, so a rise in breached SLAs showed green. Pass the sentiment the
1504
+ change has for your metric.
1505
+ - `Stat tone`: the border stays neutral and a dot leads the toned value (**changed:** the border was toned).
1506
+ - `Alert`: the borders are a 24 % hairline of the tone (**changed:** 30 to 40 %); the warning icon is the text
1507
+ role, at least 3:1 on its tint in both themes.
1508
+
1509
+ #### A Stat link tile looks like a link at rest (HAR-1803)
1510
+
1511
+ A tablet has no hover, so a tile that leads somewhere has to show it before it is touched. A link tile
1512
+ (`href`) draws a trailing chevron (`ChevronRightIcon`) in its label row; a tile that opens a dialog or menu
1513
+ keeps its down chevron; an in-place action tile (`onActivate` with `haspopup={false}`, class
1514
+ `.uix-stat--action`) underlines its value with a dotted line. All three are shapes, so they read without
1515
+ colour and in forced-colours mode, and none depends on hover. A plain figure tile is unchanged. The
1516
+ hover tint of interactive tiles now applies only where the device can hover (`@media (hover: hover)`).
1517
+
1518
+ ```tsx
1519
+ <Stat label={t('overdue')} value={4} href="/loans?overdue" renderLink={toRoute} /> // › at rest
1520
+ ```
1521
+
1522
+ #### Inset: a well inside a card body (HAR-1818)
1523
+
1524
+ ```tsx
1525
+ <Card title={t('current')}>
1526
+ <Inset role="group" aria-label={t('headerPreview')}><BrandBar brand={brand} /></Inset>
1527
+ <Inset padding="sm"><code>{css}</code></Inset>
1528
+ </Card>
1529
+ ```
1530
+
1531
+ `Inset` (`.uix-inset`) frames a preview or a specimen inside a card body: a hairline border, the md radius,
1532
+ the subtle background and `--uix-space-4` padding (`padding="sm"`: `--uix-space-3`). Use it instead of a `Card`
1533
+ in a `Card`, which brings a second shadow, card padding and card semantics. `tone="surface"` is for a well on
1534
+ a panel that is already subtle. It lays out nothing; give it a `role="group"` and a name when the preview needs
1535
+ one.
1536
+
1537
+ #### Icons, not text glyphs (HAR-1576)
1538
+
1539
+ Toast, Pagination, MetricInput, SavedViewMenu and Steps draw the icon set (`XIcon`, the chevrons, `PlusIcon`
1540
+ / `MinusIcon`, `StarIcon`) where they drew `× ‹ › « + − ★ !`. The table sort arrows and the `Select` trigger's
1541
+ chevron are the icon set's glyphs through a CSS mask in `currentColor`, so they follow the theme; plain CSS gets
1542
+ the same through `.uix-glyph`:
1543
+
1544
+ ```html
1545
+ <span class="uix-glyph uix-glyph--chevron-down" aria-hidden="true"></span>
1546
+ ```
1547
+
1548
+ `node packages/react/scripts/glyph-icons.mjs` is the guard (it runs in `npm test`): a glyph as the whole content
1549
+ of a button, a link or an icon slot, or in a CSS `content:`, fails with its file and line. Text that only looks
1550
+ like an icon (a Kbd legend, a diff sign) goes in `glyph-icons.allow.json` with a reason.
1551
+
1552
+ #### The staff shell: full viewport and the type presets (HAR-1815)
1553
+
1554
+ ```tsx
1555
+ <AppShell layout="viewport" sidebar={<StaffNav />}>
1556
+ <PageHeader size="display" eyebrow={t('today')} title={t('overview')} subtitle={t('overviewLead')} actions={actions} />
1557
+ </AppShell>
1558
+ <DetailPage variant="staff" back={{ href: '/loans', label: t('allLoans') }} title={loan.code} metrics={metrics}>
1559
+ <Card><SectionHead size="compact" as="h3" title={t('copies')} /><Table>…<CellSub tabular>{copy.inventory}</CellSub>…</Table></Card>
1560
+ </DetailPage>
1561
+ ```
1562
+
1563
+ - `AppShell layout="viewport"`: no frame, `100dvh`, no top-bar row (a `topbar` sits at the top of the main
1564
+ column), staff padding (`space-7 space-6 space-6`, `space-8` at the sides from 80rem), and below 40rem one
1565
+ column with the sidebar hidden. `layout="frame"` is the default and the old look.
1566
+ - `PageHeader size="display"`: 40 px heading-font title, bold hushed eyebrow, body-size hushed subtitle (42rem),
1567
+ actions on the bottom line.
1568
+ - `DetailPage variant="staff"`: h2 / 1.15 title, un-underlined hushed back link, eyebrow metric labels over
1569
+ regular tabular h3 values.
1570
+ - `SectionHead size="compact"` (15 px / 1.3 / 600) and `as="h3"`; `CellSub tabular`.
1571
+
1572
+ #### Sidebar actions and search, AccountMenu, the rail hotkey and the phone nav tier (HAR-1371)
1573
+
1574
+ ```tsx
1575
+ const [nav, setNav] = useState<ShellNav>('full');
1576
+ <AppShell layout="viewport" navHotkey="[" navTierStorageKey="staff-nav" onNavChange={setNav} phoneNav="drawer"
1577
+ sidebar={(
1578
+ <Sidebar brand={brand} nav={nav === 'rail' ? 'rail' : 'full'}
1579
+ actions={<><ModeToggle /><Menu trigger={<Button icon size="sm" variant="ghost" aria-label={t('tools')}><EllipsisIcon /></Button>}>…</Menu></>}
1580
+ search={{ label: t('search'), shortcut: ['Mod', 'K'], onActivate: openPalette }}
1581
+ footer={<AccountMenu collapsed={nav === 'rail'} user={{ name, sub: email, status: 'online' }} items={<>
1582
+ <MenuItem href="/profile">{t('profile')}</MenuItem>
1583
+ <MenuItem onSelect={signOut} tone="danger">{t('signOut')}</MenuItem>
1584
+ </>} />}>
1585
+ {navItems}
1586
+ </Sidebar>
1587
+ )}>
1588
+ {page}
1589
+ </AppShell>
1590
+ ```
1591
+
1592
+ - `Sidebar actions` sit beside the brand (hidden in the rail); `search` is a row that opens your search and shows
1593
+ its keys (it does not bind them: your command palette listens); `footer` is pinned to the bottom.
1594
+ - `AccountMenu`: avatar (photo, your node, or initials), name and a second line as a `Menu` trigger;
1595
+ `collapsed` in the rail.
1596
+ - `navHotkey` switches full ↔ rail instantly, never while typing; `navTierStorageKey` remembers the tier before
1597
+ the first paint and reports it through `onNavChange`.
1598
+ - `phoneNav="drawer"`: at 40rem and under, a menu button opens the sidebar in a start-side `Drawer`.
1599
+
1600
+ #### Display preferences: theme, contrast, text size, motion, density (HAR-1372)
1601
+
1602
+ ```tsx
1603
+ // app/layout.tsx: before the stylesheets, so nothing flashes. The script sets attributes on <html>
1604
+ // before React hydrates, so <html> needs suppressHydrationWarning; under a Content-Security-Policy
1605
+ // give the inline script your per-request nonce (or allow its hash).
1606
+ <html lang="bs" suppressHydrationWarning>
1607
+ <head>
1608
+ <script nonce={nonce} dangerouslySetInnerHTML={{ __html: displayPreferencesScript('staff-display') }} />
1609
+ </head>
1610
+ <body>{children}</body>
1611
+ </html>
1612
+
1613
+ // a settings page, or a dialog from AccountMenu
1614
+ <DisplayPreferences storageKey="staff-display" />
1615
+ // the header
1616
+ <ThemeSwitcher storageKey="staff-display" display="icons" />
1617
+ // your own control
1618
+ const { preferences, setPreference, resolvedTheme } = useDisplayPreferences({ storageKey: 'staff-display' });
1619
+ ```
1620
+
1621
+ The tokens read five attributes on `<html>`: `data-theme` (light / dark), `data-contrast="more"`,
1622
+ `data-text-scale="sm|lg|xl"`, `data-motion="reduce"` and `data-density="compact|comfortable"`. A choice applies
1623
+ at once (transitions are suspended for that frame), is stored, and `system` follows the OS. The head script and the
1624
+ hook read the same key and the same defaults; pass both the same. `useReducedMotion` honours `data-motion`.
1625
+
1626
+ #### StatRow: a row of tiles that reflows
1627
+
1628
+ ```tsx
1629
+ <StatRow>{facts.map((f) => <Stat key={f.id} size="compact" {...f} />)}</StatRow>
1630
+ <StatRow className="uix-stat-row--hero">{kpis}</StatRow> // 12rem tiles for 34 px values
1631
+ ```
1632
+
1633
+ `StatRow` (`.uix-stat-row`) puts as many tiles side by side as fit at 8.5rem each (12rem with
1634
+ `.uix-stat-row--hero`, or as soon as a hero tile is in the row; `minTileWidth` / `--stat-row-min` to
1635
+ choose), all the same height, down to one per row on a phone. A fixed column count squeezed six tiles
1636
+ into 320 px until their labels broke letter by letter. Labels now wrap at a space and break inside a
1637
+ word only when that word alone is wider than the tile.
@@ -2,6 +2,8 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { CalendarWeekday } from './calendar-model.js';
4
4
  import { MoveProposal, MonthSpanLayout } from './scheduling-calendar-model.js';
5
+ import { SchedulingSlotSelection } from './components/SchedulingResourceDay.js';
6
+ import { SchedulingAvailabilityKind, SchedulingResource, SchedulingResourceSpan } from './scheduling-resource-model.js';
5
7
 
6
8
  interface TimeGridEntryContext {
7
9
  /** Whole text lines that fit in the item: 0 shows the markers only. */
@@ -236,6 +238,18 @@ interface SchedulingCalendarLabels {
236
238
  moveCancelled?: string;
237
239
  /** Announced when the time asked for does not exist that day. `{time}` is the time used instead. */
238
240
  gapForward?: string;
241
+ /** Names the resource day (HAR-983). `{date}` placeholder. */
242
+ resourceGrid?: string;
243
+ /** The words for the kinds of a resource span. */
244
+ availability?: Partial<Record<SchedulingAvailabilityKind, string>>;
245
+ /** Time of a resource that no span covers: no schedule, which is not free. */
246
+ noSchedule?: string;
247
+ /** A resource slot's or booking's accessible name. `{resource}`, `{start}`, `{end}`, `{kind}`, `{label}`. */
248
+ slotName?: string;
249
+ /** How to move in the resource day, read with it. */
250
+ resourceHint?: string;
251
+ /** The resource day with no resources. */
252
+ noResources?: string;
239
253
  }
240
254
  declare const DEFAULT_SCHEDULING_CALENDAR_LABELS: SchedulingCalendarLabels;
241
255
  interface SchedulingCalendarProps {
@@ -376,13 +390,48 @@ interface SchedulingCalendarProps {
376
390
  topLaneCap?: number;
377
391
  /** Lanes side by side in one day column of the time grid, at most. Fewer are used when the column is narrow. Default 4. */
378
392
  maxLanes?: number;
393
+ /**
394
+ * With `view="day"`, one column per resource (a practitioner, a room) instead of the time grid
395
+ * (HAR-983). Their stretches of the day come from `resourceSpans`; a time no span covers is
396
+ * shown as "no schedule", never as free.
397
+ */
398
+ resources?: SchedulingResource[];
399
+ /** The free, booked and off stretches of each resource, as the server composed them. */
400
+ resourceSpans?: SchedulingResourceSpan[];
401
+ /** Minutes per slot of the resource day: each free span is cut into bookable slots of this length. Default 15. */
402
+ slotMinutes?: number;
403
+ /** The first local time of the resource day's axis, `HH:MM`. Default `'07:00'`. */
404
+ dayStart?: string;
405
+ /** The local time the resource day's axis ends, `HH:MM`. Default `'19:00'`. */
406
+ dayEnd?: string;
407
+ /** Called by a click, Enter or Space on a free slot (its own start and end) or a booking. */
408
+ onSelectSlot?: (slot: SchedulingSlotSelection) => void;
409
+ /** Marks the slot or booking that holds this instant for this resource, e.g. while its popover is open. */
410
+ selectedSlot?: {
411
+ resourceId: string;
412
+ start: string;
413
+ };
414
+ /**
415
+ * `auto` (default): columns, and a list of one resource after another when the calendar is
416
+ * narrower than `resourceListBelow`. `columns` or `list` fixes one.
417
+ */
418
+ resourceLayout?: 'auto' | 'columns' | 'list';
419
+ /** The width in CSS pixels under which `auto` shows the list. Default 480. */
420
+ resourceListBelow?: number;
421
+ /**
422
+ * The first load shows skeletons; a refetch keeps what is on screen, dimmed and `aria-busy`
423
+ * (the resource day; the other views show `labels.loading` instead).
424
+ */
379
425
  loading?: boolean;
380
426
  error?: string;
381
427
  onRetry?: () => void;
382
428
  className?: string;
383
429
  labels?: Partial<SchedulingCalendarLabels>;
384
430
  }
385
- /** Month/week/agenda calendar for UTC ranges rendered in an explicit IANA time zone. */
386
- declare function SchedulingCalendar({ entries, anchorDate, timeZone, view: controlledView, onViewChange, onAnchorDateChange, onSelectEntry, overlays, onSelectOverlay, filter, renderEntry, locale, weekStartsOn, formatDate, formatInstant: formatInstantProp, maxEntriesPerDay, onShowMore, renderDayBadge, days: dayInfo, dayEntries, spanLayout, windowLaneCap, spanLaneCap, legend, legendCaption, showHeader, notice, emptyNote, onSelectDate, agendaGroups, agendaHeadingLevel, virtualizeAbove, monthDensity, timeGrid, now, canMove, step, onProposeMove, topLaneCrossMidnightMinutes, topLaneCap, maxLanes, loading, error, onRetry, className, labels: labelOverrides, }: SchedulingCalendarProps): react.JSX.Element;
431
+ /**
432
+ * Month/week/day/agenda calendar for UTC ranges rendered in an explicit IANA time zone. With
433
+ * `resources`, the day view is one column per resource (HAR-983).
434
+ */
435
+ declare function SchedulingCalendar({ entries, anchorDate, timeZone, view: controlledView, onViewChange, onAnchorDateChange, onSelectEntry, overlays, onSelectOverlay, filter, renderEntry, locale, weekStartsOn, formatDate, formatInstant: formatInstantProp, maxEntriesPerDay, onShowMore, renderDayBadge, days: dayInfo, dayEntries, spanLayout, windowLaneCap, spanLaneCap, legend, legendCaption, showHeader, notice, emptyNote, onSelectDate, agendaGroups, agendaHeadingLevel, virtualizeAbove, monthDensity, timeGrid, now, canMove, step, onProposeMove, topLaneCrossMidnightMinutes, topLaneCap, maxLanes, resources, resourceSpans, slotMinutes, dayStart, dayEnd, onSelectSlot, selectedSlot, resourceLayout, resourceListBelow, loading, error, onRetry, className, labels: labelOverrides, }: SchedulingCalendarProps): react.JSX.Element;
387
436
 
388
437
  export { DEFAULT_SCHEDULING_CALENDAR_LABELS as D, type SchedulingAgendaGroup as S, type TimeGridEntryContext as T, type SchedulingBand as a, SchedulingCalendar as b, type SchedulingCalendarDay as c, type SchedulingCalendarEntry as d, type SchedulingCalendarLabels as e, type SchedulingCalendarOverlay as f, type SchedulingCalendarProps as g, type SchedulingCalendarView as h, type SchedulingDatePart as i, type SchedulingEntryState as j, type SchedulingLegendItem as k, type SchedulingMarker as l, type SchedulingMarkerEmphasis as m, type SchedulingOverlayKind as n, type SchedulingOverlayPattern as o, type SchedulingStatus as p, SchedulingTimeGrid as q, type SchedulingTimeGridProps as r };
@@ -0,0 +1,10 @@
1
+ /** Create the polite region `announcePolite(…, from)` will use, ahead of its first message. */
2
+ declare function ensureAnnouncer(from?: Element | null): void;
3
+ /**
4
+ * Say `text` through a polite live region (`[data-uix-announcer]`): the page's, or the open
5
+ * dialog's when `from` is inside one. The region is cleared first and filled a moment later,
6
+ * so the same words twice are spoken twice; a newer message replaces one not yet spoken.
7
+ */
8
+ declare function announcePolite(text: string, from?: Element | null): void;
9
+
10
+ export { announcePolite, ensureAnnouncer };
@@ -0,0 +1,30 @@
1
+ const timers = /* @__PURE__ */ new WeakMap();
2
+ function ensureAnnouncer(from) {
3
+ liveRegion(from);
4
+ }
5
+ function liveRegion(from) {
6
+ if (typeof document === "undefined") return null;
7
+ const container = from?.closest("dialog") ?? document.body;
8
+ let region = container.querySelector(":scope > [data-uix-announcer]");
9
+ if (!region) {
10
+ region = document.createElement("div");
11
+ region.setAttribute("role", "status");
12
+ region.setAttribute("data-uix-announcer", "");
13
+ region.className = "uix-visually-hidden";
14
+ container.append(region);
15
+ }
16
+ return region;
17
+ }
18
+ function announcePolite(text, from) {
19
+ const el = liveRegion(from);
20
+ if (!el) return;
21
+ clearTimeout(timers.get(el));
22
+ el.textContent = "";
23
+ timers.set(el, setTimeout(() => {
24
+ el.textContent = text;
25
+ }, 0));
26
+ }
27
+ export {
28
+ announcePolite,
29
+ ensureAnnouncer
30
+ };