@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.
- package/README.md +470 -1
- package/dist/{SchedulingCalendar-B6TakO2h.d.ts → SchedulingCalendar-WWyFL3Eo.d.ts} +51 -2
- package/dist/announce.d.ts +10 -0
- package/dist/announce.js +30 -0
- package/dist/chart-analytic.cjs +33 -25
- package/dist/chart-preset.cjs +23 -15
- package/dist/chart.cjs +23 -15
- package/dist/combobox-model.d.ts +10 -1
- package/dist/combobox-model.js +15 -0
- package/dist/command-palette-model.d.ts +44 -0
- package/dist/command-palette-model.js +36 -0
- package/dist/components/AccountMenu.d.ts +44 -0
- package/dist/components/AccountMenu.js +43 -0
- package/dist/components/AppShell.d.ts +45 -2
- package/dist/components/AppShell.js +116 -5
- package/dist/components/AuditLog.d.ts +128 -0
- package/dist/components/AuditLog.js +196 -0
- package/dist/components/BrandProfiles.js +4 -3
- package/dist/components/Breadcrumbs.d.ts +9 -2
- package/dist/components/Breadcrumbs.js +3 -2
- package/dist/components/Button.d.ts +12 -5
- package/dist/components/Button.js +7 -3
- package/dist/components/Card.d.ts +18 -1
- package/dist/components/Card.js +5 -1
- package/dist/components/ChangeHighlight.d.ts +30 -0
- package/dist/components/ChangeHighlight.js +21 -0
- package/dist/components/ChartCore.js +2 -1
- package/dist/components/Checkbox.d.ts +27 -1
- package/dist/components/Checkbox.js +31 -2
- package/dist/components/CodeDisplay.d.ts +65 -0
- package/dist/components/CodeDisplay.js +124 -0
- package/dist/components/Combobox.d.ts +27 -6
- package/dist/components/Combobox.js +195 -58
- package/dist/components/CommandPalette.d.ts +36 -1
- package/dist/components/CommandPalette.js +254 -2
- package/dist/components/ContactCard.d.ts +66 -0
- package/dist/components/ContactCard.js +74 -0
- package/dist/components/Count.d.ts +24 -0
- package/dist/components/Count.js +32 -0
- package/dist/components/DataWorkspace.d.ts +167 -0
- package/dist/components/DataWorkspace.js +383 -0
- package/dist/components/DateFieldParts.d.ts +2 -1
- package/dist/components/DateFieldParts.js +7 -0
- package/dist/components/DatePicker.d.ts +9 -2
- package/dist/components/DatePicker.js +67 -28
- package/dist/components/DateRangePicker.d.ts +12 -2
- package/dist/components/DateRangePicker.js +13 -12
- package/dist/components/DetailPage.d.ts +7 -1
- package/dist/components/DetailPage.js +2 -1
- package/dist/components/Dialogs.d.ts +71 -10
- package/dist/components/Dialogs.js +130 -30
- package/dist/components/DiffViewer.d.ts +23 -3
- package/dist/components/DiffViewer.js +26 -12
- package/dist/components/DiscardPrompt.js +16 -0
- package/dist/components/DisplayPreferences.d.ts +83 -0
- package/dist/components/DisplayPreferences.js +185 -0
- package/dist/components/EditorialHome.d.ts +23 -3
- package/dist/components/EditorialHome.js +16 -11
- package/dist/components/EntityPicker.d.ts +20 -3
- package/dist/components/EntityPicker.js +32 -13
- package/dist/components/FilterEditor.d.ts +18 -4
- package/dist/components/FilterEditor.js +141 -19
- package/dist/components/InfoTip.js +11 -12
- package/dist/components/InlineEdit.d.ts +87 -20
- package/dist/components/InlineEdit.js +197 -54
- package/dist/components/Input.d.ts +12 -2
- package/dist/components/Input.js +5 -3
- package/dist/components/Kbd.d.ts +6 -1
- package/dist/components/Kbd.js +6 -6
- package/dist/components/Lightbox.d.ts +41 -5
- package/dist/components/Lightbox.js +95 -9
- package/dist/components/MatchReview.d.ts +40 -1
- package/dist/components/MatchReview.js +130 -4
- package/dist/components/MetricInput.js +3 -2
- package/dist/components/Modal.d.ts +2 -1
- package/dist/components/Modal.js +13 -4
- package/dist/components/NavFavourites.d.ts +5 -0
- package/dist/components/NavFavourites.js +7 -7
- package/dist/components/OtpInput.d.ts +33 -0
- package/dist/components/OtpInput.js +123 -0
- package/dist/components/PageHeader.d.ts +7 -1
- package/dist/components/PageHeader.js +2 -2
- package/dist/components/Pagination.js +6 -5
- package/dist/components/PasswordInput.d.ts +34 -0
- package/dist/components/PasswordInput.js +101 -0
- package/dist/components/Peek.js +3 -3
- package/dist/components/Popover.js +2 -0
- package/dist/components/PrintLayout.d.ts +63 -0
- package/dist/components/PrintLayout.js +114 -0
- package/dist/components/QrCode.d.ts +47 -0
- package/dist/components/QrCode.js +55 -0
- package/dist/components/QrScanner.d.ts +95 -0
- package/dist/components/QrScanner.js +266 -0
- package/dist/components/RelationshipGraph.d.ts +32 -1
- package/dist/components/RelationshipGraph.js +18 -5
- package/dist/components/RelationshipGraphLayered.d.ts +1 -1
- package/dist/components/RelationshipGraphLayered.js +190 -115
- package/dist/components/RelationshipGraphList.js +33 -6
- package/dist/components/RuleBuilder.js +39 -7
- package/dist/components/SchedulingAgenda.d.ts +3 -1
- package/dist/components/SchedulingCalendar.d.ts +3 -1
- package/dist/components/SchedulingCalendar.js +43 -4
- package/dist/components/SchedulingResourceDay.d.ts +57 -0
- package/dist/components/SchedulingResourceDay.js +287 -0
- package/dist/components/SchedulingTimeGrid.d.ts +3 -1
- package/dist/components/SchedulingTimeline.d.ts +3 -1
- package/dist/components/SearchSuggest.d.ts +23 -3
- package/dist/components/SearchSuggest.js +48 -17
- package/dist/components/Segmented.d.ts +39 -4
- package/dist/components/Segmented.js +31 -3
- package/dist/components/Select.d.ts +3 -2
- package/dist/components/Select.js +1 -1
- package/dist/components/ShortcutHint.d.ts +11 -10
- package/dist/components/ShortcutHint.js +44 -38
- package/dist/components/Sidebar.d.ts +36 -4
- package/dist/components/Sidebar.js +37 -12
- package/dist/components/SortableList.d.ts +45 -0
- package/dist/components/SortableList.js +134 -0
- package/dist/components/Stat.d.ts +28 -3
- package/dist/components/Stat.js +13 -3
- package/dist/components/StatusPill.d.ts +14 -3
- package/dist/components/StatusPill.js +11 -3
- package/dist/components/Steps.js +2 -1
- package/dist/components/Table.d.ts +9 -2
- package/dist/components/Table.js +4 -2
- package/dist/components/TableControls.js +18 -58
- package/dist/components/TableOverflowProbe.d.ts +12 -0
- package/dist/components/TableOverflowProbe.js +27 -0
- package/dist/components/Tabs.d.ts +2 -1
- package/dist/components/Tabs.js +1 -0
- package/dist/components/TagInput.d.ts +67 -0
- package/dist/components/TagInput.js +342 -0
- package/dist/components/Textarea.d.ts +5 -0
- package/dist/components/Textarea.js +2 -2
- package/dist/components/TimePicker.d.ts +80 -0
- package/dist/components/TimePicker.js +347 -0
- package/dist/components/Toast.js +3 -2
- package/dist/components/Tooltip.d.ts +8 -4
- package/dist/components/Tooltip.js +24 -21
- package/dist/components/Tree.d.ts +8 -0
- package/dist/components/Tree.js +11 -5
- package/dist/display-preferences-model.d.ts +53 -0
- package/dist/display-preferences-model.js +64 -0
- package/dist/emoji.cjs +28 -35
- package/dist/filter-model.d.ts +59 -3
- package/dist/filter-model.js +67 -2
- package/dist/hooks/useChangeHighlight.d.ts +33 -0
- package/dist/hooks/useChangeHighlight.js +44 -0
- package/dist/hooks/useDialog.d.ts +5 -0
- package/dist/hooks/useDialog.js +3 -2
- package/dist/hooks/useOptimisticValue.d.ts +51 -0
- package/dist/hooks/useOptimisticValue.js +61 -0
- package/dist/hooks/useReducedMotion.d.ts +4 -1
- package/dist/hooks/useReducedMotion.js +7 -1
- package/dist/hooks/useResolvedFilterLabels.d.ts +15 -0
- package/dist/hooks/useResolvedFilterLabels.js +34 -0
- package/dist/hooks/useVirtualRows.d.ts +21 -2
- package/dist/hooks/useVirtualRows.js +30 -3
- package/dist/icons/Icon.d.ts +1 -1
- package/dist/index.cjs +18644 -13210
- package/dist/index.d.cts +3003 -699
- package/dist/index.d.ts +42 -16
- package/dist/index.js +139 -7
- package/dist/labels-context.d.ts +90 -3
- package/dist/overlay-stack.d.ts +6 -0
- package/dist/overlay-stack.js +13 -0
- package/dist/qr-model.d.ts +65 -0
- package/dist/qr-model.js +323 -0
- package/dist/relationship-graph-model.d.ts +78 -3
- package/dist/relationship-graph-model.js +112 -5
- package/dist/reorder.d.ts +30 -0
- package/dist/reorder.js +74 -0
- package/dist/rich-text.cjs +4 -2
- package/dist/scheduling-resource-model.d.ts +76 -0
- package/dist/scheduling-resource-model.js +74 -0
- package/dist/search-suggest-model.js +3 -1
- package/dist/shell-context.d.ts +11 -0
- package/dist/shell-context.js +7 -0
- package/dist/time-field-model.d.ts +46 -0
- package/dist/time-field-model.js +152 -0
- package/dist/toast-store.d.ts +17 -1
- package/dist/toast-store.js +23 -7
- 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`)
|
|
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
|
-
/**
|
|
386
|
-
|
|
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 };
|
package/dist/announce.js
ADDED
|
@@ -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
|
+
};
|