@principal-ai/principal-view-react 0.16.56 → 0.16.58
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.
- package/dist/graphify/consolidated.d.ts +16 -1
- package/dist/graphify/consolidated.d.ts.map +1 -1
- package/dist/graphify/index.d.ts +1 -1
- package/dist/graphify/index.d.ts.map +1 -1
- package/dist/graphify/index.js.map +1 -1
- package/dist/graphify/kind.d.ts +1 -1
- package/dist/graphify/kind.d.ts.map +1 -1
- package/dist/graphify/kind.js +7 -1
- package/dist/graphify/kind.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/pierre/constructColors.d.ts +24 -0
- package/dist/pierre/constructColors.d.ts.map +1 -0
- package/dist/pierre/constructColors.js +91 -0
- package/dist/pierre/constructColors.js.map +1 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +8 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +116 -0
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -0
- package/dist/subsystem/ComponentDeclaration.d.ts +2 -2
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +12 -11
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +10 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +401 -40
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
- package/dist/subsystem/formatDeclaration.js +49 -10
- package/dist/subsystem/formatDeclaration.js.map +1 -1
- package/dist/subsystem/model.d.ts +89 -12
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +55 -25
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +32 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +122 -24
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/tokenizeComponent.js +1 -1
- package/dist/subsystem/tokenizeComponent.js.map +1 -1
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +4 -3
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +1 -1
- package/src/graphify/consolidated.ts +18 -1
- package/src/graphify/index.ts +1 -0
- package/src/graphify/kind.ts +7 -1
- package/src/index.ts +3 -2
- package/src/pierre/constructColors.test.ts +52 -0
- package/src/pierre/constructColors.ts +112 -0
- package/src/stories/{BoundaryNodes.stories.tsx → Audit/BoundaryNodes.stories.tsx} +1 -1
- package/src/stories/{CanvasEdgeTypes.stories.tsx → Audit/CanvasEdgeTypes.stories.tsx} +1 -1
- package/src/stories/{CanvasNodeTypes.stories.tsx → Audit/CanvasNodeTypes.stories.tsx} +1 -1
- package/src/stories/{ColorPriority.stories.tsx → Audit/ColorPriority.stories.tsx} +1 -1
- package/src/stories/{DiamondBadges.stories.tsx → Audit/DiamondBadges.stories.tsx} +1 -1
- package/src/stories/{NodeDimensionsTesting.stories.tsx → Audit/NodeDimensionsTesting.stories.tsx} +2 -2
- package/src/stories/{NodeFieldsAudit.stories.tsx → Audit/NodeFieldsAudit.stories.tsx} +1 -1
- package/src/stories/{NodeFontSizeTesting.stories.tsx → Audit/NodeFontSizeTesting.stories.tsx} +2 -2
- package/src/stories/{NodeShapes.stories.tsx → Audit/NodeShapes.stories.tsx} +1 -1
- package/src/stories/{WideNodesTesting.stories.tsx → Audit/WideNodesTesting.stories.tsx} +2 -2
- package/src/stories/{dashboard → Components}/DashboardRenderer.stories.tsx +2 -2
- package/src/stories/{GraphRenderer.stories.tsx → Components/GraphRenderer.stories.tsx} +2 -2
- package/src/stories/{MultiCanvasRenderer.stories.tsx → Components/MultiCanvasRenderer.stories.tsx} +2 -2
- package/src/stories/{SequenceDiagram.stories.tsx → Components/SequenceDiagram.stories.tsx} +2 -2
- package/src/stories/{StateView.stories.tsx → Components/StateView.stories.tsx} +2 -2
- package/src/stories/{TraceViewer.stories.tsx → Components/TraceViewer.stories.tsx} +3 -3
- package/src/stories/{WorkflowSequenceDiagram.stories.tsx → Components/WorkflowSequenceDiagram.stories.tsx} +1 -1
- package/src/stories/{AnimationWorkshop.stories.tsx → Features/AnimationWorkshop.stories.tsx} +5 -5
- package/src/stories/{ChangeTypeVisual.stories.tsx → Features/ChangeTypeVisual.stories.tsx} +2 -2
- package/src/stories/{EventDrivenAnimations.stories.tsx → Features/EventDrivenAnimations.stories.tsx} +2 -2
- package/src/stories/{MultiConfig.stories.tsx → Features/MultiConfig.stories.tsx} +2 -2
- package/src/stories/{ElkEdgeRouting.stories.tsx → Layout/ElkEdgeRouting.stories.tsx} +2 -2
- package/src/stories/{GraphOrientation.stories.tsx → Layout/GraphOrientation.stories.tsx} +3 -3
- package/src/stories/{MultiDirectionalConnections.stories.tsx → Layout/MultiDirectionalConnections.stories.tsx} +2 -2
- package/src/stories/{NodeDefinitionComparison.stories.tsx → Nodes/NodeDefinitionComparison.stories.tsx} +2 -2
- package/src/stories/{NodeNameFallbacks.stories.tsx → Nodes/NodeNameFallbacks.stories.tsx} +2 -2
- package/src/stories/{NumericColors.stories.tsx → Nodes/NumericColors.stories.tsx} +2 -2
- package/src/stories/{EventNameDisplay.stories.tsx → OTEL/EventNameDisplay.stories.tsx} +2 -2
- package/src/stories/{EventNameFallback.stories.tsx → OTEL/EventNameFallback.stories.tsx} +2 -2
- package/src/stories/{OtelComponents.stories.tsx → OTEL/OtelComponents.stories.tsx} +3 -3
- package/src/stories/{OtelNodeTypesPrototype.stories.tsx → OTEL/OtelNodeTypesPrototype.stories.tsx} +2 -2
- package/src/stories/{RawEventCards.stories.tsx → OTEL/RawEventCards.stories.tsx} +3 -3
- package/src/stories/{SpanBadges.stories.tsx → OTEL/SpanBadges.stories.tsx} +1 -1
- package/src/stories/{ValidationEventsCanvas.stories.tsx → OTEL/ValidationEventsCanvas.stories.tsx} +3 -3
- package/src/stories/{BacklogMdCanvases.stories.tsx → RealWorld/BacklogMdCanvases.stories.tsx} +2 -2
- package/src/stories/{FileCitySequence.stories.tsx → RealWorld/FileCitySequence.stories.tsx} +4 -4
- package/src/stories/{RealCanvasFiles.stories.tsx → RealWorld/RealCanvasFiles.stories.tsx} +7 -7
- package/src/stories/{ComponentDeclarationAudit.stories.tsx → Subsystem/ComponentDeclarationAudit.stories.tsx} +19 -20
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +365 -0
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +137 -0
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +238 -0
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +315 -0
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +181 -0
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +959 -0
- package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +646 -0
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +129 -0
- package/src/subsystem/ComponentDeclaration.tsx +13 -13
- package/src/subsystem/SubsystemComponentGraph.tsx +558 -44
- package/src/subsystem/formatDeclaration.ts +57 -11
- package/src/subsystem/model.test.ts +38 -6
- package/src/subsystem/model.ts +145 -32
- package/src/subsystem/nodes.test.ts +56 -0
- package/src/subsystem/nodes.tsx +161 -31
- package/src/subsystem/tokenizeComponent.ts +1 -1
- package/src/utils/elkLayout.ts +4 -3
- package/src/stories/SubsystemComponentGraph.stories.tsx +0 -1225
- /package/src/stories/{dashboard/sample-dashboards → data}/activity-feed-analytics.dashboard.json +0 -0
|
@@ -15,10 +15,10 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
15
15
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
16
16
|
import { ReactFlow, ReactFlowProvider, Background, BackgroundVariant, Controls, useReactFlow, useViewport, applyNodeChanges, } from '@xyflow/react';
|
|
17
17
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
18
|
-
import { Map as MapIcon } from 'lucide-react';
|
|
18
|
+
import { Map as MapIcon, X } from 'lucide-react';
|
|
19
19
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
20
20
|
import { buildSubsystemGraph, MECHANISM_COLOR, subsystemGraphLayoutKey, } from './model';
|
|
21
|
-
import { SubsystemComponentNode, SubsystemEdge, SUBSYSTEM_CALLBACKS } from './nodes';
|
|
21
|
+
import { SubsystemComponentNode, SubsystemEdge, SUBSYSTEM_CALLBACKS, hexWithAlpha, EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility } from './nodes';
|
|
22
22
|
import { SubsystemFileTree } from './SubsystemFileTree';
|
|
23
23
|
import { GraphLayoutCover } from './GraphLayoutCover';
|
|
24
24
|
import { ComponentDeclaration } from './ComponentDeclaration';
|
|
@@ -40,7 +40,7 @@ const DrawerContent = memo(function DrawerContent({ render, file, startLine, })
|
|
|
40
40
|
return null;
|
|
41
41
|
return _jsx(_Fragment, { children: render(file, startLine != null ? { startLine } : undefined) });
|
|
42
42
|
});
|
|
43
|
-
function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, onFileSelect, onVerifyComponent, componentVerification }) {
|
|
43
|
+
function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, onFileSelect, onVerifyComponent, componentVerification }) {
|
|
44
44
|
const { theme } = useTheme();
|
|
45
45
|
const { fitView } = useReactFlow();
|
|
46
46
|
const viewport = useViewport();
|
|
@@ -58,6 +58,16 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
58
58
|
const [legendOpen, setLegendOpen] = useState(false);
|
|
59
59
|
// Component the pointer is over (null on leave) → transient tree highlight.
|
|
60
60
|
const [hoveredComponentId, setHoveredComponentId] = useState(null);
|
|
61
|
+
// Throughline focus — selected flow (or step) is full strength; other
|
|
62
|
+
// opened-flow members stay visible but dimmed; everything else is hidden.
|
|
63
|
+
const [focusedThroughlineId, setFocusedThroughlineId] = useState(null);
|
|
64
|
+
// `null` = whole flow focused; a number = that single step's edge focused.
|
|
65
|
+
const [focusedStepIndex, setFocusedStepIndex] = useState(null);
|
|
66
|
+
// Sidebar bottom half: which panel is shown when throughlines exist.
|
|
67
|
+
const [sidebarView, setSidebarView] = useState(() => throughlines?.length ? 'flows' : 'files');
|
|
68
|
+
// Throughline flows the user has expanded (via the title row). Closed by
|
|
69
|
+
// default so a graph with several flows doesn't dump every step list at once.
|
|
70
|
+
const [expandedThroughlines, setExpandedThroughlines] = useState(new Set());
|
|
61
71
|
// Ref mirror of `selected` so the SUBSYSTEM_CALLBACKS click handler (a
|
|
62
72
|
// closure over the effect deps) can toggle without a stale value.
|
|
63
73
|
const selectedRef = useRef(null);
|
|
@@ -88,11 +98,20 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
88
98
|
measuredDimsRef.current = new Map();
|
|
89
99
|
prevMeasuredSigRef.current = '';
|
|
90
100
|
const doc = { components: componentsRef.current, edges: edgesRef.current };
|
|
91
|
-
void buildSubsystemGraph(doc, { maxNodeWidth, showEdgeLabels })
|
|
101
|
+
void buildSubsystemGraph(doc, { maxNodeWidth, showEdgeLabels })
|
|
102
|
+
.then(({ nodes, edges: e }) => {
|
|
92
103
|
if (!alive)
|
|
93
104
|
return;
|
|
94
105
|
setBuilt({ nodes, edges: e });
|
|
95
106
|
setLayoutReady(false);
|
|
107
|
+
})
|
|
108
|
+
.catch((err) => {
|
|
109
|
+
console.warn('[subsystem-graph] initial layout failed:', err);
|
|
110
|
+
if (!alive)
|
|
111
|
+
return;
|
|
112
|
+
// Reveal the cover even on failure so the UI is not stuck forever.
|
|
113
|
+
setBuilt({ nodes: [], edges: [] });
|
|
114
|
+
setLayoutReady(true);
|
|
96
115
|
});
|
|
97
116
|
return () => { alive = false; };
|
|
98
117
|
}, [layoutKey, maxNodeWidth, showEdgeLabels]);
|
|
@@ -141,6 +160,9 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
141
160
|
}
|
|
142
161
|
const src = edgeById.get(edgeId);
|
|
143
162
|
setSelectedEdgeId(edgeId);
|
|
163
|
+
// A direct edge selection on the canvas supersedes any throughline focus.
|
|
164
|
+
setFocusedThroughlineId(null);
|
|
165
|
+
setFocusedStepIndex(null);
|
|
144
166
|
if (src)
|
|
145
167
|
onEdgeSelect?.(src);
|
|
146
168
|
}, [edgeById, onEdgeSelect]);
|
|
@@ -157,6 +179,8 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
157
179
|
}
|
|
158
180
|
setSelected(comp);
|
|
159
181
|
setSelectedEdgeId(null);
|
|
182
|
+
setFocusedThroughlineId(null);
|
|
183
|
+
setFocusedStepIndex(null);
|
|
160
184
|
onSelect?.(id);
|
|
161
185
|
}
|
|
162
186
|
};
|
|
@@ -173,45 +197,151 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
173
197
|
const { nodes, edges: convertedEdges } = built;
|
|
174
198
|
const xyflowNodesBase = nodes;
|
|
175
199
|
const baseEdges = convertedEdges;
|
|
200
|
+
// When an edge is selected, dim every other edge + its label to focus it.
|
|
201
|
+
const [selectedEdgeId, setSelectedEdgeId] = useState(null);
|
|
202
|
+
// Ref mirror so `selectEdge` (a useCallback over early deps) can toggle
|
|
203
|
+
// without a stale closure value.
|
|
204
|
+
const selectedEdgeIdRef = useRef(null);
|
|
205
|
+
selectedEdgeIdRef.current = selectedEdgeId;
|
|
206
|
+
// Edge ids in throughline focus (an active flow's edge set, or a single
|
|
207
|
+
// step's edge). Used to frame the camera. `null` = no throughline focus.
|
|
208
|
+
const focusEdgeIds = useMemo(() => {
|
|
209
|
+
if (focusedThroughlineId == null || !throughlines)
|
|
210
|
+
return null;
|
|
211
|
+
const tl = throughlines.find((t) => t.id === focusedThroughlineId);
|
|
212
|
+
if (!tl)
|
|
213
|
+
return null;
|
|
214
|
+
if (focusedStepIndex != null) {
|
|
215
|
+
const step = tl.steps[focusedStepIndex];
|
|
216
|
+
return step ? new Set([step.edgeId]) : null;
|
|
217
|
+
}
|
|
218
|
+
return new Set(tl.steps.map((s) => s.edgeId));
|
|
219
|
+
}, [throughlines, focusedThroughlineId, focusedStepIndex]);
|
|
220
|
+
// 1-based step numbers per edge of the selected flow (an edge can appear
|
|
221
|
+
// in more than one step).
|
|
222
|
+
const selectedFlowStepNos = useMemo(() => {
|
|
223
|
+
if (focusedThroughlineId == null || !throughlines)
|
|
224
|
+
return null;
|
|
225
|
+
const tl = throughlines.find((t) => t.id === focusedThroughlineId);
|
|
226
|
+
if (!tl)
|
|
227
|
+
return null;
|
|
228
|
+
const map = new Map();
|
|
229
|
+
tl.steps.forEach((s, i) => {
|
|
230
|
+
const list = map.get(s.edgeId) ?? [];
|
|
231
|
+
list.push(i + 1);
|
|
232
|
+
map.set(s.edgeId, list);
|
|
233
|
+
});
|
|
234
|
+
return map;
|
|
235
|
+
}, [throughlines, focusedThroughlineId]);
|
|
236
|
+
// Union of every expanded (opened) throughline's edges — the visible set.
|
|
237
|
+
const openedEdgeIds = useMemo(() => {
|
|
238
|
+
if (!throughlines || expandedThroughlines.size === 0)
|
|
239
|
+
return null;
|
|
240
|
+
const ids = new Set();
|
|
241
|
+
for (const tl of throughlines) {
|
|
242
|
+
if (!expandedThroughlines.has(tl.id))
|
|
243
|
+
continue;
|
|
244
|
+
for (const s of tl.steps)
|
|
245
|
+
ids.add(s.edgeId);
|
|
246
|
+
}
|
|
247
|
+
return ids.size > 0 ? ids : null;
|
|
248
|
+
}, [throughlines, expandedThroughlines]);
|
|
249
|
+
const endpointsOf = (edgeIds) => {
|
|
250
|
+
if (!edgeIds)
|
|
251
|
+
return null;
|
|
252
|
+
const ids = new Set();
|
|
253
|
+
for (const e of baseEdges) {
|
|
254
|
+
if (!edgeIds.has(e.id))
|
|
255
|
+
continue;
|
|
256
|
+
ids.add(e.source);
|
|
257
|
+
ids.add(e.target);
|
|
258
|
+
}
|
|
259
|
+
return ids.size > 0 ? ids : null;
|
|
260
|
+
};
|
|
261
|
+
const openedNodeIds = useMemo(() => endpointsOf(openedEdgeIds),
|
|
262
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
263
|
+
[baseEdges, openedEdgeIds]);
|
|
264
|
+
// Endpoints of the bright set: the selected step's edge, or the whole flow
|
|
265
|
+
// when no step is focused.
|
|
266
|
+
const brightNodeIds = useMemo(() => endpointsOf(focusEdgeIds),
|
|
267
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
268
|
+
[baseEdges, focusEdgeIds]);
|
|
269
|
+
// Source + target of the focused step/flow (or a canvas-selected edge). If a
|
|
270
|
+
// file is open, those endpoints stay undimmed even when they don't live in
|
|
271
|
+
// that file.
|
|
272
|
+
const focusNodeIds = useMemo(() => {
|
|
273
|
+
if (brightNodeIds)
|
|
274
|
+
return brightNodeIds;
|
|
275
|
+
if (selectedEdgeId) {
|
|
276
|
+
const e = baseEdges.find((x) => x.id === selectedEdgeId);
|
|
277
|
+
if (e)
|
|
278
|
+
return new Set([e.source, e.target]);
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}, [baseEdges, brightNodeIds, selectedEdgeId]);
|
|
176
282
|
// While a file is open in the drawer, tag each node with whether its
|
|
177
283
|
// component lives in that file — the node renderer spotlights matches and
|
|
178
284
|
// dims non-matches (mirrors the edge-dimming behavior on selection).
|
|
285
|
+
// Opened-but-unselected members are dimmed; a selected step further dims
|
|
286
|
+
// the rest of its own flow. Nodes not on any opened flow are hidden.
|
|
179
287
|
// `isSelected` rides in data because the node's stopPropagation() keeps
|
|
180
|
-
// React Flow's own selection state from
|
|
288
|
+
// React Flow's own selection state from updating.
|
|
181
289
|
const dispNodes = useMemo(() => {
|
|
182
290
|
return xyflowNodesBase.map((n) => {
|
|
183
291
|
const comp = n.data?.component;
|
|
184
|
-
const fileMatch =
|
|
292
|
+
const fileMatch = fileMatchForNode(comp?.file, openFile, focusNodeIds?.has(n.id) === true);
|
|
185
293
|
const isSelected = selected?.id !== undefined && comp?.id === selected.id;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
294
|
+
const vis = flowElementVisibility({
|
|
295
|
+
inOpened: openedNodeIds?.has(n.id) === true,
|
|
296
|
+
inSelected: brightNodeIds?.has(n.id) === true,
|
|
297
|
+
anyOpened: openedNodeIds != null,
|
|
298
|
+
anySelected: brightNodeIds != null,
|
|
299
|
+
});
|
|
300
|
+
if (fileMatch === undefined && !isSelected && !vis.dimmed) {
|
|
301
|
+
const { fileMatch: _f, isSelected: _s, dimmed: _d, ...rest } = n.data;
|
|
302
|
+
return { ...n, hidden: vis.hidden, data: rest };
|
|
189
303
|
}
|
|
190
304
|
return {
|
|
191
305
|
...n,
|
|
192
|
-
|
|
306
|
+
hidden: vis.hidden,
|
|
307
|
+
data: {
|
|
308
|
+
...n.data,
|
|
309
|
+
...(fileMatch !== undefined && { fileMatch }),
|
|
310
|
+
...(isSelected && { isSelected }),
|
|
311
|
+
...(vis.dimmed && { dimmed: true }),
|
|
312
|
+
},
|
|
193
313
|
};
|
|
194
314
|
});
|
|
195
|
-
}, [xyflowNodesBase, openFile, selected]);
|
|
315
|
+
}, [xyflowNodesBase, openFile, selected, focusNodeIds, openedNodeIds, brightNodeIds]);
|
|
196
316
|
const baseNodesKey = useMemo(() => nodes.map((n) => n.id).sort().join(','), [nodes]);
|
|
197
317
|
const baseEdgesKey = useMemo(() => convertedEdges.map((e) => e.id).sort().join(','), [convertedEdges]);
|
|
198
|
-
// When an edge is selected, dim every other edge + its label to focus it.
|
|
199
|
-
const [selectedEdgeId, setSelectedEdgeId] = useState(null);
|
|
200
|
-
// Ref mirror so `selectEdge` (a useCallback over early deps) can toggle
|
|
201
|
-
// without a stale closure value.
|
|
202
|
-
const selectedEdgeIdRef = useRef(null);
|
|
203
|
-
selectedEdgeIdRef.current = selectedEdgeId;
|
|
204
318
|
const dispEdges = useMemo(() => {
|
|
319
|
+
const paint = (e, dimmed) => {
|
|
320
|
+
const markerEnd = e.markerEnd;
|
|
321
|
+
const nextMarker = dimmed && markerEnd && typeof markerEnd === 'object' && typeof markerEnd.color === 'string'
|
|
322
|
+
? { ...markerEnd, color: hexWithAlpha(markerEnd.color, EDGE_DIM_ALPHA) }
|
|
323
|
+
: markerEnd;
|
|
324
|
+
return {
|
|
325
|
+
...e,
|
|
326
|
+
data: { ...e.data, dimmed },
|
|
327
|
+
markerEnd: nextMarker,
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
if (openedEdgeIds || focusEdgeIds) {
|
|
331
|
+
return baseEdges.map((e) => {
|
|
332
|
+
const vis = flowElementVisibility({
|
|
333
|
+
inOpened: openedEdgeIds?.has(e.id) === true,
|
|
334
|
+
inSelected: focusEdgeIds?.has(e.id) === true,
|
|
335
|
+
anyOpened: openedEdgeIds != null,
|
|
336
|
+
anySelected: focusEdgeIds != null,
|
|
337
|
+
});
|
|
338
|
+
return { ...paint(e, vis.dimmed), hidden: vis.hidden };
|
|
339
|
+
});
|
|
340
|
+
}
|
|
205
341
|
if (!selectedEdgeId)
|
|
206
342
|
return baseEdges;
|
|
207
|
-
return baseEdges.map((e) => (
|
|
208
|
-
|
|
209
|
-
data: {
|
|
210
|
-
...e.data,
|
|
211
|
-
dimmed: e.id !== selectedEdgeId,
|
|
212
|
-
},
|
|
213
|
-
}));
|
|
214
|
-
}, [baseEdges, selectedEdgeId]);
|
|
343
|
+
return baseEdges.map((e) => paint(e, e.id !== selectedEdgeId));
|
|
344
|
+
}, [baseEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds]);
|
|
215
345
|
const onNodesChange = useCallback((changes) => {
|
|
216
346
|
// Capture dimension changes (React Flow's measurement callback).
|
|
217
347
|
for (const ch of changes) {
|
|
@@ -241,6 +371,8 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
241
371
|
const onNodeClick = useCallback((_e, node) => {
|
|
242
372
|
const comp = node.data?.component;
|
|
243
373
|
setSelectedEdgeId(null);
|
|
374
|
+
setFocusedThroughlineId(null);
|
|
375
|
+
setFocusedStepIndex(null);
|
|
244
376
|
if (node.type === 'subsystem-component' && comp) {
|
|
245
377
|
// Clicking the already-selected node unselects it (toggle off).
|
|
246
378
|
// Selection is independent of the file drawer — nodes never open it.
|
|
@@ -260,10 +392,13 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
260
392
|
const onPaneClick = useCallback(() => {
|
|
261
393
|
setSelected(null);
|
|
262
394
|
setSelectedEdgeId(null);
|
|
395
|
+
setFocusedThroughlineId(null);
|
|
396
|
+
setFocusedStepIndex(null);
|
|
263
397
|
}, []);
|
|
264
398
|
// Sidebar file trees — one per repo on multi-repo graphs, each under its
|
|
265
399
|
// own owner-avatar header. Clicking a header collapses that repo's tree.
|
|
266
400
|
const repoGroups = useMemo(() => buildRepoGroups(components), [components]);
|
|
401
|
+
const hasThroughlines = useMemo(() => (throughlines?.length ?? 0) > 0, [throughlines]);
|
|
267
402
|
const [collapsedRepos, setCollapsedRepos] = useState(new Set());
|
|
268
403
|
const toggleRepoCollapsed = useCallback((key) => {
|
|
269
404
|
setCollapsedRepos((prev) => {
|
|
@@ -294,6 +429,74 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
294
429
|
setOpenFileTarget({ file, startLine });
|
|
295
430
|
onFileSelect?.(file);
|
|
296
431
|
}, [onFileSelect]);
|
|
432
|
+
// Camera helper shared by the throughline interactions: frames the focused
|
|
433
|
+
// edges' endpoint nodes via `fitView({ nodes })`, which uses the store's
|
|
434
|
+
// live positions + measured dims — so the frame always includes BOTH
|
|
435
|
+
// components the edge attaches to (and therefore the edge line between them).
|
|
436
|
+
const fitFocusBounds = useCallback((ids) => {
|
|
437
|
+
const nodeIds = new Set();
|
|
438
|
+
for (const e of baseEdges) {
|
|
439
|
+
if (!ids.has(e.id))
|
|
440
|
+
continue;
|
|
441
|
+
nodeIds.add(e.source);
|
|
442
|
+
nodeIds.add(e.target);
|
|
443
|
+
}
|
|
444
|
+
if (nodeIds.size === 0)
|
|
445
|
+
return;
|
|
446
|
+
fitView({
|
|
447
|
+
nodes: [...nodeIds].map((id) => ({ id })),
|
|
448
|
+
padding: 0.25,
|
|
449
|
+
duration: 300,
|
|
450
|
+
});
|
|
451
|
+
}, [baseEdges, fitView]);
|
|
452
|
+
// Focus an entire flow: hide everything but the flow's nodes and edges, and
|
|
453
|
+
// frame the flow on the canvas. Selection state is cleared — the graph now
|
|
454
|
+
// reads as the narrative.
|
|
455
|
+
const focusThroughlineEdges = useCallback((tl) => {
|
|
456
|
+
setSelected(null);
|
|
457
|
+
setSelectedEdgeId(null);
|
|
458
|
+
setFocusedStepIndex(null);
|
|
459
|
+
setFocusedThroughlineId(tl.id);
|
|
460
|
+
fitFocusBounds(new Set(tl.steps.map((s) => s.edgeId)));
|
|
461
|
+
}, [fitFocusBounds]);
|
|
462
|
+
const clearThroughlineFocus = useCallback(() => {
|
|
463
|
+
setFocusedThroughlineId(null);
|
|
464
|
+
setFocusedStepIndex(null);
|
|
465
|
+
}, []);
|
|
466
|
+
// Focus a single step's edge on the canvas. The step's file:line is listed
|
|
467
|
+
// in the row; we don't open the drawer from here.
|
|
468
|
+
const focusThroughlineStep = useCallback((tl, stepIndex) => {
|
|
469
|
+
const step = tl.steps[stepIndex];
|
|
470
|
+
if (!step)
|
|
471
|
+
return;
|
|
472
|
+
setSelected(null);
|
|
473
|
+
setSelectedEdgeId(null);
|
|
474
|
+
setFocusedStepIndex(stepIndex);
|
|
475
|
+
setFocusedThroughlineId(tl.id);
|
|
476
|
+
fitFocusBounds(new Set([step.edgeId]));
|
|
477
|
+
}, [fitFocusBounds]);
|
|
478
|
+
const toggleThroughlineCollapsed = useCallback((tlId) => {
|
|
479
|
+
setExpandedThroughlines((prev) => {
|
|
480
|
+
const next = new Set(prev);
|
|
481
|
+
if (next.has(tlId))
|
|
482
|
+
next.delete(tlId);
|
|
483
|
+
else
|
|
484
|
+
next.add(tlId);
|
|
485
|
+
return next;
|
|
486
|
+
});
|
|
487
|
+
}, []);
|
|
488
|
+
// Filename-badge clicks on nodes open the drawer through the same path as
|
|
489
|
+
// the declaration panel's file link (toggle + tree sync, no start line).
|
|
490
|
+
useEffect(() => {
|
|
491
|
+
SUBSYSTEM_CALLBACKS.onOpenFile = (componentId) => {
|
|
492
|
+
const comp = components.find((c) => c.id === componentId);
|
|
493
|
+
if (comp?.file)
|
|
494
|
+
onOpenDeclarationFile(comp.file);
|
|
495
|
+
};
|
|
496
|
+
return () => {
|
|
497
|
+
SUBSYSTEM_CALLBACKS.onOpenFile = undefined;
|
|
498
|
+
};
|
|
499
|
+
}, [components, onOpenDeclarationFile]);
|
|
297
500
|
// Detail-panel links: related-name clicks select the matching component —
|
|
298
501
|
// resolved by id, name, or symbol (call labels may carry a trailing `()`).
|
|
299
502
|
const resolveRelatedComponent = useCallback((ref) => {
|
|
@@ -306,13 +509,17 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
306
509
|
return;
|
|
307
510
|
setSelected(comp);
|
|
308
511
|
setSelectedEdgeId(null);
|
|
512
|
+
setFocusedThroughlineId(null);
|
|
513
|
+
setFocusedStepIndex(null);
|
|
309
514
|
onSelect?.(comp.id);
|
|
310
515
|
}, [components, onSelect]);
|
|
311
516
|
// Edge label data for the overlay (rendered OUTSIDE ReactFlow so the pane
|
|
312
517
|
// doesn't intercept pointer events). Uses ELK-computed label midpoints from
|
|
313
518
|
// the actual edge path (not node-center approximations).
|
|
314
519
|
const edgeLabels = useMemo(() => {
|
|
315
|
-
return dispEdges
|
|
520
|
+
return dispEdges
|
|
521
|
+
.filter((e) => !e.hidden)
|
|
522
|
+
.map((e) => {
|
|
316
523
|
const d = e.data;
|
|
317
524
|
return {
|
|
318
525
|
id: e.id,
|
|
@@ -320,9 +527,10 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
320
527
|
dimmed: d?.dimmed === true,
|
|
321
528
|
midX: d?.labelX ?? 0,
|
|
322
529
|
midY: d?.labelY ?? 0,
|
|
530
|
+
stepNos: selectedFlowStepNos?.get(e.id),
|
|
323
531
|
};
|
|
324
532
|
});
|
|
325
|
-
}, [dispEdges]);
|
|
533
|
+
}, [dispEdges, selectedFlowStepNos]);
|
|
326
534
|
const usedMechanisms = useMemo(() => {
|
|
327
535
|
return new Set(edgeLabels.map((l) => l.mechanism));
|
|
328
536
|
}, [edgeLabels]);
|
|
@@ -351,7 +559,7 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
351
559
|
const fileViewerRef = useRef(fileViewer);
|
|
352
560
|
fileViewerRef.current = fileViewer;
|
|
353
561
|
const renderDrawerContent = useCallback((file, opts) => fileViewerRef.current?.(file, opts) ?? null, []);
|
|
354
|
-
return (_jsxs("div", { style: { width: '100%', height: '100%', display: 'flex', flexDirection: 'row' }, children: [(title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0) && (_jsxs("div", { style: {
|
|
562
|
+
return (_jsxs("div", { style: { width: '100%', height: '100%', display: 'flex', flexDirection: 'row' }, children: [(title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasThroughlines) && (_jsxs("div", { style: {
|
|
355
563
|
width: 340,
|
|
356
564
|
minWidth: 340,
|
|
357
565
|
borderRight: `1px solid ${theme.colors.border}`,
|
|
@@ -373,7 +581,7 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
373
581
|
fontWeight: 600,
|
|
374
582
|
color: theme.colors.text,
|
|
375
583
|
fontFamily: theme.fonts.heading,
|
|
376
|
-
}, children: title })), description && (_jsx("div", { style: { fontSize: theme.fontSizes[0], lineHeight: 1.5 }, children: _jsx(IndustryMarkdownSlide, { content: description, slideIdPrefix: "subsystem-desc", slideIndex: 0, isVisible: true, theme: theme, disableScroll: true, fontSizeScale: 0.9, enableKeyboardScrolling: false, autoFocusOnVisible: false }) })), sidebarAfterDescription] }), treeFilePaths.length > 0 && (
|
|
584
|
+
}, children: title })), description && (_jsx("div", { style: { fontSize: theme.fontSizes[0], lineHeight: 1.5 }, children: _jsx(IndustryMarkdownSlide, { content: description, slideIdPrefix: "subsystem-desc", slideIndex: 0, isVisible: true, theme: theme, disableScroll: true, fontSizeScale: 0.9, enableKeyboardScrolling: false, autoFocusOnVisible: false }) })), sidebarAfterDescription] }), (treeFilePaths.length > 0 || hasThroughlines) && (_jsxs("div", { style: {
|
|
377
585
|
height: '50%',
|
|
378
586
|
minHeight: 160,
|
|
379
587
|
flexShrink: 0,
|
|
@@ -381,17 +589,42 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
381
589
|
display: 'flex',
|
|
382
590
|
flexDirection: 'column',
|
|
383
591
|
overflow: 'hidden',
|
|
384
|
-
}, children:
|
|
385
|
-
const groupKey = group.repoKey ?? '__no-repo__';
|
|
386
|
-
const collapsed = collapsedRepos.has(groupKey);
|
|
387
|
-
return (_jsxs("div", { style: {
|
|
388
|
-
flex: collapsed ? '0 0 auto' : 1,
|
|
389
|
-
minHeight: collapsed ? 0 : undefined,
|
|
592
|
+
}, children: [hasThroughlines && (_jsx("div", { role: "tablist", "aria-label": "Sidebar view", style: {
|
|
390
593
|
display: 'flex',
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
594
|
+
width: '100%',
|
|
595
|
+
flexShrink: 0,
|
|
596
|
+
borderBottom: `1px solid ${theme.colors.border}`,
|
|
597
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
598
|
+
}, children: ['files', 'flows'].map((view) => (_jsx("button", { type: "button", role: "tab", "aria-selected": sidebarView === view, onClick: () => setSidebarView(view), style: {
|
|
599
|
+
flex: 1,
|
|
600
|
+
minWidth: 0,
|
|
601
|
+
padding: '8px 8px',
|
|
602
|
+
border: 'none',
|
|
603
|
+
borderRadius: 0,
|
|
604
|
+
background: sidebarView === view ? theme.colors.background : 'transparent',
|
|
605
|
+
color: sidebarView === view
|
|
606
|
+
? theme.colors.text
|
|
607
|
+
: theme.colors.textSecondary,
|
|
608
|
+
fontSize: theme.fontSizes[1],
|
|
609
|
+
fontFamily: theme.fonts.monospace,
|
|
610
|
+
textTransform: 'capitalize',
|
|
611
|
+
cursor: 'pointer',
|
|
612
|
+
}, children: view }, view))) })), sidebarView === 'flows' && throughlines && throughlines.length > 0 ? (_jsx("div", { style: {
|
|
613
|
+
flex: 1,
|
|
614
|
+
minHeight: 0,
|
|
615
|
+
overflowY: 'auto',
|
|
616
|
+
padding: '0 4px 12px',
|
|
617
|
+
}, children: throughlines.map((tl) => (_jsx(ThroughlineFlow, { throughline: tl, edges: edges, collapsed: !expandedThroughlines.has(tl.id), active: focusedThroughlineId === tl.id ? { stepIndex: focusedStepIndex } : null, onToggleCollapsed: toggleThroughlineCollapsed, onFocusFlow: focusThroughlineEdges, onClearFocus: clearThroughlineFocus, onFocusStep: focusThroughlineStep }, tl.id))) })) : treeFilePaths.length > 0 ? (_jsx(_Fragment, { children: repoGroups.groups.map((group, i) => {
|
|
618
|
+
const groupKey = group.repoKey ?? '__no-repo__';
|
|
619
|
+
const collapsed = collapsedRepos.has(groupKey);
|
|
620
|
+
return (_jsxs("div", { style: {
|
|
621
|
+
flex: collapsed ? '0 0 auto' : 1,
|
|
622
|
+
minHeight: collapsed ? 0 : undefined,
|
|
623
|
+
display: 'flex',
|
|
624
|
+
flexDirection: 'column',
|
|
625
|
+
borderTop: i > 0 ? `1px solid ${theme.colors.border}` : undefined,
|
|
626
|
+
}, children: [repoGroups.multiRepo && (_jsx(RepoGroupHeader, { group: group, collapsed: collapsed, onToggle: () => toggleRepoCollapsed(groupKey) })), !collapsed && (_jsx(SubsystemFileTree, { files: group.entries.map((e) => e.displayPath), selectedFile: selected?.file ?? openFile, hoveredFile: hoveredFile, onSelectFile: onTreeSelectFile, headerless: repoGroups.multiRepo }))] }, groupKey));
|
|
627
|
+
}) })) : null] }))] })), _jsxs("div", { style: { flex: 1, position: 'relative', display: 'flex', flexDirection: 'column', minHeight: 0 }, children: [_jsxs("div", { style: { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }, children: [showEdgeLabels !== false && (_jsx("div", { style: {
|
|
395
628
|
position: 'absolute',
|
|
396
629
|
inset: 0,
|
|
397
630
|
overflow: 'hidden',
|
|
@@ -410,6 +643,10 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
410
643
|
position: 'absolute',
|
|
411
644
|
left: screenX,
|
|
412
645
|
top: screenY,
|
|
646
|
+
// Center on the flow-space midpoint. Labels live in screen
|
|
647
|
+
// space (fixed size when zoomed out), so top-left anchoring
|
|
648
|
+
// would drift them right/down of the edge as zoom drops.
|
|
649
|
+
transform: 'translate(-50%, -50%)',
|
|
413
650
|
fontSize: 10,
|
|
414
651
|
fontFamily: theme.fonts.monospace,
|
|
415
652
|
fontWeight: 500,
|
|
@@ -422,7 +659,7 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
422
659
|
pointerEvents: 'auto',
|
|
423
660
|
opacity: lbl.dimmed ? 0.15 : 1,
|
|
424
661
|
whiteSpace: 'nowrap',
|
|
425
|
-
}, children: lbl.mechanism }, lbl.id));
|
|
662
|
+
}, children: lbl.stepNos?.length ? `${lbl.stepNos.map((n) => `${n}:`).join(' ')} ${lbl.mechanism}` : lbl.mechanism }, lbl.id));
|
|
426
663
|
}) })), _jsxs(ReactFlow, { nodes: dispNodes, edges: dispEdges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, minZoom: 0.05, maxZoom: 4, onNodeClick: onNodeClick, onEdgeClick: onEdgeClick, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, proOptions: { hideAttribution: true }, nodesDraggable: false, elementsSelectable: true, selectNodesOnDrag: false, nodesConnectable: false, edgesReconnectable: false, onPaneClick: onPaneClick, panOnDrag: true, panOnScroll: true, zoomOnScroll: false, zoomOnPinch: true, zoomOnDoubleClick: false, style: {
|
|
427
664
|
width: '100%',
|
|
428
665
|
height: '100%',
|
|
@@ -500,6 +737,130 @@ function RepoGroupHeader({ group, collapsed, onToggle, }) {
|
|
|
500
737
|
whiteSpace: 'nowrap',
|
|
501
738
|
}, children: label })] }));
|
|
502
739
|
}
|
|
740
|
+
/** One collapsible throughline in the sidebar's flows panel. Clicking the
|
|
741
|
+
* title: closed → open + select; open and unselected → select; open and
|
|
742
|
+
* selected → close + clear focus. The right-aligned close button collapses
|
|
743
|
+
* without selecting. A step row focuses that step's edge. */
|
|
744
|
+
function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollapsed, onFocusFlow, onClearFocus, onFocusStep, }) {
|
|
745
|
+
const { theme } = useTheme();
|
|
746
|
+
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
747
|
+
const hoverBg = theme.colors.background;
|
|
748
|
+
const edgeById = useMemo(() => new Map(edges.map((e) => [e.id, e])), [edges]);
|
|
749
|
+
const wholeFlowActive = active !== null && active.stepIndex === null;
|
|
750
|
+
const [headerHover, setHeaderHover] = useState(false);
|
|
751
|
+
const [closeHover, setCloseHover] = useState(false);
|
|
752
|
+
const [hoveredStep, setHoveredStep] = useState(null);
|
|
753
|
+
return (_jsxs("div", { style: { margin: '4px 0', borderRadius: 8 }, children: [_jsxs("div", { onMouseEnter: () => setHeaderHover(true), onMouseLeave: () => setHeaderHover(false), style: {
|
|
754
|
+
display: 'flex',
|
|
755
|
+
alignItems: 'center',
|
|
756
|
+
gap: 4,
|
|
757
|
+
borderRadius: 6,
|
|
758
|
+
background: wholeFlowActive || headerHover ? hoverBg : 'transparent',
|
|
759
|
+
transition: 'background 120ms ease',
|
|
760
|
+
}, children: [_jsx("button", { type: "button", onClick: () => {
|
|
761
|
+
if (collapsed) {
|
|
762
|
+
onToggleCollapsed(throughline.id);
|
|
763
|
+
onFocusFlow(throughline);
|
|
764
|
+
}
|
|
765
|
+
else if (active === null) {
|
|
766
|
+
onFocusFlow(throughline);
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
onToggleCollapsed(throughline.id);
|
|
770
|
+
onClearFocus();
|
|
771
|
+
}
|
|
772
|
+
}, style: {
|
|
773
|
+
flex: 1,
|
|
774
|
+
display: 'flex',
|
|
775
|
+
alignItems: 'center',
|
|
776
|
+
minWidth: 0,
|
|
777
|
+
padding: '6px 8px',
|
|
778
|
+
borderRadius: 6,
|
|
779
|
+
border: 'none',
|
|
780
|
+
background: 'transparent',
|
|
781
|
+
textAlign: 'left',
|
|
782
|
+
cursor: 'pointer',
|
|
783
|
+
}, children: _jsx("span", { style: {
|
|
784
|
+
overflow: 'hidden',
|
|
785
|
+
textOverflow: 'ellipsis',
|
|
786
|
+
whiteSpace: 'nowrap',
|
|
787
|
+
fontSize: theme.fontSizes[1],
|
|
788
|
+
fontFamily: theme.fonts.monospace,
|
|
789
|
+
fontWeight: 600,
|
|
790
|
+
color: theme.colors.text,
|
|
791
|
+
}, children: throughline.title }) }), !collapsed && (_jsx("button", { type: "button", "aria-label": `Close ${throughline.title}`, onMouseEnter: () => setCloseHover(true), onMouseLeave: () => setCloseHover(false), onClick: (e) => {
|
|
792
|
+
e.stopPropagation();
|
|
793
|
+
onToggleCollapsed(throughline.id);
|
|
794
|
+
if (active !== null)
|
|
795
|
+
onClearFocus();
|
|
796
|
+
}, style: {
|
|
797
|
+
display: 'inline-flex',
|
|
798
|
+
alignItems: 'center',
|
|
799
|
+
justifyContent: 'center',
|
|
800
|
+
flexShrink: 0,
|
|
801
|
+
width: 22,
|
|
802
|
+
height: 22,
|
|
803
|
+
marginRight: 4,
|
|
804
|
+
padding: 0,
|
|
805
|
+
border: 'none',
|
|
806
|
+
borderRadius: 4,
|
|
807
|
+
background: closeHover ? theme.colors.border : 'transparent',
|
|
808
|
+
color: closeHover ? theme.colors.text : muted,
|
|
809
|
+
cursor: 'pointer',
|
|
810
|
+
transition: 'background 120ms ease, color 120ms ease',
|
|
811
|
+
}, children: _jsx(X, { size: 12, strokeWidth: 2 }) }))] }), !collapsed && (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 1 }, children: throughline.steps.map((step, i) => {
|
|
812
|
+
const edge = edgeById.get(step.edgeId);
|
|
813
|
+
const mech = edge?.mechanism;
|
|
814
|
+
const color = mech ? MECHANISM_COLOR[mech] : muted;
|
|
815
|
+
const stepActive = active !== null && active.stepIndex === i;
|
|
816
|
+
return (_jsxs("button", { type: "button", onMouseEnter: () => setHoveredStep(i), onMouseLeave: () => setHoveredStep(null), onClick: () => onFocusStep(throughline, i), style: {
|
|
817
|
+
display: 'flex',
|
|
818
|
+
alignItems: 'center',
|
|
819
|
+
gap: 8,
|
|
820
|
+
minWidth: 0,
|
|
821
|
+
padding: '4px 8px 4px 12px',
|
|
822
|
+
textAlign: 'left',
|
|
823
|
+
borderRadius: 6,
|
|
824
|
+
border: 'none',
|
|
825
|
+
background: stepActive || hoveredStep === i ? hoverBg : 'transparent',
|
|
826
|
+
cursor: 'pointer',
|
|
827
|
+
transition: 'background 120ms ease',
|
|
828
|
+
}, children: [_jsx("span", { style: {
|
|
829
|
+
flexShrink: 0,
|
|
830
|
+
width: 14,
|
|
831
|
+
fontSize: theme.fontSizes[0] * 0.8,
|
|
832
|
+
fontFamily: theme.fonts.monospace,
|
|
833
|
+
color: stepActive ? theme.colors.text : muted,
|
|
834
|
+
}, children: i + 1 }), step.symbol ? (_jsx("span", { style: {
|
|
835
|
+
flex: 1,
|
|
836
|
+
minWidth: 0,
|
|
837
|
+
overflow: 'hidden',
|
|
838
|
+
textOverflow: 'ellipsis',
|
|
839
|
+
whiteSpace: 'nowrap',
|
|
840
|
+
fontSize: theme.fontSizes[0],
|
|
841
|
+
fontFamily: theme.fonts.monospace,
|
|
842
|
+
color: stepActive ? theme.colors.text : muted,
|
|
843
|
+
}, children: step.symbol })) : (_jsxs(_Fragment, { children: [_jsx("span", { style: {
|
|
844
|
+
flexShrink: 0,
|
|
845
|
+
width: 74,
|
|
846
|
+
overflow: 'hidden',
|
|
847
|
+
textOverflow: 'ellipsis',
|
|
848
|
+
whiteSpace: 'nowrap',
|
|
849
|
+
fontSize: theme.fontSizes[0],
|
|
850
|
+
fontFamily: theme.fonts.monospace,
|
|
851
|
+
color,
|
|
852
|
+
}, children: mech ?? step.edgeId }), _jsxs("span", { style: {
|
|
853
|
+
flex: 1,
|
|
854
|
+
minWidth: 0,
|
|
855
|
+
overflow: 'hidden',
|
|
856
|
+
textOverflow: 'ellipsis',
|
|
857
|
+
whiteSpace: 'nowrap',
|
|
858
|
+
fontSize: theme.fontSizes[0],
|
|
859
|
+
fontFamily: theme.fonts.monospace,
|
|
860
|
+
color: stepActive ? theme.colors.text : muted,
|
|
861
|
+
}, children: [step.file.split('/').pop(), _jsxs("span", { style: { opacity: 0.7 }, children: [":", step.line] })] })] }))] }, `${step.edgeId}-${i}`));
|
|
862
|
+
}) }))] }));
|
|
863
|
+
}
|
|
503
864
|
export function SubsystemComponentGraph(props) {
|
|
504
865
|
const wrapRef = useRef(null);
|
|
505
866
|
const [size, setSize] = useState(null);
|