@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
@@ -5,6 +5,7 @@ import { cx } from "../cx.js";
5
5
  import { useUixLabels } from "../labels-context.js";
6
6
  import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
7
7
  import { searchSegments } from "../search-suggest-model.js";
8
+ import { useDelayedPending } from "../hooks/useDelayedPending.js";
8
9
  import { Spinner } from "./Spinner.js";
9
10
  const SearchIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
10
11
  /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
@@ -23,6 +24,7 @@ function SearchSuggest({
23
24
  onValueChange,
24
25
  options,
25
26
  onSelect,
27
+ freeText = false,
26
28
  label,
27
29
  placeholder,
28
30
  highlight,
@@ -47,6 +49,9 @@ function SearchSuggest({
47
49
  name,
48
50
  inputId,
49
51
  inputDescribedBy,
52
+ inputLabelledBy,
53
+ disabled = false,
54
+ invalid = false,
50
55
  className,
51
56
  onBlur,
52
57
  ...props
@@ -67,15 +72,27 @@ function SearchSuggest({
67
72
  };
68
73
  const [focused, setFocused] = useState(false);
69
74
  const [activeState, setActive] = useState(-1);
75
+ const showSpinner = useDelayedPending(loading) && !disabled;
76
+ const [labelled, setLabelled] = useState(false);
77
+ useEffect(() => {
78
+ setLabelled((fieldRef.current?.labels?.length ?? 0) > 0);
79
+ });
70
80
  const itemCount = options.length + (footer ? 1 : 0);
71
81
  const active = activeState < itemCount ? activeState : -1;
72
82
  const optionKey = options.map((option) => option.id).join("|");
73
83
  useEffect(() => setActive(-1), [optionKey]);
74
84
  const hasText = value.trim().length > 0;
75
- const showEmpty = hasText && !loading && !error && options.length === 0 && empty != null;
76
- const showLoading = loading && options.length === 0;
85
+ const showLoading = showSpinner && loading && options.length === 0;
86
+ const current = { empty: hasText && error == null && empty != null, error: error ?? null };
87
+ const settled = useRef({ empty: false, error: null });
88
+ useEffect(() => {
89
+ if (!loading) settled.current = current;
90
+ });
91
+ const state = options.length > 0 || showLoading ? null : loading ? settled.current : current;
92
+ const shownError = state?.error ?? null;
93
+ const showEmpty = !!state?.empty && shownError == null;
77
94
  const showIdle = idle != null && itemCount === 0 && !loading && error == null && !showEmpty;
78
- const visible = open && (itemCount > 0 || showLoading || showEmpty || showIdle || error != null);
95
+ const visible = open && !disabled && (itemCount > 0 || showLoading || showEmpty || showIdle || shownError != null);
79
96
  const optionId = (index) => `${id}-option-${index}`;
80
97
  useEffect(() => {
81
98
  if (!visible || active < 0) return;
@@ -84,8 +101,10 @@ function SearchSuggest({
84
101
  }, [active, visible]);
85
102
  const choose = (index) => {
86
103
  const option = options[index];
87
- if (option) onSelect(option.id, option);
88
- else if (footer && index === options.length) footer.onSelect();
104
+ if (option) {
105
+ if (freeText && option.title !== value) onValueChange(option.title);
106
+ onSelect?.(option.id, option);
107
+ } else if (footer && index === options.length) footer.onSelect();
89
108
  else return;
90
109
  setOpen(false);
91
110
  setActive(-1);
@@ -107,9 +126,9 @@ function SearchSuggest({
107
126
  return;
108
127
  }
109
128
  setActive((stored) => {
110
- const current = stored < itemCount ? stored : -1;
111
- if (current < 0) return down ? 0 : itemCount - 1;
112
- return (current + (down ? 1 : -1) + itemCount) % itemCount;
129
+ const current2 = stored < itemCount ? stored : -1;
130
+ if (current2 < 0) return down ? 0 : itemCount - 1;
131
+ return (current2 + (down ? 1 : -1) + itemCount) % itemCount;
113
132
  });
114
133
  return;
115
134
  }
@@ -121,11 +140,20 @@ function SearchSuggest({
121
140
  }
122
141
  return;
123
142
  case "Enter":
143
+ if (loading && !freeText) {
144
+ event.preventDefault();
145
+ return;
146
+ }
124
147
  if (visible && active >= 0) {
125
148
  event.preventDefault();
126
149
  choose(active);
127
150
  return;
128
151
  }
152
+ if (freeText) {
153
+ event.preventDefault();
154
+ if (open) setOpen(false);
155
+ return;
156
+ }
129
157
  if (visible && hasText && options.length) {
130
158
  event.preventDefault();
131
159
  choose(0);
@@ -137,7 +165,7 @@ function SearchSuggest({
137
165
  event.stopPropagation();
138
166
  setOpen(false);
139
167
  setActive(-1);
140
- } else if (value) {
168
+ } else if (value && !freeText) {
141
169
  event.preventDefault();
142
170
  event.stopPropagation();
143
171
  onValueChange("");
@@ -179,13 +207,13 @@ function SearchSuggest({
179
207
  "div",
180
208
  {
181
209
  ref: rootRef,
182
- className: cx("uix-search-suggest", size === "lg" && "uix-search-suggest--lg", className),
210
+ className: cx("uix-search-suggest", size === "lg" && "uix-search-suggest--lg", freeText && "uix-search-suggest--free-text", className),
183
211
  "data-open": visible || void 0,
184
212
  onBlur: handleBlur,
185
213
  ...props,
186
214
  children: [
187
215
  /* @__PURE__ */ jsxs("div", { ref: fieldBoxRef, className: "uix-search-suggest__field", children: [
188
- /* @__PURE__ */ jsx("span", { className: "uix-search-suggest__lead", "aria-hidden": "true", children: /* @__PURE__ */ jsx(SearchIcon, {}) }),
216
+ !freeText && /* @__PURE__ */ jsx("span", { className: "uix-search-suggest__lead", "aria-hidden": "true", children: /* @__PURE__ */ jsx(SearchIcon, {}) }),
189
217
  /* @__PURE__ */ jsx(
190
218
  "input",
191
219
  {
@@ -195,7 +223,10 @@ function SearchSuggest({
195
223
  className: "uix-input uix-search-suggest__input",
196
224
  type: "text",
197
225
  role: "combobox",
198
- "aria-label": label,
226
+ "aria-label": inputLabelledBy || labelled ? void 0 : label,
227
+ "aria-labelledby": inputLabelledBy,
228
+ "aria-invalid": invalid || void 0,
229
+ disabled,
199
230
  "aria-autocomplete": "list",
200
231
  "aria-expanded": visible,
201
232
  "aria-controls": listboxId,
@@ -203,7 +234,7 @@ function SearchSuggest({
203
234
  "aria-busy": loading || void 0,
204
235
  autoComplete: "off",
205
236
  spellCheck: false,
206
- enterKeyHint: "search",
237
+ enterKeyHint: freeText ? "done" : "search",
207
238
  name,
208
239
  value,
209
240
  placeholder,
@@ -222,9 +253,9 @@ function SearchSuggest({
222
253
  }
223
254
  ),
224
255
  /* @__PURE__ */ jsxs("span", { className: "uix-search-suggest__trail", children: [
225
- loading && /* @__PURE__ */ jsx(Spinner, { className: "uix-search-suggest__spinner", label: loadingLabel }),
226
- !loading && value && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-search-suggest__clear", "aria-label": clearLabel, onMouseDown: keepFocus, onClick: clear, children: /* @__PURE__ */ jsx(ClearIcon, {}) }),
227
- !loading && !value && !focused && shortcutHint && /* @__PURE__ */ jsx("kbd", { className: "uix-kbd", "aria-hidden": "true", children: shortcutHint })
256
+ showSpinner && /* @__PURE__ */ jsx(Spinner, { className: "uix-search-suggest__spinner", label: loadingLabel }),
257
+ !showSpinner && value && !disabled && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-search-suggest__clear", "aria-label": clearLabel, onMouseDown: keepFocus, onClick: clear, children: /* @__PURE__ */ jsx(ClearIcon, {}) }),
258
+ !showSpinner && !value && !focused && shortcutHint && /* @__PURE__ */ jsx("kbd", { className: "uix-kbd", "aria-hidden": "true", children: shortcutHint })
228
259
  ] })
229
260
  ] }),
230
261
  /* @__PURE__ */ jsxs("div", { ref: popupRef, className: "uix-search-suggest__popup", "data-strategy": floating ? "fixed" : void 0, hidden: !visible, children: [
@@ -296,7 +327,7 @@ function SearchSuggest({
296
327
  /* @__PURE__ */ jsx("span", { className: "uix-spinner" }),
297
328
  loadingLabel && /* @__PURE__ */ jsx("span", { children: loadingLabel })
298
329
  ] }),
299
- error != null && options.length === 0 && !loading && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: error }),
330
+ shownError != null && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: shownError }),
300
331
  showEmpty && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: empty }),
301
332
  showIdle && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: idle }),
302
333
  note != null && options.length > 0 && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__note", children: note })
@@ -1,5 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { HTMLAttributes, ReactNode, ButtonHTMLAttributes } from 'react';
3
+ import { UixRenderLink } from '../link.js';
3
4
 
4
5
  interface SegmentedProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
5
6
  /** The currently-selected option value. */
@@ -12,8 +13,15 @@ interface SegmentedProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'
12
13
  * toggle bar. `'radio'`: `role="radiogroup"` of `role="radio"` with `aria-checked`, for a
13
14
  * setting with exactly one value (theme, density, language). Name the group with
14
15
  * `aria-label` or `aria-labelledby` in either mode. The keyboard model is the same in both.
16
+ *
17
+ * `'link'` (HAR-1380): navigation, not a choice, such as a language switcher where each
18
+ * option is a URL. A `<nav>` (named by your `aria-label`) around a list of links, each
19
+ * option needs an `href`, and the one whose `value` is `value` carries `aria-current="page"`.
20
+ * The same pill look; every link is its own tab stop and Enter follows it, like any nav.
15
21
  */
16
- selection?: 'toggle' | 'radio';
22
+ selection?: 'toggle' | 'radio' | 'link';
23
+ /** `selection="link"`: renders each option's link with your router. Default: `<a>`. */
24
+ renderLink?: UixRenderLink;
17
25
  children?: ReactNode;
18
26
  }
19
27
  /**
@@ -27,9 +35,13 @@ interface SegmentedProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'
27
35
  * enabled option and select it, wrapping at the ends; Home and End jump; Left/Right swap in
28
36
  * a right-to-left group. Before 2.32.0 every option was its own tab stop.
29
37
  */
30
- declare function Segmented({ value, onChange, selection, children, className, onKeyDown, ...props }: SegmentedProps): react.JSX.Element;
38
+ declare function Segmented({ value, onChange, selection, renderLink, children, className, onKeyDown, ...props }: SegmentedProps): react.JSX.Element;
31
39
  interface SegmentedOptionProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'value' | 'onChange'> {
32
40
  value: string;
41
+ /** In a `selection="link"` group: where the option goes. Required there, ignored otherwise. */
42
+ href?: string;
43
+ /** In a `selection="link"` group: the target's language, e.g. `de` on a locale switcher. */
44
+ hrefLang?: string;
33
45
  children?: ReactNode;
34
46
  }
35
47
  /**
@@ -38,6 +50,29 @@ interface SegmentedOptionProps extends Omit<ButtonHTMLAttributes<HTMLButtonEleme
38
50
  * `.uix-segmented__option[aria-pressed="true"]` (or `[aria-checked="true"]` in a
39
51
  * `selection="radio"` group).
40
52
  */
41
- declare function SegmentedOption({ value, children, className, onClick, ...props }: SegmentedOptionProps): react.JSX.Element;
53
+ declare function SegmentedOption({ value, href, hrefLang, children, className, onClick, ...props }: SegmentedOptionProps): react.JSX.Element;
54
+ interface LocaleSwitcherLocale {
55
+ /** BCP 47 code: `de`, `bs`, `en-GB`. Sets the link's `lang` and `hreflang`. */
56
+ code: string;
57
+ /** What the option shows: "Deutsch", or a short "DE". */
58
+ label: ReactNode;
59
+ /** This page in that language. */
60
+ href: string;
61
+ }
62
+ interface LocaleSwitcherProps extends Omit<HTMLAttributes<HTMLElement>, 'children' | 'onChange'> {
63
+ /** One link per language, in the order shown. */
64
+ locales: readonly LocaleSwitcherLocale[];
65
+ /** The code of the page's language; that link is `aria-current="page"`. */
66
+ current: string;
67
+ /** Names the navigation, in the page's language: "Language", "Sprache". */
68
+ label: string;
69
+ /** Renders each link with your router (locale-aware links keep client navigation). */
70
+ renderLink?: UixRenderLink;
71
+ }
72
+ /**
73
+ * A language switcher (HAR-1380; MOTUS C-12): a `Segmented selection="link"` whose links carry
74
+ * `lang` and `hreflang`, so each language's name is read in that language.
75
+ */
76
+ declare function LocaleSwitcher({ locales, current, label, renderLink, ...props }: LocaleSwitcherProps): react.JSX.Element;
42
77
 
43
- export { Segmented, SegmentedOption, type SegmentedOptionProps, type SegmentedProps };
78
+ export { LocaleSwitcher, type LocaleSwitcherLocale, type LocaleSwitcherProps, Segmented, SegmentedOption, type SegmentedOptionProps, type SegmentedProps };
@@ -2,14 +2,15 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { createContext, useContext, useEffect, useMemo, useRef } from "react";
4
4
  import { cx } from "../cx.js";
5
+ import { renderUixLink } from "../link.js";
5
6
  const SegmentedCtx = createContext({});
6
7
  const OPTION = ".uix-segmented__option";
7
- function Segmented({ value, onChange, selection = "toggle", children, className, onKeyDown, ...props }) {
8
+ function Segmented({ value, onChange, selection = "toggle", renderLink, children, className, onKeyDown, ...props }) {
8
9
  const ref = useRef(null);
9
- const contextValue = useMemo(() => ({ value, onChange, selection }), [value, onChange, selection]);
10
+ const contextValue = useMemo(() => ({ value, onChange, selection, renderLink }), [value, onChange, selection, renderLink]);
10
11
  useEffect(() => {
11
12
  const group = ref.current;
12
- if (!group) return;
13
+ if (!group || selection === "link") return;
13
14
  const all = Array.from(group.querySelectorAll(OPTION));
14
15
  const enabled = all.filter((o) => !o.disabled);
15
16
  const current = enabled.find((o) => o.getAttribute("aria-pressed") === "true" || o.getAttribute("aria-checked") === "true") ?? enabled[0];
@@ -55,10 +56,15 @@ function Segmented({ value, onChange, selection = "toggle", children, className,
55
56
  target.focus();
56
57
  target.click();
57
58
  };
59
+ if (selection === "link") {
60
+ return /* @__PURE__ */ jsx(SegmentedCtx.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("nav", { className: cx("uix-segmented-nav", className), onKeyDown, ...props, children: /* @__PURE__ */ jsx("ul", { className: "uix-segmented", role: "list", children }) }) });
61
+ }
58
62
  return /* @__PURE__ */ jsx(SegmentedCtx.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { ref, role: selection === "radio" ? "radiogroup" : "group", className: cx("uix-segmented", className), onKeyDown: handleKeyDown, ...props, children }) });
59
63
  }
60
64
  function SegmentedOption({
61
65
  value,
66
+ href,
67
+ hrefLang,
62
68
  children,
63
69
  className,
64
70
  onClick,
@@ -67,6 +73,24 @@ function SegmentedOption({
67
73
  const ctx = useContext(SegmentedCtx);
68
74
  const selected = ctx.value === value;
69
75
  const radio = ctx.selection === "radio";
76
+ if (ctx.selection === "link") {
77
+ const { type: _type, disabled, form: _form, formAction: _formAction, name: _name, ...linkProps } = props;
78
+ if (disabled || !href) {
79
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("a", { ...linkProps, hrefLang, className: cx("uix-segmented__option", className), "aria-disabled": "true", "aria-current": selected ? "page" : void 0, children }) });
80
+ }
81
+ return /* @__PURE__ */ jsx("li", { children: renderUixLink(ctx.renderLink, {
82
+ ...linkProps,
83
+ href,
84
+ hrefLang,
85
+ className: cx("uix-segmented__option", className),
86
+ "aria-current": selected ? "page" : void 0,
87
+ onClick: (event) => {
88
+ ctx.onChange?.(value);
89
+ onClick?.(event);
90
+ },
91
+ children
92
+ }) });
93
+ }
70
94
  return /* @__PURE__ */ jsx(
71
95
  "button",
72
96
  {
@@ -85,7 +109,11 @@ function SegmentedOption({
85
109
  }
86
110
  );
87
111
  }
112
+ function LocaleSwitcher({ locales, current, label, renderLink, ...props }) {
113
+ return /* @__PURE__ */ jsx(Segmented, { selection: "link", value: current, "aria-label": label, renderLink, ...props, children: locales.map((locale) => /* @__PURE__ */ jsx(SegmentedOption, { value: locale.code, href: locale.href, hrefLang: locale.code, lang: locale.code, children: locale.label }, locale.code)) });
114
+ }
88
115
  export {
116
+ LocaleSwitcher,
89
117
  Segmented,
90
118
  SegmentedOption
91
119
  };
@@ -27,7 +27,8 @@ interface SelectLabels {
27
27
  results: string;
28
28
  }
29
29
  interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
30
- size?: 'sm' | 'md';
30
+ /** `sm` 28 px, `md` 36 px (default), `lg` 44 px, the public touch-target size (HAR-1812). */
31
+ size?: 'sm' | 'md' | 'lg';
31
32
  /** The options as data, instead of `<option>` / `<optgroup>` children. */
32
33
  options?: readonly (SelectOption | SelectGroup)[];
33
34
  /**
@@ -64,7 +65,7 @@ interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'siz
64
65
  * A select that draws its own list (HAR-1572): the WAI-ARIA APG select-only combobox — focus
65
66
  * stays on the trigger, `aria-activedescendant` marks the active option — in a top-layer popover
66
67
  * placed with `useAnchoredPosition`, or a bottom sheet on a phone. A visually hidden native
67
- * `<select>` carries `name`, `required`, `form`, `multiple` and the value, so `<option>` children,
68
+ * select element (the form proxy) carries `name`, `required`, `form`, `multiple` and the value, so `<option>` children,
68
69
  * `onChange(e)` with `e.target.value`, FormData, `form.reset()`, `ref` and react-hook-form keep
69
70
  * working; `ref` points at that select.
70
71
  */
@@ -706,7 +706,7 @@ const Select = forwardRef(function Select2(props, forwardedRef) {
706
706
  ref: triggerRef,
707
707
  type: "button",
708
708
  role: "combobox",
709
- className: cx("uix-select", size === "sm" && "uix-select--sm", className),
709
+ className: cx("uix-select", size !== "md" && `uix-select--${size}`, className),
710
710
  style,
711
711
  disabled,
712
712
  "aria-label": ariaLabel,
@@ -5,17 +5,18 @@ interface ShortcutHintProps {
5
5
  keys: KeyShortcut;
6
6
  /** Text before the keys, e.g. an icon button's name. */
7
7
  label?: string;
8
- /** Set the button's `aria-keyshortcuts`. `false` when the button already has its own (a prop, or a Tooltip's). */
9
- ownsAria: boolean;
8
+ /** The control's own `aria-describedby`: the spoken keys are added after it, never instead. */
9
+ describedBy?: string;
10
10
  }
11
11
  /**
12
- * The hover / keyboard-focus hint of a `Button` with `shortcut` (HAR-2031). In the button it
13
- * leaves only an empty, hidden marker, so the button's text, name, layout and selectors are
14
- * unchanged (no wrapper, no sibling). The bubble itself is portalled after mount into the
15
- * nearest `<dialog>` (outside one a modal dialog makes everything inert) or `<body>`, as a
16
- * top-layer popover, `aria-hidden`: assistive technology gets the key from the button's
17
- * `aria-keyshortcuts`, which this sets for the viewer's platform (Mod is Meta on Apple, Control
18
- * elsewhere) unless the button already has its own.
12
+ * The hover / keyboard-focus hint of a `Button` or `ButtonLink` with `shortcut` (HAR-2031,
13
+ * HAR-2416). In the control it leaves only an empty, hidden marker, so the control's text, name,
14
+ * layout and selectors are unchanged (no wrapper, no sibling). The bubble itself is portalled
15
+ * after mount into the nearest `<dialog>` (outside one a modal dialog makes everything inert) or
16
+ * `<body>`, as a top-layer popover, `aria-hidden`. Screen readers hear the keys once, as the
17
+ * control's description ("Control+K"; ⌘ is "Command" on Apple), the same way a `Tooltip` with a
18
+ * shortcut says them: the description is the one channel every screen reader announces, and an
19
+ * `aria-keyshortcuts` beside it was read a second time by those that announce that too.
19
20
  *
20
21
  * It opens on hover and on keyboard focus (`:focus-visible`), so focus a menu or dialog sends back
21
22
  * after a mouse choice does not open it. It closes on leave, blur, Escape and when the button is
@@ -24,6 +25,6 @@ interface ShortcutHintProps {
24
25
  *
25
26
  * It is its own client component so that `Button`, which has no hooks, still renders on a server.
26
27
  */
27
- declare function ShortcutHint({ keys, label, ownsAria }: ShortcutHintProps): react.JSX.Element;
28
+ declare function ShortcutHint({ keys, label, describedBy }: ShortcutHintProps): react.JSX.Element;
28
29
 
29
30
  export { ShortcutHint, type ShortcutHintProps };
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
6
- import { KbdCombo, ariaKeyShortcuts, useKbdPlatform } from "./Kbd.js";
6
+ import { KbdCombo, spokenKeys, useKbdPlatform } from "./Kbd.js";
7
7
  const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
8
8
  const isShown = (el) => {
9
9
  try {
@@ -20,26 +20,15 @@ const focusVisible = (el) => {
20
20
  }
21
21
  };
22
22
  const stop = (event) => event.stopPropagation();
23
- function ShortcutHint({ keys, label, ownsAria }) {
23
+ function ShortcutHint({ keys, label, describedBy }) {
24
24
  const markerRef = useRef(null);
25
25
  const bubbleRef = useRef(null);
26
26
  const anchorRef = useRef(null);
27
27
  const [host, setHost] = useState(null);
28
28
  const [open, setOpen] = useState(false);
29
29
  const platform = useKbdPlatform();
30
- const shortcuts = ariaKeyShortcuts(keys, platform);
30
+ const spokenId = `${useId()}-keys`;
31
31
  useAnchoredPosition(anchorRef, bubbleRef, { open, placement: "top", offset: 6 });
32
- useIsomorphicLayoutEffect(() => {
33
- const button = markerRef.current?.parentElement;
34
- if (!button || !ownsAria) return void 0;
35
- const before = button.getAttribute("aria-keyshortcuts");
36
- button.setAttribute("aria-keyshortcuts", shortcuts);
37
- return () => {
38
- if (button.getAttribute("aria-keyshortcuts") !== shortcuts) return;
39
- if (before == null) button.removeAttribute("aria-keyshortcuts");
40
- else button.setAttribute("aria-keyshortcuts", before);
41
- };
42
- }, [shortcuts, ownsAria]);
43
32
  useIsomorphicLayoutEffect(() => {
44
33
  const button = markerRef.current?.parentElement ?? null;
45
34
  anchorRef.current = button;
@@ -49,6 +38,17 @@ function ShortcutHint({ keys, label, ownsAria }) {
49
38
  }
50
39
  setHost(button.closest("dialog") ?? button.ownerDocument.body);
51
40
  }, []);
41
+ useIsomorphicLayoutEffect(() => {
42
+ const control = anchorRef.current;
43
+ if (!host || !control) return void 0;
44
+ const value = [describedBy, spokenId].filter(Boolean).join(" ");
45
+ control.setAttribute("aria-describedby", value);
46
+ return () => {
47
+ if (control.getAttribute("aria-describedby") !== value) return;
48
+ if (describedBy) control.setAttribute("aria-describedby", describedBy);
49
+ else control.removeAttribute("aria-describedby");
50
+ };
51
+ }, [host, describedBy, spokenId]);
52
52
  useEffect(() => {
53
53
  const button = anchorRef.current;
54
54
  const bubble = bubbleRef.current;
@@ -85,7 +85,10 @@ function ShortcutHint({ keys, label, ownsAria }) {
85
85
  if (focusVisible(button)) show();
86
86
  };
87
87
  const onKey = (event) => {
88
- if (event.key === "Escape") hide();
88
+ if (event.key !== "Escape" || !isShown(bubble)) return;
89
+ event.preventDefault();
90
+ event.stopPropagation();
91
+ hide();
89
92
  };
90
93
  const watch = new MutationObserver(() => {
91
94
  if (button.hasAttribute("disabled") || button.getAttribute("aria-disabled") === "true") hide();
@@ -97,7 +100,7 @@ function ShortcutHint({ keys, label, ownsAria }) {
97
100
  button.addEventListener("blur", hide);
98
101
  bubble.addEventListener("mouseenter", show);
99
102
  bubble.addEventListener("mouseleave", hideSoon);
100
- button.ownerDocument.addEventListener("keydown", onKey);
103
+ button.ownerDocument.addEventListener("keydown", onKey, true);
101
104
  return () => {
102
105
  window.clearTimeout(timer);
103
106
  watch.disconnect();
@@ -107,7 +110,7 @@ function ShortcutHint({ keys, label, ownsAria }) {
107
110
  button.removeEventListener("blur", hide);
108
111
  bubble.removeEventListener("mouseenter", show);
109
112
  bubble.removeEventListener("mouseleave", hideSoon);
110
- button.ownerDocument.removeEventListener("keydown", onKey);
113
+ button.ownerDocument.removeEventListener("keydown", onKey, true);
111
114
  if (isShown(bubble)) {
112
115
  try {
113
116
  bubble.hidePopover();
@@ -120,26 +123,29 @@ function ShortcutHint({ keys, label, ownsAria }) {
120
123
  return /* @__PURE__ */ jsxs(Fragment, { children: [
121
124
  /* @__PURE__ */ jsx("span", { ref: markerRef, hidden: true, "data-uix-shortcut-hint": "" }),
122
125
  host && createPortal(
123
- /* @__PURE__ */ jsxs(
124
- "span",
125
- {
126
- ref: bubbleRef,
127
- className: "uix-tooltip__bubble uix-tooltip__bubble--hint",
128
- "aria-hidden": "true",
129
- ...bubbleAttr,
130
- onClick: stop,
131
- onDoubleClick: stop,
132
- onContextMenu: stop,
133
- onPointerDown: stop,
134
- onPointerUp: stop,
135
- onMouseDown: stop,
136
- onMouseUp: stop,
137
- children: [
138
- label && /* @__PURE__ */ jsx("span", { children: label }),
139
- /* @__PURE__ */ jsx("span", { className: "uix-tooltip__shortcut", children: /* @__PURE__ */ jsx(KbdCombo, { keys: [...keys], platform }) })
140
- ]
141
- }
142
- ),
126
+ /* @__PURE__ */ jsxs(Fragment, { children: [
127
+ /* @__PURE__ */ jsx("span", { id: spokenId, hidden: true, children: spokenKeys(keys, platform) }),
128
+ /* @__PURE__ */ jsxs(
129
+ "span",
130
+ {
131
+ ref: bubbleRef,
132
+ className: "uix-tooltip__bubble uix-tooltip__bubble--hint",
133
+ "aria-hidden": "true",
134
+ ...bubbleAttr,
135
+ onClick: stop,
136
+ onDoubleClick: stop,
137
+ onContextMenu: stop,
138
+ onPointerDown: stop,
139
+ onPointerUp: stop,
140
+ onMouseDown: stop,
141
+ onMouseUp: stop,
142
+ children: [
143
+ label && /* @__PURE__ */ jsx("span", { children: label }),
144
+ /* @__PURE__ */ jsx("span", { className: "uix-tooltip__shortcut", children: /* @__PURE__ */ jsx(KbdCombo, { keys: [...keys], platform }) })
145
+ ]
146
+ }
147
+ )
148
+ ] }),
143
149
  host
144
150
  )
145
151
  ] });
@@ -1,17 +1,42 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, AnchorHTMLAttributes, HTMLAttributes } from 'react';
3
+ import { KeyShortcut } from './Kbd.js';
3
4
 
5
+ /** The search row under the sidebar head (HAR-1371): it opens your search or command palette. */
6
+ interface SidebarSearch {
7
+ /** The row's words, e.g. "Search". In the rail they are the button's name only. */
8
+ label: ReactNode;
9
+ /**
10
+ * The keys that open the same search, shown at the end of the row and given to assistive
11
+ * technology as `aria-keyshortcuts`, e.g. `['Mod', 'K']`. Showing them does not bind them:
12
+ * your command palette (or app) listens for the keys.
13
+ */
14
+ shortcut?: KeyShortcut;
15
+ onActivate: () => void;
16
+ }
4
17
  interface SidebarProps extends HTMLAttributes<HTMLElement> {
5
18
  collapsed?: boolean;
6
19
  nav?: 'full' | 'rail';
7
20
  onToggle?: () => void;
8
21
  brand?: ReactNode;
9
22
  toggleIcon?: ReactNode;
23
+ /**
24
+ * Controls in the head beside the brand (HAR-1371): a mode toggle, a "…" utility `Menu`,
25
+ * collapse-all. Icon buttons (`Button icon size="sm"`) fit the row. Hidden in the rail.
26
+ */
27
+ actions?: ReactNode;
28
+ /** A search row under the head that opens your search (HAR-1371). */
29
+ search?: SidebarSearch;
30
+ /**
31
+ * A slot pinned to the bottom of the sidebar (HAR-1371), for an `AccountMenu`. In the rail it
32
+ * keeps what it renders; give an `AccountMenu` `collapsed` there.
33
+ */
34
+ footer?: ReactNode;
10
35
  children?: ReactNode;
11
36
  expandLabel?: string;
12
37
  collapseLabel?: string;
13
38
  }
14
- declare function Sidebar({ collapsed, nav, onToggle, brand, toggleIcon, children, expandLabel: expandLabelProp, collapseLabel: collapseLabelProp, className, ...props }: SidebarProps): react.JSX.Element;
39
+ declare function Sidebar({ collapsed, nav, onToggle, brand, toggleIcon, actions, search, footer, children, expandLabel: expandLabelProp, collapseLabel: collapseLabelProp, className, ...props }: SidebarProps): react.JSX.Element;
15
40
  interface SidebarSectionProps {
16
41
  label?: string;
17
42
  children?: ReactNode;
@@ -29,7 +54,13 @@ interface NavItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
29
54
  description?: ReactNode;
30
55
  children?: ReactNode;
31
56
  }
32
- declare function NavItem({ icon, badge, active, as: Tag, description, children, className, ...props }: NavItemProps): react.JSX.Element;
57
+ /**
58
+ * One navigation row: a link (`as="a"`, default) or, for a row that acts in place (a mode toggle,
59
+ * an account menu), a `button`. The forwarded ref reaches the rendered `<a>` or `<button>`
60
+ * (HAR-1810), so a row can anchor a `Popover` (`anchor={ref}`); `popoverTarget` and other button
61
+ * attributes pass through with `as="button"`.
62
+ */
63
+ declare const NavItem: react.ForwardRefExoticComponent<NavItemProps & react.RefAttributes<HTMLButtonElement | HTMLAnchorElement>>;
33
64
  interface NavGroupProps {
34
65
  icon?: ReactNode;
35
66
  label: ReactNode;
@@ -44,6 +75,7 @@ interface SubNavItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
44
75
  as?: 'a' | 'button';
45
76
  children?: ReactNode;
46
77
  }
47
- declare function SubNavItem({ active, as: Tag, children, className, ...props }: SubNavItemProps): react.JSX.Element;
78
+ /** A nested navigation row. Like `NavItem`, it forwards its ref to the `<a>` or `<button>` (HAR-1810). */
79
+ declare const SubNavItem: react.ForwardRefExoticComponent<SubNavItemProps & react.RefAttributes<HTMLButtonElement | HTMLAnchorElement>>;
48
80
 
49
- export { NavGroup, type NavGroupProps, NavItem, type NavItemProps, Sidebar, type SidebarProps, SidebarSection, type SidebarSectionProps, SubNavItem, type SubNavItemProps };
81
+ export { NavGroup, type NavGroupProps, NavItem, type NavItemProps, Sidebar, type SidebarProps, type SidebarSearch, SidebarSection, type SidebarSectionProps, SubNavItem, type SubNavItemProps };