@signalsafe/tree-spec-editor-react 0.1.1 → 0.1.3

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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -28
  3. package/README.standalone.md +14 -0
  4. package/dist/GraphEditorCanvasContext.d.ts +26 -0
  5. package/dist/GraphEditorCanvasContext.d.ts.map +1 -0
  6. package/dist/GraphEditorCanvasContext.js +20 -0
  7. package/dist/TreeSpecGraphEditor.d.ts +22 -1
  8. package/dist/TreeSpecGraphEditor.d.ts.map +1 -1
  9. package/dist/TreeSpecGraphEditor.js +133 -260
  10. package/dist/canvas/constants.d.ts +41 -0
  11. package/dist/canvas/constants.d.ts.map +1 -0
  12. package/dist/canvas/constants.js +40 -0
  13. package/dist/canvas/edgeBuilders.d.ts +6 -0
  14. package/dist/canvas/edgeBuilders.d.ts.map +1 -0
  15. package/dist/canvas/edgeBuilders.js +57 -0
  16. package/dist/canvas/edgeStyle.d.ts +16 -0
  17. package/dist/canvas/edgeStyle.d.ts.map +1 -0
  18. package/dist/canvas/edgeStyle.js +44 -0
  19. package/dist/canvas/focusChoice.d.ts +3 -0
  20. package/dist/canvas/focusChoice.d.ts.map +1 -0
  21. package/dist/canvas/focusChoice.js +12 -0
  22. package/dist/canvas/typeGuards.d.ts +3 -0
  23. package/dist/canvas/typeGuards.d.ts.map +1 -0
  24. package/dist/canvas/typeGuards.js +16 -0
  25. package/dist/contextMenu/GraphCanvasContextMenu.d.ts +10 -0
  26. package/dist/contextMenu/GraphCanvasContextMenu.d.ts.map +1 -0
  27. package/dist/contextMenu/GraphCanvasContextMenu.js +39 -0
  28. package/dist/contextMenu/types.d.ts +11 -0
  29. package/dist/contextMenu/types.d.ts.map +1 -0
  30. package/dist/contextMenu/types.js +1 -0
  31. package/dist/hooks/keyboardShortcutDispatch.d.ts +30 -0
  32. package/dist/hooks/keyboardShortcutDispatch.d.ts.map +1 -0
  33. package/dist/hooks/keyboardShortcutDispatch.js +88 -0
  34. package/dist/hooks/types.d.ts +32 -2
  35. package/dist/hooks/types.d.ts.map +1 -1
  36. package/dist/hooks/useCanvasContextMenu.d.ts +15 -0
  37. package/dist/hooks/useCanvasContextMenu.d.ts.map +1 -0
  38. package/dist/hooks/useCanvasContextMenu.js +50 -0
  39. package/dist/hooks/useCanvasGraphState.d.ts +29 -0
  40. package/dist/hooks/useCanvasGraphState.d.ts.map +1 -0
  41. package/dist/hooks/useCanvasGraphState.js +150 -0
  42. package/dist/hooks/useCanvasIssueIndex.d.ts +12 -0
  43. package/dist/hooks/useCanvasIssueIndex.d.ts.map +1 -0
  44. package/dist/hooks/useCanvasIssueIndex.js +32 -0
  45. package/dist/hooks/useCanvasNodeResize.d.ts +17 -0
  46. package/dist/hooks/useCanvasNodeResize.d.ts.map +1 -0
  47. package/dist/hooks/useCanvasNodeResize.js +53 -0
  48. package/dist/hooks/useCanvasViewport.d.ts +12 -0
  49. package/dist/hooks/useCanvasViewport.d.ts.map +1 -0
  50. package/dist/hooks/useCanvasViewport.js +84 -0
  51. package/dist/hooks/useChoiceDragDrop.d.ts +25 -0
  52. package/dist/hooks/useChoiceDragDrop.d.ts.map +1 -0
  53. package/dist/hooks/useChoiceDragDrop.js +40 -0
  54. package/dist/hooks/useEditorAdapter.d.ts +46 -0
  55. package/dist/hooks/useEditorAdapter.d.ts.map +1 -0
  56. package/dist/hooks/useEditorAdapter.js +281 -0
  57. package/dist/hooks/useEditorAutosave.d.ts +18 -0
  58. package/dist/hooks/useEditorAutosave.d.ts.map +1 -0
  59. package/dist/hooks/useEditorAutosave.js +37 -0
  60. package/dist/hooks/useEditorHistory.d.ts +16 -0
  61. package/dist/hooks/useEditorHistory.d.ts.map +1 -0
  62. package/dist/hooks/useEditorHistory.js +103 -0
  63. package/dist/hooks/useEditorSelection.d.ts +22 -0
  64. package/dist/hooks/useEditorSelection.d.ts.map +1 -0
  65. package/dist/hooks/useEditorSelection.js +75 -0
  66. package/dist/hooks/useGraphConnect.d.ts +22 -0
  67. package/dist/hooks/useGraphConnect.d.ts.map +1 -0
  68. package/dist/hooks/useGraphConnect.js +75 -0
  69. package/dist/hooks/useTreeSpecEditor.d.ts +1 -9
  70. package/dist/hooks/useTreeSpecEditor.d.ts.map +1 -1
  71. package/dist/hooks/useTreeSpecEditor.js +231 -462
  72. package/dist/index.d.ts +4 -4
  73. package/dist/index.js +2 -2
  74. package/dist/nodes/ChoiceCanvasRow.d.ts +10 -0
  75. package/dist/nodes/ChoiceCanvasRow.d.ts.map +1 -0
  76. package/dist/nodes/ChoiceCanvasRow.js +55 -0
  77. package/dist/nodes/EndNode.d.ts +3 -0
  78. package/dist/nodes/EndNode.d.ts.map +1 -0
  79. package/dist/nodes/EndNode.js +7 -0
  80. package/dist/nodes/PromptNode.d.ts +6 -0
  81. package/dist/nodes/PromptNode.d.ts.map +1 -0
  82. package/dist/nodes/PromptNode.js +51 -0
  83. package/dist/nodes/PromptNodeChoicesList.d.ts +14 -0
  84. package/dist/nodes/PromptNodeChoicesList.d.ts.map +1 -0
  85. package/dist/nodes/PromptNodeChoicesList.js +24 -0
  86. package/dist/nodes/PromptNodeHeader.d.ts +10 -0
  87. package/dist/nodes/PromptNodeHeader.d.ts.map +1 -0
  88. package/dist/nodes/PromptNodeHeader.js +6 -0
  89. package/dist/nodes/PromptNodeIssueBadges.d.ts +7 -0
  90. package/dist/nodes/PromptNodeIssueBadges.d.ts.map +1 -0
  91. package/dist/nodes/PromptNodeIssueBadges.js +6 -0
  92. package/dist/nodes/PromptNodeToolbar.d.ts +6 -0
  93. package/dist/nodes/PromptNodeToolbar.d.ts.map +1 -0
  94. package/dist/nodes/PromptNodeToolbar.js +5 -0
  95. package/dist/nodes/types.d.ts +13 -0
  96. package/dist/nodes/types.d.ts.map +1 -0
  97. package/dist/nodes/types.js +1 -0
  98. package/dist/utils/joinClasses.d.ts +2 -0
  99. package/dist/utils/joinClasses.d.ts.map +1 -0
  100. package/dist/utils/joinClasses.js +3 -0
  101. package/package.json +36 -13
@@ -1,276 +1,149 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useRef } from 'react';
3
- import ReactFlow, { Background, Controls, Handle, MiniMap, Position, ReactFlowProvider, addEdge, useEdgesState, useNodesState, useReactFlow, } from 'reactflow';
2
+ import { useCallback, useMemo, useRef, useState } from 'react';
3
+ import ReactFlow, { Background, ConnectionMode, Controls, MiniMap, ReactFlowProvider, useReactFlow, } from 'reactflow';
4
4
  import 'reactflow/dist/style.css';
5
- import { TERMINAL_OUTCOME, TREE_SPEC_ISSUE_SEVERITY } from '@signalsafe/tree-spec';
6
- import { END_NODE_ID, GRAPH_SELECTION_KIND, safeUUID, } from '@signalsafe/tree-spec-editor-core';
7
- /** Background highlight when the node matches editor selection (sidebar / issues / canvas). */
8
- const CANVAS_NODE_SELECTED_CLASS = 'bg-primary-subtle';
9
- function joinClasses(...parts) {
10
- return parts.filter(Boolean).join(' ');
11
- }
12
- function getPromptNodeBorderClass(hasErrors, warningCount) {
13
- if (hasErrors) {
14
- return 'border-danger';
15
- }
16
- if (warningCount > 0) {
17
- return 'border-warning';
18
- }
19
- return '';
20
- }
21
- function getIssueEdgeStyle(style, hasIssue) {
22
- if (!hasIssue) {
23
- return style;
24
- }
25
- return style ? { ...style, strokeWidth: 2, strokeDasharray: '6 4' } : { strokeWidth: 2, strokeDasharray: '6 4' };
26
- }
27
- function PromptNode({ data, selected }) {
28
- const n = data.node;
29
- const choices = n.choices ?? [];
30
- const hasErrors = data.issuesErrors > 0;
31
- const borderClass = getPromptNodeBorderClass(hasErrors, data.issuesWarnings);
32
- return (_jsxs("div", { className: joinClasses('card', 'card-min-width-280', borderClass, selected && CANVAS_NODE_SELECTED_CLASS), children: [_jsx(Handle, { type: "target", position: Position.Left, id: "in", className: "handle-bg-default" }), _jsxs("div", { className: "card-body p-2", children: [_jsxs("div", { className: "d-flex justify-content-between align-items-center", children: [_jsxs("div", { children: [_jsxs("div", { className: "fw-bold font-size-13", children: [data.isStart ? '▶ ' : '', n.type, data.issuesTotal > 0 ? (_jsxs("span", { className: "ms-2", title: `${data.issuesErrors} errors, ${data.issuesWarnings} warnings, ${data.issuesInfo} info`, children: [data.issuesErrors > 0 ? _jsx("span", { className: "badge bg-danger me-1", children: data.issuesErrors }) : null, data.issuesWarnings > 0 ? (_jsx("span", { className: "badge bg-warning text-dark me-1", children: data.issuesWarnings })) : null, data.issuesInfo > 0 ? _jsx("span", { className: "badge bg-info text-dark", children: data.issuesInfo }) : null] })) : null] }), _jsx("div", { className: "font-size-12 opacity-90 text-truncate", children: n.prompt || _jsx("em", { className: "text-muted", children: "(empty prompt)" }) })] }), _jsx("div", { className: "text-muted font-size-11", children: n.id.slice(0, 8) })] }), _jsx("hr", { className: "my-2" }), _jsx("div", { className: "font-size-12", children: choices.length === 0 ? (_jsx("div", { className: "text-muted", children: _jsx("em", { children: "No choices" }) })) : (_jsx("ul", { className: "list-unstyled mb-0", children: choices.map((c) => (_jsxs("li", { className: "d-flex justify-content-between align-items-center position-relative pr-18", children: [_jsx("span", { className: "text-truncate max-w-210", children: c.label }), _jsx("span", { className: "badge bg-light text-dark", children: c.id }), _jsx(Handle, { type: "source", position: Position.Right, id: `choice:${c.id}`, className: "graph-editor-handle", style: {
33
- position: 'absolute',
34
- right: -6,
35
- top: '50%',
36
- transform: 'translateY(-50%)',
37
- } })] }, c.id))) })) })] })] }));
38
- }
39
- function EndNode({ selected }) {
40
- return (_jsxs("div", { className: joinClasses('card', 'border-danger', 'w-180', selected && CANVAS_NODE_SELECTED_CLASS), children: [_jsx(Handle, { type: "target", position: Position.Left, id: "in", className: "handle-bg-danger" }), _jsxs("div", { className: "card-body p-2 text-center", children: [_jsx("div", { className: "fw-bold text-danger", children: "END" }), _jsx("div", { className: "text-muted font-size-12", children: "Outcome required" })] })] }));
41
- }
42
- function choiceIdFromHandle(h) {
43
- if (!h)
44
- return '';
45
- if (h.startsWith('choice:'))
46
- return h.slice('choice:'.length);
47
- return h;
48
- }
49
- function edgeLabelForTransition(tree, t) {
50
- const node = tree.nodes[t.fromNodeId];
51
- const choice = node?.choices?.find((c) => c.id === t.fromChoiceId);
52
- const base = choice?.label || t.fromChoiceId;
53
- if (t.toNodeId === END_NODE_ID) {
54
- const oc = t.outcome ?? TERMINAL_OUTCOME.AT_RISK;
55
- return `${base} → END (${oc})`;
56
- }
57
- return base;
58
- }
59
- function buildEdgesFromTransitions(tree) {
60
- return tree.transitions.map((t) => ({
61
- id: t.id,
62
- source: t.fromNodeId,
63
- target: t.toNodeId,
64
- sourceHandle: `choice:${t.fromChoiceId}`,
65
- label: edgeLabelForTransition(tree, t),
66
- }));
67
- }
68
- function buildTransitionsFromEdges(edges, existing) {
69
- const existingById = new Map(existing.map((t) => [t.id, t]));
70
- return edges
71
- .filter((e) => e.source && e.target && e.source !== END_NODE_ID)
72
- .map((e) => {
73
- const fromChoiceId = choiceIdFromHandle(e.sourceHandle);
74
- const prior = existingById.get(String(e.id));
75
- return {
76
- id: String(e.id),
77
- fromNodeId: String(e.source),
78
- fromChoiceId: fromChoiceId,
79
- toNodeId: String(e.target),
80
- outcome: String(e.target) === END_NODE_ID ? (prior?.outcome ?? TERMINAL_OUTCOME.AT_RISK) : undefined,
81
- };
82
- })
83
- .filter((t) => t.fromChoiceId.length > 0);
84
- }
85
- function TreeSpecGraphInner({ tree, onChange, issues = [], selected, onSelect, focusNodeId, showMiniMap = true, fitViewNonce, className = 'h-70vh border rounded', }) {
5
+ import { GRAPH_SELECTION_KIND, choiceIdFromHandle, resolveGraphViewport, LAYOUT_SNAP_GRID, } from '@signalsafe/tree-spec-editor-core';
6
+ import { GraphEditorCanvasContext, } from './GraphEditorCanvasContext.js';
7
+ import { buildEdgeMarker, getIssueEdgeStyle, resolveSelectedEdgeStroke } from './canvas/edgeStyle.js';
8
+ import { CANVAS_CLASS } from './canvas/constants.js';
9
+ import { resolveCanvasFocusChoiceId } from './canvas/focusChoice.js';
10
+ import { isChoiceRowClickTarget } from './canvas/typeGuards.js';
11
+ import { GraphCanvasContextMenu } from './contextMenu/GraphCanvasContextMenu.js';
12
+ import { EndNode } from './nodes/EndNode.js';
13
+ import { PromptNode } from './nodes/PromptNode.js';
14
+ import { joinClasses } from './utils/joinClasses.js';
15
+ import { useCanvasContextMenu } from './hooks/useCanvasContextMenu.js';
16
+ import { useCanvasGraphState } from './hooks/useCanvasGraphState.js';
17
+ import { useCanvasIssueIndex } from './hooks/useCanvasIssueIndex.js';
18
+ import { useCanvasNodeResize } from './hooks/useCanvasNodeResize.js';
19
+ import { useCanvasViewport } from './hooks/useCanvasViewport.js';
20
+ import { useChoiceDragDrop } from './hooks/useChoiceDragDrop.js';
21
+ import { useGraphConnect } from './hooks/useGraphConnect.js';
22
+ function TreeSpecGraphInner({ tree, onChange, issues = [], selected, onSelect, onChoiceSelect, onRepositionChoice, focusNodeId, focusChoiceId = null, showMiniMap = true, fitViewNonce, className = 'h-70vh border rounded', readOnly = false, onDuplicateNode, onDeleteNode, onAutoLayout, contextualZoom = true, colorMode = 'light', }) {
86
23
  const rf = useReactFlow();
87
- const isDraggingRef = useRef(false);
88
- const nodesRef = useRef([]);
89
- const edgesRef = useRef([]);
90
- const issuesByNode = useMemo(() => {
91
- const m = new Map();
92
- for (const i of issues) {
93
- if (!i.node_id)
94
- continue;
95
- const cur = m.get(i.node_id) ?? { total: 0, errors: 0, warnings: 0, info: 0 };
96
- cur.total += 1;
97
- const sev = String(i.severity ?? '').toLowerCase();
98
- if (sev === TREE_SPEC_ISSUE_SEVERITY.WARNING)
99
- cur.warnings += 1;
100
- else if (sev === TREE_SPEC_ISSUE_SEVERITY.INFO)
101
- cur.info += 1;
102
- else
103
- cur.errors += 1;
104
- m.set(i.node_id, cur);
105
- }
106
- return m;
107
- }, [issues]);
108
- const issueKeySet = useMemo(() => {
109
- const s = new Set();
110
- for (const i of issues) {
111
- if (!i.node_id || !i.choice_id)
112
- continue;
113
- s.add(`${i.node_id}::${i.choice_id}`);
114
- }
115
- return s;
116
- }, [issues]);
117
- const initialNodes = useMemo(() => {
118
- const arr = [];
119
- for (const n of Object.values(tree.nodes)) {
120
- arr.push({
121
- id: n.id,
122
- type: 'promptNode',
123
- position: n.position ?? { x: 0, y: 0 },
124
- data: {
125
- node: n,
126
- isStart: tree.start_node === n.id,
127
- issuesTotal: issuesByNode.get(n.id)?.total ?? 0,
128
- issuesErrors: issuesByNode.get(n.id)?.errors ?? 0,
129
- issuesWarnings: issuesByNode.get(n.id)?.warnings ?? 0,
130
- issuesInfo: issuesByNode.get(n.id)?.info ?? 0,
131
- },
132
- });
133
- }
134
- arr.push({
135
- id: END_NODE_ID,
136
- type: 'endNode',
137
- position: { x: 700, y: 0 },
138
- data: {},
139
- selectable: true,
140
- draggable: true,
141
- });
142
- return arr;
143
- }, [tree.nodes, tree.start_node, issuesByNode]);
144
- const initialEdges = useMemo(() => buildEdgesFromTransitions(tree), [tree]);
145
- const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
146
- const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
147
- useEffect(() => {
148
- nodesRef.current = nodes;
149
- }, [nodes]);
150
- useEffect(() => {
151
- edgesRef.current = edges;
152
- }, [edges]);
153
- const lastTreeRef = useRef('');
154
- useEffect(() => {
155
- const nextKey = JSON.stringify({ tree, issues: issues.length });
156
- if (nextKey === lastTreeRef.current)
157
- return;
158
- lastTreeRef.current = nextKey;
159
- setNodes(initialNodes);
160
- setEdges(initialEdges);
161
- }, [tree, issues.length, initialNodes, initialEdges, setNodes, setEdges]);
162
- useEffect(() => {
163
- if (!focusNodeId)
164
- return;
165
- try {
166
- const n = rf.getNode(focusNodeId);
167
- if (!n)
168
- return;
169
- rf.setCenter(n.position.x + 150, n.position.y + 60, { zoom: 1, duration: 300 });
170
- }
171
- catch {
172
- // ignore
173
- }
174
- }, [focusNodeId, rf]);
175
- useEffect(() => {
176
- if (!fitViewNonce)
177
- return;
178
- const id = setTimeout(() => {
179
- try {
180
- rf.fitView({ padding: 0.2, duration: 300 });
181
- }
182
- catch {
183
- // ignore
184
- }
185
- }, 100);
186
- return () => clearTimeout(id);
187
- }, [fitViewNonce, rf]);
24
+ const treeRef = useRef(tree);
25
+ treeRef.current = tree;
26
+ const suppressViewportSaveRef = useRef(true);
27
+ const isResizingRef = useRef(false);
28
+ const { issuesByNode, issueKeySet } = useCanvasIssueIndex(issues);
29
+ const { choiceDrag, choiceDropTarget, handleChoiceDragStart, handleChoiceDragEnd, handleChoiceDragOver, handleChoiceDrop, } = useChoiceDragDrop({
30
+ readOnly,
31
+ onChoiceSelect,
32
+ onSelect,
33
+ onRepositionChoice,
34
+ });
35
+ const { contextMenu, closeContextMenu, onNodeContextMenu, onPaneContextMenu } = useCanvasContextMenu({
36
+ readOnly,
37
+ onAutoLayout,
38
+ });
39
+ const [resizeHeightByNodeId, setResizeHeightByNodeId] = useState({});
40
+ const graphState = useCanvasGraphState({
41
+ tree,
42
+ onChange,
43
+ issues,
44
+ issuesByNode,
45
+ readOnly,
46
+ resizeHeightByNodeId,
47
+ isResizingRef,
48
+ suppressViewportSaveRef,
49
+ });
50
+ const { nodes, edges, onNodesChangeWrapped, onEdgesChangeWrapped, onNodeDragStart, onNodeDragStop, onMoveEnd, setNodes, } = graphState;
51
+ const resizeHandlers = useCanvasNodeResize({
52
+ readOnly,
53
+ treeRef,
54
+ onChange,
55
+ setNodes,
56
+ isResizingRef,
57
+ resizeHeightByNodeId,
58
+ setResizeHeightByNodeId,
59
+ });
60
+ useCanvasViewport({
61
+ rf,
62
+ focusNodeId,
63
+ fitViewNonce,
64
+ contextualZoom,
65
+ selected,
66
+ suppressViewportSaveRef,
67
+ });
68
+ const { onConnect, onConnectStart, onConnectEnd, onReconnect, onEdgesDelete, isValidConnection, } = useGraphConnect({
69
+ treeRef,
70
+ onChange,
71
+ onSelect,
72
+ readOnly,
73
+ });
74
+ const savedViewport = useMemo(() => resolveGraphViewport(tree), [tree._meta]);
188
75
  const nodeTypes = useMemo(() => ({
189
76
  promptNode: PromptNode,
190
77
  endNode: EndNode,
191
78
  }), []);
192
- const commit = useCallback((nextNodes, nextEdges) => {
193
- if (isDraggingRef.current)
79
+ const onNodeClick = useCallback((evt, node) => {
80
+ if (isChoiceRowClickTarget(evt.target))
194
81
  return;
195
- const updatedNodes = { ...tree.nodes };
196
- for (const n of nextNodes) {
197
- if (n.id === END_NODE_ID)
198
- continue;
199
- const existing = updatedNodes[n.id];
200
- if (!existing)
201
- continue;
202
- updatedNodes[n.id] = {
203
- ...existing,
204
- position: { x: n.position.x, y: n.position.y },
205
- };
206
- }
207
- const transitions = buildTransitionsFromEdges(nextEdges, tree.transitions);
208
- onChange({
209
- ...tree,
210
- nodes: updatedNodes,
211
- transitions,
212
- });
213
- }, [tree, onChange]);
214
- const onConnect = useCallback((conn) => {
215
- const choiceId = choiceIdFromHandle(conn.sourceHandle);
216
- if (!conn.source || !conn.target || !choiceId)
217
- return;
218
- const newEdge = {
219
- id: safeUUID(),
220
- source: conn.source,
221
- target: conn.target,
222
- sourceHandle: conn.sourceHandle,
223
- label: choiceId,
224
- };
225
- setEdges((eds) => {
226
- const next = addEdge(newEdge, eds);
227
- commit(nodesRef.current, next);
228
- return next;
229
- });
230
- }, [setEdges, commit]);
231
- const onNodesChangeWrapped = useCallback((changes) => {
232
- onNodesChange(changes);
233
- const shouldCommit = changes.some((c) => {
234
- if (c?.type === 'select' || c?.type === 'dimensions')
235
- return false;
236
- if (c?.type === 'position' && c?.dragging)
237
- return false;
238
- return true;
239
- });
240
- if (shouldCommit)
241
- queueMicrotask(() => commit(nodesRef.current, edgesRef.current));
242
- }, [onNodesChange, commit]);
243
- const onEdgesChangeWrapped = useCallback((changes) => {
244
- onEdgesChange(changes);
245
- const shouldCommit = changes.some((c) => c?.type !== 'select');
246
- if (shouldCommit)
247
- queueMicrotask(() => commit(nodesRef.current, edgesRef.current));
248
- }, [onEdgesChange, commit]);
249
- const onNodeDragStart = useCallback(() => {
250
- isDraggingRef.current = true;
251
- }, []);
252
- const onNodeDragStop = useCallback(() => {
253
- isDraggingRef.current = false;
254
- commit(nodesRef.current, edgesRef.current);
255
- }, [commit]);
256
- const onNodeClick = useCallback((_evt, node) => {
257
82
  onSelect?.({ kind: GRAPH_SELECTION_KIND.NODE, id: node.id });
258
83
  }, [onSelect]);
259
84
  const onEdgeClick = useCallback((_evt, edge) => {
260
85
  onSelect?.({ kind: GRAPH_SELECTION_KIND.EDGE, id: edge.id });
261
86
  }, [onSelect]);
262
- return (_jsx("div", { className: className, children: _jsxs(ReactFlow, { nodes: nodes.map((n) => ({
263
- ...n,
264
- selected: selected?.kind === GRAPH_SELECTION_KIND.NODE && selected.id === n.id,
265
- })), edges: edges.map((e) => {
266
- const fromChoiceId = choiceIdFromHandle(e.sourceHandle);
267
- const hasIssue = fromChoiceId ? issueKeySet.has(`${e.source}::${fromChoiceId}`) : false;
268
- return {
269
- ...e,
270
- selected: selected?.kind === GRAPH_SELECTION_KIND.EDGE && selected.id === e.id,
271
- style: getIssueEdgeStyle(e.style, hasIssue),
272
- };
273
- }), onNodesChange: onNodesChangeWrapped, onEdgesChange: onEdgesChangeWrapped, onConnect: onConnect, onNodeDragStart: onNodeDragStart, onNodeDragStop: onNodeDragStop, onNodeClick: onNodeClick, onEdgeClick: onEdgeClick, nodeTypes: nodeTypes, children: [showMiniMap ? _jsx(MiniMap, {}) : null, _jsx(Controls, {}), _jsx(Background, {})] }) }));
87
+ const onPaneClick = useCallback(() => {
88
+ onSelect?.({ kind: null, id: null });
89
+ }, [onSelect]);
90
+ const canvasContextValue = useMemo(() => ({
91
+ readOnly,
92
+ onDuplicateNode: (nodeId) => onDuplicateNode?.(nodeId),
93
+ onDeleteNode: (nodeId) => onDeleteNode?.(nodeId),
94
+ onResizeNode: resizeHandlers.handleResizeNode,
95
+ onResizeNodeStart: resizeHandlers.handleResizeNodeStart,
96
+ onSelectChoice: (nodeId, choiceId) => {
97
+ if (onChoiceSelect) {
98
+ onChoiceSelect(nodeId, choiceId);
99
+ return;
100
+ }
101
+ onSelect?.({ kind: GRAPH_SELECTION_KIND.NODE, id: nodeId });
102
+ },
103
+ choiceDrag,
104
+ choiceDropTarget,
105
+ onChoiceDragStart: handleChoiceDragStart,
106
+ onChoiceDragEnd: handleChoiceDragEnd,
107
+ onChoiceDragOver: handleChoiceDragOver,
108
+ onChoiceDrop: handleChoiceDrop,
109
+ }), [
110
+ readOnly,
111
+ onDuplicateNode,
112
+ onDeleteNode,
113
+ resizeHandlers.handleResizeNode,
114
+ resizeHandlers.handleResizeNodeStart,
115
+ onSelect,
116
+ onChoiceSelect,
117
+ choiceDrag,
118
+ choiceDropTarget,
119
+ handleChoiceDragStart,
120
+ handleChoiceDragEnd,
121
+ handleChoiceDragOver,
122
+ handleChoiceDrop,
123
+ ]);
124
+ return (_jsx(GraphEditorCanvasContext.Provider, { value: canvasContextValue, children: _jsxs("div", { className: joinClasses(className, CANVAS_CLASS), "data-color-mode": colorMode, children: [_jsx(GraphCanvasContextMenu, { menu: contextMenu, readOnly: readOnly, onClose: closeContextMenu, onDuplicateNode: onDuplicateNode, onDeleteNode: onDeleteNode, onAutoLayout: onAutoLayout }), _jsxs(ReactFlow, { defaultViewport: savedViewport, elementsSelectable: false, selectNodesOnDrag: false, nodes: nodes.map((n) => ({
125
+ ...n,
126
+ selected: selected?.kind === GRAPH_SELECTION_KIND.NODE && selected.id === n.id,
127
+ data: n.type === 'promptNode'
128
+ ? {
129
+ ...n.data,
130
+ focusChoiceId: resolveCanvasFocusChoiceId(n.id, selected, focusNodeId, focusChoiceId),
131
+ }
132
+ : n.data,
133
+ })), edges: edges.map((e) => {
134
+ const fromChoiceId = choiceIdFromHandle(e.sourceHandle);
135
+ const hasIssue = fromChoiceId ? issueKeySet.has(`${e.source}::${fromChoiceId}`) : false;
136
+ const isEdgeSelected = selected?.kind === GRAPH_SELECTION_KIND.EDGE && selected.id === e.id;
137
+ let style = getIssueEdgeStyle(e.style, hasIssue);
138
+ const { style: resolvedStyle, stroke } = resolveSelectedEdgeStroke(style, isEdgeSelected);
139
+ style = resolvedStyle;
140
+ return {
141
+ ...e,
142
+ selected: isEdgeSelected,
143
+ style,
144
+ markerEnd: buildEdgeMarker(stroke),
145
+ };
146
+ }), onNodesChange: onNodesChangeWrapped, onEdgesChange: onEdgesChangeWrapped, onConnect: onConnect, onConnectStart: onConnectStart, onConnectEnd: onConnectEnd, onReconnect: onReconnect, onEdgesDelete: onEdgesDelete, isValidConnection: isValidConnection, connectionMode: ConnectionMode.Strict, snapToGrid: true, snapGrid: [LAYOUT_SNAP_GRID, LAYOUT_SNAP_GRID], onNodeDragStart: onNodeDragStart, onNodeDragStop: onNodeDragStop, onNodeClick: onNodeClick, onEdgeClick: onEdgeClick, onPaneClick: onPaneClick, onNodeContextMenu: onNodeContextMenu, onPaneContextMenu: onPaneContextMenu, onMoveEnd: onMoveEnd, nodeTypes: nodeTypes, children: [showMiniMap ? _jsx(MiniMap, { pannable: true, ariaLabel: "Canvas overview \u2014 drag to pan" }) : null, _jsx(Controls, {}), _jsx(Background, {})] })] }) }));
274
147
  }
275
148
  export default function TreeSpecGraphEditor(props) {
276
149
  return (_jsx(ReactFlowProvider, { children: _jsx(TreeSpecGraphInner, { ...props }) }));
@@ -0,0 +1,41 @@
1
+ import { MarkerType } from 'reactflow';
2
+ /** Background highlight when the node matches editor selection (sidebar / issues / canvas). */
3
+ export declare const CANVAS_NODE_SELECTED_CLASS = "bg-primary-subtle";
4
+ /** Dark readable text on selected canvas node cards. */
5
+ export declare const CANVAS_NODE_SELECTED_TEXT_CLASS = "graph-editor-canvas-selected";
6
+ export declare const CANVAS_CLASS = "graph-editor-canvas";
7
+ export declare const CANVAS_NODE_CLASS = "graph-editor-canvas-node";
8
+ export declare const CANVAS_NODE_BODY_CLASS = "graph-editor-canvas-node-body";
9
+ export declare const CANVAS_CHOICE_SELECTED_CLASS = "graph-editor-canvas-choice-selected";
10
+ export declare const CHOICE_ROW_CLASS = "graph-editor-choice-row";
11
+ export declare const CHOICE_ROW_SELECTOR = ".graph-editor-choice-row";
12
+ export declare const CHOICE_DRAG_HANDLE_CLASS = "graph-editor-choice-drag-handle";
13
+ export declare const CHOICE_DRAG_HANDLE_SELECTOR = ".graph-editor-choice-drag-handle";
14
+ export declare const CHOICE_DROP_TARGET_CLASS = "graph-editor-choice-drop-target";
15
+ export declare const CHOICE_DROP_APPEND_CLASS = "graph-editor-choice-drop-append";
16
+ export declare const CHOICE_ROW_SELECT_CLASS = "graph-editor-choice-row-select";
17
+ export declare const CHOICE_ROW_SELECTABLE_CLASS = "graph-editor-choice-row-selectable";
18
+ export declare const CHOICE_HANDLE_CLASS = "graph-editor-handle graph-editor-choice-handle";
19
+ export declare const NODE_DRAG_HANDLE_CLASS = "graph-editor-drag-handle";
20
+ export declare const NODE_DRAG_HANDLE_SELECTOR = ".graph-editor-drag-handle";
21
+ export declare const TARGET_HANDLE_CLASS_DEFAULT = "handle-bg-default graph-editor-target-handle";
22
+ export declare const TARGET_HANDLE_CLASS_DANGER = "handle-bg-danger graph-editor-target-handle";
23
+ export declare const CONTEXT_MENU_CLASS = "graph-editor-context-menu dropdown-menu show position-fixed shadow-sm";
24
+ export declare const REACT_FLOW_PANE_CLASS = "react-flow__pane";
25
+ export declare const BORDER_DANGER_CLASS = "border-danger";
26
+ export declare const BORDER_WARNING_CLASS = "border-warning";
27
+ export declare const END_NODE_WIDTH_CLASS = "w-180";
28
+ export declare const MIN_NODE_WIDTH = 180;
29
+ export declare const MAX_NODE_WIDTH = 560;
30
+ export declare const MIN_NODE_HEIGHT = 80;
31
+ export declare const MAX_NODE_HEIGHT = 480;
32
+ /** Matches React Flow selected edge stroke (`.react-flow__edge.selected .react-flow__edge-path`). */
33
+ export declare const SELECTED_EDGE_STROKE = "#555";
34
+ export declare const EDGE_ARROW_MARKER: {
35
+ type: MarkerType;
36
+ width: number;
37
+ height: number;
38
+ };
39
+ export declare const TARGET_HANDLE_ID = "in";
40
+ export declare const CHOICE_HANDLE_PREFIX = "choice:";
41
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/canvas/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,+FAA+F;AAC/F,eAAO,MAAM,0BAA0B,sBAAsB,CAAC;AAE9D,wDAAwD;AACxD,eAAO,MAAM,+BAA+B,iCAAiC,CAAC;AAE9E,eAAO,MAAM,YAAY,wBAAwB,CAAC;AAClD,eAAO,MAAM,iBAAiB,6BAA6B,CAAC;AAC5D,eAAO,MAAM,sBAAsB,kCAAkC,CAAC;AACtE,eAAO,MAAM,4BAA4B,wCAAwC,CAAC;AAElF,eAAO,MAAM,gBAAgB,4BAA4B,CAAC;AAC1D,eAAO,MAAM,mBAAmB,6BAAyB,CAAC;AAC1D,eAAO,MAAM,wBAAwB,oCAAoC,CAAC;AAC1E,eAAO,MAAM,2BAA2B,qCAAiC,CAAC;AAC1E,eAAO,MAAM,wBAAwB,oCAAoC,CAAC;AAC1E,eAAO,MAAM,wBAAwB,oCAAoC,CAAC;AAC1E,eAAO,MAAM,uBAAuB,mCAAmC,CAAC;AACxE,eAAO,MAAM,2BAA2B,uCAAuC,CAAC;AAChF,eAAO,MAAM,mBAAmB,mDAAmD,CAAC;AAEpF,eAAO,MAAM,sBAAsB,6BAA6B,CAAC;AACjE,eAAO,MAAM,yBAAyB,8BAA+B,CAAC;AAEtE,eAAO,MAAM,2BAA2B,iDAAiD,CAAC;AAC1F,eAAO,MAAM,0BAA0B,gDAAgD,CAAC;AAExF,eAAO,MAAM,kBAAkB,0EAA0E,CAAC;AAE1G,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AAExD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,oBAAoB,mBAAmB,CAAC;AAErD,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAE5C,eAAO,MAAM,cAAc,MAAM,CAAC;AAClC,eAAO,MAAM,cAAc,MAAM,CAAC;AAClC,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,eAAe,MAAM,CAAC;AAEnC,qGAAqG;AACrG,eAAO,MAAM,oBAAoB,SAAS,CAAC;AAE3C,eAAO,MAAM,iBAAiB;;;;CAI7B,CAAC;AAEF,eAAO,MAAM,gBAAgB,OAAO,CAAC;AACrC,eAAO,MAAM,oBAAoB,YAAY,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { MarkerType } from 'reactflow';
2
+ /** Background highlight when the node matches editor selection (sidebar / issues / canvas). */
3
+ export const CANVAS_NODE_SELECTED_CLASS = 'bg-primary-subtle';
4
+ /** Dark readable text on selected canvas node cards. */
5
+ export const CANVAS_NODE_SELECTED_TEXT_CLASS = 'graph-editor-canvas-selected';
6
+ export const CANVAS_CLASS = 'graph-editor-canvas';
7
+ export const CANVAS_NODE_CLASS = 'graph-editor-canvas-node';
8
+ export const CANVAS_NODE_BODY_CLASS = 'graph-editor-canvas-node-body';
9
+ export const CANVAS_CHOICE_SELECTED_CLASS = 'graph-editor-canvas-choice-selected';
10
+ export const CHOICE_ROW_CLASS = 'graph-editor-choice-row';
11
+ export const CHOICE_ROW_SELECTOR = `.${CHOICE_ROW_CLASS}`;
12
+ export const CHOICE_DRAG_HANDLE_CLASS = 'graph-editor-choice-drag-handle';
13
+ export const CHOICE_DRAG_HANDLE_SELECTOR = `.${CHOICE_DRAG_HANDLE_CLASS}`;
14
+ export const CHOICE_DROP_TARGET_CLASS = 'graph-editor-choice-drop-target';
15
+ export const CHOICE_DROP_APPEND_CLASS = 'graph-editor-choice-drop-append';
16
+ export const CHOICE_ROW_SELECT_CLASS = 'graph-editor-choice-row-select';
17
+ export const CHOICE_ROW_SELECTABLE_CLASS = 'graph-editor-choice-row-selectable';
18
+ export const CHOICE_HANDLE_CLASS = 'graph-editor-handle graph-editor-choice-handle';
19
+ export const NODE_DRAG_HANDLE_CLASS = 'graph-editor-drag-handle';
20
+ export const NODE_DRAG_HANDLE_SELECTOR = `.${NODE_DRAG_HANDLE_CLASS}`;
21
+ export const TARGET_HANDLE_CLASS_DEFAULT = 'handle-bg-default graph-editor-target-handle';
22
+ export const TARGET_HANDLE_CLASS_DANGER = 'handle-bg-danger graph-editor-target-handle';
23
+ export const CONTEXT_MENU_CLASS = 'graph-editor-context-menu dropdown-menu show position-fixed shadow-sm';
24
+ export const REACT_FLOW_PANE_CLASS = 'react-flow__pane';
25
+ export const BORDER_DANGER_CLASS = 'border-danger';
26
+ export const BORDER_WARNING_CLASS = 'border-warning';
27
+ export const END_NODE_WIDTH_CLASS = 'w-180';
28
+ export const MIN_NODE_WIDTH = 180;
29
+ export const MAX_NODE_WIDTH = 560;
30
+ export const MIN_NODE_HEIGHT = 80;
31
+ export const MAX_NODE_HEIGHT = 480;
32
+ /** Matches React Flow selected edge stroke (`.react-flow__edge.selected .react-flow__edge-path`). */
33
+ export const SELECTED_EDGE_STROKE = '#555';
34
+ export const EDGE_ARROW_MARKER = {
35
+ type: MarkerType.ArrowClosed,
36
+ width: 16,
37
+ height: 16,
38
+ };
39
+ export const TARGET_HANDLE_ID = 'in';
40
+ export const CHOICE_HANDLE_PREFIX = 'choice:';
@@ -0,0 +1,6 @@
1
+ import { type EditorTree, type EditorTransition } from '@signalsafe/tree-spec-editor-core';
2
+ import type { Edge } from 'reactflow';
3
+ export declare function edgeLabelForTransition(tree: EditorTree, t: EditorTransition): string;
4
+ export declare function buildEdgesFromTransitions(tree: EditorTree): Edge[];
5
+ export declare function buildTransitionsFromEdges(edges: Edge[], existing: EditorTransition[]): EditorTransition[];
6
+ //# sourceMappingURL=edgeBuilders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edgeBuilders.d.ts","sourceRoot":"","sources":["../../src/canvas/edgeBuilders.ts"],"names":[],"mappings":"AACA,OAAO,EAOH,KAAK,UAAU,EACf,KAAK,gBAAgB,EACxB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAQtC,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,gBAAgB,GAAG,MAAM,CASpF;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,EAAE,CA0BlE;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,EAAE,CAgBzG"}
@@ -0,0 +1,57 @@
1
+ import { TERMINAL_OUTCOME } from '@signalsafe/tree-spec';
2
+ import { END_NODE_ID, choiceIdFromHandle, resolveDefaultEdgeType, resolveEffectiveEdgeType, resolveEdgeStrokeColor, shouldShowEdgeLabel, } from '@signalsafe/tree-spec-editor-core';
3
+ import { CHOICE_HANDLE_PREFIX, TARGET_HANDLE_ID, } from './constants.js';
4
+ import { buildEdgeMarker, buildEdgeStyle, resolveEdgePathStroke } from './edgeStyle.js';
5
+ export function edgeLabelForTransition(tree, t) {
6
+ const node = tree.nodes[t.fromNodeId];
7
+ const choice = node?.choices?.find((c) => c.id === t.fromChoiceId);
8
+ const base = choice?.label || t.fromChoiceId;
9
+ if (t.toNodeId === END_NODE_ID) {
10
+ const oc = t.outcome ?? TERMINAL_OUTCOME.AT_RISK;
11
+ return `${base} → END (${oc})`;
12
+ }
13
+ return base;
14
+ }
15
+ export function buildEdgesFromTransitions(tree) {
16
+ return tree.transitions.map((t) => {
17
+ const node = tree.nodes[t.fromNodeId];
18
+ const choice = node?.choices?.find((c) => c.id === t.fromChoiceId);
19
+ const showLabel = choice ? shouldShowEdgeLabel(choice) : true;
20
+ const strokeColor = choice ? resolveEdgeStrokeColor(choice) : undefined;
21
+ const edgeType = choice
22
+ ? resolveEffectiveEdgeType(choice, tree._meta)
23
+ : resolveDefaultEdgeType(tree._meta);
24
+ const style = buildEdgeStyle(strokeColor);
25
+ const markerStroke = resolveEdgePathStroke(style);
26
+ return {
27
+ id: t.id,
28
+ source: t.fromNodeId,
29
+ target: t.toNodeId,
30
+ sourceHandle: `${CHOICE_HANDLE_PREFIX}${t.fromChoiceId}`,
31
+ targetHandle: TARGET_HANDLE_ID,
32
+ reconnectable: 'target',
33
+ type: edgeType,
34
+ label: showLabel ? edgeLabelForTransition(tree, t) : undefined,
35
+ labelShowBg: true,
36
+ markerEnd: buildEdgeMarker(markerStroke),
37
+ style,
38
+ };
39
+ });
40
+ }
41
+ export function buildTransitionsFromEdges(edges, existing) {
42
+ const existingById = new Map(existing.map((t) => [t.id, t]));
43
+ return edges
44
+ .filter((e) => e.source && e.target && e.source !== END_NODE_ID)
45
+ .map((e) => {
46
+ const fromChoiceId = choiceIdFromHandle(e.sourceHandle);
47
+ const prior = existingById.get(String(e.id));
48
+ return {
49
+ id: String(e.id),
50
+ fromNodeId: String(e.source),
51
+ fromChoiceId: fromChoiceId,
52
+ toNodeId: String(e.target),
53
+ outcome: String(e.target) === END_NODE_ID ? (prior?.outcome ?? TERMINAL_OUTCOME.AT_RISK) : undefined,
54
+ };
55
+ })
56
+ .filter((t) => t.fromChoiceId.length > 0);
57
+ }
@@ -0,0 +1,16 @@
1
+ import type { Edge } from 'reactflow';
2
+ export declare function getPromptNodeBorderClass(hasErrors: boolean, warningCount: number): string;
3
+ export declare function getIssueEdgeStyle(style: Edge['style'], hasIssue: boolean): Edge['style'];
4
+ export declare function resolveEdgePathStroke(style: Edge['style'] | undefined): string;
5
+ export declare function buildEdgeMarker(stroke: string): {
6
+ color: string;
7
+ type: import("reactflow").MarkerType;
8
+ width: number;
9
+ height: number;
10
+ };
11
+ export declare function buildEdgeStyle(strokeColor: string | undefined): Edge['style'];
12
+ export declare function resolveSelectedEdgeStroke(style: Edge['style'], isEdgeSelected: boolean): {
13
+ style: Edge['style'];
14
+ stroke: string;
15
+ };
16
+ //# sourceMappingURL=edgeStyle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edgeStyle.d.ts","sourceRoot":"","sources":["../../src/canvas/edgeStyle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAItC,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAQzF;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CASxF;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,SAAS,GAAG,MAAM,CAK9E;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM;;;;;EAK7C;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,CAE7E;AAED,wBAAgB,yBAAyB,CACrC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,EACpB,cAAc,EAAE,OAAO,GACxB;IAAE,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAO1C"}
@@ -0,0 +1,44 @@
1
+ import { DEFAULT_CANVAS_EDGE_STROKE } from '@signalsafe/tree-spec-editor-core';
2
+ import { BORDER_DANGER_CLASS, BORDER_WARNING_CLASS, EDGE_ARROW_MARKER, SELECTED_EDGE_STROKE } from './constants.js';
3
+ export function getPromptNodeBorderClass(hasErrors, warningCount) {
4
+ if (hasErrors) {
5
+ return BORDER_DANGER_CLASS;
6
+ }
7
+ if (warningCount > 0) {
8
+ return BORDER_WARNING_CLASS;
9
+ }
10
+ return '';
11
+ }
12
+ export function getIssueEdgeStyle(style, hasIssue) {
13
+ if (!hasIssue) {
14
+ return style;
15
+ }
16
+ return {
17
+ ...style,
18
+ strokeWidth: 2,
19
+ strokeDasharray: '6 4',
20
+ };
21
+ }
22
+ export function resolveEdgePathStroke(style) {
23
+ if (style?.stroke && typeof style.stroke === 'string') {
24
+ return style.stroke;
25
+ }
26
+ return DEFAULT_CANVAS_EDGE_STROKE;
27
+ }
28
+ export function buildEdgeMarker(stroke) {
29
+ return {
30
+ ...EDGE_ARROW_MARKER,
31
+ color: stroke,
32
+ };
33
+ }
34
+ export function buildEdgeStyle(strokeColor) {
35
+ return strokeColor ? { stroke: strokeColor, strokeWidth: 2 } : { strokeWidth: 2 };
36
+ }
37
+ export function resolveSelectedEdgeStroke(style, isEdgeSelected) {
38
+ let nextStyle = style;
39
+ const stroke = isEdgeSelected ? SELECTED_EDGE_STROKE : resolveEdgePathStroke(nextStyle);
40
+ if (isEdgeSelected) {
41
+ nextStyle = { ...nextStyle, stroke: SELECTED_EDGE_STROKE };
42
+ }
43
+ return { style: nextStyle, stroke };
44
+ }
@@ -0,0 +1,3 @@
1
+ import { type GraphSelection } from '@signalsafe/tree-spec-editor-core';
2
+ export declare function resolveCanvasFocusChoiceId(nodeId: string, selected: GraphSelection | undefined, focusNodeId: string | null | undefined, focusChoiceId: string | null | undefined): string | null;
3
+ //# sourceMappingURL=focusChoice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focusChoice.d.ts","sourceRoot":"","sources":["../../src/canvas/focusChoice.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAE9F,wBAAgB,0BAA0B,CACtC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,cAAc,GAAG,SAAS,EACpC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACzC,MAAM,GAAG,IAAI,CASf"}