@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { boundRelationshipGraph, edgeLabelSize, layeredNeighbor, layoutLayeredGraph, separateEdgeLabels } from "../relationship-graph-model.js";
|
|
5
5
|
import { cx } from "../cx.js";
|
|
6
6
|
import { Maximize2Icon, Minimize2Icon, MinusIcon, PlusIcon, RotateCcwIcon, ScanIcon } from "../icons/components.js";
|
|
@@ -9,6 +9,7 @@ import { prefersReducedMotion } from "../hooks/useReducedMotion.js";
|
|
|
9
9
|
import { EquivalentRelationshipList } from "./RelationshipGraphList.js";
|
|
10
10
|
import { Tooltip } from "./Tooltip.js";
|
|
11
11
|
const EMPTY_IDS = /* @__PURE__ */ new Set();
|
|
12
|
+
const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
|
|
12
13
|
const MIN_ZOOM = 0.4;
|
|
13
14
|
const MAX_ZOOM = 1.5;
|
|
14
15
|
const READABLE_ZOOM = 0.75;
|
|
@@ -17,8 +18,13 @@ const PAN_STEP = 64;
|
|
|
17
18
|
const FOCUS_MARGIN = 24;
|
|
18
19
|
const DESCRIBED_RELATIONS = 5;
|
|
19
20
|
const HINT_MS = 1600;
|
|
21
|
+
const LABEL_FONT_PX = 12.5;
|
|
22
|
+
const MIN_LABEL_PX = 11;
|
|
23
|
+
const LABEL_CHIP_PAD_X = 6;
|
|
24
|
+
const LABEL_CHIP_PAD_Y = 2;
|
|
20
25
|
const NAV_KEYS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End"];
|
|
21
26
|
const clampZoom = (zoom) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
|
|
27
|
+
const footerGutter = (viewport, footer) => footer && footer.parentElement === viewport ? Math.min(footer.offsetHeight, viewport.clientHeight) : 0;
|
|
22
28
|
function initialView(layout, size) {
|
|
23
29
|
if (size.width === 0 || size.height === 0 || layout.width === 0) return { x: 0, y: 0, zoom: 1 };
|
|
24
30
|
const fit = Math.min(size.width / layout.width, size.height / layout.height);
|
|
@@ -91,6 +97,7 @@ function LayeredRelationshipGraph({
|
|
|
91
97
|
const itemRefs = useRef(/* @__PURE__ */ new Map());
|
|
92
98
|
const fullscreenButtonRef = useRef(null);
|
|
93
99
|
const footerRef = useRef(null);
|
|
100
|
+
const edgesRef = useRef(null);
|
|
94
101
|
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
95
102
|
const [view, setView] = useState({ x: 0, y: 0, zoom: 1 });
|
|
96
103
|
const [animate, setAnimate] = useState(false);
|
|
@@ -123,13 +130,14 @@ function LayeredRelationshipGraph({
|
|
|
123
130
|
useEffect(() => {
|
|
124
131
|
const element = viewportRef.current;
|
|
125
132
|
if (!element) return;
|
|
126
|
-
const measure = () => setSize({ width: element.clientWidth, height: element.clientHeight });
|
|
133
|
+
const measure = () => setSize({ width: element.clientWidth, height: Math.max(0, element.clientHeight - footerGutter(element, footerRef.current)) });
|
|
127
134
|
measure();
|
|
128
135
|
if (typeof ResizeObserver === "undefined") return;
|
|
129
136
|
const observer = new ResizeObserver(measure);
|
|
130
137
|
observer.observe(element);
|
|
138
|
+
if (footerRef.current) observer.observe(footerRef.current);
|
|
131
139
|
return () => observer.disconnect();
|
|
132
|
-
}, []);
|
|
140
|
+
}, [compact]);
|
|
133
141
|
useEffect(() => {
|
|
134
142
|
if (size.width === 0) return;
|
|
135
143
|
const key = `${layout.rootId ?? ""}`;
|
|
@@ -382,42 +390,105 @@ function LayeredRelationshipGraph({
|
|
|
382
390
|
return edge.autoLabel || activeIds.has(edge.source) || activeIds.has(edge.target) || focusWithinId === edge.source || focusWithinId === edge.target;
|
|
383
391
|
};
|
|
384
392
|
const viewportStyle = height !== void 0 ? { "--uix-relationship-graph-height": height } : {};
|
|
393
|
+
const zoomStep = Math.min(1, Math.max(0.05, Math.floor(view.zoom * 20) / 20));
|
|
385
394
|
const stageStyle = {
|
|
386
395
|
width: layout.width,
|
|
387
396
|
height: layout.height,
|
|
388
397
|
transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom})`
|
|
389
398
|
};
|
|
390
399
|
const zoomPercent = Math.round(view.zoom * 100);
|
|
391
|
-
const labelPlacement = useMemo(() =>
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
400
|
+
const labelPlacement = useMemo(() => {
|
|
401
|
+
const shown = layout.edges.filter(showLabel);
|
|
402
|
+
const count = /* @__PURE__ */ new Map();
|
|
403
|
+
for (const edge of shown) count.set(edge.label ?? "", (count.get(edge.label ?? "") ?? 0) + 1);
|
|
404
|
+
const scales = /* @__PURE__ */ new Map();
|
|
405
|
+
const boxes = [];
|
|
406
|
+
for (const edge of shown) {
|
|
407
|
+
const lines = edge.kind === "cycle" ? [...edge.labelLines, labels.cycle] : edge.labelLines;
|
|
408
|
+
const size2 = edgeLabelSize(lines);
|
|
409
|
+
const scale = Math.max(1, Math.min(1 / zoomStep, edge.labelRoom / size2.width));
|
|
410
|
+
if (edgeLabels !== "always" && LABEL_FONT_PX * scale * zoomStep < MIN_LABEL_PX - 0.01) continue;
|
|
411
|
+
scales.set(edge.id, scale);
|
|
412
|
+
const priority = (activeIds.has(edge.source) || activeIds.has(edge.target) ? 4 : 0) + (edge.tone || edge.emphasis ? 2 : 0) + (count.get(edge.label ?? "") === 1 ? 1 : 0);
|
|
413
|
+
boxes.push({ id: edge.id, x: edge.labelX, y: edge.labelY, width: size2.width * scale, height: size2.height * scale, priority });
|
|
414
|
+
}
|
|
415
|
+
const placed = separateEdgeLabels(boxes, { maxShift: 48 / zoomStep, obstacles: layout.items });
|
|
416
|
+
return new Map([...placed].map(([id, place]) => [id, { ...place, scale: scales.get(id) ?? 1 }]));
|
|
417
|
+
}, [layout, edgeLabels, activeIds, focusWithinId, labels, zoomStep]);
|
|
395
418
|
const live = useRef({ activate, onItemKeyDown, onFocusChange, toggleCluster, onSelect });
|
|
396
419
|
live.current = { activate, onItemKeyDown, onFocusChange, toggleCluster, onSelect };
|
|
397
|
-
const edgeLayer = useMemo(() =>
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
return /* @__PURE__ */ jsxs(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
420
|
+
const edgeLayer = useMemo(() => {
|
|
421
|
+
const ordered = layout.edges.map((edge) => ({ edge, active: activeIds.has(edge.source) || activeIds.has(edge.target) || edge.edgeIds.some((id) => highlightedEdgeIds.has(id)) })).sort((a, b) => Number(a.active) - Number(b.active));
|
|
422
|
+
const dimmed = (edge) => dimmedNodeIds.has(edge.source) && dimmedNodeIds.has(edge.target) || void 0;
|
|
423
|
+
return /* @__PURE__ */ jsxs("svg", { ref: edgesRef, className: "uix-relationship-graph__edges", width: layout.width, height: layout.height, "aria-hidden": "true", focusable: "false", children: [
|
|
424
|
+
ordered.map(({ edge, active }) => {
|
|
425
|
+
const angle = edge.arrowAngle * 180 / Math.PI;
|
|
426
|
+
return /* @__PURE__ */ jsxs(
|
|
427
|
+
"g",
|
|
428
|
+
{
|
|
429
|
+
className: "uix-relationship-graph__link",
|
|
430
|
+
"data-edge": edge.id,
|
|
431
|
+
"data-kind": edge.kind,
|
|
432
|
+
"data-bus": edge.bus || void 0,
|
|
433
|
+
"data-active": active || void 0,
|
|
434
|
+
"data-emphasis": edge.emphasis || void 0,
|
|
435
|
+
"data-conflicted": edge.edgeIds.some((id) => conflictedEdgeIds.has(id)) || void 0,
|
|
436
|
+
"data-dimmed": dimmed(edge),
|
|
437
|
+
"data-variant": edge.variant === "dashed" ? "dashed" : void 0,
|
|
438
|
+
"data-tone": edge.tone,
|
|
439
|
+
children: [
|
|
440
|
+
/* @__PURE__ */ jsx("path", { d: edge.path }),
|
|
441
|
+
edge.arrow !== "none" && /* @__PURE__ */ jsx("polygon", { points: arrowPoints, transform: `translate(${edge.arrowX} ${edge.arrowY}) rotate(${angle})` })
|
|
442
|
+
]
|
|
443
|
+
},
|
|
444
|
+
edge.id
|
|
445
|
+
);
|
|
446
|
+
}),
|
|
447
|
+
/* @__PURE__ */ jsx("g", { className: "uix-relationship-graph__edge-labels", children: ordered.map(({ edge, active }) => {
|
|
448
|
+
const placed = labelPlacement.get(edge.id);
|
|
449
|
+
if (!placed || placed.hidden) return null;
|
|
450
|
+
const lines = edge.kind === "cycle" ? [...edge.labelLines, labels.cycle] : edge.labelLines;
|
|
451
|
+
const chip = edgeLabelSize(lines);
|
|
452
|
+
return /* @__PURE__ */ jsxs(
|
|
453
|
+
"g",
|
|
454
|
+
{
|
|
455
|
+
className: "uix-relationship-graph__edge-label",
|
|
456
|
+
"data-edge": edge.id,
|
|
457
|
+
"data-active": active || void 0,
|
|
458
|
+
"data-dimmed": dimmed(edge),
|
|
459
|
+
"data-tone": edge.tone,
|
|
460
|
+
"data-moved": placed.y !== edge.labelY || void 0,
|
|
461
|
+
style: placed.scale !== 1 ? { "--uix-graph-label-scale": placed.scale } : void 0,
|
|
462
|
+
children: [
|
|
463
|
+
/* @__PURE__ */ jsx("rect", { x: placed.x - chip.width / 2, y: placed.y - chip.height / 2, width: chip.width, height: chip.height, rx: 4 }),
|
|
464
|
+
/* @__PURE__ */ jsx("text", { x: placed.x, y: placed.y, textAnchor: "middle", dominantBaseline: "central", children: lines.map((line, index) => /* @__PURE__ */ jsx("tspan", { x: placed.x, dy: index === 0 ? `${-(lines.length - 1) * 0.6}em` : "1.2em", children: line }, index)) })
|
|
465
|
+
]
|
|
466
|
+
},
|
|
467
|
+
edge.id
|
|
468
|
+
);
|
|
469
|
+
}) })
|
|
470
|
+
] });
|
|
471
|
+
}, [layout, activeIds, highlightedEdgeIds, conflictedEdgeIds, dimmedNodeIds, labelPlacement, labels]);
|
|
472
|
+
useIsomorphicLayoutEffect(() => {
|
|
473
|
+
const svg = edgesRef.current;
|
|
474
|
+
if (!svg) return;
|
|
475
|
+
for (const group of Array.from(svg.querySelectorAll(".uix-relationship-graph__edge-label"))) {
|
|
476
|
+
const text = group.querySelector("text");
|
|
477
|
+
const rect = group.querySelector("rect");
|
|
478
|
+
if (!text || !rect || typeof text.getBBox !== "function") continue;
|
|
479
|
+
let box;
|
|
480
|
+
try {
|
|
481
|
+
box = text.getBBox();
|
|
482
|
+
} catch {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (box.width === 0) continue;
|
|
486
|
+
rect.setAttribute("x", String(box.x - LABEL_CHIP_PAD_X));
|
|
487
|
+
rect.setAttribute("y", String(box.y - LABEL_CHIP_PAD_Y));
|
|
488
|
+
rect.setAttribute("width", String(box.width + 2 * LABEL_CHIP_PAD_X));
|
|
489
|
+
rect.setAttribute("height", String(box.height + 2 * LABEL_CHIP_PAD_Y));
|
|
490
|
+
}
|
|
491
|
+
}, [edgeLayer]);
|
|
421
492
|
const itemLayer = useMemo(() => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
422
493
|
layout.columns.map((column) => /* @__PURE__ */ jsxs(
|
|
423
494
|
"div",
|
|
@@ -561,6 +632,7 @@ function LayeredRelationshipGraph({
|
|
|
561
632
|
onPointerMove,
|
|
562
633
|
onPointerUp: onPointerEnd,
|
|
563
634
|
onPointerCancel: onPointerEnd,
|
|
635
|
+
onDragStart: (event) => event.preventDefault(),
|
|
564
636
|
...compact ? {
|
|
565
637
|
tabIndex: 0,
|
|
566
638
|
role: "group",
|
|
@@ -597,7 +669,10 @@ function LayeredRelationshipGraph({
|
|
|
597
669
|
]
|
|
598
670
|
}
|
|
599
671
|
),
|
|
600
|
-
legend.length > 0 && /* @__PURE__ */ jsx("ul", { className: "uix-relationship-graph__legend", "aria-label": labels.legend, children: legend.map((entry) => /* @__PURE__ */
|
|
672
|
+
legend.length > 0 && /* @__PURE__ */ jsx("ul", { className: "uix-relationship-graph__legend", "aria-label": labels.legend, children: legend.map((entry) => /* @__PURE__ */ jsxs("li", { "data-type": entry.id, "data-tone": entry.tone, "data-shape": entry.shape, "data-variant": entry.variant, children: [
|
|
673
|
+
/* @__PURE__ */ jsx("span", { className: "uix-relationship-graph__legend-mark", "aria-hidden": "true" }),
|
|
674
|
+
entry.label
|
|
675
|
+
] }, entry.id)) }),
|
|
601
676
|
listLayer
|
|
602
677
|
]
|
|
603
678
|
}
|
|
@@ -93,6 +93,13 @@ function SearchSuggest({
|
|
|
93
93
|
const showEmpty = !!state?.empty && shownError == null;
|
|
94
94
|
const showIdle = idle != null && itemCount === 0 && !loading && error == null && !showEmpty;
|
|
95
95
|
const visible = open && !disabled && (itemCount > 0 || showLoading || showEmpty || showIdle || shownError != null);
|
|
96
|
+
const [entered, setEntered] = useState(false);
|
|
97
|
+
const wasVisible = useRef(false);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!open) setEntered(false);
|
|
100
|
+
else if (!visible && wasVisible.current) setEntered(true);
|
|
101
|
+
wasVisible.current = visible;
|
|
102
|
+
}, [open, visible]);
|
|
96
103
|
const optionId = (index) => `${id}-option-${index}`;
|
|
97
104
|
useEffect(() => {
|
|
98
105
|
if (!visible || active < 0) return;
|
|
@@ -258,7 +265,7 @@ function SearchSuggest({
|
|
|
258
265
|
!showSpinner && !value && !focused && shortcutHint && /* @__PURE__ */ jsx("kbd", { className: "uix-kbd", "aria-hidden": "true", children: shortcutHint })
|
|
259
266
|
] })
|
|
260
267
|
] }),
|
|
261
|
-
/* @__PURE__ */ jsxs("div", { ref: popupRef, className: "uix-search-suggest__popup", "data-strategy": floating ? "fixed" : void 0, hidden: !visible, children: [
|
|
268
|
+
/* @__PURE__ */ jsxs("div", { ref: popupRef, className: "uix-search-suggest__popup", "data-strategy": floating ? "fixed" : void 0, "data-settled": entered ? "" : void 0, hidden: !visible, children: [
|
|
262
269
|
heading && options.length > 0 && /* @__PURE__ */ jsxs("div", { className: "uix-search-suggest__header", children: [
|
|
263
270
|
/* @__PURE__ */ jsx("span", { className: "uix-search-suggest__heading", id: headingId, children: heading }),
|
|
264
271
|
headingAction && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-search-suggest__header-action", onMouseDown: keepFocus, onClick: headingAction.onSelect, children: headingAction.label })
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useContext, useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { renderUixLink } from "../link.js";
|
|
6
|
+
import { useSlidingIndicator } from "../hooks/useSlidingIndicator.js";
|
|
6
7
|
const SegmentedCtx = createContext({});
|
|
7
8
|
const OPTION = ".uix-segmented__option";
|
|
8
9
|
function Segmented({ value, onChange, selection = "toggle", renderLink, children, className, onKeyDown, ...props }) {
|
|
9
10
|
const ref = useRef(null);
|
|
10
11
|
const contextValue = useMemo(() => ({ value, onChange, selection, renderLink }), [value, onChange, selection, renderLink]);
|
|
12
|
+
const indicatorRef = useRef(null);
|
|
13
|
+
useSlidingIndicator(ref, indicatorRef, `${OPTION}:is([aria-pressed="true"], [aria-checked="true"])`);
|
|
11
14
|
useEffect(() => {
|
|
12
15
|
const group = ref.current;
|
|
13
16
|
if (!group || selection === "link") return;
|
|
@@ -59,7 +62,10 @@ function Segmented({ value, onChange, selection = "toggle", renderLink, children
|
|
|
59
62
|
if (selection === "link") {
|
|
60
63
|
return /* @__PURE__ */ jsx(SegmentedCtx.Provider, { value: contextValue, children: /* @__PURE__ */ jsx("nav", { className: cx("uix-segmented-nav", className), onKeyDown, ...props, children: /* @__PURE__ */ jsx("ul", { className: "uix-segmented", role: "list", children }) }) });
|
|
61
64
|
}
|
|
62
|
-
return /* @__PURE__ */ jsx(SegmentedCtx.Provider, { value: contextValue, children: /* @__PURE__ */
|
|
65
|
+
return /* @__PURE__ */ jsx(SegmentedCtx.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs("div", { ref, role: selection === "radio" ? "radiogroup" : "group", className: cx("uix-segmented", className), onKeyDown: handleKeyDown, ...props, children: [
|
|
66
|
+
children,
|
|
67
|
+
/* @__PURE__ */ jsx("span", { ref: indicatorRef, className: "uix-segmented__indicator", "aria-hidden": "true", hidden: true })
|
|
68
|
+
] }) });
|
|
63
69
|
}
|
|
64
70
|
function SegmentedOption({
|
|
65
71
|
value,
|
|
@@ -4,6 +4,7 @@ import { Children, Fragment as Fragment2, forwardRef, isValidElement, useCallbac
|
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
5
|
import { useUixLabels } from "../labels-context.js";
|
|
6
6
|
import { useAnchoredPosition, zoomOf } from "../hooks/useAnchoredPosition.js";
|
|
7
|
+
import { markKeyOpened, watchInputModality } from "../motion.js";
|
|
7
8
|
import { Drawer } from "./Drawer.js";
|
|
8
9
|
import {
|
|
9
10
|
buildSelectModel,
|
|
@@ -542,18 +543,23 @@ const Select = forwardRef(function Select2(props, forwardedRef) {
|
|
|
542
543
|
const room = Math.max(r.top, window.innerHeight - r.bottom) / zoom - OFFSET - EDGE;
|
|
543
544
|
popup.style.minWidth = `${Math.round(r.width / zoom)}px`;
|
|
544
545
|
popup.style.maxHeight = `${Math.max(96, Math.min(MAX_HEIGHT, Math.floor(room)))}px`;
|
|
546
|
+
if (!isOpen()) markKeyOpened(popup);
|
|
545
547
|
try {
|
|
546
548
|
if (!isOpen()) popup.showPopover();
|
|
547
549
|
} catch {
|
|
548
550
|
}
|
|
549
551
|
if (isSearchable) searchRef.current?.focus({ preventScroll: true });
|
|
550
552
|
} else {
|
|
553
|
+
if (isOpen()) markKeyOpened(popup);
|
|
551
554
|
try {
|
|
552
555
|
if (isOpen()) popup.hidePopover();
|
|
553
556
|
} catch {
|
|
554
557
|
}
|
|
555
558
|
}
|
|
556
559
|
}, [showing, isSearchable]);
|
|
560
|
+
useEffect(() => {
|
|
561
|
+
watchInputModality();
|
|
562
|
+
}, []);
|
|
557
563
|
useAnchoredPosition(triggerRef, popupRef, { open: showing, placement, offset: OFFSET, padding: EDGE });
|
|
558
564
|
useEffect(() => {
|
|
559
565
|
if (open && mode === "sheet") (isSearchable ? searchRef.current : listRef.current)?.focus({ preventScroll: true });
|
|
@@ -36,8 +36,8 @@ interface SortableListProps<T> extends Omit<HTMLAttributes<HTMLUListElement>, 'c
|
|
|
36
36
|
* order): media, priorities, steps. Each row has a grip to drag with a pointer, and Move up /
|
|
37
37
|
* Move down buttons that are always there (a drag is never the only way). With focus anywhere
|
|
38
38
|
* in a row, Alt+↑ / Alt+↓ move it; a focused grip moves with ↑ / ↓ alone. Focus stays with the
|
|
39
|
-
* moved row, and a polite status says where it went. Keyboard moves are instant; a drag
|
|
40
|
-
* the row under the pointer
|
|
39
|
+
* moved row, and a polite status says where it went. Keyboard moves are instant; a drag lifts
|
|
40
|
+
* the row (drag.css), places it under the pointer frame by frame, and it settles when dropped. Shares its order model with `ViewMenu`,
|
|
41
41
|
* `SavedViewMenu` and `NavFavourites`.
|
|
42
42
|
*/
|
|
43
43
|
declare function SortableList<T>({ items, getId, getLabel, renderItem, onReorder, disabled, labels: labelOverrides, className, ...props }: SortableListProps<T>): react.JSX.Element;
|
|
@@ -6,6 +6,7 @@ import { fillLabel } from "../fill-label.js";
|
|
|
6
6
|
import { useUixLabels } from "../labels-context.js";
|
|
7
7
|
import { placeId, sameOrder, useRowDrag } from "../reorder.js";
|
|
8
8
|
import { EditorIcon } from "./EditorIcons.js";
|
|
9
|
+
import { settleDrop } from "../hooks/useKeyboardGrab.js";
|
|
9
10
|
const DEFAULT_SORTABLE_LIST_LABELS = {
|
|
10
11
|
grip: "Drag to reorder: {label}",
|
|
11
12
|
moveUp: "Move {label} up",
|
|
@@ -65,6 +66,16 @@ function SortableList({
|
|
|
65
66
|
refocus.current = { id, part, el };
|
|
66
67
|
commit(next, id);
|
|
67
68
|
};
|
|
69
|
+
const settle = useRef(null);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const pending = settle.current;
|
|
72
|
+
if (!pending || draft !== null) return;
|
|
73
|
+
if (orderKey === pending.from) return;
|
|
74
|
+
settle.current = null;
|
|
75
|
+
if (orderKey !== pending.to) return;
|
|
76
|
+
const row = Array.from(listRef.current?.children ?? []).find((child) => child.dataset.id === pending.id);
|
|
77
|
+
settleDrop(row);
|
|
78
|
+
});
|
|
68
79
|
const dragStart = useRef(null);
|
|
69
80
|
const { dragging, start } = useRowDrag(".uix-sortable__row", {
|
|
70
81
|
over: (id, index) => {
|
|
@@ -74,8 +85,10 @@ function SortableList({
|
|
|
74
85
|
drop: (id) => {
|
|
75
86
|
const begin = dragStart.current;
|
|
76
87
|
dragStart.current = null;
|
|
77
|
-
if (begin && !sameOrder(begin, order))
|
|
78
|
-
|
|
88
|
+
if (begin && !sameOrder(begin, order)) {
|
|
89
|
+
settle.current = { id, to: order.join("\0"), from: orderKey };
|
|
90
|
+
commit(order, id);
|
|
91
|
+
} else setDraft(null);
|
|
79
92
|
},
|
|
80
93
|
cancel: () => {
|
|
81
94
|
dragStart.current = null;
|
|
@@ -95,7 +108,7 @@ function SortableList({
|
|
|
95
108
|
const item = byId.get(id);
|
|
96
109
|
if (item === void 0) return null;
|
|
97
110
|
const name = nameOf(id);
|
|
98
|
-
return /* @__PURE__ */ jsxs("li", { className: "uix-sortable__row", "data-id": id, "data-dragging": dragging === id || void 0, onKeyDown: (event) => onRowKeyDown(event, id), children: [
|
|
111
|
+
return /* @__PURE__ */ jsxs("li", { className: "uix-sortable__row", "data-id": id, "data-dragging": dragging === id || void 0, "data-uix-drag": dragging === id ? "lifted" : void 0, onKeyDown: (event) => onRowKeyDown(event, id), children: [
|
|
99
112
|
!disabled && /* @__PURE__ */ jsx(
|
|
100
113
|
"button",
|
|
101
114
|
{
|
|
@@ -38,6 +38,38 @@ declare function ForbiddenState({ icon, title, detail, action, variant, classNam
|
|
|
38
38
|
declare function NotFoundState({ icon, title, detail, action, variant, className, ...props }: AccessStateProps): react.JSX.Element;
|
|
39
39
|
/** Empty-result state for an active search or filter, kept distinct for analytics and recovery copy. */
|
|
40
40
|
declare function FilteredEmptyState(props: EmptyStateProps): react.JSX.Element;
|
|
41
|
+
interface SuccessStateProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
42
|
+
/** What happened, e.g. "Request submitted". Rendered as a heading that takes focus on mount. */
|
|
43
|
+
title: ReactNode;
|
|
44
|
+
/** The heading level (default 1: the outcome usually replaces the page's form). */
|
|
45
|
+
headingLevel?: 1 | 2 | 3;
|
|
46
|
+
/** Supporting copy under the title. */
|
|
47
|
+
children?: ReactNode;
|
|
48
|
+
/** The record's reference, e.g. `<>REQ-0233 <CopyButton … /></>`. */
|
|
49
|
+
reference?: ReactNode;
|
|
50
|
+
/** The words before the reference (default "Your reference"). */
|
|
51
|
+
referenceLabel?: ReactNode;
|
|
52
|
+
/** A short, ordered "what happens next": two to four steps in the person's words. */
|
|
53
|
+
next?: ReactNode[];
|
|
54
|
+
/** The heading of `next` (default "What happens next"). */
|
|
55
|
+
nextTitle?: ReactNode;
|
|
56
|
+
/** The next action, e.g. a `ButtonLink` to the request or back to the portal. */
|
|
57
|
+
action?: ReactNode;
|
|
58
|
+
/**
|
|
59
|
+
* Move focus to the title when it mounts. Default: on for `page` and `full-page` (the outcome
|
|
60
|
+
* replaces the form the person was in), off for a state inside a section, card or drawer.
|
|
61
|
+
*/
|
|
62
|
+
focusOnMount?: boolean;
|
|
63
|
+
variant?: StateVariant;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The outcome of a rare, important journey (HAR-2588; motion standard §2.2 "Rare", §2.8
|
|
67
|
+
* "portal peak"): a request submitted, an application sent. A success mark draws once — never
|
|
68
|
+
* repeats, never a game — then the title, the reference, what happens next, and the next
|
|
69
|
+
* action. On a page, focus moves to the title, so a screen reader reads the outcome first. Under
|
|
70
|
+
* reduced motion the mark is simply there.
|
|
71
|
+
*/
|
|
72
|
+
declare function SuccessState({ title, headingLevel, children, reference, referenceLabel, next, nextTitle, action, focusOnMount, variant, className, ...props }: SuccessStateProps): react.JSX.Element;
|
|
41
73
|
interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {
|
|
42
74
|
variant?: 'block' | 'text' | 'circle';
|
|
43
75
|
width?: number | string;
|
|
@@ -61,4 +93,4 @@ interface LoadingStateProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
61
93
|
/** A stack of skeleton rows for list/section loading. `role="status"`. */
|
|
62
94
|
declare function LoadingState({ delay, ...props }: LoadingStateProps): react.JSX.Element;
|
|
63
95
|
|
|
64
|
-
export { type AccessStateProps, EmptyState, type EmptyStateProps, ErrorState, type ErrorStateProps, FilteredEmptyState, ForbiddenState, LoadingState, type LoadingStateProps, NotFoundState, Skeleton, type SkeletonProps, type StateVariant };
|
|
96
|
+
export { type AccessStateProps, EmptyState, type EmptyStateProps, ErrorState, type ErrorStateProps, FilteredEmptyState, ForbiddenState, LoadingState, type LoadingStateProps, NotFoundState, Skeleton, type SkeletonProps, type StateVariant, SuccessState, type SuccessStateProps };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createElement } from "react";
|
|
2
3
|
import { cx } from "../cx.js";
|
|
3
4
|
import { DelayedRender } from "./DelayedRender.js";
|
|
5
|
+
import { FocusOnMount } from "./FocusOnMount.js";
|
|
4
6
|
import { PENDING_DELAY_MS } from "../motion-timing.js";
|
|
5
7
|
function EmptyState({ icon, title, children, action, variant = "section", className, ...props }) {
|
|
6
8
|
return /* @__PURE__ */ jsxs("div", { className: cx("uix-empty", `uix-empty--${variant}`, className), ...props, children: [
|
|
@@ -37,6 +39,45 @@ function NotFoundState({ icon, title, detail, action, variant = "section", class
|
|
|
37
39
|
function FilteredEmptyState(props) {
|
|
38
40
|
return /* @__PURE__ */ jsx(EmptyState, { "data-filtered": "true", ...props });
|
|
39
41
|
}
|
|
42
|
+
function SuccessState({
|
|
43
|
+
title,
|
|
44
|
+
headingLevel = 1,
|
|
45
|
+
children,
|
|
46
|
+
reference,
|
|
47
|
+
referenceLabel = "Your reference",
|
|
48
|
+
next,
|
|
49
|
+
nextTitle = "What happens next",
|
|
50
|
+
action,
|
|
51
|
+
focusOnMount,
|
|
52
|
+
variant = "page",
|
|
53
|
+
className,
|
|
54
|
+
...props
|
|
55
|
+
}) {
|
|
56
|
+
const focusTitle = focusOnMount ?? (variant === "page" || variant === "full-page");
|
|
57
|
+
const heading = `h${headingLevel}`;
|
|
58
|
+
const subheading = `h${Math.min(headingLevel + 1, 6)}`;
|
|
59
|
+
return (
|
|
60
|
+
// `section` is the base look: no modifier class (there is no rule for one)
|
|
61
|
+
/* @__PURE__ */ jsxs("div", { className: cx("uix-empty", "uix-success", variant !== "section" && `uix-empty--${variant}`, className), ...props, children: [
|
|
62
|
+
/* @__PURE__ */ jsx("div", { className: "uix-success__mark", "aria-hidden": "true", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 48 48", fill: "none", children: [
|
|
63
|
+
/* @__PURE__ */ jsx("circle", { className: "uix-success__disc", cx: "24", cy: "24", r: "22" }),
|
|
64
|
+
/* @__PURE__ */ jsx("path", { className: "uix-success__check", d: "M14 24.5 21 31.5 34 17.5", pathLength: 1 })
|
|
65
|
+
] }) }),
|
|
66
|
+
/* @__PURE__ */ jsx(FocusOnMount, { as: heading, enabled: focusTitle, className: "uix-empty__title uix-success__title", children: title }),
|
|
67
|
+
children != null && /* @__PURE__ */ jsx("div", { className: "uix-success__body", children }),
|
|
68
|
+
reference != null && /* @__PURE__ */ jsxs("p", { className: "uix-success__reference", children: [
|
|
69
|
+
/* @__PURE__ */ jsx("span", { className: "uix-success__reference-label", children: referenceLabel }),
|
|
70
|
+
" ",
|
|
71
|
+
/* @__PURE__ */ jsx("span", { className: "uix-success__reference-value", children: reference })
|
|
72
|
+
] }),
|
|
73
|
+
next && next.length > 0 && /* @__PURE__ */ jsxs("div", { className: "uix-success__next", children: [
|
|
74
|
+
createElement(subheading, { className: "uix-success__next-title" }, nextTitle),
|
|
75
|
+
/* @__PURE__ */ jsx("ol", { className: "uix-success__steps", children: next.map((step, i) => /* @__PURE__ */ jsx("li", { children: step }, i)) })
|
|
76
|
+
] }),
|
|
77
|
+
action
|
|
78
|
+
] })
|
|
79
|
+
);
|
|
80
|
+
}
|
|
40
81
|
function Skeleton({ variant = "block", width, height, className, style, ...props }) {
|
|
41
82
|
const dims = { width, height, ...style };
|
|
42
83
|
return /* @__PURE__ */ jsx(
|
|
@@ -73,5 +114,6 @@ export {
|
|
|
73
114
|
ForbiddenState,
|
|
74
115
|
LoadingState,
|
|
75
116
|
NotFoundState,
|
|
76
|
-
Skeleton
|
|
117
|
+
Skeleton,
|
|
118
|
+
SuccessState
|
|
77
119
|
};
|
package/dist/components/Tabs.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Children, createContext, isValidElement, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { cx } from "../cx.js";
|
|
5
|
+
import { useSlidingIndicator } from "../hooks/useSlidingIndicator.js";
|
|
5
6
|
const TabsCtx = createContext({});
|
|
6
7
|
const Chevron = ({ d }) => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d }) });
|
|
7
8
|
const inlineOffset = (el, rtl) => rtl ? -el.scrollLeft : el.scrollLeft;
|
|
@@ -18,6 +19,9 @@ function Tabs({
|
|
|
18
19
|
}) {
|
|
19
20
|
const baseId = useId();
|
|
20
21
|
const listRef = useRef(null);
|
|
22
|
+
const indicatorRef = useRef(null);
|
|
23
|
+
const sliding = variant !== "enclosed";
|
|
24
|
+
useSlidingIndicator(listRef, indicatorRef, sliding ? '[role="tab"][aria-selected="true"]' : ":not(*)");
|
|
21
25
|
const contextValue = useMemo(() => ({ value, onChange, baseId }), [value, onChange, baseId]);
|
|
22
26
|
const scroll = overflow === "scroll";
|
|
23
27
|
const [hidden, setHidden] = useState({ start: false, end: false });
|
|
@@ -101,7 +105,7 @@ function Tabs({
|
|
|
101
105
|
target.focus();
|
|
102
106
|
if (activation === "automatic") target.click();
|
|
103
107
|
};
|
|
104
|
-
const list = /* @__PURE__ */
|
|
108
|
+
const list = /* @__PURE__ */ jsxs(
|
|
105
109
|
"div",
|
|
106
110
|
{
|
|
107
111
|
...listProps,
|
|
@@ -109,7 +113,10 @@ function Tabs({
|
|
|
109
113
|
role: "tablist",
|
|
110
114
|
className: cx("uix-tabs", `uix-tabs--${variant}`, scroll && "uix-tabs--scroll", className),
|
|
111
115
|
onKeyDown,
|
|
112
|
-
children:
|
|
116
|
+
children: [
|
|
117
|
+
tabs,
|
|
118
|
+
sliding && /* @__PURE__ */ jsx("span", { ref: indicatorRef, className: "uix-tabs__indicator", "aria-hidden": "true", hidden: true })
|
|
119
|
+
]
|
|
113
120
|
}
|
|
114
121
|
);
|
|
115
122
|
return /* @__PURE__ */ jsxs(TabsCtx.Provider, { value: contextValue, children: [
|
|
@@ -15,7 +15,7 @@ interface ToastProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
|
15
15
|
onClose?: () => void;
|
|
16
16
|
leaving?: boolean;
|
|
17
17
|
}
|
|
18
|
-
declare function Toast({ title, message, tone, icon, action, onClose, leaving, dismissLabel: dismissLabelProp, className, ...props }: ToastProps): react.JSX.Element;
|
|
18
|
+
declare function Toast({ title, message, tone, icon, action, onClose, leaving, dismissLabel: dismissLabelProp, className, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, ...props }: ToastProps): react.JSX.Element;
|
|
19
19
|
interface ToasterProps extends HTMLAttributes<HTMLDivElement> {
|
|
20
20
|
/** TENSOR RX-125 (UIX-04): translatable; English default. */
|
|
21
21
|
regionLabel?: string;
|