@tensor_1/react 2.46.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 (204) hide show
  1. package/README.md +509 -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/BusyBar.d.ts +13 -0
  22. package/dist/components/BusyBar.js +10 -0
  23. package/dist/components/BusyRegion.d.ts +18 -0
  24. package/dist/components/BusyRegion.js +12 -0
  25. package/dist/components/Button.d.ts +33 -5
  26. package/dist/components/Button.js +12 -6
  27. package/dist/components/ButtonFeedback.d.ts +30 -0
  28. package/dist/components/ButtonFeedback.js +78 -0
  29. package/dist/components/Card.d.ts +18 -1
  30. package/dist/components/Card.js +5 -1
  31. package/dist/components/ChangeHighlight.d.ts +30 -0
  32. package/dist/components/ChangeHighlight.js +21 -0
  33. package/dist/components/ChartCore.js +2 -1
  34. package/dist/components/Checkbox.d.ts +27 -1
  35. package/dist/components/Checkbox.js +31 -2
  36. package/dist/components/CodeDisplay.d.ts +65 -0
  37. package/dist/components/CodeDisplay.js +124 -0
  38. package/dist/components/Combobox.d.ts +27 -6
  39. package/dist/components/Combobox.js +195 -58
  40. package/dist/components/CommandPalette.d.ts +36 -1
  41. package/dist/components/CommandPalette.js +254 -2
  42. package/dist/components/ContactCard.d.ts +66 -0
  43. package/dist/components/ContactCard.js +74 -0
  44. package/dist/components/Count.d.ts +24 -0
  45. package/dist/components/Count.js +32 -0
  46. package/dist/components/DataWorkspace.d.ts +167 -0
  47. package/dist/components/DataWorkspace.js +383 -0
  48. package/dist/components/DateFieldParts.d.ts +2 -1
  49. package/dist/components/DateFieldParts.js +7 -0
  50. package/dist/components/DatePicker.d.ts +9 -2
  51. package/dist/components/DatePicker.js +67 -28
  52. package/dist/components/DateRangePicker.d.ts +12 -2
  53. package/dist/components/DateRangePicker.js +13 -12
  54. package/dist/components/DelayedRender.d.ts +15 -0
  55. package/dist/components/DelayedRender.js +10 -0
  56. package/dist/components/DetailPage.d.ts +7 -1
  57. package/dist/components/DetailPage.js +2 -1
  58. package/dist/components/Dialogs.d.ts +71 -10
  59. package/dist/components/Dialogs.js +130 -30
  60. package/dist/components/DiffViewer.d.ts +23 -3
  61. package/dist/components/DiffViewer.js +26 -12
  62. package/dist/components/DiscardPrompt.d.ts +1 -0
  63. package/dist/components/DiscardPrompt.js +16 -0
  64. package/dist/components/DisplayPreferences.d.ts +83 -0
  65. package/dist/components/DisplayPreferences.js +185 -0
  66. package/dist/components/Drawer.d.ts +1 -0
  67. package/dist/components/EditorialHome.d.ts +23 -3
  68. package/dist/components/EditorialHome.js +16 -11
  69. package/dist/components/EntityPicker.d.ts +20 -3
  70. package/dist/components/EntityPicker.js +32 -13
  71. package/dist/components/FilterEditor.d.ts +18 -4
  72. package/dist/components/FilterEditor.js +141 -19
  73. package/dist/components/InfoTip.js +11 -12
  74. package/dist/components/InlineEdit.d.ts +87 -20
  75. package/dist/components/InlineEdit.js +197 -54
  76. package/dist/components/Input.d.ts +12 -2
  77. package/dist/components/Input.js +5 -3
  78. package/dist/components/Kbd.d.ts +6 -1
  79. package/dist/components/Kbd.js +6 -6
  80. package/dist/components/Lightbox.d.ts +41 -5
  81. package/dist/components/Lightbox.js +95 -9
  82. package/dist/components/MatchReview.d.ts +40 -1
  83. package/dist/components/MatchReview.js +130 -4
  84. package/dist/components/MetricInput.js +3 -2
  85. package/dist/components/Modal.d.ts +3 -1
  86. package/dist/components/Modal.js +13 -4
  87. package/dist/components/NavFavourites.d.ts +5 -0
  88. package/dist/components/NavFavourites.js +7 -7
  89. package/dist/components/OtpInput.d.ts +33 -0
  90. package/dist/components/OtpInput.js +123 -0
  91. package/dist/components/PageHeader.d.ts +7 -1
  92. package/dist/components/PageHeader.js +2 -2
  93. package/dist/components/Pagination.js +6 -5
  94. package/dist/components/PasswordInput.d.ts +34 -0
  95. package/dist/components/PasswordInput.js +101 -0
  96. package/dist/components/Peek.js +3 -3
  97. package/dist/components/Popover.js +2 -0
  98. package/dist/components/PrintLayout.d.ts +63 -0
  99. package/dist/components/PrintLayout.js +114 -0
  100. package/dist/components/QrCode.d.ts +47 -0
  101. package/dist/components/QrCode.js +55 -0
  102. package/dist/components/QrScanner.d.ts +95 -0
  103. package/dist/components/QrScanner.js +266 -0
  104. package/dist/components/RelationshipGraph.d.ts +32 -1
  105. package/dist/components/RelationshipGraph.js +18 -5
  106. package/dist/components/RelationshipGraphLayered.d.ts +1 -1
  107. package/dist/components/RelationshipGraphLayered.js +190 -115
  108. package/dist/components/RelationshipGraphList.js +33 -6
  109. package/dist/components/RuleBuilder.js +39 -7
  110. package/dist/components/SaveStatus.d.ts +7 -1
  111. package/dist/components/SaveStatus.js +13 -4
  112. package/dist/components/SchedulingAgenda.d.ts +3 -1
  113. package/dist/components/SchedulingCalendar.d.ts +3 -1
  114. package/dist/components/SchedulingCalendar.js +43 -4
  115. package/dist/components/SchedulingResourceDay.d.ts +57 -0
  116. package/dist/components/SchedulingResourceDay.js +287 -0
  117. package/dist/components/SchedulingTimeGrid.d.ts +3 -1
  118. package/dist/components/SchedulingTimeline.d.ts +3 -1
  119. package/dist/components/SearchBar.d.ts +1 -0
  120. package/dist/components/SearchSuggest.d.ts +23 -3
  121. package/dist/components/SearchSuggest.js +48 -17
  122. package/dist/components/Segmented.d.ts +39 -4
  123. package/dist/components/Segmented.js +31 -3
  124. package/dist/components/Select.d.ts +3 -2
  125. package/dist/components/Select.js +1 -1
  126. package/dist/components/ShortcutHint.d.ts +11 -10
  127. package/dist/components/ShortcutHint.js +44 -38
  128. package/dist/components/Sidebar.d.ts +36 -4
  129. package/dist/components/Sidebar.js +37 -12
  130. package/dist/components/SortableList.d.ts +45 -0
  131. package/dist/components/SortableList.js +134 -0
  132. package/dist/components/Spinner.d.ts +7 -1
  133. package/dist/components/Spinner.js +7 -1
  134. package/dist/components/Stat.d.ts +28 -3
  135. package/dist/components/Stat.js +13 -3
  136. package/dist/components/States.d.ts +7 -1
  137. package/dist/components/States.js +7 -1
  138. package/dist/components/StatusPill.d.ts +14 -3
  139. package/dist/components/StatusPill.js +11 -3
  140. package/dist/components/Steps.js +2 -1
  141. package/dist/components/Table.d.ts +16 -2
  142. package/dist/components/Table.js +8 -4
  143. package/dist/components/TableControls.js +18 -58
  144. package/dist/components/TableOverflowProbe.d.ts +12 -0
  145. package/dist/components/TableOverflowProbe.js +27 -0
  146. package/dist/components/Tabs.d.ts +2 -1
  147. package/dist/components/Tabs.js +1 -0
  148. package/dist/components/TagInput.d.ts +67 -0
  149. package/dist/components/TagInput.js +342 -0
  150. package/dist/components/Textarea.d.ts +5 -0
  151. package/dist/components/Textarea.js +2 -2
  152. package/dist/components/TimePicker.d.ts +80 -0
  153. package/dist/components/TimePicker.js +347 -0
  154. package/dist/components/Toast.js +3 -2
  155. package/dist/components/Tooltip.d.ts +8 -4
  156. package/dist/components/Tooltip.js +24 -21
  157. package/dist/components/Tree.d.ts +8 -0
  158. package/dist/components/Tree.js +11 -5
  159. package/dist/display-preferences-model.d.ts +53 -0
  160. package/dist/display-preferences-model.js +64 -0
  161. package/dist/emoji.cjs +28 -35
  162. package/dist/filter-model.d.ts +59 -3
  163. package/dist/filter-model.js +67 -2
  164. package/dist/hooks/useChangeHighlight.d.ts +33 -0
  165. package/dist/hooks/useChangeHighlight.js +44 -0
  166. package/dist/hooks/useDelayedPending.d.ts +22 -0
  167. package/dist/hooks/useDelayedPending.js +35 -0
  168. package/dist/hooks/useDialog.d.ts +5 -0
  169. package/dist/hooks/useDialog.js +3 -2
  170. package/dist/hooks/useOptimisticValue.d.ts +51 -0
  171. package/dist/hooks/useOptimisticValue.js +61 -0
  172. package/dist/hooks/useReducedMotion.d.ts +4 -1
  173. package/dist/hooks/useReducedMotion.js +7 -1
  174. package/dist/hooks/useResolvedFilterLabels.d.ts +15 -0
  175. package/dist/hooks/useResolvedFilterLabels.js +34 -0
  176. package/dist/hooks/useVirtualRows.d.ts +21 -2
  177. package/dist/hooks/useVirtualRows.js +30 -3
  178. package/dist/icons/Icon.d.ts +1 -1
  179. package/dist/index.cjs +18977 -13378
  180. package/dist/index.d.cts +3065 -664
  181. package/dist/index.d.ts +46 -16
  182. package/dist/index.js +149 -7
  183. package/dist/labels-context.d.ts +93 -3
  184. package/dist/motion-timing.d.ts +8 -0
  185. package/dist/motion-timing.js +8 -0
  186. package/dist/overlay-stack.d.ts +6 -0
  187. package/dist/overlay-stack.js +13 -0
  188. package/dist/qr-model.d.ts +65 -0
  189. package/dist/qr-model.js +323 -0
  190. package/dist/relationship-graph-model.d.ts +78 -3
  191. package/dist/relationship-graph-model.js +112 -5
  192. package/dist/reorder.d.ts +30 -0
  193. package/dist/reorder.js +74 -0
  194. package/dist/rich-text.cjs +4 -2
  195. package/dist/scheduling-resource-model.d.ts +76 -0
  196. package/dist/scheduling-resource-model.js +74 -0
  197. package/dist/search-suggest-model.js +3 -1
  198. package/dist/shell-context.d.ts +11 -0
  199. package/dist/shell-context.js +7 -0
  200. package/dist/time-field-model.d.ts +46 -0
  201. package/dist/time-field-model.js +152 -0
  202. package/dist/toast-store.d.ts +17 -1
  203. package/dist/toast-store.js +23 -7
  204. package/package.json +2 -1
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useEffect, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
4
4
  import { cx } from "../cx.js";
5
5
  import { fillLabel } from "../fill-label.js";
6
6
  import { useDialog } from "../hooks/useDialog.js";
7
+ import { useUixLabels } from "../labels-context.js";
7
8
  const DEFAULT_LIGHTBOX_LABELS = {
8
9
  region: "Media viewer",
9
10
  previous: "Previous",
@@ -13,10 +14,42 @@ const DEFAULT_LIGHTBOX_LABELS = {
13
14
  zoomIn: "Show actual size",
14
15
  zoomOut: "Fit to screen"
15
16
  };
17
+ function containedBox(media) {
18
+ const boxW = media.clientWidth;
19
+ const boxH = media.clientHeight;
20
+ const naturalW = "naturalWidth" in media ? media.naturalWidth : media.videoWidth;
21
+ const naturalH = "naturalHeight" in media ? media.naturalHeight : media.videoHeight;
22
+ if (!naturalW || !naturalH || !boxW || !boxH) return { left: media.offsetLeft, top: media.offsetTop, width: boxW, height: boxH };
23
+ const scale = Math.min(boxW / naturalW, boxH / naturalH);
24
+ const width = naturalW * scale;
25
+ const height = naturalH * scale;
26
+ return { left: media.offsetLeft + (boxW - width) / 2, top: media.offsetTop + (boxH - height) / 2, width, height };
27
+ }
16
28
  const Chevron = ({ dir }) => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: dir === "left" ? "m15 18-6-6 6-6" : "m9 18 6-6-6-6" }) });
17
- function Lightbox({ items, index, onIndexChange, open, onClose, loop = false, labels: labelOverrides, className }) {
18
- const labels = { ...DEFAULT_LIGHTBOX_LABELS, ...labelOverrides };
19
- const ref = useDialog(open, onClose);
29
+ function Lightbox({
30
+ items,
31
+ index,
32
+ onIndexChange,
33
+ open,
34
+ onClose,
35
+ loop = false,
36
+ labels: labelOverrides,
37
+ className,
38
+ actions,
39
+ renderOverlay,
40
+ onKeyDown: onKeyDownProp,
41
+ onEscapeKeyDown
42
+ }) {
43
+ const labels = { ...DEFAULT_LIGHTBOX_LABELS, ...useUixLabels().lightbox, ...labelOverrides };
44
+ const escapeClaimed = useRef(false);
45
+ const ref = useDialog(open, onClose, () => {
46
+ const claimed = escapeClaimed.current;
47
+ escapeClaimed.current = false;
48
+ return claimed;
49
+ });
50
+ const mediaRef = useRef(null);
51
+ const pressedBackdrop = useRef(false);
52
+ const [overlayBox, setOverlayBox] = useState(null);
20
53
  const closeRef = useRef(null);
21
54
  const returnTo = useRef(null);
22
55
  const swipe = useRef(null);
@@ -37,13 +70,47 @@ function Lightbox({ items, index, onIndexChange, open, onClose, loop = false, la
37
70
  useEffect(() => {
38
71
  setZoomed(false);
39
72
  }, [index]);
73
+ useEffect(() => {
74
+ if (!open) return;
75
+ const reset = () => {
76
+ escapeClaimed.current = false;
77
+ };
78
+ window.addEventListener("keydown", reset, true);
79
+ return () => window.removeEventListener("keydown", reset, true);
80
+ }, [open]);
81
+ const hasOverlay = renderOverlay != null;
82
+ const measure = useCallback(() => {
83
+ const media = mediaRef.current;
84
+ if (!media) {
85
+ setOverlayBox(null);
86
+ return;
87
+ }
88
+ const box = containedBox(media);
89
+ setOverlayBox((prev) => prev && prev.left === box.left && prev.top === box.top && prev.width === box.width && prev.height === box.height ? prev : box);
90
+ }, []);
91
+ useLayoutEffect(() => {
92
+ if (!open || !hasOverlay) return;
93
+ measure();
94
+ const media = mediaRef.current;
95
+ if (!media || typeof ResizeObserver === "undefined") return;
96
+ const observer = new ResizeObserver(measure);
97
+ observer.observe(media);
98
+ return () => observer.disconnect();
99
+ }, [open, index, zoomed, hasOverlay, measure]);
40
100
  const go = (to) => {
41
101
  if (total === 0) return;
42
102
  const next = loop ? (to + total) % total : Math.min(Math.max(to, 0), total - 1);
43
103
  if (next !== index) onIndexChange(next);
44
104
  };
45
105
  const onKeyDown = (event) => {
106
+ onKeyDownProp?.(event);
107
+ if (event.key === "Escape" && !event.defaultPrevented) onEscapeKeyDown?.(event);
108
+ if (event.defaultPrevented) {
109
+ if (event.key === "Escape") escapeClaimed.current = true;
110
+ return;
111
+ }
46
112
  if (event.target.closest("video")) return;
113
+ if (event.target.closest('input, textarea, select, [contenteditable="true"]')) return;
47
114
  if (event.key === "ArrowLeft") {
48
115
  event.preventDefault();
49
116
  go(index - 1);
@@ -58,8 +125,9 @@ function Lightbox({ items, index, onIndexChange, open, onClose, loop = false, la
58
125
  go(total - 1);
59
126
  }
60
127
  };
128
+ const onOverlay = (event) => event.target.closest?.(".uix-lightbox__overlay") != null;
61
129
  const onPointerDown = (event) => {
62
- if (event.pointerType !== "mouse") swipe.current = { x: event.clientX, id: event.pointerId };
130
+ if (event.pointerType !== "mouse" && !onOverlay(event)) swipe.current = { x: event.clientX, id: event.pointerId };
63
131
  };
64
132
  const onPointerUp = (event) => {
65
133
  const start = swipe.current;
@@ -74,15 +142,30 @@ function Lightbox({ items, index, onIndexChange, open, onClose, loop = false, la
74
142
  ref,
75
143
  className: cx("uix-lightbox", "uix-lightbox--gallery", className),
76
144
  "aria-label": labels.region,
145
+ "data-actions": actions != null || void 0,
77
146
  onKeyDown,
147
+ onPointerDown: (event) => {
148
+ pressedBackdrop.current = event.target === event.currentTarget;
149
+ },
78
150
  onClick: (event) => {
79
- if (event.target === event.currentTarget) onClose();
151
+ if (event.target === event.currentTarget && pressedBackdrop.current) onClose();
152
+ pressedBackdrop.current = false;
80
153
  },
81
154
  children: [
82
155
  current && /* @__PURE__ */ jsxs("figure", { className: "uix-lightbox__figure", children: [
83
- /* @__PURE__ */ jsx("div", { className: "uix-lightbox__stage", "data-zoomed": zoomed || void 0, onPointerDown, onPointerUp, children: current.kind === "video" ? /* @__PURE__ */ jsx("video", { className: "uix-lightbox__media", src: current.src, controls: true, "aria-label": current.alt, width: current.width, height: current.height }) : /* @__PURE__ */ jsx("img", { className: "uix-lightbox__media", src: current.src, alt: current.alt, width: current.width, height: current.height }) }),
156
+ /* @__PURE__ */ jsxs("div", { className: "uix-lightbox__stage", "data-zoomed": zoomed || void 0, onPointerDown, onPointerUp, children: [
157
+ current.kind === "video" ? /* @__PURE__ */ jsx("video", { ref: mediaRef, className: "uix-lightbox__media", src: current.src, controls: true, "aria-label": current.alt, width: current.width, height: current.height, onLoadedMetadata: hasOverlay ? measure : void 0 }) : /* @__PURE__ */ jsx("img", { ref: mediaRef, className: "uix-lightbox__media", src: current.src, alt: current.alt, width: current.width, height: current.height, onLoad: hasOverlay ? measure : void 0 }),
158
+ renderOverlay && /* @__PURE__ */ jsx(
159
+ "div",
160
+ {
161
+ className: "uix-lightbox__overlay",
162
+ style: overlayBox ? { left: overlayBox.left, top: overlayBox.top, width: overlayBox.width, height: overlayBox.height } : { inset: 0 },
163
+ children: renderOverlay({ index, item: current, media: mediaRef.current, width: overlayBox?.width ?? 0, height: overlayBox?.height ?? 0 })
164
+ }
165
+ )
166
+ ] }),
84
167
  /* @__PURE__ */ jsxs("figcaption", { className: "uix-lightbox__bar", children: [
85
- /* @__PURE__ */ jsx("span", { className: "uix-lightbox__counter", "aria-live": "polite", children: fillLabel(labels.counter, { index: index + 1, total }) }),
168
+ total > 1 && /* @__PURE__ */ jsx("span", { className: "uix-lightbox__counter", "aria-live": "polite", children: fillLabel(labels.counter, { index: index + 1, total }) }),
86
169
  current.caption != null && /* @__PURE__ */ jsx("span", { className: "uix-lightbox__caption", children: current.caption }),
87
170
  current.kind !== "video" && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-lightbox__tool", "aria-pressed": zoomed, onClick: () => setZoomed((z) => !z), children: zoomed ? labels.zoomOut : labels.zoomIn })
88
171
  ] })
@@ -91,7 +174,10 @@ function Lightbox({ items, index, onIndexChange, open, onClose, loop = false, la
91
174
  /* @__PURE__ */ jsx("button", { type: "button", className: "uix-lightbox__nav uix-lightbox__nav--prev", "aria-label": labels.previous, disabled: !hasPrev, onClick: () => go(index - 1), children: /* @__PURE__ */ jsx(Chevron, { dir: "left" }) }),
92
175
  /* @__PURE__ */ jsx("button", { type: "button", className: "uix-lightbox__nav uix-lightbox__nav--next", "aria-label": labels.next, disabled: !hasNext, onClick: () => go(index + 1), children: /* @__PURE__ */ jsx(Chevron, { dir: "right" }) })
93
176
  ] }),
94
- /* @__PURE__ */ jsx("button", { ref: closeRef, type: "button", className: "uix-lightbox__close", "aria-label": labels.close, onClick: onClose, children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) }) })
177
+ /* @__PURE__ */ jsxs("div", { className: "uix-lightbox__toolbar", children: [
178
+ actions,
179
+ /* @__PURE__ */ jsx("button", { ref: closeRef, type: "button", className: "uix-lightbox__close", "aria-label": labels.close, onClick: onClose, children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) }) })
180
+ ] })
95
181
  ]
96
182
  }
97
183
  );
@@ -63,6 +63,33 @@ interface MatchReviewLabels {
63
63
  failedSuffix: string;
64
64
  bulkAcceptFailed: string;
65
65
  bulkDismissFailed: string;
66
+ /** Adopt mode (HAR-1369): a candidate's heading when `candidateLabel` gives none. `{id}`. */
67
+ candidate: string;
68
+ /** Adopt mode: the compare table's caption. `{candidate}`. */
69
+ compareCaption: string;
70
+ field: string;
71
+ /** Adopt mode: the column of the record as it is now. */
72
+ currentValue: string;
73
+ /** Adopt mode: the column of the candidate's values. */
74
+ candidateValue: string;
75
+ /** Adopt mode: the column of checkboxes. */
76
+ adopt: string;
77
+ /** Adopt mode: one checkbox's name. `{field}`, `{candidate}`. */
78
+ adoptField: string;
79
+ /** Adopt mode: shown instead of a checkbox when the candidate has the same value. */
80
+ same: string;
81
+ /** Adopt mode: the button with one field chosen. `{count}`. */
82
+ adoptSelectedOne: string;
83
+ /** Adopt mode: the button with any other number of fields chosen (0 too). `{count}`. */
84
+ adoptSelectedMany: string;
85
+ /** Adopt mode: the provenance disclosure. */
86
+ provenance: string;
87
+ /** Adopt mode: announced after `onAdopt` resolves, one field. `{count}`, `{candidate}`. */
88
+ adoptedOne: string;
89
+ /** Adopt mode: announced after `onAdopt` resolves, more fields. `{count}`, `{candidate}`. */
90
+ adoptedMany: string;
91
+ /** Adopt mode: shown when `onAdopt` rejects. */
92
+ adoptFailed: string;
66
93
  }
67
94
  declare const DEFAULT_MATCH_REVIEW_LABELS: MatchReviewLabels;
68
95
  interface MatchReviewProps {
@@ -79,8 +106,20 @@ interface MatchReviewProps {
79
106
  onRetry?: () => void;
80
107
  className?: string;
81
108
  labels?: Partial<MatchReviewLabels>;
109
+ /**
110
+ * `decide` (default): one row per candidate, decided as a whole. `adopt` (HAR-1369, MOTUS B-A9
111
+ * registry candidates): per candidate a table of the record now against the candidate, field
112
+ * by field, a checkbox per field that differs, and an Adopt button that calls `onAdopt`.
113
+ */
114
+ fieldMode?: 'decide' | 'adopt';
115
+ /** Adopt mode: take the chosen fields from the candidate. A promise keeps the button busy until it settles. */
116
+ onAdopt?: (candidateId: string, fieldIds: string[]) => void | Promise<unknown>;
117
+ /** Adopt mode: the candidate's heading, e.g. its source ("DNB", "Wikidata"). Default `labels.candidate`. */
118
+ candidateLabel?: (candidate: MatchReviewCandidate) => ReactNode;
119
+ /** Adopt mode: where a candidate comes from, in a disclosure under its table (source, retrieved, match reason). */
120
+ provenance?: (candidate: MatchReviewCandidate) => ReactNode;
82
121
  }
83
122
  /** Generic descriptor-driven incoming-record to candidate-match decision surface. */
84
- declare function MatchReview({ incoming, incomingLabel, fields, candidates, selectedIds, onSelectionChange, onDecision, onBulkDecision, loading, error, onRetry, className, labels: labelOverrides, }: MatchReviewProps): react.JSX.Element;
123
+ declare function MatchReview({ incoming, incomingLabel, fields, candidates, selectedIds, onSelectionChange, onDecision, onBulkDecision, loading, error, onRetry, className, labels: labelOverrides, fieldMode, onAdopt, candidateLabel, provenance, }: MatchReviewProps): react.JSX.Element;
85
124
 
86
125
  export { DEFAULT_MATCH_REVIEW_LABELS, type MatchBulkResult, MatchReview, type MatchReviewAction, type MatchReviewCandidate, type MatchReviewField, type MatchReviewLabels, type MatchReviewProps };
@@ -1,9 +1,12 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useMemo, useState } from "react";
3
+ import { useId, useMemo, useState } from "react";
4
4
  import { cx } from "../cx.js";
5
5
  import { StatusPill } from "./StatusPill.js";
6
+ import { Button } from "./Button.js";
6
7
  import { fillLabel } from "../fill-label.js";
8
+ import { useUixLabels } from "../labels-context.js";
9
+ import { ChevronDownIcon } from "../icons/components.js";
7
10
  const DEFAULT_MATCH_REVIEW_LABELS = {
8
11
  region: "Match review",
9
12
  loading: "Loading match candidates\u2026",
@@ -43,7 +46,21 @@ const DEFAULT_MATCH_REVIEW_LABELS = {
43
46
  dismissedMany: "{count} candidates dismissed",
44
47
  failedSuffix: "; {count} failed",
45
48
  bulkAcceptFailed: "Bulk accept failed. Try again.",
46
- bulkDismissFailed: "Bulk dismiss failed. Try again."
49
+ bulkDismissFailed: "Bulk dismiss failed. Try again.",
50
+ candidate: "Candidate {id}",
51
+ compareCaption: "{candidate}: the record now and the candidate, field by field",
52
+ field: "Field",
53
+ currentValue: "Now",
54
+ candidateValue: "Candidate",
55
+ adopt: "Adopt",
56
+ adoptField: "Adopt {field} from {candidate}",
57
+ same: "Same",
58
+ adoptSelectedOne: "Adopt {count} field",
59
+ adoptSelectedMany: "Adopt {count} fields",
60
+ provenance: "Where this candidate comes from",
61
+ adoptedOne: "{count} field adopted from {candidate}",
62
+ adoptedMany: "{count} fields adopted from {candidate}",
63
+ adoptFailed: "Could not adopt the fields. Try again."
47
64
  };
48
65
  const valueText = (value) => value == null ? "\u2014" : typeof value === "string" ? value : JSON.stringify(value);
49
66
  function MatchReview({
@@ -59,9 +76,13 @@ function MatchReview({
59
76
  error,
60
77
  onRetry,
61
78
  className,
62
- labels: labelOverrides
79
+ labels: labelOverrides,
80
+ fieldMode = "decide",
81
+ onAdopt,
82
+ candidateLabel,
83
+ provenance
63
84
  }) {
64
- const labels = { ...DEFAULT_MATCH_REVIEW_LABELS, ...labelOverrides };
85
+ const labels = { ...DEFAULT_MATCH_REVIEW_LABELS, ...useUixLabels().matchReview, ...labelOverrides };
65
86
  const plural = (n, one, many) => fillLabel(n === 1 ? one : many, { count: n });
66
87
  const decisionLabel = { accepted: labels.decisionAccepted, dismissed: labels.decisionDismissed, split: labels.decisionSplit, other: labels.decisionOther };
67
88
  const actionLabel = { accept: labels.accept, dismiss: labels.dismiss, split: labels.split, "pick-other": labels.pickOther };
@@ -107,6 +128,29 @@ function MatchReview({
107
128
  /* @__PURE__ */ jsx("p", { children: labels.empty }),
108
129
  /* @__PURE__ */ jsx("p", { children: labels.emptyDetail })
109
130
  ] });
131
+ if (fieldMode === "adopt") {
132
+ return /* @__PURE__ */ jsxs("section", { className: cx("uix-match-review", "uix-match-review--adopt", className), "aria-label": labels.region, children: [
133
+ /* @__PURE__ */ jsx("div", { className: "uix-match-review__summary", children: /* @__PURE__ */ jsxs("div", { children: [
134
+ /* @__PURE__ */ jsx("strong", { children: incomingLabel }),
135
+ /* @__PURE__ */ jsx("span", { children: fillLabel(labels.pendingOf, { pending: pending.length, total: candidates.length }) })
136
+ ] }) }),
137
+ candidates.map((candidate) => /* @__PURE__ */ jsx(
138
+ AdoptCandidate,
139
+ {
140
+ candidate,
141
+ incoming,
142
+ fields,
143
+ labels,
144
+ heading: candidateLabel?.(candidate) ?? fillLabel(labels.candidate, { id: candidate.id }),
145
+ provenance: provenance?.(candidate),
146
+ onAdopt,
147
+ onDecision,
148
+ decisionLabel
149
+ },
150
+ candidate.id
151
+ ))
152
+ ] });
153
+ }
110
154
  return /* @__PURE__ */ jsxs("section", { className: cx("uix-match-review", className), "aria-label": labels.region, children: [
111
155
  /* @__PURE__ */ jsxs("div", { className: "uix-match-review__summary", children: [
112
156
  /* @__PURE__ */ jsxs("div", { children: [
@@ -156,6 +200,88 @@ function MatchReview({
156
200
  /* @__PURE__ */ jsx("p", { className: "uix-visually-hidden", "aria-live": "polite", children: announcement })
157
201
  ] });
158
202
  }
203
+ const sameValue = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
204
+ function AdoptCandidate({ candidate, incoming, fields, labels, heading, provenance, onAdopt, onDecision, decisionLabel }) {
205
+ const id = useId();
206
+ const [chosen, setChosen] = useState(/* @__PURE__ */ new Set());
207
+ const [pending, setPending] = useState(false);
208
+ const [message, setMessage] = useState(null);
209
+ const resolved = candidate.status === "resolved";
210
+ const headingText = typeof heading === "string" ? heading : fillLabel(labels.candidate, { id: candidate.id });
211
+ const toggle = (fieldId, on) => {
212
+ if (pending) return;
213
+ const next = new Set(chosen);
214
+ if (on) next.add(fieldId);
215
+ else next.delete(fieldId);
216
+ setChosen(next);
217
+ setMessage(null);
218
+ };
219
+ const adopt = async () => {
220
+ if (!onAdopt || chosen.size === 0 || pending) return;
221
+ const fieldIds = fields.map((f) => f.id).filter((f) => chosen.has(f));
222
+ setPending(true);
223
+ setMessage(null);
224
+ try {
225
+ await onAdopt(candidate.id, fieldIds);
226
+ setChosen(/* @__PURE__ */ new Set());
227
+ setMessage({ text: fillLabel(fieldIds.length === 1 ? labels.adoptedOne : labels.adoptedMany, { count: fieldIds.length, candidate: headingText }), failed: false });
228
+ } catch {
229
+ setMessage({ text: labels.adoptFailed, failed: true });
230
+ } finally {
231
+ setPending(false);
232
+ }
233
+ };
234
+ const show = (field, record) => field.render?.(record[field.id], record) ?? valueText(record[field.id]);
235
+ return /* @__PURE__ */ jsxs("section", { className: "uix-match-review__candidate", "aria-labelledby": `${id}-heading`, "data-status": candidate.status ?? "pending", children: [
236
+ /* @__PURE__ */ jsxs("div", { className: "uix-match-review__candidate-head", children: [
237
+ /* @__PURE__ */ jsx("h3", { id: `${id}-heading`, className: "uix-match-review__candidate-title", children: heading }),
238
+ candidate.confidence != null && /* @__PURE__ */ jsx("span", { className: "uix-match-review__confidence", children: `${Math.round(candidate.confidence * 100)}%` }),
239
+ resolved && /* @__PURE__ */ jsx(StatusPill, { tone: "success", children: candidate.decision ? decisionLabel[candidate.decision] : labels.resolved })
240
+ ] }),
241
+ /* @__PURE__ */ jsx("div", { className: "uix-match-review__table-wrap", children: /* @__PURE__ */ jsxs("table", { className: "uix-table uix-match-review__compare", role: "table", children: [
242
+ /* @__PURE__ */ jsx("caption", { className: "uix-visually-hidden", children: fillLabel(labels.compareCaption, { candidate: headingText }) }),
243
+ /* @__PURE__ */ jsx("thead", { role: "rowgroup", children: /* @__PURE__ */ jsxs("tr", { role: "row", children: [
244
+ /* @__PURE__ */ jsx("th", { role: "columnheader", scope: "col", children: labels.field }),
245
+ /* @__PURE__ */ jsx("th", { role: "columnheader", scope: "col", children: labels.currentValue }),
246
+ /* @__PURE__ */ jsx("th", { role: "columnheader", scope: "col", children: labels.candidateValue }),
247
+ /* @__PURE__ */ jsx("th", { role: "columnheader", scope: "col", children: labels.adopt })
248
+ ] }) }),
249
+ /* @__PURE__ */ jsx("tbody", { role: "rowgroup", children: fields.map((field) => {
250
+ const same = sameValue(incoming[field.id], candidate.record[field.id]);
251
+ return /* @__PURE__ */ jsxs("tr", { role: "row", "data-differs": same ? void 0 : "", "data-chosen": chosen.has(field.id) || void 0, children: [
252
+ /* @__PURE__ */ jsx("th", { role: "rowheader", scope: "row", children: field.label }),
253
+ /* @__PURE__ */ jsx("td", { role: "cell", "data-label": labels.currentValue, children: show(field, incoming) }),
254
+ /* @__PURE__ */ jsx("td", { role: "cell", "data-label": labels.candidateValue, children: show(field, candidate.record) }),
255
+ /* @__PURE__ */ jsx("td", { role: "cell", children: same ? /* @__PURE__ */ jsx("span", { className: "uix-match-review__same", children: labels.same }) : /* @__PURE__ */ jsx(
256
+ "input",
257
+ {
258
+ type: "checkbox",
259
+ className: "uix-match-review__adopt",
260
+ checked: chosen.has(field.id),
261
+ disabled: resolved,
262
+ "aria-label": fillLabel(labels.adoptField, { field: field.label, candidate: headingText }),
263
+ onChange: (event) => toggle(field.id, event.currentTarget.checked)
264
+ }
265
+ ) })
266
+ ] }, field.id);
267
+ }) })
268
+ ] }) }),
269
+ provenance != null && provenance !== false && /* @__PURE__ */ jsxs("details", { className: "uix-match-review__provenance", children: [
270
+ /* @__PURE__ */ jsxs("summary", { className: "uix-match-review__provenance-summary", children: [
271
+ /* @__PURE__ */ jsx("span", { children: labels.provenance }),
272
+ /* @__PURE__ */ jsx("span", { className: "uix-collapsible__chevron", "aria-hidden": "true", children: /* @__PURE__ */ jsx(ChevronDownIcon, { size: "sm" }) })
273
+ ] }),
274
+ /* @__PURE__ */ jsx("div", { className: "uix-match-review__provenance-body", children: provenance })
275
+ ] }),
276
+ /* @__PURE__ */ jsxs("div", { className: "uix-match-review__candidate-actions", children: [
277
+ !resolved && onAdopt && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "primary", disabled: chosen.size === 0, loading: pending, onClick: () => void adopt(), children: fillLabel(chosen.size === 1 ? labels.adoptSelectedOne : labels.adoptSelectedMany, { count: chosen.size }) }),
278
+ !resolved && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: () => {
279
+ if (!pending) onDecision(candidate.id, "dismiss");
280
+ }, children: labels.dismiss }),
281
+ /* @__PURE__ */ jsx("span", { className: cx("uix-match-review__result", message?.failed && "uix-match-review__result--failed"), role: "status", children: message?.text })
282
+ ] })
283
+ ] });
284
+ }
159
285
  export {
160
286
  DEFAULT_MATCH_REVIEW_LABELS,
161
287
  MatchReview
@@ -4,6 +4,7 @@ import { useEffect, useId, useState } from "react";
4
4
  import { cx } from "../cx.js";
5
5
  import { clampMetricValue, parseMetricValue, stepMetricValue } from "../metric-model.js";
6
6
  import { Input } from "./Input.js";
7
+ import { MinusIcon, PlusIcon } from "../icons/components.js";
7
8
  const defaultMetricFormat = (input) => input == null ? "" : String(input);
8
9
  function MetricInput({
9
10
  value,
@@ -83,8 +84,8 @@ function MetricInput({
83
84
  ),
84
85
  unit && /* @__PURE__ */ jsx("span", { className: "uix-metric-input__unit", "aria-hidden": "true", children: unit }),
85
86
  /* @__PURE__ */ jsxs("span", { className: "uix-metric-input__steps", children: [
86
- /* @__PURE__ */ jsx("button", { type: "button", className: "uix-metric-input__step", onClick: () => stepBy(1), disabled: disabled || readOnly, "aria-label": `Increase ${label}`, children: "+" }),
87
- /* @__PURE__ */ jsx("button", { type: "button", className: "uix-metric-input__step", onClick: () => stepBy(-1), disabled: disabled || readOnly, "aria-label": `Decrease ${label}`, children: "\u2212" })
87
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-metric-input__step", onClick: () => stepBy(1), disabled: disabled || readOnly, "aria-label": `Increase ${label}`, children: /* @__PURE__ */ jsx(PlusIcon, {}) }),
88
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-metric-input__step", onClick: () => stepBy(-1), disabled: disabled || readOnly, "aria-label": `Decrease ${label}`, children: /* @__PURE__ */ jsx(MinusIcon, {}) })
88
89
  ] })
89
90
  ] }),
90
91
  unit && /* @__PURE__ */ jsxs("span", { className: "uix-visually-hidden", children: [
@@ -4,6 +4,7 @@ import { DialogCloseEvent, DiscardPromptLabels } from './DiscardPrompt.js';
4
4
  import './Button.js';
5
5
  import '../link.js';
6
6
  import './Kbd.js';
7
+ import './ButtonFeedback.js';
7
8
 
8
9
  interface ModalProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
9
10
  open: boolean;
@@ -34,6 +35,7 @@ interface ModalProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
34
35
  */
35
36
  instant?: boolean;
36
37
  }
37
- declare function Modal({ open, onClose, dirty, pending, discardLabels, title, children, footer, className, closeLabel: closeLabelProp, role, instant, onKeyDown, ...rest }: ModalProps): react.JSX.Element;
38
+ /** A modal `<dialog>`; the ref reaches it (HAR-1831). */
39
+ declare const Modal: react.ForwardRefExoticComponent<ModalProps & react.RefAttributes<HTMLDialogElement>>;
38
40
 
39
41
  export { Modal, type ModalProps };
@@ -1,11 +1,11 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useId, useRef } from "react";
3
+ import { forwardRef, useCallback, useId, useRef } from "react";
4
4
  import { cx } from "../cx.js";
5
5
  import { useUixLabels } from "../labels-context.js";
6
6
  import { useGuardedDialog } from "./DiscardPrompt.js";
7
7
  const CloseIcon = () => /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) });
8
- function Modal({
8
+ const Modal = forwardRef(function Modal2({
9
9
  open,
10
10
  onClose,
11
11
  dirty = false,
@@ -20,16 +20,17 @@ function Modal({
20
20
  instant = false,
21
21
  onKeyDown,
22
22
  ...rest
23
- }) {
23
+ }, ref) {
24
24
  const uixLabels = useUixLabels();
25
25
  const closeLabel = closeLabelProp ?? uixLabels.modal?.close ?? "Close dialog";
26
26
  const closeRef = useRef(null);
27
27
  const guard = useGuardedDialog({ open, onClose, dirty, pending, labels: discardLabels, closeRef });
28
28
  const titleId = useId();
29
+ const dialogRef = useMergedRef(guard.ref, ref);
29
30
  return /* @__PURE__ */ jsxs(
30
31
  "dialog",
31
32
  {
32
- ref: guard.ref,
33
+ ref: dialogRef,
33
34
  className: cx("uix-dialog", className),
34
35
  role,
35
36
  "aria-labelledby": title != null ? titleId : void 0,
@@ -65,6 +66,14 @@ function Modal({
65
66
  ]
66
67
  }
67
68
  );
69
+ });
70
+ Modal.displayName = "Modal";
71
+ function useMergedRef(own, forwarded) {
72
+ return useCallback((el) => {
73
+ own.current = el;
74
+ if (typeof forwarded === "function") forwarded(el);
75
+ else if (forwarded) forwarded.current = el;
76
+ }, [own, forwarded]);
68
77
  }
69
78
  export {
70
79
  Modal
@@ -20,6 +20,11 @@ interface NavFavouritesLabels {
20
20
  readonly remove: string;
21
21
  /** Per-row overflow-trigger accessible-name template with `{label}`. Defaults to "{label} favourite options". */
22
22
  readonly options?: string;
23
+ /**
24
+ * Polite announcement after a move: `{label}`, `{position}`, `{count}`. Defaults to
25
+ * "{label} moved to position {position} of {count}" (it was English only before HAR-1375).
26
+ */
27
+ readonly moved?: string;
23
28
  }
24
29
  interface NavFavouritesProps {
25
30
  /** The already-resolved favourites, in display order. */
@@ -2,6 +2,8 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
4
4
  import { cx } from "../cx.js";
5
+ import { fillLabel } from "../fill-label.js";
6
+ import { placeId } from "../reorder.js";
5
7
  const ChevronDownIcon = () => /* @__PURE__ */ jsx(
6
8
  "svg",
7
9
  {
@@ -91,15 +93,13 @@ function NavFavourites({
91
93
  const idx = order.indexOf(id);
92
94
  const next = idx + dir;
93
95
  if (idx < 0 || next < 0 || next >= order.length) return;
94
- const reordered = [...order];
95
- const [it] = reordered.splice(idx, 1);
96
- if (it === void 0) return;
97
- reordered.splice(next, 0, it);
96
+ const reordered = placeId(order, id, next);
97
+ if (reordered === order) return;
98
98
  focusAfterRef.current = id;
99
99
  const moved = items.find((i) => i.id === id);
100
- if (moved) setAnnouncement(`${moved.label} moved to position ${next + 1} of ${order.length}`);
101
- onReorder(reordered);
102
- }, [onReorder, order]);
100
+ if (moved) setAnnouncement(fillLabel(labels.moved ?? "{label} moved to position {position} of {count}", { label: moved.label, position: next + 1, count: order.length }));
101
+ onReorder([...reordered]);
102
+ }, [onReorder, order, items, labels.moved]);
103
103
  const remove = useCallback((id) => {
104
104
  const idx = order.indexOf(id);
105
105
  const neighbour = order[idx + 1] ?? order[idx - 1] ?? null;
@@ -0,0 +1,33 @@
1
+ import * as react from 'react';
2
+ import { InputHTMLAttributes, AriaAttributes, CSSProperties } from 'react';
3
+
4
+ interface OtpInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'defaultValue' | 'onChange' | 'type' | 'size' | 'maxLength' | 'pattern' | 'inputMode'> {
5
+ /** How many characters the code has. Default 6. */
6
+ length?: number;
7
+ /** Controlled value: the characters typed so far. */
8
+ value?: string;
9
+ defaultValue?: string;
10
+ /** Every change, with the cleaned code (only allowed characters, at most `length`). */
11
+ onValueChange?: (value: string) => void;
12
+ /** Called once each time the code reaches `length` characters. */
13
+ onComplete?: (code: string) => void;
14
+ /** `numeric` (default): digits, with the phone's number pad. `alphanumeric`: letters and digits, upper-cased. */
15
+ mode?: 'numeric' | 'alphanumeric';
16
+ /** Draw a gap after every n cells (3 → "123 456"). Visual only: the value has no separator. */
17
+ groupSize?: number;
18
+ invalid?: boolean;
19
+ 'aria-invalid'?: AriaAttributes['aria-invalid'];
20
+ className?: string;
21
+ style?: CSSProperties;
22
+ }
23
+ /**
24
+ * A one-time code (HAR-1376): ONE native input, so screen readers, the phone's code autofill
25
+ * (`autoComplete="one-time-code"`) and password managers see a single field, drawn as `length`
26
+ * cells. Pasting "123 456" or "123-456" keeps the characters; typing past the end is ignored;
27
+ * Backspace deletes the last one. `onComplete(code)` runs when the last character arrives. Put it in
28
+ * a `Field` for its label, hint and error (the cells turn red with `aria-invalid`). `name` submits
29
+ * the code. `ref` is the input.
30
+ */
31
+ declare const OtpInput: react.ForwardRefExoticComponent<OtpInputProps & react.RefAttributes<HTMLInputElement>>;
32
+
33
+ export { OtpInput, type OtpInputProps };