@tensor_1/react 2.47.0 → 2.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +470 -1
  2. package/dist/{SchedulingCalendar-B6TakO2h.d.ts → SchedulingCalendar-WWyFL3Eo.d.ts} +51 -2
  3. package/dist/announce.d.ts +10 -0
  4. package/dist/announce.js +30 -0
  5. package/dist/chart-analytic.cjs +33 -25
  6. package/dist/chart-preset.cjs +23 -15
  7. package/dist/chart.cjs +23 -15
  8. package/dist/combobox-model.d.ts +10 -1
  9. package/dist/combobox-model.js +15 -0
  10. package/dist/command-palette-model.d.ts +44 -0
  11. package/dist/command-palette-model.js +36 -0
  12. package/dist/components/AccountMenu.d.ts +44 -0
  13. package/dist/components/AccountMenu.js +43 -0
  14. package/dist/components/AppShell.d.ts +45 -2
  15. package/dist/components/AppShell.js +116 -5
  16. package/dist/components/AuditLog.d.ts +128 -0
  17. package/dist/components/AuditLog.js +196 -0
  18. package/dist/components/BrandProfiles.js +4 -3
  19. package/dist/components/Breadcrumbs.d.ts +9 -2
  20. package/dist/components/Breadcrumbs.js +3 -2
  21. package/dist/components/Button.d.ts +12 -5
  22. package/dist/components/Button.js +7 -3
  23. package/dist/components/Card.d.ts +18 -1
  24. package/dist/components/Card.js +5 -1
  25. package/dist/components/ChangeHighlight.d.ts +30 -0
  26. package/dist/components/ChangeHighlight.js +21 -0
  27. package/dist/components/ChartCore.js +2 -1
  28. package/dist/components/Checkbox.d.ts +27 -1
  29. package/dist/components/Checkbox.js +31 -2
  30. package/dist/components/CodeDisplay.d.ts +65 -0
  31. package/dist/components/CodeDisplay.js +124 -0
  32. package/dist/components/Combobox.d.ts +27 -6
  33. package/dist/components/Combobox.js +195 -58
  34. package/dist/components/CommandPalette.d.ts +36 -1
  35. package/dist/components/CommandPalette.js +254 -2
  36. package/dist/components/ContactCard.d.ts +66 -0
  37. package/dist/components/ContactCard.js +74 -0
  38. package/dist/components/Count.d.ts +24 -0
  39. package/dist/components/Count.js +32 -0
  40. package/dist/components/DataWorkspace.d.ts +167 -0
  41. package/dist/components/DataWorkspace.js +383 -0
  42. package/dist/components/DateFieldParts.d.ts +2 -1
  43. package/dist/components/DateFieldParts.js +7 -0
  44. package/dist/components/DatePicker.d.ts +9 -2
  45. package/dist/components/DatePicker.js +67 -28
  46. package/dist/components/DateRangePicker.d.ts +12 -2
  47. package/dist/components/DateRangePicker.js +13 -12
  48. package/dist/components/DetailPage.d.ts +7 -1
  49. package/dist/components/DetailPage.js +2 -1
  50. package/dist/components/Dialogs.d.ts +71 -10
  51. package/dist/components/Dialogs.js +130 -30
  52. package/dist/components/DiffViewer.d.ts +23 -3
  53. package/dist/components/DiffViewer.js +26 -12
  54. package/dist/components/DiscardPrompt.js +16 -0
  55. package/dist/components/DisplayPreferences.d.ts +83 -0
  56. package/dist/components/DisplayPreferences.js +185 -0
  57. package/dist/components/EditorialHome.d.ts +23 -3
  58. package/dist/components/EditorialHome.js +16 -11
  59. package/dist/components/EntityPicker.d.ts +20 -3
  60. package/dist/components/EntityPicker.js +32 -13
  61. package/dist/components/FilterEditor.d.ts +18 -4
  62. package/dist/components/FilterEditor.js +141 -19
  63. package/dist/components/InfoTip.js +11 -12
  64. package/dist/components/InlineEdit.d.ts +87 -20
  65. package/dist/components/InlineEdit.js +197 -54
  66. package/dist/components/Input.d.ts +12 -2
  67. package/dist/components/Input.js +5 -3
  68. package/dist/components/Kbd.d.ts +6 -1
  69. package/dist/components/Kbd.js +6 -6
  70. package/dist/components/Lightbox.d.ts +41 -5
  71. package/dist/components/Lightbox.js +95 -9
  72. package/dist/components/MatchReview.d.ts +40 -1
  73. package/dist/components/MatchReview.js +130 -4
  74. package/dist/components/MetricInput.js +3 -2
  75. package/dist/components/Modal.d.ts +2 -1
  76. package/dist/components/Modal.js +13 -4
  77. package/dist/components/NavFavourites.d.ts +5 -0
  78. package/dist/components/NavFavourites.js +7 -7
  79. package/dist/components/OtpInput.d.ts +33 -0
  80. package/dist/components/OtpInput.js +123 -0
  81. package/dist/components/PageHeader.d.ts +7 -1
  82. package/dist/components/PageHeader.js +2 -2
  83. package/dist/components/Pagination.js +6 -5
  84. package/dist/components/PasswordInput.d.ts +34 -0
  85. package/dist/components/PasswordInput.js +101 -0
  86. package/dist/components/Peek.js +3 -3
  87. package/dist/components/Popover.js +2 -0
  88. package/dist/components/PrintLayout.d.ts +63 -0
  89. package/dist/components/PrintLayout.js +114 -0
  90. package/dist/components/QrCode.d.ts +47 -0
  91. package/dist/components/QrCode.js +55 -0
  92. package/dist/components/QrScanner.d.ts +95 -0
  93. package/dist/components/QrScanner.js +266 -0
  94. package/dist/components/RelationshipGraph.d.ts +32 -1
  95. package/dist/components/RelationshipGraph.js +18 -5
  96. package/dist/components/RelationshipGraphLayered.d.ts +1 -1
  97. package/dist/components/RelationshipGraphLayered.js +190 -115
  98. package/dist/components/RelationshipGraphList.js +33 -6
  99. package/dist/components/RuleBuilder.js +39 -7
  100. package/dist/components/SchedulingAgenda.d.ts +3 -1
  101. package/dist/components/SchedulingCalendar.d.ts +3 -1
  102. package/dist/components/SchedulingCalendar.js +43 -4
  103. package/dist/components/SchedulingResourceDay.d.ts +57 -0
  104. package/dist/components/SchedulingResourceDay.js +287 -0
  105. package/dist/components/SchedulingTimeGrid.d.ts +3 -1
  106. package/dist/components/SchedulingTimeline.d.ts +3 -1
  107. package/dist/components/SearchSuggest.d.ts +23 -3
  108. package/dist/components/SearchSuggest.js +48 -17
  109. package/dist/components/Segmented.d.ts +39 -4
  110. package/dist/components/Segmented.js +31 -3
  111. package/dist/components/Select.d.ts +3 -2
  112. package/dist/components/Select.js +1 -1
  113. package/dist/components/ShortcutHint.d.ts +11 -10
  114. package/dist/components/ShortcutHint.js +44 -38
  115. package/dist/components/Sidebar.d.ts +36 -4
  116. package/dist/components/Sidebar.js +37 -12
  117. package/dist/components/SortableList.d.ts +45 -0
  118. package/dist/components/SortableList.js +134 -0
  119. package/dist/components/Stat.d.ts +28 -3
  120. package/dist/components/Stat.js +13 -3
  121. package/dist/components/StatusPill.d.ts +14 -3
  122. package/dist/components/StatusPill.js +11 -3
  123. package/dist/components/Steps.js +2 -1
  124. package/dist/components/Table.d.ts +9 -2
  125. package/dist/components/Table.js +4 -2
  126. package/dist/components/TableControls.js +18 -58
  127. package/dist/components/TableOverflowProbe.d.ts +12 -0
  128. package/dist/components/TableOverflowProbe.js +27 -0
  129. package/dist/components/Tabs.d.ts +2 -1
  130. package/dist/components/Tabs.js +1 -0
  131. package/dist/components/TagInput.d.ts +67 -0
  132. package/dist/components/TagInput.js +342 -0
  133. package/dist/components/Textarea.d.ts +5 -0
  134. package/dist/components/Textarea.js +2 -2
  135. package/dist/components/TimePicker.d.ts +80 -0
  136. package/dist/components/TimePicker.js +347 -0
  137. package/dist/components/Toast.js +3 -2
  138. package/dist/components/Tooltip.d.ts +8 -4
  139. package/dist/components/Tooltip.js +24 -21
  140. package/dist/components/Tree.d.ts +8 -0
  141. package/dist/components/Tree.js +11 -5
  142. package/dist/display-preferences-model.d.ts +53 -0
  143. package/dist/display-preferences-model.js +64 -0
  144. package/dist/emoji.cjs +28 -35
  145. package/dist/filter-model.d.ts +59 -3
  146. package/dist/filter-model.js +67 -2
  147. package/dist/hooks/useChangeHighlight.d.ts +33 -0
  148. package/dist/hooks/useChangeHighlight.js +44 -0
  149. package/dist/hooks/useDialog.d.ts +5 -0
  150. package/dist/hooks/useDialog.js +3 -2
  151. package/dist/hooks/useOptimisticValue.d.ts +51 -0
  152. package/dist/hooks/useOptimisticValue.js +61 -0
  153. package/dist/hooks/useReducedMotion.d.ts +4 -1
  154. package/dist/hooks/useReducedMotion.js +7 -1
  155. package/dist/hooks/useResolvedFilterLabels.d.ts +15 -0
  156. package/dist/hooks/useResolvedFilterLabels.js +34 -0
  157. package/dist/hooks/useVirtualRows.d.ts +21 -2
  158. package/dist/hooks/useVirtualRows.js +30 -3
  159. package/dist/icons/Icon.d.ts +1 -1
  160. package/dist/index.cjs +18644 -13210
  161. package/dist/index.d.cts +3003 -699
  162. package/dist/index.d.ts +42 -16
  163. package/dist/index.js +139 -7
  164. package/dist/labels-context.d.ts +90 -3
  165. package/dist/overlay-stack.d.ts +6 -0
  166. package/dist/overlay-stack.js +13 -0
  167. package/dist/qr-model.d.ts +65 -0
  168. package/dist/qr-model.js +323 -0
  169. package/dist/relationship-graph-model.d.ts +78 -3
  170. package/dist/relationship-graph-model.js +112 -5
  171. package/dist/reorder.d.ts +30 -0
  172. package/dist/reorder.js +74 -0
  173. package/dist/rich-text.cjs +4 -2
  174. package/dist/scheduling-resource-model.d.ts +76 -0
  175. package/dist/scheduling-resource-model.js +74 -0
  176. package/dist/search-suggest-model.js +3 -1
  177. package/dist/shell-context.d.ts +11 -0
  178. package/dist/shell-context.js +7 -0
  179. package/dist/time-field-model.d.ts +46 -0
  180. package/dist/time-field-model.js +152 -0
  181. package/dist/toast-store.d.ts +17 -1
  182. package/dist/toast-store.js +23 -7
  183. package/package.json +2 -1
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useId, useMemo, useState } from "react";
4
+ import { useUixLabels } from "../labels-context.js";
4
5
  import { buildThreeWayDiff, summarizeDiff } from "../diff-model.js";
5
6
  import { cx } from "../cx.js";
6
7
  import { StatusPill } from "./StatusPill.js";
@@ -34,7 +35,7 @@ const DEFAULT_DIFF_VIEWER_LABELS = {
34
35
  progress: "{resolved} of {total} differences resolved."
35
36
  };
36
37
  const GROUPS = ["conflicted", "changed", "added", "removed"];
37
- const renderValue = (value, notPresent) => value === void 0 ? notPresent : JSON.stringify(value, null, 2);
38
+ const jsonText = (value, notPresent) => value === void 0 ? notPresent : JSON.stringify(value, null, 2);
38
39
  function DiffViewer({
39
40
  base,
40
41
  current,
@@ -44,9 +45,17 @@ function DiffViewer({
44
45
  onResolutionChange,
45
46
  labels: labelOverrides,
46
47
  controlSize = "sm",
47
- className
48
+ className,
49
+ labelFor,
50
+ renderValue,
51
+ provenance
48
52
  }) {
49
- const labels = { ...DEFAULT_DIFF_VIEWER_LABELS, ...labelOverrides };
53
+ const labels = { ...DEFAULT_DIFF_VIEWER_LABELS, ...useUixLabels().diffViewer, ...labelOverrides };
54
+ const nameOf = (path) => {
55
+ const node = labelFor?.(path);
56
+ if (node == null || node === false) return { node: /* @__PURE__ */ jsx("code", { children: path }), text: path };
57
+ return { node, text: typeof node === "string" || typeof node === "number" ? String(node) : path };
58
+ };
50
59
  const resolutionLabel = { accept: labels.resolutionAccept, skip: labels.resolutionSkip, pending: labels.resolutionPending };
51
60
  const buttonSize = controlSize === "md" ? void 0 : `uix-btn--${controlSize}`;
52
61
  const id = useId();
@@ -91,19 +100,24 @@ function DiffViewer({
91
100
  ] }),
92
101
  grouped.map((entry) => {
93
102
  const resolution = resolutions[entry.path] ?? "pending";
103
+ const name = nameOf(entry.path);
94
104
  return /* @__PURE__ */ jsxs("details", { className: "uix-diff-viewer__entry", "data-kind": entry.kind, open: entry.kind === "conflicted", children: [
95
105
  /* @__PURE__ */ jsxs("summary", { children: [
96
- /* @__PURE__ */ jsx("code", { children: entry.path }),
106
+ /* @__PURE__ */ jsx("span", { className: "uix-diff-viewer__name", children: name.node }),
97
107
  /* @__PURE__ */ jsx(StatusPill, { tone: resolution === "pending" ? "warning" : "success", children: resolutionLabel[resolution] })
98
108
  ] }),
99
- /* @__PURE__ */ jsx("div", { className: "uix-diff-viewer__versions", children: ["base", "current", "incoming"].map((version) => /* @__PURE__ */ jsxs("div", { role: "region", "aria-label": fillLabel(labels.valueFor, { version: labels[version], path: entry.path }), children: [
100
- /* @__PURE__ */ jsx("strong", { children: labels[version] }),
101
- /* @__PURE__ */ jsx("pre", { children: renderValue(entry[version], labels.notPresent) })
102
- ] }, version)) }),
103
- /* @__PURE__ */ jsxs("div", { className: "uix-diff-viewer__actions", role: "group", "aria-label": fillLabel(labels.resolve, { path: entry.path }), children: [
104
- /* @__PURE__ */ jsx("button", { type: "button", className: cx("uix-btn uix-btn--primary", buttonSize), "aria-label": fillLabel(labels.acceptIncomingFor, { path: entry.path }), "aria-pressed": resolution === "accept", onClick: () => resolve(entry.path, "accept"), children: labels.acceptIncoming }),
105
- /* @__PURE__ */ jsx("button", { type: "button", className: cx("uix-btn uix-btn--secondary", buttonSize), "aria-label": fillLabel(labels.keepCurrentFor, { path: entry.path }), "aria-pressed": resolution === "skip", onClick: () => resolve(entry.path, "skip"), children: labels.keepCurrent }),
106
- /* @__PURE__ */ jsx("button", { type: "button", className: cx("uix-btn uix-btn--ghost", buttonSize), "aria-label": fillLabel(labels.markPendingFor, { path: entry.path }), "aria-pressed": resolution === "pending", onClick: () => resolve(entry.path, "pending"), children: labels.markPending })
109
+ /* @__PURE__ */ jsx("div", { className: "uix-diff-viewer__versions", children: ["base", "current", "incoming"].map((version) => {
110
+ const origin = provenance?.(entry.path, version);
111
+ return /* @__PURE__ */ jsxs("div", { role: "region", "aria-label": fillLabel(labels.valueFor, { version: labels[version], path: name.text }), children: [
112
+ /* @__PURE__ */ jsx("strong", { children: labels[version] }),
113
+ renderValue ? /* @__PURE__ */ jsx("div", { className: "uix-diff-viewer__value", children: renderValue(entry[version], entry.path, version) ?? labels.notPresent }) : /* @__PURE__ */ jsx("pre", { children: jsonText(entry[version], labels.notPresent) }),
114
+ origin != null && origin !== false && /* @__PURE__ */ jsx("div", { className: "uix-diff-viewer__provenance", children: origin })
115
+ ] }, version);
116
+ }) }),
117
+ /* @__PURE__ */ jsxs("div", { className: "uix-diff-viewer__actions", role: "group", "aria-label": fillLabel(labels.resolve, { path: name.text }), children: [
118
+ /* @__PURE__ */ jsx("button", { type: "button", className: cx("uix-btn uix-btn--primary", buttonSize), "aria-label": fillLabel(labels.acceptIncomingFor, { path: name.text }), "aria-pressed": resolution === "accept", onClick: () => resolve(entry.path, "accept"), children: labels.acceptIncoming }),
119
+ /* @__PURE__ */ jsx("button", { type: "button", className: cx("uix-btn uix-btn--secondary", buttonSize), "aria-label": fillLabel(labels.keepCurrentFor, { path: name.text }), "aria-pressed": resolution === "skip", onClick: () => resolve(entry.path, "skip"), children: labels.keepCurrent }),
120
+ /* @__PURE__ */ jsx("button", { type: "button", className: cx("uix-btn uix-btn--ghost", buttonSize), "aria-label": fillLabel(labels.markPendingFor, { path: name.text }), "aria-pressed": resolution === "pending", onClick: () => resolve(entry.path, "pending"), children: labels.markPending })
107
121
  ] })
108
122
  ] }, entry.path);
109
123
  })
@@ -68,6 +68,7 @@ function useGuardedDialog({ open, onClose, dirty = false, pending = false, label
68
68
  setPrompt({ anchor: anchor ?? closeRef.current, returnTo: active instanceof HTMLElement ? active : null });
69
69
  }, [closeRef, setPrompt]);
70
70
  const dialogRef = useRef(null);
71
+ const lightDismissed = useRef(false);
71
72
  const backOut = useRef({ focus: null, forced: false });
72
73
  const keepEditing = useCallback(() => {
73
74
  const asked = latest.current.prompt;
@@ -79,6 +80,7 @@ function useGuardedDialog({ open, onClose, dirty = false, pending = false, label
79
80
  back?.focus();
80
81
  }, [closeRef, setPrompt]);
81
82
  const ref = useDialog(open, onClose, () => {
83
+ if (lightDismissed.current) return true;
82
84
  if (!guarded()) return false;
83
85
  if (backOut.current.forced) {
84
86
  backOut.current.forced = false;
@@ -104,7 +106,19 @@ function useGuardedDialog({ open, onClose, dirty = false, pending = false, label
104
106
  dialogRef.current = el;
105
107
  if (!el || !open) return;
106
108
  const ours = (event) => event.target?.closest?.("dialog") === el;
109
+ const onBeforeToggle = (event) => {
110
+ const kind = event.target.getAttribute?.("popover");
111
+ if (event.newState !== "closed" || kind == null || kind === "manual") return;
112
+ lightDismissed.current = true;
113
+ window.setTimeout(() => {
114
+ lightDismissed.current = false;
115
+ }, 0);
116
+ };
107
117
  const onCancel = (event) => {
118
+ if (lightDismissed.current) {
119
+ event.preventDefault();
120
+ return;
121
+ }
108
122
  if (!guarded()) return;
109
123
  event.preventDefault();
110
124
  if (latest.current.prompt) {
@@ -133,6 +147,7 @@ function useGuardedDialog({ open, onClose, dirty = false, pending = false, label
133
147
  setPrompt(null);
134
148
  };
135
149
  el.addEventListener("cancel", onCancel);
150
+ document.addEventListener("beforetoggle", onBeforeToggle, true);
136
151
  el.addEventListener("pointerdown", onPointerDownCapture, true);
137
152
  el.addEventListener("pointerup", onPointerUpCapture, true);
138
153
  el.addEventListener("pointercancel", forget, true);
@@ -141,6 +156,7 @@ function useGuardedDialog({ open, onClose, dirty = false, pending = false, label
141
156
  el.addEventListener("keydown", forget, true);
142
157
  return () => {
143
158
  el.removeEventListener("cancel", onCancel);
159
+ document.removeEventListener("beforetoggle", onBeforeToggle, true);
144
160
  el.removeEventListener("pointerdown", onPointerDownCapture, true);
145
161
  el.removeEventListener("pointerup", onPointerUpCapture, true);
146
162
  el.removeEventListener("pointercancel", forget, true);
@@ -0,0 +1,83 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes } from 'react';
3
+ import { DisplayPreferenceValues, ThemePreference } from '../display-preferences-model.js';
4
+
5
+ interface UseDisplayPreferencesOptions {
6
+ /** `localStorage` key. Default `DISPLAY_PREFERENCES_STORAGE_KEY`; pass the same key to `displayPreferencesScript`. */
7
+ storageKey?: string;
8
+ /** Your defaults for anything not stored yet. Pass the same to `displayPreferencesScript`. */
9
+ defaults?: Partial<DisplayPreferenceValues>;
10
+ /** Write the attributes on `<html>`. Default `true`; `false` for a preview that must not restyle the page. */
11
+ apply?: boolean;
12
+ }
13
+ interface UseDisplayPreferencesResult {
14
+ /** The person's choices (`system` where they left it to the system). */
15
+ preferences: DisplayPreferenceValues;
16
+ /** The theme in force: `system` resolved against `prefers-color-scheme`. */
17
+ resolvedTheme: 'light' | 'dark';
18
+ setPreference: <K extends keyof DisplayPreferenceValues>(key: K, value: DisplayPreferenceValues[K]) => void;
19
+ setPreferences: (next: DisplayPreferenceValues) => void;
20
+ }
21
+ /**
22
+ * The display preferences (HAR-1372): read from `localStorage`, kept in step with the system
23
+ * (`prefers-color-scheme`, `prefers-contrast`), with other tabs and with other users of the hook in
24
+ * this tab, and written to `<html>` as the attributes the tokens read. Changing one applies at once,
25
+ * with every transition suspended for that frame so nothing fades between themes.
26
+ * The server renders `defaults`; the stored choice is read before the first paint on the client
27
+ * (pair it with `displayPreferencesScript` in the head, so the page itself never flashes).
28
+ */
29
+ declare function useDisplayPreferences({ storageKey, defaults: defaultOverrides, apply }?: UseDisplayPreferencesOptions): UseDisplayPreferencesResult;
30
+ interface DisplayPreferencesLabels {
31
+ theme: string;
32
+ light: string;
33
+ dark: string;
34
+ system: string;
35
+ contrast: string;
36
+ contrastStandard: string;
37
+ contrastMore: string;
38
+ textScale: string;
39
+ textSmall: string;
40
+ textDefault: string;
41
+ textLarge: string;
42
+ textLarger: string;
43
+ motion: string;
44
+ motionSystem: string;
45
+ motionReduce: string;
46
+ density: string;
47
+ densityCompact: string;
48
+ densityStandard: string;
49
+ densityComfortable: string;
50
+ }
51
+ declare const DEFAULT_DISPLAY_PREFERENCES_LABELS: DisplayPreferencesLabels;
52
+ interface ThemeSwitcherProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
53
+ /** Controlled theme. Leave it out and the switcher keeps the preference itself (`useDisplayPreferences`). */
54
+ value?: ThemePreference;
55
+ onValueChange?: (value: ThemePreference) => void;
56
+ /** The storage key when uncontrolled. */
57
+ storageKey?: string;
58
+ /** `icons` shows the icons only (each keeps its word as its name); default shows icon and word. */
59
+ display?: 'labels' | 'icons';
60
+ labels?: Partial<DisplayPreferencesLabels>;
61
+ }
62
+ /**
63
+ * Light / dark / system (HAR-1372), a radio group over `Segmented`. Uncontrolled, it reads and
64
+ * writes the display preferences itself; controlled, it only reports.
65
+ */
66
+ declare function ThemeSwitcher({ value, onValueChange, storageKey, display, labels: labelOverrides, className, ...props }: ThemeSwitcherProps): react.JSX.Element;
67
+ interface DisplayPreferencesProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
68
+ /** Controlled values. Leave out to keep them in `localStorage` (`useDisplayPreferences`). */
69
+ value?: DisplayPreferenceValues;
70
+ onValueChange?: (value: DisplayPreferenceValues) => void;
71
+ storageKey?: string;
72
+ /** Show only some of the five settings, in this order. Default all. */
73
+ settings?: readonly (keyof DisplayPreferenceValues)[];
74
+ labels?: Partial<DisplayPreferencesLabels>;
75
+ }
76
+ /**
77
+ * The five display settings (HAR-1372; MOTUS C-4, TENSOR A7): theme, contrast, text size, motion
78
+ * and density, each a labelled radio group. Put it in a preferences page or an account menu's
79
+ * dialog. Changes apply at once and are remembered.
80
+ */
81
+ declare function DisplayPreferences({ value, onValueChange, storageKey, settings, labels: labelOverrides, className, ...props }: DisplayPreferencesProps): react.JSX.Element;
82
+
83
+ export { DEFAULT_DISPLAY_PREFERENCES_LABELS, DisplayPreferences, type DisplayPreferencesLabels, type DisplayPreferencesProps, ThemeSwitcher, type ThemeSwitcherProps, type UseDisplayPreferencesOptions, type UseDisplayPreferencesResult, useDisplayPreferences };
@@ -0,0 +1,185 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
4
+ import { cx } from "../cx.js";
5
+ import { useUixLabels } from "../labels-context.js";
6
+ import { MonitorIcon, MoonIcon, SunIcon } from "../icons/components.js";
7
+ import { Segmented, SegmentedOption } from "./Segmented.js";
8
+ import {
9
+ DEFAULT_DISPLAY_PREFERENCES,
10
+ DISPLAY_PREFERENCES_STORAGE_KEY,
11
+ applyDisplayAttributes,
12
+ parseDisplayPreferences,
13
+ readSystemDisplayPreferences,
14
+ resolveDisplayAttributes
15
+ } from "../display-preferences-model.js";
16
+ const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
17
+ const CHANGE_EVENT = "uixdisplaypreferencechange";
18
+ function useDisplayPreferences({ storageKey = DISPLAY_PREFERENCES_STORAGE_KEY, defaults: defaultOverrides, apply = true } = {}) {
19
+ const defaults = { ...DEFAULT_DISPLAY_PREFERENCES, ...defaultOverrides };
20
+ const defaultsKey = JSON.stringify(defaults);
21
+ const [preferences, setState] = useState(defaults);
22
+ const [system, setSystem] = useState({ dark: false, moreContrast: false });
23
+ const [loaded, setLoaded] = useState(false);
24
+ const read = useCallback(() => {
25
+ let raw = null;
26
+ try {
27
+ raw = window.localStorage.getItem(storageKey);
28
+ } catch {
29
+ }
30
+ return parseDisplayPreferences(raw, JSON.parse(defaultsKey));
31
+ }, [storageKey, defaultsKey]);
32
+ const latest = useRef(preferences);
33
+ latest.current = preferences;
34
+ useIsomorphicLayoutEffect(() => {
35
+ setState(read());
36
+ setSystem(readSystemDisplayPreferences());
37
+ setLoaded(true);
38
+ }, [read]);
39
+ useEffect(() => {
40
+ const queries = typeof matchMedia === "function" ? [matchMedia("(prefers-color-scheme: dark)"), matchMedia("(prefers-contrast: more)")] : [];
41
+ const onSystem = () => setSystem(readSystemDisplayPreferences());
42
+ const onStorage = (event) => {
43
+ if (event.key === storageKey) setState(read());
44
+ };
45
+ const onChange = (event) => {
46
+ const detail = event.detail;
47
+ if (detail?.storageKey === storageKey) setState(detail.value);
48
+ };
49
+ for (const q of queries) q.addEventListener("change", onSystem);
50
+ window.addEventListener("storage", onStorage);
51
+ window.addEventListener(CHANGE_EVENT, onChange);
52
+ return () => {
53
+ for (const q of queries) q.removeEventListener("change", onSystem);
54
+ window.removeEventListener("storage", onStorage);
55
+ window.removeEventListener(CHANGE_EVENT, onChange);
56
+ };
57
+ }, [storageKey, read]);
58
+ useIsomorphicLayoutEffect(() => {
59
+ if (!apply || !loaded) return;
60
+ const root = document.documentElement;
61
+ const next = resolveDisplayAttributes(preferences, system);
62
+ const names = ["data-theme", "data-contrast", "data-text-scale", "data-motion", "data-density"];
63
+ if (names.every((name) => (next[name] ?? null) === root.getAttribute(name))) return void 0;
64
+ root.setAttribute("data-uix-switching", "");
65
+ applyDisplayAttributes(root, next);
66
+ const frame = requestAnimationFrame(() => requestAnimationFrame(() => root.removeAttribute("data-uix-switching")));
67
+ return () => {
68
+ cancelAnimationFrame(frame);
69
+ root.removeAttribute("data-uix-switching");
70
+ };
71
+ }, [apply, loaded, preferences, system]);
72
+ const setPreferences = useCallback((next) => {
73
+ setState(next);
74
+ try {
75
+ window.localStorage.setItem(storageKey, JSON.stringify(next));
76
+ } catch {
77
+ }
78
+ window.dispatchEvent(new CustomEvent(CHANGE_EVENT, { detail: { storageKey, value: next } }));
79
+ }, [storageKey]);
80
+ const setPreference = useCallback((key, value) => {
81
+ setPreferences({ ...latest.current, [key]: value });
82
+ }, [setPreferences]);
83
+ return { preferences, resolvedTheme: resolveDisplayAttributes(preferences, system)["data-theme"], setPreference, setPreferences };
84
+ }
85
+ const DEFAULT_DISPLAY_PREFERENCES_LABELS = {
86
+ theme: "Theme",
87
+ light: "Light",
88
+ dark: "Dark",
89
+ system: "System",
90
+ contrast: "Contrast",
91
+ contrastStandard: "Standard",
92
+ contrastMore: "More",
93
+ textScale: "Text size",
94
+ textSmall: "Small",
95
+ textDefault: "Default",
96
+ textLarge: "Large",
97
+ textLarger: "Larger",
98
+ motion: "Motion",
99
+ motionSystem: "System",
100
+ motionReduce: "Reduce",
101
+ density: "Density",
102
+ densityCompact: "Compact",
103
+ densityStandard: "Standard",
104
+ densityComfortable: "Comfortable"
105
+ };
106
+ const useDisplayLabels = (overrides) => {
107
+ const uixLabels = useUixLabels();
108
+ return { ...DEFAULT_DISPLAY_PREFERENCES_LABELS, ...uixLabels.displayPreferences, ...overrides };
109
+ };
110
+ function ThemeOptions({ labels, iconsOnly }) {
111
+ const option = (value, icon, word) => /* @__PURE__ */ jsxs(SegmentedOption, { value, "aria-label": iconsOnly ? word : void 0, children: [
112
+ icon,
113
+ iconsOnly ? null : /* @__PURE__ */ jsx("span", { children: word })
114
+ ] });
115
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
116
+ option("light", /* @__PURE__ */ jsx(SunIcon, { size: "sm" }), labels.light),
117
+ option("dark", /* @__PURE__ */ jsx(MoonIcon, { size: "sm" }), labels.dark),
118
+ option("system", /* @__PURE__ */ jsx(MonitorIcon, { size: "sm" }), labels.system)
119
+ ] });
120
+ }
121
+ function ThemeSwitcher({ value, onValueChange, storageKey, display = "labels", labels: labelOverrides, className, ...props }) {
122
+ const labels = useDisplayLabels(labelOverrides);
123
+ const own = useDisplayPreferences({ storageKey, apply: value === void 0 });
124
+ const current = value ?? own.preferences.theme;
125
+ return /* @__PURE__ */ jsx(
126
+ Segmented,
127
+ {
128
+ selection: "radio",
129
+ "aria-label": labels.theme,
130
+ className: cx("uix-theme-switcher", className),
131
+ value: current,
132
+ onChange: (next) => {
133
+ if (value === void 0) own.setPreference("theme", next);
134
+ onValueChange?.(next);
135
+ },
136
+ ...props,
137
+ children: /* @__PURE__ */ jsx(ThemeOptions, { labels, iconsOnly: display === "icons" })
138
+ }
139
+ );
140
+ }
141
+ function DisplayPreferences({ value, onValueChange, storageKey, settings = ["theme", "contrast", "textScale", "motion", "density"], labels: labelOverrides, className, ...props }) {
142
+ const labels = useDisplayLabels(labelOverrides);
143
+ const own = useDisplayPreferences({ storageKey, apply: value === void 0 });
144
+ const current = value ?? own.preferences;
145
+ const id = useId();
146
+ const change = (key, next) => {
147
+ const all = { ...current, [key]: next };
148
+ if (value === void 0) own.setPreference(key, next);
149
+ onValueChange?.(all);
150
+ };
151
+ const row = (key, title, options) => /* @__PURE__ */ jsxs("div", { className: "uix-display-prefs__row", children: [
152
+ /* @__PURE__ */ jsx("span", { className: "uix-display-prefs__label", id: `${id}-${key}`, children: title }),
153
+ /* @__PURE__ */ jsx(Segmented, { selection: "radio", "aria-labelledby": `${id}-${key}`, value: current[key], onChange: (next) => change(key, next), children: options })
154
+ ] }, key);
155
+ const rows = {
156
+ theme: () => row("theme", labels.theme, /* @__PURE__ */ jsx(ThemeOptions, { labels, iconsOnly: false })),
157
+ contrast: () => row("contrast", labels.contrast, /* @__PURE__ */ jsxs(Fragment, { children: [
158
+ /* @__PURE__ */ jsx(SegmentedOption, { value: "standard", children: labels.contrastStandard }),
159
+ /* @__PURE__ */ jsx(SegmentedOption, { value: "more", children: labels.contrastMore }),
160
+ /* @__PURE__ */ jsx(SegmentedOption, { value: "system", children: labels.system })
161
+ ] })),
162
+ textScale: () => row("textScale", labels.textScale, /* @__PURE__ */ jsxs(Fragment, { children: [
163
+ /* @__PURE__ */ jsx(SegmentedOption, { value: "sm", children: labels.textSmall }),
164
+ /* @__PURE__ */ jsx(SegmentedOption, { value: "md", children: labels.textDefault }),
165
+ /* @__PURE__ */ jsx(SegmentedOption, { value: "lg", children: labels.textLarge }),
166
+ /* @__PURE__ */ jsx(SegmentedOption, { value: "xl", children: labels.textLarger })
167
+ ] })),
168
+ motion: () => row("motion", labels.motion, /* @__PURE__ */ jsxs(Fragment, { children: [
169
+ /* @__PURE__ */ jsx(SegmentedOption, { value: "system", children: labels.motionSystem }),
170
+ /* @__PURE__ */ jsx(SegmentedOption, { value: "reduce", children: labels.motionReduce })
171
+ ] })),
172
+ density: () => row("density", labels.density, /* @__PURE__ */ jsxs(Fragment, { children: [
173
+ /* @__PURE__ */ jsx(SegmentedOption, { value: "compact", children: labels.densityCompact }),
174
+ /* @__PURE__ */ jsx(SegmentedOption, { value: "standard", children: labels.densityStandard }),
175
+ /* @__PURE__ */ jsx(SegmentedOption, { value: "comfortable", children: labels.densityComfortable })
176
+ ] }))
177
+ };
178
+ return /* @__PURE__ */ jsx("div", { className: cx("uix-display-prefs", className), ...props, children: settings.map((key) => rows[key]()) });
179
+ }
180
+ export {
181
+ DEFAULT_DISPLAY_PREFERENCES_LABELS,
182
+ DisplayPreferences,
183
+ ThemeSwitcher,
184
+ useDisplayPreferences
185
+ };
@@ -34,9 +34,16 @@ interface SectionHeadProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'>
34
34
  helpLabel?: string;
35
35
  /** Right-aligned slot — a `.uix-section-link` anchor, a button, a pill. */
36
36
  action?: ReactNode;
37
+ /**
38
+ * `compact` is the in-card section preset (HAR-1815): a 15 px / 1.3 / 600 title in place of
39
+ * the 24 px editorial h2. Default `default`.
40
+ */
41
+ size?: 'default' | 'compact';
42
+ /** Heading level of the title. Default `h2`; an in-card section is often an `h3`. */
43
+ as?: 'h2' | 'h3';
37
44
  }
38
45
  /** Section head — title row (+ optional `help` ?) with an optional trailing action (`.uix-section-head`). */
39
- declare function SectionHead({ title, titleId, help, helpLabel, action, className, ...props }: SectionHeadProps): react.JSX.Element;
46
+ declare function SectionHead({ title, titleId, help, helpLabel, action, size, as: Heading, className, ...props }: SectionHeadProps): react.JSX.Element;
40
47
  interface NoticeQueueProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
41
48
  /** Leading icon slot. */
42
49
  icon?: ReactNode;
@@ -173,9 +180,22 @@ interface EventRowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
173
180
  day: ReactNode;
174
181
  title: ReactNode;
175
182
  meta?: ReactNode;
183
+ /** Weekday above the month in the date tile, e.g. "Sri" (HAR-1370). */
184
+ weekday?: ReactNode;
185
+ /** Year under the day in the date tile, for a list that crosses a year (HAR-1370). */
186
+ year?: ReactNode;
187
+ /** Start time at the foot of the date tile, e.g. "18:00" (HAR-1370). */
188
+ time?: ReactNode;
189
+ /**
190
+ * The machine-readable date (and time) of the tile, e.g. `"2026-10-22T18:00+02:00"`: the tile
191
+ * becomes a `<time>` element, so the date is not only words.
192
+ */
193
+ dateTime?: string;
194
+ /** A thumbnail at the end of the row (an `<img>`), drawn 4:3 and cropped to fill (HAR-1370). */
195
+ media?: ReactNode;
176
196
  }
177
- /** Event row — date block + title/meta (`.uix-event-row`). */
178
- declare function EventRow({ month, day, title, meta, className, ...props }: EventRowProps): react.JSX.Element;
197
+ /** Event row — date block + title/meta (`.uix-event-row`), with an optional thumbnail. */
198
+ declare function EventRow({ month, day, title, meta, weekday, year, time, dateTime, media, className, ...props }: EventRowProps): react.JSX.Element;
179
199
  interface StatusRowProps extends HTMLAttributes<HTMLDivElement> {
180
200
  /** Leading indicator slot (e.g. a `.uix-dot`). */
181
201
  indicator?: ReactNode;
@@ -1,4 +1,4 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { cx } from "../cx.js";
3
3
  import { InfoTip } from "./InfoTip.js";
4
4
  import { hasHelp, helpLabelFor } from "../info-tip-model.js";
@@ -28,9 +28,9 @@ function PageIntro({
28
28
  children
29
29
  ] });
30
30
  }
31
- function SectionHead({ title, titleId, help, helpLabel, action, className, ...props }) {
32
- const heading = /* @__PURE__ */ jsx("h2", { className: "uix-section-title", id: titleId, children: title });
33
- return /* @__PURE__ */ jsxs("div", { className: cx("uix-section-head", className), ...props, children: [
31
+ function SectionHead({ title, titleId, help, helpLabel, action, size = "default", as: Heading = "h2", className, ...props }) {
32
+ const heading = /* @__PURE__ */ jsx(Heading, { className: "uix-section-title", id: titleId, children: title });
33
+ return /* @__PURE__ */ jsxs("div", { className: cx("uix-section-head", size === "compact" && "uix-section-head--compact", className), ...props, children: [
34
34
  hasHelp(help) ? /* @__PURE__ */ jsxs("div", { className: "uix-section-head__title-row", children: [
35
35
  heading,
36
36
  /* @__PURE__ */ jsx(InfoTip, { content: help, label: helpLabel ?? helpLabelFor(title) })
@@ -153,16 +153,21 @@ function StatLine({ items, children, className, ...props }) {
153
153
  children
154
154
  ] });
155
155
  }
156
- function EventRow({ month, day, title, meta, className, ...props }) {
157
- return /* @__PURE__ */ jsxs("div", { className: cx("uix-event-row", className), ...props, children: [
158
- /* @__PURE__ */ jsxs("div", { className: "uix-event-date", children: [
159
- /* @__PURE__ */ jsx("span", { className: "uix-event-date__month", children: month }),
160
- /* @__PURE__ */ jsx("span", { className: "uix-event-date__day", children: day })
161
- ] }),
156
+ function EventRow({ month, day, title, meta, weekday, year, time, dateTime, media, className, ...props }) {
157
+ const tile = /* @__PURE__ */ jsxs(Fragment, { children: [
158
+ weekday != null && /* @__PURE__ */ jsx("span", { className: "uix-event-date__weekday", children: weekday }),
159
+ /* @__PURE__ */ jsx("span", { className: "uix-event-date__month", children: month }),
160
+ /* @__PURE__ */ jsx("span", { className: "uix-event-date__day", children: day }),
161
+ year != null && /* @__PURE__ */ jsx("span", { className: "uix-event-date__year", children: year }),
162
+ time != null && /* @__PURE__ */ jsx("span", { className: "uix-event-date__time", children: time })
163
+ ] });
164
+ return /* @__PURE__ */ jsxs("div", { className: cx("uix-event-row", media != null && "uix-event-row--media", className), ...props, children: [
165
+ dateTime ? /* @__PURE__ */ jsx("time", { className: "uix-event-date", dateTime, children: tile }) : /* @__PURE__ */ jsx("div", { className: "uix-event-date", children: tile }),
162
166
  /* @__PURE__ */ jsxs("div", { children: [
163
167
  /* @__PURE__ */ jsx("p", { className: "uix-event-row__title", children: title }),
164
168
  meta != null && /* @__PURE__ */ jsx("p", { className: "uix-event-row__meta", children: meta })
165
- ] })
169
+ ] }),
170
+ media != null && /* @__PURE__ */ jsx("div", { className: "uix-event-row__media", children: media })
166
171
  ] });
167
172
  }
168
173
  function StatusRow({ indicator, name, meta, className, ...props }) {
@@ -24,12 +24,23 @@ interface EntityPickerLabels {
24
24
  more: string;
25
25
  /** Polite count when there are more matches than shown. `{count}` is the number shown. */
26
26
  resultsMore: string;
27
+ /**
28
+ * Shown, with no Retry, when the search failed for good (HAR-1834): `onSearch` rejected with
29
+ * `{ retryable: false }` (and no `message`), or `isRetryable` said no. E.g. no permission.
30
+ */
31
+ forbidden: string;
27
32
  /** Shown in the open list while the query is shorter than `minQueryLength`. `{count}`. */
28
33
  tooShort: string;
29
34
  /** The same when `minQueryLength` is 1. */
30
35
  typeToSearch: string;
31
36
  }
32
37
  declare const DEFAULT_ENTITY_PICKER_LABELS: EntityPickerLabels;
38
+ /** What `onSearch` may reject with when trying again cannot help (HAR-1834). */
39
+ interface EntityPickerSearchError {
40
+ retryable: false;
41
+ /** Shown instead of the `forbidden` words. */
42
+ message?: string;
43
+ }
33
44
  /** What `onSearch` may resolve to when it knows its list was cut off. */
34
45
  interface EntityPickerSearchResult {
35
46
  options: readonly SearchSuggestOption[];
@@ -43,9 +54,15 @@ interface EntityPickerProps {
43
54
  /**
44
55
  * Finds records for the query; a rejection shows the error state with a Retry row. Resolve
45
56
  * with the options, or with `{ options, hasMore: true }` when the list was cut off (a page of
46
- * 20 out of many): the list then says that more match.
57
+ * 20 out of many): the list then says that more match. Reject with
58
+ * `{ retryable: false, message? }` (an {@link EntityPickerSearchError}) when trying again
59
+ * cannot help, e.g. no permission: the error shows without Retry.
47
60
  */
48
61
  onSearch: (query: string) => Promise<readonly SearchSuggestOption[] | EntityPickerSearchResult>;
62
+ /** Decide from the rejection whether Retry is offered. Default: not for `{ retryable: false }`. */
63
+ isRetryable?: (error: unknown) => boolean;
64
+ /** Ids of the visible label that names the search field (instead of `label`). */
65
+ 'aria-labelledby'?: string;
49
66
  /** The field's name (the visible label usually comes from a surrounding `Field`). */
50
67
  label: string;
51
68
  /** Id of the control, for `Field htmlFor`/`label for`. */
@@ -76,6 +93,6 @@ interface EntityPickerProps {
76
93
  * clear ×; choosing again or typing opens a `SearchSuggest` list with loading, empty and error
77
94
  * states, and Retry is a list row, so the keyboard reaches it. Stale results are ignored.
78
95
  */
79
- declare function EntityPicker({ value, onValueChange, onSearch, label, id, name, placeholder, minQueryLength, delay, disabled, invalid, 'aria-describedby': describedBy, renderValue, labels: labelOverrides, className, }: EntityPickerProps): react.JSX.Element;
96
+ declare function EntityPicker({ value, onValueChange, onSearch, isRetryable, label, id, name, placeholder, minQueryLength, delay, disabled, invalid, 'aria-describedby': describedBy, 'aria-labelledby': labelledBy, renderValue, labels: labelOverrides, className, }: EntityPickerProps): react.JSX.Element;
80
97
 
81
- export { DEFAULT_ENTITY_PICKER_LABELS, EntityPicker, type EntityPickerLabels, type EntityPickerProps, type EntityPickerSearchResult };
98
+ export { DEFAULT_ENTITY_PICKER_LABELS, EntityPicker, type EntityPickerLabels, type EntityPickerProps, type EntityPickerSearchError, type EntityPickerSearchResult };
@@ -18,12 +18,14 @@ const DEFAULT_ENTITY_PICKER_LABELS = {
18
18
  more: "More matches. Keep typing to narrow the search.",
19
19
  resultsMore: "{count} results shown, more match. Keep typing to narrow the search.",
20
20
  tooShort: "Type at least {count} characters.",
21
- typeToSearch: "Type to search."
21
+ typeToSearch: "Type to search.",
22
+ forbidden: "You do not have access to search these records."
22
23
  };
23
24
  function EntityPicker({
24
25
  value,
25
26
  onValueChange,
26
27
  onSearch,
28
+ isRetryable,
27
29
  label,
28
30
  id,
29
31
  name,
@@ -33,6 +35,7 @@ function EntityPicker({
33
35
  disabled,
34
36
  invalid,
35
37
  "aria-describedby": describedBy,
38
+ "aria-labelledby": labelledBy,
36
39
  renderValue,
37
40
  labels: labelOverrides,
38
41
  className
@@ -43,6 +46,7 @@ function EntityPicker({
43
46
  const [results, setResults] = useState([]);
44
47
  const [hasMore, setHasMore] = useState(false);
45
48
  const [status, setStatus] = useState("idle");
49
+ const [fatal, setFatal] = useState(null);
46
50
  const [attempt, setAttempt] = useState(0);
47
51
  const latest = useRef(0);
48
52
  const inputRef = useRef(null);
@@ -53,7 +57,11 @@ function EntityPicker({
53
57
  else setEditing(true);
54
58
  }, [value?.id]);
55
59
  useEffect(() => {
56
- if (!editing || disabled) return;
60
+ const run = ++latest.current;
61
+ if (!editing || disabled) {
62
+ setStatus((s) => s === "loading" ? "idle" : s);
63
+ return;
64
+ }
57
65
  const q = query.trim();
58
66
  if (q.length < minQueryLength) {
59
67
  setStatus("idle");
@@ -61,23 +69,26 @@ function EntityPicker({
61
69
  setHasMore(false);
62
70
  return;
63
71
  }
64
- const run = ++latest.current;
72
+ setStatus("loading");
65
73
  const timer = window.setTimeout(() => {
66
- setStatus("loading");
67
74
  onSearch(q).then(
68
75
  (found) => {
69
76
  if (run !== latest.current) return;
70
77
  const list = "options" in found ? found.options : found;
71
78
  setResults(list);
72
79
  setHasMore("options" in found && !!found.hasMore && list.length > 0);
80
+ setFatal(null);
73
81
  setStatus("done");
74
82
  },
75
- () => {
76
- if (run === latest.current) {
77
- setResults([]);
78
- setHasMore(false);
79
- setStatus("error");
80
- }
83
+ (error) => {
84
+ if (run !== latest.current) return;
85
+ const declared = typeof error === "object" && error !== null && error.retryable === false;
86
+ const retryable = isRetryable ? isRetryable(error) : !declared;
87
+ const message = declared ? error.message : void 0;
88
+ setFatal(retryable ? null : typeof message === "string" && message ? message : labels.forbidden);
89
+ setResults([]);
90
+ setHasMore(false);
91
+ setStatus("error");
81
92
  }
82
93
  );
83
94
  }, delay);
@@ -100,6 +111,11 @@ function EntityPicker({
100
111
  };
101
112
  const clear = () => {
102
113
  onValueChange(null);
114
+ setQuery("");
115
+ setResults([]);
116
+ setHasMore(false);
117
+ setStatus("idle");
118
+ setFatal(null);
103
119
  returnFocus.current = true;
104
120
  setEditing(true);
105
121
  };
@@ -154,17 +170,20 @@ function EntityPicker({
154
170
  inputRef,
155
171
  value: query,
156
172
  onValueChange: setQuery,
157
- options: status === "done" ? results : [],
173
+ options: status === "done" || status === "loading" ? results : [],
158
174
  onSelect: (_, option) => choose(option),
159
175
  label: `${label}. ${labels.search}`,
176
+ inputLabelledBy: labelledBy,
177
+ disabled,
178
+ invalid,
160
179
  placeholder: placeholder ?? labels.none,
161
180
  loading: status === "loading",
162
181
  loadingLabel: labels.loading,
163
182
  empty: tooShort ? void 0 : labels.empty,
164
183
  idle: tooShort ? fillLabel(minQueryLength === 1 ? labels.typeToSearch : labels.tooShort, { count: minQueryLength }) : void 0,
165
184
  note: status === "done" && hasMore ? labels.more : void 0,
166
- error: status === "error" ? labels.error : void 0,
167
- footer: status === "error" ? { label: labels.retry, onSelect: () => setAttempt((n) => n + 1) } : void 0,
185
+ error: status === "error" ? fatal ?? labels.error : void 0,
186
+ footer: status === "error" && fatal == null ? { label: labels.retry, onSelect: () => setAttempt((n) => n + 1) } : void 0,
168
187
  status: status === "done" ? fillLabel(hasMore ? labels.resultsMore : labels.results, { count: results.length }) : void 0,
169
188
  strategy: "fixed"
170
189
  }