@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,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(() => separateEdgeLabels(layout.edges.filter(showLabel).map((edge) => {
392
- const lines = edge.kind === "cycle" ? [...edge.labelLines, labels.cycle] : edge.labelLines;
393
- return { id: edge.id, x: edge.labelX, y: edge.labelY, ...edgeLabelSize(lines) };
394
- })), [layout, edgeLabels, activeIds, focusWithinId, labels]);
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(() => /* @__PURE__ */ jsx("svg", { className: "uix-relationship-graph__edges", width: layout.width, height: layout.height, "aria-hidden": "true", focusable: "false", children: 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)).map(({ edge, active }) => {
398
- const angle = edge.arrowAngle * 180 / Math.PI;
399
- const placed = labelPlacement.get(edge.id);
400
- return /* @__PURE__ */ jsxs(
401
- "g",
402
- {
403
- className: "uix-relationship-graph__link",
404
- "data-kind": edge.kind,
405
- "data-bus": edge.bus || void 0,
406
- "data-active": active || void 0,
407
- "data-emphasis": edge.emphasis || void 0,
408
- "data-conflicted": edge.edgeIds.some((id) => conflictedEdgeIds.has(id)) || void 0,
409
- "data-dimmed": dimmedNodeIds.has(edge.source) && dimmedNodeIds.has(edge.target) || void 0,
410
- "data-variant": edge.variant === "dashed" ? "dashed" : void 0,
411
- "data-tone": edge.tone,
412
- children: [
413
- /* @__PURE__ */ jsx("path", { d: edge.path }),
414
- edge.arrow !== "none" && /* @__PURE__ */ jsx("polygon", { points: arrowPoints, transform: `translate(${edge.arrowX} ${edge.arrowY}) rotate(${angle})` }),
415
- placed && !placed.hidden && /* @__PURE__ */ jsx("text", { x: placed.x, y: placed.y, textAnchor: "middle", dominantBaseline: "middle", "data-moved": placed.y !== edge.labelY || void 0, children: (edge.kind === "cycle" ? [...edge.labelLines, labels.cycle] : edge.labelLines).map((line, index, lines) => /* @__PURE__ */ jsx("tspan", { x: placed.x, dy: index === 0 ? `${-(lines.length - 1) * 0.55}em` : "1.1em", children: line }, index)) })
416
- ]
417
- },
418
- edge.id
419
- );
420
- }) }), [layout, activeIds, highlightedEdgeIds, conflictedEdgeIds, dimmedNodeIds, labelPlacement, labels]);
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__ */ jsx("li", { "data-type": entry.id, "data-tone": entry.tone, "data-shape": entry.shape, "data-variant": entry.variant, children: entry.label }, entry.id)) }),
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__ */ jsx("div", { ref, role: selection === "radio" ? "radiogroup" : "group", className: cx("uix-segmented", className), onKeyDown: handleKeyDown, ...props, children }) });
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 places
40
- * the row under the pointer with no animation. Shares its order model with `ViewMenu`,
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)) commit(order, id);
78
- else setDraft(null);
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
  };
@@ -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__ */ jsx(
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: tabs
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;