@tensor_1/react 2.31.0 → 2.33.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 +367 -0
- package/dist/SchedulingCalendar-C4XVbqZr.d.ts +339 -0
- package/dist/attachment-labels.d.ts +16 -0
- package/dist/attachment-labels.js +9 -0
- package/dist/calendar-model.d.ts +141 -1
- package/dist/calendar-model.js +200 -11
- package/dist/chart-preset.cjs +127 -7
- package/dist/chart-preset.d.cts +2 -2
- package/dist/chart-preset.d.ts +1 -0
- package/dist/chart-preset.js +6 -1
- package/dist/chart-theme-Cl0IrkKQ.d.cts +112 -0
- package/dist/chart-theme.d.ts +51 -0
- package/dist/chart-theme.js +99 -0
- package/dist/chart.cjs +127 -7
- package/dist/chart.d.cts +2 -2
- package/dist/chart.d.ts +1 -0
- package/dist/chart.js +6 -2
- package/dist/components/Alert.d.ts +17 -1
- package/dist/components/Alert.js +9 -4
- package/dist/components/AlertDismiss.d.ts +14 -0
- package/dist/components/AlertDismiss.js +11 -0
- package/dist/components/Attachment.d.ts +15 -12
- package/dist/components/Attachment.js +17 -17
- package/dist/components/AttachmentParts.d.ts +24 -0
- package/dist/components/AttachmentParts.js +20 -0
- package/dist/components/Chart.d.ts +1 -0
- package/dist/components/Chart.js +6 -1
- package/dist/components/ChartCore.d.ts +6 -1
- package/dist/components/ChartCore.js +23 -5
- package/dist/components/Chip.d.ts +17 -2
- package/dist/components/Chip.js +35 -11
- package/dist/components/ChipRemove.d.ts +17 -0
- package/dist/components/ChipRemove.js +13 -0
- package/dist/components/CollapsibleParts.d.ts +29 -0
- package/dist/components/CollapsibleParts.js +20 -0
- package/dist/components/CollapsibleSection.d.ts +30 -3
- package/dist/components/CollapsibleSection.js +7 -11
- package/dist/components/CollapsibleSectionState.d.ts +1 -1
- package/dist/components/CollapsibleSectionState.js +54 -27
- package/dist/components/DashboardGrid.d.ts +60 -0
- package/dist/components/DashboardGrid.js +40 -0
- package/dist/components/DateRangePicker.d.ts +7 -1
- package/dist/components/DateRangePicker.js +5 -1
- package/dist/components/DescriptionList.d.ts +12 -1
- package/dist/components/DescriptionList.js +5 -5
- package/dist/components/Dialogs.d.ts +24 -3
- package/dist/components/Dialogs.js +74 -16
- package/dist/components/FileUpload.d.ts +10 -2
- package/dist/components/FileUpload.js +10 -4
- package/dist/components/FilterEditor.d.ts +4 -0
- package/dist/components/FilterEditor.js +13 -2
- package/dist/components/Heartbeat.d.ts +31 -0
- package/dist/components/Heartbeat.js +28 -0
- package/dist/components/Menu.d.ts +53 -5
- package/dist/components/Menu.js +104 -4
- package/dist/components/Meter.d.ts +7 -3
- package/dist/components/Meter.js +2 -1
- package/dist/components/Popover.d.ts +21 -2
- package/dist/components/Popover.js +94 -17
- package/dist/components/Prose.d.ts +6 -2
- package/dist/components/Prose.js +1 -1
- package/dist/components/RuleBuilder.d.ts +52 -4
- package/dist/components/RuleBuilder.js +109 -24
- package/dist/components/SchedulingCalendar.d.ts +4 -110
- package/dist/components/SchedulingCalendar.js +299 -63
- package/dist/components/SchedulingTimeGrid.d.ts +4 -0
- package/dist/components/SchedulingTimeGrid.js +521 -0
- package/dist/components/SchedulingTimeline.d.ts +2 -1
- package/dist/components/Segmented.d.ts +17 -3
- package/dist/components/Segmented.js +59 -5
- package/dist/components/Select.d.ts +66 -2
- package/dist/components/Select.js +800 -10
- package/dist/components/Spinner.d.ts +2 -1
- package/dist/components/Spinner.js +1 -1
- package/dist/components/StatusPill.d.ts +8 -2
- package/dist/components/StatusPill.js +2 -2
- package/dist/components/Steps.d.ts +28 -4
- package/dist/components/Steps.js +46 -16
- package/dist/components/TableControls.js +4 -3
- package/dist/components/Tabs.d.ts +7 -2
- package/dist/components/Tabs.js +14 -1
- package/dist/components/TextDiff.d.ts +49 -0
- package/dist/components/TextDiff.js +124 -0
- package/dist/{emoji-image-6mPZ15Zy.d.cts → emoji-image-DjCKpdwE.d.cts} +1 -1
- package/dist/emoji.cjs +169 -27
- package/dist/emoji.d.cts +3 -3
- package/dist/file-model.d.ts +22 -3
- package/dist/file-model.js +6 -4
- package/dist/filter-model.d.ts +26 -2
- package/dist/filter-model.js +13 -1
- package/dist/hooks/useAnchoredPosition.d.ts +24 -1
- package/dist/hooks/useAnchoredPosition.js +58 -8
- package/dist/hooks/useVirtualRows.d.ts +17 -2
- package/dist/hooks/useVirtualRows.js +6 -5
- package/dist/icons/Icon.d.ts +14 -14
- package/dist/index.cjs +4888 -1714
- package/dist/index.d.cts +1871 -658
- package/dist/index.d.ts +16 -9
- package/dist/index.js +67 -4
- package/dist/labels-context.d.ts +29 -1
- package/dist/{overlay-position-Cjv5E7mn.d.cts → overlay-position-2Afke51l.d.cts} +20 -0
- package/dist/overlay-position.d.ts +20 -0
- package/dist/overlay-position.js +17 -2
- package/dist/rich-text.cjs +169 -27
- package/dist/rich-text.d.cts +3 -3
- package/dist/rule-builder-model.d.ts +34 -2
- package/dist/rule-builder-model.js +48 -12
- package/dist/scheduling-calendar-model.d.ts +169 -0
- package/dist/scheduling-calendar-model.js +176 -0
- package/dist/select-model.d.ts +80 -0
- package/dist/select-model.js +109 -0
- package/dist/table-engine.d.ts +7 -0
- package/dist/table-engine.js +2 -1
- package/dist/text-diff-model.d.ts +71 -0
- package/dist/text-diff-model.js +261 -0
- package/package.json +1 -1
- package/dist/ChartCore-DzWe10bN.d.cts +0 -59
package/README.md
CHANGED
|
@@ -354,6 +354,69 @@ const icons: Record<EntityType, UixIcon> = { incident: SirenIcon, change: GitBra
|
|
|
354
354
|
- **Accessibility:** decorative unless `label` is set. Never let an icon carry meaning alone; pair it with text.
|
|
355
355
|
- **Bundle size:** each `<Name>Icon` is tree-shaken; one icon adds well under 3 KB minified. `<Icon name>` and `ICON_GLYPHS` load all of them.
|
|
356
356
|
- **A glyph is missing?** Add its Lucide name to `scripts/icon-names.txt`, run `node scripts/generate-icons.mjs <lucide-react dir>` and release. Do not paste an SVG into a product.
|
|
357
|
+
### SchedulingCalendar
|
|
358
|
+
|
|
359
|
+
A month, week or agenda of UTC ranges in one explicit IANA zone (HAR-1347, HAR-1506). The calendar draws; the
|
|
360
|
+
consumer owns every word, count and ranking.
|
|
361
|
+
|
|
362
|
+
```tsx
|
|
363
|
+
import { SchedulingCalendar, layoutMonthSpans, schedulingGridDays } from '@tensor_1/react';
|
|
364
|
+
import { TriangleAlertIcon } from '@tensor_1/react/icons';
|
|
365
|
+
|
|
366
|
+
// The item: one hue dimension (band), one state channel (status), markers with text.
|
|
367
|
+
const entry = {
|
|
368
|
+
id: 'e1', title: 'Firewall rule update', start: '2026-10-06T08:00:00Z', end: '2026-10-06T09:00:00Z',
|
|
369
|
+
band: 'high', // 'none' | 'low' | 'medium' | 'high' — only 'high' is filled
|
|
370
|
+
status: 'tentative', // 'tentative' | 'committed' | 'live' | 'done' | 'dead' — line style, never a hue
|
|
371
|
+
markers: [{ id: 'm', label: 'Needs sign-off', emphasis: 'warning', icon: <TriangleAlertIcon /> }],
|
|
372
|
+
accessibleName: 'Firewall rule update, high, tentative, 6 October 10:00 to 11:00',
|
|
373
|
+
};
|
|
374
|
+
// A window: neutral, hatched, named, focusable. `global: false` never gets the global treatment.
|
|
375
|
+
const hold = { id: 'w1', label: 'Quarter close', kindLabel: 'Hold', scopeLabel: 'Payroll services', pattern: 'cross',
|
|
376
|
+
start: '2026-10-04T22:00:00Z', end: '2026-10-09T22:00:00Z' };
|
|
377
|
+
|
|
378
|
+
// Controlled month: the consumer ranks, cuts and counts; the calendar renders what it is given.
|
|
379
|
+
const grid = schedulingGridDays('2026-10-07', 'month', 1);
|
|
380
|
+
const spanLayout = layoutMonthSpans(spansInSalienceOrder, grid, { timeZone, weekStartsOn: 1 }); // lanes in the order given
|
|
381
|
+
<SchedulingCalendar timeZone="Europe/Berlin" anchorDate="2026-10-07" showHeader={false}
|
|
382
|
+
entries={multiDayEntries} overlays={[hold]} spanLayout={spanLayout}
|
|
383
|
+
dayEntries={{ '2026-10-07': topThree }} // already ranked and cut
|
|
384
|
+
days={{ '2026-10-07': { count: 50, overflowCount: 47, label: '50 items' } }} // "+47 more", never entries.length
|
|
385
|
+
maxEntriesPerDay={3} onShowMore={(date) => openDay(date)} // a controlled day never expands in place
|
|
386
|
+
onSelectEntry={openItem} onSelectOverlay={openWindow} onSelectDate={openDayView}
|
|
387
|
+
legend={legendOfWhatIsOnScreen} legendCaption="Times in Europe/Berlin"
|
|
388
|
+
notice={truncated && <p>Showing the first 2,000 items.</p>} emptyNote="Nothing is scheduled this month." />
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
- **Drawn once.** An entry or window that covers several days is one bar per week row, in lanes above the chips
|
|
392
|
+
(`windowLaneCap`, `spanLaneCap`, default 2 each). Windows never take a chip slot. A row with more spans than lanes
|
|
393
|
+
shows "+N" and calls `onShowMore` with the first day that has a hidden span.
|
|
394
|
+
- **Day membership is end-exclusive in `timeZone`**: 22:00–00:00 is on its start day only.
|
|
395
|
+
- **Fixed geometry.** With `days` / `dayEntries` (or `maxEntriesPerDay` plus `onShowMore`) every cell keeps one
|
|
396
|
+
height: the head, the lane caps, the chip rows and the "+N" line. A chip reads `HH:MM title` (24 h, in the zone)
|
|
397
|
+
unless `renderEntry` replaces it.
|
|
398
|
+
- **Week and Day as a time grid** (`timeGrid`, HAR-1509): day columns on an hour axis.
|
|
399
|
+
|
|
400
|
+
```tsx
|
|
401
|
+
<SchedulingCalendar view="week" timeGrid timeZone="Europe/Berlin" anchorDate="2026-10-07" showHeader={false}
|
|
402
|
+
entries={entries} overlays={windows} days={dayCounts} now={serverNow}
|
|
403
|
+
canMove={userMayMove} step={15}
|
|
404
|
+
onProposeMove={(id, { start, end, adjusted }) => reschedule(id, start, end)} // the server decides; the calendar never moves an item
|
|
405
|
+
onSelectEntry={openItem} onShowMore={openDay} />
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
- A column is as tall as its day (23, 24 or 25 hours); an item sits at its real time. `view="day"` is one column.
|
|
409
|
+
- An entry that crosses midnight by up to `topLaneCrossMidnightMinutes` (default 360) is one item in two joined
|
|
410
|
+
parts; all-day (`allDay`), longer and later-running entries go to the lane above the hours (`placesInTopLane`).
|
|
411
|
+
- Moves are proposals: drag, or Shift and an arrow key, then Enter (Escape drops it). An entry whose props do not
|
|
412
|
+
change stays where it was, so a refused move snaps back by construction. `movable: false` pins an entry.
|
|
413
|
+
- Overlapping items share at most `maxLanes` lanes, fewer in a narrow column; the column "+N" is the consumer's
|
|
414
|
+
`days[date].overflowCount`. `renderEntry(entry, { availableLines })` says how many text lines fit.
|
|
415
|
+
- **Colour.** Every calendar colour is named once at the top of `scheduling-calendar.css` (`--calendar-*`).
|
|
416
|
+
- **Deprecated, still working in 2.x:** `entry.state` / `SchedulingEntryState`, `overlay.kind` /
|
|
417
|
+
`SchedulingOverlayKind` and the `previous` / `next` labels. An entry with `state` is no longer tinted: the two
|
|
418
|
+
problem states show a marker with the word for that state.
|
|
419
|
+
|
|
357
420
|
### SchedulingTimeline
|
|
358
421
|
|
|
359
422
|
Lanes of bars on a time axis (HAR-1364; TENSOR's change day/week timeline, rollout Gantt rows and licence-renewal
|
|
@@ -378,6 +441,44 @@ markers). It pairs with `SchedulingCalendar`: same entry states, the same explic
|
|
|
378
441
|
- **Windows and markers:** `overlays` (`freeze`, `maintenance`, `blackout`; all lanes, or one with `laneId`), `markers` (a point in time) and `now` are drawn behind the bars and listed for screen readers.
|
|
379
442
|
- Pure helpers are exported for tests and server code: `timelineTicks`, `placeSpan`, `layoutLane`, `shiftSpan`, `snapToStep`, `pixelsToMs`, `defaultTimelineStep`.
|
|
380
443
|
|
|
444
|
+
### Calendar model (zoned days, hour slots, lanes)
|
|
445
|
+
|
|
446
|
+
Pure functions for calendar geometry in one explicit IANA display zone (HAR-1504). They use only
|
|
447
|
+
`Intl.DateTimeFormat`, never the process time zone, and never a fixed day length: DST days have 23 or 25
|
|
448
|
+
hours. Every interval is half-open, `[start, end)`.
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
zonedDayBounds('2026-10-25', 'Europe/Berlin'); // { start: 2026-10-24T22:00Z, end: 2026-10-25T23:00Z } (25 h)
|
|
452
|
+
zonedHourSlots('2026-10-25', 'Europe/Berlin'); // 25 slots; '02' twice, offsetLabel '+02:00' then '+01:00'
|
|
453
|
+
zonedHourSlots('2026-03-29', 'Europe/Berlin'); // 23 slots; no '02'
|
|
454
|
+
addZonedDays('2026-10-24T20:00Z', 1, 'Europe/Berlin'); // { instant: 2026-10-25T21:00Z, adjusted: null }
|
|
455
|
+
addZonedDays('2026-03-28T01:30Z', 1, 'Europe/Berlin'); // 03:30 local, adjusted: 'gap_forward'
|
|
456
|
+
zonedDaySpan('2026-10-10T22:00Z', '2026-10-11T22:00Z', 'Europe/Berlin'); // { start: '2026-10-11', end: '2026-10-12' }
|
|
457
|
+
enumerateDateKeys({ start: '2026-10-11', end: '2026-10-12' }); // { dates: ['2026-10-11'], truncated: false }
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
- **Days:** `zonedDayBounds(dateKey, tz)` is the day's first instant and the next day's first instant, so the end of
|
|
461
|
+
one day is the start of the next. A day whose midnight is skipped starts at its first existing instant (01:00).
|
|
462
|
+
- **Hour slots:** `zonedHourSlots(dateKey, tz)` gives one `{ instant, label, offsetLabel }` per real hour start.
|
|
463
|
+
`label` is the local hour (`'00'`–`'23'`); `offsetLabel` (`'+02:00'`) is set on both occurrences of a repeated
|
|
464
|
+
hour and is `null` otherwise.
|
|
465
|
+
- **"+1 day":** `addZonedDays(instant, n, tz)` keeps the wall-clock time. A repeated wall time keeps the source's
|
|
466
|
+
offset; a skipped one moves forward once and returns `adjusted: 'gap_forward'`, so the caller can announce it.
|
|
467
|
+
- **Day membership:** `zonedDaySpan(start, end, tz)` returns the days an entry touches as `[start, end)` date keys;
|
|
468
|
+
an entry ending at local 00:00 does not touch the next day. It throws a `RangeError` when `end` is not after
|
|
469
|
+
`start` and never swaps. `enumerateDateKeys(span, { limit })` lists them (default limit 370, `Infinity` for none)
|
|
470
|
+
and returns `truncated: true` instead of stopping silently.
|
|
471
|
+
- **Lanes:** `packLanes(intervals, maxLanes, { order })` assigns first-fit lanes; overlapping intervals never share a
|
|
472
|
+
lane, touching ones may. Intervals that do not fit get `lane: null`, and each overlap cluster reports its
|
|
473
|
+
`laneCount` and `overflow`. `order: 'start'` (default) is greedy by start, then end, then id; `order: 'given'`
|
|
474
|
+
packs in input order, so a consumer's ranking decides who gets a lane.
|
|
475
|
+
- **Top-N:** `rankOverflow(items, compare, n)` is a stable sort and cut: `{ visible, hidden }`. `hidden` counts only
|
|
476
|
+
what it was given; show the consumer's own total when it has one.
|
|
477
|
+
- **Formatters:** `cachedDateTimeFormat(locale, options)` returns one shared `Intl.DateTimeFormat` per locale and
|
|
478
|
+
options shape. The model formats through it, so a 2,100-entry month builds at most a handful of formatters.
|
|
479
|
+
- **Deprecated:** `zonedDateSpan` (end-inclusive, swaps inverted ends) and `enumerateDateSpan` (stops silently at
|
|
480
|
+
370) are unchanged in 2.x. Use `zonedDaySpan` and `enumerateDateKeys`.
|
|
481
|
+
|
|
381
482
|
### FilterEditor (typed column filters)
|
|
382
483
|
|
|
383
484
|
`FilterEditor` is the content of one column-filter popover with a typed value (HAR-1365; TENSOR's
|
|
@@ -403,6 +504,26 @@ const field: FilterField = { id: 'state', label: t('state'), kind: 'enum', optio
|
|
|
403
504
|
|
|
404
505
|
`isFilterEmpty`, `emptyFilterValue`, the operator lists and all words (`labels`) are exported, and the value is plain
|
|
405
506
|
data for your own query string. Stale search results are ignored.
|
|
507
|
+
|
|
508
|
+
**Presets (enum, HAR-1505).** `field.presets` names selections the product defines, e.g. a default scope. They
|
|
509
|
+
render as toggle `Chip`s (`aria-pressed`) in a labelled group above the search box and options. Activating one
|
|
510
|
+
replaces the selection with its `values`; the preset whose values equal the selection (as a set) is pressed, so
|
|
511
|
+
ticking another option un-presses it. `summarizeFilter` then returns the preset's label (the `preset` summary
|
|
512
|
+
label, default `'{label}'`; use `'{field}: {label}'` to keep the column name), and `matchFilterPreset(field, value)`
|
|
513
|
+
returns the matching preset for your own use. The group's name is `labels.presets` (default "Presets").
|
|
514
|
+
|
|
515
|
+
```tsx
|
|
516
|
+
const state: FilterField = {
|
|
517
|
+
id: 'state', label: t('state'), kind: 'enum', options: states,
|
|
518
|
+
presets: [
|
|
519
|
+
{ id: 'open', label: t('openWork'), values: ['new', 'open', 'pending'] },
|
|
520
|
+
{ id: 'all', label: t('all'), values: states.map((s) => s.value) },
|
|
521
|
+
],
|
|
522
|
+
};
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Every `FilterEditor` word can also come from `<UixLabelsProvider labels={{ filterEditor: { … } }}>`; an
|
|
526
|
+
explicit `labels` prop still wins.
|
|
406
527
|
### EntityPicker
|
|
407
528
|
|
|
408
529
|
A form field that holds one record found by an async search (HAR-1366; TENSOR's `entity-picker.tsx` and its
|
|
@@ -457,3 +578,249 @@ A full-screen media viewer (HAR-1367; TENSOR's capture viewer, MOTUS's programme
|
|
|
457
578
|
- **List:** each file shows its size and status. Uploads show a progress bar (indeterminate without `progress`), and failures show the reason and a Retry button. `previewUrl` shows a thumbnail, and `onAltChange` asks for alt text on images.
|
|
458
579
|
- **Attachment:** the name is the link (`href`, `download`, `renderLink`). It has a quieter `meta` line, a `state` slot (e.g. a "Scanning" `StatusPill`), `actions`, `onRemove`, and `status` `loading` | `error` | `forbidden` (no link, and the reason shown). It's server-renderable.
|
|
459
580
|
- Pure helpers: `formatFileSize`, `fileMatchesAccept`, `partitionFiles`, `fileKind`.
|
|
581
|
+
|
|
582
|
+
### TextDiff
|
|
583
|
+
|
|
584
|
+
A two-way diff of two texts (HAR-1368; TENSOR's knowledge-article version diff). `DiffViewer` stays the three-way configuration diff.
|
|
585
|
+
|
|
586
|
+
```tsx
|
|
587
|
+
<TextDiff before={v3.body} after={v4.body} beforeLabel={t('version', { n: 3 })} afterLabel={t('version', { n: 4 })}
|
|
588
|
+
label={t('bodyChanges')} context={3} />
|
|
589
|
+
<TextDiff before={v3.title} after={v4.title} granularity="word" label={t('titleChanges')} />
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
- **Lines** (`granularity="line"`, the default): a Myers diff. Removed and added lines are paired with their closest counterpart, so a renumbered list still lines up, and only the changed words are marked inside a pair.
|
|
593
|
+
- **Words** (`granularity="word"`): prose compared as one flow. Changed words across a space join into one change.
|
|
594
|
+
- **Views:** `split` (the default) puts the versions side by side and stacks below about 36rem of container width. `unified` interleaves them.
|
|
595
|
+
- **Not colour alone:** changes are `<del>` / `<ins>`, with a −/+ sign and a spoken "Removed:" / "Added:" (`labels`). Struck and underlined words mark changes inside a line.
|
|
596
|
+
- **Long texts:** `context` keeps that many unchanged lines around each change. Longer runs fold behind "Show n unchanged lines", and focus moves to the first revealed line. Over `maxTokens` (20 000) a note replaces the diff. Past `maxEdits` (2 000) the changed middle shows as one replacement instead of stalling.
|
|
597
|
+
- Pure model for tests and servers: `diffText`, `diffTokens`, `textDiffRows`, `tokenizeText`.
|
|
598
|
+
|
|
599
|
+
### DashboardGrid
|
|
600
|
+
|
|
601
|
+
The layout grid for dashboard widgets (HAR-1555; TENSOR's role dashboards). Pure layout: drag-and-drop, persistence and widget chrome stay in the product. Server-renderable.
|
|
602
|
+
|
|
603
|
+
```tsx
|
|
604
|
+
<DashboardGrid columns={{ base: 1, sm: 2, lg: 3 }} gap="md">
|
|
605
|
+
<DashboardGrid.Item><Stat label="Open incidents" value={42} /></DashboardGrid.Item>
|
|
606
|
+
<DashboardGrid.Item span={2}><Card title="Queue by team">…</Card></DashboardGrid.Item>
|
|
607
|
+
<DashboardGrid.Item span="full">
|
|
608
|
+
<Chart title="Resolved per hour" option={option} height="clamp(160px, 40cqi, 360px)" />
|
|
609
|
+
</DashboardGrid.Item>
|
|
610
|
+
</DashboardGrid>
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
- **Columns follow the grid's own width**, not the viewport (a container query), so the same dashboard has fewer columns beside an open sidebar. Breakpoints: `sm` 36rem, `md` 48rem, `lg` 60rem, `xl` 80rem of grid width, sized for widgets of about 280–320 px (2 columns from 36rem, 3 from 60rem, 4 from 80rem). A breakpoint you leave out keeps the count of the next smaller one; a missing `base` is 1. Without `columns`: 1, 2 from `sm`, 3 from `lg`. Counts are 1–4.
|
|
614
|
+
- **Spans:** `span={1}` (default), `span={2}` (clamps to the columns there are, so it is the whole row at one column), `span="full"` (`grid-column: 1 / -1`, the whole row at every width).
|
|
615
|
+
- **Size-aware content:** each item is an inline-size container named `uix-dashboard-grid-item`. Give a Chart a height in `cqi` (a share of the item's width), or pass your own height per widget size; the Chart already redraws at the item's width. Use `@container uix-dashboard-grid-item (min-width: …)` for a card's own split layout. Widgets in one row share a height (the item's last child grows).
|
|
616
|
+
- **`gap`:** `sm` (--uix-space-3), `md` (default, --uix-space-4), `lg` (--uix-space-6).
|
|
617
|
+
- Refs reach the grid and item `<div>`s, and other props pass through. For an element you render yourself (a drag-and-drop library's sortable node, a server template), use `dashboardGridClassName(columns, gap)` and `dashboardGridItemClassName(span)`.
|
|
618
|
+
- Plain HTML: `.uix-dashboard-grid` with `--cols-N` / `--{sm,md,lg,xl}-cols-N` and `--gap-sm|lg`, items `.uix-dashboard-grid__item` with `--span-2` / `--full`. Every direct child of the grid is a cell.
|
|
619
|
+
|
|
620
|
+
### Select draws its own list
|
|
621
|
+
|
|
622
|
+
`Select` (HAR-1572) no longer opens the browser's dropdown. It is the WAI-ARIA select-only combobox: a `<button role="combobox">` trigger and a UIx-drawn listbox in the top layer (a bottom sheet on phones). The props did not change, so existing call sites keep working:
|
|
623
|
+
|
|
624
|
+
```tsx
|
|
625
|
+
<Select id="status" name="status" value={status} onChange={(e) => setStatus(e.target.value)}>
|
|
626
|
+
<option value="" disabled hidden>Choose a status</option>
|
|
627
|
+
<optgroup label="Active">{active.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}</optgroup>
|
|
628
|
+
</Select>
|
|
629
|
+
<Select options={teams} value={team} onValueChange={(v) => setTeam(v as string)} searchable="auto" placeholder={t('team')} />
|
|
630
|
+
<Select multiple name="labels" options={labels} defaultValue={['network']} />
|
|
631
|
+
<Select loadOptions={(q, signal) => api.owners(q, { signal })} searchable options={[owner]} value={owner.value} />
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
- **Forms:** a visually hidden `<select data-uix-select-proxy>` holds `name`, `required`, `form`, `multiple` and the value. `ref` points at it, so `FormData`, `form.reset()`, autofill, react-hook-form `register` and `Controller` work as before. `onChange(e)` gets `e.target.value`, and a failed `required` focuses the trigger.
|
|
635
|
+
- **Keyboard:** ↑ ↓ Home End PageUp PageDown and typing move (repeat one letter to cycle). Enter, Space, Tab and Alt+↑ choose. Escape closes and keeps the value. With `multiple`, Space toggles and Delete clears.
|
|
636
|
+
- Plain Select for up to about 12 options, `searchable` for longer lists, `EntityPicker` or `SearchSuggest` for records.
|
|
637
|
+
|
|
638
|
+
### Gaps from the TENSOR migration (2.32.0)
|
|
639
|
+
|
|
640
|
+
Found on 2026-10-07/08 while TENSOR moved its hand-built UI onto 2.31.0. Everything is optional
|
|
641
|
+
unless a line says **Behaviour change**.
|
|
642
|
+
|
|
643
|
+
#### Anchored overlays stay inside the viewport
|
|
644
|
+
|
|
645
|
+
A `Popover` / `useAnchoredPosition` panel that fits neither above nor below its anchor used to
|
|
646
|
+
leave the viewport (a 384 px panel under a trigger in the middle of a 640 px phone screen got a
|
|
647
|
+
negative `top`). It now slides over the anchor and stays inside the 8 px padding, after a scroll
|
|
648
|
+
and after a resize as well. Once the anchor itself has left the viewport the panel follows it out.
|
|
649
|
+
|
|
650
|
+
```tsx
|
|
651
|
+
<Popover anchor={triggerRef} capHeight>…</Popover> // taller than the screen: capped, scrolls inside
|
|
652
|
+
useAnchoredPosition(anchorRef, panelRef, {
|
|
653
|
+
open,
|
|
654
|
+
capHeight: true, // max-height = viewport − 2 × padding (a smaller own max-height still applies)
|
|
655
|
+
onAnchorHidden: () => setOpen(false), // what `Popover closeWhenAnchorHidden` is built on
|
|
656
|
+
shiftMainAxis: false, // opt out: the 2.31.0 placement
|
|
657
|
+
});
|
|
658
|
+
computePosition(anchor, size, viewport).mainAxisRange // [min, max] the top is kept in, or null
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
With CSS anchor positioning the browser moves a box with its anchor *after* resolving `top`, so
|
|
662
|
+
no value can hold it at the viewport limit while the page scrolls. While a panel is held at the
|
|
663
|
+
limit, or within 64 px of it, the hook writes fixed coordinates instead; they cannot drift.
|
|
664
|
+
|
|
665
|
+
#### Menu: radio and checkbox items, attributes, Escape on the trigger
|
|
666
|
+
|
|
667
|
+
```tsx
|
|
668
|
+
<Menu trigger={<Button>Preset</Button>}>
|
|
669
|
+
<MenuItem id="manage" data-action-id="preset.manage" onSelect={manage}>Manage presets</MenuItem>
|
|
670
|
+
<MenuSeparator />
|
|
671
|
+
<MenuRadioGroup label="Dashboard preset" value={preset} onValueChange={setPreset}>
|
|
672
|
+
<MenuItemRadio value="mine">My queue</MenuItemRadio>
|
|
673
|
+
<MenuItemRadio value="team">Team</MenuItemRadio>
|
|
674
|
+
</MenuRadioGroup>
|
|
675
|
+
<MenuItemCheckbox checked={zebra} onCheckedChange={setZebra}>Alternating rows</MenuItemCheckbox>
|
|
676
|
+
</Menu>
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
- `MenuItemRadio` / `MenuItemCheckbox` render `menuitemradio` / `menuitemcheckbox` with
|
|
680
|
+
`aria-checked` and a check column. The menu opens on the checked radio item.
|
|
681
|
+
- A click or Enter follows `closeOnSelect` (radio: `true`, checkbox: `false`); Space changes the
|
|
682
|
+
state and leaves the menu open.
|
|
683
|
+
- Every item kind forwards `id` and `data-*` to its own element.
|
|
684
|
+
- Escape (and Tab) on the trigger closes a menu that has no item to focus (every item disabled).
|
|
685
|
+
- The menu is capped to the viewport and scrolls.
|
|
686
|
+
|
|
687
|
+
#### CollapsibleSection and Steps
|
|
688
|
+
|
|
689
|
+
```tsx
|
|
690
|
+
<CollapsibleSection title="Details" defaultOpen persistKey={`incident:${id}:details`} persistStorage="local">…</CollapsibleSection>
|
|
691
|
+
<CollapsibleSection title="SLA" compact headingLevel={3} lazy openRequest={jumpToSla}>…</CollapsibleSection>
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
- `defaultOpen` is the starting state; with `persistKey`, what the person chose wins over it. A
|
|
695
|
+
passed `open` is the same starting state and no longer overrides the remembered state at
|
|
696
|
+
mount; it still applies when the parent changes it.
|
|
697
|
+
- `persistStorage`: `'session'` (default) or `'local'`.
|
|
698
|
+
- `openRequest` already set at mount is honoured (a deep link), and every request moves focus to
|
|
699
|
+
the summary. `undefined` and `0` mean "no request yet".
|
|
700
|
+
- `compact`: a quiet row inside a card. `headingLevel`: the title is a real heading and the body a
|
|
701
|
+
`role="region"` named by it.
|
|
702
|
+
|
|
703
|
+
```tsx
|
|
704
|
+
<Steps progress={false} headingLevel={2} label="New change">
|
|
705
|
+
<Step title="What is changing" description="One sentence is enough."><Field …/></Step>
|
|
706
|
+
<Step title="When"><Field …/></Step>
|
|
707
|
+
</Steps>
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
`progress={false}` is a numbered list of sections that are all on screen: no state is drawn or
|
|
711
|
+
announced, and each step's children are its content, in a group named by the step title. Without
|
|
712
|
+
it `Steps` is the progress indicator it was.
|
|
713
|
+
|
|
714
|
+
Also: `DescriptionItem termProps` / `descriptionProps` (attributes for the `<dt>` / `<dd>`), and
|
|
715
|
+
`closeLabel` is optional on `PromptDialog` and `ConfirmDialog`.
|
|
716
|
+
|
|
717
|
+
#### Tabs: a name for the tablist
|
|
718
|
+
|
|
719
|
+
```tsx
|
|
720
|
+
<Tabs aria-label="Inbox filters" value={tab} onChange={setTab}>…</Tabs>
|
|
721
|
+
<Tabs aria-labelledby="news-heading" overflow="scroll" …>…</Tabs>
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
`Tabs` passes HTML attributes to its `role="tablist"` element, in both overflow modes.
|
|
725
|
+
|
|
726
|
+
#### Segmented: one tab stop, arrow keys, a radiogroup mode
|
|
727
|
+
|
|
728
|
+
```tsx
|
|
729
|
+
<Segmented aria-label="Row density" value={density} onChange={setDensity}>…</Segmented>
|
|
730
|
+
<Segmented selection="radio" aria-labelledby="theme-label" value={theme} onChange={setTheme}>…</Segmented>
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
- **Behaviour change:** a `Segmented` is one tab stop (the selected option, or the first enabled
|
|
734
|
+
one). Arrow Right/Down and Left/Up move and select, wrapping; Home / End jump; Left and Right
|
|
735
|
+
swap in a right-to-left group. Before, every option was a tab stop and no key was handled.
|
|
736
|
+
- `selection="radio"` renders `radiogroup` / `radio` / `aria-checked` instead of `group` /
|
|
737
|
+
`aria-pressed`, for a setting with exactly one value.
|
|
738
|
+
|
|
739
|
+
#### useVirtualRows
|
|
740
|
+
|
|
741
|
+
```tsx
|
|
742
|
+
const v = useVirtualRows(rows, { rowHeight: 44, estimatedViewportHeight: 600, enabled: rows.length > 0 });
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
- `estimatedViewportHeight`: the height to assume until the scroller is measured (and whenever it
|
|
746
|
+
measures 0), so a 5,000-row table mounts with one window, not 5,000 rows.
|
|
747
|
+
- When the row count shrinks under a large `scrollTop`, the window is clamped to the new count in
|
|
748
|
+
the same render: the spacer is never taller than the list.
|
|
749
|
+
- `enabled: false` returns every row and no spacers. Rows are windowed when
|
|
750
|
+
`rows.length > threshold` (default 100).
|
|
751
|
+
|
|
752
|
+
#### RuleBuilder for a flat AND-list
|
|
753
|
+
|
|
754
|
+
```tsx
|
|
755
|
+
<RuleBuilder value={rule} onChange={setRule} fields={fields} operators={operators}
|
|
756
|
+
conditionsOnly combinator="and" allowGroups={false} checks={{ emptyGroup: false }} labels={t.ruleBuilder} />
|
|
757
|
+
<RuleBuilder … readOnly /> // the builder, every control disabled
|
|
758
|
+
<RuleBuilder … readOnly="summary" /> // one sentence
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
- The built-in validation messages are labels (`issueEmptyGroup`, `issueNoActions`,
|
|
762
|
+
`issueMissingField`, …) and `checks` turns individual checks off.
|
|
763
|
+
- `conditionsOnly`: no "Then" block; `actions` is then optional.
|
|
764
|
+
- `combinator` fixes every group to one value (shown as text, never emits the other);
|
|
765
|
+
`hideCombinator` removes the control. `allowGroups={false}` hides "Add group".
|
|
766
|
+
- **Behaviour change:** `readOnly` renders the builder with its controls disabled, not a sentence.
|
|
767
|
+
The sentence is `readOnly="summary"`, and its words are labels.
|
|
768
|
+
- `UixLabelsProvider labels={{ ruleBuilder }}` translates every builder below it.
|
|
769
|
+
|
|
770
|
+
#### Chip
|
|
771
|
+
|
|
772
|
+
```tsx
|
|
773
|
+
<Chip ref={chipRef} onClick={openEditor} onRemove={clear}
|
|
774
|
+
bodyProps={{ 'aria-haspopup': 'dialog', 'aria-expanded': open, 'aria-controls': editorId }}>Status: Open</Chip>
|
|
775
|
+
<Chip href="/incidents" current renderLink={link}>Incidents</Chip>
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
- The × is named from `UixLabelsProvider` `chip.remove` (`'{label} entfernen'`); `removeLabel`
|
|
779
|
+
still wins.
|
|
780
|
+
- The ref is the body (the button, link or span that carries the chip's action), also on a
|
|
781
|
+
removable chip; `bodyProps` puts attributes there.
|
|
782
|
+
- `current` on a link chip: `aria-current="page"` and the filled look.
|
|
783
|
+
|
|
784
|
+
#### FileUpload and Attachment
|
|
785
|
+
|
|
786
|
+
- A long unbroken file name no longer widens `FileUpload` past its container.
|
|
787
|
+
- Both read `UixLabelsProvider` (`fileUpload`, `attachment`); a `labels` prop wins per word.
|
|
788
|
+
- `sizeBase={1024}` counts in 1024s, `formatSize` takes the product's formatter, and
|
|
789
|
+
`formatFileSize(bytes, locale, { base: 1024, units })` does the same outside a component.
|
|
790
|
+
|
|
791
|
+
#### Alert, Note, StatusPill, PromptDialog, Popover
|
|
792
|
+
|
|
793
|
+
```tsx
|
|
794
|
+
<Alert ref={errorRef} tone="info" title="…" actions={<Button size="xs" variant="ghost">Details</Button>} onDismiss={hide}>…</Alert>
|
|
795
|
+
<StatusPill size="lg" tone="success" dot>Operational</StatusPill>
|
|
796
|
+
<PromptDialog … multiline destructive error={serverError} />
|
|
797
|
+
<Popover anchor={userRef} openOnHover>…</Popover>
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
- `Alert`: `onDismiss` + `dismissLabel` (`UixLabelsProvider` `alert.dismiss`), a trailing `actions`
|
|
801
|
+
slot, a forwarded ref, and a text column that wraps a long title at 320 px.
|
|
802
|
+
- `Note tone={undefined}` and `tone="neutral"` are the default look.
|
|
803
|
+
- `StatusPill size`: `'sm' | 'md' | 'lg'`.
|
|
804
|
+
- `PromptDialog`: `multiline` (a textarea; Ctrl/⌘+Enter submits), `destructive` (a danger submit,
|
|
805
|
+
`role="alertdialog"`), an `error` slot, `role`. The field takes focus when the dialog opens.
|
|
806
|
+
- `Popover openOnHover`: opens on pointer or focus on its anchor, stays while either is on the
|
|
807
|
+
anchor or the popover, closes after both left and on Escape.
|
|
808
|
+
|
|
809
|
+
#### Spinner, Meter, Heartbeat
|
|
810
|
+
|
|
811
|
+
```tsx
|
|
812
|
+
<Spinner size="sm" label="Uploading" />
|
|
813
|
+
<Meter tone="neutral" value={42} label="Option A, 42% of votes" />
|
|
814
|
+
<LiveIndicator state="live">Live</LiveIndicator> <Heartbeat state="danger" label="Connection lost" />
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
- `Spinner size="sm"` is 16 px.
|
|
818
|
+
- `Meter tone="neutral"` (grey) and `tone="accent"` (brand) are fills for a proportion that is not
|
|
819
|
+
a status; both clear 3:1 against the track in light and dark, and no tone word is spoken.
|
|
820
|
+
- `Heartbeat` is the pulsing dot (`live`, `idle`, `warning`, `danger`), decorative unless it has a
|
|
821
|
+
`label`; `LiveIndicator` adds the state in text. The ring stops under `prefers-reduced-motion`.
|
|
822
|
+
|
|
823
|
+
#### ViewMenu
|
|
824
|
+
|
|
825
|
+
The density options no longer break a label inside a word at phone width ("Großzügig"); options
|
|
826
|
+
that do not fit one row wrap as whole options.
|