@tensor_1/react 2.48.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 (47) 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/FocusOnMount.d.ts +282 -0
  7. package/dist/components/FocusOnMount.js +12 -0
  8. package/dist/components/Kanban.d.ts +14 -3
  9. package/dist/components/Kanban.js +9 -4
  10. package/dist/components/MotionList.d.ts +303 -0
  11. package/dist/components/MotionList.js +175 -0
  12. package/dist/components/Popover.js +9 -0
  13. package/dist/components/RelationshipGraph.js +4 -1
  14. package/dist/components/RelationshipGraphLayered.js +107 -32
  15. package/dist/components/SearchSuggest.js +8 -1
  16. package/dist/components/Segmented.js +8 -2
  17. package/dist/components/Select.js +6 -0
  18. package/dist/components/SortableList.d.ts +2 -2
  19. package/dist/components/SortableList.js +16 -3
  20. package/dist/components/States.d.ts +33 -1
  21. package/dist/components/States.js +43 -1
  22. package/dist/components/Tabs.js +9 -2
  23. package/dist/components/Toast.d.ts +1 -1
  24. package/dist/components/Toast.js +106 -2
  25. package/dist/components/Tooltip.js +62 -7
  26. package/dist/emoji.cjs +115 -14
  27. package/dist/hooks/useAnchoredPosition.js +14 -2
  28. package/dist/hooks/useDeferredReorder.d.ts +32 -0
  29. package/dist/hooks/useDeferredReorder.js +47 -0
  30. package/dist/hooks/useDialog.js +15 -2
  31. package/dist/hooks/useKeyboardGrab.d.ts +58 -0
  32. package/dist/hooks/useKeyboardGrab.js +118 -0
  33. package/dist/hooks/useSlidingIndicator.d.ts +18 -0
  34. package/dist/hooks/useSlidingIndicator.js +80 -0
  35. package/dist/index.cjs +2203 -1373
  36. package/dist/index.d.cts +477 -13
  37. package/dist/index.d.ts +7 -3
  38. package/dist/index.js +14 -2
  39. package/dist/labels-context.d.ts +3 -0
  40. package/dist/motion.d.ts +11 -0
  41. package/dist/motion.js +35 -0
  42. package/dist/relationship-graph-model.d.ts +17 -3
  43. package/dist/relationship-graph-model.js +28 -16
  44. package/dist/rich-text.cjs +42 -2
  45. package/dist/toast-store.d.ts +5 -2
  46. package/dist/toast-store.js +3 -2
  47. package/package.json +1 -1
@@ -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,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
+ };