@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.
- package/README.md +70 -1
- package/dist/components/Chip.d.ts +7 -3
- package/dist/components/Chip.js +2 -2
- package/dist/components/DashboardCanvas.js +14 -0
- package/dist/components/DataWorkspace.js +1 -4
- package/dist/components/Dialogs.js +5 -5
- package/dist/components/FocusOnMount.d.ts +282 -0
- package/dist/components/FocusOnMount.js +12 -0
- package/dist/components/Kanban.d.ts +14 -3
- package/dist/components/Kanban.js +9 -4
- package/dist/components/MotionList.d.ts +303 -0
- package/dist/components/MotionList.js +175 -0
- package/dist/components/Popover.js +9 -0
- package/dist/components/RelationshipGraph.js +4 -1
- package/dist/components/RelationshipGraphLayered.js +107 -32
- package/dist/components/SearchSuggest.js +8 -1
- package/dist/components/Segmented.js +8 -2
- package/dist/components/Select.js +6 -0
- package/dist/components/SortableList.d.ts +2 -2
- package/dist/components/SortableList.js +16 -3
- package/dist/components/States.d.ts +33 -1
- package/dist/components/States.js +43 -1
- package/dist/components/Tabs.js +9 -2
- package/dist/components/Toast.d.ts +1 -1
- package/dist/components/Toast.js +106 -2
- package/dist/components/Tooltip.js +62 -7
- package/dist/emoji.cjs +115 -14
- package/dist/hooks/useAnchoredPosition.js +14 -2
- package/dist/hooks/useDeferredReorder.d.ts +32 -0
- package/dist/hooks/useDeferredReorder.js +47 -0
- package/dist/hooks/useDialog.js +15 -2
- package/dist/hooks/useKeyboardGrab.d.ts +58 -0
- package/dist/hooks/useKeyboardGrab.js +118 -0
- package/dist/hooks/useSlidingIndicator.d.ts +18 -0
- package/dist/hooks/useSlidingIndicator.js +80 -0
- package/dist/index.cjs +2208 -1378
- package/dist/index.d.cts +477 -13
- package/dist/index.d.ts +7 -3
- package/dist/index.js +14 -2
- package/dist/labels-context.d.ts +3 -0
- package/dist/motion.d.ts +11 -0
- package/dist/motion.js +35 -0
- package/dist/relationship-graph-model.d.ts +17 -3
- package/dist/relationship-graph-model.js +28 -16
- package/dist/rich-text.cjs +42 -2
- package/dist/toast-store.d.ts +5 -2
- package/dist/toast-store.js +3 -2
- package/package.json +1 -1
package/dist/components/Toast.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
26
|
-
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
560
|
-
if (
|
|
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((
|
|
834
|
-
list.push({ key:
|
|
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
|
|
1160
|
-
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
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
|
+
};
|
package/dist/hooks/useDialog.js
CHANGED
|
@@ -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)
|
|
47
|
+
if (el && el.open) {
|
|
48
|
+
markKeyOpened(el);
|
|
49
|
+
el.close();
|
|
50
|
+
}
|
|
44
51
|
return;
|
|
45
52
|
}
|
|
46
|
-
if (!el.open)
|
|
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();
|