@tensor_1/react 2.17.0 → 2.18.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.
@@ -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
+ };
@@ -0,0 +1,16 @@
1
+ import * as react from 'react';
2
+ import { RelationshipGraphNode, RelationshipGraphEdge } from '../relationship-graph-model.js';
3
+ import { RelationshipGraphLabels } from './RelationshipGraph.js';
4
+
5
+ /** The list every RelationshipGraph mode pairs with its picture (internal). */
6
+ declare function EquivalentRelationshipList({ nodes, edges, selectedId, labels, onSelect, onExpandNeighbors, onOpenDetails, }: {
7
+ nodes: RelationshipGraphNode[];
8
+ edges: RelationshipGraphEdge[];
9
+ selectedId: string | undefined;
10
+ labels: RelationshipGraphLabels;
11
+ onSelect: (id: string) => void;
12
+ onExpandNeighbors: ((node: RelationshipGraphNode) => void) | undefined;
13
+ onOpenDetails: ((node: RelationshipGraphNode) => void) | undefined;
14
+ }): react.JSX.Element;
15
+
16
+ export { EquivalentRelationshipList };
@@ -0,0 +1,33 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { relationshipNeighbors } from "../relationship-graph-model.js";
3
+ function EquivalentRelationshipList({
4
+ nodes,
5
+ edges,
6
+ selectedId,
7
+ labels,
8
+ onSelect,
9
+ onExpandNeighbors,
10
+ onOpenDetails
11
+ }) {
12
+ const labelById = new Map(nodes.map((node) => [node.id, node.label]));
13
+ return /* @__PURE__ */ jsxs("div", { className: "uix-relationship-graph__list", role: "region", "aria-label": labels.list, children: [
14
+ /* @__PURE__ */ jsx("h3", { children: labels.listHeading }),
15
+ /* @__PURE__ */ jsx("ul", { children: nodes.map((node) => {
16
+ const neighbors = relationshipNeighbors(node.id, edges).map((id) => labelById.get(id) ?? id);
17
+ return /* @__PURE__ */ jsxs("li", { "data-selected": selectedId === node.id || void 0, children: [
18
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => onSelect(node.id), "aria-pressed": selectedId === node.id, children: [
19
+ /* @__PURE__ */ jsx("strong", { children: node.label }),
20
+ node.type && /* @__PURE__ */ jsx("span", { children: node.type }),
21
+ /* @__PURE__ */ jsx("span", { children: neighbors.length ? labels.connectedTo(neighbors.join(", ")) : labels.noConnections })
22
+ ] }),
23
+ /* @__PURE__ */ jsxs("span", { className: "uix-relationship-graph__list-actions", children: [
24
+ onExpandNeighbors && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onExpandNeighbors(node), children: labels.expandNeighbors }),
25
+ onOpenDetails && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onOpenDetails(node), children: labels.openDetails })
26
+ ] })
27
+ ] }, node.id);
28
+ }) })
29
+ ] });
30
+ }
31
+ export {
32
+ EquivalentRelationshipList
33
+ };