@tensor_1/react 2.48.0 → 2.49.1

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 (48) hide show
  1. package/README.md +70 -1
  2. package/dist/components/Chip.d.ts +7 -3
  3. package/dist/components/Chip.js +2 -2
  4. package/dist/components/DashboardCanvas.js +14 -0
  5. package/dist/components/DataWorkspace.js +1 -4
  6. package/dist/components/Dialogs.js +5 -5
  7. package/dist/components/FocusOnMount.d.ts +282 -0
  8. package/dist/components/FocusOnMount.js +12 -0
  9. package/dist/components/Kanban.d.ts +14 -3
  10. package/dist/components/Kanban.js +9 -4
  11. package/dist/components/MotionList.d.ts +303 -0
  12. package/dist/components/MotionList.js +175 -0
  13. package/dist/components/Popover.js +9 -0
  14. package/dist/components/RelationshipGraph.js +4 -1
  15. package/dist/components/RelationshipGraphLayered.js +107 -32
  16. package/dist/components/SearchSuggest.js +8 -1
  17. package/dist/components/Segmented.js +8 -2
  18. package/dist/components/Select.js +6 -0
  19. package/dist/components/SortableList.d.ts +2 -2
  20. package/dist/components/SortableList.js +16 -3
  21. package/dist/components/States.d.ts +33 -1
  22. package/dist/components/States.js +43 -1
  23. package/dist/components/Tabs.js +9 -2
  24. package/dist/components/Toast.d.ts +1 -1
  25. package/dist/components/Toast.js +106 -2
  26. package/dist/components/Tooltip.js +62 -7
  27. package/dist/emoji.cjs +115 -14
  28. package/dist/hooks/useAnchoredPosition.js +14 -2
  29. package/dist/hooks/useDeferredReorder.d.ts +32 -0
  30. package/dist/hooks/useDeferredReorder.js +47 -0
  31. package/dist/hooks/useDialog.js +15 -2
  32. package/dist/hooks/useKeyboardGrab.d.ts +58 -0
  33. package/dist/hooks/useKeyboardGrab.js +118 -0
  34. package/dist/hooks/useSlidingIndicator.d.ts +18 -0
  35. package/dist/hooks/useSlidingIndicator.js +80 -0
  36. package/dist/index.cjs +2208 -1378
  37. package/dist/index.d.cts +477 -13
  38. package/dist/index.d.ts +7 -3
  39. package/dist/index.js +14 -2
  40. package/dist/labels-context.d.ts +3 -0
  41. package/dist/motion.d.ts +11 -0
  42. package/dist/motion.js +35 -0
  43. package/dist/relationship-graph-model.d.ts +17 -3
  44. package/dist/relationship-graph-model.js +28 -16
  45. package/dist/rich-text.cjs +42 -2
  46. package/dist/toast-store.d.ts +5 -2
  47. package/dist/toast-store.js +3 -2
  48. package/package.json +1 -1
@@ -1,12 +1,91 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
+ import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { cx } from "../cx.js";
5
5
  import { useUixLabels } from "../labels-context.js";
6
+ import { prefersReducedMotion } from "../hooks/useReducedMotion.js";
7
+ import { tokenMs } from "../motion.js";
6
8
  import { XIcon } from "../icons/components.js";
7
9
  import { toast as defaultToast } from "../toast-store.js";
8
10
  const ToasterContext = createContext(null);
9
- function Toast({ title, message, tone, icon, action, onClose, leaving, dismissLabel: dismissLabelProp, className, ...props }) {
11
+ const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
12
+ const SWIPE_VELOCITY = 0.11;
13
+ const SWIPE_SLOP = 6;
14
+ const SWIPE_WINDOW = 100;
15
+ function releaseVelocity(samples, x, t) {
16
+ const recent = samples.filter((s) => t - s.t <= SWIPE_WINDOW);
17
+ if (recent.length === 0) return 0;
18
+ return (x - recent[0].x) / Math.max(1, t - recent[0].t);
19
+ }
20
+ function useSwipeToDismiss(onClose, own) {
21
+ const drag = useRef(null);
22
+ const end = (e) => {
23
+ const d = drag.current;
24
+ drag.current = null;
25
+ if (!d?.active || !onClose) return;
26
+ const el = e.currentTarget;
27
+ el.removeAttribute("data-swiping");
28
+ const v = releaseVelocity(d.samples, e.clientX, e.timeStamp);
29
+ const fling = Math.abs(v) > SWIPE_VELOCITY && Math.sign(v) === Math.sign(d.dx);
30
+ if (e.type === "pointerup" && (fling || Math.abs(d.dx) > el.offsetWidth / 3)) {
31
+ if (!prefersReducedMotion()) el.style.translate = `${Math.sign(d.dx) * (el.offsetWidth + 32)}px 0`;
32
+ onClose();
33
+ } else {
34
+ el.style.translate = "";
35
+ }
36
+ };
37
+ return {
38
+ onPointerDown: (e) => {
39
+ own.onPointerDown?.(e);
40
+ if (!onClose || e.defaultPrevented || e.button !== 0) return;
41
+ if (e.target.closest('button, a, input, textarea, select, [role="button"]')) return;
42
+ drag.current = { id: e.pointerId, x0: e.clientX, y0: e.clientY, dx: 0, samples: [{ x: e.clientX, t: e.timeStamp }], active: false };
43
+ },
44
+ onPointerMove: (e) => {
45
+ own.onPointerMove?.(e);
46
+ const d = drag.current;
47
+ if (!d || d.id !== e.pointerId) return;
48
+ const dx = e.clientX - d.x0;
49
+ if (!d.active) {
50
+ if (Math.abs(dx) < SWIPE_SLOP) return;
51
+ if (Math.abs(e.clientY - d.y0) > Math.abs(dx)) {
52
+ drag.current = null;
53
+ return;
54
+ }
55
+ d.active = true;
56
+ e.currentTarget.setPointerCapture?.(e.pointerId);
57
+ e.currentTarget.setAttribute("data-swiping", "");
58
+ }
59
+ d.samples = [...d.samples.filter((s) => e.timeStamp - s.t <= SWIPE_WINDOW), { x: e.clientX, t: e.timeStamp }];
60
+ d.dx = dx;
61
+ e.currentTarget.style.translate = `${dx}px 0`;
62
+ },
63
+ onPointerUp: (e) => {
64
+ own.onPointerUp?.(e);
65
+ end(e);
66
+ },
67
+ onPointerCancel: (e) => {
68
+ own.onPointerCancel?.(e);
69
+ end(e);
70
+ }
71
+ };
72
+ }
73
+ function Toast({
74
+ title,
75
+ message,
76
+ tone,
77
+ icon,
78
+ action,
79
+ onClose,
80
+ leaving,
81
+ dismissLabel: dismissLabelProp,
82
+ className,
83
+ onPointerDown,
84
+ onPointerMove,
85
+ onPointerUp,
86
+ onPointerCancel,
87
+ ...props
88
+ }) {
10
89
  const uixLabels = useUixLabels();
11
90
  const dismissLabel = dismissLabelProp ?? uixLabels.toast?.dismiss ?? "Dismiss";
12
91
  const assertive = tone === "danger";
@@ -29,6 +108,7 @@ function Toast({ title, message, tone, icon, action, onClose, leaving, dismissLa
29
108
  }
30
109
  onClose?.();
31
110
  };
111
+ const swipe = useSwipeToDismiss(onClose, { onPointerDown, onPointerMove, onPointerUp, onPointerCancel });
32
112
  const ownLiveRegion = assertive || !toaster;
33
113
  return /* @__PURE__ */ jsxs(
34
114
  "div",
@@ -38,6 +118,7 @@ function Toast({ title, message, tone, icon, action, onClose, leaving, dismissLa
38
118
  role: ownLiveRegion ? assertive ? "alert" : "status" : void 0,
39
119
  "aria-live": ownLiveRegion ? assertive ? "assertive" : "polite" : void 0,
40
120
  ...props,
121
+ ...swipe,
41
122
  children: [
42
123
  icon && /* @__PURE__ */ jsx("div", { className: "uix-toast__icon", "aria-hidden": "true", children: icon }),
43
124
  /* @__PURE__ */ jsxs("div", { className: "uix-toast__body", ref: bodyRef, children: [
@@ -169,6 +250,28 @@ function Toaster({ children, regionLabel: regionLabelProp, store = defaultToast.
169
250
  return () => document.removeEventListener("visibilitychange", sync);
170
251
  }, []);
171
252
  useToastTimers(store, visible, hovered || focused || hidden);
253
+ const regionRef = useRef(null);
254
+ const tops = useRef(/* @__PURE__ */ new Map());
255
+ useIsomorphicLayoutEffect(() => {
256
+ const region = regionRef.current;
257
+ if (!region) return;
258
+ const base = region.getBoundingClientRect().top;
259
+ const next = /* @__PURE__ */ new Map();
260
+ const glide = !prefersReducedMotion();
261
+ for (const el of Array.from(region.querySelectorAll(":scope > [data-toast-id]"))) {
262
+ const id = el.dataset.toastId;
263
+ const top = base + el.offsetTop;
264
+ next.set(id, top);
265
+ const before = tops.current.get(id);
266
+ if (!glide || before === void 0 || Math.abs(before - top) < 1 || typeof el.animate !== "function") continue;
267
+ el.animate([{ transform: `translateY(${before - top}px)` }, { transform: "translateY(0)" }], {
268
+ duration: tokenMs("--uix-dur", 160, el),
269
+ easing: getComputedStyle(el).getPropertyValue("--uix-ease-out").trim() || "ease-out",
270
+ composite: "add"
271
+ });
272
+ }
273
+ tops.current = next;
274
+ });
172
275
  const handleKeyDown = (event) => {
173
276
  onKeyDown?.(event);
174
277
  if (event.defaultPrevented || event.key !== "Escape") return;
@@ -181,6 +284,7 @@ function Toaster({ children, regionLabel: regionLabelProp, store = defaultToast.
181
284
  return /* @__PURE__ */ jsx(ToasterContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxs(
182
285
  "div",
183
286
  {
287
+ ref: regionRef,
184
288
  className: cx("uix-toaster", className),
185
289
  role: "region",
186
290
  "aria-label": regionLabel,
@@ -4,11 +4,15 @@ import { cloneElement, isValidElement, useEffect, useId, useLayoutEffect, useRef
4
4
  import { cx } from "../cx.js";
5
5
  import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
6
6
  import { KbdCombo, isKeyShortcut, spokenKeys, useKbdPlatform } from "./Kbd.js";
7
+ import { markKeyOpened, tokenMs, watchInputModality } from "../motion.js";
8
+ const group = { open: 0, lastClosed: -Infinity, current: null };
7
9
  const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
8
10
  function Tooltip({ label, content, placement = "top", shortcut, className, children, ...props }) {
9
11
  const triggerRef = useRef(null);
10
12
  const bubbleRef = useRef(null);
11
13
  const closeTimer = useRef(null);
14
+ const openTimer = useRef(null);
15
+ const shown = useRef(false);
12
16
  const [open, setOpen] = useState(false);
13
17
  const id = useId();
14
18
  const platform = useKbdPlatform();
@@ -16,37 +20,81 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
16
20
  const own = shortcut ?? (isKeyShortcut(childShortcut) ? childShortcut : void 0);
17
21
  const keys = own && own.length > 0 ? own : null;
18
22
  useAnchoredPosition(triggerRef, bubbleRef, { open, placement, offset: 6 });
23
+ useEffect(() => {
24
+ watchInputModality();
25
+ }, []);
19
26
  const cancelClose = () => {
20
27
  if (closeTimer.current != null) {
21
28
  clearTimeout(closeTimer.current);
22
29
  closeTimer.current = null;
23
30
  }
24
31
  };
25
- const show = () => {
26
- cancelClose();
32
+ const cancelOpen = () => {
33
+ if (openTimer.current != null) {
34
+ clearTimeout(openTimer.current);
35
+ openTimer.current = null;
36
+ }
37
+ };
38
+ const reveal = (instant) => {
39
+ cancelOpen();
40
+ if (group.current && group.current !== self) group.current.current(true);
41
+ group.current = self;
27
42
  const b = bubbleRef.current;
28
43
  if (b && !b.matches(":popover-open")) {
44
+ b.toggleAttribute("data-uix-instant", instant);
45
+ markKeyOpened(b);
29
46
  try {
30
47
  b.showPopover();
31
48
  } catch {
32
49
  }
33
50
  }
51
+ if (!shown.current) {
52
+ shown.current = true;
53
+ group.open += 1;
54
+ }
34
55
  setOpen(true);
35
56
  };
36
- const hide = () => {
57
+ const show = (via) => {
58
+ cancelClose();
59
+ if (shown.current) return;
60
+ const warm = group.open > 0 || performance.now() - group.lastClosed < tokenMs("--uix-delay-tooltip-skip", 300);
61
+ if (via === "focus" || warm) {
62
+ reveal(via === "pointer");
63
+ return;
64
+ }
65
+ if (openTimer.current == null) openTimer.current = window.setTimeout(() => reveal(false), tokenMs("--uix-delay-tooltip", 400));
66
+ };
67
+ const hide = (instant = false) => {
37
68
  cancelClose();
69
+ cancelOpen();
38
70
  const b = bubbleRef.current;
39
71
  if (b && b.matches(":popover-open")) {
72
+ b.toggleAttribute("data-uix-instant", instant);
40
73
  try {
41
74
  b.hidePopover();
42
75
  } catch {
43
76
  }
44
77
  }
78
+ if (shown.current) {
79
+ shown.current = false;
80
+ group.open -= 1;
81
+ group.lastClosed = performance.now();
82
+ }
83
+ if (group.current === self) group.current = null;
45
84
  setOpen(false);
46
85
  };
86
+ const self = useRef(hide);
87
+ self.current = hide;
47
88
  const scheduleHide = () => {
48
89
  cancelClose();
49
- closeTimer.current = window.setTimeout(hide, 100);
90
+ closeTimer.current = window.setTimeout(() => {
91
+ closeTimer.current = null;
92
+ try {
93
+ if (triggerRef.current?.matches(":hover")) return;
94
+ } catch {
95
+ }
96
+ hide();
97
+ }, 100);
50
98
  };
51
99
  useIsomorphicLayoutEffect(() => {
52
100
  if (!open) return void 0;
@@ -61,6 +109,13 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
61
109
  }, [open]);
62
110
  useEffect(() => () => {
63
111
  if (closeTimer.current != null) clearTimeout(closeTimer.current);
112
+ if (openTimer.current != null) clearTimeout(openTimer.current);
113
+ if (shown.current) {
114
+ shown.current = false;
115
+ group.open -= 1;
116
+ group.lastClosed = performance.now();
117
+ }
118
+ if (group.current === self) group.current = null;
64
119
  const b = bubbleRef.current;
65
120
  if (b && b.matches(":popover-open")) {
66
121
  try {
@@ -83,10 +138,10 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
83
138
  className: cx("uix-tooltip", className),
84
139
  "aria-describedby": single ? void 0 : description,
85
140
  tabIndex: single ? void 0 : 0,
86
- onMouseEnter: show,
141
+ onMouseEnter: () => show("pointer"),
87
142
  onMouseLeave: scheduleHide,
88
- onFocus: show,
89
- onBlur: hide,
143
+ onFocus: () => show("focus"),
144
+ onBlur: () => hide(),
90
145
  ...props,
91
146
  children: [
92
147
  child,
package/dist/emoji.cjs CHANGED
@@ -169,12 +169,17 @@ function removeAnchorName(el, name) {
169
169
  else el.style.removeProperty("anchor-name");
170
170
  }
171
171
  function translationOf(el) {
172
- const t = getComputedStyle(el).transform;
172
+ const style = getComputedStyle(el);
173
+ const t = style.transform;
173
174
  if (!t || t === "none" || typeof DOMMatrixReadOnly === "undefined") return { x: 0, y: 0 };
174
175
  try {
175
176
  const m = new DOMMatrixReadOnly(t);
177
+ const [ox = 0, oy = 0] = style.transformOrigin.split(" ").map((v) => parseFloat(v) || 0);
178
+ const cx2 = el.offsetWidth / 2 - ox;
179
+ const cy = el.offsetHeight / 2 - oy;
180
+ const p = m.transformPoint({ x: cx2, y: cy });
176
181
  const z = zoomOf(el);
177
- return { x: m.m41 * z, y: m.m42 * z };
182
+ return { x: (p.x - cx2) * z, y: (p.y - cy) * z };
178
183
  } catch {
179
184
  return { x: 0, y: 0 };
180
185
  }
@@ -271,6 +276,13 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
271
276
  const measured = size.width > 0 || size.height > 0;
272
277
  if (measured && session.current) side.current = pos.side;
273
278
  if (floating.dataset.placement !== pos.placement) floating.dataset.placement = pos.placement;
279
+ if (measured) {
280
+ const clamp2 = (v, max) => Math.min(Math.max(v, 0), max);
281
+ const ox = pos.side === "left" ? size.width : pos.side === "right" ? 0 : clamp2(a.x + a.width / 2 - pos.x, size.width);
282
+ const oy = pos.side === "top" ? size.height : pos.side === "bottom" ? 0 : clamp2(a.y + a.height / 2 - pos.y, size.height);
283
+ const origin = `${Math.round(ox / zoom)}px ${Math.round(oy / zoom)}px`;
284
+ if (s.getPropertyValue("--uix-anchor-origin") !== origin) s.setProperty("--uix-anchor-origin", origin);
285
+ }
274
286
  const writeFixed = (at) => {
275
287
  const left2 = `${Math.round(at.x / zoom)}px`;
276
288
  const top2 = `${Math.round(at.y / zoom)}px`;
@@ -367,6 +379,33 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
367
379
  return reposition;
368
380
  }
369
381
 
382
+ // src/motion.ts
383
+ var KEY_OPENED = "data-uix-key-opened";
384
+ var keyboard = false;
385
+ var watching = false;
386
+ function watchInputModality() {
387
+ if (watching || typeof document === "undefined") return;
388
+ watching = true;
389
+ document.addEventListener("keydown", (event) => {
390
+ if (!["Shift", "Control", "Alt", "Meta"].includes(event.key)) keyboard = true;
391
+ }, true);
392
+ document.addEventListener("pointerdown", () => {
393
+ keyboard = false;
394
+ }, true);
395
+ }
396
+ function markKeyOpened(el) {
397
+ if (!el) return;
398
+ if (keyboard) el.setAttribute(KEY_OPENED, "");
399
+ else el.removeAttribute(KEY_OPENED);
400
+ }
401
+ function tokenMs(name, fallback, el) {
402
+ if (typeof getComputedStyle === "undefined" || typeof document === "undefined") return fallback;
403
+ const raw = getComputedStyle(el ?? document.documentElement).getPropertyValue(name).trim();
404
+ const m = /^(-?\d*\.?\d+)(ms|s)$/.exec(raw);
405
+ if (!m) return fallback;
406
+ return m[2] === "s" ? Number(m[1]) * 1e3 : Number(m[1]);
407
+ }
408
+
370
409
  // src/components/Popover.tsx
371
410
  var import_jsx_runtime = require("react/jsx-runtime");
372
411
  var useIsomorphicLayoutEffect2 = typeof document !== "undefined" ? import_react2.useLayoutEffect : import_react2.useEffect;
@@ -420,6 +459,14 @@ function Popover({
420
459
  el.hidePopover();
421
460
  } : void 0
422
461
  });
462
+ (0, import_react2.useEffect)(() => {
463
+ watchInputModality();
464
+ const el = ref.current;
465
+ if (!el) return;
466
+ const mark = () => markKeyOpened(el);
467
+ el.addEventListener("beforetoggle", mark);
468
+ return () => el.removeEventListener("beforetoggle", mark);
469
+ }, []);
423
470
  (0, import_react2.useEffect)(() => {
424
471
  const el = ref.current;
425
472
  if (!el || !anchor) return;
@@ -556,9 +603,9 @@ function buildEmojiData(locale, compact, messages) {
556
603
  const sorted = [...compact].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
557
604
  for (const item of sorted) {
558
605
  if (item.group === void 0) continue;
559
- const group = groupIndex.get(item.group);
560
- if (group === void 0) continue;
561
- const entry = { emoji: item.unicode, name: item.label, tags: item.tags ?? [], group };
606
+ const group2 = groupIndex.get(item.group);
607
+ if (group2 === void 0) continue;
608
+ const entry = { emoji: item.unicode, name: item.label, tags: item.tags ?? [], group: group2 };
562
609
  emojis.push(entry);
563
610
  names.set(item.unicode, item.label);
564
611
  names.set(item.unicode.replace(/️/g, ""), item.label);
@@ -830,8 +877,8 @@ function EmojiPicker({
830
877
  if (quickPicks?.length) list.push({ key: "quick", title: labels.quickPicks, emojis: [...new Set(quickPicks)] });
831
878
  if (recent.length) list.push({ key: "recent", title: labels.recent, emojis: recent });
832
879
  if (data) {
833
- data.groups.forEach((group, index) => {
834
- list.push({ key: group.key, title: group.name, emojis: data.emojis.filter((e) => e.group === index).map((e) => e.emoji) });
880
+ data.groups.forEach((group2, index) => {
881
+ list.push({ key: group2.key, title: group2.name, emojis: data.emojis.filter((e) => e.group === index).map((e) => e.emoji) });
835
882
  });
836
883
  }
837
884
  return list;
@@ -1138,11 +1185,14 @@ function KbdCombo({ keys, platform, keyLabels, className, ...props }) {
1138
1185
 
1139
1186
  // src/components/Tooltip.tsx
1140
1187
  var import_jsx_runtime6 = require("react/jsx-runtime");
1188
+ var group = { open: 0, lastClosed: -Infinity, current: null };
1141
1189
  var useIsomorphicLayoutEffect4 = typeof document !== "undefined" ? import_react6.useLayoutEffect : import_react6.useEffect;
1142
1190
  function Tooltip({ label, content, placement = "top", shortcut, className, children, ...props }) {
1143
1191
  const triggerRef = (0, import_react6.useRef)(null);
1144
1192
  const bubbleRef = (0, import_react6.useRef)(null);
1145
1193
  const closeTimer = (0, import_react6.useRef)(null);
1194
+ const openTimer = (0, import_react6.useRef)(null);
1195
+ const shown = (0, import_react6.useRef)(false);
1146
1196
  const [open, setOpen] = (0, import_react6.useState)(false);
1147
1197
  const id = (0, import_react6.useId)();
1148
1198
  const platform = useKbdPlatform();
@@ -1150,37 +1200,81 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
1150
1200
  const own = shortcut ?? (isKeyShortcut(childShortcut) ? childShortcut : void 0);
1151
1201
  const keys = own && own.length > 0 ? own : null;
1152
1202
  useAnchoredPosition(triggerRef, bubbleRef, { open, placement, offset: 6 });
1203
+ (0, import_react6.useEffect)(() => {
1204
+ watchInputModality();
1205
+ }, []);
1153
1206
  const cancelClose = () => {
1154
1207
  if (closeTimer.current != null) {
1155
1208
  clearTimeout(closeTimer.current);
1156
1209
  closeTimer.current = null;
1157
1210
  }
1158
1211
  };
1159
- const show = () => {
1160
- cancelClose();
1212
+ const cancelOpen = () => {
1213
+ if (openTimer.current != null) {
1214
+ clearTimeout(openTimer.current);
1215
+ openTimer.current = null;
1216
+ }
1217
+ };
1218
+ const reveal = (instant) => {
1219
+ cancelOpen();
1220
+ if (group.current && group.current !== self) group.current.current(true);
1221
+ group.current = self;
1161
1222
  const b = bubbleRef.current;
1162
1223
  if (b && !b.matches(":popover-open")) {
1224
+ b.toggleAttribute("data-uix-instant", instant);
1225
+ markKeyOpened(b);
1163
1226
  try {
1164
1227
  b.showPopover();
1165
1228
  } catch {
1166
1229
  }
1167
1230
  }
1231
+ if (!shown.current) {
1232
+ shown.current = true;
1233
+ group.open += 1;
1234
+ }
1168
1235
  setOpen(true);
1169
1236
  };
1170
- const hide = () => {
1237
+ const show = (via) => {
1171
1238
  cancelClose();
1239
+ if (shown.current) return;
1240
+ const warm = group.open > 0 || performance.now() - group.lastClosed < tokenMs("--uix-delay-tooltip-skip", 300);
1241
+ if (via === "focus" || warm) {
1242
+ reveal(via === "pointer");
1243
+ return;
1244
+ }
1245
+ if (openTimer.current == null) openTimer.current = window.setTimeout(() => reveal(false), tokenMs("--uix-delay-tooltip", 400));
1246
+ };
1247
+ const hide = (instant = false) => {
1248
+ cancelClose();
1249
+ cancelOpen();
1172
1250
  const b = bubbleRef.current;
1173
1251
  if (b && b.matches(":popover-open")) {
1252
+ b.toggleAttribute("data-uix-instant", instant);
1174
1253
  try {
1175
1254
  b.hidePopover();
1176
1255
  } catch {
1177
1256
  }
1178
1257
  }
1258
+ if (shown.current) {
1259
+ shown.current = false;
1260
+ group.open -= 1;
1261
+ group.lastClosed = performance.now();
1262
+ }
1263
+ if (group.current === self) group.current = null;
1179
1264
  setOpen(false);
1180
1265
  };
1266
+ const self = (0, import_react6.useRef)(hide);
1267
+ self.current = hide;
1181
1268
  const scheduleHide = () => {
1182
1269
  cancelClose();
1183
- closeTimer.current = window.setTimeout(hide, 100);
1270
+ closeTimer.current = window.setTimeout(() => {
1271
+ closeTimer.current = null;
1272
+ try {
1273
+ if (triggerRef.current?.matches(":hover")) return;
1274
+ } catch {
1275
+ }
1276
+ hide();
1277
+ }, 100);
1184
1278
  };
1185
1279
  useIsomorphicLayoutEffect4(() => {
1186
1280
  if (!open) return void 0;
@@ -1195,6 +1289,13 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
1195
1289
  }, [open]);
1196
1290
  (0, import_react6.useEffect)(() => () => {
1197
1291
  if (closeTimer.current != null) clearTimeout(closeTimer.current);
1292
+ if (openTimer.current != null) clearTimeout(openTimer.current);
1293
+ if (shown.current) {
1294
+ shown.current = false;
1295
+ group.open -= 1;
1296
+ group.lastClosed = performance.now();
1297
+ }
1298
+ if (group.current === self) group.current = null;
1198
1299
  const b = bubbleRef.current;
1199
1300
  if (b && b.matches(":popover-open")) {
1200
1301
  try {
@@ -1217,10 +1318,10 @@ function Tooltip({ label, content, placement = "top", shortcut, className, child
1217
1318
  className: cx("uix-tooltip", className),
1218
1319
  "aria-describedby": single ? void 0 : description,
1219
1320
  tabIndex: single ? void 0 : 0,
1220
- onMouseEnter: show,
1321
+ onMouseEnter: () => show("pointer"),
1221
1322
  onMouseLeave: scheduleHide,
1222
- onFocus: show,
1223
- onBlur: hide,
1323
+ onFocus: () => show("focus"),
1324
+ onBlur: () => hide(),
1224
1325
  ...props,
1225
1326
  children: [
1226
1327
  child,
@@ -29,12 +29,17 @@ function removeAnchorName(el, name) {
29
29
  else el.style.removeProperty("anchor-name");
30
30
  }
31
31
  function translationOf(el) {
32
- const t = getComputedStyle(el).transform;
32
+ const style = getComputedStyle(el);
33
+ const t = style.transform;
33
34
  if (!t || t === "none" || typeof DOMMatrixReadOnly === "undefined") return { x: 0, y: 0 };
34
35
  try {
35
36
  const m = new DOMMatrixReadOnly(t);
37
+ const [ox = 0, oy = 0] = style.transformOrigin.split(" ").map((v) => parseFloat(v) || 0);
38
+ const cx = el.offsetWidth / 2 - ox;
39
+ const cy = el.offsetHeight / 2 - oy;
40
+ const p = m.transformPoint({ x: cx, y: cy });
36
41
  const z = zoomOf(el);
37
- return { x: m.m41 * z, y: m.m42 * z };
42
+ return { x: (p.x - cx) * z, y: (p.y - cy) * z };
38
43
  } catch {
39
44
  return { x: 0, y: 0 };
40
45
  }
@@ -131,6 +136,13 @@ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-st
131
136
  const measured = size.width > 0 || size.height > 0;
132
137
  if (measured && session.current) side.current = pos.side;
133
138
  if (floating.dataset.placement !== pos.placement) floating.dataset.placement = pos.placement;
139
+ if (measured) {
140
+ const clamp = (v, max) => Math.min(Math.max(v, 0), max);
141
+ const ox = pos.side === "left" ? size.width : pos.side === "right" ? 0 : clamp(a.x + a.width / 2 - pos.x, size.width);
142
+ const oy = pos.side === "top" ? size.height : pos.side === "bottom" ? 0 : clamp(a.y + a.height / 2 - pos.y, size.height);
143
+ const origin = `${Math.round(ox / zoom)}px ${Math.round(oy / zoom)}px`;
144
+ if (s.getPropertyValue("--uix-anchor-origin") !== origin) s.setProperty("--uix-anchor-origin", origin);
145
+ }
134
146
  const writeFixed = (at) => {
135
147
  const left2 = `${Math.round(at.x / zoom)}px`;
136
148
  const top2 = `${Math.round(at.y / zoom)}px`;
@@ -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
+ };
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { useRef, useEffect, useLayoutEffect } from "react";
3
+ import { markKeyOpened, watchInputModality } from "../motion.js";
3
4
  const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
4
5
  let lockCount = 0;
5
6
  let saved = null;
@@ -37,13 +38,22 @@ function useDialog(open, onClose, keepOpen) {
37
38
  onCloseRef.current = onClose;
38
39
  const keepOpenRef = useRef(keepOpen);
39
40
  keepOpenRef.current = keepOpen;
41
+ useEffect(() => {
42
+ watchInputModality();
43
+ }, []);
40
44
  useIsomorphicLayoutEffect(() => {
41
45
  const el = ref.current;
42
46
  if (!el || !open) {
43
- if (el && el.open) el.close();
47
+ if (el && el.open) {
48
+ markKeyOpened(el);
49
+ el.close();
50
+ }
44
51
  return;
45
52
  }
46
- if (!el.open) el.showModal();
53
+ if (!el.open) {
54
+ markKeyOpened(el);
55
+ el.showModal();
56
+ }
47
57
  lockBodyScroll();
48
58
  let locked = true;
49
59
  const handleClose = () => {
@@ -61,9 +71,12 @@ function useDialog(open, onClose, keepOpen) {
61
71
  }
62
72
  onCloseRef.current?.();
63
73
  };
74
+ const handleCancel = () => markKeyOpened(el);
64
75
  el.addEventListener("close", handleClose);
76
+ el.addEventListener("cancel", handleCancel);
65
77
  return () => {
66
78
  el.removeEventListener("close", handleClose);
79
+ el.removeEventListener("cancel", handleCancel);
67
80
  if (locked) {
68
81
  locked = false;
69
82
  unlockBodyScroll();