@tensor_1/react 2.17.0 → 2.19.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 (60) hide show
  1. package/README.md +131 -1
  2. package/dist/components/Comments.d.ts +18 -2
  3. package/dist/components/Comments.js +37 -12
  4. package/dist/components/EditorIcons.d.ts +39 -0
  5. package/dist/components/EditorIcons.js +51 -0
  6. package/dist/components/EmojiGlyph.d.ts +10 -0
  7. package/dist/components/EmojiGlyph.js +18 -0
  8. package/dist/components/EmojiPicker.d.ts +60 -0
  9. package/dist/components/EmojiPicker.js +277 -0
  10. package/dist/components/Markdown.d.ts +36 -0
  11. package/dist/components/Markdown.js +104 -0
  12. package/dist/components/ReactionBar.d.ts +57 -0
  13. package/dist/components/ReactionBar.js +81 -0
  14. package/dist/components/RelationshipGraph.d.ts +81 -3
  15. package/dist/components/RelationshipGraph.js +102 -59
  16. package/dist/components/RelationshipGraphLayered.d.ts +8 -0
  17. package/dist/components/RelationshipGraphLayered.js +420 -0
  18. package/dist/components/RelationshipGraphList.d.ts +16 -0
  19. package/dist/components/RelationshipGraphList.js +33 -0
  20. package/dist/components/RichTextEditor.d.ts +128 -0
  21. package/dist/components/RichTextEditor.js +954 -0
  22. package/dist/components/SaveStatus.d.ts +32 -0
  23. package/dist/components/SaveStatus.js +21 -0
  24. package/dist/components/Tabs.d.ts +10 -3
  25. package/dist/components/Tabs.js +5 -3
  26. package/dist/emoji-image-WQLh8ZQP.d.cts +128 -0
  27. package/dist/emoji-image.d.ts +25 -0
  28. package/dist/emoji-image.js +80 -0
  29. package/dist/emoji-model.d.ts +66 -0
  30. package/dist/emoji-model.js +108 -0
  31. package/dist/emoji.cjs +871 -0
  32. package/dist/emoji.d.cts +57 -0
  33. package/dist/emoji.d.ts +6 -0
  34. package/dist/emoji.js +18 -0
  35. package/dist/index.cjs +1503 -540
  36. package/dist/index.d.cts +245 -65
  37. package/dist/index.d.ts +3 -2
  38. package/dist/index.js +11 -2
  39. package/dist/markdown-model.d.ts +75 -0
  40. package/dist/markdown-model.js +438 -0
  41. package/dist/markdown.cjs +585 -0
  42. package/dist/markdown.d.cts +115 -0
  43. package/dist/markdown.d.ts +4 -0
  44. package/dist/markdown.js +9 -0
  45. package/dist/overlay-position-CEGSz6Z2.d.cts +59 -0
  46. package/dist/relationship-graph-model.d.ts +112 -1
  47. package/dist/relationship-graph-model.js +420 -1
  48. package/dist/rich-text/emoji-items.d.ts +14 -0
  49. package/dist/rich-text/emoji-items.js +7 -0
  50. package/dist/rich-text/pipeline.d.ts +91 -0
  51. package/dist/rich-text/pipeline.js +379 -0
  52. package/dist/rich-text/round-trip.d.ts +24 -0
  53. package/dist/rich-text/round-trip.js +11 -0
  54. package/dist/rich-text.cjs +2578 -0
  55. package/dist/rich-text.d.cts +148 -0
  56. package/dist/rich-text.d.ts +8 -0
  57. package/dist/rich-text.js +10 -0
  58. package/dist/url-policy.d.ts +9 -0
  59. package/dist/url-policy.js +11 -0
  60. package/package.json +82 -1
@@ -1,11 +1,88 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useMemo, useRef, useState } from "react";
4
- import { boundRelationshipGraph, layoutRelationshipGraph, relationshipNeighbors, traverseRelationshipNode } from "../relationship-graph-model.js";
4
+ import { boundRelationshipGraph, layoutRelationshipGraph, traverseRelationshipNode } from "../relationship-graph-model.js";
5
5
  import { cx } from "../cx.js";
6
+ import { LayeredRelationshipGraph } from "./RelationshipGraphLayered.js";
7
+ import { EquivalentRelationshipList } from "./RelationshipGraphList.js";
8
+ const DEFAULT_RELATIONSHIP_GRAPH_LABELS = {
9
+ graph: "Relationship graph",
10
+ controls: "Graph view controls",
11
+ list: "Accessible relationship list",
12
+ listHeading: "Relationship list",
13
+ legend: "Relationship types",
14
+ panLeft: "Pan left",
15
+ panRight: "Pan right",
16
+ panUp: "Pan up",
17
+ panDown: "Pan down",
18
+ left: "Left",
19
+ right: "Right",
20
+ up: "Up",
21
+ down: "Down",
22
+ zoomIn: "Zoom in",
23
+ zoomOut: "Zoom out",
24
+ fit: "Fit",
25
+ reset: "Reset view",
26
+ zoomLevel: (percent) => `Zoom ${percent}%`,
27
+ bounded: (shown, omitted) => `Showing ${shown} nodes; ${omitted} outside this bounded neighborhood.`,
28
+ graphSummary: (nodes, edges) => `Graph with ${nodes} nodes and ${edges} relationships`,
29
+ relationship: "Relationship",
30
+ conflict: "conflict",
31
+ connectedTo: (names) => `Connected to ${names}`,
32
+ noConnections: "No visible connections",
33
+ expandNeighbors: "Expand neighbors",
34
+ openDetails: "Open details",
35
+ column: (depth, count) => `${depth === 0 ? "Start" : `${depth} ${depth === 1 ? "hop" : "hops"}`} \xB7 ${count}`,
36
+ cluster: (count, type, edgeLabel) => `${count} \xD7 ${type ?? "items"}${edgeLabel ? ` \xB7 ${edgeLabel}` : ""}`,
37
+ expandCluster: "Show all",
38
+ collapseCluster: (count, type) => `Collapse ${count} ${type ?? "items"}`,
39
+ cycle: "cycle",
40
+ relationships: (count) => `${count} ${count === 1 ? "relationship" : "relationships"}`,
41
+ roleDescription: "relationship map",
42
+ keyboardHint: "Arrow keys move between connected items, Enter selects, Home returns to the start.",
43
+ loading: "Loading relationships\u2026",
44
+ retry: "Try again",
45
+ emptyTitle: "No relationships to display.",
46
+ emptyHint: "Adjust the neighborhood or filters to include a node."
47
+ };
6
48
  const EMPTY_IDS = /* @__PURE__ */ new Set();
7
49
  const EMPTY_LEGEND = [];
8
- function RelationshipGraph({
50
+ const RADIAL_SCALE_X = 6;
51
+ const RADIAL_SCALE_Y = 4;
52
+ const RADIAL_NODE_HALF_W = 55;
53
+ const RADIAL_NODE_HALF_H = 18;
54
+ const RADIAL_MARGIN = 12;
55
+ function radialViewBox(nodes) {
56
+ if (nodes.length === 0) return "0 0 600 400";
57
+ const xs = nodes.map((node) => node.x * RADIAL_SCALE_X);
58
+ const ys = nodes.map((node) => node.y * RADIAL_SCALE_Y);
59
+ const minX = Math.min(...xs) - RADIAL_NODE_HALF_W - RADIAL_MARGIN;
60
+ const maxX = Math.max(...xs) + RADIAL_NODE_HALF_W + RADIAL_MARGIN;
61
+ const minY = Math.min(...ys) - RADIAL_NODE_HALF_H - RADIAL_MARGIN;
62
+ const maxY = Math.max(...ys) + RADIAL_NODE_HALF_H + RADIAL_MARGIN;
63
+ const round = (value) => Number(value.toFixed(2));
64
+ const width = Math.max(round(maxX - minX), 600);
65
+ const height = Math.max(round(maxY - minY), 400);
66
+ return `${round(minX)} ${round(minY)} ${width} ${height}`;
67
+ }
68
+ function RelationshipGraph(props) {
69
+ const labels = useMemo(() => ({ ...DEFAULT_RELATIONSHIP_GRAPH_LABELS, ...props.labels }), [props.labels]);
70
+ const stateClass = cx("uix-relationship-graph uix-relationship-graph--state", props.className);
71
+ if (props.loading) return /* @__PURE__ */ jsx("div", { className: stateClass, role: "status", children: labels.loading });
72
+ if (props.error) {
73
+ return /* @__PURE__ */ jsxs("div", { className: stateClass, role: "alert", children: [
74
+ /* @__PURE__ */ jsx("p", { children: props.error }),
75
+ props.onRetry && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary", onClick: props.onRetry, children: labels.retry })
76
+ ] });
77
+ }
78
+ if (props.nodes.length === 0) return /* @__PURE__ */ jsxs("div", { className: stateClass, children: [
79
+ /* @__PURE__ */ jsx("p", { children: labels.emptyTitle }),
80
+ /* @__PURE__ */ jsx("p", { children: labels.emptyHint })
81
+ ] });
82
+ if (props.layout === "layered") return /* @__PURE__ */ jsx(LayeredRelationshipGraph, { ...props, labels });
83
+ return /* @__PURE__ */ jsx(RadialRelationshipGraph, { ...props, labels });
84
+ }
85
+ function RadialRelationshipGraph({
9
86
  nodes,
10
87
  edges,
11
88
  selectedId,
@@ -19,10 +96,9 @@ function RelationshipGraph({
19
96
  highlightedEdgeIds = EMPTY_IDS,
20
97
  conflictedEdgeIds = EMPTY_IDS,
21
98
  legend = EMPTY_LEGEND,
22
- loading,
23
- error,
24
- onRetry,
25
- className
99
+ className,
100
+ labels,
101
+ showList = true
26
102
  }) {
27
103
  const bounded = useMemo(() => boundRelationshipGraph(nodes, edges, maxNodes), [nodes, edges, maxNodes]);
28
104
  const positioned = useMemo(() => layoutRelationshipGraph(bounded.nodes, bounded.edges, selectedId), [bounded, selectedId]);
@@ -53,49 +129,30 @@ function RelationshipGraph({
53
129
  setZoom(1);
54
130
  setPan({ x: 0, y: 0 });
55
131
  };
56
- if (loading) return /* @__PURE__ */ jsx("div", { className: cx("uix-relationship-graph uix-relationship-graph--state", className), role: "status", children: "Loading relationships\u2026" });
57
- if (error) return /* @__PURE__ */ jsxs("div", { className: cx("uix-relationship-graph uix-relationship-graph--state", className), role: "alert", children: [
58
- /* @__PURE__ */ jsx("p", { children: error }),
59
- onRetry && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary", onClick: onRetry, children: "Try again" })
60
- ] });
61
- if (bounded.nodes.length === 0) return /* @__PURE__ */ jsxs("div", { className: cx("uix-relationship-graph uix-relationship-graph--state", className), children: [
62
- /* @__PURE__ */ jsx("p", { children: "No relationships to display." }),
63
- /* @__PURE__ */ jsx("p", { children: "Adjust the neighborhood or filters to include a node." })
64
- ] });
65
- return /* @__PURE__ */ jsxs("section", { className: cx("uix-relationship-graph", `uix-relationship-graph--${density}`, className), "aria-label": "Relationship graph", children: [
66
- /* @__PURE__ */ jsxs("div", { className: "uix-relationship-graph__toolbar", "aria-label": "Graph view controls", children: [
67
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPan((value) => ({ ...value, x: value.x - 24 })), "aria-label": "Pan left", children: "Left" }),
68
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPan((value) => ({ ...value, y: value.y - 24 })), "aria-label": "Pan up", children: "Up" }),
69
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPan((value) => ({ ...value, y: value.y + 24 })), "aria-label": "Pan down", children: "Down" }),
70
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPan((value) => ({ ...value, x: value.x + 24 })), "aria-label": "Pan right", children: "Right" }),
71
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setZoom((value) => Math.min(2, Number((value + 0.2).toFixed(1)))), "aria-label": "Zoom in", children: "Zoom in" }),
72
- /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setZoom((value) => Math.max(0.5, Number((value - 0.2).toFixed(1)))), "aria-label": "Zoom out", children: "Zoom out" }),
73
- /* @__PURE__ */ jsx("button", { type: "button", onClick: reset, children: "Reset view" }),
74
- /* @__PURE__ */ jsxs("span", { "aria-live": "polite", children: [
75
- "Zoom ",
76
- Math.round(zoom * 100),
77
- "%"
78
- ] })
79
- ] }),
80
- bounded.omittedNodeCount > 0 && /* @__PURE__ */ jsxs("p", { className: "uix-relationship-graph__bounded", role: "status", children: [
81
- "Showing ",
82
- bounded.nodes.length,
83
- " nodes; ",
84
- bounded.omittedNodeCount,
85
- " outside this bounded neighborhood."
132
+ return /* @__PURE__ */ jsxs("section", { className: cx("uix-relationship-graph", `uix-relationship-graph--${density}`, className), "aria-label": labels.graph, children: [
133
+ /* @__PURE__ */ jsxs("div", { className: "uix-relationship-graph__toolbar", "aria-label": labels.controls, children: [
134
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPan((value) => ({ ...value, x: value.x - 24 })), "aria-label": labels.panLeft, children: labels.left }),
135
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPan((value) => ({ ...value, y: value.y - 24 })), "aria-label": labels.panUp, children: labels.up }),
136
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPan((value) => ({ ...value, y: value.y + 24 })), "aria-label": labels.panDown, children: labels.down }),
137
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPan((value) => ({ ...value, x: value.x + 24 })), "aria-label": labels.panRight, children: labels.right }),
138
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setZoom((value) => Math.min(2, Number((value + 0.2).toFixed(1)))), "aria-label": labels.zoomIn, children: labels.zoomIn }),
139
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setZoom((value) => Math.max(0.5, Number((value - 0.2).toFixed(1)))), "aria-label": labels.zoomOut, children: labels.zoomOut }),
140
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: reset, children: labels.reset }),
141
+ /* @__PURE__ */ jsx("span", { "aria-live": "polite", children: labels.zoomLevel(Math.round(zoom * 100)) })
86
142
  ] }),
87
- /* @__PURE__ */ jsx("div", { className: "uix-relationship-graph__visual", children: /* @__PURE__ */ jsx("svg", { ref: svgRef, viewBox: "0 0 600 400", role: "group", "aria-label": `Graph with ${bounded.nodes.length} nodes and ${bounded.edges.length} relationships`, children: /* @__PURE__ */ jsxs("g", { transform: `translate(${pan.x} ${pan.y}) scale(${zoom})`, children: [
143
+ bounded.omittedNodeCount > 0 && /* @__PURE__ */ jsx("p", { className: "uix-relationship-graph__bounded", role: "status", children: labels.bounded(bounded.nodes.length, bounded.omittedNodeCount) }),
144
+ /* @__PURE__ */ jsx("div", { className: "uix-relationship-graph__visual", children: /* @__PURE__ */ jsx("svg", { ref: svgRef, viewBox: radialViewBox(positioned), role: "group", "aria-label": labels.graphSummary(bounded.nodes.length, bounded.edges.length), children: /* @__PURE__ */ jsxs("g", { transform: `translate(${pan.x} ${pan.y}) scale(${zoom})`, children: [
88
145
  bounded.edges.map((edge) => {
89
146
  const source = positionedById.get(edge.source);
90
147
  const target = positionedById.get(edge.target);
91
- return /* @__PURE__ */ jsx("line", { x1: source.x * 6, y1: source.y * 4, x2: target.x * 6, y2: target.y * 4, className: "uix-relationship-graph__edge", "data-highlighted": highlightedEdgeIds.has(edge.id) || void 0, "data-conflicted": conflictedEdgeIds.has(edge.id) || void 0, children: /* @__PURE__ */ jsx("title", { children: edge.label ?? edge.type ?? "Relationship" }) }, edge.id);
148
+ return /* @__PURE__ */ jsx("line", { x1: source.x * RADIAL_SCALE_X, y1: source.y * RADIAL_SCALE_Y, x2: target.x * RADIAL_SCALE_X, y2: target.y * RADIAL_SCALE_Y, className: "uix-relationship-graph__edge", "data-highlighted": highlightedEdgeIds.has(edge.id) || void 0, "data-conflicted": conflictedEdgeIds.has(edge.id) || void 0, children: /* @__PURE__ */ jsx("title", { children: edge.label ?? edge.type ?? labels.relationship }) }, edge.id);
92
149
  }),
93
150
  positioned.map((node) => /* @__PURE__ */ jsxs(
94
151
  "g",
95
152
  {
96
- transform: `translate(${node.x * 6} ${node.y * 4})`,
153
+ transform: `translate(${node.x * RADIAL_SCALE_X} ${node.y * RADIAL_SCALE_Y})`,
97
154
  role: "button",
98
- "aria-label": `${node.label}${node.type ? `, ${node.type}` : ""}${conflictedNodeIds.has(node.id) ? ", conflict" : ""}`,
155
+ "aria-label": `${node.label}${node.type ? `, ${node.type}` : ""}${conflictedNodeIds.has(node.id) ? `, ${labels.conflict}` : ""}`,
99
156
  tabIndex: focusedId === node.id ? 0 : -1,
100
157
  "data-node-id": node.id,
101
158
  className: "uix-relationship-graph__node",
@@ -106,33 +163,19 @@ function RelationshipGraph({
106
163
  onKeyDown: (event) => onNodeKeyDown(event, node.id),
107
164
  onClick: () => select(node.id),
108
165
  children: [
109
- /* @__PURE__ */ jsx("rect", { x: "-55", y: "-18", width: "110", height: "36", rx: "8" }),
166
+ /* @__PURE__ */ jsx("title", { children: node.label }),
167
+ /* @__PURE__ */ jsx("rect", { x: -RADIAL_NODE_HALF_W, y: -RADIAL_NODE_HALF_H, width: RADIAL_NODE_HALF_W * 2, height: RADIAL_NODE_HALF_H * 2, rx: "8" }),
110
168
  /* @__PURE__ */ jsx("text", { textAnchor: "middle", dominantBaseline: "middle", children: node.label.length > 16 ? `${node.label.slice(0, 15)}\u2026` : node.label })
111
169
  ]
112
170
  },
113
171
  node.id
114
172
  ))
115
173
  ] }) }) }),
116
- legend.length > 0 && /* @__PURE__ */ jsx("ul", { className: "uix-relationship-graph__legend", "aria-label": "Relationship types", children: legend.map((item) => /* @__PURE__ */ jsx("li", { "data-type": item.id, children: item.label }, item.id)) }),
117
- /* @__PURE__ */ jsxs("div", { className: "uix-relationship-graph__list", role: "region", "aria-label": "Accessible relationship list", children: [
118
- /* @__PURE__ */ jsx("h3", { children: "Relationship list" }),
119
- /* @__PURE__ */ jsx("ul", { children: bounded.nodes.map((node) => {
120
- const neighbors = relationshipNeighbors(node.id, bounded.edges).map((id) => bounded.nodes.find((item) => item.id === id)?.label ?? id);
121
- return /* @__PURE__ */ jsxs("li", { "data-selected": selectedId === node.id || void 0, children: [
122
- /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => select(node.id), "aria-pressed": selectedId === node.id, children: [
123
- /* @__PURE__ */ jsx("strong", { children: node.label }),
124
- node.type && /* @__PURE__ */ jsx("span", { children: node.type }),
125
- /* @__PURE__ */ jsx("span", { children: neighbors.length ? `Connected to ${neighbors.join(", ")}` : "No visible connections" })
126
- ] }),
127
- /* @__PURE__ */ jsxs("span", { className: "uix-relationship-graph__list-actions", children: [
128
- onExpandNeighbors && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onExpandNeighbors(node), children: "Expand neighbors" }),
129
- onOpenDetails && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onOpenDetails(node), children: "Open details" })
130
- ] })
131
- ] }, node.id);
132
- }) })
133
- ] })
174
+ legend.length > 0 && /* @__PURE__ */ jsx("ul", { className: "uix-relationship-graph__legend", "aria-label": labels.legend, children: legend.map((item) => /* @__PURE__ */ jsx("li", { "data-type": item.id, children: item.label }, item.id)) }),
175
+ showList && /* @__PURE__ */ jsx(EquivalentRelationshipList, { nodes: bounded.nodes, edges: bounded.edges, selectedId, labels, onSelect: select, onExpandNeighbors, onOpenDetails })
134
176
  ] });
135
177
  }
136
178
  export {
179
+ DEFAULT_RELATIONSHIP_GRAPH_LABELS,
137
180
  RelationshipGraph
138
181
  };
@@ -0,0 +1,8 @@
1
+ import * as react from 'react';
2
+ import { ResolvedRelationshipGraphProps } from './RelationshipGraph.js';
3
+ import '../relationship-graph-model.js';
4
+
5
+ /** `RelationshipGraph layout="layered"` (ADR-0003). Internal — reached through RelationshipGraph. */
6
+ declare function LayeredRelationshipGraph({ nodes, edges, selectedId, onSelect, onExpandNeighbors, onOpenDetails, maxNodes, density, highlightedNodeIds, conflictedNodeIds, highlightedEdgeIds, conflictedEdgeIds, legend, className, labels, rootId, renderNode, nodeAriaLabel, renderCluster, clusterThreshold, expandedClusterIds, onToggleCluster, dimmedNodeIds, edgeLabels, showList, height, focusId, onFocusChange, }: ResolvedRelationshipGraphProps): react.JSX.Element;
7
+
8
+ export { LayeredRelationshipGraph };
@@ -0,0 +1,420 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
4
+ import { boundRelationshipGraph, layeredNeighbor, layoutLayeredGraph } from "../relationship-graph-model.js";
5
+ import { cx } from "../cx.js";
6
+ import { EquivalentRelationshipList } from "./RelationshipGraphList.js";
7
+ const EMPTY_IDS = /* @__PURE__ */ new Set();
8
+ const MIN_ZOOM = 0.4;
9
+ const MAX_ZOOM = 1.5;
10
+ const READABLE_ZOOM = 0.75;
11
+ const ZOOM_STEP = 1.2;
12
+ const PAN_STEP = 64;
13
+ const FOCUS_MARGIN = 24;
14
+ const NAV_KEYS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End"];
15
+ const clampZoom = (zoom) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
16
+ function initialView(layout, size) {
17
+ if (size.width === 0 || size.height === 0 || layout.width === 0) return { x: 0, y: 0, zoom: 1 };
18
+ const fit = Math.min(size.width / layout.width, size.height / layout.height);
19
+ if (fit < READABLE_ZOOM) return { x: 0, y: 0, zoom: READABLE_ZOOM };
20
+ const zoom = Math.min(fit, 1);
21
+ return { x: (size.width - layout.width * zoom) / 2, y: Math.max(0, (size.height - layout.height * zoom) / 2), zoom };
22
+ }
23
+ function fitView(layout, size) {
24
+ if (size.width === 0 || layout.width === 0) return { x: 0, y: 0, zoom: 1 };
25
+ const zoom = clampZoom(Math.min(size.width / layout.width, size.height / layout.height, 1));
26
+ return { x: (size.width - layout.width * zoom) / 2, y: Math.max(0, (size.height - layout.height * zoom) / 2), zoom };
27
+ }
28
+ const arrowPoints = "0,0 -8,-4.5 -8,4.5";
29
+ function LayeredRelationshipGraph({
30
+ nodes,
31
+ edges,
32
+ selectedId,
33
+ onSelect,
34
+ onExpandNeighbors,
35
+ onOpenDetails,
36
+ maxNodes = 400,
37
+ density = "standard",
38
+ highlightedNodeIds = EMPTY_IDS,
39
+ conflictedNodeIds = EMPTY_IDS,
40
+ highlightedEdgeIds = EMPTY_IDS,
41
+ conflictedEdgeIds = EMPTY_IDS,
42
+ legend = [],
43
+ className,
44
+ labels,
45
+ rootId,
46
+ renderNode,
47
+ nodeAriaLabel,
48
+ renderCluster,
49
+ clusterThreshold,
50
+ expandedClusterIds,
51
+ onToggleCluster,
52
+ dimmedNodeIds = EMPTY_IDS,
53
+ edgeLabels = "auto",
54
+ showList = true,
55
+ height,
56
+ focusId,
57
+ onFocusChange
58
+ }) {
59
+ const hintId = useId();
60
+ const bounded = useMemo(() => boundRelationshipGraph(nodes, edges, maxNodes), [nodes, edges, maxNodes]);
61
+ const [localExpanded, setLocalExpanded] = useState(EMPTY_IDS);
62
+ const expanded = expandedClusterIds ?? localExpanded;
63
+ const layout = useMemo(
64
+ () => layoutLayeredGraph(bounded.nodes, bounded.edges, {
65
+ ...rootId !== void 0 ? { rootId } : {},
66
+ ...clusterThreshold !== void 0 ? { clusterThreshold } : {},
67
+ expandedClusterIds: expanded,
68
+ density
69
+ }),
70
+ [bounded, rootId, clusterThreshold, expanded, density]
71
+ );
72
+ const itemById = useMemo(() => new Map(layout.items.map((item) => [item.id, item])), [layout]);
73
+ const viewportRef = useRef(null);
74
+ const itemRefs = useRef(/* @__PURE__ */ new Map());
75
+ const [size, setSize] = useState({ width: 0, height: 0 });
76
+ const [view, setView] = useState({ x: 0, y: 0, zoom: 1 });
77
+ const [animate, setAnimate] = useState(false);
78
+ const [localFocus, setLocalFocus] = useState(void 0);
79
+ const [hoverId, setHoverId] = useState(void 0);
80
+ const viewRef = useRef(view);
81
+ viewRef.current = view;
82
+ const layoutRef = useRef(layout);
83
+ layoutRef.current = layout;
84
+ const pendingFocus = useRef(void 0);
85
+ const initialisedFor = useRef(void 0);
86
+ const rootItemId = layout.rootId !== void 0 ? layout.itemOf[layout.rootId] : layout.items[0]?.id;
87
+ const requestedFocus = focusId !== void 0 ? layout.itemOf[focusId] ?? focusId : localFocus;
88
+ const focusedId = requestedFocus !== void 0 && itemById.has(requestedFocus) ? requestedFocus : rootItemId;
89
+ const selectedItemId = selectedId !== void 0 ? layout.itemOf[selectedId] : void 0;
90
+ const activeIds = useMemo(
91
+ () => new Set([hoverId, selectedItemId].filter((id) => id !== void 0)),
92
+ [hoverId, selectedItemId]
93
+ );
94
+ useEffect(() => {
95
+ const element = viewportRef.current;
96
+ if (!element) return;
97
+ const measure = () => setSize({ width: element.clientWidth, height: element.clientHeight });
98
+ measure();
99
+ if (typeof ResizeObserver === "undefined") return;
100
+ const observer = new ResizeObserver(measure);
101
+ observer.observe(element);
102
+ return () => observer.disconnect();
103
+ }, []);
104
+ useEffect(() => {
105
+ if (size.width === 0) return;
106
+ const key = `${layout.rootId ?? ""}`;
107
+ if (initialisedFor.current === key) return;
108
+ initialisedFor.current = key;
109
+ setView(initialView(layout, size));
110
+ }, [layout, size]);
111
+ const moveView = useCallback((next, smooth) => {
112
+ setAnimate(smooth);
113
+ setView(next);
114
+ }, []);
115
+ const ensureVisible = useCallback((id, smooth) => {
116
+ const element = viewportRef.current;
117
+ const item = layoutRef.current.items.find((candidate) => candidate.id === id);
118
+ if (!element || !item) return;
119
+ const { x, y, zoom } = viewRef.current;
120
+ const width = element.clientWidth;
121
+ const heightPx = element.clientHeight;
122
+ const left = item.x * zoom + x;
123
+ const top = item.y * zoom + y;
124
+ const right = left + item.width * zoom;
125
+ const bottom = top + item.height * zoom;
126
+ let nextX = x;
127
+ let nextY = y;
128
+ if (left < FOCUS_MARGIN) nextX += FOCUS_MARGIN - left;
129
+ else if (right > width - FOCUS_MARGIN) nextX -= right - (width - FOCUS_MARGIN);
130
+ if (top < FOCUS_MARGIN) nextY += FOCUS_MARGIN - top;
131
+ else if (bottom > heightPx - FOCUS_MARGIN) nextY -= bottom - (heightPx - FOCUS_MARGIN);
132
+ if (nextX !== x || nextY !== y) moveView({ x: nextX, y: nextY, zoom }, smooth);
133
+ }, [moveView]);
134
+ const focusItem = useCallback((id) => {
135
+ setLocalFocus(id);
136
+ onFocusChange?.(id);
137
+ requestAnimationFrame(() => {
138
+ itemRefs.current.get(id)?.focus({ preventScroll: true });
139
+ ensureVisible(id, true);
140
+ });
141
+ }, [ensureVisible, onFocusChange]);
142
+ useEffect(() => {
143
+ const wanted = pendingFocus.current;
144
+ if (wanted !== void 0 && itemById.has(wanted)) {
145
+ pendingFocus.current = void 0;
146
+ focusItem(wanted);
147
+ }
148
+ }, [itemById, focusItem]);
149
+ const controlledTarget = focusId !== void 0 ? layout.itemOf[focusId] ?? focusId : void 0;
150
+ const controlledPresent = controlledTarget !== void 0 && itemById.has(controlledTarget);
151
+ const controlledKey = `${controlledTarget ?? ""}|${controlledPresent}`;
152
+ const lastControlledKey = useRef(controlledKey);
153
+ useEffect(() => {
154
+ if (lastControlledKey.current === controlledKey) return;
155
+ lastControlledKey.current = controlledKey;
156
+ if (!controlledPresent || controlledTarget === void 0) return;
157
+ requestAnimationFrame(() => {
158
+ itemRefs.current.get(controlledTarget)?.focus({ preventScroll: true });
159
+ ensureVisible(controlledTarget, true);
160
+ });
161
+ }, [controlledKey, controlledTarget, controlledPresent, ensureVisible]);
162
+ useEffect(() => {
163
+ const element = viewportRef.current;
164
+ if (!element) return;
165
+ const onWheel = (event) => {
166
+ if (!event.ctrlKey && !event.metaKey) return;
167
+ event.preventDefault();
168
+ const rect = element.getBoundingClientRect();
169
+ const px = event.clientX - rect.left;
170
+ const py = event.clientY - rect.top;
171
+ const current = viewRef.current;
172
+ const zoom = clampZoom(current.zoom * Math.exp(-event.deltaY * 2e-3));
173
+ const ratio = zoom / current.zoom;
174
+ setAnimate(false);
175
+ setView({ zoom, x: px - (px - current.x) * ratio, y: py - (py - current.y) * ratio });
176
+ };
177
+ element.addEventListener("wheel", onWheel, { passive: false });
178
+ return () => element.removeEventListener("wheel", onWheel);
179
+ }, []);
180
+ const pointers = useRef(/* @__PURE__ */ new Map());
181
+ const pinchDistance = useRef(void 0);
182
+ const onPointerDown = (event) => {
183
+ if (event.target.closest("button")) return;
184
+ event.currentTarget.setPointerCapture?.(event.pointerId);
185
+ pointers.current.set(event.pointerId, { x: event.clientX, y: event.clientY });
186
+ pinchDistance.current = void 0;
187
+ setAnimate(false);
188
+ };
189
+ const onPointerMove = (event) => {
190
+ const previous = pointers.current.get(event.pointerId);
191
+ if (!previous) return;
192
+ const current = { x: event.clientX, y: event.clientY };
193
+ pointers.current.set(event.pointerId, current);
194
+ const all = [...pointers.current.values()];
195
+ if (all.length >= 2) {
196
+ const [a, b] = all;
197
+ const distance = Math.hypot(a.x - b.x, a.y - b.y);
198
+ const rect = event.currentTarget.getBoundingClientRect();
199
+ const mx = (a.x + b.x) / 2 - rect.left;
200
+ const my = (a.y + b.y) / 2 - rect.top;
201
+ if (pinchDistance.current !== void 0 && pinchDistance.current > 0) {
202
+ const base = viewRef.current;
203
+ const zoom = clampZoom(base.zoom * (distance / pinchDistance.current));
204
+ const ratio = zoom / base.zoom;
205
+ setView({ zoom, x: mx - (mx - base.x) * ratio, y: my - (my - base.y) * ratio });
206
+ }
207
+ pinchDistance.current = distance;
208
+ return;
209
+ }
210
+ const dx = current.x - previous.x;
211
+ const dy = current.y - previous.y;
212
+ setView((value) => ({ ...value, x: value.x + dx, y: value.y + dy }));
213
+ };
214
+ const onPointerEnd = (event) => {
215
+ pointers.current.delete(event.pointerId);
216
+ pinchDistance.current = void 0;
217
+ };
218
+ const zoomBy = (factor) => {
219
+ const current = viewRef.current;
220
+ const zoom = clampZoom(current.zoom * factor);
221
+ const ratio = zoom / current.zoom;
222
+ const cx0 = size.width / 2;
223
+ const cy0 = size.height / 2;
224
+ moveView({ zoom, x: cx0 - (cx0 - current.x) * ratio, y: cy0 - (cy0 - current.y) * ratio }, true);
225
+ };
226
+ const panBy = (dx, dy) => moveView({ ...viewRef.current, x: viewRef.current.x + dx, y: viewRef.current.y + dy }, true);
227
+ const toggleCluster = (clusterId, focusAfter) => {
228
+ const isExpanded = expanded.has(clusterId);
229
+ pendingFocus.current = focusAfter;
230
+ if (expandedClusterIds === void 0) {
231
+ setLocalExpanded((previous) => {
232
+ const next = new Set(previous);
233
+ if (isExpanded) next.delete(clusterId);
234
+ else next.add(clusterId);
235
+ return next;
236
+ });
237
+ }
238
+ onToggleCluster?.(clusterId, !isExpanded);
239
+ };
240
+ const activate = (item) => {
241
+ if (item.cluster) {
242
+ toggleCluster(item.cluster.id, item.cluster.members[0]?.id ?? item.id);
243
+ return;
244
+ }
245
+ if (item.node) {
246
+ setLocalFocus(item.id);
247
+ onFocusChange?.(item.id);
248
+ onSelect?.(item.node);
249
+ }
250
+ };
251
+ const onItemKeyDown = (event, item) => {
252
+ if (NAV_KEYS.includes(event.key)) {
253
+ event.preventDefault();
254
+ const next = layeredNeighbor(layout, item.id, event.key);
255
+ if (next !== void 0) focusItem(next);
256
+ }
257
+ };
258
+ const labelFor = (item) => {
259
+ if (item.cluster) {
260
+ const cluster = item.cluster;
261
+ return `${labels.cluster(cluster.members.length, cluster.type, cluster.edgeLabel)}, ${labels.expandCluster}`;
262
+ }
263
+ const node = item.node;
264
+ const conflict = conflictedNodeIds.has(node.id) ? labels.conflict : void 0;
265
+ if (nodeAriaLabel) return [nodeAriaLabel(node), item.inCycle ? labels.cycle : void 0, conflict].filter(Boolean).join(", ");
266
+ return [node.label, node.type, node.description, labels.relationships(item.degree), item.inCycle ? labels.cycle : void 0, conflict].filter(Boolean).join(", ");
267
+ };
268
+ const showLabel = (edge) => {
269
+ if (edgeLabels === "never" || edge.label === void 0) return false;
270
+ if (edgeLabels === "always") return true;
271
+ return edge.autoLabel || activeIds.has(edge.source) || activeIds.has(edge.target) || focusedId === edge.source || focusedId === edge.target;
272
+ };
273
+ const viewportStyle = height !== void 0 ? { "--uix-relationship-graph-height": height } : {};
274
+ const stageStyle = {
275
+ width: layout.width,
276
+ height: layout.height,
277
+ transform: `translate(${view.x}px, ${view.y}px) scale(${view.zoom})`
278
+ };
279
+ return /* @__PURE__ */ jsxs("section", { className: cx("uix-relationship-graph", "uix-relationship-graph--layered", density === "compact" && "uix-relationship-graph--compact", className), "aria-label": labels.graph, children: [
280
+ /* @__PURE__ */ jsxs("div", { className: "uix-relationship-graph__toolbar", role: "group", "aria-label": labels.controls, children: [
281
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => zoomBy(ZOOM_STEP), disabled: view.zoom >= MAX_ZOOM, children: labels.zoomIn }),
282
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => zoomBy(1 / ZOOM_STEP), disabled: view.zoom <= MIN_ZOOM, children: labels.zoomOut }),
283
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => moveView(fitView(layout, size), true), children: labels.fit }),
284
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => moveView(initialView(layout, size), true), children: labels.reset }),
285
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => panBy(PAN_STEP, 0), "aria-label": labels.panLeft, children: labels.left }),
286
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => panBy(0, PAN_STEP), "aria-label": labels.panUp, children: labels.up }),
287
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => panBy(0, -PAN_STEP), "aria-label": labels.panDown, children: labels.down }),
288
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => panBy(-PAN_STEP, 0), "aria-label": labels.panRight, children: labels.right }),
289
+ /* @__PURE__ */ jsx("span", { "aria-live": "polite", children: labels.zoomLevel(Math.round(view.zoom * 100)) })
290
+ ] }),
291
+ bounded.omittedNodeCount > 0 && /* @__PURE__ */ jsx("p", { className: "uix-relationship-graph__bounded", role: "status", children: labels.bounded(bounded.nodes.length, bounded.omittedNodeCount) }),
292
+ /* @__PURE__ */ jsx("p", { id: hintId, className: "uix-visually-hidden", children: labels.keyboardHint }),
293
+ /* @__PURE__ */ jsx(
294
+ "div",
295
+ {
296
+ ref: viewportRef,
297
+ className: "uix-relationship-graph__viewport",
298
+ style: viewportStyle,
299
+ onPointerDown,
300
+ onPointerMove,
301
+ onPointerUp: onPointerEnd,
302
+ onPointerCancel: onPointerEnd,
303
+ children: /* @__PURE__ */ jsxs("div", { className: "uix-relationship-graph__stage", style: stageStyle, "data-animate": animate || void 0, onTransitionEnd: () => setAnimate(false), children: [
304
+ /* @__PURE__ */ jsx("svg", { className: "uix-relationship-graph__edges", width: layout.width, height: layout.height, "aria-hidden": "true", focusable: "false", children: layout.edges.map((edge) => {
305
+ const active = activeIds.has(edge.source) || activeIds.has(edge.target) || edge.edgeIds.some((id) => highlightedEdgeIds.has(id));
306
+ const angle = edge.arrowAngle * 180 / Math.PI;
307
+ return /* @__PURE__ */ jsxs(
308
+ "g",
309
+ {
310
+ className: "uix-relationship-graph__link",
311
+ "data-kind": edge.kind,
312
+ "data-active": active || void 0,
313
+ "data-emphasis": edge.emphasis || void 0,
314
+ "data-conflicted": edge.edgeIds.some((id) => conflictedEdgeIds.has(id)) || void 0,
315
+ "data-dimmed": dimmedNodeIds.has(edge.source) && dimmedNodeIds.has(edge.target) || void 0,
316
+ children: [
317
+ /* @__PURE__ */ jsx("path", { d: edge.path }),
318
+ edge.arrow !== "none" && /* @__PURE__ */ jsx("polygon", { points: arrowPoints, transform: `translate(${edge.arrowX} ${edge.arrowY}) rotate(${angle})` }),
319
+ showLabel(edge) && /* @__PURE__ */ jsx("text", { x: edge.labelX, y: edge.labelY, textAnchor: "middle", dominantBaseline: "middle", children: (edge.kind === "cycle" ? [...edge.labelLines, labels.cycle] : edge.labelLines).map((line, index, lines) => /* @__PURE__ */ jsx("tspan", { x: edge.labelX, dy: index === 0 ? `${-(lines.length - 1) * 0.55}em` : "1.1em", children: line }, index)) })
320
+ ]
321
+ },
322
+ edge.id
323
+ );
324
+ }) }),
325
+ layout.columns.map((column) => /* @__PURE__ */ jsx(
326
+ "div",
327
+ {
328
+ className: "uix-relationship-graph__column",
329
+ "aria-hidden": "true",
330
+ style: { left: column.x, width: layout.nodeWidth },
331
+ children: labels.column(column.depth, column.count)
332
+ },
333
+ column.depth
334
+ )),
335
+ /* @__PURE__ */ jsx("div", { role: "group", "aria-roledescription": labels.roleDescription, "aria-label": labels.graphSummary(bounded.nodes.length, bounded.edges.length), "aria-describedby": hintId, children: layout.items.map((item) => {
336
+ const selected = item.id === selectedItemId;
337
+ const dimmed = item.node ? dimmedNodeIds.has(item.node.id) : item.cluster.members.every((member) => dimmedNodeIds.has(member.id));
338
+ const body = item.cluster ? renderCluster?.(item.cluster) ?? /* @__PURE__ */ jsxs(Fragment, { children: [
339
+ /* @__PURE__ */ jsx("span", { className: "uix-relationship-graph__item-label", children: labels.cluster(item.cluster.members.length, item.cluster.type, item.cluster.edgeLabel) }),
340
+ /* @__PURE__ */ jsx("span", { className: "uix-relationship-graph__item-meta", children: labels.expandCluster })
341
+ ] }) : renderNode?.(item.node, { selected, dimmed }) ?? /* @__PURE__ */ jsxs(Fragment, { children: [
342
+ /* @__PURE__ */ jsx("span", { className: "uix-relationship-graph__item-label", children: item.node.label }),
343
+ (item.node.type || item.node.description) && /* @__PURE__ */ jsx("span", { className: "uix-relationship-graph__item-meta", children: [item.node.type, item.node.description].filter(Boolean).join(" \xB7 ") })
344
+ ] });
345
+ return /* @__PURE__ */ jsxs(
346
+ "button",
347
+ {
348
+ ref: (element) => {
349
+ if (element) itemRefs.current.set(item.id, element);
350
+ else itemRefs.current.delete(item.id);
351
+ },
352
+ type: "button",
353
+ className: "uix-relationship-graph__item",
354
+ "data-kind": item.kind,
355
+ "data-node-id": item.id,
356
+ "data-selected": selected || void 0,
357
+ "data-dimmed": dimmed || void 0,
358
+ "data-highlighted": item.node && highlightedNodeIds.has(item.node.id) || void 0,
359
+ "data-conflicted": item.node && conflictedNodeIds.has(item.node.id) || void 0,
360
+ "data-root": item.id === rootItemId || void 0,
361
+ style: { left: item.x, top: item.y, width: item.width, height: item.height },
362
+ tabIndex: item.id === focusedId ? 0 : -1,
363
+ "aria-label": labelFor(item),
364
+ ...item.cluster ? { "aria-expanded": false } : { "aria-pressed": selected },
365
+ onClick: () => activate(item),
366
+ onKeyDown: (event) => onItemKeyDown(event, item),
367
+ onFocus: () => {
368
+ if (item.id === focusedId) return;
369
+ setLocalFocus(item.id);
370
+ onFocusChange?.(item.id);
371
+ },
372
+ onPointerEnter: () => setHoverId(item.id),
373
+ onPointerLeave: () => setHoverId((current) => current === item.id ? void 0 : current),
374
+ children: [
375
+ body,
376
+ item.inCycle && /* @__PURE__ */ jsx("span", { className: "uix-relationship-graph__item-flag", "aria-hidden": "true", children: labels.cycle })
377
+ ]
378
+ },
379
+ item.id
380
+ );
381
+ }) }),
382
+ layout.items.filter((item) => item.expandedGroup).map((item) => {
383
+ const group = item.expandedGroup;
384
+ return /* @__PURE__ */ jsx(
385
+ "button",
386
+ {
387
+ type: "button",
388
+ className: "uix-relationship-graph__collapse",
389
+ "aria-expanded": "true",
390
+ style: { left: item.x, top: item.y - 26, width: item.width },
391
+ onClick: () => toggleCluster(group.id, group.id),
392
+ children: labels.collapseCluster(group.members.length, group.type)
393
+ },
394
+ `collapse:${group.id}`
395
+ );
396
+ })
397
+ ] })
398
+ }
399
+ ),
400
+ 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, children: entry.label }, entry.id)) }),
401
+ showList && /* @__PURE__ */ jsx(
402
+ EquivalentRelationshipList,
403
+ {
404
+ nodes: bounded.nodes,
405
+ edges: bounded.edges,
406
+ selectedId,
407
+ labels,
408
+ onSelect: (id) => {
409
+ const node = bounded.nodes.find((candidate) => candidate.id === id);
410
+ if (node) onSelect?.(node);
411
+ },
412
+ onExpandNeighbors,
413
+ onOpenDetails
414
+ }
415
+ )
416
+ ] });
417
+ }
418
+ export {
419
+ LayeredRelationshipGraph
420
+ };