@principal-ai/subsystems-react 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -85
- package/dist/index.d.ts +7 -44
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -32
- package/dist/index.js.map +1 -1
- package/dist/subsystem/SubsystemCarousel.d.ts +36 -0
- package/dist/subsystem/SubsystemCarousel.d.ts.map +1 -0
- package/dist/subsystem/SubsystemCarousel.js +163 -0
- package/dist/subsystem/SubsystemCarousel.js.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts +25 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +329 -68
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +9 -199
- package/src/subsystem/SubsystemComponentGraph.tsx +396 -50
- package/src/components/ConfigurationSelector.tsx +0 -133
- package/src/components/GraphRenderer.tsx +0 -3195
- package/src/components/MultiCanvasRenderer.tsx +0 -295
- package/src/components/NodeTooltip.tsx +0 -279
- package/src/components/PendingChanges.test.tsx +0 -375
- package/src/components/SelectionSidebar.tsx +0 -352
- package/src/components/SequenceDiagramRenderer.tsx +0 -1156
- package/src/components/WorkflowSequenceDiagram.tsx +0 -270
- package/src/components/dashboard/DashboardRenderer.tsx +0 -317
- package/src/components/dashboard/MetricPanel.tsx +0 -254
- package/src/components/dashboard/MockDataProvider.ts +0 -330
- package/src/components/dashboard/components/BarChart.tsx +0 -299
- package/src/components/dashboard/components/LineChart.tsx +0 -279
- package/src/components/dashboard/components/MetricCard.tsx +0 -270
- package/src/components/dashboard/components/SourceLink.tsx +0 -63
- package/src/components/dashboard/components/TimeRangeSelector.tsx +0 -280
- package/src/components/dashboard/components/index.ts +0 -5
- package/src/components/dashboard/index.ts +0 -47
- package/src/components/dashboard/types.ts +0 -126
- package/src/components/state-view/PipelineView.tsx +0 -347
- package/src/components/state-view/index.ts +0 -14
- package/src/components/state-view/types.ts +0 -261
- package/src/components/state-view/useStateView.ts +0 -205
- package/src/contexts/GraphEditContext.tsx +0 -27
- package/src/contexts/TooltipPortalContext.ts +0 -8
- package/src/edges/CustomEdge.tsx +0 -402
- package/src/edges/GenericEdge.tsx +0 -36
- package/src/hooks/usePathBasedEvents.ts +0 -192
- package/src/hooks/useSequenceLayout.ts +0 -543
- package/src/hooks/useUndoRedo.ts +0 -144
- package/src/nodes/CustomNode.tsx +0 -1231
- package/src/nodes/GenericNode.tsx +0 -55
- package/src/nodes/otel/OtelBoundaryNode.tsx +0 -224
- package/src/nodes/otel/OtelEventNode.tsx +0 -225
- package/src/nodes/otel/OtelResourceNode.tsx +0 -251
- package/src/nodes/otel/OtelScopeNode.tsx +0 -221
- package/src/nodes/otel/OtelSpanConventionNode.tsx +0 -295
- package/src/nodes/otel/index.ts +0 -23
- package/src/nodes/otel/shared/NodeBadges.tsx +0 -295
- package/src/nodes/otel/shared/NodeContent.tsx +0 -204
- package/src/nodes/otel/shared/index.ts +0 -8
- package/src/nodes/otel/shared/types.ts +0 -138
- package/src/nodes/otel/shared/useNodeBehavior.ts +0 -114
- package/src/stories/Audit/BoundaryNodes.stories.tsx +0 -385
- package/src/stories/Audit/CanvasEdgeTypes.stories.tsx +0 -1448
- package/src/stories/Audit/CanvasNodeTypes.stories.tsx +0 -1049
- package/src/stories/Audit/ColorPriority.stories.tsx +0 -895
- package/src/stories/Audit/DiamondBadges.stories.tsx +0 -629
- package/src/stories/Audit/NodeDimensionsTesting.stories.tsx +0 -724
- package/src/stories/Audit/NodeFieldsAudit.stories.tsx +0 -996
- package/src/stories/Audit/NodeFontSizeTesting.stories.tsx +0 -715
- package/src/stories/Audit/NodeShapes.stories.tsx +0 -1208
- package/src/stories/Audit/WideNodesTesting.stories.tsx +0 -1045
- package/src/stories/Components/DashboardRenderer.stories.tsx +0 -263
- package/src/stories/Components/GraphRenderer.stories.tsx +0 -3236
- package/src/stories/Components/MultiCanvasRenderer.stories.tsx +0 -370
- package/src/stories/Components/SequenceDiagram.stories.tsx +0 -255
- package/src/stories/Components/StateView.stories.tsx +0 -417
- package/src/stories/Components/TraceViewer.stories.tsx +0 -442
- package/src/stories/Components/WorkflowSequenceDiagram.stories.tsx +0 -802
- package/src/stories/Features/AnimationWorkshop.stories.tsx +0 -727
- package/src/stories/Features/ChangeTypeVisual.stories.tsx +0 -67
- package/src/stories/Features/EventDrivenAnimations.stories.tsx +0 -513
- package/src/stories/Features/MultiConfig.stories.tsx +0 -539
- package/src/stories/Layout/ElkEdgeRouting.stories.tsx +0 -539
- package/src/stories/Layout/GraphOrientation.stories.tsx +0 -500
- package/src/stories/Layout/MultiDirectionalConnections.stories.tsx +0 -352
- package/src/stories/Nodes/NodeDefinitionComparison.stories.tsx +0 -560
- package/src/stories/Nodes/NodeNameFallbacks.stories.tsx +0 -491
- package/src/stories/Nodes/NumericColors.stories.tsx +0 -440
- package/src/stories/OTEL/EventNameDisplay.stories.tsx +0 -694
- package/src/stories/OTEL/EventNameFallback.stories.tsx +0 -578
- package/src/stories/OTEL/OtelComponents.stories.tsx +0 -422
- package/src/stories/OTEL/OtelNodeTypesPrototype.stories.tsx +0 -806
- package/src/stories/OTEL/RawEventCards.stories.tsx +0 -57
- package/src/stories/OTEL/SpanBadges.stories.tsx +0 -840
- package/src/stories/OTEL/ValidationEventsCanvas.stories.tsx +0 -209
- package/src/stories/RealWorld/BacklogMdCanvases.stories.tsx +0 -721
- package/src/stories/RealWorld/FileCitySequence.stories.tsx +0 -516
- package/src/stories/RealWorld/RealCanvasFiles.stories.tsx +0 -336
- package/src/stories/__traces__/test-run.canvas.json +0 -241
- package/src/stories/data/activity-feed-analytics.dashboard.json +0 -300
- package/src/stories/data/file-city-images-transformed.otel.canvas.json +0 -295
- package/src/stories/data/file-city-workflows.json +0 -269
- package/src/stories/data/graph-converter-test-execution.json +0 -443
- package/src/stories/data/graph-converter-test.workflow.json +0 -105
- package/src/stories/data/graph-converter-validated-execution.json +0 -58
- package/src/utils/animationMapping.ts +0 -166
- package/src/utils/canvasBounds.ts +0 -146
- package/src/utils/graphConverter.ts +0 -148
- package/src/utils/iconResolver.tsx +0 -53
- package/src/utils/orientationUtils.ts +0 -73
|
@@ -15,7 +15,7 @@ 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 { ChevronDown, ChevronUp, X } from 'lucide-react';
|
|
18
|
+
import { ChevronDown, ChevronUp, Pause, Play, X } from 'lucide-react';
|
|
19
19
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
20
20
|
import { buildSubsystemGraph, MECHANISM_COLOR, MECHANISM_DESCRIPTIONS, subsystemGraphLayoutKey, } from './model';
|
|
21
21
|
import { SubsystemComponentNode, SubsystemGroupNode, SubsystemEdge, SUBSYSTEM_CALLBACKS, hexWithAlpha, EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility } from './nodes';
|
|
@@ -29,6 +29,8 @@ const EDGE_LABEL_MAX_EDGE_FRACTION = 0.55;
|
|
|
29
29
|
/** Rough monospace width at fontSize 10 + horizontal padding/border. */
|
|
30
30
|
const EDGE_LABEL_CHAR_PX = 6.2;
|
|
31
31
|
const EDGE_LABEL_PAD_PX = 18;
|
|
32
|
+
/** Pause (ms) between steps when a throughline autoplays. */
|
|
33
|
+
const THROUGHLINE_PLAY_PAUSE_MS = 2500;
|
|
32
34
|
const nodeTypes = {
|
|
33
35
|
'subsystem-component': SubsystemComponentNode,
|
|
34
36
|
'subsystem-group': SubsystemGroupNode,
|
|
@@ -46,7 +48,7 @@ const FileDrawerContent = memo(function FileDrawerContent({ render, file, startL
|
|
|
46
48
|
const ThroughlineDrawerContent = memo(function ThroughlineDrawerContent({ render, throughline, stepIndex, }) {
|
|
47
49
|
return _jsx(_Fragment, { children: render({ throughline, stepIndex }) });
|
|
48
50
|
});
|
|
49
|
-
function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, hideSidebar, graphTitle, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, renderThroughlineViewer, onFileSelect, onVerifyComponent, componentVerification }) {
|
|
51
|
+
function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, hideSidebar, throughlineStepMode = 'focus', autoPlayThroughlines = false, throughlineAutoPlayIntervalMs = THROUGHLINE_PLAY_PAUSE_MS, zoomOnThroughlineFocus = true, graphTitle, showThroughlineTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, renderThroughlineViewer, onFileSelect, onVerifyComponent, componentVerification }) {
|
|
50
52
|
const { theme } = useTheme();
|
|
51
53
|
const { fitView } = useReactFlow();
|
|
52
54
|
const viewport = useViewport();
|
|
@@ -65,6 +67,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
65
67
|
const [focusedThroughlineId, setFocusedThroughlineId] = useState(null);
|
|
66
68
|
// `null` = whole flow focused; a number = that single step's edge focused.
|
|
67
69
|
const [focusedStepIndex, setFocusedStepIndex] = useState(null);
|
|
70
|
+
// Hovered step in the flows panel: dims every canvas node/edge not involved
|
|
71
|
+
// with that step (transient — no camera move, no drawer, no focus change).
|
|
72
|
+
const [hoveredThroughlineStep, setHoveredThroughlineStep] = useState(null);
|
|
68
73
|
// Sidebar bottom half: which panel is shown when throughlines exist.
|
|
69
74
|
const [sidebarView, setSidebarView] = useState(() => throughlines?.length ? 'flows' : 'files');
|
|
70
75
|
// Throughline flows the user has expanded (via the title row). Closed by
|
|
@@ -92,6 +97,50 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
92
97
|
return null;
|
|
93
98
|
return throughlines.find((t) => t.id === drawerTarget.throughlineId) ?? null;
|
|
94
99
|
}, [drawerTarget, throughlines]);
|
|
100
|
+
// Throughline shown on the canvas title chip (focus or hover/autoplay highlight).
|
|
101
|
+
const overlayThroughlineTitle = useMemo(() => {
|
|
102
|
+
if (!showThroughlineTitle || !throughlines?.length)
|
|
103
|
+
return null;
|
|
104
|
+
const id = focusedThroughlineId ?? hoveredThroughlineStep?.throughlineId;
|
|
105
|
+
if (!id)
|
|
106
|
+
return null;
|
|
107
|
+
return throughlines.find((t) => t.id === id)?.title ?? null;
|
|
108
|
+
}, [
|
|
109
|
+
showThroughlineTitle,
|
|
110
|
+
throughlines,
|
|
111
|
+
focusedThroughlineId,
|
|
112
|
+
hoveredThroughlineStep,
|
|
113
|
+
]);
|
|
114
|
+
// Active step for the bottom-of-title progress + annotation chip.
|
|
115
|
+
const overlayThroughlineStep = useMemo(() => {
|
|
116
|
+
if (!showThroughlineTitle || !throughlines?.length)
|
|
117
|
+
return null;
|
|
118
|
+
const tlId = focusedThroughlineId ?? hoveredThroughlineStep?.throughlineId ?? null;
|
|
119
|
+
let stepIndex = null;
|
|
120
|
+
if (focusedThroughlineId != null) {
|
|
121
|
+
stepIndex = focusedStepIndex;
|
|
122
|
+
}
|
|
123
|
+
else if (hoveredThroughlineStep != null) {
|
|
124
|
+
stepIndex = hoveredThroughlineStep.stepIndex;
|
|
125
|
+
}
|
|
126
|
+
if (tlId == null || stepIndex == null)
|
|
127
|
+
return null;
|
|
128
|
+
const tl = throughlines.find((t) => t.id === tlId);
|
|
129
|
+
const step = tl?.steps[stepIndex];
|
|
130
|
+
if (!step || !tl)
|
|
131
|
+
return null;
|
|
132
|
+
return {
|
|
133
|
+
index: stepIndex + 1,
|
|
134
|
+
total: tl.steps.length,
|
|
135
|
+
annotation: step.annotation,
|
|
136
|
+
};
|
|
137
|
+
}, [
|
|
138
|
+
showThroughlineTitle,
|
|
139
|
+
throughlines,
|
|
140
|
+
focusedThroughlineId,
|
|
141
|
+
focusedStepIndex,
|
|
142
|
+
hoveredThroughlineStep,
|
|
143
|
+
]);
|
|
95
144
|
const drawerTitle = useMemo(() => {
|
|
96
145
|
if (!drawerTarget)
|
|
97
146
|
return null;
|
|
@@ -253,12 +302,13 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
253
302
|
}
|
|
254
303
|
return new Set(tl.steps.map((s) => s.edgeId));
|
|
255
304
|
}, [throughlines, focusedThroughlineId, focusedStepIndex]);
|
|
256
|
-
// 1-based step numbers per edge of the
|
|
257
|
-
// in more than one step
|
|
305
|
+
// 1-based step numbers per edge of the active flow (focused or
|
|
306
|
+
// hover/autoplay-highlighted). An edge can appear in more than one step.
|
|
258
307
|
const selectedFlowStepNos = useMemo(() => {
|
|
259
|
-
|
|
308
|
+
const activeId = focusedThroughlineId ?? hoveredThroughlineStep?.throughlineId;
|
|
309
|
+
if (activeId == null || !throughlines)
|
|
260
310
|
return null;
|
|
261
|
-
const tl = throughlines.find((t) => t.id ===
|
|
311
|
+
const tl = throughlines.find((t) => t.id === activeId);
|
|
262
312
|
if (!tl)
|
|
263
313
|
return null;
|
|
264
314
|
const map = new Map();
|
|
@@ -268,7 +318,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
268
318
|
map.set(s.edgeId, list);
|
|
269
319
|
});
|
|
270
320
|
return map;
|
|
271
|
-
}, [throughlines, focusedThroughlineId]);
|
|
321
|
+
}, [throughlines, focusedThroughlineId, hoveredThroughlineStep]);
|
|
272
322
|
// Union of every expanded (opened) throughline's edges — the visible set.
|
|
273
323
|
const openedEdgeIds = useMemo(() => {
|
|
274
324
|
if (!throughlines || expandedThroughlines.size === 0)
|
|
@@ -294,6 +344,19 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
294
344
|
}
|
|
295
345
|
return ids.size > 0 ? ids : null;
|
|
296
346
|
};
|
|
347
|
+
// Edge/nodes involved in the hovered step — dim everything else.
|
|
348
|
+
const hoverEdgeIds = useMemo(() => {
|
|
349
|
+
if (!hoveredThroughlineStep || !throughlines)
|
|
350
|
+
return null;
|
|
351
|
+
const tl = throughlines.find((t) => t.id === hoveredThroughlineStep.throughlineId);
|
|
352
|
+
if (!tl)
|
|
353
|
+
return null;
|
|
354
|
+
const step = tl.steps[hoveredThroughlineStep.stepIndex];
|
|
355
|
+
return step ? new Set([step.edgeId]) : null;
|
|
356
|
+
}, [throughlines, hoveredThroughlineStep]);
|
|
357
|
+
const hoverNodeIds = useMemo(() => endpointsOf(hoverEdgeIds),
|
|
358
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
359
|
+
[baseEdges, hoverEdgeIds]);
|
|
297
360
|
const openedNodeIds = useMemo(() => endpointsOf(openedEdgeIds),
|
|
298
361
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
299
362
|
[baseEdges, openedEdgeIds]);
|
|
@@ -323,6 +386,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
323
386
|
// `isSelected` rides in data because the node's stopPropagation() keeps
|
|
324
387
|
// React Flow's own selection state from updating.
|
|
325
388
|
const dispNodes = useMemo(() => {
|
|
389
|
+
// While a step is hovered, only the hovered step's endpoints stay bright;
|
|
390
|
+
// everything else is dimmed (never hidden) so the step's context survives.
|
|
326
391
|
return xyflowNodesBase.map((n) => {
|
|
327
392
|
// Boundary frames follow their members: hidden when no member is
|
|
328
393
|
// visible, dimmed when members are dimmed. Never selectable.
|
|
@@ -334,13 +399,17 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
334
399
|
anyOpened: openedNodeIds != null,
|
|
335
400
|
anySelected: brightNodeIds != null,
|
|
336
401
|
});
|
|
402
|
+
const dimmed = hoverNodeIds
|
|
403
|
+
? !memberIds.some((id) => hoverNodeIds.has(id))
|
|
404
|
+
: vis.dimmed;
|
|
405
|
+
const hidden = hoverNodeIds ? false : vis.hidden;
|
|
337
406
|
return {
|
|
338
407
|
...n,
|
|
339
|
-
hidden
|
|
408
|
+
hidden,
|
|
340
409
|
selectable: false,
|
|
341
410
|
data: {
|
|
342
411
|
...n.data,
|
|
343
|
-
...(
|
|
412
|
+
...(dimmed && { dimmed: true }),
|
|
344
413
|
},
|
|
345
414
|
};
|
|
346
415
|
}
|
|
@@ -353,22 +422,24 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
353
422
|
anyOpened: openedNodeIds != null,
|
|
354
423
|
anySelected: brightNodeIds != null,
|
|
355
424
|
});
|
|
356
|
-
|
|
425
|
+
const dimmed = hoverNodeIds ? !hoverNodeIds.has(n.id) : vis.dimmed;
|
|
426
|
+
const hidden = hoverNodeIds ? false : vis.hidden;
|
|
427
|
+
if (fileMatch === undefined && !isSelected && !dimmed) {
|
|
357
428
|
const { fileMatch: _f, isSelected: _s, dimmed: _d, ...rest } = n.data;
|
|
358
|
-
return { ...n, hidden
|
|
429
|
+
return { ...n, hidden, data: rest };
|
|
359
430
|
}
|
|
360
431
|
return {
|
|
361
432
|
...n,
|
|
362
|
-
hidden
|
|
433
|
+
hidden,
|
|
363
434
|
data: {
|
|
364
435
|
...n.data,
|
|
365
436
|
...(fileMatch !== undefined && { fileMatch }),
|
|
366
437
|
...(isSelected && { isSelected }),
|
|
367
|
-
...(
|
|
438
|
+
...(dimmed && { dimmed: true }),
|
|
368
439
|
},
|
|
369
440
|
};
|
|
370
441
|
});
|
|
371
|
-
}, [xyflowNodesBase, openFile, selected, focusNodeIds, openedNodeIds, brightNodeIds]);
|
|
442
|
+
}, [xyflowNodesBase, openFile, selected, focusNodeIds, openedNodeIds, brightNodeIds, hoverNodeIds]);
|
|
372
443
|
const baseNodesKey = useMemo(() => nodes.map((n) => n.id).sort().join(','), [nodes]);
|
|
373
444
|
const baseEdgesKey = useMemo(() => convertedEdges.map((e) => e.id).sort().join(','), [convertedEdges]);
|
|
374
445
|
const dispEdges = useMemo(() => {
|
|
@@ -383,7 +454,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
383
454
|
markerEnd: nextMarker,
|
|
384
455
|
};
|
|
385
456
|
};
|
|
386
|
-
if (openedEdgeIds || focusEdgeIds) {
|
|
457
|
+
if (openedEdgeIds || focusEdgeIds || hoverEdgeIds) {
|
|
387
458
|
return baseEdges.map((e) => {
|
|
388
459
|
const vis = flowElementVisibility({
|
|
389
460
|
inOpened: openedEdgeIds?.has(e.id) === true,
|
|
@@ -391,13 +462,15 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
391
462
|
anyOpened: openedEdgeIds != null,
|
|
392
463
|
anySelected: focusEdgeIds != null,
|
|
393
464
|
});
|
|
394
|
-
|
|
465
|
+
const dimmed = hoverEdgeIds ? !hoverEdgeIds.has(e.id) : vis.dimmed;
|
|
466
|
+
const hidden = hoverEdgeIds ? false : vis.hidden;
|
|
467
|
+
return { ...paint(e, dimmed), hidden };
|
|
395
468
|
});
|
|
396
469
|
}
|
|
397
470
|
if (!selectedEdgeId)
|
|
398
471
|
return baseEdges;
|
|
399
472
|
return baseEdges.map((e) => paint(e, e.id !== selectedEdgeId));
|
|
400
|
-
}, [baseEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds]);
|
|
473
|
+
}, [baseEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds, hoverEdgeIds]);
|
|
401
474
|
const onNodesChange = useCallback((changes) => {
|
|
402
475
|
// Capture dimension changes (React Flow's measurement callback).
|
|
403
476
|
// Group parents are ELK-sized — ignore their measurements.
|
|
@@ -492,6 +565,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
492
565
|
// live positions + measured dims — so the frame always includes BOTH
|
|
493
566
|
// components the edge attaches to (and therefore the edge line between them).
|
|
494
567
|
const fitFocusBounds = useCallback((ids) => {
|
|
568
|
+
if (!zoomOnThroughlineFocus)
|
|
569
|
+
return;
|
|
495
570
|
const nodeIds = new Set();
|
|
496
571
|
for (const e of baseEdges) {
|
|
497
572
|
if (!ids.has(e.id))
|
|
@@ -506,35 +581,55 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
506
581
|
padding: 0.25,
|
|
507
582
|
duration: 300,
|
|
508
583
|
});
|
|
509
|
-
}, [baseEdges, fitView]);
|
|
584
|
+
}, [baseEdges, fitView, zoomOnThroughlineFocus]);
|
|
510
585
|
// Focus an entire flow: hide everything but the flow's nodes and edges, and
|
|
511
586
|
// frame the flow on the canvas. Selection state is cleared — the graph now
|
|
512
587
|
// reads as the narrative. No drawer: the code view only opens on a step
|
|
513
588
|
// click. A stale throughline drawer (from a previously focused flow's step)
|
|
514
589
|
// closes; an explicitly opened file drawer stays.
|
|
590
|
+
// In `dim` mode: clear step highlight and leave the full graph visible
|
|
591
|
+
// (no zoom / hide) — matching "step away from a hovered step".
|
|
515
592
|
const focusThroughlineEdges = useCallback((tl) => {
|
|
516
593
|
setSelected(null);
|
|
517
594
|
setSelectedEdgeId(null);
|
|
595
|
+
setHoveredThroughlineStep(null);
|
|
596
|
+
if (throughlineStepMode === 'dim') {
|
|
597
|
+
setFocusedStepIndex(null);
|
|
598
|
+
setFocusedThroughlineId(null);
|
|
599
|
+
setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
518
602
|
setFocusedStepIndex(null);
|
|
519
603
|
setFocusedThroughlineId(tl.id);
|
|
520
604
|
fitFocusBounds(new Set(tl.steps.map((s) => s.edgeId)));
|
|
521
605
|
setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
|
|
522
|
-
}, [fitFocusBounds]);
|
|
606
|
+
}, [fitFocusBounds, throughlineStepMode]);
|
|
523
607
|
const clearThroughlineFocus = useCallback(() => {
|
|
524
608
|
setFocusedThroughlineId(null);
|
|
525
609
|
setFocusedStepIndex(null);
|
|
610
|
+
setHoveredThroughlineStep(null);
|
|
526
611
|
setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
|
|
527
612
|
}, []);
|
|
528
613
|
// Focus a single step's edge on the canvas and open/scroll the throughline
|
|
529
614
|
// drawer to that step's snippet.
|
|
615
|
+
// In `dim` mode: only dim non-participants (same as hovering a step) —
|
|
616
|
+
// no camera move, no drawer, no hide.
|
|
530
617
|
const focusThroughlineStep = useCallback((tl, stepIndex) => {
|
|
531
618
|
const step = tl.steps[stepIndex];
|
|
532
619
|
if (!step)
|
|
533
620
|
return;
|
|
534
621
|
setSelected(null);
|
|
535
622
|
setSelectedEdgeId(null);
|
|
623
|
+
if (throughlineStepMode === 'dim') {
|
|
624
|
+
setFocusedStepIndex(null);
|
|
625
|
+
setFocusedThroughlineId(null);
|
|
626
|
+
setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex });
|
|
627
|
+
setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
536
630
|
setFocusedStepIndex(stepIndex);
|
|
537
631
|
setFocusedThroughlineId(tl.id);
|
|
632
|
+
setHoveredThroughlineStep(null);
|
|
538
633
|
fitFocusBounds(new Set([step.edgeId]));
|
|
539
634
|
if (renderThroughlineViewer) {
|
|
540
635
|
setDrawerTarget({
|
|
@@ -550,7 +645,43 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
550
645
|
startLine: step.line,
|
|
551
646
|
});
|
|
552
647
|
}
|
|
553
|
-
}, [fitFocusBounds, renderThroughlineViewer]);
|
|
648
|
+
}, [fitFocusBounds, renderThroughlineViewer, throughlineStepMode]);
|
|
649
|
+
// Graph-only embeds: cycle throughline steps with hover-style dimming.
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
if (!autoPlayThroughlines || !throughlines?.length || !layoutReady)
|
|
652
|
+
return;
|
|
653
|
+
const playable = throughlines.filter((tl) => tl.steps.length > 0);
|
|
654
|
+
if (playable.length === 0)
|
|
655
|
+
return;
|
|
656
|
+
let cancelled = false;
|
|
657
|
+
let tlIdx = 0;
|
|
658
|
+
let stepIdx = 0;
|
|
659
|
+
let timer = null;
|
|
660
|
+
const interval = Math.max(400, throughlineAutoPlayIntervalMs);
|
|
661
|
+
const tick = () => {
|
|
662
|
+
if (cancelled)
|
|
663
|
+
return;
|
|
664
|
+
const tl = playable[tlIdx];
|
|
665
|
+
setSelected(null);
|
|
666
|
+
setSelectedEdgeId(null);
|
|
667
|
+
setFocusedThroughlineId(null);
|
|
668
|
+
setFocusedStepIndex(null);
|
|
669
|
+
setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex: stepIdx });
|
|
670
|
+
stepIdx += 1;
|
|
671
|
+
if (stepIdx >= tl.steps.length) {
|
|
672
|
+
stepIdx = 0;
|
|
673
|
+
tlIdx = (tlIdx + 1) % playable.length;
|
|
674
|
+
}
|
|
675
|
+
timer = window.setTimeout(tick, interval);
|
|
676
|
+
};
|
|
677
|
+
tick();
|
|
678
|
+
return () => {
|
|
679
|
+
cancelled = true;
|
|
680
|
+
if (timer != null)
|
|
681
|
+
window.clearTimeout(timer);
|
|
682
|
+
setHoveredThroughlineStep(null);
|
|
683
|
+
};
|
|
684
|
+
}, [autoPlayThroughlines, throughlines, throughlineAutoPlayIntervalMs, layoutReady]);
|
|
554
685
|
// Arrow keys step through the focused throughline once a step is active
|
|
555
686
|
// (sidebar click or drawer open). Ignores typing targets and chords.
|
|
556
687
|
useEffect(() => {
|
|
@@ -749,8 +880,12 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
749
880
|
flex: 1,
|
|
750
881
|
minHeight: 0,
|
|
751
882
|
overflowY: 'auto',
|
|
752
|
-
padding: '0
|
|
753
|
-
}, children: throughlines.map((tl) => (_jsx(ThroughlineFlow, { throughline: tl, edges: edges, collapsed: !expandedThroughlines.has(tl.id), active: focusedThroughlineId === tl.id
|
|
883
|
+
padding: '0 0 12px',
|
|
884
|
+
}, children: throughlines.map((tl) => (_jsx(ThroughlineFlow, { throughline: tl, edges: edges, collapsed: !expandedThroughlines.has(tl.id), active: focusedThroughlineId === tl.id
|
|
885
|
+
? { stepIndex: focusedStepIndex }
|
|
886
|
+
: hoveredThroughlineStep?.throughlineId === tl.id
|
|
887
|
+
? { stepIndex: hoveredThroughlineStep.stepIndex }
|
|
888
|
+
: null, onToggleCollapsed: toggleThroughlineCollapsed, onFocusFlow: focusThroughlineEdges, onClearFocus: clearThroughlineFocus, onFocusStep: focusThroughlineStep, onHoverStep: (tl, i) => setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex: i }), onLeaveStep: () => setHoveredThroughlineStep(null) }, tl.id))) })) : treeFilePaths.length > 0 ? (_jsx(_Fragment, { children: repoGroups.groups.map((group, i) => {
|
|
754
889
|
const groupKey = group.repoKey ?? '__no-repo__';
|
|
755
890
|
const collapsed = collapsedRepos.has(groupKey);
|
|
756
891
|
return (_jsxs("div", { style: {
|
|
@@ -816,27 +951,88 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
816
951
|
flex: 1,
|
|
817
952
|
minHeight: 0,
|
|
818
953
|
background: theme.colors.background,
|
|
819
|
-
}, children: [_jsx(Background, { variant: BackgroundVariant.Dots, gap: 16, size: 1 }), _jsx(Controls, { showZoom: true, showFitView: true, showInteractive: true })] }, `${baseNodesKey}-${baseEdgesKey}`), graphTitle && (
|
|
954
|
+
}, children: [_jsx(Background, { variant: BackgroundVariant.Dots, gap: 16, size: 1 }), _jsx(Controls, { showZoom: true, showFitView: true, showInteractive: true })] }, `${baseNodesKey}-${baseEdgesKey}`), (graphTitle || overlayThroughlineTitle || overlayThroughlineStep) && (_jsxs("div", { style: {
|
|
820
955
|
position: 'absolute',
|
|
821
956
|
top: 10,
|
|
822
957
|
left: '50%',
|
|
823
958
|
transform: 'translateX(-50%)',
|
|
824
959
|
zIndex: 6,
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
fontSize: theme.fontSizes[3],
|
|
831
|
-
fontWeight: 600,
|
|
832
|
-
fontFamily: theme.fonts.monospace,
|
|
833
|
-
color: theme.colors.text,
|
|
834
|
-
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
835
|
-
border: `1px solid ${theme.colors.border}`,
|
|
836
|
-
borderRadius: 6,
|
|
837
|
-
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
960
|
+
display: 'flex',
|
|
961
|
+
flexDirection: 'column',
|
|
962
|
+
alignItems: 'center',
|
|
963
|
+
gap: 6,
|
|
964
|
+
maxWidth: '70%',
|
|
838
965
|
pointerEvents: 'none',
|
|
839
|
-
}, children: graphTitle
|
|
966
|
+
}, children: [graphTitle && (_jsx("div", { style: {
|
|
967
|
+
maxWidth: '100%',
|
|
968
|
+
overflow: 'hidden',
|
|
969
|
+
textOverflow: 'ellipsis',
|
|
970
|
+
whiteSpace: 'nowrap',
|
|
971
|
+
padding: '6px 18px',
|
|
972
|
+
fontSize: theme.fontSizes[3],
|
|
973
|
+
fontWeight: 600,
|
|
974
|
+
fontFamily: theme.fonts.monospace,
|
|
975
|
+
color: theme.colors.text,
|
|
976
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
977
|
+
border: `1px solid ${theme.colors.border}`,
|
|
978
|
+
borderRadius: 6,
|
|
979
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
980
|
+
}, children: graphTitle })), overlayThroughlineTitle && (_jsxs("div", { style: {
|
|
981
|
+
maxWidth: '100%',
|
|
982
|
+
minWidth: overlayThroughlineStep ? 160 : undefined,
|
|
983
|
+
display: 'flex',
|
|
984
|
+
flexDirection: 'column',
|
|
985
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
986
|
+
border: `1px solid ${theme.colors.border}`,
|
|
987
|
+
borderRadius: 6,
|
|
988
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
989
|
+
overflow: 'hidden',
|
|
990
|
+
opacity: 0.95,
|
|
991
|
+
}, "aria-label": overlayThroughlineStep
|
|
992
|
+
? `${overlayThroughlineTitle}, step ${overlayThroughlineStep.index} of ${overlayThroughlineStep.total}`
|
|
993
|
+
: overlayThroughlineTitle, children: [_jsx("div", { style: {
|
|
994
|
+
padding: '5px 14px',
|
|
995
|
+
fontSize: theme.fontSizes[2] ?? theme.fontSizes[1],
|
|
996
|
+
fontWeight: 600,
|
|
997
|
+
fontFamily: theme.fonts.monospace,
|
|
998
|
+
color: theme.colors.text,
|
|
999
|
+
overflow: 'hidden',
|
|
1000
|
+
textOverflow: 'ellipsis',
|
|
1001
|
+
whiteSpace: 'nowrap',
|
|
1002
|
+
textAlign: 'center',
|
|
1003
|
+
}, children: overlayThroughlineTitle }), overlayThroughlineStep && overlayThroughlineStep.total > 0 && (_jsx("div", { style: {
|
|
1004
|
+
display: 'flex',
|
|
1005
|
+
gap: 3,
|
|
1006
|
+
padding: '0 6px 5px',
|
|
1007
|
+
}, "aria-hidden": "true", children: Array.from({ length: overlayThroughlineStep.total }, (_, i) => {
|
|
1008
|
+
const n = i + 1;
|
|
1009
|
+
const active = n === overlayThroughlineStep.index;
|
|
1010
|
+
const done = n < overlayThroughlineStep.index;
|
|
1011
|
+
return (_jsx("span", { style: {
|
|
1012
|
+
flex: 1,
|
|
1013
|
+
height: 2,
|
|
1014
|
+
borderRadius: 1,
|
|
1015
|
+
background: active
|
|
1016
|
+
? (theme.colors.accent ?? theme.colors.primary ?? theme.colors.text)
|
|
1017
|
+
: done
|
|
1018
|
+
? (theme.colors.textSecondary ?? theme.colors.text)
|
|
1019
|
+
: (theme.colors.border ?? 'rgba(127,127,127,0.45)'),
|
|
1020
|
+
opacity: active ? 1 : done ? 0.75 : 0.4,
|
|
1021
|
+
transition: 'opacity 120ms ease, background 120ms ease',
|
|
1022
|
+
} }, n));
|
|
1023
|
+
}) }))] })), overlayThroughlineStep?.annotation && (_jsx("div", { style: {
|
|
1024
|
+
maxWidth: '100%',
|
|
1025
|
+
padding: '6px 14px',
|
|
1026
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
1027
|
+
border: `1px solid ${theme.colors.border}`,
|
|
1028
|
+
borderRadius: 6,
|
|
1029
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
1030
|
+
textAlign: 'center',
|
|
1031
|
+
fontSize: theme.fontSizes[0],
|
|
1032
|
+
fontFamily: theme.fonts.body,
|
|
1033
|
+
color: theme.colors.textMuted ?? theme.colors.textSecondary,
|
|
1034
|
+
lineHeight: 1.35,
|
|
1035
|
+
}, children: overlayThroughlineStep.annotation }))] })), selected && (_jsx("div", { style: {
|
|
840
1036
|
position: 'absolute',
|
|
841
1037
|
top: 10,
|
|
842
1038
|
right: 10,
|
|
@@ -896,7 +1092,7 @@ function RepoGroupHeader({ group, collapsed, onToggle, }) {
|
|
|
896
1092
|
* title: closed → open + select; open and unselected → select; open and
|
|
897
1093
|
* selected → close + clear focus. The right-aligned close button collapses
|
|
898
1094
|
* without selecting. A step row focuses that step's edge. */
|
|
899
|
-
function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollapsed, onFocusFlow, onClearFocus, onFocusStep, }) {
|
|
1095
|
+
function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollapsed, onFocusFlow, onClearFocus, onFocusStep, onHoverStep, onLeaveStep, }) {
|
|
900
1096
|
const { theme } = useTheme();
|
|
901
1097
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
902
1098
|
const hoverBg = theme.colors.background;
|
|
@@ -904,8 +1100,52 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
904
1100
|
const wholeFlowActive = active !== null && active.stepIndex === null;
|
|
905
1101
|
const [headerHover, setHeaderHover] = useState(false);
|
|
906
1102
|
const [closeHover, setCloseHover] = useState(false);
|
|
1103
|
+
const [playHover, setPlayHover] = useState(false);
|
|
907
1104
|
const [hoveredStep, setHoveredStep] = useState(null);
|
|
908
1105
|
const stepButtonRefs = useRef([]);
|
|
1106
|
+
// Autoplay: stepping through the flow's steps with a pause between each.
|
|
1107
|
+
const [playing, setPlaying] = useState(false);
|
|
1108
|
+
const playTimerRef = useRef(null);
|
|
1109
|
+
const stopPlaying = useCallback(() => {
|
|
1110
|
+
if (playTimerRef.current != null) {
|
|
1111
|
+
window.clearTimeout(playTimerRef.current);
|
|
1112
|
+
playTimerRef.current = null;
|
|
1113
|
+
}
|
|
1114
|
+
setPlaying(false);
|
|
1115
|
+
}, []);
|
|
1116
|
+
// Clear any pending timer on unmount.
|
|
1117
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1118
|
+
useEffect(() => () => stopPlaying(), []);
|
|
1119
|
+
const startPlaying = useCallback(() => {
|
|
1120
|
+
if (collapsed)
|
|
1121
|
+
onToggleCollapsed(throughline.id);
|
|
1122
|
+
if (active === null || active.stepIndex !== null)
|
|
1123
|
+
onFocusFlow(throughline);
|
|
1124
|
+
const stepCount = throughline.steps.length;
|
|
1125
|
+
if (stepCount === 0)
|
|
1126
|
+
return;
|
|
1127
|
+
setPlaying(true);
|
|
1128
|
+
let i = 0;
|
|
1129
|
+
const tick = () => {
|
|
1130
|
+
if (i >= stepCount) {
|
|
1131
|
+
playTimerRef.current = null;
|
|
1132
|
+
setPlaying(false);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
onFocusStep(throughline, i);
|
|
1136
|
+
i += 1;
|
|
1137
|
+
playTimerRef.current = window.setTimeout(tick, THROUGHLINE_PLAY_PAUSE_MS);
|
|
1138
|
+
};
|
|
1139
|
+
tick();
|
|
1140
|
+
}, [collapsed, active, onToggleCollapsed, onFocusFlow, throughline, onFocusStep]);
|
|
1141
|
+
const togglePlay = useCallback(() => {
|
|
1142
|
+
if (playing) {
|
|
1143
|
+
stopPlaying();
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
startPlaying();
|
|
1147
|
+
}
|
|
1148
|
+
}, [playing, stopPlaying, startPlaying]);
|
|
909
1149
|
// Keep DOM focus on the active step so the browser focus ring (and
|
|
910
1150
|
// subsequent arrow keys) follow arrow navigation, not the originally
|
|
911
1151
|
// clicked button.
|
|
@@ -914,11 +1154,10 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
914
1154
|
return;
|
|
915
1155
|
stepButtonRefs.current[active.stepIndex]?.focus({ preventScroll: true });
|
|
916
1156
|
}, [active?.stepIndex]);
|
|
917
|
-
return (_jsxs("div", {
|
|
1157
|
+
return (_jsxs("div", { children: [_jsxs("div", { onMouseEnter: () => setHeaderHover(true), onMouseLeave: () => setHeaderHover(false), style: {
|
|
918
1158
|
display: 'flex',
|
|
919
1159
|
alignItems: 'center',
|
|
920
1160
|
gap: 4,
|
|
921
|
-
borderRadius: 6,
|
|
922
1161
|
background: wholeFlowActive || headerHover ? hoverBg : 'transparent',
|
|
923
1162
|
transition: 'background 120ms ease',
|
|
924
1163
|
}, children: [_jsx("button", { type: "button", onClick: () => {
|
|
@@ -938,8 +1177,7 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
938
1177
|
display: 'flex',
|
|
939
1178
|
alignItems: 'center',
|
|
940
1179
|
minWidth: 0,
|
|
941
|
-
padding: '
|
|
942
|
-
borderRadius: 6,
|
|
1180
|
+
padding: '10px 8px',
|
|
943
1181
|
border: 'none',
|
|
944
1182
|
background: 'transparent',
|
|
945
1183
|
textAlign: 'left',
|
|
@@ -952,39 +1190,62 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
952
1190
|
fontFamily: theme.fonts.monospace,
|
|
953
1191
|
fontWeight: 600,
|
|
954
1192
|
color: theme.colors.text,
|
|
955
|
-
}, children: throughline.title }) }), !collapsed && (_jsx("button", { type: "button", "aria-label": `
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1193
|
+
}, children: throughline.title }) }), !collapsed && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", "aria-label": playing ? `Pause ${throughline.title} autoplay` : `Play ${throughline.title}`, title: playing ? 'Pause' : 'Play through steps', onMouseEnter: () => setPlayHover(true), onMouseLeave: () => setPlayHover(false), onClick: (e) => {
|
|
1194
|
+
e.stopPropagation();
|
|
1195
|
+
togglePlay();
|
|
1196
|
+
}, style: {
|
|
1197
|
+
display: 'inline-flex',
|
|
1198
|
+
alignItems: 'center',
|
|
1199
|
+
justifyContent: 'center',
|
|
1200
|
+
flexShrink: 0,
|
|
1201
|
+
width: 22,
|
|
1202
|
+
height: 22,
|
|
1203
|
+
padding: 0,
|
|
1204
|
+
border: 'none',
|
|
1205
|
+
borderRadius: 4,
|
|
1206
|
+
background: playing || playHover ? theme.colors.border : 'transparent',
|
|
1207
|
+
color: playing || playHover ? theme.colors.text : muted,
|
|
1208
|
+
cursor: 'pointer',
|
|
1209
|
+
transition: 'background 120ms ease, color 120ms ease',
|
|
1210
|
+
}, children: playing ? _jsx(Pause, { size: 12, strokeWidth: 2 }) : _jsx(Play, { size: 12, strokeWidth: 2 }) }), _jsx("button", { type: "button", "aria-label": `Close ${throughline.title}`, onMouseEnter: () => setCloseHover(true), onMouseLeave: () => setCloseHover(false), onClick: (e) => {
|
|
1211
|
+
e.stopPropagation();
|
|
1212
|
+
onToggleCollapsed(throughline.id);
|
|
1213
|
+
if (active !== null)
|
|
1214
|
+
onClearFocus();
|
|
1215
|
+
}, style: {
|
|
1216
|
+
display: 'inline-flex',
|
|
1217
|
+
alignItems: 'center',
|
|
1218
|
+
justifyContent: 'center',
|
|
1219
|
+
flexShrink: 0,
|
|
1220
|
+
width: 22,
|
|
1221
|
+
height: 22,
|
|
1222
|
+
marginRight: 4,
|
|
1223
|
+
padding: 0,
|
|
1224
|
+
border: 'none',
|
|
1225
|
+
borderRadius: 4,
|
|
1226
|
+
background: closeHover ? theme.colors.border : 'transparent',
|
|
1227
|
+
color: closeHover ? theme.colors.text : muted,
|
|
1228
|
+
cursor: 'pointer',
|
|
1229
|
+
transition: 'background 120ms ease, color 120ms ease',
|
|
1230
|
+
}, children: _jsx(X, { size: 12, strokeWidth: 2 }) })] }))] }), !collapsed && (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 1 }, children: throughline.steps.map((step, i) => {
|
|
976
1231
|
const edge = edgeById.get(step.edgeId);
|
|
977
1232
|
const mech = edge?.mechanism;
|
|
978
1233
|
const color = mech ? MECHANISM_COLOR[mech] : muted;
|
|
979
1234
|
const stepActive = active !== null && active.stepIndex === i;
|
|
980
1235
|
return (_jsxs("button", { ref: (el) => {
|
|
981
1236
|
stepButtonRefs.current[i] = el;
|
|
982
|
-
}, type: "button", onMouseEnter: () =>
|
|
1237
|
+
}, type: "button", onMouseEnter: () => {
|
|
1238
|
+
setHoveredStep(i);
|
|
1239
|
+
onHoverStep(throughline, i);
|
|
1240
|
+
}, onMouseLeave: () => {
|
|
1241
|
+
setHoveredStep(null);
|
|
1242
|
+
onLeaveStep();
|
|
1243
|
+
}, onClick: () => onFocusStep(throughline, i), style: {
|
|
983
1244
|
display: 'flex',
|
|
984
1245
|
alignItems: 'center',
|
|
985
1246
|
gap: 8,
|
|
986
1247
|
minWidth: 0,
|
|
987
|
-
padding: '
|
|
1248
|
+
padding: '8px 8px 8px 12px',
|
|
988
1249
|
textAlign: 'left',
|
|
989
1250
|
borderRadius: 6,
|
|
990
1251
|
border: 'none',
|
|
@@ -1004,9 +1265,9 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1004
1265
|
overflow: 'hidden',
|
|
1005
1266
|
textOverflow: 'ellipsis',
|
|
1006
1267
|
whiteSpace: 'nowrap',
|
|
1007
|
-
fontSize: theme.fontSizes[
|
|
1268
|
+
fontSize: theme.fontSizes[1],
|
|
1008
1269
|
fontFamily: theme.fonts.monospace,
|
|
1009
|
-
color:
|
|
1270
|
+
color: theme.colors.text,
|
|
1010
1271
|
}, children: step.symbol })) : (_jsxs(_Fragment, { children: [_jsx("span", { style: {
|
|
1011
1272
|
flexShrink: 0,
|
|
1012
1273
|
width: 74,
|
|
@@ -1024,7 +1285,7 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1024
1285
|
whiteSpace: 'nowrap',
|
|
1025
1286
|
fontSize: theme.fontSizes[0],
|
|
1026
1287
|
fontFamily: theme.fonts.monospace,
|
|
1027
|
-
color:
|
|
1288
|
+
color: theme.colors.text,
|
|
1028
1289
|
}, children: [step.file.split('/').pop(), _jsxs("span", { style: { opacity: 0.7 }, children: [":", step.line] })] })] }))] }, `${step.edgeId}-${i}`));
|
|
1029
1290
|
}) }))] }));
|
|
1030
1291
|
}
|