@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
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { useEffect, useRef } from "react";
3
+ import { announcePolite, ensureAnnouncer } from "../announce.js";
4
+ function useChangeHighlight(value, { announce, disabled } = {}) {
5
+ const ref = useRef(null);
6
+ const previous = useRef(value);
7
+ const announces = announce !== void 0;
8
+ useEffect(() => {
9
+ if (announces) ensureAnnouncer(ref.current);
10
+ }, [announces]);
11
+ useEffect(() => {
12
+ if (Object.is(previous.current, value)) return;
13
+ previous.current = value;
14
+ const el = ref.current;
15
+ if (disabled || !el) return;
16
+ flashChanged(el);
17
+ if (announce) announcePolite(announce, el);
18
+ }, [value, announce, disabled]);
19
+ return ref;
20
+ }
21
+ const handlers = /* @__PURE__ */ new WeakMap();
22
+ function flashChanged(el) {
23
+ if (el.hasAttribute("data-uix-changed")) {
24
+ el.removeAttribute("data-uix-changed");
25
+ void el.offsetWidth;
26
+ }
27
+ el.setAttribute("data-uix-changed", "");
28
+ const previous = handlers.get(el);
29
+ if (previous) el.removeEventListener("animationend", previous);
30
+ const done = (event) => {
31
+ const target = event.target;
32
+ if (!event.animationName.startsWith("uix-changed")) return;
33
+ if (target !== el && target?.parentNode !== el) return;
34
+ el.removeAttribute("data-uix-changed");
35
+ el.removeEventListener("animationend", done);
36
+ handlers.delete(el);
37
+ };
38
+ handlers.set(el, done);
39
+ el.addEventListener("animationend", done);
40
+ }
41
+ export {
42
+ flashChanged,
43
+ useChangeHighlight
44
+ };
@@ -0,0 +1,32 @@
1
+ import { PointerEvent, FocusEvent } from 'react';
2
+
3
+ type Key = string | number;
4
+ interface UseDeferredReorderResult<T> {
5
+ /** The items to render: the new data, in the order that was showing while the list is held. */
6
+ items: T[];
7
+ /** A new order is waiting until the pointer and focus leave the list. Say so if it matters ("New order — move away to apply"). */
8
+ held: boolean;
9
+ /** Spread on the list element. */
10
+ listProps: {
11
+ onPointerEnter: (event: PointerEvent<HTMLElement>) => void;
12
+ onPointerLeave: (event: PointerEvent<HTMLElement>) => void;
13
+ onFocus: (event: FocusEvent<HTMLElement>) => void;
14
+ onBlur: (event: FocusEvent<HTMLElement>) => void;
15
+ };
16
+ }
17
+ /**
18
+ * Never re-sort under the pointer (HAR-2586, motion standard §2.5). While a mouse pointer is
19
+ * over the list, or keyboard focus is inside it, a new order from live data waits: rows keep
20
+ * their places (their content still updates), new rows join at the end, removed rows still go.
21
+ * When the pointer and keyboard focus leave, the new order applies — with `MotionList`, as a
22
+ * glide. A click alone does not hold the list (focus from a click is not keyboard focus), and
23
+ * a hold whose focused row disappeared lets go.
24
+ *
25
+ * ```tsx
26
+ * const queue = useDeferredReorder(liveRows, (row) => row.id);
27
+ * <MotionList {...queue.listProps} items={queue.items} getKey={(row) => row.id} renderItem={…} />
28
+ * ```
29
+ */
30
+ declare function useDeferredReorder<T>(items: readonly T[], getKey: (item: T) => Key): UseDeferredReorderResult<T>;
31
+
32
+ export { type UseDeferredReorderResult, useDeferredReorder };
@@ -0,0 +1,47 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ function useDeferredReorder(items, getKey) {
4
+ const [pointer, setPointer] = useState(false);
5
+ const [focus, setFocus] = useState(false);
6
+ const holding = pointer || focus;
7
+ const keyOf = useRef(getKey);
8
+ keyOf.current = getKey;
9
+ const shownOrder = useRef(items.map(getKey));
10
+ const list = useRef(null);
11
+ const shown = useMemo(() => {
12
+ const key = keyOf.current;
13
+ if (!holding) return [...items];
14
+ const byKey = new Map(items.map((item) => [key(item), item]));
15
+ const kept = shownOrder.current.filter((k) => byKey.has(k)).map((k) => byKey.get(k));
16
+ const keptKeys = new Set(shownOrder.current);
17
+ const added = items.filter((item) => !keptKeys.has(key(item)));
18
+ return [...kept, ...added];
19
+ }, [items, holding]);
20
+ shownOrder.current = shown.map((item) => keyOf.current(item));
21
+ const held = holding && shown.some((item, i) => keyOf.current(item) !== keyOf.current(items[i]));
22
+ useEffect(() => {
23
+ if (focus && list.current && !list.current.contains(document.activeElement)) setFocus(false);
24
+ }, [items, focus]);
25
+ const onPointerEnter = useCallback((event) => {
26
+ list.current = event.currentTarget;
27
+ if (event.pointerType === "mouse" || event.pointerType === "pen") setPointer(true);
28
+ }, []);
29
+ const onPointerLeave = useCallback(() => setPointer(false), []);
30
+ const onFocus = useCallback((event) => {
31
+ list.current = event.currentTarget;
32
+ const target = event.target;
33
+ let keyboard = true;
34
+ try {
35
+ keyboard = target.matches(":focus-visible");
36
+ } catch {
37
+ }
38
+ if (keyboard) setFocus(true);
39
+ }, []);
40
+ const onBlur = useCallback((event) => {
41
+ if (!event.currentTarget.contains(event.relatedTarget)) setFocus(false);
42
+ }, []);
43
+ return { items: shown, held, listProps: { onPointerEnter, onPointerLeave, onFocus, onBlur } };
44
+ }
45
+ export {
46
+ useDeferredReorder
47
+ };
@@ -2,6 +2,11 @@
2
2
  * `keepOpen` is asked when the browser closed the dialog on its own (Escape or a close request
3
3
  * nobody cancelled, a `method="dialog"` form). Returning `true` reopens it instead of calling
4
4
  * `onClose`: a form dialog with unsaved input or a save in flight (HAR-2029).
5
+ *
6
+ * The dialog opens and closes in a layout effect (HAR-1831). A dialog its caller unmounts while
7
+ * open (`{open && <ConfirmDialog …/>}`, or a page that navigates away) is closed in the commit,
8
+ * while it is still in the document, so the browser returns focus to the control that opened it;
9
+ * a passive cleanup ran after the element was removed, and focus fell to `<body>`.
5
10
  */
6
11
  declare function useDialog(open: boolean, onClose?: () => void, keepOpen?: () => boolean): React.RefObject<HTMLDialogElement>;
7
12
 
@@ -1,5 +1,7 @@
1
1
  "use client";
2
- import { useRef, useEffect } from "react";
2
+ import { useRef, useEffect, useLayoutEffect } from "react";
3
+ import { markKeyOpened, watchInputModality } from "../motion.js";
4
+ const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
3
5
  let lockCount = 0;
4
6
  let saved = null;
5
7
  function lockBodyScroll() {
@@ -37,12 +39,21 @@ function useDialog(open, onClose, keepOpen) {
37
39
  const keepOpenRef = useRef(keepOpen);
38
40
  keepOpenRef.current = keepOpen;
39
41
  useEffect(() => {
42
+ watchInputModality();
43
+ }, []);
44
+ useIsomorphicLayoutEffect(() => {
40
45
  const el = ref.current;
41
46
  if (!el || !open) {
42
- if (el && el.open) el.close();
47
+ if (el && el.open) {
48
+ markKeyOpened(el);
49
+ el.close();
50
+ }
43
51
  return;
44
52
  }
45
- if (!el.open) el.showModal();
53
+ if (!el.open) {
54
+ markKeyOpened(el);
55
+ el.showModal();
56
+ }
46
57
  lockBodyScroll();
47
58
  let locked = true;
48
59
  const handleClose = () => {
@@ -60,9 +71,12 @@ function useDialog(open, onClose, keepOpen) {
60
71
  }
61
72
  onCloseRef.current?.();
62
73
  };
74
+ const handleCancel = () => markKeyOpened(el);
63
75
  el.addEventListener("close", handleClose);
76
+ el.addEventListener("cancel", handleCancel);
64
77
  return () => {
65
78
  el.removeEventListener("close", handleClose);
79
+ el.removeEventListener("cancel", handleCancel);
66
80
  if (locked) {
67
81
  locked = false;
68
82
  unlockBodyScroll();
@@ -0,0 +1,58 @@
1
+ import { KeyboardEvent, FocusEvent } from 'react';
2
+
3
+ type KeyboardGrabDirection = 'up' | 'down' | 'left' | 'right';
4
+ /** The words `useKeyboardGrab` says. `{label}` is the item's name. */
5
+ interface KeyboardGrabLabels {
6
+ grabbed: string;
7
+ dropped: string;
8
+ cancelled: string;
9
+ }
10
+ declare const DEFAULT_KEYBOARD_GRAB_LABELS: KeyboardGrabLabels;
11
+ interface UseKeyboardGrabOptions {
12
+ /**
13
+ * The item's stable id. The picked-up state follows the id, so an item that a move puts in
14
+ * another list (a kanban card going to the next column) is still picked up, and focused, when
15
+ * it mounts there.
16
+ */
17
+ id: string;
18
+ /** The item's name in what is said. */
19
+ label: string;
20
+ /** Move the picked-up item one step; return what to say (e.g. "Position 3 of 8"), or nothing. */
21
+ onMove: (direction: KeyboardGrabDirection) => string | void;
22
+ /** The item was put down where it is now. */
23
+ onDrop?: () => void;
24
+ /** Escape, or focus leaving the item: put it back where it was picked up. */
25
+ onCancel?: () => void;
26
+ /** Overrides `UixLabelsProvider` `keyboardGrab`, then English. */
27
+ labels?: Partial<KeyboardGrabLabels>;
28
+ }
29
+ interface UseKeyboardGrabResult {
30
+ grabbed: boolean;
31
+ /** Spread on the focusable item: its ref, the key handling, and `data-uix-drag="grabbed"` while it is picked up. */
32
+ itemProps: {
33
+ ref: (el: HTMLElement | null) => void;
34
+ onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
35
+ onBlur: (event: FocusEvent<HTMLElement>) => void;
36
+ 'data-uix-drag'?: 'grabbed';
37
+ };
38
+ }
39
+ /**
40
+ * The keyboard path for anything a person drags (HAR-2590, WCAG 2.1.1 and 2.5.7): Space or Enter on
41
+ * the focused item picks it up (a dashed ring, `data-uix-drag="grabbed"`), the arrow keys move it
42
+ * one step at a time, Space or Enter puts it down, Escape or focus leaving puts it back. Each step
43
+ * is said through the polite live region. Moves from the keyboard are instant: no glide (§2.2).
44
+ * A held key does not toggle; keys on a control inside the item are left to it.
45
+ *
46
+ * The kit's own lists already have a keyboard path (SortableList's Move up / Move down and
47
+ * Alt+arrows, KanbanCard `onMove`, DashboardCanvas handles); this is for a draggable thing a
48
+ * product builds itself.
49
+ */
50
+ declare function useKeyboardGrab({ id, label, onMove, onDrop, onCancel, labels }: UseKeyboardGrabOptions): UseKeyboardGrabResult;
51
+ /**
52
+ * Let a dropped item land (HAR-2590): `data-uix-settle` for one short ease-out from the lifted
53
+ * scale to rest. Call it on the item's element once it is in its new place (after the commit
54
+ * that moved it). Under reduced motion there is no landing and nothing is left behind.
55
+ */
56
+ declare function settleDrop(el: HTMLElement | null | undefined): void;
57
+
58
+ export { DEFAULT_KEYBOARD_GRAB_LABELS, type KeyboardGrabDirection, type KeyboardGrabLabels, type UseKeyboardGrabOptions, type UseKeyboardGrabResult, settleDrop, useKeyboardGrab };
@@ -0,0 +1,118 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
3
+ import { announcePolite } from "../announce.js";
4
+ import { fillLabel } from "../fill-label.js";
5
+ import { useUixLabels } from "../labels-context.js";
6
+ const DEFAULT_KEYBOARD_GRAB_LABELS = {
7
+ grabbed: "{label} picked up. Use the arrow keys to move it, Space to drop it, Escape to cancel.",
8
+ dropped: "{label} dropped.",
9
+ cancelled: "Move cancelled. {label} is back where it was."
10
+ };
11
+ let grabbedId = null;
12
+ let refocusId = null;
13
+ const listeners = /* @__PURE__ */ new Set();
14
+ const setGrabbedId = (id) => {
15
+ grabbedId = id;
16
+ listeners.forEach((l) => l());
17
+ };
18
+ const subscribe = (l) => {
19
+ listeners.add(l);
20
+ return () => {
21
+ listeners.delete(l);
22
+ };
23
+ };
24
+ const DIRECTIONS = { ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right" };
25
+ function useKeyboardGrab({ id, label, onMove, onDrop, onCancel, labels }) {
26
+ const grabbed = useSyncExternalStore(subscribe, () => grabbedId === id, () => false);
27
+ const words = { ...DEFAULT_KEYBOARD_GRAB_LABELS, ...useUixLabels().keyboardGrab, ...labels };
28
+ const live = useRef({ onMove, onDrop, onCancel, words, label, id });
29
+ live.current = { onMove, onDrop, onCancel, words, label, id };
30
+ const element = useRef(null);
31
+ const say = (template) => announcePolite(fillLabel(template, { label: live.current.label }), element.current);
32
+ const cancel = useCallback(() => {
33
+ if (grabbedId !== live.current.id) return;
34
+ setGrabbedId(null);
35
+ live.current.onCancel?.();
36
+ announcePolite(fillLabel(live.current.words.cancelled, { label: live.current.label }), element.current);
37
+ }, []);
38
+ useEffect(() => {
39
+ if (!grabbed) return void 0;
40
+ const escape = (event) => {
41
+ if (event.key !== "Escape") return;
42
+ event.preventDefault();
43
+ event.stopPropagation();
44
+ cancel();
45
+ };
46
+ window.addEventListener("keydown", escape, true);
47
+ return () => window.removeEventListener("keydown", escape, true);
48
+ }, [grabbed, cancel]);
49
+ const ref = useCallback((el) => {
50
+ element.current = el;
51
+ if (el && refocusId === live.current.id) {
52
+ refocusId = null;
53
+ el.focus({ preventScroll: false });
54
+ }
55
+ }, []);
56
+ const onKeyDown = (event) => {
57
+ if (event.target !== event.currentTarget || event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) return;
58
+ if (event.key === " " || event.key === "Enter") {
59
+ event.preventDefault();
60
+ if (event.repeat) return;
61
+ if (grabbed) {
62
+ setGrabbedId(null);
63
+ say(live.current.words.dropped);
64
+ live.current.onDrop?.();
65
+ } else {
66
+ setGrabbedId(live.current.id);
67
+ say(live.current.words.grabbed);
68
+ }
69
+ return;
70
+ }
71
+ if (!grabbed) return;
72
+ const direction = DIRECTIONS[event.key];
73
+ if (!direction) return;
74
+ event.preventDefault();
75
+ refocusId = live.current.id;
76
+ const said = live.current.onMove(direction);
77
+ if (said) announcePolite(said, element.current);
78
+ queueMicrotask(() => {
79
+ if (refocusId === live.current.id && element.current?.isConnected) refocusId = null;
80
+ });
81
+ };
82
+ const onBlur = (event) => {
83
+ if (!grabbed) return;
84
+ const to = event.relatedTarget;
85
+ if (to && event.currentTarget.contains(to)) return;
86
+ if (refocusId === live.current.id) return;
87
+ cancel();
88
+ };
89
+ return {
90
+ grabbed,
91
+ itemProps: { ref, onKeyDown, onBlur, ...grabbed ? { "data-uix-drag": "grabbed" } : {} }
92
+ };
93
+ }
94
+ function settleDrop(el) {
95
+ if (!el) return;
96
+ if (el.hasAttribute("data-uix-settle")) {
97
+ el.removeAttribute("data-uix-settle");
98
+ void el.offsetWidth;
99
+ }
100
+ el.setAttribute("data-uix-settle", "");
101
+ if (typeof getComputedStyle === "function" && getComputedStyle(el).animationName === "none") {
102
+ el.removeAttribute("data-uix-settle");
103
+ return;
104
+ }
105
+ const done = (event) => {
106
+ if (event.target !== el) return;
107
+ el.removeAttribute("data-uix-settle");
108
+ el.removeEventListener("animationend", done);
109
+ el.removeEventListener("animationcancel", done);
110
+ };
111
+ el.addEventListener("animationend", done);
112
+ el.addEventListener("animationcancel", done);
113
+ }
114
+ export {
115
+ DEFAULT_KEYBOARD_GRAB_LABELS,
116
+ settleDrop,
117
+ useKeyboardGrab
118
+ };
@@ -0,0 +1,51 @@
1
+ interface OptimisticRollback<T> {
2
+ /** The value the person asked for. */
3
+ attempted: T;
4
+ /** The value shown again. */
5
+ restored: T;
6
+ error: unknown;
7
+ }
8
+ interface UseOptimisticValueOptions<T> {
9
+ /** Called when a change fails and the value is put back, e.g. to show a toast that says what was restored. */
10
+ onRollback?: (rollback: OptimisticRollback<T>) => void;
11
+ }
12
+ interface UseOptimisticValueResult<T> {
13
+ /** What to show: the person's change at once, the server's value otherwise. */
14
+ value: T;
15
+ /** Show `next` now and save it; resolves when the save settles (it never rejects). */
16
+ set: (next: T) => Promise<void>;
17
+ /** A save is in flight. */
18
+ pending: boolean;
19
+ /** The last rollback, until the next change; `null` when the last change saved. */
20
+ rollback: OptimisticRollback<T> | null;
21
+ /**
22
+ * Counts rollbacks. Pass it to `ChangeHighlight value={rollbacks}` (or `useChangeHighlight`)
23
+ * so the restored element flashes; the person's own change does not.
24
+ */
25
+ rollbacks: number;
26
+ }
27
+ /**
28
+ * An optimistic value with a visible rollback (HAR-2583, motion standard §2.6). Only for
29
+ * reversible, personal, nearly-always-successful actions: star, pin, a reaction, mark read, a
30
+ * display preference, the order of one's own saved views. Never for anything the server can
31
+ * refuse or an auditor reads (a state transition, an assignment, an approval, a priority):
32
+ * those stay pessimistic, with `Button loading` and `result`.
33
+ *
34
+ * `set(next)` shows `next` at once and calls `commit(next)`. If the commit fails, the last
35
+ * confirmed value comes back, `rollbacks` counts up (flash the element with it), `rollback`
36
+ * says what happened, and `onRollback` runs so the product can say it in words. When changes
37
+ * overlap, only the latest one's outcome counts. While nothing is pending, `value` follows
38
+ * `serverValue` when it changes; a change that saved stays shown until then.
39
+ *
40
+ * ```tsx
41
+ * const star = useOptimisticValue(view.starred, (next) => api.star(view.id, next), {
42
+ * onRollback: () => toast.error(t('starNotSaved')),
43
+ * });
44
+ * <ChangeHighlight value={star.rollbacks} variant="ring" announce={star.rollback ? t('starRestored') : undefined}>
45
+ * <StarButton starred={star.value} onToggle={() => star.set(!star.value)} />
46
+ * </ChangeHighlight>
47
+ * ```
48
+ */
49
+ declare function useOptimisticValue<T>(serverValue: T, commit: (next: T) => Promise<unknown>, { onRollback }?: UseOptimisticValueOptions<T>): UseOptimisticValueResult<T>;
50
+
51
+ export { type OptimisticRollback, type UseOptimisticValueOptions, type UseOptimisticValueResult, useOptimisticValue };
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ function useOptimisticValue(serverValue, commit, { onRollback } = {}) {
4
+ const [shown, setShown] = useState(serverValue);
5
+ const [pending, setPending] = useState(false);
6
+ const [rollback, setRollback] = useState(null);
7
+ const [rollbacks, setRollbacks] = useState(0);
8
+ const confirmed = useRef(serverValue);
9
+ const latest = useRef(0);
10
+ const confirmedId = useRef(0);
11
+ const mounted = useRef(true);
12
+ const commitRef = useRef(commit);
13
+ const onRollbackRef = useRef(onRollback);
14
+ commitRef.current = commit;
15
+ onRollbackRef.current = onRollback;
16
+ useEffect(() => {
17
+ mounted.current = true;
18
+ return () => {
19
+ mounted.current = false;
20
+ };
21
+ }, []);
22
+ const pendingRef = useRef(false);
23
+ pendingRef.current = pending;
24
+ const firstServer = useRef(true);
25
+ useEffect(() => {
26
+ if (firstServer.current) {
27
+ firstServer.current = false;
28
+ return;
29
+ }
30
+ confirmed.current = serverValue;
31
+ if (!pendingRef.current) setShown(serverValue);
32
+ }, [serverValue]);
33
+ const set = useCallback(async (next) => {
34
+ const id = ++latest.current;
35
+ setShown(next);
36
+ setPending(true);
37
+ setRollback(null);
38
+ try {
39
+ await commitRef.current(next);
40
+ if (id > confirmedId.current) {
41
+ confirmedId.current = id;
42
+ confirmed.current = next;
43
+ }
44
+ if (!mounted.current || id !== latest.current) return;
45
+ setPending(false);
46
+ } catch (error) {
47
+ if (!mounted.current || id !== latest.current) return;
48
+ const restored = confirmed.current;
49
+ const event = { attempted: next, restored, error };
50
+ setShown(restored);
51
+ setPending(false);
52
+ setRollback(event);
53
+ setRollbacks((n) => n + 1);
54
+ onRollbackRef.current?.(event);
55
+ }
56
+ }, []);
57
+ return { value: shown, set, pending, rollback, rollbacks };
58
+ }
59
+ export {
60
+ useOptimisticValue
61
+ };
@@ -1,4 +1,7 @@
1
- /** Non-hook read for event handlers and effects. `false` where there is no `matchMedia` (SSR, tests). */
1
+ /**
2
+ * Non-hook read for event handlers and effects: the OS setting, or the in-app preference
3
+ * `<html data-motion="reduce">` (HAR-1372). `false` where there is no `matchMedia` (SSR, tests).
4
+ */
2
5
  declare function prefersReducedMotion(): boolean;
3
6
  /** True while the user asks for reduced motion; re-renders when the setting changes. `false` on the server. */
4
7
  declare function useReducedMotion(): boolean;
@@ -6,9 +6,15 @@ function subscribe(onChange) {
6
6
  };
7
7
  const mql = matchMedia(QUERY);
8
8
  mql.addEventListener("change", onChange);
9
- return () => mql.removeEventListener("change", onChange);
9
+ const observer = typeof MutationObserver === "function" && typeof document !== "undefined" ? new MutationObserver(onChange) : null;
10
+ observer?.observe(document.documentElement, { attributes: true, attributeFilter: ["data-motion"] });
11
+ return () => {
12
+ mql.removeEventListener("change", onChange);
13
+ observer?.disconnect();
14
+ };
10
15
  }
11
16
  function prefersReducedMotion() {
17
+ if (typeof document !== "undefined" && document.documentElement.getAttribute("data-motion") === "reduce") return true;
12
18
  return typeof matchMedia !== "undefined" && matchMedia(QUERY).matches;
13
19
  }
14
20
  function useReducedMotion() {
@@ -0,0 +1,15 @@
1
+ import { FilterField, FilterValue } from '../filter-model.js';
2
+
3
+ /**
4
+ * A filter chip's field and value with the labels of ids restored from a URL or a saved view
5
+ * (PR #168 review: a chip read "Assignee: usr_8f3a"). A `reference` value's choices without a
6
+ * label, and an `enum` value's ids that are not among `field.options`, are looked up once with
7
+ * `field.resolveLabels`. Until the answer arrives, and when the lookup fails, the ids show.
8
+ * Internal: DataWorkspace and AuditLog summarise their chips through it.
9
+ */
10
+ declare function useResolvedFilterLabels(field: FilterField, value: FilterValue | undefined): {
11
+ field: FilterField;
12
+ value: FilterValue | undefined;
13
+ };
14
+
15
+ export { useResolvedFilterLabels };
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { isUnresolvedChoice } from "../filter-model.js";
4
+ function useResolvedFilterLabels(field, value) {
5
+ const [known, setKnown] = useState(() => /* @__PURE__ */ new Map());
6
+ const asked = useRef(/* @__PURE__ */ new Set());
7
+ const resolver = useRef(field.resolveLabels);
8
+ resolver.current = field.resolveLabels;
9
+ const optionIds = new Set((field.options ?? []).map((o) => o.value));
10
+ const missing = value?.kind === "reference" ? value.values.filter(isUnresolvedChoice).map((v) => v.value) : value?.kind === "enum" ? value.values.filter((v) => !optionIds.has(v)) : [];
11
+ const key = missing.filter((id) => !known.has(id) && !asked.current.has(id)).join("\0");
12
+ useEffect(() => {
13
+ if (!key || !resolver.current) return;
14
+ const ids = key.split("\0");
15
+ for (const id of ids) asked.current.add(id);
16
+ resolver.current(ids).then(
17
+ (choices) => setKnown((prev) => new Map([...prev, ...choices.map((c) => [c.value, c.label])])),
18
+ () => {
19
+ }
20
+ );
21
+ }, [key]);
22
+ if (known.size === 0 || !value) return { field, value };
23
+ if (value.kind === "reference") {
24
+ return { field, value: { kind: "reference", values: value.values.map((v) => isUnresolvedChoice(v) && known.has(v.value) ? { value: v.value, label: known.get(v.value) } : v) } };
25
+ }
26
+ if (value.kind === "enum") {
27
+ const extra = value.values.filter((v) => !optionIds.has(v) && known.has(v)).map((v) => ({ value: v, label: known.get(v) }));
28
+ if (extra.length) return { field: { ...field, options: [...field.options ?? [], ...extra] }, value };
29
+ }
30
+ return { field, value };
31
+ }
32
+ export {
33
+ useResolvedFilterLabels
34
+ };
@@ -0,0 +1,18 @@
1
+ import { RefObject } from 'react';
2
+
3
+ /**
4
+ * Slides the selection mark from the old item to the new one when a pointer changes it
5
+ * (HAR-2589): the selected tab's underline, the pill, the lifted segment. At rest every item
6
+ * keeps its own selected look (server HTML, forced colours, wrapped rows: nothing to keep in
7
+ * place); only during the slide does one indicator (`indicatorRef`, a child of `containerRef`)
8
+ * carry the mark — the container has `data-uix-indicator` and the new item's own mark steps
9
+ * aside — and at the end of the slide it hands the mark back and hides. The indicator is sized
10
+ * to the new item and played back from the old one with a transform (FLIP): nothing animates
11
+ * width or position.
12
+ *
13
+ * - A change from the keyboard jumps: arrow keys are the 100×-a-day path (§2.2).
14
+ * - Reduced motion jumps.
15
+ */
16
+ declare function useSlidingIndicator(containerRef: RefObject<HTMLElement | null>, indicatorRef: RefObject<HTMLElement | null>, selectedSelector: string): void;
17
+
18
+ export { useSlidingIndicator };
@@ -0,0 +1,80 @@
1
+ "use client";
2
+ import { useEffect, useLayoutEffect, useRef } from "react";
3
+ import { prefersReducedMotion } from "./useReducedMotion.js";
4
+ const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
5
+ const SLIDE_FALLBACK_MS = 1e3;
6
+ let lastInput = "pointer";
7
+ let listening = false;
8
+ function trackInput() {
9
+ if (listening || typeof document === "undefined") return;
10
+ listening = true;
11
+ document.addEventListener("keydown", () => {
12
+ lastInput = "keyboard";
13
+ }, true);
14
+ document.addEventListener("pointerdown", () => {
15
+ lastInput = "pointer";
16
+ }, true);
17
+ }
18
+ function useSlidingIndicator(containerRef, indicatorRef, selectedSelector) {
19
+ const last = useRef(null);
20
+ const finish = useRef(null);
21
+ useEffect(() => () => finish.current?.(), []);
22
+ useIsomorphicLayoutEffect(() => {
23
+ trackInput();
24
+ const container = containerRef.current;
25
+ const indicator = indicatorRef.current;
26
+ const selected = container?.querySelector(selectedSelector) ?? null;
27
+ if (!container || !selected || selected.offsetParent !== container) {
28
+ finish.current?.();
29
+ last.current = null;
30
+ return;
31
+ }
32
+ const box = { x: selected.offsetLeft, y: selected.offsetTop, w: selected.offsetWidth, h: selected.offsetHeight };
33
+ const before = last.current;
34
+ last.current = { el: selected, box };
35
+ if (!before || before.el === selected) return;
36
+ finish.current?.();
37
+ if (!indicator || lastInput !== "pointer" || prefersReducedMotion() || !box.w || !box.h) return;
38
+ slide(container, indicator, selected, before.box, box, finish);
39
+ });
40
+ }
41
+ function slide(container, indicator, target, from, to, finish) {
42
+ indicator.hidden = false;
43
+ indicator.style.inlineSize = `${to.w}px`;
44
+ indicator.style.blockSize = `${to.h}px`;
45
+ indicator.style.left = "0";
46
+ indicator.style.top = "0";
47
+ indicator.style.translate = `${to.x}px ${to.y}px`;
48
+ indicator.style.transform = `translate(${from.x - to.x}px, ${from.y - to.y}px) scale(${from.w / to.w}, ${from.h / to.h})`;
49
+ container.setAttribute("data-uix-indicator", "");
50
+ void indicator.offsetWidth;
51
+ indicator.setAttribute("data-moving", "");
52
+ indicator.style.transform = "";
53
+ let timer;
54
+ const size = () => `${container.offsetWidth} ${container.offsetHeight} ${target.offsetWidth} ${target.offsetHeight}`;
55
+ const startSize = size();
56
+ const resized = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
57
+ if (size() !== startSize) end();
58
+ });
59
+ resized?.observe(container);
60
+ resized?.observe(target);
61
+ const end = () => {
62
+ clearTimeout(timer);
63
+ resized?.disconnect();
64
+ indicator.removeEventListener("transitionend", onEnd);
65
+ indicator.getAnimations?.().forEach((a) => a.cancel());
66
+ indicator.removeAttribute("data-moving");
67
+ indicator.hidden = true;
68
+ container.removeAttribute("data-uix-indicator");
69
+ if (finish.current === end) finish.current = null;
70
+ };
71
+ const onEnd = (event) => {
72
+ if (event.target === indicator && event.propertyName === "transform") end();
73
+ };
74
+ indicator.addEventListener("transitionend", onEnd);
75
+ timer = setTimeout(end, SLIDE_FALLBACK_MS);
76
+ finish.current = end;
77
+ }
78
+ export {
79
+ useSlidingIndicator
80
+ };