@tensor_1/react 2.34.0 → 2.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +164 -15
  2. package/dist/{SchedulingCalendar-DSLQ6AY5.d.ts → SchedulingCalendar-B6TakO2h.d.ts} +13 -1
  3. package/dist/components/Avatar.d.ts +26 -4
  4. package/dist/components/Avatar.js +26 -3
  5. package/dist/components/CommandPalette.d.ts +6 -1
  6. package/dist/components/CommandPalette.js +9 -3
  7. package/dist/components/DateFieldParts.d.ts +61 -0
  8. package/dist/components/DateFieldParts.js +195 -0
  9. package/dist/components/DatePicker.d.ts +129 -0
  10. package/dist/components/DatePicker.js +340 -0
  11. package/dist/components/DateRangePicker.d.ts +34 -3
  12. package/dist/components/DateRangePicker.js +120 -17
  13. package/dist/components/EntityPicker.d.ts +29 -4
  14. package/dist/components/EntityPicker.js +20 -8
  15. package/dist/components/InlineEdit.d.ts +74 -0
  16. package/dist/components/InlineEdit.js +176 -0
  17. package/dist/components/List.d.ts +8 -1
  18. package/dist/components/List.js +3 -1
  19. package/dist/components/ListRoving.d.ts +12 -0
  20. package/dist/components/ListRoving.js +51 -0
  21. package/dist/components/Radio.d.ts +23 -2
  22. package/dist/components/Radio.js +18 -3
  23. package/dist/components/SchedulingAgenda.d.ts +1 -1
  24. package/dist/components/SchedulingAgenda.js +101 -4
  25. package/dist/components/SchedulingCalendar.d.ts +1 -1
  26. package/dist/components/SchedulingCalendar.js +89 -9
  27. package/dist/components/SchedulingTimeGrid.d.ts +1 -1
  28. package/dist/components/SchedulingTimeGrid.js +101 -4
  29. package/dist/components/SchedulingTimeline.d.ts +130 -15
  30. package/dist/components/SchedulingTimeline.js +765 -141
  31. package/dist/components/SearchSuggest.d.ts +12 -1
  32. package/dist/components/SearchSuggest.js +7 -2
  33. package/dist/components/Tree.d.ts +30 -2
  34. package/dist/components/Tree.js +135 -10
  35. package/dist/date-field-model.d.ts +40 -0
  36. package/dist/date-field-model.js +79 -0
  37. package/dist/index.cjs +4179 -2056
  38. package/dist/index.d.cts +829 -264
  39. package/dist/index.d.ts +10 -7
  40. package/dist/index.js +40 -4
  41. package/dist/labels-context.d.ts +15 -2
  42. package/dist/roving.d.ts +15 -0
  43. package/dist/roving.js +17 -0
  44. package/dist/scheduling-timeline-model.d.ts +61 -11
  45. package/dist/scheduling-timeline-model.js +85 -26
  46. package/dist/scheduling-timeline-rows.d.ts +40 -0
  47. package/dist/scheduling-timeline-rows.js +42 -0
  48. package/package.json +5 -3
package/README.md CHANGED
@@ -437,6 +437,29 @@ const spanLayout = layoutMonthSpans(spansInSalienceOrder, grid, { timeZone, week
437
437
  - **A narrow month** (`monthDensity="counts"`): a cell is the date, the consumer's `days[date].count` and its markers.
438
438
  No entry is drawn and none is counted: a row's "+N" is for windows over `windowLaneCap` only. The setting does not
439
439
  touch the week.
440
+ - **Keyboard (HAR-1527).** The month grid, the time grid and the agenda are each **one tab stop**; no item is a
441
+ tab stop of its own.
442
+ - Month: arrows move between days, Home/End go to the first/last day shown. **Enter** on a day with items moves
443
+ into them (ArrowUp/ArrowDown, Home, End between them; Enter calls `onSelectEntry`; **Esc** returns to the day).
444
+ **Space** or a click on the day number calls `onSelectDate`; so does Enter on a day with no items.
445
+ - Week/Day time grid: the tab stop is a day head (ArrowLeft/ArrowRight between days). Enter goes into the
446
+ day's items and windows. On an item, Enter selects it, or confirms a pending move (`onProposeMove`); Esc drops
447
+ a pending move, otherwise returns to the day head.
448
+ - Agenda (grouped or flat): ArrowUp/ArrowDown, Home and End walk rows, window notes and "open day", also
449
+ through the rows a long agenda has not mounted yet.
450
+ - The keys act on the calendar's own items only: a field or link you render inside a cell or an item keeps its
451
+ keys. A row's "+N" is reached from the day it opens. An item reached with the pointer takes the tab stop to
452
+ its day. Esc on an item returns to its day and is not passed on; a second Esc reaches a surrounding dialog.
453
+ - A day button is named with the date and `days[date].label` (the count and the highest signal), through
454
+ `labels.dayName` (`'{date}, {label}'`). If your labels already say the date, set `dayName: '{label}'`.
455
+ - To put focus back on an item after closing your own panel: `querySelector('[data-item-id="…"]')?.focus()`.
456
+ - **Emphasis.** `entry.emphasis: 'highlight' | 'dim'` sets `data-highlight` / `data-dim` on that item in every
457
+ view. Highlight is a heavier edge in the text colour and a heavier weight, dim is quieter text;
458
+ neither changes a hue, the line style of the state, or the band's leading edge. With forced colours a
459
+ highlighted title is underlined and dimmed text is grey.
460
+ - **`<List roving>`**: the items are one tab stop (ArrowUp/ArrowDown, Home, End; Enter or Space activates the
461
+ focused item's `onClick`). A control inside an item keeps its own tab stop and keys. Items may be wrapped or
462
+ rendered later by a child component.
440
463
  - **Colour.** Every calendar colour is named once at the top of `scheduling-calendar.css` (`--calendar-*`).
441
464
  - **Deprecated, still working in 2.x:** `entry.state` / `SchedulingEntryState`, `overlay.kind` /
442
465
  `SchedulingOverlayKind` and the `previous` / `next` labels. An entry with `state` is no longer tinted: the two
@@ -444,27 +467,43 @@ const spanLayout = layoutMonthSpans(spansInSalienceOrder, grid, { timeZone, week
444
467
 
445
468
  ### SchedulingTimeline
446
469
 
447
- Lanes of bars on a time axis (HAR-1364; TENSOR's change day/week timeline, rollout Gantt rows and licence-renewal
448
- markers). It pairs with `SchedulingCalendar`: same entry states, the same explicit time zone.
470
+ Lanes of bars on a time axis (HAR-1364, HAR-1521): schedules by service, rollout rows, renewal markers. It pairs
471
+ with `SchedulingCalendar`: the same item API (`band`, `status`, `markers`, `accessibleName`), the same explicit
472
+ time zone, the same proposal model for moves.
449
473
 
450
474
  ```tsx
451
475
  <SchedulingTimeline
452
- lanes={[{ id: 'net', label: 'Network', meta: '3 changes' }]}
453
- items={changes.map((c) => ({ id: c.id, laneId: c.team, title: c.title, start: c.start, end: c.end, state: c.state }))}
454
- range={{ start: weekStart, end: weekEnd }} scale="day" timeZone={tz} now={nowIso}
455
- overlays={[{ id: 'q4', kind: 'freeze', label: 'Q4 freeze', start, end }]}
456
- markers={[{ id: 'r1', label: 'Renewal: Fortinet', at, laneId: 'net' }]}
457
- onSelectItem={(item) => openChange(item.id)}
458
- onMoveItem={(id, { start, end }) => reschedule(id, start, end)}
476
+ lanes={services.map((s) => ({ id: s.id, label: s.name }))}
477
+ groups={[{ id: 'pay', label: 'Payments', laneIds: ['checkout', 'ledger'], collapsed, summary: { count: 7 } }]}
478
+ onToggleGroup={(id) => toggle(id)}
479
+ items={rows.map((r) => ({ id: r.id, laneId: r.serviceId, title: r.title, start: r.start, end: r.end, band: r.band, status: r.status, markers: r.markers }))}
480
+ range={{ start: weekStart, end: weekEnd }} scale="day" subTicks={[6, 12, 18]} timeZone={tz} now={nowIso}
481
+ overlays={[{ id: 'q4', kindLabel: 'Hold', label: 'Quarter close', pattern: 'diagonal', laneIds: ['ledger'], start, end }]}
482
+ flagOverlaps={false}
483
+ onSelectItem={(item) => open(item.id)}
484
+ onProposeMove={(id, { start, end }) => askToMove(id, start, end)}
485
+ notice={truncated && 'Showing 500 of 1,240 items.'}
486
+ columnNotes={{ '2026-10-08': '12 more not shown' }}
459
487
  />
460
488
  ```
461
489
 
462
- - **Axis:** `scale` is `hour`, `day` (default), `week` or `month`. Ticks fall on wall-clock boundaries in `timeZone` (DST and half-hour zones included). `tickWidth` sets the width of one unit; the axis scrolls inside the component and the lane column stays put.
463
- - **Bars:** overlapping bars in a lane stack and are hatched (`data-conflict`), so an overlap reads without colour. A bar cut off by the range shows a dashed edge. `renderItem` replaces the bar text. The full title is in the name and the `title` tooltip.
464
- - **Moving:** with `onMoveItem`, bars move by drag or Shift+←/→ and their end changes with Alt+Shift+←/→. Moves snap to `step` (15 min on an hour axis, 1 h on a day axis, 1 day otherwise), and each move is announced. `movable: false` pins a bar. The component never moves a bar itself; it reports the new start and end and renders what you pass back.
465
- - **Keyboard:** one tab stop. ←/→ go to the previous or next bar in a lane, ↑/↓ to the nearest bar in the next lane, Home/End to the lane's ends, and Enter selects.
466
- - **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.
467
- - Pure helpers are exported for tests and server code: `timelineTicks`, `placeSpan`, `layoutLane`, `shiftSpan`, `snapToStep`, `pixelsToMs`, `defaultTimelineStep`.
490
+ - **Axis:** `scale` is `hour`, `day` (default), `week` or `month`. Ticks fall on wall-clock boundaries in `timeZone` (DST and half-hour zones included). `tickWidth` sets the width of one unit; the axis scrolls inside the component and the lane column stays put. On a `day` axis `subTicks` (local hours) draws minor ticks inside each day; on an `hour` axis an hour that occurs twice (the clocks went back) shows its UTC offset.
491
+ - **Groups:** `groups` puts lanes into collapsible blocks, in the order given; a lane no group names is drawn after them. A collapsed group is one row with `summary.count` and `summary.markers`, which the consumer supplies: the timeline derives no count, and the `groupSummary` label words it (`Items: {count}` by default). That row stands for the lanes it hides: when a window covers one of them (or every lane), the row gets a strip under its head, one sub-row tall, and the window is drawn there, where the group label cannot lie over it. With `onToggleGroup` the consumer owns `collapsed`; without it the group opens and closes itself.
492
+ - **Bars:** bars that share time in a lane stack in sub-rows, packed by `packLanes`. A bar is drawn at least 24 px wide (`--timeline-bar-min`) and is packed as that wide, so two bars never cover each other and each is a 24 × 24 px target with nothing over it. The price is height: where bars are closer together than 24 px of axis, a lane takes more sub-rows than it has bars running at once. On a month axis at the default `tickWidth` 24 px is about six days, so a lane of daily bars is six sub-rows tall; a wider `tickWidth` brings it back down. The packing follows the width of the track (it is measured), and when it changes the first row in view is kept where it was. Only `band="high"` is filled; `status` is a line style; `markers` are shapes or icons with text. `renderItem` replaces the bar text, which is cut at a word: a first word that does not fit is not shown, so no part of a word or of a time is. The full title is in the name and the `title` tooltip.
493
+ - **Stacking flag:** by default a bar that shares time with another bar of its lane is hatched (`data-conflict`) and its name says so (`labels.conflict`). With `flagOverlaps={false}` the stacking stays and the hatch and the words go: the consumer says what clashes, with a reason, through `markers`.
494
+ - **Windows and markers:** an overlay is one neutral element with a `pattern` and its `kindLabel`, name and `scopeLabel` as text, however many rows there are. `laneIds` limits it to the rows of those lanes (unset: every row). With `onSelectOverlay` each window is a named button; without, windows are listed for screen readers. `markers` (a point in time, optionally with `laneIds`) and `now` are one line each. The now-line is neutral.
495
+ - **Moving:** three models, chosen by the callback.
496
+ - `onProposeMove(id, { start, end, adjusted })`: drag a bar, or press Shift+←/→ to build a pending move, Enter to send it and Escape to drop it. One call per finished gesture. The timeline never moves the bar: an item whose props do not change is back where it was, so a refused move snaps back. Return a promise to keep the outline until it settles. On a week or month axis a step is a calendar day (the wall-clock time is kept across a clock change).
497
+ - `onMoveItem(id, { start, end })`: the 2.x model, one call for every key press and every drop.
498
+ - `onResizeItem(id, { start, end })`: Alt+Shift+←/→ moves the end of a bar. Without it there is no resize and no hint offers one.
499
+ Moves snap to `step` (15 min on an hour axis, 1 h on a day axis, 1 day otherwise). `movable: false` pins a bar.
500
+ - **Keyboard:** the bars are one tab stop. ←/→ go to the previous or next bar in a lane, ↑/↓ to the nearest bar in the next lane (over group heads), Home/End to the ends of the lane, and Enter selects, or confirms a pending move, never both (a click or Space does nothing while a move is pending). A move that was sent is announced (`moveSent`).
501
+ - **Long timelines:** above `virtualizeAbove` rows (default 150) only the rows near the viewport are mounted, and an arrow key that goes to a bar that is not mounted mounts it, scrolls to it and focuses it. The rows counted are the ones passed, `lanes` plus `groups`: the answer does not move with the width, with what is collapsed or with how the bars pack. What is mounted is what the viewport shows plus overscan (under 150 row elements up to a viewport of about 7,000 px). The row of the focused control is never unmounted. `maxHeight` makes the lanes scroll inside the timeline under the axis; without it the page scrolls.
502
+ - **Notice and counts:** `notice` is shown above the axis; `columnNotes` puts a note on the column of a day (`YYYY-MM-DD` in `timeZone`).
503
+ - **Row heights:** with `groups`, `laneIds` or a virtual window every row has a fixed height (a lane is its sub-rows, a group head one line) and a lane label that does not fit is cut with an ellipsis. A timeline that uses none of them keeps rows that grow with their label.
504
+ - **Colour:** every colour is named once at the top of `scheduling-timeline.css` (`--timeline-*`). Forced colours are handled in `forced-colors.css`, as for the calendar.
505
+ - **Deprecated, still working in 2.x:** `item.state` / `SchedulingEntryState`, `overlay.kind` / `SchedulingTimelineOverlayKind` and the `states` / `overlays` labels. A bar with `state` is no longer tinted: the two problem states show a marker with the word for that state, and the kinds are told apart by pattern.
506
+ - Pure helpers are exported for tests and server code: `timelineTicks`, `timelineSubTicks`, `timelineRepeatedHourOffset`, `placeSpan`, `layoutLane`, `shiftSpan`, `snapToStep`, `pixelsToMs`, `defaultTimelineStep`, `timelineStepDelta`.
468
507
 
469
508
  ### Calendar model (zoned days, hour slots, lanes)
470
509
 
@@ -849,3 +888,113 @@ const v = useVirtualRows(rows, { rowHeight: 44, estimatedViewportHeight: 600, en
849
888
 
850
889
  The density options no longer break a label inside a word at phone width ("Großzügig"); options
851
890
  that do not fit one row wrap as whole options.
891
+
892
+ ### Date fields, InlineEdit and other older gaps (2.36.0)
893
+
894
+ Older kit issues that TENSOR's triage still found blocking on 2026-10-08. Everything is additive.
895
+
896
+ #### DatePicker, DateTimePicker, DateRangePicker `mode="field"`
897
+
898
+ ```tsx
899
+ <Field label={t('dueDate')} hint={t('workingDaysOnly')}>
900
+ <DatePicker value={due} onValueChange={setDue} name="due" locale={locale} weekStartsOn={1}
901
+ min="2026-11-02" max="2026-12-31" isUnavailable={isWeekend} />
902
+ </Field>
903
+
904
+ <Field label={t('starts')}>
905
+ <DateTimePicker value={starts} onValueChange={setStarts} timeZone="Europe/Vienna" minuteStep={15} />
906
+ </Field>
907
+
908
+ <DateRangePicker mode="field" value={period} onChange={setPeriod} label={t('period')} locale={locale} />
909
+ ```
910
+
911
+ - **`DatePicker`** is a text input that shows the date and reads a typed one, with a button that
912
+ opens one month in a popover. The value is ISO `YYYY-MM-DD` or `null`; `name` submits it in a
913
+ hidden input whatever the field shows.
914
+ - **Typing:** read on Enter and when focus leaves. ISO is always accepted; otherwise three numbers
915
+ in the locale's order (`3.4.2026` is 3 April in German, `3/4/2026` is 4 March in en-US). Text
916
+ that is not a date, or a date outside `min` / `max` / `isUnavailable`, marks the field invalid,
917
+ says why, and leaves `value` alone (`onInputProblem` tells a form). Clearing the field sets
918
+ `null`.
919
+ - **Format:** the locale's numeric date by default. `formatDate` changes what the field writes;
920
+ pass `parseDate` with it when your format is not three numbers, and a `placeholder` so the
921
+ message can name the shape.
922
+ - **Calendar keys:** arrows by day and week, Home / End to the ends of the week, PageUp / PageDown
923
+ by month, with Shift by year. Focus never leaves `min` / `max`; a day refused by `isUnavailable`
924
+ can take focus and is read out, but cannot be chosen. ArrowDown in the field opens the
925
+ calendar; Escape closes it, returns focus to the field and does not close a drawer around it.
926
+ - **`DateTimePicker`** adds a time input. The value is `YYYY-MM-DDTHH:mm`, the shape
927
+ `datetime-local` holds, so it replaces that input without changing what a form submits. The
928
+ zone the wall clock is in is shown beside the time and is its description: `timeZone` shows
929
+ that zone's short name on the chosen day (it follows daylight saving), `timeZoneLabel` replaces
930
+ the text, and without either the viewer's own zone is shown. The value carries no zone.
931
+ - **`DateRangePicker mode="field"`** is a trigger that shows the range and opens the months in a
932
+ popover; it closes when the range has both ends. `DateRangePicker` also takes `weekStartsOn`.
933
+ - **Words:** `UixLabelsProvider` `datePicker`, `dateTimePicker` and the three new
934
+ `dateRangePicker` keys (`placeholder`, `fieldStart`, `fieldRange`).
935
+ - **Helpers:** `parseDateInput`, `formatDateKey`, `datePattern`, `dateOrder`, `isValidDateKey`,
936
+ `splitDateTime`, `joinDateTime`, `timeZoneName`.
937
+
938
+ #### InlineEdit
939
+
940
+ ```tsx
941
+ <DescriptionItem term={t('title')}>
942
+ <InlineEdit label={t('title')} value={record.title} onSave={(next) => api.rename(record.id, next)}
943
+ validate={(next) => (next.trim() ? undefined : t('titleRequired'))} />
944
+ </DescriptionItem>
945
+ ```
946
+
947
+ The value is a button named by the value and "Edit {label}". Activating it swaps in an editor with
948
+ Save and Cancel: Enter saves (Ctrl/⌘+Enter in `multiline`), Escape cancels without reaching a
949
+ drawer around it, and nothing is saved on blur. A promise from `onSave` shows the busy state; a
950
+ rejection keeps the draft and shows the reason. Afterwards focus is back on the value.
951
+ `renderView` and `renderEditor` replace either half (a `Select`, a `DatePicker`); `disabled`
952
+ renders plain text. Words: `labels` or `UixLabelsProvider` `inlineEdit`.
953
+
954
+ #### RadioCard
955
+
956
+ ```tsx
957
+ <RadioGroup variant="card" label={t('rollout')}>
958
+ <RadioCard name="rollout" value="team" title={t('oneTeam')} description={t('oneTeamHint')}
959
+ checked={rollout === 'team'} onChange={() => setRollout('team')} />
960
+ …
961
+ </RadioGroup>
962
+ ```
963
+
964
+ A native radio inside a card: one tab stop for the group, arrow keys choose, the whole card is
965
+ the target. The choice is a border and a check mark, not colour alone. `media` puts a preview
966
+ above the text. The grid wraps to one column on a phone.
967
+
968
+ #### Tree loads children on expand
969
+
970
+ ```tsx
971
+ <Tree nodes={roots} loadChildren={(node) => api.org.children(node.id)} /> // roots: { id, label, hasChildren: true }
972
+ ```
973
+
974
+ A node with `hasChildren` and no `children` is expandable. The first time it is expanded
975
+ `loadChildren` runs: the node is `aria-busy` and shows a loading row; a rejection shows an error
976
+ row whose Enter, Space or click retries. Focus follows the row that replaces the one it was on,
977
+ and lands on the node once its children are there. The tree keeps the loaded children; pass them
978
+ back in `nodes` when you own the data. Words: `labels` or `UixLabelsProvider` `tree`.
979
+
980
+ #### Avatar presence
981
+
982
+ ```tsx
983
+ <Avatar presence="busy" src={user.photo} alt={user.name} /> <PresenceDot presence="away" />
984
+ ```
985
+
986
+ `presence` is `online` (a filled dot), `busy` (a dot with a bar), `away` and `offline` (two
987
+ rings): a shape as well as a colour, and the state in visually hidden text (`presenceLabels`
988
+ translates it). `status` is still `presence="online"`. `PresenceDot` is the dot alone, named by
989
+ its state (`label={null}` makes it decorative). Fixed: the dot was clipped to a quarter by the avatar's round
990
+ mask; an avatar with a dot no longer clips it.
991
+
992
+ #### EntityPicker and CommandPalette words
993
+
994
+ - `EntityPicker onSearch` may resolve `{ options, hasMore: true }` when its list was cut off: the
995
+ list ends with a row that says more match (not an option; arrow keys skip it) and the polite
996
+ status says so too. Below `minQueryLength` the open list shows "Type at least {count}
997
+ characters." Both, and every other word, come from `labels` or `UixLabelsProvider`
998
+ `entityPicker`. `SearchSuggest` has the two slots these use: `idle` and `note`.
999
+ - `CommandPalette` announces its result count through `UixLabelsProvider` `commandPalette`
1000
+ (`resultsOne`, `resultsMany`, `{count}`) or a `resultsLabel(count)` prop; English is unchanged.
@@ -97,6 +97,12 @@ interface SchedulingCalendarEntry {
97
97
  allDay?: boolean;
98
98
  /** `false` keeps this entry where it is when the grid allows moves (`canMove`). */
99
99
  movable?: boolean;
100
+ /**
101
+ * Draws the item with more or less weight than its neighbours, for example the partners of
102
+ * the item the user picked (`'highlight'`) against everything else (`'dim'`). It is a line
103
+ * weight and a quieter text colour, never a hue.
104
+ */
105
+ emphasis?: 'highlight' | 'dim';
100
106
  }
101
107
  interface SchedulingCalendarOverlay {
102
108
  id: string;
@@ -123,7 +129,11 @@ interface SchedulingCalendarDay {
123
129
  count: number;
124
130
  /** The "+N" of the cell. 0 shows none. */
125
131
  overflowCount: number;
126
- /** The accessible text of the count, e.g. "7 items, 2 need review". */
132
+ /**
133
+ * The accessible text of the count, e.g. "7 items, 2 need review". The day's button is named
134
+ * with the date and this text (`labels.dayName`), so a screen reader hears the date, the count
135
+ * and the highest signal in one go.
136
+ */
127
137
  label?: string;
128
138
  markers?: SchedulingMarker[];
129
139
  }
@@ -193,6 +203,8 @@ interface SchedulingCalendarLabels {
193
203
  moreSpans?: string;
194
204
  /** Accessible name of that control. `{count}` and `{date}` (the first day with a hidden span). */
195
205
  moreSpansLabel?: string;
206
+ /** The name of a day's button when `days[date].label` is given. `{date}` and `{label}` placeholders; use `'{label}'` if your labels already say the date. */
207
+ dayName?: string;
196
208
  /** The accessible text of a day's count when `days[date].label` is not given. `{count}` placeholder. */
197
209
  dayCount?: string;
198
210
  states: Record<SchedulingEntryState, string>;
@@ -1,19 +1,41 @@
1
1
  import * as react from 'react';
2
2
  import { HTMLAttributes, ReactNode } from 'react';
3
3
 
4
+ /** Presence of a person (HAR-1374). Each state has its own shape as well as its own colour. */
5
+ type AvatarPresence = 'online' | 'busy' | 'away' | 'offline';
6
+ /** The words read for each presence state. English; pass translations as `presenceLabels`. */
7
+ declare const DEFAULT_PRESENCE_LABELS: Record<AvatarPresence, string>;
4
8
  interface AvatarProps extends HTMLAttributes<HTMLSpanElement> {
5
- /** TENSOR RX-125 (UIX-04): translatable; English default. */
9
+ /** TENSOR RX-125 (UIX-04): translatable; English default. Still honoured for `online`. */
6
10
  onlineLabel?: string;
7
11
  size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
8
12
  src?: string;
9
13
  alt?: string;
10
14
  /** Initials / fallback shown when there is no `src`. */
11
15
  children?: ReactNode;
12
- /** Online status dot. */
16
+ /** Online status dot. The same as `presence="online"`; kept for existing callers. */
13
17
  status?: boolean;
18
+ /**
19
+ * A presence dot: `online` (filled green), `busy` (red with a bar), `away` (an amber ring),
20
+ * `offline` (a grey ring). The state is also in visually hidden text, so it is never colour
21
+ * alone.
22
+ */
23
+ presence?: AvatarPresence;
24
+ /** Translations of the presence words. */
25
+ presenceLabels?: Partial<Record<AvatarPresence, string>>;
14
26
  }
15
27
  /** Avatar over `.uix-avatar`. Renders an image when `src` is set, else children (initials). */
16
- declare function Avatar({ size, src, alt, children, status, onlineLabel, className, ...props }: AvatarProps): react.JSX.Element;
28
+ declare function Avatar({ size, src, alt, children, status, presence, presenceLabels, onlineLabel, className, ...props }: AvatarProps): react.JSX.Element;
29
+ interface PresenceDotProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
30
+ presence: AvatarPresence;
31
+ /**
32
+ * The state in words for assistive technology. Default: the English word. Pass `null` when
33
+ * the state is already written next to the dot, to make the dot decorative.
34
+ */
35
+ label?: string | null;
36
+ }
37
+ /** The presence dot on its own, for a row or a menu that shows no avatar (HAR-1374). */
38
+ declare function PresenceDot({ presence, label, className, ...props }: PresenceDotProps): react.JSX.Element;
17
39
  interface AvatarGroupProps extends HTMLAttributes<HTMLSpanElement> {
18
40
  children?: ReactNode;
19
41
  }
@@ -29,4 +51,4 @@ interface UserChipProps extends HTMLAttributes<HTMLSpanElement> {
29
51
  /** Avatar + name (+ optional sub-line) over `.uix-user-chip`. */
30
52
  declare function UserChip({ avatar, name, sub, className, children, ...props }: UserChipProps): react.JSX.Element;
31
53
 
32
- export { Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps, UserChip, type UserChipProps };
54
+ export { Avatar, AvatarGroup, type AvatarGroupProps, type AvatarPresence, type AvatarProps, DEFAULT_PRESENCE_LABELS, PresenceDot, type PresenceDotProps, UserChip, type UserChipProps };
@@ -1,12 +1,33 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { cx } from "../cx.js";
3
- function Avatar({ size = "md", src, alt, children, status, onlineLabel = "online", className, ...props }) {
3
+ const DEFAULT_PRESENCE_LABELS = {
4
+ online: "online",
5
+ busy: "busy",
6
+ away: "away",
7
+ offline: "offline"
8
+ };
9
+ function Avatar({ size = "md", src, alt, children, status, presence, presenceLabels, onlineLabel, className, ...props }) {
10
+ const state = presence ?? (status ? "online" : void 0);
11
+ const word = state && (presenceLabels?.[state] ?? (state === "online" ? onlineLabel : void 0) ?? DEFAULT_PRESENCE_LABELS[state]);
4
12
  return /* @__PURE__ */ jsxs("span", { className: cx("uix-avatar", size !== "md" && `uix-avatar--${size}`, className), ...props, children: [
5
13
  src ? /* @__PURE__ */ jsx("img", { src, alt: alt ?? "" }) : children,
6
- status && /* @__PURE__ */ jsx("span", { className: "uix-avatar__status", "aria-hidden": "true" }),
7
- status && /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: onlineLabel })
14
+ state && /* @__PURE__ */ jsx("span", { className: "uix-avatar__status", "data-presence": state === "online" ? void 0 : state, "aria-hidden": "true" }),
15
+ state && /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: word })
8
16
  ] });
9
17
  }
18
+ function PresenceDot({ presence, label, className, ...props }) {
19
+ const decorative = label === null;
20
+ return /* @__PURE__ */ jsx(
21
+ "span",
22
+ {
23
+ className: cx("uix-presence", `uix-presence--${presence}`, className),
24
+ role: decorative ? void 0 : "img",
25
+ "aria-label": decorative ? void 0 : label ?? DEFAULT_PRESENCE_LABELS[presence],
26
+ "aria-hidden": decorative ? true : void 0,
27
+ ...props
28
+ }
29
+ );
30
+ }
10
31
  function AvatarGroup({ children, className, ...props }) {
11
32
  return /* @__PURE__ */ jsx("span", { className: cx("uix-avatar-group", className), ...props, children });
12
33
  }
@@ -23,5 +44,7 @@ function UserChip({ avatar, name, sub, className, children, ...props }) {
23
44
  export {
24
45
  Avatar,
25
46
  AvatarGroup,
47
+ DEFAULT_PRESENCE_LABELS,
48
+ PresenceDot,
26
49
  UserChip
27
50
  };
@@ -4,6 +4,11 @@ import { ReactNode, HTMLAttributes, InputHTMLAttributes } from 'react';
4
4
  interface CommandPaletteProps extends HTMLAttributes<HTMLDivElement> {
5
5
  /** TENSOR RX-125 (UIX-04): translatable; English default. */
6
6
  inputLabel?: string;
7
+ /**
8
+ * The polite result count for this palette (HAR-1647). Default: `UixLabelsProvider`
9
+ * `commandPalette.resultsOne` / `resultsMany` (`{count}`), then "1 result" / "3 results".
10
+ */
11
+ resultsLabel?: (count: number) => string;
7
12
  /** Props for the search input (value/onChange/placeholder wired by the consumer). */
8
13
  inputProps?: InputHTMLAttributes<HTMLInputElement>;
9
14
  children?: ReactNode;
@@ -14,7 +19,7 @@ interface CommandPaletteProps extends HTMLAttributes<HTMLDivElement> {
14
19
  * a combobox over the listbox of `CommandItem`s; spreads override every default, so
15
20
  * consumers already passing their own roles/labels keep working.
16
21
  */
17
- declare function CommandPalette({ inputProps, children, inputLabel: inputLabelProp, className, ...props }: CommandPaletteProps): react.JSX.Element;
22
+ declare function CommandPalette({ inputProps, children, inputLabel: inputLabelProp, resultsLabel, className, ...props }: CommandPaletteProps): react.JSX.Element;
18
23
  interface CommandGroupProps {
19
24
  label?: ReactNode;
20
25
  children?: ReactNode;
@@ -1,12 +1,18 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { createContext, useCallback, useContext, useEffect, useId, useMemo, useState } from "react";
3
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
4
4
  import { cx } from "../cx.js";
5
+ import { fillLabel } from "../fill-label.js";
5
6
  import { useUixLabels } from "../labels-context.js";
6
7
  const CmdkContext = createContext(null);
7
- function CommandPalette({ inputProps, children, inputLabel: inputLabelProp, className, ...props }) {
8
+ function CommandPalette({ inputProps, children, inputLabel: inputLabelProp, resultsLabel, className, ...props }) {
8
9
  const uixLabels = useUixLabels();
9
10
  const inputLabel = inputLabelProp ?? uixLabels.commandPalette?.input ?? "Command palette";
11
+ const resultsOne = uixLabels.commandPalette?.resultsOne ?? "{count} result";
12
+ const resultsMany = uixLabels.commandPalette?.resultsMany ?? "{count} results";
13
+ const resultsText = (count2) => resultsLabel?.(count2) ?? fillLabel(count2 === 1 ? resultsOne : resultsMany, { count: count2 });
14
+ const resultsTextRef = useRef(resultsText);
15
+ resultsTextRef.current = resultsText;
10
16
  const listId = useId();
11
17
  const [activeId, setActiveId] = useState(null);
12
18
  const [count, setCount] = useState(0);
@@ -19,7 +25,7 @@ function CommandPalette({ inputProps, children, inputLabel: inputLabelProp, clas
19
25
  return () => setCount((c) => c - 1);
20
26
  }, []);
21
27
  useEffect(() => {
22
- const t = window.setTimeout(() => setAnnounced(`${count} result${count === 1 ? "" : "s"}`), 150);
28
+ const t = window.setTimeout(() => setAnnounced(resultsTextRef.current(count)), 150);
23
29
  return () => clearTimeout(t);
24
30
  }, [count]);
25
31
  const ctx = useMemo(() => ({ listId, setActive, registerItem }), [listId, setActive, registerItem]);
@@ -0,0 +1,61 @@
1
+ import * as react from 'react';
2
+ import { MouseEvent } from 'react';
3
+ import { CalendarWeekday } from '../calendar-model.js';
4
+
5
+ /** The viewer's local calendar date as `YYYY-MM-DD` (not UTC: "today" is a local idea). */
6
+ declare const localDateKey: (now?: Date) => string;
7
+ declare const monthLabel: (date: string, locale?: string) => string;
8
+ declare const fullDateLabel: (date: string, locale?: string) => string;
9
+ /**
10
+ * Short weekday names in `locale`, starting on `weekStartsOn`. 2024-01-07 is a Sunday;
11
+ * formatting in UTC keeps each reference day on its own date.
12
+ */
13
+ declare const weekdayNames: (locale: string | undefined, weekStartsOn?: CalendarWeekday) => string[];
14
+ /**
15
+ * Open state and trigger wiring for a field whose calendar lives in a kit `Popover`
16
+ * (`popover="auto"`, so a press outside or Escape closes it). `restoreFocus` is called when it
17
+ * closes while focus is inside it or nowhere: the field takes focus back.
18
+ */
19
+ declare function useFieldPopover(popoverId: string, restoreFocus: () => void, onOpenChange?: (open: boolean) => void): {
20
+ open: boolean;
21
+ show: () => void;
22
+ hide: (restore?: boolean) => void;
23
+ /** Spread on the button that opens the calendar. */
24
+ triggerProps: {
25
+ 'aria-haspopup': "dialog";
26
+ 'aria-expanded': boolean;
27
+ 'aria-controls': string;
28
+ onPointerDown: () => void;
29
+ onPointerLeave: () => void;
30
+ onPointerCancel: () => void;
31
+ onClick: (event: MouseEvent<HTMLElement>) => void;
32
+ };
33
+ };
34
+ interface MonthCalendarProps {
35
+ /** The selected day, or `null`. */
36
+ value: string | null;
37
+ onSelect: (date: string) => void;
38
+ /** The day that has focus when the calendar opens. */
39
+ initialDate: string;
40
+ min?: string;
41
+ max?: string;
42
+ isUnavailable?: (date: string) => boolean;
43
+ locale?: string;
44
+ weekStartsOn?: CalendarWeekday;
45
+ today: string | null;
46
+ labels: {
47
+ previousMonth: string;
48
+ nextMonth: string;
49
+ };
50
+ /** Move focus to the active day when the calendar mounts. */
51
+ autoFocus?: boolean;
52
+ }
53
+ /**
54
+ * One month of day buttons with the full calendar key path: arrows by day and week, Home / End
55
+ * to the ends of the week, PageUp / PageDown by month, with Shift by year. Days outside
56
+ * `min` / `max` are never reached; unavailable days inside them can be reached and read out,
57
+ * but not chosen. Shares the day-cell look of the range picker.
58
+ */
59
+ declare function MonthCalendar({ value, onSelect, initialDate, min, max, isUnavailable, locale, weekStartsOn, today, labels, autoFocus, }: MonthCalendarProps): react.JSX.Element;
60
+
61
+ export { MonthCalendar, type MonthCalendarProps, fullDateLabel, localDateKey, monthLabel, useFieldPopover, weekdayNames };