@tensor_1/react 2.47.0 → 2.49.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 +540 -2
- 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/Chip.d.ts +7 -3
- package/dist/components/Chip.js +2 -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/DashboardCanvas.js +14 -0
- package/dist/components/DataWorkspace.d.ts +167 -0
- package/dist/components/DataWorkspace.js +380 -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/FocusOnMount.d.ts +282 -0
- package/dist/components/FocusOnMount.js +12 -0
- 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/Kanban.d.ts +14 -3
- package/dist/components/Kanban.js +9 -4
- 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/MotionList.d.ts +303 -0
- package/dist/components/MotionList.js +175 -0
- 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 +11 -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 +22 -6
- package/dist/components/RelationshipGraphLayered.d.ts +1 -1
- package/dist/components/RelationshipGraphLayered.js +268 -118
- 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 +56 -18
- package/dist/components/Segmented.d.ts +39 -4
- package/dist/components/Segmented.js +39 -5
- package/dist/components/Select.d.ts +3 -2
- package/dist/components/Select.js +7 -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 +147 -0
- package/dist/components/Stat.d.ts +28 -3
- package/dist/components/Stat.js +13 -3
- package/dist/components/States.d.ts +33 -1
- package/dist/components/States.js +43 -1
- 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 +10 -2
- 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.d.ts +1 -1
- package/dist/components/Toast.js +109 -4
- package/dist/components/Tooltip.d.ts +8 -4
- package/dist/components/Tooltip.js +86 -28
- 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 +143 -49
- package/dist/filter-model.d.ts +59 -3
- package/dist/filter-model.js +67 -2
- package/dist/hooks/useAnchoredPosition.js +14 -2
- package/dist/hooks/useChangeHighlight.d.ts +33 -0
- package/dist/hooks/useChangeHighlight.js +44 -0
- package/dist/hooks/useDeferredReorder.d.ts +32 -0
- package/dist/hooks/useDeferredReorder.js +47 -0
- package/dist/hooks/useDialog.d.ts +5 -0
- package/dist/hooks/useDialog.js +17 -3
- package/dist/hooks/useKeyboardGrab.d.ts +58 -0
- package/dist/hooks/useKeyboardGrab.js +118 -0
- 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/useSlidingIndicator.d.ts +18 -0
- package/dist/hooks/useSlidingIndicator.js +80 -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 +19687 -13423
- package/dist/index.d.cts +3419 -651
- package/dist/index.d.ts +48 -18
- package/dist/index.js +153 -9
- package/dist/labels-context.d.ts +93 -3
- package/dist/motion.d.ts +11 -0
- package/dist/motion.js +35 -0
- 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 +92 -3
- package/dist/relationship-graph-model.js +134 -15
- package/dist/reorder.d.ts +30 -0
- package/dist/reorder.js +74 -0
- package/dist/rich-text.cjs +46 -4
- 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 +22 -3
- package/dist/toast-store.js +26 -9
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -192,7 +192,7 @@ Rules the component cannot enforce for you:
|
|
|
192
192
|
- **Dimming is not a signal.** `dimmedNodeIds` must be accompanied by text that says what is dimmed and why.
|
|
193
193
|
- **Localise everything.** Every visible and announced string comes from `labels`; the English defaults live in `DEFAULT_RELATIONSHIP_GRAPH_LABELS`.
|
|
194
194
|
|
|
195
|
-
**Map controls (`controls`, `initialView`).** The default `controls="full"` is the labelled toolbar above the map. `controls="compact"` is the map convention: an icon cluster in the map's bottom-right corner (zoom in, zoom out, fit, reset, full screen, each with a tooltip) and the zoom level beside it; drag pans, Ctrl/⌘ + wheel or a pinch zooms around the pointer, and a plain wheel keeps scrolling the page and briefly says how to zoom. The map itself takes keyboard focus: arrow keys pan (on an item they still move between items), `+` and `−` zoom, `0` resets and Escape leaves full screen. Full screen covers the page (the page behind does not scroll) and there a plain wheel zooms. `controls="none"` draws nothing; drag, pinch and Ctrl/⌘ + wheel still work. `initialView="fit"` opens with every item in view (down to the 40 % minimum) instead of the 75 % readability floor, and Reset returns to it.
|
|
195
|
+
**Map controls (`controls`, `initialView`).** The default `controls="full"` is the labelled toolbar above the map. `controls="compact"` is the map convention: an icon cluster in the map's bottom-right corner (zoom in, zoom out, fit, reset, full screen, each with a tooltip) and the zoom level beside it; drag pans, Ctrl/⌘ + wheel or a pinch zooms around the pointer, and a plain wheel keeps scrolling the page and briefly says how to zoom. The map itself takes keyboard focus: arrow keys pan (on an item they still move between items), `+` and `−` zoom, `0` resets and Escape leaves full screen. Full screen covers the page (the page behind does not scroll) and there a plain wheel zooms. `controls="none"` draws nothing; drag, pinch and Ctrl/⌘ + wheel still work. `initialView="fit"` opens with every item in view (down to the 40 % minimum) instead of the 75 % readability floor, and Reset returns to it. With `controls="compact"` the hint, the zoom level and the cluster sit in a band along the map's bottom edge; the map ends at the band, and fit, the first view and Reset leave its height free.
|
|
196
196
|
|
|
197
197
|
```tsx
|
|
198
198
|
<RelationshipGraph layout="layered" controls="compact" initialView="fit" height="clamp(24rem, 62vh, 44rem)" … />
|
|
@@ -256,10 +256,102 @@ 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
|
+
|
|
288
|
+
### Lists that move, the success outcome, sliding indicators (motion standard §2.5, §2.8)
|
|
289
|
+
|
|
290
|
+
- **`MotionList`** (`items`, `getKey`, `renderItem`, `as`): a new item rises 8 px and fades in;
|
|
291
|
+
a removed item stays in place (hidden from assistive tech, `inert`), fades out, and only then
|
|
292
|
+
leaves, so the items below glide up; a re-sort glides (FLIP, transform only). Nothing moves on
|
|
293
|
+
the first render. Focus stays on a moved item and goes to the next item when the focused one
|
|
294
|
+
leaves; focus outside the list is never pulled in. `renderItem` must pass unknown props to its
|
|
295
|
+
element (`<li>`, or a `Tr` with `as="tbody"`).
|
|
296
|
+
- **`useDeferredReorder(items, getKey)`**: never re-sort under the pointer. While a mouse is over
|
|
297
|
+
the list or keyboard focus is inside it, a new order waits (content still updates, new rows go
|
|
298
|
+
to the end); `held` says so; the order applies when they leave. Spread `listProps` on the list.
|
|
299
|
+
- **`SuccessState`** in `States`: the outcome of a rare journey (a request submitted): a mark
|
|
300
|
+
that draws once, the title (focused on mount for `page` and `full-page`), `reference`,
|
|
301
|
+
`next` (what happens next), `action`. Reduced motion shows the mark at once.
|
|
302
|
+
- **Tabs, Segmented**: the line underline, the pill and the lifted segment slide to the chosen
|
|
303
|
+
option on a click (160 ms); a keyboard change and reduced motion jump. Server HTML, forced
|
|
304
|
+
colours and Segmented's link mode keep each option's own mark. `useSlidingIndicator` is the
|
|
305
|
+
hook behind it.
|
|
306
|
+
- **CollapsibleSection** opening fades the body in and settles it 4 px (on `::details-content`,
|
|
307
|
+
where supported); an already open section, a keyboard open and closing are instant.
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
const queue = useDeferredReorder(rows, (row) => row.id);
|
|
311
|
+
<MotionList {...queue.listProps} as="tbody" items={queue.items} getKey={(row) => row.id}
|
|
312
|
+
renderItem={(row) => <Tr>…</Tr>} />
|
|
313
|
+
|
|
314
|
+
<SuccessState title={t('requestSubmitted')} reference={<>{req.number} <CopyButton … /></>}
|
|
315
|
+
next={[t('confirmationSent'), t('agentPicksUp')]} action={<ButtonLink href={`/requests/${req.id}`}>{t('viewRequest')}</ButtonLink>} />
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Drag affordances (motion standard §2.8)
|
|
319
|
+
|
|
320
|
+
One set of drag states for every draggable thing (`drag.css`), as attributes so any element can
|
|
321
|
+
carry them: `[data-uix-drag="lifted"]` (picked up by the pointer: a little larger, a deeper
|
|
322
|
+
shadow), `[data-uix-drag="grabbed"]` (picked up from the keyboard: a dashed ring, no motion),
|
|
323
|
+
`[data-uix-drop-target]` (the zone it would land in), `.uix-drop-line` (the gap it would drop
|
|
324
|
+
into) and `[data-uix-settle]` (the landing). Reduced motion keeps the states and drops the scale
|
|
325
|
+
and the landing.
|
|
326
|
+
|
|
327
|
+
- **SortableList** and **DashboardCanvas** lift and settle on their own.
|
|
328
|
+
- **Kanban**: `KanbanCard dragging`, `KanbanColumn dropTarget`, `<KanbanDropLine />` at the gap;
|
|
329
|
+
call `settleDrop(cardElement)` once the card is in its new place.
|
|
330
|
+
- **`useKeyboardGrab({ id, label, onMove, onDrop, onCancel, labels })`** for a draggable thing you
|
|
331
|
+
build: Space or Enter picks the focused item up, the arrows call `onMove(direction)` (return
|
|
332
|
+
what to say, e.g. "Position 3 of 8"), Space or Enter drops, Escape or leaving puts it back;
|
|
333
|
+
every step is said politely. Spread `itemProps` (it includes a `ref`) on the focusable item; the
|
|
334
|
+
picked-up state follows `id`, so an item moved into another list stays picked up and focused.
|
|
335
|
+
Words: `labels` or `UixLabelsProvider` `keyboardGrab`. The kit's lists have their own paths.
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
<KanbanColumn title={t('doing')} dropTarget={drag?.column === 'doing'}>
|
|
339
|
+
{cards.map((card, i) => <Fragment key={card.id}>
|
|
340
|
+
{drag?.column === 'doing' && drag.index === i && <KanbanDropLine />}
|
|
341
|
+
<KanbanCard title={card.title} dragging={drag?.id === card.id} onMove={…} />
|
|
342
|
+
</Fragment>)}
|
|
343
|
+
{drag?.column === 'doing' && drag.index >= cards.length && <KanbanDropLine />}
|
|
344
|
+
</KanbanColumn>
|
|
345
|
+
|
|
346
|
+
// your own draggable list: the picked-up state follows the id, across lists too
|
|
347
|
+
const grab = useKeyboardGrab({ id: item.id, label: item.title, onMove: (dir) => move(item.id, dir), onCancel: restore });
|
|
348
|
+
<li tabIndex={0} {...grab.itemProps}>{item.title}</li>
|
|
349
|
+
```
|
|
350
|
+
|
|
259
351
|
### Scrolling tabs, toned stat tiles and copying
|
|
260
352
|
|
|
261
353
|
- **`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`)
|
|
354
|
+
- **`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
355
|
- **`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
356
|
- **`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
357
|
- **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).
|
|
@@ -1012,6 +1104,9 @@ const v = useVirtualRows(rows, { rowHeight: 44, estimatedViewportHeight: 600, en
|
|
|
1012
1104
|
- The ref is the body (the button, link or span that carries the chip's action), also on a
|
|
1013
1105
|
removable chip; `bodyProps` puts attributes there.
|
|
1014
1106
|
- `current` on a link chip: `aria-current="page"` and the filled look.
|
|
1107
|
+
- `onClick` gets the click event, so a chip can be a `Menu` trigger:
|
|
1108
|
+
`<Menu trigger={<Chip variant="add" icon={<PlusIcon />}>Filter</Chip>}>…</Menu>`. A handler that takes no
|
|
1109
|
+
argument still works.
|
|
1015
1110
|
|
|
1016
1111
|
#### FileUpload and Attachment
|
|
1017
1112
|
|
|
@@ -1166,3 +1261,446 @@ mask; an avatar with a dot no longer clips it.
|
|
|
1166
1261
|
`entityPicker`. `SearchSuggest` has the two slots these use: `idle` and `note`.
|
|
1167
1262
|
- `CommandPalette` announces its result count through `UixLabelsProvider` `commandPalette`
|
|
1168
1263
|
(`resultsOne`, `resultsMany`, `{count}`) or a `resultsLabel(count)` prop; English is unchanged.
|
|
1264
|
+
|
|
1265
|
+
### Codes, contacts and paper (batch 5)
|
|
1266
|
+
|
|
1267
|
+
#### QrCode
|
|
1268
|
+
|
|
1269
|
+
`QrCode` draws `value` as a scannable SVG QR code with the standard four-module quiet zone.
|
|
1270
|
+
Name it for where it leads (`label`), pick `errorCorrection` (`M` by default; `Q` or `H` for a
|
|
1271
|
+
poster that may be creased or partly covered) and a `size`: CSS pixels, or a length such as
|
|
1272
|
+
`'32mm'` on paper. Keep at least 2 px per module on a screen and 0.4 mm per module on paper.
|
|
1273
|
+
The symbol is ink on paper in light, dark, print and forced colours, because scanners read dark
|
|
1274
|
+
on light. The encoder is the kit's own (`encodeQr`, `qrPath`), with no runtime dependency.
|
|
1275
|
+
|
|
1276
|
+
```tsx
|
|
1277
|
+
<QrCode value={campaignUrl} label={t('qrOpensCampaign')} errorCorrection="Q" size="40mm" />
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
#### CodeDisplay
|
|
1281
|
+
|
|
1282
|
+
`CodeDisplay` shows a one-time code large enough to read across a counter: monospace groups
|
|
1283
|
+
(`groupSize`, or the code's own spaces and hyphens), a visible `label`, and a screen-reader
|
|
1284
|
+
spelling character by character (`spell` for a spelling alphabet or to tell O from 0).
|
|
1285
|
+
`expiresAt` adds a countdown that is never a live region: a screen reader hears "{time} left"
|
|
1286
|
+
once at `warnBefore` seconds (60) and "expired" once, and `onExpire` runs once. An expired code
|
|
1287
|
+
is struck through and says so in words; `copyable` adds a `CopyButton` while the code is live.
|
|
1288
|
+
The product makes the code and enforces its expiry.
|
|
1289
|
+
|
|
1290
|
+
```tsx
|
|
1291
|
+
<CodeDisplay code={pairing.code} label={t('pairingCode')} expiresAt={pairing.expiresAt} copyable onExpire={refresh} />
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
#### ContactCard
|
|
1295
|
+
|
|
1296
|
+
`ContactCard` renders `.uix-contact` in a card (`bare` for a peek, a popover or a card body that
|
|
1297
|
+
already has padding): avatar, `name` (a quiet link with `href` / `renderLink`), `role`, the
|
|
1298
|
+
`email` and `phone` as selectable text, and Email / Call as `mailto:` / `tel:` `ButtonLink`s
|
|
1299
|
+
named for the person ("Email Amra Hadžić"), then your own `actions` and `stats`.
|
|
1300
|
+
`state="purged"` (the person's data was erased) shows a neutral placeholder and nothing that
|
|
1301
|
+
reaches anyone, even when stale props still carry an address; `state="unavailable"` keeps the
|
|
1302
|
+
name and says the details are unavailable. Words: `labels` or `UixLabelsProvider` `contactCard`.
|
|
1303
|
+
|
|
1304
|
+
```tsx
|
|
1305
|
+
<ContactCard name={p.name} role={p.role} avatar={<Avatar size="lg" src={p.photo} alt="" />} email={p.email} phone={p.phone}
|
|
1306
|
+
state={p.erasedAt ? 'purged' : 'active'} href={`/people/${p.id}`} renderLink={(props) => <Link {...props} />} />
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
#### EventRow slots
|
|
1310
|
+
|
|
1311
|
+
`EventRow` takes `weekday`, `year` and `time` in its date tile (the tile widens to its longest
|
|
1312
|
+
line), `dateTime` to make the tile a `<time>` with a machine-readable date, and `media` for a
|
|
1313
|
+
thumbnail at the row's end: an `<img>` drawn 4:3 and cropped to fill, moved under the text on a
|
|
1314
|
+
phone. Without them the markup is unchanged.
|
|
1315
|
+
|
|
1316
|
+
```tsx
|
|
1317
|
+
<EventRow weekday="Wed" month="Oct" day="22" year="2026" time="18:00" dateTime="2026-10-22T18:00+02:00"
|
|
1318
|
+
title={<Link href="/program/42">Book launch</Link>} meta="National library" media={<img src={cover} alt="" />} />
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
#### PrintLayout
|
|
1322
|
+
|
|
1323
|
+
`PrintLayout` is a print document frame for the browser's own print (`window.print()`): `size`
|
|
1324
|
+
`A4` (default) or `A5`, `orientation`, a `header` and a `footer` that repeat on every printed page
|
|
1325
|
+
(it renders a layout `<table>`: Chromium repeats a real `<thead>` / `<tfoot>`, not a CSS table of
|
|
1326
|
+
divs), "page / pages" in the bottom-right margin (`pageNumbers`), and ink-on-paper tokens for
|
|
1327
|
+
everything inside, also as a paper preview on screen in the dark theme. While it is on the page,
|
|
1328
|
+
printing shows only the documents (`isolate`, default): the app shell, toolbars and toasts are
|
|
1329
|
+
left out and scroll boxes cannot cut the document. `singlePage` keeps a poster or a one-page
|
|
1330
|
+
readout in one piece; when its content does not fit, the preview is outlined and
|
|
1331
|
+
`onOverflowChange(true)` is called. `PrintSignature` is a signature line with place and date and
|
|
1332
|
+
an optional stamp box (never split across pages); `PrintPageBreak` starts a new page. Utilities:
|
|
1333
|
+
`.uix-print-keep`, `.uix-print-hide`, `.uix-print-only`.
|
|
1334
|
+
|
|
1335
|
+
```tsx
|
|
1336
|
+
<PrintLayout header={<PracticeHeader />} footer={<DoctorFooter />}>
|
|
1337
|
+
<h1>{t('findings')}</h1>
|
|
1338
|
+
<DescriptionList items={patient} />
|
|
1339
|
+
<PrintSignature placeAndDate={placeAndDate} name={doctor} stamp />
|
|
1340
|
+
</PrintLayout>
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
#### QrScanner
|
|
1344
|
+
|
|
1345
|
+
`QrScanner` is a camera viewfinder for a decoder you supply: `decode(frame)` gets
|
|
1346
|
+
`{ imageData, video }` (frames at most `maxFrameSize` px on the long side, every `interval` ms)
|
|
1347
|
+
and returns the text it read; the kit ships no decoder and makes no network request.
|
|
1348
|
+
`onResult(value)` is called once per new text (the same text again within `repeatAfter` ms is
|
|
1349
|
+
not repeated), and a status message says it once. Before the browser's prompt the scanner shows
|
|
1350
|
+
your `explainer` and a start button (with `autoStart`, a camera the browser already allows starts
|
|
1351
|
+
at once); after a refusal it says how to allow the camera again; it says so when the browser has
|
|
1352
|
+
no camera API, no camera is found, or the camera sends no picture. `manualEntry` is rendered in
|
|
1353
|
+
every state. A hint appears after `hintAfter` ms of frames without a code; a torch toggle appears
|
|
1354
|
+
when the camera has one. The camera stops on Stop, on unmount and while the page is hidden.
|
|
1355
|
+
|
|
1356
|
+
```tsx
|
|
1357
|
+
<QrScanner decode={({ imageData }) => wasmDecoder.read(imageData)} onResult={redeem}
|
|
1358
|
+
explainer={<p>{t('cameraWhy')}</p>} manualEntry={<VoucherCodeField />} />
|
|
1359
|
+
```
|
|
1360
|
+
|
|
1361
|
+
A scan trail (what was read, what the product did with it, what to do next) is the product's:
|
|
1362
|
+
keep `onResult` and `onStatusChange` in state and show the last few as a `Timeline` under the
|
|
1363
|
+
scanner, newest first, with the outcome in words ("Not changed: already lent").
|
|
1364
|
+
|
|
1365
|
+
#### SchedulingCalendar: the day by resource
|
|
1366
|
+
|
|
1367
|
+
`view="day"` with `resources` draws one column per resource (a practitioner, a room) on a slot
|
|
1368
|
+
axis in `timeZone`: `resourceSpans` are the free, booked and off stretches the server composed;
|
|
1369
|
+
a free span is cut into bookable slots of `slotMinutes` (15), a booking and time off stay whole,
|
|
1370
|
+
and a time no span covers is hatched "no schedule" (never free; nothing is inferred). `dayStart`
|
|
1371
|
+
/ `dayEnd` set the axis (07:00–19:00). `onSelectSlot` gets `{ resourceId, start, end, kind, span }`
|
|
1372
|
+
for a click, Enter or Space on a slot or a booking; `selectedSlot` marks one. The view is one tab
|
|
1373
|
+
stop: arrows move between slots and resources, Home / End to a column's ends. The gutter and the
|
|
1374
|
+
heads stay in view while the grid scrolls. `loading` shows skeletons on the first load and dims
|
|
1375
|
+
what is on screen on a refetch. Below `resourceListBelow` (480 px of its own width) the day is a
|
|
1376
|
+
list, one resource after another (`resourceLayout` fixes either).
|
|
1377
|
+
|
|
1378
|
+
```tsx
|
|
1379
|
+
<SchedulingCalendar view="day" anchorDate={day} timeZone={tenantZone} entries={[]}
|
|
1380
|
+
resources={practitioners} resourceSpans={spans} slotMinutes={15} dayStart="07:30" dayEnd="17:00"
|
|
1381
|
+
onSelectSlot={openSlotPopover} selectedSlot={open && { resourceId: open.resourceId, start: open.start }} loading={isFetching} />
|
|
1382
|
+
```
|
|
1383
|
+
|
|
1384
|
+
#### RelationshipGraph: gaps on the map
|
|
1385
|
+
|
|
1386
|
+
For a map of what is expected and what is there (TENSOR's K3 gaps): an edge takes
|
|
1387
|
+
`variant: 'dashed'` (an expected relation that is missing), a `tone` and a `description` that
|
|
1388
|
+
both nodes say to a screen reader ("covers, from Control K3.2 (expected, missing)"); a node takes
|
|
1389
|
+
a `tone`; a legend item takes the same `tone`, a `shape` (`edge` or `node`) and a `variant`, so
|
|
1390
|
+
it matches what it explains. Say a tone's meaning in words too (`description`, the node's text).
|
|
1391
|
+
`revealId` scrolls a node into view without moving focus (a table row beside the map), and with
|
|
1392
|
+
reduced motion it jumps. Without `rootId` no node gets the root border (`markRoot` forces it
|
|
1393
|
+
either way). `controls="none"` hides the toolbar in the radial layout as well. Edge labels that
|
|
1394
|
+
would overlap in a dense column are moved apart, or left out when they would drift too far (the
|
|
1395
|
+
relationship stays in the nodes' descriptions and the list). An edge across more than one column
|
|
1396
|
+
is a solid "skip" edge in a channel under the columns it crosses, no longer dashed or flagged as a
|
|
1397
|
+
cycle; `LayeredEdgeKind` gains `skip` and `back`.
|
|
1398
|
+
|
|
1399
|
+
`RelationshipGraph layout="layered"` was measured at CMDB topology sizes (HAR-1387; 300 and 600
|
|
1400
|
+
items): no items overlap; a column that takes many forward edges says their common relation once
|
|
1401
|
+
in its header and only edges that differ keep a label; a busy node fans out along one bus; a
|
|
1402
|
+
node's description names five relationships and points to the list, whose rows list at most eight
|
|
1403
|
+
neighbours until "Show all N" (`connectedHeading`, `showAllRelations`, `showFewerRelations`,
|
|
1404
|
+
`moreRelations`); a pan or a zoom only moves the stage, so dragging stays at the display's frame
|
|
1405
|
+
rate. A force layout was not needed.
|
|
1406
|
+
|
|
1407
|
+
### Form fields (HAR-1812, HAR-1799, …)
|
|
1408
|
+
|
|
1409
|
+
#### CheckboxGroup
|
|
1410
|
+
|
|
1411
|
+
```tsx
|
|
1412
|
+
<CheckboxGroup label={t('tags')} hint={t('tagsHint')} error={errors.tags} columns={3} required>
|
|
1413
|
+
{tags.map((tag) => (
|
|
1414
|
+
<Checkbox key={tag.id} name="tags" value={tag.id} label={tag.name}
|
|
1415
|
+
checked={chosen.includes(tag.id)} onChange={() => toggle(tag.id)} />
|
|
1416
|
+
))}
|
|
1417
|
+
</CheckboxGroup>
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
The checkbox counterpart of `<RadioGroup label>`: a `<fieldset>` whose `<legend>` names the group,
|
|
1421
|
+
so it is announced with each box, and the field's message slot below (`hint`, or `error` with
|
|
1422
|
+
`role="alert"`, tied to the group with `aria-describedby`). `columns` is a ceiling: no column is
|
|
1423
|
+
narrower than 10rem, so a phone gets one. `disabled` disables every box; `required` draws the
|
|
1424
|
+
marker (which boxes must be ticked is your rule).
|
|
1425
|
+
|
|
1426
|
+
#### PasswordInput and OtpInput
|
|
1427
|
+
|
|
1428
|
+
```tsx
|
|
1429
|
+
<Field label={t('password')} error={errors.password}>
|
|
1430
|
+
<PasswordInput name="password" autoComplete="current-password" />
|
|
1431
|
+
</Field>
|
|
1432
|
+
<Field label={t('code')} hint={t('codeHint')} error={errors.code}>
|
|
1433
|
+
<OtpInput name="code" length={6} groupSize={3} onComplete={(code) => verify(code)} />
|
|
1434
|
+
</Field>
|
|
1435
|
+
```
|
|
1436
|
+
|
|
1437
|
+
`PasswordInput` is an `Input type="password"` with a reveal toggle inside its end: a button with
|
|
1438
|
+
`aria-pressed` whose name stays "Show password" (`labels.show`, or `UixLabelsProvider`
|
|
1439
|
+
`passwordInput`). A pointer press keeps the caret in the field; Tab reaches the toggle. When the
|
|
1440
|
+
form submits, a revealed password is hidden first, so the browser still offers to save it.
|
|
1441
|
+
`revealed` / `defaultRevealed` / `onRevealedChange`, `size`, and every `Input` prop pass through.
|
|
1442
|
+
|
|
1443
|
+
`OtpInput` is ONE input (`inputMode="numeric"`, `autoComplete="one-time-code"`, `pattern`;
|
|
1444
|
+
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
|
|
1445
|
+
readers see one field. Typing or pasting "123 456" keeps the characters; `onComplete(code)` runs
|
|
1446
|
+
once each time the code fills. `mode="alphanumeric"` takes letters (upper-cased); `groupSize`
|
|
1447
|
+
draws a gap; `aria-invalid` (from `Field error`) turns the cells red.
|
|
1448
|
+
|
|
1449
|
+
`InputGroup` gains `trailingAction`: a real control inside the field's end (class
|
|
1450
|
+
`uix-input-group__action`, square, the field's height less 4 px a side), which both use.
|
|
1451
|
+
|
|
1452
|
+
#### TimePicker
|
|
1453
|
+
|
|
1454
|
+
```tsx
|
|
1455
|
+
<Field label={t('opensAt')} hint={t('businessHours')}>
|
|
1456
|
+
<TimePicker name="opens" value={opens} onValueChange={setOpens} locale={locale} minuteStep={30} min="08:00" max="18:00" />
|
|
1457
|
+
</Field>
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1460
|
+
A time of day beside `DatePicker`; the value is `HH:mm` (what `<input type="time">` holds) or
|
|
1461
|
+
`null`. The field shows the locale's clock (14:30 in `de`, 2:30 PM in `en-US`; `hour12` overrides)
|
|
1462
|
+
and reads a typed time on Enter or blur: `1430`, `14.30`, `9`, `2:30 pm`. ArrowUp / ArrowDown step
|
|
1463
|
+
the part under the caret (the hour, or the minutes to the next `minuteStep`), PageUp / PageDown the
|
|
1464
|
+
hour, Alt+ArrowDown opens the list. The clock button lists the times at `minuteStep` between `min`
|
|
1465
|
+
and `max`; the list takes focus on the chosen time, Enter picks, Escape closes and focus goes back
|
|
1466
|
+
to the field. Text that is not a time, or one before `min` / after `max`, marks the field invalid and
|
|
1467
|
+
says why. `name` submits `HH:mm` through a hidden input. Words: `labels` or `UixLabelsProvider`
|
|
1468
|
+
`timePicker`. `DateTimePicker` now uses it for its time (no native `type="time"` is left);
|
|
1469
|
+
`minuteStep` defaults to 15 there and only steps the keys and the list, any typed minute is kept.
|
|
1470
|
+
|
|
1471
|
+
#### Date fields without call-site workarounds (HAR-2432)
|
|
1472
|
+
|
|
1473
|
+
```tsx
|
|
1474
|
+
<UixLabelsProvider labels={{
|
|
1475
|
+
dateFormat: { locale: 'de', formatDate: toDDMMYYYY, parseDate: fromDDMMYYYY, placeholder: 'TT.MM.JJJJ' },
|
|
1476
|
+
dateRangePicker: { label: t('chooseRange'), invalidMessage: t('rangeInvalid') },
|
|
1477
|
+
}}>
|
|
1478
|
+
```
|
|
1479
|
+
|
|
1480
|
+
- `dateFormat` sets `locale`, `formatDate`, `parseDate` and `placeholder` for every `DatePicker`,
|
|
1481
|
+
`DateTimePicker`, `DateRangePicker` (and the `locale` of `TimePicker`) below the provider; a prop
|
|
1482
|
+
on one field still wins.
|
|
1483
|
+
- `FilterPopover type="date"` renders the kit `DatePicker`; the value stays `YYYY-MM-DD` (`''` for
|
|
1484
|
+
none).
|
|
1485
|
+
- `DateRangePickerLabels` has `label` and `invalidMessage`, so no English default is left outside
|
|
1486
|
+
the labels group; the props still win.
|
|
1487
|
+
- `DateTimePicker` names its time from the label's accessible text (an `aria-hidden` "*" is left
|
|
1488
|
+
out) and follows the label when its text changes after mount.
|
|
1489
|
+
|
|
1490
|
+
#### Combobox at parity with Select (HAR-1588)
|
|
1491
|
+
|
|
1492
|
+
```tsx
|
|
1493
|
+
<span id="cat-label" className="uix-field__label">{t('category')}</span>
|
|
1494
|
+
<Combobox aria-labelledby="cat-label" label={t('category')} emptyLabel={t('noMatch')} value={category} onValueChange={setCategory}
|
|
1495
|
+
options={[{ label: t('hardware'), options: hardware }, { label: t('software'), options: software }, other]} />
|
|
1496
|
+
```
|
|
1497
|
+
|
|
1498
|
+
The list is a top-layer popover placed with `useAnchoredPosition` (like `Select`), so a Drawer, a
|
|
1499
|
+
Popover or an `overflow: hidden` cell never clips it. ↑ ↓ PageUp PageDown move (Home End too while
|
|
1500
|
+
an option is active), disabled options are skipped and the list does not wrap; Enter chooses;
|
|
1501
|
+
Escape closes it and puts the chosen option's text back, as leaving the field does. `options` take
|
|
1502
|
+
groups (`{ label, options, disabled }`, rendered as `role="group"`); the no-match row is outside the
|
|
1503
|
+
listbox; the chosen option has a check mark. New: `aria-labelledby` (replaces `label` as the name),
|
|
1504
|
+
`aria-describedby`, `aria-invalid` and `inputId` land on the input; `placement`;
|
|
1505
|
+
`filterComboboxItems`.
|
|
1506
|
+
|
|
1507
|
+
#### TagInput
|
|
1508
|
+
|
|
1509
|
+
```tsx
|
|
1510
|
+
<Field label={t('labels')} hint={t('labelsHint')}>
|
|
1511
|
+
<TagInput name="labels" value={labels} onValueChange={setLabels} suggestions={known} maxTags={10}
|
|
1512
|
+
validate={(tag) => (tag.length > 40 ? t('tooLong') : null)} />
|
|
1513
|
+
</Field>
|
|
1514
|
+
```
|
|
1515
|
+
|
|
1516
|
+
Type and press Enter or a comma to add a chip; a paste of "a, b, c" adds three. A duplicate (ignoring
|
|
1517
|
+
case and accents), a `validate` refusal or one past `maxTags` keeps the text and says why under the
|
|
1518
|
+
field. Suggestions (those not added yet) open in a top-layer list once the person types or presses
|
|
1519
|
+
ArrowDown; Enter adds the active one. Backspace in the empty field reaches the last chip; ArrowLeft /
|
|
1520
|
+
ArrowRight move between chips and back; Backspace or Delete removes the focused chip and focus stays
|
|
1521
|
+
with the tags; the field is the only tab stop. Each change is announced politely. Inside `Field` the
|
|
1522
|
+
text input gets the label, hint and error; `name` submits one value per tag. Words: `labels` or
|
|
1523
|
+
`UixLabelsProvider` `tagInput`.
|
|
1524
|
+
|
|
1525
|
+
#### SearchSuggest freeText (HAR-2465)
|
|
1526
|
+
|
|
1527
|
+
```tsx
|
|
1528
|
+
<label className="uix-field__label" htmlFor="maker">{t('manufacturer')}</label>
|
|
1529
|
+
<SearchSuggest freeText inputId="maker" name="manufacturer" label={t('manufacturer')}
|
|
1530
|
+
value={maker} onValueChange={setMaker} options={spellings} loading={loading} />
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
A text field with suggestions: the value is the text. Enter keeps what was typed and never submits
|
|
1534
|
+
the surrounding form (also while `loading`); no row is picked unless the person moves to it
|
|
1535
|
+
(ArrowDown / ArrowUp, then Enter) or clicks it, and then its title replaces the text through
|
|
1536
|
+
`onValueChange`, again without submitting. Blur, Tab and Escape keep the text (a second Escape does
|
|
1537
|
+
not clear it). No magnifier is drawn. `onSelect` is optional. Without `freeText` the search mode is
|
|
1538
|
+
unchanged. Decision: `Docs/adr/0004-search-suggest-free-text.md`.
|
|
1539
|
+
|
|
1540
|
+
#### EntityPicker and SearchSuggest gaps (HAR-1834)
|
|
1541
|
+
|
|
1542
|
+
- A failure Retry cannot fix: reject `onSearch` with `{ retryable: false, message? }`, or pass
|
|
1543
|
+
`isRetryable(error)`. The list shows the message (or `labels.forbidden`) and no Retry row.
|
|
1544
|
+
- `SearchSuggest` takes `disabled` and `invalid`; `EntityPicker` forwards them, so an empty disabled
|
|
1545
|
+
picker cannot be typed in and an empty required one with an error is announced invalid.
|
|
1546
|
+
- Clear starts a new search: the old query and rows are gone and the field has focus.
|
|
1547
|
+
- A visible label names the field: with a `<label for>` (a `Field`) or `aria-labelledby`
|
|
1548
|
+
(`EntityPicker`) / `inputLabelledBy` (`SearchSuggest`), no `aria-label` overrides it.
|
|
1549
|
+
- While the next query loads the last rows stay in view (`aria-busy` at once), and the spinner waits
|
|
1550
|
+
300 ms (`useDelayedPending`), so a fast answer shows none; Enter during a search picks nothing and
|
|
1551
|
+
submits nothing.
|
|
1552
|
+
### Calm status colour, the app shell and display preferences (2.48)
|
|
1553
|
+
|
|
1554
|
+
#### StatusPill treatments, Stat sentiment, quiet Alert (HAR-1563)
|
|
1555
|
+
|
|
1556
|
+
```tsx
|
|
1557
|
+
<StatusPill tone="danger" treatment="dot">Breached</StatusPill> // a table cell
|
|
1558
|
+
<StatusPill tone="progress">In progress</StatusPill> // the record's header (soft, the default)
|
|
1559
|
+
<StatusPill tone="warning" treatment="outline">Due in 2h</StatusPill> // a flag among other chrome
|
|
1560
|
+
<Stat label="Breached SLAs" value={7} trend={{ direction: 'up', label: '2', sentiment: 'negative' }} />
|
|
1561
|
+
```
|
|
1562
|
+
|
|
1563
|
+
- `StatusPill treatment`: `'soft'` (default, the tinted wash; `'filled'` is its old name and still works), `'dot'`
|
|
1564
|
+
(no fill, a neutral label, the tone only in the dot: for tables) and `'outline'` (a neutral hairline, a hushed
|
|
1565
|
+
label and the dot; only `critical` keeps a toned border). `dot` and `outline` always draw the dot. **Changed:**
|
|
1566
|
+
`outline` used to colour the label and a 40 % border in the tone's hue.
|
|
1567
|
+
- `StatusPill tone="progress"`: in flight, the same blue as `info`.
|
|
1568
|
+
- `Stat trend.sentiment`: `'positive'` (green), `'negative'` (red) or `'neutral'` (grey, the default). **Changed:** the
|
|
1569
|
+
chip used to be green for `up` and red for `down`, so a rise in breached SLAs showed green. Pass the sentiment the
|
|
1570
|
+
change has for your metric.
|
|
1571
|
+
- `Stat tone`: the border stays neutral and a dot leads the toned value (**changed:** the border was toned).
|
|
1572
|
+
- `Alert`: the borders are a 24 % hairline of the tone (**changed:** 30 to 40 %); the warning icon is the text
|
|
1573
|
+
role, at least 3:1 on its tint in both themes.
|
|
1574
|
+
|
|
1575
|
+
#### A Stat link tile looks like a link at rest (HAR-1803)
|
|
1576
|
+
|
|
1577
|
+
A tablet has no hover, so a tile that leads somewhere has to show it before it is touched. A link tile
|
|
1578
|
+
(`href`) draws a trailing chevron (`ChevronRightIcon`) in its label row; a tile that opens a dialog or menu
|
|
1579
|
+
keeps its down chevron; an in-place action tile (`onActivate` with `haspopup={false}`, class
|
|
1580
|
+
`.uix-stat--action`) underlines its value with a dotted line. All three are shapes, so they read without
|
|
1581
|
+
colour and in forced-colours mode, and none depends on hover. A plain figure tile is unchanged. The
|
|
1582
|
+
hover tint of interactive tiles now applies only where the device can hover (`@media (hover: hover)`).
|
|
1583
|
+
|
|
1584
|
+
```tsx
|
|
1585
|
+
<Stat label={t('overdue')} value={4} href="/loans?overdue" renderLink={toRoute} /> // › at rest
|
|
1586
|
+
```
|
|
1587
|
+
|
|
1588
|
+
#### Inset: a well inside a card body (HAR-1818)
|
|
1589
|
+
|
|
1590
|
+
```tsx
|
|
1591
|
+
<Card title={t('current')}>
|
|
1592
|
+
<Inset role="group" aria-label={t('headerPreview')}><BrandBar brand={brand} /></Inset>
|
|
1593
|
+
<Inset padding="sm"><code>{css}</code></Inset>
|
|
1594
|
+
</Card>
|
|
1595
|
+
```
|
|
1596
|
+
|
|
1597
|
+
`Inset` (`.uix-inset`) frames a preview or a specimen inside a card body: a hairline border, the md radius,
|
|
1598
|
+
the subtle background and `--uix-space-4` padding (`padding="sm"`: `--uix-space-3`). Use it instead of a `Card`
|
|
1599
|
+
in a `Card`, which brings a second shadow, card padding and card semantics. `tone="surface"` is for a well on
|
|
1600
|
+
a panel that is already subtle. It lays out nothing; give it a `role="group"` and a name when the preview needs
|
|
1601
|
+
one.
|
|
1602
|
+
|
|
1603
|
+
#### Icons, not text glyphs (HAR-1576)
|
|
1604
|
+
|
|
1605
|
+
Toast, Pagination, MetricInput, SavedViewMenu and Steps draw the icon set (`XIcon`, the chevrons, `PlusIcon`
|
|
1606
|
+
/ `MinusIcon`, `StarIcon`) where they drew `× ‹ › « + − ★ !`. The table sort arrows and the `Select` trigger's
|
|
1607
|
+
chevron are the icon set's glyphs through a CSS mask in `currentColor`, so they follow the theme; plain CSS gets
|
|
1608
|
+
the same through `.uix-glyph`:
|
|
1609
|
+
|
|
1610
|
+
```html
|
|
1611
|
+
<span class="uix-glyph uix-glyph--chevron-down" aria-hidden="true"></span>
|
|
1612
|
+
```
|
|
1613
|
+
|
|
1614
|
+
`node packages/react/scripts/glyph-icons.mjs` is the guard (it runs in `npm test`): a glyph as the whole content
|
|
1615
|
+
of a button, a link or an icon slot, or in a CSS `content:`, fails with its file and line. Text that only looks
|
|
1616
|
+
like an icon (a Kbd legend, a diff sign) goes in `glyph-icons.allow.json` with a reason.
|
|
1617
|
+
|
|
1618
|
+
#### The staff shell: full viewport and the type presets (HAR-1815)
|
|
1619
|
+
|
|
1620
|
+
```tsx
|
|
1621
|
+
<AppShell layout="viewport" sidebar={<StaffNav />}>
|
|
1622
|
+
<PageHeader size="display" eyebrow={t('today')} title={t('overview')} subtitle={t('overviewLead')} actions={actions} />
|
|
1623
|
+
</AppShell>
|
|
1624
|
+
<DetailPage variant="staff" back={{ href: '/loans', label: t('allLoans') }} title={loan.code} metrics={metrics}>
|
|
1625
|
+
<Card><SectionHead size="compact" as="h3" title={t('copies')} /><Table>…<CellSub tabular>{copy.inventory}</CellSub>…</Table></Card>
|
|
1626
|
+
</DetailPage>
|
|
1627
|
+
```
|
|
1628
|
+
|
|
1629
|
+
- `AppShell layout="viewport"`: no frame, `100dvh`, no top-bar row (a `topbar` sits at the top of the main
|
|
1630
|
+
column), staff padding (`space-7 space-6 space-6`, `space-8` at the sides from 80rem), and below 40rem one
|
|
1631
|
+
column with the sidebar hidden. `layout="frame"` is the default and the old look.
|
|
1632
|
+
- `PageHeader size="display"`: 40 px heading-font title, bold hushed eyebrow, body-size hushed subtitle (42rem),
|
|
1633
|
+
actions on the bottom line.
|
|
1634
|
+
- `DetailPage variant="staff"`: h2 / 1.15 title, un-underlined hushed back link, eyebrow metric labels over
|
|
1635
|
+
regular tabular h3 values.
|
|
1636
|
+
- `SectionHead size="compact"` (15 px / 1.3 / 600) and `as="h3"`; `CellSub tabular`.
|
|
1637
|
+
|
|
1638
|
+
#### Sidebar actions and search, AccountMenu, the rail hotkey and the phone nav tier (HAR-1371)
|
|
1639
|
+
|
|
1640
|
+
```tsx
|
|
1641
|
+
const [nav, setNav] = useState<ShellNav>('full');
|
|
1642
|
+
<AppShell layout="viewport" navHotkey="[" navTierStorageKey="staff-nav" onNavChange={setNav} phoneNav="drawer"
|
|
1643
|
+
sidebar={(
|
|
1644
|
+
<Sidebar brand={brand} nav={nav === 'rail' ? 'rail' : 'full'}
|
|
1645
|
+
actions={<><ModeToggle /><Menu trigger={<Button icon size="sm" variant="ghost" aria-label={t('tools')}><EllipsisIcon /></Button>}>…</Menu></>}
|
|
1646
|
+
search={{ label: t('search'), shortcut: ['Mod', 'K'], onActivate: openPalette }}
|
|
1647
|
+
footer={<AccountMenu collapsed={nav === 'rail'} user={{ name, sub: email, status: 'online' }} items={<>
|
|
1648
|
+
<MenuItem href="/profile">{t('profile')}</MenuItem>
|
|
1649
|
+
<MenuItem onSelect={signOut} tone="danger">{t('signOut')}</MenuItem>
|
|
1650
|
+
</>} />}>
|
|
1651
|
+
{navItems}
|
|
1652
|
+
</Sidebar>
|
|
1653
|
+
)}>
|
|
1654
|
+
{page}
|
|
1655
|
+
</AppShell>
|
|
1656
|
+
```
|
|
1657
|
+
|
|
1658
|
+
- `Sidebar actions` sit beside the brand (hidden in the rail); `search` is a row that opens your search and shows
|
|
1659
|
+
its keys (it does not bind them: your command palette listens); `footer` is pinned to the bottom.
|
|
1660
|
+
- Key caps are for a keyboard: on a touch phone or tablet (`(hover: none) and (pointer: coarse)`) the kit draws
|
|
1661
|
+
none, in the search row, Menu and CommandPalette rows, Tooltips, SearchSuggest and a Button's hint. What a
|
|
1662
|
+
screen reader hears is unchanged (`aria-keyshortcuts`, the spoken keys and descriptions stay).
|
|
1663
|
+
- `AccountMenu`: avatar (photo, your node, or initials), name and a second line as a `Menu` trigger;
|
|
1664
|
+
`collapsed` in the rail.
|
|
1665
|
+
- `navHotkey` switches full ↔ rail instantly, never while typing; `navTierStorageKey` remembers the tier before
|
|
1666
|
+
the first paint and reports it through `onNavChange`.
|
|
1667
|
+
- `phoneNav="drawer"`: at 40rem and under, a menu button opens the sidebar in a start-side `Drawer`.
|
|
1668
|
+
|
|
1669
|
+
#### Display preferences: theme, contrast, text size, motion, density (HAR-1372)
|
|
1670
|
+
|
|
1671
|
+
```tsx
|
|
1672
|
+
// app/layout.tsx: before the stylesheets, so nothing flashes. The script sets attributes on <html>
|
|
1673
|
+
// before React hydrates, so <html> needs suppressHydrationWarning; under a Content-Security-Policy
|
|
1674
|
+
// give the inline script your per-request nonce (or allow its hash).
|
|
1675
|
+
<html lang="bs" suppressHydrationWarning>
|
|
1676
|
+
<head>
|
|
1677
|
+
<script nonce={nonce} dangerouslySetInnerHTML={{ __html: displayPreferencesScript('staff-display') }} />
|
|
1678
|
+
</head>
|
|
1679
|
+
<body>{children}</body>
|
|
1680
|
+
</html>
|
|
1681
|
+
|
|
1682
|
+
// a settings page, or a dialog from AccountMenu
|
|
1683
|
+
<DisplayPreferences storageKey="staff-display" />
|
|
1684
|
+
// the header
|
|
1685
|
+
<ThemeSwitcher storageKey="staff-display" display="icons" />
|
|
1686
|
+
// your own control
|
|
1687
|
+
const { preferences, setPreference, resolvedTheme } = useDisplayPreferences({ storageKey: 'staff-display' });
|
|
1688
|
+
```
|
|
1689
|
+
|
|
1690
|
+
The tokens read five attributes on `<html>`: `data-theme` (light / dark), `data-contrast="more"`,
|
|
1691
|
+
`data-text-scale="sm|lg|xl"`, `data-motion="reduce"` and `data-density="compact|comfortable"`. A choice applies
|
|
1692
|
+
at once (transitions are suspended for that frame), is stored, and `system` follows the OS. The head script and the
|
|
1693
|
+
hook read the same key and the same defaults; pass both the same. `useReducedMotion` honours `data-motion`.
|
|
1694
|
+
|
|
1695
|
+
#### StatRow: a row of tiles that reflows
|
|
1696
|
+
|
|
1697
|
+
```tsx
|
|
1698
|
+
<StatRow>{facts.map((f) => <Stat key={f.id} size="compact" {...f} />)}</StatRow>
|
|
1699
|
+
<StatRow className="uix-stat-row--hero">{kpis}</StatRow> // 12rem tiles for 34 px values
|
|
1700
|
+
```
|
|
1701
|
+
|
|
1702
|
+
`StatRow` (`.uix-stat-row`) puts as many tiles side by side as fit at 8.5rem each (12rem with
|
|
1703
|
+
`.uix-stat-row--hero`, or as soon as a hero tile is in the row; `minTileWidth` / `--stat-row-min` to
|
|
1704
|
+
choose), all the same height, down to one per row on a phone. A fixed column count squeezed six tiles
|
|
1705
|
+
into 320 px until their labels broke letter by letter. Labels now wrap at a space and break inside a
|
|
1706
|
+
word only when that word alone is wider than the tile.
|