@principal-ai/subsystems-react 0.18.0 → 0.19.1
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/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 +376 -72
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/package.json +3 -3
- package/src/subsystem/SubsystemComponentGraph.tsx +460 -53
|
@@ -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
|
|
@@ -76,6 +81,42 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
76
81
|
const [descToggleHover, setDescToggleHover] = useState(false);
|
|
77
82
|
// `true` only when a description exists AND the user opened it.
|
|
78
83
|
const showDesc = !!description && descriptionVisible;
|
|
84
|
+
// Sidebar width (px). Draggable via the resize handle between the sidebar
|
|
85
|
+
// and the graph canvas; clamps to sensible bounds while dragging.
|
|
86
|
+
const [sidebarWidth, setSidebarWidth] = useState(340);
|
|
87
|
+
const [sidebarDrag, setSidebarDrag] = useState(false);
|
|
88
|
+
const sidebarDragStartX = useRef(0);
|
|
89
|
+
const sidebarDragStartWidth = useRef(340);
|
|
90
|
+
const sidebarMinWidth = 240;
|
|
91
|
+
const sidebarMaxWidth = useMemo(() => Math.max(Math.min((_measured?.w ?? 680) * 0.5, 600), sidebarMinWidth), [_measured?.w]);
|
|
92
|
+
// Resize drag: capture the drag state so document-level move/up listeners
|
|
93
|
+
// stay attached for the duration of the gesture, then release on mouseup.
|
|
94
|
+
const onSidebarResizeStart = useCallback((e) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
sidebarDragStartX.current = e.clientX;
|
|
97
|
+
sidebarDragStartWidth.current = sidebarWidth;
|
|
98
|
+
setSidebarDrag(true);
|
|
99
|
+
}, [sidebarWidth]);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!sidebarDrag)
|
|
102
|
+
return;
|
|
103
|
+
const onMove = (e) => {
|
|
104
|
+
const delta = e.clientX - sidebarDragStartX.current;
|
|
105
|
+
const next = Math.min(Math.max(sidebarDragStartWidth.current + delta, sidebarMinWidth), sidebarMaxWidth);
|
|
106
|
+
setSidebarWidth(next);
|
|
107
|
+
};
|
|
108
|
+
const onUp = () => {
|
|
109
|
+
setSidebarDrag(false);
|
|
110
|
+
// Re-fit the canvas so the graph re-centers in the new available space.
|
|
111
|
+
requestAnimationFrame(() => fitView());
|
|
112
|
+
};
|
|
113
|
+
document.addEventListener('mousemove', onMove);
|
|
114
|
+
document.addEventListener('mouseup', onUp);
|
|
115
|
+
return () => {
|
|
116
|
+
document.removeEventListener('mousemove', onMove);
|
|
117
|
+
document.removeEventListener('mouseup', onUp);
|
|
118
|
+
};
|
|
119
|
+
}, [sidebarDrag, sidebarMaxWidth, fitView]);
|
|
79
120
|
// Ref mirror of `selected` so the SUBSYSTEM_CALLBACKS click handler (a
|
|
80
121
|
// closure over the effect deps) can toggle without a stale value.
|
|
81
122
|
const selectedRef = useRef(null);
|
|
@@ -92,6 +133,50 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
92
133
|
return null;
|
|
93
134
|
return throughlines.find((t) => t.id === drawerTarget.throughlineId) ?? null;
|
|
94
135
|
}, [drawerTarget, throughlines]);
|
|
136
|
+
// Throughline shown on the canvas title chip (focus or hover/autoplay highlight).
|
|
137
|
+
const overlayThroughlineTitle = useMemo(() => {
|
|
138
|
+
if (!showThroughlineTitle || !throughlines?.length)
|
|
139
|
+
return null;
|
|
140
|
+
const id = focusedThroughlineId ?? hoveredThroughlineStep?.throughlineId;
|
|
141
|
+
if (!id)
|
|
142
|
+
return null;
|
|
143
|
+
return throughlines.find((t) => t.id === id)?.title ?? null;
|
|
144
|
+
}, [
|
|
145
|
+
showThroughlineTitle,
|
|
146
|
+
throughlines,
|
|
147
|
+
focusedThroughlineId,
|
|
148
|
+
hoveredThroughlineStep,
|
|
149
|
+
]);
|
|
150
|
+
// Active step for the bottom-of-title progress + annotation chip.
|
|
151
|
+
const overlayThroughlineStep = useMemo(() => {
|
|
152
|
+
if (!showThroughlineTitle || !throughlines?.length)
|
|
153
|
+
return null;
|
|
154
|
+
const tlId = focusedThroughlineId ?? hoveredThroughlineStep?.throughlineId ?? null;
|
|
155
|
+
let stepIndex = null;
|
|
156
|
+
if (focusedThroughlineId != null) {
|
|
157
|
+
stepIndex = focusedStepIndex;
|
|
158
|
+
}
|
|
159
|
+
else if (hoveredThroughlineStep != null) {
|
|
160
|
+
stepIndex = hoveredThroughlineStep.stepIndex;
|
|
161
|
+
}
|
|
162
|
+
if (tlId == null || stepIndex == null)
|
|
163
|
+
return null;
|
|
164
|
+
const tl = throughlines.find((t) => t.id === tlId);
|
|
165
|
+
const step = tl?.steps[stepIndex];
|
|
166
|
+
if (!step || !tl)
|
|
167
|
+
return null;
|
|
168
|
+
return {
|
|
169
|
+
index: stepIndex + 1,
|
|
170
|
+
total: tl.steps.length,
|
|
171
|
+
annotation: step.annotation,
|
|
172
|
+
};
|
|
173
|
+
}, [
|
|
174
|
+
showThroughlineTitle,
|
|
175
|
+
throughlines,
|
|
176
|
+
focusedThroughlineId,
|
|
177
|
+
focusedStepIndex,
|
|
178
|
+
hoveredThroughlineStep,
|
|
179
|
+
]);
|
|
95
180
|
const drawerTitle = useMemo(() => {
|
|
96
181
|
if (!drawerTarget)
|
|
97
182
|
return null;
|
|
@@ -253,12 +338,13 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
253
338
|
}
|
|
254
339
|
return new Set(tl.steps.map((s) => s.edgeId));
|
|
255
340
|
}, [throughlines, focusedThroughlineId, focusedStepIndex]);
|
|
256
|
-
// 1-based step numbers per edge of the
|
|
257
|
-
// in more than one step
|
|
341
|
+
// 1-based step numbers per edge of the active flow (focused or
|
|
342
|
+
// hover/autoplay-highlighted). An edge can appear in more than one step.
|
|
258
343
|
const selectedFlowStepNos = useMemo(() => {
|
|
259
|
-
|
|
344
|
+
const activeId = focusedThroughlineId ?? hoveredThroughlineStep?.throughlineId;
|
|
345
|
+
if (activeId == null || !throughlines)
|
|
260
346
|
return null;
|
|
261
|
-
const tl = throughlines.find((t) => t.id ===
|
|
347
|
+
const tl = throughlines.find((t) => t.id === activeId);
|
|
262
348
|
if (!tl)
|
|
263
349
|
return null;
|
|
264
350
|
const map = new Map();
|
|
@@ -268,7 +354,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
268
354
|
map.set(s.edgeId, list);
|
|
269
355
|
});
|
|
270
356
|
return map;
|
|
271
|
-
}, [throughlines, focusedThroughlineId]);
|
|
357
|
+
}, [throughlines, focusedThroughlineId, hoveredThroughlineStep]);
|
|
272
358
|
// Union of every expanded (opened) throughline's edges — the visible set.
|
|
273
359
|
const openedEdgeIds = useMemo(() => {
|
|
274
360
|
if (!throughlines || expandedThroughlines.size === 0)
|
|
@@ -294,6 +380,19 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
294
380
|
}
|
|
295
381
|
return ids.size > 0 ? ids : null;
|
|
296
382
|
};
|
|
383
|
+
// Edge/nodes involved in the hovered step — dim everything else.
|
|
384
|
+
const hoverEdgeIds = useMemo(() => {
|
|
385
|
+
if (!hoveredThroughlineStep || !throughlines)
|
|
386
|
+
return null;
|
|
387
|
+
const tl = throughlines.find((t) => t.id === hoveredThroughlineStep.throughlineId);
|
|
388
|
+
if (!tl)
|
|
389
|
+
return null;
|
|
390
|
+
const step = tl.steps[hoveredThroughlineStep.stepIndex];
|
|
391
|
+
return step ? new Set([step.edgeId]) : null;
|
|
392
|
+
}, [throughlines, hoveredThroughlineStep]);
|
|
393
|
+
const hoverNodeIds = useMemo(() => endpointsOf(hoverEdgeIds),
|
|
394
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
395
|
+
[baseEdges, hoverEdgeIds]);
|
|
297
396
|
const openedNodeIds = useMemo(() => endpointsOf(openedEdgeIds),
|
|
298
397
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
299
398
|
[baseEdges, openedEdgeIds]);
|
|
@@ -323,6 +422,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
323
422
|
// `isSelected` rides in data because the node's stopPropagation() keeps
|
|
324
423
|
// React Flow's own selection state from updating.
|
|
325
424
|
const dispNodes = useMemo(() => {
|
|
425
|
+
// While a step is hovered, only the hovered step's endpoints stay bright;
|
|
426
|
+
// everything else is dimmed (never hidden) so the step's context survives.
|
|
326
427
|
return xyflowNodesBase.map((n) => {
|
|
327
428
|
// Boundary frames follow their members: hidden when no member is
|
|
328
429
|
// visible, dimmed when members are dimmed. Never selectable.
|
|
@@ -334,13 +435,17 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
334
435
|
anyOpened: openedNodeIds != null,
|
|
335
436
|
anySelected: brightNodeIds != null,
|
|
336
437
|
});
|
|
438
|
+
const dimmed = hoverNodeIds
|
|
439
|
+
? !memberIds.some((id) => hoverNodeIds.has(id))
|
|
440
|
+
: vis.dimmed;
|
|
441
|
+
const hidden = hoverNodeIds ? false : vis.hidden;
|
|
337
442
|
return {
|
|
338
443
|
...n,
|
|
339
|
-
hidden
|
|
444
|
+
hidden,
|
|
340
445
|
selectable: false,
|
|
341
446
|
data: {
|
|
342
447
|
...n.data,
|
|
343
|
-
...(
|
|
448
|
+
...(dimmed && { dimmed: true }),
|
|
344
449
|
},
|
|
345
450
|
};
|
|
346
451
|
}
|
|
@@ -353,22 +458,24 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
353
458
|
anyOpened: openedNodeIds != null,
|
|
354
459
|
anySelected: brightNodeIds != null,
|
|
355
460
|
});
|
|
356
|
-
|
|
461
|
+
const dimmed = hoverNodeIds ? !hoverNodeIds.has(n.id) : vis.dimmed;
|
|
462
|
+
const hidden = hoverNodeIds ? false : vis.hidden;
|
|
463
|
+
if (fileMatch === undefined && !isSelected && !dimmed) {
|
|
357
464
|
const { fileMatch: _f, isSelected: _s, dimmed: _d, ...rest } = n.data;
|
|
358
|
-
return { ...n, hidden
|
|
465
|
+
return { ...n, hidden, data: rest };
|
|
359
466
|
}
|
|
360
467
|
return {
|
|
361
468
|
...n,
|
|
362
|
-
hidden
|
|
469
|
+
hidden,
|
|
363
470
|
data: {
|
|
364
471
|
...n.data,
|
|
365
472
|
...(fileMatch !== undefined && { fileMatch }),
|
|
366
473
|
...(isSelected && { isSelected }),
|
|
367
|
-
...(
|
|
474
|
+
...(dimmed && { dimmed: true }),
|
|
368
475
|
},
|
|
369
476
|
};
|
|
370
477
|
});
|
|
371
|
-
}, [xyflowNodesBase, openFile, selected, focusNodeIds, openedNodeIds, brightNodeIds]);
|
|
478
|
+
}, [xyflowNodesBase, openFile, selected, focusNodeIds, openedNodeIds, brightNodeIds, hoverNodeIds]);
|
|
372
479
|
const baseNodesKey = useMemo(() => nodes.map((n) => n.id).sort().join(','), [nodes]);
|
|
373
480
|
const baseEdgesKey = useMemo(() => convertedEdges.map((e) => e.id).sort().join(','), [convertedEdges]);
|
|
374
481
|
const dispEdges = useMemo(() => {
|
|
@@ -383,7 +490,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
383
490
|
markerEnd: nextMarker,
|
|
384
491
|
};
|
|
385
492
|
};
|
|
386
|
-
if (openedEdgeIds || focusEdgeIds) {
|
|
493
|
+
if (openedEdgeIds || focusEdgeIds || hoverEdgeIds) {
|
|
387
494
|
return baseEdges.map((e) => {
|
|
388
495
|
const vis = flowElementVisibility({
|
|
389
496
|
inOpened: openedEdgeIds?.has(e.id) === true,
|
|
@@ -391,13 +498,15 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
391
498
|
anyOpened: openedEdgeIds != null,
|
|
392
499
|
anySelected: focusEdgeIds != null,
|
|
393
500
|
});
|
|
394
|
-
|
|
501
|
+
const dimmed = hoverEdgeIds ? !hoverEdgeIds.has(e.id) : vis.dimmed;
|
|
502
|
+
const hidden = hoverEdgeIds ? false : vis.hidden;
|
|
503
|
+
return { ...paint(e, dimmed), hidden };
|
|
395
504
|
});
|
|
396
505
|
}
|
|
397
506
|
if (!selectedEdgeId)
|
|
398
507
|
return baseEdges;
|
|
399
508
|
return baseEdges.map((e) => paint(e, e.id !== selectedEdgeId));
|
|
400
|
-
}, [baseEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds]);
|
|
509
|
+
}, [baseEdges, selectedEdgeId, openedEdgeIds, focusEdgeIds, hoverEdgeIds]);
|
|
401
510
|
const onNodesChange = useCallback((changes) => {
|
|
402
511
|
// Capture dimension changes (React Flow's measurement callback).
|
|
403
512
|
// Group parents are ELK-sized — ignore their measurements.
|
|
@@ -492,6 +601,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
492
601
|
// live positions + measured dims — so the frame always includes BOTH
|
|
493
602
|
// components the edge attaches to (and therefore the edge line between them).
|
|
494
603
|
const fitFocusBounds = useCallback((ids) => {
|
|
604
|
+
if (!zoomOnThroughlineFocus)
|
|
605
|
+
return;
|
|
495
606
|
const nodeIds = new Set();
|
|
496
607
|
for (const e of baseEdges) {
|
|
497
608
|
if (!ids.has(e.id))
|
|
@@ -506,35 +617,55 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
506
617
|
padding: 0.25,
|
|
507
618
|
duration: 300,
|
|
508
619
|
});
|
|
509
|
-
}, [baseEdges, fitView]);
|
|
620
|
+
}, [baseEdges, fitView, zoomOnThroughlineFocus]);
|
|
510
621
|
// Focus an entire flow: hide everything but the flow's nodes and edges, and
|
|
511
622
|
// frame the flow on the canvas. Selection state is cleared — the graph now
|
|
512
623
|
// reads as the narrative. No drawer: the code view only opens on a step
|
|
513
624
|
// click. A stale throughline drawer (from a previously focused flow's step)
|
|
514
625
|
// closes; an explicitly opened file drawer stays.
|
|
626
|
+
// In `dim` mode: clear step highlight and leave the full graph visible
|
|
627
|
+
// (no zoom / hide) — matching "step away from a hovered step".
|
|
515
628
|
const focusThroughlineEdges = useCallback((tl) => {
|
|
516
629
|
setSelected(null);
|
|
517
630
|
setSelectedEdgeId(null);
|
|
631
|
+
setHoveredThroughlineStep(null);
|
|
632
|
+
if (throughlineStepMode === 'dim') {
|
|
633
|
+
setFocusedStepIndex(null);
|
|
634
|
+
setFocusedThroughlineId(null);
|
|
635
|
+
setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
518
638
|
setFocusedStepIndex(null);
|
|
519
639
|
setFocusedThroughlineId(tl.id);
|
|
520
640
|
fitFocusBounds(new Set(tl.steps.map((s) => s.edgeId)));
|
|
521
641
|
setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
|
|
522
|
-
}, [fitFocusBounds]);
|
|
642
|
+
}, [fitFocusBounds, throughlineStepMode]);
|
|
523
643
|
const clearThroughlineFocus = useCallback(() => {
|
|
524
644
|
setFocusedThroughlineId(null);
|
|
525
645
|
setFocusedStepIndex(null);
|
|
646
|
+
setHoveredThroughlineStep(null);
|
|
526
647
|
setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
|
|
527
648
|
}, []);
|
|
528
649
|
// Focus a single step's edge on the canvas and open/scroll the throughline
|
|
529
650
|
// drawer to that step's snippet.
|
|
651
|
+
// In `dim` mode: only dim non-participants (same as hovering a step) —
|
|
652
|
+
// no camera move, no drawer, no hide.
|
|
530
653
|
const focusThroughlineStep = useCallback((tl, stepIndex) => {
|
|
531
654
|
const step = tl.steps[stepIndex];
|
|
532
655
|
if (!step)
|
|
533
656
|
return;
|
|
534
657
|
setSelected(null);
|
|
535
658
|
setSelectedEdgeId(null);
|
|
659
|
+
if (throughlineStepMode === 'dim') {
|
|
660
|
+
setFocusedStepIndex(null);
|
|
661
|
+
setFocusedThroughlineId(null);
|
|
662
|
+
setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex });
|
|
663
|
+
setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
536
666
|
setFocusedStepIndex(stepIndex);
|
|
537
667
|
setFocusedThroughlineId(tl.id);
|
|
668
|
+
setHoveredThroughlineStep(null);
|
|
538
669
|
fitFocusBounds(new Set([step.edgeId]));
|
|
539
670
|
if (renderThroughlineViewer) {
|
|
540
671
|
setDrawerTarget({
|
|
@@ -550,7 +681,43 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
550
681
|
startLine: step.line,
|
|
551
682
|
});
|
|
552
683
|
}
|
|
553
|
-
}, [fitFocusBounds, renderThroughlineViewer]);
|
|
684
|
+
}, [fitFocusBounds, renderThroughlineViewer, throughlineStepMode]);
|
|
685
|
+
// Graph-only embeds: cycle throughline steps with hover-style dimming.
|
|
686
|
+
useEffect(() => {
|
|
687
|
+
if (!autoPlayThroughlines || !throughlines?.length || !layoutReady)
|
|
688
|
+
return;
|
|
689
|
+
const playable = throughlines.filter((tl) => tl.steps.length > 0);
|
|
690
|
+
if (playable.length === 0)
|
|
691
|
+
return;
|
|
692
|
+
let cancelled = false;
|
|
693
|
+
let tlIdx = 0;
|
|
694
|
+
let stepIdx = 0;
|
|
695
|
+
let timer = null;
|
|
696
|
+
const interval = Math.max(400, throughlineAutoPlayIntervalMs);
|
|
697
|
+
const tick = () => {
|
|
698
|
+
if (cancelled)
|
|
699
|
+
return;
|
|
700
|
+
const tl = playable[tlIdx];
|
|
701
|
+
setSelected(null);
|
|
702
|
+
setSelectedEdgeId(null);
|
|
703
|
+
setFocusedThroughlineId(null);
|
|
704
|
+
setFocusedStepIndex(null);
|
|
705
|
+
setHoveredThroughlineStep({ throughlineId: tl.id, stepIndex: stepIdx });
|
|
706
|
+
stepIdx += 1;
|
|
707
|
+
if (stepIdx >= tl.steps.length) {
|
|
708
|
+
stepIdx = 0;
|
|
709
|
+
tlIdx = (tlIdx + 1) % playable.length;
|
|
710
|
+
}
|
|
711
|
+
timer = window.setTimeout(tick, interval);
|
|
712
|
+
};
|
|
713
|
+
tick();
|
|
714
|
+
return () => {
|
|
715
|
+
cancelled = true;
|
|
716
|
+
if (timer != null)
|
|
717
|
+
window.clearTimeout(timer);
|
|
718
|
+
setHoveredThroughlineStep(null);
|
|
719
|
+
};
|
|
720
|
+
}, [autoPlayThroughlines, throughlines, throughlineAutoPlayIntervalMs, layoutReady]);
|
|
554
721
|
// Arrow keys step through the focused throughline once a step is active
|
|
555
722
|
// (sidebar click or drawer open). Ignores typing targets and chords.
|
|
556
723
|
useEffect(() => {
|
|
@@ -679,9 +846,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
679
846
|
const throughlineViewerRef = useRef(renderThroughlineViewer);
|
|
680
847
|
throughlineViewerRef.current = renderThroughlineViewer;
|
|
681
848
|
const renderThroughlineDrawerContent = useCallback((ctx) => throughlineViewerRef.current?.(ctx) ?? null, []);
|
|
682
|
-
return (_jsxs("div", { style: { width: '100%', height: '100%', display: 'flex', flexDirection: 'row' }, children: [!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasThroughlines) && (_jsxs("div", { style: {
|
|
683
|
-
width:
|
|
684
|
-
minWidth:
|
|
849
|
+
return (_jsxs("div", { style: { width: '100%', height: '100%', display: 'flex', flexDirection: 'row', userSelect: sidebarDrag ? 'none' : undefined }, children: [!hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasThroughlines) && (_jsxs("div", { style: {
|
|
850
|
+
width: sidebarWidth,
|
|
851
|
+
minWidth: sidebarWidth,
|
|
685
852
|
borderRight: `1px solid ${theme.colors.border}`,
|
|
686
853
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
687
854
|
display: 'flex',
|
|
@@ -749,8 +916,12 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
749
916
|
flex: 1,
|
|
750
917
|
minHeight: 0,
|
|
751
918
|
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
|
|
919
|
+
padding: '0 0 12px',
|
|
920
|
+
}, children: throughlines.map((tl) => (_jsx(ThroughlineFlow, { throughline: tl, edges: edges, collapsed: !expandedThroughlines.has(tl.id), active: focusedThroughlineId === tl.id
|
|
921
|
+
? { stepIndex: focusedStepIndex }
|
|
922
|
+
: hoveredThroughlineStep?.throughlineId === tl.id
|
|
923
|
+
? { stepIndex: hoveredThroughlineStep.stepIndex }
|
|
924
|
+
: 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
925
|
const groupKey = group.repoKey ?? '__no-repo__';
|
|
755
926
|
const collapsed = collapsedRepos.has(groupKey);
|
|
756
927
|
return (_jsxs("div", { style: {
|
|
@@ -760,7 +931,14 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
760
931
|
flexDirection: 'column',
|
|
761
932
|
borderTop: i > 0 ? `1px solid ${theme.colors.border}` : undefined,
|
|
762
933
|
}, 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));
|
|
763
|
-
}) })) : null] }))] })),
|
|
934
|
+
}) })) : null] }))] })), !hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasThroughlines) && (_jsx("div", { onMouseDown: onSidebarResizeStart, "aria-label": "Resize sidebar", title: "Drag to resize", style: {
|
|
935
|
+
width: 3,
|
|
936
|
+
flexShrink: 0,
|
|
937
|
+
cursor: 'col-resize',
|
|
938
|
+
background: theme.colors.border,
|
|
939
|
+
transition: 'background 120ms ease',
|
|
940
|
+
zIndex: 1,
|
|
941
|
+
} })), _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: {
|
|
764
942
|
position: 'absolute',
|
|
765
943
|
inset: 0,
|
|
766
944
|
overflow: 'hidden',
|
|
@@ -816,27 +994,88 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
816
994
|
flex: 1,
|
|
817
995
|
minHeight: 0,
|
|
818
996
|
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 && (
|
|
997
|
+
}, 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
998
|
position: 'absolute',
|
|
821
999
|
top: 10,
|
|
822
1000
|
left: '50%',
|
|
823
1001
|
transform: 'translateX(-50%)',
|
|
824
1002
|
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)',
|
|
1003
|
+
display: 'flex',
|
|
1004
|
+
flexDirection: 'column',
|
|
1005
|
+
alignItems: 'center',
|
|
1006
|
+
gap: 6,
|
|
1007
|
+
maxWidth: '70%',
|
|
838
1008
|
pointerEvents: 'none',
|
|
839
|
-
}, children: graphTitle
|
|
1009
|
+
}, children: [graphTitle && (_jsx("div", { style: {
|
|
1010
|
+
maxWidth: '100%',
|
|
1011
|
+
overflow: 'hidden',
|
|
1012
|
+
textOverflow: 'ellipsis',
|
|
1013
|
+
whiteSpace: 'nowrap',
|
|
1014
|
+
padding: '6px 18px',
|
|
1015
|
+
fontSize: theme.fontSizes[3],
|
|
1016
|
+
fontWeight: 600,
|
|
1017
|
+
fontFamily: theme.fonts.monospace,
|
|
1018
|
+
color: theme.colors.text,
|
|
1019
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
1020
|
+
border: `1px solid ${theme.colors.border}`,
|
|
1021
|
+
borderRadius: 6,
|
|
1022
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
1023
|
+
}, children: graphTitle })), overlayThroughlineTitle && (_jsxs("div", { style: {
|
|
1024
|
+
maxWidth: '100%',
|
|
1025
|
+
minWidth: overlayThroughlineStep ? 160 : undefined,
|
|
1026
|
+
display: 'flex',
|
|
1027
|
+
flexDirection: 'column',
|
|
1028
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
1029
|
+
border: `1px solid ${theme.colors.border}`,
|
|
1030
|
+
borderRadius: 6,
|
|
1031
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
1032
|
+
overflow: 'hidden',
|
|
1033
|
+
opacity: 0.95,
|
|
1034
|
+
}, "aria-label": overlayThroughlineStep
|
|
1035
|
+
? `${overlayThroughlineTitle}, step ${overlayThroughlineStep.index} of ${overlayThroughlineStep.total}`
|
|
1036
|
+
: overlayThroughlineTitle, children: [_jsx("div", { style: {
|
|
1037
|
+
padding: '5px 14px',
|
|
1038
|
+
fontSize: theme.fontSizes[2] ?? theme.fontSizes[1],
|
|
1039
|
+
fontWeight: 600,
|
|
1040
|
+
fontFamily: theme.fonts.monospace,
|
|
1041
|
+
color: theme.colors.text,
|
|
1042
|
+
overflow: 'hidden',
|
|
1043
|
+
textOverflow: 'ellipsis',
|
|
1044
|
+
whiteSpace: 'nowrap',
|
|
1045
|
+
textAlign: 'center',
|
|
1046
|
+
}, children: overlayThroughlineTitle }), overlayThroughlineStep && overlayThroughlineStep.total > 0 && (_jsx("div", { style: {
|
|
1047
|
+
display: 'flex',
|
|
1048
|
+
gap: 3,
|
|
1049
|
+
padding: '0 6px 5px',
|
|
1050
|
+
}, "aria-hidden": "true", children: Array.from({ length: overlayThroughlineStep.total }, (_, i) => {
|
|
1051
|
+
const n = i + 1;
|
|
1052
|
+
const active = n === overlayThroughlineStep.index;
|
|
1053
|
+
const done = n < overlayThroughlineStep.index;
|
|
1054
|
+
return (_jsx("span", { style: {
|
|
1055
|
+
flex: 1,
|
|
1056
|
+
height: 2,
|
|
1057
|
+
borderRadius: 1,
|
|
1058
|
+
background: active
|
|
1059
|
+
? (theme.colors.accent ?? theme.colors.primary ?? theme.colors.text)
|
|
1060
|
+
: done
|
|
1061
|
+
? (theme.colors.textSecondary ?? theme.colors.text)
|
|
1062
|
+
: (theme.colors.border ?? 'rgba(127,127,127,0.45)'),
|
|
1063
|
+
opacity: active ? 1 : done ? 0.75 : 0.4,
|
|
1064
|
+
transition: 'opacity 120ms ease, background 120ms ease',
|
|
1065
|
+
} }, n));
|
|
1066
|
+
}) }))] })), overlayThroughlineStep?.annotation && (_jsx("div", { style: {
|
|
1067
|
+
maxWidth: '100%',
|
|
1068
|
+
padding: '6px 14px',
|
|
1069
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
1070
|
+
border: `1px solid ${theme.colors.border}`,
|
|
1071
|
+
borderRadius: 6,
|
|
1072
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
1073
|
+
textAlign: 'center',
|
|
1074
|
+
fontSize: theme.fontSizes[0],
|
|
1075
|
+
fontFamily: theme.fonts.body,
|
|
1076
|
+
color: theme.colors.textMuted ?? theme.colors.textSecondary,
|
|
1077
|
+
lineHeight: 1.35,
|
|
1078
|
+
}, children: overlayThroughlineStep.annotation }))] })), selected && (_jsx("div", { style: {
|
|
840
1079
|
position: 'absolute',
|
|
841
1080
|
top: 10,
|
|
842
1081
|
right: 10,
|
|
@@ -896,7 +1135,7 @@ function RepoGroupHeader({ group, collapsed, onToggle, }) {
|
|
|
896
1135
|
* title: closed → open + select; open and unselected → select; open and
|
|
897
1136
|
* selected → close + clear focus. The right-aligned close button collapses
|
|
898
1137
|
* without selecting. A step row focuses that step's edge. */
|
|
899
|
-
function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollapsed, onFocusFlow, onClearFocus, onFocusStep, }) {
|
|
1138
|
+
function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollapsed, onFocusFlow, onClearFocus, onFocusStep, onHoverStep, onLeaveStep, }) {
|
|
900
1139
|
const { theme } = useTheme();
|
|
901
1140
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
902
1141
|
const hoverBg = theme.colors.background;
|
|
@@ -904,8 +1143,52 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
904
1143
|
const wholeFlowActive = active !== null && active.stepIndex === null;
|
|
905
1144
|
const [headerHover, setHeaderHover] = useState(false);
|
|
906
1145
|
const [closeHover, setCloseHover] = useState(false);
|
|
1146
|
+
const [playHover, setPlayHover] = useState(false);
|
|
907
1147
|
const [hoveredStep, setHoveredStep] = useState(null);
|
|
908
1148
|
const stepButtonRefs = useRef([]);
|
|
1149
|
+
// Autoplay: stepping through the flow's steps with a pause between each.
|
|
1150
|
+
const [playing, setPlaying] = useState(false);
|
|
1151
|
+
const playTimerRef = useRef(null);
|
|
1152
|
+
const stopPlaying = useCallback(() => {
|
|
1153
|
+
if (playTimerRef.current != null) {
|
|
1154
|
+
window.clearTimeout(playTimerRef.current);
|
|
1155
|
+
playTimerRef.current = null;
|
|
1156
|
+
}
|
|
1157
|
+
setPlaying(false);
|
|
1158
|
+
}, []);
|
|
1159
|
+
// Clear any pending timer on unmount.
|
|
1160
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1161
|
+
useEffect(() => () => stopPlaying(), []);
|
|
1162
|
+
const startPlaying = useCallback(() => {
|
|
1163
|
+
if (collapsed)
|
|
1164
|
+
onToggleCollapsed(throughline.id);
|
|
1165
|
+
if (active === null || active.stepIndex !== null)
|
|
1166
|
+
onFocusFlow(throughline);
|
|
1167
|
+
const stepCount = throughline.steps.length;
|
|
1168
|
+
if (stepCount === 0)
|
|
1169
|
+
return;
|
|
1170
|
+
setPlaying(true);
|
|
1171
|
+
let i = 0;
|
|
1172
|
+
const tick = () => {
|
|
1173
|
+
if (i >= stepCount) {
|
|
1174
|
+
playTimerRef.current = null;
|
|
1175
|
+
setPlaying(false);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
onFocusStep(throughline, i);
|
|
1179
|
+
i += 1;
|
|
1180
|
+
playTimerRef.current = window.setTimeout(tick, THROUGHLINE_PLAY_PAUSE_MS);
|
|
1181
|
+
};
|
|
1182
|
+
tick();
|
|
1183
|
+
}, [collapsed, active, onToggleCollapsed, onFocusFlow, throughline, onFocusStep]);
|
|
1184
|
+
const togglePlay = useCallback(() => {
|
|
1185
|
+
if (playing) {
|
|
1186
|
+
stopPlaying();
|
|
1187
|
+
}
|
|
1188
|
+
else {
|
|
1189
|
+
startPlaying();
|
|
1190
|
+
}
|
|
1191
|
+
}, [playing, stopPlaying, startPlaying]);
|
|
909
1192
|
// Keep DOM focus on the active step so the browser focus ring (and
|
|
910
1193
|
// subsequent arrow keys) follow arrow navigation, not the originally
|
|
911
1194
|
// clicked button.
|
|
@@ -914,11 +1197,10 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
914
1197
|
return;
|
|
915
1198
|
stepButtonRefs.current[active.stepIndex]?.focus({ preventScroll: true });
|
|
916
1199
|
}, [active?.stepIndex]);
|
|
917
|
-
return (_jsxs("div", {
|
|
1200
|
+
return (_jsxs("div", { children: [_jsxs("div", { onMouseEnter: () => setHeaderHover(true), onMouseLeave: () => setHeaderHover(false), style: {
|
|
918
1201
|
display: 'flex',
|
|
919
1202
|
alignItems: 'center',
|
|
920
1203
|
gap: 4,
|
|
921
|
-
borderRadius: 6,
|
|
922
1204
|
background: wholeFlowActive || headerHover ? hoverBg : 'transparent',
|
|
923
1205
|
transition: 'background 120ms ease',
|
|
924
1206
|
}, children: [_jsx("button", { type: "button", onClick: () => {
|
|
@@ -938,8 +1220,7 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
938
1220
|
display: 'flex',
|
|
939
1221
|
alignItems: 'center',
|
|
940
1222
|
minWidth: 0,
|
|
941
|
-
padding: '
|
|
942
|
-
borderRadius: 6,
|
|
1223
|
+
padding: '10px 8px',
|
|
943
1224
|
border: 'none',
|
|
944
1225
|
background: 'transparent',
|
|
945
1226
|
textAlign: 'left',
|
|
@@ -952,39 +1233,62 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
952
1233
|
fontFamily: theme.fonts.monospace,
|
|
953
1234
|
fontWeight: 600,
|
|
954
1235
|
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
|
-
|
|
1236
|
+
}, 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) => {
|
|
1237
|
+
e.stopPropagation();
|
|
1238
|
+
togglePlay();
|
|
1239
|
+
}, style: {
|
|
1240
|
+
display: 'inline-flex',
|
|
1241
|
+
alignItems: 'center',
|
|
1242
|
+
justifyContent: 'center',
|
|
1243
|
+
flexShrink: 0,
|
|
1244
|
+
width: 22,
|
|
1245
|
+
height: 22,
|
|
1246
|
+
padding: 0,
|
|
1247
|
+
border: 'none',
|
|
1248
|
+
borderRadius: 4,
|
|
1249
|
+
background: playing || playHover ? theme.colors.border : 'transparent',
|
|
1250
|
+
color: playing || playHover ? theme.colors.text : muted,
|
|
1251
|
+
cursor: 'pointer',
|
|
1252
|
+
transition: 'background 120ms ease, color 120ms ease',
|
|
1253
|
+
}, 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) => {
|
|
1254
|
+
e.stopPropagation();
|
|
1255
|
+
onToggleCollapsed(throughline.id);
|
|
1256
|
+
if (active !== null)
|
|
1257
|
+
onClearFocus();
|
|
1258
|
+
}, style: {
|
|
1259
|
+
display: 'inline-flex',
|
|
1260
|
+
alignItems: 'center',
|
|
1261
|
+
justifyContent: 'center',
|
|
1262
|
+
flexShrink: 0,
|
|
1263
|
+
width: 22,
|
|
1264
|
+
height: 22,
|
|
1265
|
+
marginRight: 4,
|
|
1266
|
+
padding: 0,
|
|
1267
|
+
border: 'none',
|
|
1268
|
+
borderRadius: 4,
|
|
1269
|
+
background: closeHover ? theme.colors.border : 'transparent',
|
|
1270
|
+
color: closeHover ? theme.colors.text : muted,
|
|
1271
|
+
cursor: 'pointer',
|
|
1272
|
+
transition: 'background 120ms ease, color 120ms ease',
|
|
1273
|
+
}, children: _jsx(X, { size: 12, strokeWidth: 2 }) })] }))] }), !collapsed && (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 1 }, children: throughline.steps.map((step, i) => {
|
|
976
1274
|
const edge = edgeById.get(step.edgeId);
|
|
977
1275
|
const mech = edge?.mechanism;
|
|
978
1276
|
const color = mech ? MECHANISM_COLOR[mech] : muted;
|
|
979
1277
|
const stepActive = active !== null && active.stepIndex === i;
|
|
980
1278
|
return (_jsxs("button", { ref: (el) => {
|
|
981
1279
|
stepButtonRefs.current[i] = el;
|
|
982
|
-
}, type: "button", onMouseEnter: () =>
|
|
1280
|
+
}, type: "button", onMouseEnter: () => {
|
|
1281
|
+
setHoveredStep(i);
|
|
1282
|
+
onHoverStep(throughline, i);
|
|
1283
|
+
}, onMouseLeave: () => {
|
|
1284
|
+
setHoveredStep(null);
|
|
1285
|
+
onLeaveStep();
|
|
1286
|
+
}, onClick: () => onFocusStep(throughline, i), style: {
|
|
983
1287
|
display: 'flex',
|
|
984
1288
|
alignItems: 'center',
|
|
985
1289
|
gap: 8,
|
|
986
1290
|
minWidth: 0,
|
|
987
|
-
padding: '
|
|
1291
|
+
padding: '8px 8px 8px 12px',
|
|
988
1292
|
textAlign: 'left',
|
|
989
1293
|
borderRadius: 6,
|
|
990
1294
|
border: 'none',
|
|
@@ -1004,9 +1308,9 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1004
1308
|
overflow: 'hidden',
|
|
1005
1309
|
textOverflow: 'ellipsis',
|
|
1006
1310
|
whiteSpace: 'nowrap',
|
|
1007
|
-
fontSize: theme.fontSizes[
|
|
1311
|
+
fontSize: theme.fontSizes[1],
|
|
1008
1312
|
fontFamily: theme.fonts.monospace,
|
|
1009
|
-
color:
|
|
1313
|
+
color: theme.colors.text,
|
|
1010
1314
|
}, children: step.symbol })) : (_jsxs(_Fragment, { children: [_jsx("span", { style: {
|
|
1011
1315
|
flexShrink: 0,
|
|
1012
1316
|
width: 74,
|
|
@@ -1024,7 +1328,7 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1024
1328
|
whiteSpace: 'nowrap',
|
|
1025
1329
|
fontSize: theme.fontSizes[0],
|
|
1026
1330
|
fontFamily: theme.fonts.monospace,
|
|
1027
|
-
color:
|
|
1331
|
+
color: theme.colors.text,
|
|
1028
1332
|
}, children: [step.file.split('/').pop(), _jsxs("span", { style: { opacity: 0.7 }, children: [":", step.line] })] })] }))] }, `${step.edgeId}-${i}`));
|
|
1029
1333
|
}) }))] }));
|
|
1030
1334
|
}
|