@tensor_1/react 2.47.0 → 2.49.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 +540 -2
  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/Chip.d.ts +7 -3
  31. package/dist/components/Chip.js +2 -2
  32. package/dist/components/CodeDisplay.d.ts +65 -0
  33. package/dist/components/CodeDisplay.js +124 -0
  34. package/dist/components/Combobox.d.ts +27 -6
  35. package/dist/components/Combobox.js +195 -58
  36. package/dist/components/CommandPalette.d.ts +36 -1
  37. package/dist/components/CommandPalette.js +254 -2
  38. package/dist/components/ContactCard.d.ts +66 -0
  39. package/dist/components/ContactCard.js +74 -0
  40. package/dist/components/Count.d.ts +24 -0
  41. package/dist/components/Count.js +32 -0
  42. package/dist/components/DashboardCanvas.js +14 -0
  43. package/dist/components/DataWorkspace.d.ts +167 -0
  44. package/dist/components/DataWorkspace.js +380 -0
  45. package/dist/components/DateFieldParts.d.ts +2 -1
  46. package/dist/components/DateFieldParts.js +7 -0
  47. package/dist/components/DatePicker.d.ts +9 -2
  48. package/dist/components/DatePicker.js +67 -28
  49. package/dist/components/DateRangePicker.d.ts +12 -2
  50. package/dist/components/DateRangePicker.js +13 -12
  51. package/dist/components/DetailPage.d.ts +7 -1
  52. package/dist/components/DetailPage.js +2 -1
  53. package/dist/components/Dialogs.d.ts +71 -10
  54. package/dist/components/Dialogs.js +130 -30
  55. package/dist/components/DiffViewer.d.ts +23 -3
  56. package/dist/components/DiffViewer.js +26 -12
  57. package/dist/components/DiscardPrompt.js +16 -0
  58. package/dist/components/DisplayPreferences.d.ts +83 -0
  59. package/dist/components/DisplayPreferences.js +185 -0
  60. package/dist/components/EditorialHome.d.ts +23 -3
  61. package/dist/components/EditorialHome.js +16 -11
  62. package/dist/components/EntityPicker.d.ts +20 -3
  63. package/dist/components/EntityPicker.js +32 -13
  64. package/dist/components/FilterEditor.d.ts +18 -4
  65. package/dist/components/FilterEditor.js +141 -19
  66. package/dist/components/FocusOnMount.d.ts +282 -0
  67. package/dist/components/FocusOnMount.js +12 -0
  68. package/dist/components/InfoTip.js +11 -12
  69. package/dist/components/InlineEdit.d.ts +87 -20
  70. package/dist/components/InlineEdit.js +197 -54
  71. package/dist/components/Input.d.ts +12 -2
  72. package/dist/components/Input.js +5 -3
  73. package/dist/components/Kanban.d.ts +14 -3
  74. package/dist/components/Kanban.js +9 -4
  75. package/dist/components/Kbd.d.ts +6 -1
  76. package/dist/components/Kbd.js +6 -6
  77. package/dist/components/Lightbox.d.ts +41 -5
  78. package/dist/components/Lightbox.js +95 -9
  79. package/dist/components/MatchReview.d.ts +40 -1
  80. package/dist/components/MatchReview.js +130 -4
  81. package/dist/components/MetricInput.js +3 -2
  82. package/dist/components/Modal.d.ts +2 -1
  83. package/dist/components/Modal.js +13 -4
  84. package/dist/components/MotionList.d.ts +303 -0
  85. package/dist/components/MotionList.js +175 -0
  86. package/dist/components/NavFavourites.d.ts +5 -0
  87. package/dist/components/NavFavourites.js +7 -7
  88. package/dist/components/OtpInput.d.ts +33 -0
  89. package/dist/components/OtpInput.js +123 -0
  90. package/dist/components/PageHeader.d.ts +7 -1
  91. package/dist/components/PageHeader.js +2 -2
  92. package/dist/components/Pagination.js +6 -5
  93. package/dist/components/PasswordInput.d.ts +34 -0
  94. package/dist/components/PasswordInput.js +101 -0
  95. package/dist/components/Peek.js +3 -3
  96. package/dist/components/Popover.js +11 -0
  97. package/dist/components/PrintLayout.d.ts +63 -0
  98. package/dist/components/PrintLayout.js +114 -0
  99. package/dist/components/QrCode.d.ts +47 -0
  100. package/dist/components/QrCode.js +55 -0
  101. package/dist/components/QrScanner.d.ts +95 -0
  102. package/dist/components/QrScanner.js +266 -0
  103. package/dist/components/RelationshipGraph.d.ts +32 -1
  104. package/dist/components/RelationshipGraph.js +22 -6
  105. package/dist/components/RelationshipGraphLayered.d.ts +1 -1
  106. package/dist/components/RelationshipGraphLayered.js +268 -118
  107. package/dist/components/RelationshipGraphList.js +33 -6
  108. package/dist/components/RuleBuilder.js +39 -7
  109. package/dist/components/SchedulingAgenda.d.ts +3 -1
  110. package/dist/components/SchedulingCalendar.d.ts +3 -1
  111. package/dist/components/SchedulingCalendar.js +43 -4
  112. package/dist/components/SchedulingResourceDay.d.ts +57 -0
  113. package/dist/components/SchedulingResourceDay.js +287 -0
  114. package/dist/components/SchedulingTimeGrid.d.ts +3 -1
  115. package/dist/components/SchedulingTimeline.d.ts +3 -1
  116. package/dist/components/SearchSuggest.d.ts +23 -3
  117. package/dist/components/SearchSuggest.js +56 -18
  118. package/dist/components/Segmented.d.ts +39 -4
  119. package/dist/components/Segmented.js +39 -5
  120. package/dist/components/Select.d.ts +3 -2
  121. package/dist/components/Select.js +7 -1
  122. package/dist/components/ShortcutHint.d.ts +11 -10
  123. package/dist/components/ShortcutHint.js +44 -38
  124. package/dist/components/Sidebar.d.ts +36 -4
  125. package/dist/components/Sidebar.js +37 -12
  126. package/dist/components/SortableList.d.ts +45 -0
  127. package/dist/components/SortableList.js +147 -0
  128. package/dist/components/Stat.d.ts +28 -3
  129. package/dist/components/Stat.js +13 -3
  130. package/dist/components/States.d.ts +33 -1
  131. package/dist/components/States.js +43 -1
  132. package/dist/components/StatusPill.d.ts +14 -3
  133. package/dist/components/StatusPill.js +11 -3
  134. package/dist/components/Steps.js +2 -1
  135. package/dist/components/Table.d.ts +9 -2
  136. package/dist/components/Table.js +4 -2
  137. package/dist/components/TableControls.js +18 -58
  138. package/dist/components/TableOverflowProbe.d.ts +12 -0
  139. package/dist/components/TableOverflowProbe.js +27 -0
  140. package/dist/components/Tabs.d.ts +2 -1
  141. package/dist/components/Tabs.js +10 -2
  142. package/dist/components/TagInput.d.ts +67 -0
  143. package/dist/components/TagInput.js +342 -0
  144. package/dist/components/Textarea.d.ts +5 -0
  145. package/dist/components/Textarea.js +2 -2
  146. package/dist/components/TimePicker.d.ts +80 -0
  147. package/dist/components/TimePicker.js +347 -0
  148. package/dist/components/Toast.d.ts +1 -1
  149. package/dist/components/Toast.js +109 -4
  150. package/dist/components/Tooltip.d.ts +8 -4
  151. package/dist/components/Tooltip.js +86 -28
  152. package/dist/components/Tree.d.ts +8 -0
  153. package/dist/components/Tree.js +11 -5
  154. package/dist/display-preferences-model.d.ts +53 -0
  155. package/dist/display-preferences-model.js +64 -0
  156. package/dist/emoji.cjs +143 -49
  157. package/dist/filter-model.d.ts +59 -3
  158. package/dist/filter-model.js +67 -2
  159. package/dist/hooks/useAnchoredPosition.js +14 -2
  160. package/dist/hooks/useChangeHighlight.d.ts +33 -0
  161. package/dist/hooks/useChangeHighlight.js +44 -0
  162. package/dist/hooks/useDeferredReorder.d.ts +32 -0
  163. package/dist/hooks/useDeferredReorder.js +47 -0
  164. package/dist/hooks/useDialog.d.ts +5 -0
  165. package/dist/hooks/useDialog.js +17 -3
  166. package/dist/hooks/useKeyboardGrab.d.ts +58 -0
  167. package/dist/hooks/useKeyboardGrab.js +118 -0
  168. package/dist/hooks/useOptimisticValue.d.ts +51 -0
  169. package/dist/hooks/useOptimisticValue.js +61 -0
  170. package/dist/hooks/useReducedMotion.d.ts +4 -1
  171. package/dist/hooks/useReducedMotion.js +7 -1
  172. package/dist/hooks/useResolvedFilterLabels.d.ts +15 -0
  173. package/dist/hooks/useResolvedFilterLabels.js +34 -0
  174. package/dist/hooks/useSlidingIndicator.d.ts +18 -0
  175. package/dist/hooks/useSlidingIndicator.js +80 -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 +19687 -13423
  180. package/dist/index.d.cts +3419 -651
  181. package/dist/index.d.ts +48 -18
  182. package/dist/index.js +153 -9
  183. package/dist/labels-context.d.ts +93 -3
  184. package/dist/motion.d.ts +11 -0
  185. package/dist/motion.js +35 -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 +92 -3
  191. package/dist/relationship-graph-model.js +134 -15
  192. package/dist/reorder.d.ts +30 -0
  193. package/dist/reorder.js +74 -0
  194. package/dist/rich-text.cjs +46 -4
  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 +22 -3
  203. package/dist/toast-store.js +26 -9
  204. package/package.json +2 -1
@@ -8,7 +8,7 @@ function Kanban({ announcement, children, className, ...props }) {
8
8
  children
9
9
  ] });
10
10
  }
11
- function KanbanColumn({ title, count, children, className, ...props }) {
11
+ function KanbanColumn({ title, count, dropTarget, children, className, ...props }) {
12
12
  const headId = useId();
13
13
  const hasHead = title != null || count != null;
14
14
  return /* @__PURE__ */ jsxs("div", { className: cx("uix-kanban__col", className), ...props, children: [
@@ -16,7 +16,7 @@ function KanbanColumn({ title, count, children, className, ...props }) {
16
16
  title,
17
17
  count != null && /* @__PURE__ */ jsx("span", { className: "uix-kanban__count", children: count })
18
18
  ] }),
19
- /* @__PURE__ */ jsx("div", { className: "uix-kanban__body", role: "list", "aria-labelledby": hasHead ? headId : void 0, children })
19
+ /* @__PURE__ */ jsx("div", { className: "uix-kanban__body", role: "list", "aria-labelledby": hasHead ? headId : void 0, "data-uix-drop-target": dropTarget || void 0, children })
20
20
  ] });
21
21
  }
22
22
  const MOVE_KEYS = {
@@ -25,7 +25,7 @@ const MOVE_KEYS = {
25
25
  ArrowLeft: "left",
26
26
  ArrowRight: "right"
27
27
  };
28
- function KanbanCard({ title, meta, children, className, onMove, onKeyDown, ...props }) {
28
+ function KanbanCard({ title, meta, children, className, onMove, onKeyDown, dragging, ...props }) {
29
29
  const handleKeyDown = (e) => {
30
30
  onKeyDown?.(e);
31
31
  if (!onMove || !e.altKey || e.defaultPrevented) return;
@@ -42,6 +42,7 @@ function KanbanCard({ title, meta, children, className, onMove, onKeyDown, ...pr
42
42
  tabIndex: 0,
43
43
  className: cx("uix-kanban__card", className),
44
44
  onKeyDown: handleKeyDown,
45
+ "data-uix-drag": dragging ? "lifted" : void 0,
45
46
  ...props,
46
47
  children: [
47
48
  title && /* @__PURE__ */ jsx("div", { className: "uix-kanban__card-title", children: title }),
@@ -51,8 +52,12 @@ function KanbanCard({ title, meta, children, className, onMove, onKeyDown, ...pr
51
52
  }
52
53
  );
53
54
  }
55
+ function KanbanDropLine({ className, ...props }) {
56
+ return /* @__PURE__ */ jsx("div", { ...props, className: cx("uix-drop-line", className), "aria-hidden": "true" });
57
+ }
54
58
  export {
55
59
  Kanban,
56
60
  KanbanCard,
57
- KanbanColumn
61
+ KanbanColumn,
62
+ KanbanDropLine
58
63
  };
@@ -26,6 +26,11 @@ type KeyShortcut = readonly string[];
26
26
  declare function ariaKeyShortcuts(keys: KeyShortcut, platform: KbdPlatform): string;
27
27
  /** A `shortcut` prop that is key names rather than a node (HAR-2031). */
28
28
  declare const isKeyShortcut: (value: unknown) => value is KeyShortcut;
29
+ /**
30
+ * What a screen reader should hear for a shortcut: the key names joined with "+", e.g.
31
+ * "Control+K" or "Command+K" (HAR-2416). The same text `KbdCombo` speaks. Internal.
32
+ */
33
+ declare function spokenKeys(keys: KeyShortcut, platform: KbdPlatform, keyLabels?: Partial<Record<string, string>>): string;
29
34
  interface KbdComboProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
30
35
  /**
31
36
  * Keys in order, e.g. `['Mod', 'K']`. `Mod` is ⌘ on Apple and Ctrl elsewhere; `Shift`,
@@ -43,4 +48,4 @@ interface KbdComboProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'
43
48
  */
44
49
  declare function KbdCombo({ keys, platform, keyLabels, className, ...props }: KbdComboProps): react.JSX.Element;
45
50
 
46
- export { Kbd, KbdCombo, type KbdComboProps, type KbdPlatform, type KbdProps, type KeyShortcut, ariaKeyShortcuts, isKeyShortcut, useKbdPlatform };
51
+ export { Kbd, KbdCombo, type KbdComboProps, type KbdPlatform, type KbdProps, type KeyShortcut, ariaKeyShortcuts, isKeyShortcut, spokenKeys, useKbdPlatform };
@@ -48,15 +48,14 @@ function ariaKeyShortcuts(keys, platform) {
48
48
  return keys.map((key) => ARIA_KEYS[key]?.[platform] ?? (key.length === 1 ? key.toUpperCase() : key)).join("+");
49
49
  }
50
50
  const isKeyShortcut = (value) => Array.isArray(value) && value.length > 0 && value.every((key) => typeof key === "string");
51
+ function spokenKeys(keys, platform, keyLabels) {
52
+ return keys.map((key) => keyLabels?.[key] ?? KEYS[key]?.[platform]?.[1] ?? key).join("+");
53
+ }
51
54
  function KbdCombo({ keys, platform, keyLabels, className, ...props }) {
52
55
  const active = useKbdPlatform(platform);
53
- const parts = keys.map((key) => {
54
- const known = KEYS[key]?.[active];
55
- return { glyph: known?.[0] ?? key, spoken: keyLabels?.[key] ?? known?.[1] ?? key };
56
- });
57
56
  return /* @__PURE__ */ jsxs("span", { className: cx("uix-kbd-combo", className), ...props, children: [
58
- /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: parts.map((p) => p.spoken).join("+") }),
59
- /* @__PURE__ */ jsx("span", { className: "uix-kbd-combo__keys", "aria-hidden": "true", children: parts.map((p, i) => /* @__PURE__ */ jsx("kbd", { className: "uix-kbd", children: p.glyph }, `${keys[i]}-${i}`)) })
57
+ /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: spokenKeys(keys, active, keyLabels) }),
58
+ /* @__PURE__ */ jsx("span", { className: "uix-kbd-combo__keys", "aria-hidden": "true", children: keys.map((key, i) => /* @__PURE__ */ jsx("kbd", { className: "uix-kbd", children: KEYS[key]?.[active]?.[0] ?? key }, `${key}-${i}`)) })
60
59
  ] });
61
60
  }
62
61
  export {
@@ -64,5 +63,6 @@ export {
64
63
  KbdCombo,
65
64
  ariaKeyShortcuts,
66
65
  isKeyShortcut,
66
+ spokenKeys,
67
67
  useKbdPlatform
68
68
  };
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
2
+ import { ReactNode, KeyboardEvent } from 'react';
3
3
 
4
4
  interface LightboxItem {
5
5
  src: string;
@@ -22,6 +22,21 @@ interface LightboxLabels {
22
22
  zoomOut: string;
23
23
  }
24
24
  declare const DEFAULT_LIGHTBOX_LABELS: LightboxLabels;
25
+ /** What `renderOverlay` receives (HAR-1826). */
26
+ interface LightboxOverlayContext {
27
+ /** The item on show (0-based) and the item itself. */
28
+ index: number;
29
+ item: LightboxItem;
30
+ /** The rendered `<img>` or `<video>`; `naturalWidth` / `videoWidth` give the bitmap size. */
31
+ media: HTMLImageElement | HTMLVideoElement | null;
32
+ /**
33
+ * The overlay's size in CSS pixels. The overlay covers exactly the picture (not the letterbox
34
+ * around a fitted one), so a point at `(x, y)` in it is bitmap pixel
35
+ * `(x / width * naturalWidth, y / height * naturalHeight)`. 0 until the media has loaded.
36
+ */
37
+ width: number;
38
+ height: number;
39
+ }
25
40
  interface LightboxProps {
26
41
  items: readonly LightboxItem[];
27
42
  /** The item on show (0-based). */
@@ -33,13 +48,34 @@ interface LightboxProps {
33
48
  loop?: boolean;
34
49
  labels?: Partial<LightboxLabels>;
35
50
  className?: string;
51
+ /**
52
+ * Buttons for the toolbar beside Close (HAR-1826), e.g. "Hide area" and "Undo" on a viewer
53
+ * that redacts a screenshot. Tab order: these, then Close. They sit inside the dialog.
54
+ */
55
+ actions?: ReactNode;
56
+ /**
57
+ * Content laid over the picture, in the picture's own box: it moves and scales with the
58
+ * fit / actual-size toggle. Pointer events on it never swipe or page, so it can draw. Each
59
+ * child positions itself (`position: absolute` in `%`, or px of `width` / `height`).
60
+ */
61
+ renderOverlay?: (context: LightboxOverlayContext) => ReactNode;
62
+ /**
63
+ * Runs first for every key in the viewer. Call `event.preventDefault()` to claim the key:
64
+ * the viewer then neither pages (arrows, Home, End) nor closes (Escape). For a draft that
65
+ * arrow keys move and Escape cancels.
66
+ */
67
+ onKeyDown?: (event: KeyboardEvent<HTMLDialogElement>) => void;
68
+ /** Escape only: `event.preventDefault()` keeps the viewer open. */
69
+ onEscapeKeyDown?: (event: KeyboardEvent<HTMLDialogElement>) => void;
36
70
  }
37
71
  /**
38
72
  * A full-screen media viewer over `.uix-lightbox` (HAR-1367; TENSOR C12 `capture-viewer.tsx`,
39
73
  * MOTUS C-8 programme gallery): previous/next buttons, ←/→ and Home/End, swipe on touch, an
40
- * announced "n of total" counter, a caption, and a fit/actual-size toggle. Native `<dialog>`,
41
- * so focus stays inside and Escape closes; focus returns to the control that opened it.
74
+ * announced "n of total" counter (not for a single item), a caption, and a fit/actual-size
75
+ * toggle. Native `<dialog>`, so focus stays inside and Escape closes; focus returns to the
76
+ * control that opened it. An annotating viewer (HAR-1826) adds toolbar `actions`, draws in
77
+ * `renderOverlay`, and claims Escape and the arrows with `onKeyDown` while a draft is open.
42
78
  */
43
- declare function Lightbox({ items, index, onIndexChange, open, onClose, loop, labels: labelOverrides, className }: LightboxProps): react.JSX.Element;
79
+ declare function Lightbox({ items, index, onIndexChange, open, onClose, loop, labels: labelOverrides, className, actions, renderOverlay, onKeyDown: onKeyDownProp, onEscapeKeyDown, }: LightboxProps): react.JSX.Element;
44
80
 
45
- export { DEFAULT_LIGHTBOX_LABELS, Lightbox, type LightboxItem, type LightboxLabels, type LightboxProps };
81
+ export { DEFAULT_LIGHTBOX_LABELS, Lightbox, type LightboxItem, type LightboxLabels, type LightboxOverlayContext, type LightboxProps };
@@ -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: [
@@ -35,6 +35,7 @@ interface ModalProps extends Omit<HTMLAttributes<HTMLDialogElement>, 'title'> {
35
35
  */
36
36
  instant?: boolean;
37
37
  }
38
- 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>>;
39
40
 
40
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