@tensor_1/react 2.16.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.
Files changed (75) hide show
  1. package/README.md +29 -0
  2. package/dist/chart-preset.cjs +15 -3
  3. package/dist/chart.cjs +15 -3
  4. package/dist/components/Alert.js +15 -7
  5. package/dist/components/AppShell.d.ts +3 -1
  6. package/dist/components/AppShell.js +3 -1
  7. package/dist/components/Avatar.js +2 -1
  8. package/dist/components/Button.js +32 -16
  9. package/dist/components/Card.d.ts +3 -1
  10. package/dist/components/Card.js +2 -2
  11. package/dist/components/ChartCore.js +15 -3
  12. package/dist/components/CommandPalette.d.ts +4 -2
  13. package/dist/components/CommandPalette.js +66 -13
  14. package/dist/components/Comments.d.ts +5 -1
  15. package/dist/components/Comments.js +1 -1
  16. package/dist/components/Composer.d.ts +8 -1
  17. package/dist/components/Composer.js +7 -2
  18. package/dist/components/DetailLayout.js +1 -1
  19. package/dist/components/Drawer.d.ts +3 -4
  20. package/dist/components/Drawer.js +6 -4
  21. package/dist/components/Field.js +6 -3
  22. package/dist/components/Inbox.d.ts +13 -3
  23. package/dist/components/Inbox.js +62 -3
  24. package/dist/components/Input.d.ts +13 -2
  25. package/dist/components/Input.js +20 -5
  26. package/dist/components/Kanban.d.ts +15 -3
  27. package/dist/components/Kanban.js +42 -10
  28. package/dist/components/Meter.d.ts +3 -1
  29. package/dist/components/Meter.js +9 -1
  30. package/dist/components/Modal.d.ts +4 -5
  31. package/dist/components/Modal.js +4 -4
  32. package/dist/components/NavFavourites.js +11 -0
  33. package/dist/components/PageHeader.d.ts +3 -1
  34. package/dist/components/PageHeader.js +2 -2
  35. package/dist/components/Peek.d.ts +3 -4
  36. package/dist/components/Peek.js +9 -6
  37. package/dist/components/Progress.d.ts +3 -1
  38. package/dist/components/Progress.js +5 -3
  39. package/dist/components/Radio.d.ts +3 -1
  40. package/dist/components/Radio.js +6 -2
  41. package/dist/components/RelationshipGraph.d.ts +81 -3
  42. package/dist/components/RelationshipGraph.js +81 -57
  43. package/dist/components/RelationshipGraphLayered.d.ts +8 -0
  44. package/dist/components/RelationshipGraphLayered.js +420 -0
  45. package/dist/components/RelationshipGraphList.d.ts +16 -0
  46. package/dist/components/RelationshipGraphList.js +33 -0
  47. package/dist/components/Spinner.d.ts +3 -1
  48. package/dist/components/Spinner.js +3 -3
  49. package/dist/components/Stat.js +5 -1
  50. package/dist/components/States.d.ts +1 -0
  51. package/dist/components/States.js +7 -1
  52. package/dist/components/Table.d.ts +8 -1
  53. package/dist/components/Table.js +10 -6
  54. package/dist/components/Tabs.d.ts +13 -1
  55. package/dist/components/Tabs.js +79 -4
  56. package/dist/components/Timeline.d.ts +3 -3
  57. package/dist/components/Timeline.js +3 -3
  58. package/dist/components/Toast.js +42 -5
  59. package/dist/components/Tooltip.d.ts +3 -1
  60. package/dist/components/Tooltip.js +44 -5
  61. package/dist/components/Tree.d.ts +2 -0
  62. package/dist/components/Tree.js +26 -4
  63. package/dist/hooks/useDialog.d.ts +1 -1
  64. package/dist/hooks/useDialog.js +17 -2
  65. package/dist/hooks/useTable.d.ts +5 -0
  66. package/dist/hooks/useTable.js +20 -1
  67. package/dist/index.cjs +1791 -446
  68. package/dist/index.d.cts +311 -36
  69. package/dist/index.d.ts +4 -4
  70. package/dist/index.js +10 -3
  71. package/dist/relationship-graph-model.d.ts +112 -1
  72. package/dist/relationship-graph-model.js +420 -1
  73. package/dist/tree-model.d.ts +4 -0
  74. package/dist/tree-model.js +8 -0
  75. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -28,7 +28,7 @@ export { ToggleRow, ToggleRowProps } from './components/ToggleRow.js';
28
28
  export { CollapsibleSection, CollapsibleSectionProps } from './components/CollapsibleSection.js';
29
29
  export { List, ListItem, ListItemProps, ListProps } from './components/List.js';
30
30
  export { DescriptionItem, DescriptionItemProps, DescriptionList, DescriptionListItem, DescriptionListProps } from './components/DescriptionList.js';
31
- export { Tab, TabProps, Tabs, TabsProps } from './components/Tabs.js';
31
+ export { Tab, TabPanel, TabPanelProps, TabProps, Tabs, TabsProps } from './components/Tabs.js';
32
32
  export { AppShell, AppShellProps, ShellNav } from './components/AppShell.js';
33
33
  export { NavGroup, NavGroupProps, NavItem, NavItemProps, Sidebar, SidebarProps, SidebarSection, SidebarSectionProps, SubNavItem, SubNavItemProps } from './components/Sidebar.js';
34
34
  export { StarButton, StarButtonProps } from './components/StarButton.js';
@@ -61,7 +61,7 @@ export { Note, NoteProps, NoteTone, Prose, ProseProps } from './components/Prose
61
61
  export { Popover, PopoverProps } from './components/Popover.js';
62
62
  export { CommandGroup, CommandGroupProps, CommandItem, CommandItemProps, CommandPalette, CommandPaletteProps } from './components/CommandPalette.js';
63
63
  export { Inbox, InboxDetail, InboxDetailProps, InboxItem, InboxItemProps, InboxList, InboxListProps, InboxProps } from './components/Inbox.js';
64
- export { Kanban, KanbanCard, KanbanCardProps, KanbanColumn, KanbanColumnProps, KanbanProps } from './components/Kanban.js';
64
+ export { Kanban, KanbanCard, KanbanCardProps, KanbanColumn, KanbanColumnProps, KanbanMoveDirection, KanbanProps } from './components/Kanban.js';
65
65
  export { Tree, TreeNodeData, TreeNodeProps, TreeProps } from './components/Tree.js';
66
66
  export { FlatNode, TreeLike, TreeNavAction, flattenTree, treeNav } from './tree-model.js';
67
67
  export { ContentList, ContentListItem, ContentListItemProps, ContentListProps, EventRow, EventRowProps, FeaturedRundown, FeaturedRundownItem, FeaturedRundownItemProps, FeaturedRundownProps, FeaturedStage, FeaturedStageProps, NewsLead, NewsLeadProps, NoticeQueue, NoticeQueueProps, PageIntro, PageIntroProps, ResourceGrid, ResourceGridProps, SectionHead, SectionHeadProps, StatLine, StatLineItem, StatLineProps, StatusRow, StatusRowProps } from './components/EditorialHome.js';
@@ -72,8 +72,8 @@ export { BuilderCanvas, BuilderCanvasItem, BuilderCanvasProps, BuilderPaletteIte
72
72
  export { SchedulingCalendar, SchedulingCalendarEntry, SchedulingCalendarOverlay, SchedulingCalendarProps, SchedulingCalendarView, SchedulingEntryState, SchedulingOverlayKind } from './components/SchedulingCalendar.js';
73
73
  export { DateRangePicker, DateRangePickerProps } from './components/DateRangePicker.js';
74
74
  export { CalendarDay, DateRangeValue, ZonedDateSpan, addCalendarDays, addCalendarMonths, buildMonthGrid, enumerateDateSpan, isDateInRange, isDateUnavailable, selectRangeDate, startOfMonth, toDateKey, zonedDateKey, zonedDateSpan } from './calendar-model.js';
75
- export { RelationshipGraph, RelationshipGraphLegendItem, RelationshipGraphProps } from './components/RelationshipGraph.js';
76
- export { BoundedRelationshipGraph, PositionedRelationshipNode, RelationshipGraphEdge, RelationshipGraphNode, boundRelationshipGraph, layoutRelationshipGraph, relationshipNeighbors, traverseRelationshipNode } from './relationship-graph-model.js';
75
+ export { DEFAULT_RELATIONSHIP_GRAPH_LABELS, RelationshipGraph, RelationshipGraphLabels, RelationshipGraphLegendItem, RelationshipGraphProps } from './components/RelationshipGraph.js';
76
+ export { BoundedRelationshipGraph, LayeredColumn, LayeredEdgeClassification, LayeredEdgeKind, LayeredItem, LayeredLayout, LayeredLayoutOptions, LayeredNavigationKey, PositionedRelationshipNode, RelationshipGraphArrow, RelationshipGraphCluster, RelationshipGraphEdge, RelationshipGraphNode, RoutedEdge, boundRelationshipGraph, classifyLayeredEdges, clusterIdFor, layeredNeighbor, layoutLayeredGraph, layoutRelationshipGraph, relationshipNeighbors, traverseRelationshipNode, wrapEdgeLabel } from './relationship-graph-model.js';
77
77
  export { MatchBulkResult, MatchReview, MatchReviewAction, MatchReviewCandidate, MatchReviewField, MatchReviewProps } from './components/MatchReview.js';
78
78
  export { MetricInput, MetricInputProps } from './components/MetricInput.js';
79
79
  export { clampMetricValue, parseMetricValue, stepMetricValue } from './metric-model.js';
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import { ToggleRow } from "./components/ToggleRow.js";
28
28
  import { CollapsibleSection } from "./components/CollapsibleSection.js";
29
29
  import { List, ListItem } from "./components/List.js";
30
30
  import { DescriptionList, DescriptionItem } from "./components/DescriptionList.js";
31
- import { Tabs, Tab } from "./components/Tabs.js";
31
+ import { Tabs, Tab, TabPanel } from "./components/Tabs.js";
32
32
  import { AppShell } from "./components/AppShell.js";
33
33
  import { Sidebar, SidebarSection, NavItem, NavGroup, SubNavItem } from "./components/Sidebar.js";
34
34
  import { StarButton } from "./components/StarButton.js";
@@ -121,8 +121,8 @@ import {
121
121
  zonedDateKey,
122
122
  zonedDateSpan
123
123
  } from "./calendar-model.js";
124
- import { RelationshipGraph } from "./components/RelationshipGraph.js";
125
- import { boundRelationshipGraph, layoutRelationshipGraph, relationshipNeighbors, traverseRelationshipNode } from "./relationship-graph-model.js";
124
+ import { RelationshipGraph, DEFAULT_RELATIONSHIP_GRAPH_LABELS } from "./components/RelationshipGraph.js";
125
+ import { boundRelationshipGraph, layoutRelationshipGraph, relationshipNeighbors, traverseRelationshipNode, classifyLayeredEdges, clusterIdFor, layeredNeighbor, layoutLayeredGraph, wrapEdgeLabel } from "./relationship-graph-model.js";
126
126
  import { MatchReview } from "./components/MatchReview.js";
127
127
  import { MetricInput } from "./components/MetricInput.js";
128
128
  import { clampMetricValue, parseMetricValue, stepMetricValue } from "./metric-model.js";
@@ -163,6 +163,7 @@ export {
163
163
  ConfirmDialog,
164
164
  ContentList,
165
165
  ContentListItem,
166
+ DEFAULT_RELATIONSHIP_GRAPH_LABELS,
166
167
  DateRangePicker,
167
168
  DescriptionItem,
168
169
  DescriptionList,
@@ -247,6 +248,7 @@ export {
247
248
  SubNavItem,
248
249
  Switch,
249
250
  Tab,
251
+ TabPanel,
250
252
  Table,
251
253
  TableWrap,
252
254
  Tabs,
@@ -271,6 +273,8 @@ export {
271
273
  buildMonthGrid,
272
274
  buildThreeWayDiff,
273
275
  clampMetricValue,
276
+ classifyLayeredEdges,
277
+ clusterIdFor,
274
278
  computePosition,
275
279
  contrastRatio,
276
280
  cx,
@@ -284,6 +288,8 @@ export {
284
288
  isDateInRange,
285
289
  isDateUnavailable,
286
290
  isRuleGroup,
291
+ layeredNeighbor,
292
+ layoutLayeredGraph,
287
293
  layoutRelationshipGraph,
288
294
  mapRuleGroup,
289
295
  meetsContrast,
@@ -311,6 +317,7 @@ export {
311
317
  useTable,
312
318
  useVirtualRows,
313
319
  validateRuleDefinition,
320
+ wrapEdgeLabel,
314
321
  zonedDateKey,
315
322
  zonedDateSpan
316
323
  };
@@ -5,13 +5,21 @@ interface RelationshipGraphNode {
5
5
  description?: string;
6
6
  x?: number;
7
7
  y?: number;
8
+ /** Layered layout: the node's column. Omitted → BFS distance from the root along source→target. */
9
+ depth?: number;
8
10
  }
11
+ /** Where an edge's arrowhead sits: at the target (default), at the source, or nowhere. */
12
+ type RelationshipGraphArrow = 'forward' | 'backward' | 'none';
9
13
  interface RelationshipGraphEdge {
10
14
  id: string;
11
15
  source: string;
12
16
  target: string;
13
17
  type?: string;
14
18
  label?: string;
19
+ /** Layered layout: arrowhead placement (default `forward`). */
20
+ arrow?: RelationshipGraphArrow;
21
+ /** Layered layout: always draw this edge's label, whatever the label policy. */
22
+ emphasis?: boolean;
15
23
  }
16
24
  interface PositionedRelationshipNode extends RelationshipGraphNode {
17
25
  x: number;
@@ -27,5 +35,108 @@ declare function boundRelationshipGraph(nodes: RelationshipGraphNode[], edges: R
27
35
  declare function relationshipNeighbors(nodeId: string, edges: RelationshipGraphEdge[]): string[];
28
36
  declare function layoutRelationshipGraph(nodes: RelationshipGraphNode[], edges: RelationshipGraphEdge[], rootId?: string): PositionedRelationshipNode[];
29
37
  declare function traverseRelationshipNode(nodes: RelationshipGraphNode[], currentId: string | undefined, direction: -1 | 1): string | undefined;
38
+ type LayeredEdgeKind = 'forward' | 'lateral' | 'cycle';
39
+ interface LayeredLayoutOptions {
40
+ rootId?: string;
41
+ /** Minimum sibling group size that collapses into a cluster. Default 6; 0 disables. */
42
+ clusterThreshold?: number;
43
+ expandedClusterIds?: ReadonlySet<string>;
44
+ density?: 'compact' | 'standard';
45
+ }
46
+ interface RelationshipGraphCluster {
47
+ id: string;
48
+ parentId: string;
49
+ depth: number;
50
+ type?: string;
51
+ edgeType?: string;
52
+ edgeLabel?: string;
53
+ members: RelationshipGraphNode[];
54
+ }
55
+ interface LayeredItem {
56
+ /** Node id, or cluster id for a collapsed cluster. */
57
+ id: string;
58
+ kind: 'node' | 'cluster';
59
+ node?: RelationshipGraphNode;
60
+ cluster?: RelationshipGraphCluster;
61
+ depth: number;
62
+ /** Position within its column (0-based, top to bottom). */
63
+ index: number;
64
+ x: number;
65
+ y: number;
66
+ width: number;
67
+ height: number;
68
+ /** Forward parents / children as item ids, in column order. */
69
+ parents: string[];
70
+ children: string[];
71
+ /** Set on the first member of an expanded cluster: the group its collapse control belongs to. */
72
+ expandedGroup?: RelationshipGraphCluster;
73
+ /** True when a lateral or cycle edge touches this node. */
74
+ inCycle: boolean;
75
+ /** Edges (any kind) touching this item. */
76
+ degree: number;
77
+ }
78
+ interface RoutedEdge {
79
+ id: string;
80
+ /** Item ids (a cluster id when the edge enters a collapsed cluster). */
81
+ source: string;
82
+ target: string;
83
+ kind: LayeredEdgeKind;
84
+ /** Original edge ids drawn by this path (several for a cluster). */
85
+ edgeIds: string[];
86
+ type?: string;
87
+ label?: string;
88
+ arrow: RelationshipGraphArrow;
89
+ emphasis: boolean;
90
+ path: string;
91
+ labelX: number;
92
+ labelY: number;
93
+ /** Arrowhead tip and the direction it points in (radians, 0 = pointing right). */
94
+ arrowX: number;
95
+ arrowY: number;
96
+ arrowAngle: number;
97
+ /** Label policy `auto` shows this label without interaction. */
98
+ autoLabel: boolean;
99
+ /** The label wrapped to fit the column gap (at most two lines). */
100
+ labelLines: string[];
101
+ }
102
+ interface LayeredColumn {
103
+ depth: number;
104
+ x: number;
105
+ /** Underlying nodes in the column, cluster members included. */
106
+ count: number;
107
+ }
108
+ interface LayeredLayout {
109
+ rootId: string | undefined;
110
+ items: LayeredItem[];
111
+ edges: RoutedEdge[];
112
+ clusters: RelationshipGraphCluster[];
113
+ columns: LayeredColumn[];
114
+ /** Node id → id of the item that currently draws it (itself, or its cluster). */
115
+ itemOf: Record<string, string>;
116
+ width: number;
117
+ height: number;
118
+ nodeWidth: number;
119
+ nodeHeight: number;
120
+ headerHeight: number;
121
+ }
122
+ /** Word-wrap a label into at most two lines that fit `width`; overlong words end in an ellipsis. */
123
+ declare function wrapEdgeLabel(label: string, width: number): string[];
124
+ interface LayeredEdgeClassification {
125
+ depthById: Map<string, number>;
126
+ kindById: Map<string, LayeredEdgeKind>;
127
+ }
128
+ /**
129
+ * Column (depth) for every node and a kind for every drawable edge.
130
+ * Supplied `node.depth` wins; otherwise BFS from the root along source→target.
131
+ * Nodes the BFS cannot reach go to a trailing column. Self-loops and edges with
132
+ * a missing endpoint get no kind and are not drawn.
133
+ */
134
+ declare function classifyLayeredEdges(nodes: RelationshipGraphNode[], edges: RelationshipGraphEdge[], rootId?: string | undefined): LayeredEdgeClassification;
135
+ declare function clusterIdFor(parentId: string, type: string | undefined, edgeType: string | undefined): string;
136
+ /** Layered, clustered, crossing-reduced layout for `RelationshipGraph layout="layered"`. */
137
+ declare function layoutLayeredGraph(nodes: RelationshipGraphNode[], edges: RelationshipGraphEdge[], options?: LayeredLayoutOptions): LayeredLayout;
138
+ type LayeredNavigationKey = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'Home' | 'End';
139
+ /** Keyboard traversal over a layered layout: ←/→ along forward edges, ↑/↓ within a column. */
140
+ declare function layeredNeighbor(layout: LayeredLayout, currentId: string | undefined, key: LayeredNavigationKey): string | undefined;
30
141
 
31
- export { type BoundedRelationshipGraph, type PositionedRelationshipNode, type RelationshipGraphEdge, type RelationshipGraphNode, boundRelationshipGraph, layoutRelationshipGraph, relationshipNeighbors, traverseRelationshipNode };
142
+ export { type BoundedRelationshipGraph, type LayeredColumn, type LayeredEdgeClassification, type LayeredEdgeKind, type LayeredItem, type LayeredLayout, type LayeredLayoutOptions, type LayeredNavigationKey, type PositionedRelationshipNode, type RelationshipGraphArrow, type RelationshipGraphCluster, type RelationshipGraphEdge, type RelationshipGraphNode, type RoutedEdge, boundRelationshipGraph, classifyLayeredEdges, clusterIdFor, layeredNeighbor, layoutLayeredGraph, layoutRelationshipGraph, relationshipNeighbors, traverseRelationshipNode, wrapEdgeLabel };
@@ -60,9 +60,428 @@ function traverseRelationshipNode(nodes, currentId, direction) {
60
60
  const current = Math.max(0, nodes.findIndex((node) => node.id === currentId));
61
61
  return nodes[(current + direction + nodes.length) % nodes.length]?.id;
62
62
  }
63
+ const LAYERED_GEOMETRY = {
64
+ standard: { nodeWidth: 176, nodeHeight: 76, columnGap: 96, rowGap: 12 },
65
+ compact: { nodeWidth: 160, nodeHeight: 60, columnGap: 80, rowGap: 8 }
66
+ };
67
+ const LAYERED_PADDING = 16;
68
+ const LAYERED_HEADER = 28;
69
+ const GROUP_HEADER = 28;
70
+ const AUTO_LABEL_MAX_FANOUT = 4;
71
+ const LABEL_CHAR_WIDTH = 5.6;
72
+ const LABEL_MAX_LINES = 2;
73
+ function wrapEdgeLabel(label, width) {
74
+ const max = Math.max(4, Math.floor(width / LABEL_CHAR_WIDTH));
75
+ const clip = (word) => word.length > max ? `${word.slice(0, max - 1)}\u2026` : word;
76
+ const lines = [];
77
+ let current = "";
78
+ for (const word of label.split(/\s+/).filter(Boolean)) {
79
+ const next = current ? `${current} ${word}` : word;
80
+ if (next.length <= max) {
81
+ current = next;
82
+ continue;
83
+ }
84
+ if (current) lines.push(current);
85
+ current = clip(word);
86
+ if (lines.length === LABEL_MAX_LINES) break;
87
+ }
88
+ if (current && lines.length < LABEL_MAX_LINES) lines.push(current);
89
+ if (lines.length === LABEL_MAX_LINES) {
90
+ const consumed = lines.join(" ").replace(/…$/, "").length;
91
+ if (consumed < label.replace(/\s+/g, " ").trim().length) {
92
+ const last = lines[LABEL_MAX_LINES - 1];
93
+ lines[LABEL_MAX_LINES - 1] = last.endsWith("\u2026") ? last : `${last.slice(0, Math.max(1, max - 1))}\u2026`;
94
+ }
95
+ }
96
+ return lines;
97
+ }
98
+ const drawableEdge = (edge, ids) => ids.has(edge.source) && ids.has(edge.target) && edge.source !== edge.target;
99
+ function classifyLayeredEdges(nodes, edges, rootId = nodes[0]?.id) {
100
+ const ids = new Set(nodes.map((node) => node.id));
101
+ const root = rootId !== void 0 && ids.has(rootId) ? rootId : nodes[0]?.id;
102
+ const outgoing = /* @__PURE__ */ new Map();
103
+ for (const edge of edges) {
104
+ if (!drawableEdge(edge, ids)) continue;
105
+ const list = outgoing.get(edge.source);
106
+ if (list) list.push(edge.target);
107
+ else outgoing.set(edge.source, [edge.target]);
108
+ }
109
+ const bfs = /* @__PURE__ */ new Map();
110
+ if (root !== void 0) {
111
+ bfs.set(root, 0);
112
+ const queue = [root];
113
+ for (let head = 0; head < queue.length; head += 1) {
114
+ const current = queue[head];
115
+ for (const next of outgoing.get(current) ?? []) {
116
+ if (!bfs.has(next)) {
117
+ bfs.set(next, bfs.get(current) + 1);
118
+ queue.push(next);
119
+ }
120
+ }
121
+ }
122
+ }
123
+ const depthById = /* @__PURE__ */ new Map();
124
+ let deepest = 0;
125
+ for (const node of nodes) {
126
+ const supplied = node.depth !== void 0 && Number.isFinite(node.depth) ? Math.max(0, Math.floor(node.depth)) : void 0;
127
+ const depth = supplied ?? bfs.get(node.id);
128
+ if (depth === void 0) continue;
129
+ depthById.set(node.id, depth);
130
+ deepest = Math.max(deepest, depth);
131
+ }
132
+ for (const node of nodes) if (!depthById.has(node.id)) depthById.set(node.id, deepest + 1);
133
+ const forwardOut = /* @__PURE__ */ new Map();
134
+ const kindById = /* @__PURE__ */ new Map();
135
+ const backEdges = [];
136
+ for (const edge of edges) {
137
+ if (!drawableEdge(edge, ids)) continue;
138
+ const from = depthById.get(edge.source);
139
+ const to = depthById.get(edge.target);
140
+ if (to === from + 1) {
141
+ kindById.set(edge.id, "forward");
142
+ const list = forwardOut.get(edge.source);
143
+ if (list) list.push(edge.target);
144
+ else forwardOut.set(edge.source, [edge.target]);
145
+ } else if (to < from) {
146
+ backEdges.push(edge);
147
+ } else {
148
+ kindById.set(edge.id, "lateral");
149
+ }
150
+ }
151
+ for (const edge of backEdges) {
152
+ const limit = depthById.get(edge.source);
153
+ const seen = /* @__PURE__ */ new Set([edge.target]);
154
+ const stack = [edge.target];
155
+ let closes = false;
156
+ while (stack.length > 0 && !closes) {
157
+ const current = stack.pop();
158
+ for (const next of forwardOut.get(current) ?? []) {
159
+ if (next === edge.source) {
160
+ closes = true;
161
+ break;
162
+ }
163
+ if (!seen.has(next) && depthById.get(next) < limit) {
164
+ seen.add(next);
165
+ stack.push(next);
166
+ }
167
+ }
168
+ }
169
+ kindById.set(edge.id, closes ? "cycle" : "lateral");
170
+ }
171
+ return { depthById, kindById };
172
+ }
173
+ function clusterIdFor(parentId, type, edgeType) {
174
+ return `cluster:${parentId}:${type ?? ""}:${edgeType ?? ""}`;
175
+ }
176
+ const mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
177
+ const round1 = (value) => Math.round(value * 10) / 10;
178
+ function layoutLayeredGraph(nodes, edges, options = {}) {
179
+ const { nodeWidth, nodeHeight, columnGap, rowGap } = LAYERED_GEOMETRY[options.density ?? "standard"];
180
+ const threshold = options.clusterThreshold ?? 6;
181
+ const expanded = options.expandedClusterIds ?? /* @__PURE__ */ new Set();
182
+ const rootId = options.rootId !== void 0 && nodes.some((node) => node.id === options.rootId) ? options.rootId : nodes[0]?.id;
183
+ if (nodes.length === 0) {
184
+ return {
185
+ rootId,
186
+ items: [],
187
+ edges: [],
188
+ clusters: [],
189
+ columns: [],
190
+ itemOf: {},
191
+ width: 0,
192
+ height: 0,
193
+ nodeWidth,
194
+ nodeHeight,
195
+ headerHeight: LAYERED_HEADER
196
+ };
197
+ }
198
+ const { depthById, kindById } = classifyLayeredEdges(nodes, edges, rootId);
199
+ const drawable = edges.filter((edge) => kindById.has(edge.id));
200
+ const forwardParents = /* @__PURE__ */ new Map();
201
+ const forwardChildCount = /* @__PURE__ */ new Map();
202
+ const inCycle = /* @__PURE__ */ new Set();
203
+ const nodeDegree = /* @__PURE__ */ new Map();
204
+ for (const edge of drawable) {
205
+ nodeDegree.set(edge.source, (nodeDegree.get(edge.source) ?? 0) + 1);
206
+ nodeDegree.set(edge.target, (nodeDegree.get(edge.target) ?? 0) + 1);
207
+ if (kindById.get(edge.id) === "forward") {
208
+ const list = forwardParents.get(edge.target);
209
+ if (list) list.push(edge);
210
+ else forwardParents.set(edge.target, [edge]);
211
+ forwardChildCount.set(edge.source, (forwardChildCount.get(edge.source) ?? 0) + 1);
212
+ } else {
213
+ inCycle.add(edge.source);
214
+ inCycle.add(edge.target);
215
+ }
216
+ }
217
+ const groups = /* @__PURE__ */ new Map();
218
+ if (threshold > 0) {
219
+ for (const node of nodes) {
220
+ if (node.id === rootId || inCycle.has(node.id) || (forwardChildCount.get(node.id) ?? 0) > 0) continue;
221
+ const parents = forwardParents.get(node.id) ?? [];
222
+ if (parents.length !== 1) continue;
223
+ const via = parents[0];
224
+ const id = clusterIdFor(via.source, node.type, via.type);
225
+ const group = groups.get(id);
226
+ if (group) {
227
+ group.members.push(node);
228
+ continue;
229
+ }
230
+ const created = { id, parentId: via.source, depth: depthById.get(node.id), members: [node] };
231
+ if (node.type !== void 0) created.type = node.type;
232
+ if (via.type !== void 0) created.edgeType = via.type;
233
+ const edgeLabel = via.label ?? via.type;
234
+ if (edgeLabel !== void 0) created.edgeLabel = edgeLabel;
235
+ groups.set(id, created);
236
+ }
237
+ }
238
+ const clusterById = /* @__PURE__ */ new Map();
239
+ const itemOf = {};
240
+ const expandedGroupOf = /* @__PURE__ */ new Map();
241
+ for (const group of groups.values()) {
242
+ if (group.members.length < threshold) continue;
243
+ if (expanded.has(group.id)) {
244
+ for (const member of group.members) expandedGroupOf.set(member.id, group);
245
+ continue;
246
+ }
247
+ clusterById.set(group.id, group);
248
+ for (const member of group.members) itemOf[member.id] = group.id;
249
+ }
250
+ for (const node of nodes) if (itemOf[node.id] === void 0) itemOf[node.id] = node.id;
251
+ const columnsByDepth = /* @__PURE__ */ new Map();
252
+ const itemById = /* @__PURE__ */ new Map();
253
+ for (const node of nodes) {
254
+ const id = itemOf[node.id];
255
+ if (itemById.has(id)) continue;
256
+ const cluster = clusterById.get(id);
257
+ const depth = depthById.get(node.id);
258
+ const item = {
259
+ id,
260
+ kind: cluster ? "cluster" : "node",
261
+ depth,
262
+ index: 0,
263
+ x: 0,
264
+ y: 0,
265
+ width: nodeWidth,
266
+ height: nodeHeight,
267
+ parents: [],
268
+ children: [],
269
+ inCycle: cluster ? false : inCycle.has(node.id),
270
+ degree: cluster ? cluster.members.length : nodeDegree.get(node.id) ?? 0
271
+ };
272
+ if (cluster) item.cluster = cluster;
273
+ else item.node = node;
274
+ itemById.set(id, item);
275
+ const column = columnsByDepth.get(depth);
276
+ if (column) column.push(item);
277
+ else columnsByDepth.set(depth, [item]);
278
+ }
279
+ for (const edge of drawable) {
280
+ if (kindById.get(edge.id) !== "forward") continue;
281
+ const from = itemById.get(itemOf[edge.source]);
282
+ const to = itemById.get(itemOf[edge.target]);
283
+ if (!from.children.includes(to.id)) from.children.push(to.id);
284
+ if (!to.parents.includes(from.id)) to.parents.push(from.id);
285
+ }
286
+ const depths = [...columnsByDepth.keys()].sort((a, b) => a - b);
287
+ const position = /* @__PURE__ */ new Map();
288
+ const reindex = (column) => {
289
+ column.forEach((item, index) => {
290
+ item.index = index;
291
+ position.set(item.id, index);
292
+ });
293
+ };
294
+ for (const depth of depths) reindex(columnsByDepth.get(depth));
295
+ const groupOfItem = (item) => item.node ? expandedGroupOf.get(item.node.id) : void 0;
296
+ const sweep = (depth, neighbours) => {
297
+ const keyed = columnsByDepth.get(depth).map((item) => {
298
+ const around = neighbours(item).map((id) => position.get(id)).filter((value) => value !== void 0);
299
+ return { item, key: around.length > 0 ? mean(around) : item.index, tie: item.index };
300
+ });
301
+ keyed.sort((a, b) => a.key - b.key || a.tie - b.tie);
302
+ const ordered = [];
303
+ const placed = /* @__PURE__ */ new Set();
304
+ for (const { item } of keyed) {
305
+ if (placed.has(item.id)) continue;
306
+ const group = groupOfItem(item);
307
+ for (const candidate of group ? keyed.map((entry) => entry.item).filter((other) => groupOfItem(other) === group) : [item]) {
308
+ if (placed.has(candidate.id)) continue;
309
+ ordered.push(candidate);
310
+ placed.add(candidate.id);
311
+ }
312
+ }
313
+ columnsByDepth.set(depth, ordered);
314
+ reindex(ordered);
315
+ };
316
+ const inner = depths.slice(1);
317
+ for (const depth of inner) sweep(depth, (item) => item.parents);
318
+ for (const depth of [...inner].reverse()) sweep(depth, (item) => item.children);
319
+ for (const depth of inner) sweep(depth, (item) => item.parents);
320
+ const top = LAYERED_PADDING + LAYERED_HEADER;
321
+ const columns = [];
322
+ let width = 0;
323
+ let height = 0;
324
+ for (const depth of depths) {
325
+ const column = columnsByDepth.get(depth);
326
+ const x = LAYERED_PADDING + depth * (nodeWidth + columnGap);
327
+ let cursor = top;
328
+ let count = 0;
329
+ const ledGroups = /* @__PURE__ */ new Set();
330
+ for (const item of column) {
331
+ const parentCentres = item.parents.map((id) => itemById.get(id)).filter((parent) => parent.depth < depth).map((parent) => parent.y + parent.height / 2);
332
+ const group = groupOfItem(item);
333
+ const leadsGroup = group !== void 0 && !ledGroups.has(group);
334
+ if (group) ledGroups.add(group);
335
+ const floor = cursor + (leadsGroup ? GROUP_HEADER : 0);
336
+ const target = parentCentres.length > 0 ? mean(parentCentres) - item.height / 2 : floor;
337
+ item.x = x;
338
+ item.y = round1(Math.max(target, floor));
339
+ if (leadsGroup) item.expandedGroup = group;
340
+ cursor = item.y + item.height + rowGap;
341
+ count += item.cluster ? item.cluster.members.length : 1;
342
+ width = Math.max(width, x + item.width);
343
+ height = Math.max(height, item.y + item.height);
344
+ }
345
+ columns.push({ depth, x, count });
346
+ }
347
+ const byPosition = (a, b) => (position.get(a) ?? 0) - (position.get(b) ?? 0);
348
+ for (const item of itemById.values()) {
349
+ item.children.sort(byPosition);
350
+ item.parents.sort(byPosition);
351
+ }
352
+ const routed = /* @__PURE__ */ new Map();
353
+ for (const edge of drawable) {
354
+ const kind = kindById.get(edge.id);
355
+ const from = itemById.get(itemOf[edge.source]);
356
+ const to = itemById.get(itemOf[edge.target]);
357
+ const key = from.kind === "cluster" || to.kind === "cluster" ? `${from.id}->${to.id}` : edge.id;
358
+ const existing = routed.get(key);
359
+ if (existing) {
360
+ existing.edgeIds.push(edge.id);
361
+ continue;
362
+ }
363
+ let startX;
364
+ let startY;
365
+ let endX;
366
+ let endY;
367
+ let path;
368
+ let labelX;
369
+ let labelY;
370
+ let startAngle;
371
+ let endAngle;
372
+ if (kind === "forward") {
373
+ startX = from.x + from.width;
374
+ startY = from.y + from.height / 2;
375
+ endX = to.x;
376
+ endY = to.y + to.height / 2;
377
+ const bend = Math.max(columnGap / 2, 24);
378
+ path = `M${round1(startX)} ${round1(startY)} C${round1(startX + bend)} ${round1(startY)} ${round1(endX - bend)} ${round1(endY)} ${round1(endX)} ${round1(endY)}`;
379
+ labelX = (startX + endX) / 2;
380
+ labelY = (startY + endY) / 2;
381
+ startAngle = Math.PI;
382
+ endAngle = 0;
383
+ } else if (from.depth === to.depth) {
384
+ startX = from.x + from.width;
385
+ startY = from.y + from.height / 2;
386
+ endX = to.x + to.width;
387
+ endY = to.y + to.height / 2;
388
+ const bulge = Math.min(columnGap - 8, 20 + Math.abs(endY - startY) * 0.1);
389
+ path = `M${round1(startX)} ${round1(startY)} C${round1(startX + bulge)} ${round1(startY)} ${round1(endX + bulge)} ${round1(endY)} ${round1(endX)} ${round1(endY)}`;
390
+ labelX = startX + bulge * 0.75;
391
+ labelY = (startY + endY) / 2;
392
+ startAngle = Math.PI;
393
+ endAngle = Math.PI;
394
+ } else {
395
+ startX = from.x + from.width / 2;
396
+ startY = from.y + from.height;
397
+ endX = to.x + to.width / 2;
398
+ endY = to.y + to.height;
399
+ const dip = 28 + Math.abs(endX - startX) * 0.12;
400
+ const low = Math.max(startY, endY) + dip;
401
+ path = `M${round1(startX)} ${round1(startY)} C${round1(startX)} ${round1(low)} ${round1(endX)} ${round1(low)} ${round1(endX)} ${round1(endY)}`;
402
+ labelX = (startX + endX) / 2;
403
+ labelY = low - dip * 0.25;
404
+ startAngle = -Math.PI / 2;
405
+ endAngle = -Math.PI / 2;
406
+ height = Math.max(height, low);
407
+ }
408
+ const arrow = edge.arrow ?? "forward";
409
+ const atStart = arrow === "backward";
410
+ const entry = {
411
+ id: key,
412
+ source: from.id,
413
+ target: to.id,
414
+ kind,
415
+ edgeIds: [edge.id],
416
+ arrow,
417
+ emphasis: edge.emphasis === true,
418
+ path,
419
+ labelX: round1(labelX),
420
+ labelY: round1(labelY),
421
+ arrowX: round1(atStart ? startX : endX),
422
+ arrowY: round1(atStart ? startY : endY),
423
+ arrowAngle: atStart ? startAngle : endAngle,
424
+ labelLines: [],
425
+ autoLabel: to.kind !== "cluster" && (edge.emphasis === true || kind === "forward" && from.children.length <= AUTO_LABEL_MAX_FANOUT)
426
+ };
427
+ if (edge.type !== void 0) entry.type = edge.type;
428
+ const label = edge.label ?? edge.type;
429
+ if (label !== void 0) entry.label = label;
430
+ routed.set(key, entry);
431
+ }
432
+ const seenLabels = /* @__PURE__ */ new Set();
433
+ for (const entry of routed.values()) {
434
+ if (entry.label === void 0) continue;
435
+ entry.labelLines = wrapEdgeLabel(entry.label, columnGap - 8);
436
+ if (!entry.autoLabel || entry.emphasis || entry.kind !== "forward") continue;
437
+ const key = `${entry.source}|${entry.label}`;
438
+ if (seenLabels.has(key)) entry.autoLabel = false;
439
+ else seenLabels.add(key);
440
+ }
441
+ return {
442
+ rootId,
443
+ items: depths.flatMap((depth) => columnsByDepth.get(depth)),
444
+ edges: [...routed.values()],
445
+ clusters: [...clusterById.values()],
446
+ columns,
447
+ itemOf,
448
+ width: round1(width + LAYERED_PADDING),
449
+ height: round1(height + LAYERED_PADDING + 8),
450
+ nodeWidth,
451
+ nodeHeight,
452
+ headerHeight: LAYERED_HEADER
453
+ };
454
+ }
455
+ function layeredNeighbor(layout, currentId, key) {
456
+ const root = layout.rootId !== void 0 ? layout.itemOf[layout.rootId] : layout.items[0]?.id;
457
+ const current = layout.items.find((item) => item.id === currentId);
458
+ if (!current) return root ?? layout.items[0]?.id;
459
+ const column = layout.items.filter((item) => item.depth === current.depth);
460
+ switch (key) {
461
+ case "ArrowUp":
462
+ return column[current.index - 1]?.id;
463
+ case "ArrowDown":
464
+ return column[current.index + 1]?.id;
465
+ case "ArrowRight":
466
+ return current.children[0];
467
+ case "ArrowLeft":
468
+ return current.parents[0];
469
+ case "Home":
470
+ return root;
471
+ case "End":
472
+ return column[column.length - 1]?.id;
473
+ default:
474
+ return void 0;
475
+ }
476
+ }
63
477
  export {
64
478
  boundRelationshipGraph,
479
+ classifyLayeredEdges,
480
+ clusterIdFor,
481
+ layeredNeighbor,
482
+ layoutLayeredGraph,
65
483
  layoutRelationshipGraph,
66
484
  relationshipNeighbors,
67
- traverseRelationshipNode
485
+ traverseRelationshipNode,
486
+ wrapEdgeLabel
68
487
  };
@@ -11,6 +11,10 @@
11
11
  interface TreeLike<T> {
12
12
  id: string;
13
13
  children?: T[];
14
+ /** The display label — typeahead matches it only when it is a plain string (UIX-A11Y-2). */
15
+ label?: unknown;
16
+ /** Explicit typeahead text for nodes whose `label` is not a string (rich ReactNode labels). */
17
+ typeaheadLabel?: string;
14
18
  }
15
19
  interface FlatNode<T> {
16
20
  node: T;
@@ -40,6 +40,14 @@ function treeNav(flat, currentId, key) {
40
40
  case " ":
41
41
  return cur.hasChildren ? { toggleId: cur.node.id, selectId: cur.node.id } : { selectId: cur.node.id };
42
42
  default:
43
+ if (key.length === 1) {
44
+ const q = key.toLowerCase();
45
+ for (let step = 1; step <= flat.length; step++) {
46
+ const f = flat[(idx + step) % flat.length];
47
+ const label = f.node.typeaheadLabel ?? (typeof f.node.label === "string" ? f.node.label : void 0);
48
+ if (label != null && label.toLowerCase().startsWith(q)) return { focusId: f.node.id };
49
+ }
50
+ }
43
51
  return {};
44
52
  }
45
53
  }