@principal-ai/subsystems-react 0.20.5 → 0.21.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 +1 -1
- package/dist/graphify/anchor.d.ts +17 -0
- package/dist/graphify/anchor.d.ts.map +1 -1
- package/dist/graphify/anchor.js +41 -0
- package/dist/graphify/anchor.js.map +1 -1
- package/dist/graphify/consolidated.d.ts +40 -1
- package/dist/graphify/consolidated.d.ts.map +1 -1
- package/dist/graphify/construct.d.ts +23 -0
- package/dist/graphify/construct.d.ts.map +1 -0
- package/dist/graphify/construct.js +77 -0
- package/dist/graphify/construct.js.map +1 -0
- package/dist/graphify/index.d.ts +2 -2
- package/dist/graphify/index.d.ts.map +1 -1
- package/dist/graphify/index.js +1 -1
- package/dist/graphify/index.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts +22 -0
- package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -0
- package/dist/pierre/PierreWalkthroughCodeView.js +198 -0
- package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -0
- package/dist/pierre/index.d.ts +2 -2
- package/dist/pierre/index.js +1 -1
- package/dist/pierre/pierreFileLang.d.ts +1 -1
- package/dist/pierre/pierreFileLang.js +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +14 -2
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +86 -20
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +1 -1
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/FileDrawer.d.ts +2 -2
- package/dist/subsystem/FileDrawer.js +2 -2
- package/dist/subsystem/SubsystemComponentGraph.d.ts +25 -24
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +220 -194
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/formatDeclaration.d.ts +2 -1
- package/dist/subsystem/formatDeclaration.d.ts.map +1 -1
- package/dist/subsystem/formatDeclaration.js +57 -33
- package/dist/subsystem/formatDeclaration.js.map +1 -1
- package/dist/subsystem/model.d.ts +109 -42
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +107 -5
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +43 -18
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/tokenizeComponent.js +2 -2
- package/dist/subsystem/tokenizeComponent.js.map +1 -1
- package/package.json +3 -3
- package/src/graphify/anchor.test.ts +31 -2
- package/src/graphify/anchor.ts +46 -0
- package/src/graphify/consolidated.ts +43 -1
- package/src/graphify/{kind.test.ts → construct.test.ts} +20 -20
- package/src/graphify/{kind.ts → construct.ts} +17 -17
- package/src/graphify/index.ts +5 -2
- package/src/index.ts +20 -9
- package/src/pierre/{PierreThroughlineCodeView.tsx → PierreWalkthroughCodeView.tsx} +39 -47
- package/src/pierre/index.ts +2 -2
- package/src/pierre/pierreFileLang.ts +1 -1
- package/src/stories/Pierre/CodeView.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +14 -14
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +13 -15
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +8 -8
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +16 -20
- package/src/stories/Subsystem/ComponentGraph/CustomEntities.stories.tsx +11 -10
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +13 -14
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +31 -66
- package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +2 -2
- package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +4 -4
- package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentGraph/Proposed.stories.tsx +254 -0
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +518 -33
- package/src/stories/Subsystem/ComponentGraph/Spotlights.stories.tsx +13 -13
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +105 -21
- package/src/subsystem/ComponentDeclaration.tsx +1 -1
- package/src/subsystem/FileDrawer.tsx +2 -2
- package/src/subsystem/SubsystemComponentGraph.tsx +284 -254
- package/src/subsystem/formatDeclaration.test.ts +76 -1
- package/src/subsystem/formatDeclaration.ts +57 -33
- package/src/subsystem/model.test.ts +49 -15
- package/src/subsystem/model.ts +210 -51
- package/src/subsystem/nodes.tsx +63 -22
- package/src/subsystem/tokenizeComponent.ts +2 -2
|
@@ -17,7 +17,7 @@ import { ReactFlow, ReactFlowProvider, Background, BackgroundVariant, Controls,
|
|
|
17
17
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
18
18
|
import { ChevronDown, ChevronUp, Pause, Play, X } from 'lucide-react';
|
|
19
19
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
20
|
-
import { buildSubsystemGraph, MECHANISM_COLOR, MECHANISM_DESCRIPTIONS, subsystemGraphLayoutKey, } from './model';
|
|
20
|
+
import { buildSubsystemGraph, deriveGraphEdges, MECHANISM_COLOR, MECHANISM_DESCRIPTIONS, subsystemGraphLayoutKey, walkthroughStepGraphEdgeId, } from './model';
|
|
21
21
|
import { SubsystemComponentNode, SubsystemGroupNode, SubsystemEdge, SUBSYSTEM_CALLBACKS, hexWithAlpha, EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility } from './nodes';
|
|
22
22
|
import { SubsystemFileTree } from './SubsystemFileTree';
|
|
23
23
|
import { GraphLayoutCover } from './GraphLayoutCover';
|
|
@@ -29,8 +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
|
|
33
|
-
const
|
|
32
|
+
/** Pause (ms) between steps when a walkthrough autoplays. */
|
|
33
|
+
const WALKTHROUGH_PLAY_PAUSE_MS = 2500;
|
|
34
34
|
const nodeTypes = {
|
|
35
35
|
'subsystem-component': SubsystemComponentNode,
|
|
36
36
|
'subsystem-group': SubsystemGroupNode,
|
|
@@ -45,20 +45,21 @@ const edgeTypes = {
|
|
|
45
45
|
const FileDrawerContent = memo(function FileDrawerContent({ render, file, startLine, }) {
|
|
46
46
|
return _jsx(_Fragment, { children: render(file, startLine != null ? { startLine } : undefined) });
|
|
47
47
|
});
|
|
48
|
-
const
|
|
49
|
-
return _jsx(_Fragment, { children: render({
|
|
48
|
+
const WalkthroughDrawerContent = memo(function WalkthroughDrawerContent({ render, walkthrough, stepIndex, }) {
|
|
49
|
+
return _jsx(_Fragment, { children: render({ walkthrough, stepIndex }) });
|
|
50
50
|
});
|
|
51
|
-
function Inner({ components,
|
|
51
|
+
function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, onVerifyComponent, componentVerification }) {
|
|
52
52
|
const { theme } = useTheme();
|
|
53
53
|
const { fitView } = useReactFlow();
|
|
54
54
|
const viewport = useViewport();
|
|
55
|
+
const graphEdges = useMemo(() => deriveGraphEdges({ relations, walkthroughs }), [relations, walkthroughs]);
|
|
55
56
|
const [built, setBuilt] = useState({
|
|
56
57
|
nodes: [],
|
|
57
58
|
edges: [],
|
|
58
59
|
});
|
|
59
60
|
const [layoutReady, setLayoutReady] = useState(false);
|
|
60
61
|
const [selected, setSelected] = useState(null);
|
|
61
|
-
/** Bottom drawer: single file or
|
|
62
|
+
/** Bottom drawer: single file or walkthrough multi-snippet mode. */
|
|
62
63
|
const [drawerTarget, setDrawerTarget] = useState(null);
|
|
63
64
|
// Mirror so step-focus can decide whether to wait for the drawer open
|
|
64
65
|
// transition before fitView (avoids framing against full-height canvas).
|
|
@@ -68,22 +69,22 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
68
69
|
const pendingFocusFitRef = useRef(null);
|
|
69
70
|
// Component the pointer is over (null on leave) → transient tree highlight.
|
|
70
71
|
const [hoveredComponentId, setHoveredComponentId] = useState(null);
|
|
71
|
-
//
|
|
72
|
+
// Walkthrough focus — selected flow (or step) is full strength; other
|
|
72
73
|
// opened-flow members stay visible but dimmed; everything else is hidden.
|
|
73
|
-
const [
|
|
74
|
+
const [focusedWalkthroughId, setFocusedWalkthroughId] = useState(null);
|
|
74
75
|
// `null` = whole flow focused; a number = that single step's edge focused.
|
|
75
76
|
const [focusedStepIndex, setFocusedStepIndex] = useState(null);
|
|
76
|
-
// Hovered
|
|
77
|
+
// Hovered walkthrough in the flows panel: dims every canvas node/edge not
|
|
77
78
|
// involved in the hover preview (or selected ∪ hovered when a step is
|
|
78
79
|
// focused). `stepIndex: null` = whole flow (collapsed title hover);
|
|
79
80
|
// a number = that step. Transient — no drawer. Camera only reframes when
|
|
80
81
|
// a step is already selected.
|
|
81
|
-
const [
|
|
82
|
-
// Sidebar bottom half: which panel is shown when
|
|
83
|
-
const [sidebarView, setSidebarView] = useState(() =>
|
|
84
|
-
//
|
|
82
|
+
const [hoveredWalkthroughStep, setHoveredWalkthroughStep] = useState(null);
|
|
83
|
+
// Sidebar bottom half: which panel is shown when walkthroughs exist.
|
|
84
|
+
const [sidebarView, setSidebarView] = useState(() => walkthroughs?.length ? 'walkthroughs' : 'files');
|
|
85
|
+
// Walkthrough flows the user has expanded (via the title row). Closed by
|
|
85
86
|
// default so a graph with several flows doesn't dump every step list at once.
|
|
86
|
-
const [
|
|
87
|
+
const [expandedWalkthroughs, setExpandedWalkthroughs] = useState(new Set());
|
|
87
88
|
// Sidebar description visibility. Hidden by default so the files/flows
|
|
88
89
|
// panel gets the vertical room; the title-row toggle reveals it.
|
|
89
90
|
const [descriptionVisible, setDescriptionVisible] = useState(false);
|
|
@@ -137,40 +138,40 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
137
138
|
drawerTarget?.kind === 'file'
|
|
138
139
|
? { file: drawerTarget.file, startLine: drawerTarget.startLine }
|
|
139
140
|
: null;
|
|
140
|
-
const
|
|
141
|
-
if (drawerTarget?.kind !== '
|
|
141
|
+
const focusedWalkthrough = useMemo(() => {
|
|
142
|
+
if (drawerTarget?.kind !== 'walkthrough' || !walkthroughs)
|
|
142
143
|
return null;
|
|
143
|
-
return
|
|
144
|
-
}, [drawerTarget,
|
|
145
|
-
//
|
|
146
|
-
const
|
|
147
|
-
if (!
|
|
144
|
+
return walkthroughs.find((t) => t.id === drawerTarget.walkthroughId) ?? null;
|
|
145
|
+
}, [drawerTarget, walkthroughs]);
|
|
146
|
+
// Walkthrough shown on the canvas title chip (focus or hover/autoplay highlight).
|
|
147
|
+
const overlayWalkthroughTitle = useMemo(() => {
|
|
148
|
+
if (!showWalkthroughTitle || !walkthroughs?.length)
|
|
148
149
|
return null;
|
|
149
|
-
const id =
|
|
150
|
+
const id = focusedWalkthroughId ?? hoveredWalkthroughStep?.walkthroughId;
|
|
150
151
|
if (!id)
|
|
151
152
|
return null;
|
|
152
|
-
return
|
|
153
|
+
return walkthroughs.find((t) => t.id === id)?.title ?? null;
|
|
153
154
|
}, [
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
showWalkthroughTitle,
|
|
156
|
+
walkthroughs,
|
|
157
|
+
focusedWalkthroughId,
|
|
158
|
+
hoveredWalkthroughStep,
|
|
158
159
|
]);
|
|
159
160
|
// Active step for the bottom-of-title progress + annotation chip.
|
|
160
|
-
const
|
|
161
|
-
if (!
|
|
161
|
+
const overlayWalkthroughStep = useMemo(() => {
|
|
162
|
+
if (!showWalkthroughTitle || !walkthroughs?.length)
|
|
162
163
|
return null;
|
|
163
|
-
const tlId =
|
|
164
|
+
const tlId = focusedWalkthroughId ?? hoveredWalkthroughStep?.walkthroughId ?? null;
|
|
164
165
|
let stepIndex = null;
|
|
165
|
-
if (
|
|
166
|
+
if (focusedWalkthroughId != null) {
|
|
166
167
|
stepIndex = focusedStepIndex;
|
|
167
168
|
}
|
|
168
|
-
else if (
|
|
169
|
-
stepIndex =
|
|
169
|
+
else if (hoveredWalkthroughStep != null) {
|
|
170
|
+
stepIndex = hoveredWalkthroughStep.stepIndex;
|
|
170
171
|
}
|
|
171
172
|
if (tlId == null || stepIndex == null)
|
|
172
173
|
return null;
|
|
173
|
-
const tl =
|
|
174
|
+
const tl = walkthroughs.find((t) => t.id === tlId);
|
|
174
175
|
const step = tl?.steps[stepIndex];
|
|
175
176
|
if (!step || !tl)
|
|
176
177
|
return null;
|
|
@@ -180,18 +181,18 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
180
181
|
annotation: step.annotation,
|
|
181
182
|
};
|
|
182
183
|
}, [
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
showWalkthroughTitle,
|
|
185
|
+
walkthroughs,
|
|
186
|
+
focusedWalkthroughId,
|
|
186
187
|
focusedStepIndex,
|
|
187
|
-
|
|
188
|
+
hoveredWalkthroughStep,
|
|
188
189
|
]);
|
|
189
190
|
const drawerTitle = useMemo(() => {
|
|
190
191
|
if (!drawerTarget)
|
|
191
192
|
return null;
|
|
192
193
|
if (drawerTarget.kind === 'file')
|
|
193
194
|
return drawerTarget.file;
|
|
194
|
-
const tl =
|
|
195
|
+
const tl = focusedWalkthrough;
|
|
195
196
|
if (!tl)
|
|
196
197
|
return null;
|
|
197
198
|
if (drawerTarget.stepIndex == null)
|
|
@@ -201,7 +202,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
201
202
|
return tl.title;
|
|
202
203
|
const site = `${step.file.split('/').pop() ?? step.file}:${step.line}`;
|
|
203
204
|
return `${tl.title} · ${site}`;
|
|
204
|
-
}, [drawerTarget,
|
|
205
|
+
}, [drawerTarget, focusedWalkthrough]);
|
|
205
206
|
// Refresh selected component when the components list updates (e.g. verify
|
|
206
207
|
// writes back declarationRef).
|
|
207
208
|
useEffect(() => {
|
|
@@ -214,11 +215,13 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
214
215
|
// Pass 1: build with estimated widths so React Flow can measure the DOM.
|
|
215
216
|
// The pane stays hidden until Pass 2 completes. Key off layout-affecting
|
|
216
217
|
// fields only — declarationRef updates after verify must not re-run ELK.
|
|
217
|
-
const layoutKey = useMemo(() => subsystemGraphLayoutKey({ components,
|
|
218
|
+
const layoutKey = useMemo(() => subsystemGraphLayoutKey({ components, relations, walkthroughs }), [components, relations, walkthroughs]);
|
|
218
219
|
const componentsRef = useRef(components);
|
|
219
|
-
const
|
|
220
|
+
const relationsRef = useRef(relations);
|
|
221
|
+
const walkthroughsRef = useRef(walkthroughs);
|
|
220
222
|
componentsRef.current = components;
|
|
221
|
-
|
|
223
|
+
relationsRef.current = relations;
|
|
224
|
+
walkthroughsRef.current = walkthroughs;
|
|
222
225
|
// Track measured dimensions from React Flow's dimension changes.
|
|
223
226
|
// These arrive as { type: 'dimensions', id, dimensions } in onNodesChange.
|
|
224
227
|
// Retained across same-id layoutKey updates so Pass 2 can run without a remount.
|
|
@@ -233,7 +236,11 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
233
236
|
prevMeasuredSigRef.current = '';
|
|
234
237
|
// Bump generation so any in-flight Pass 2 from the prior layoutKey is ignored.
|
|
235
238
|
pass2GenRef.current += 1;
|
|
236
|
-
const doc = {
|
|
239
|
+
const doc = {
|
|
240
|
+
components: componentsRef.current,
|
|
241
|
+
relations: relationsRef.current,
|
|
242
|
+
walkthroughs: walkthroughsRef.current,
|
|
243
|
+
};
|
|
237
244
|
void buildSubsystemGraph(doc, { maxNodeWidth, showEdgeLabels })
|
|
238
245
|
.then(({ nodes, edges: e }) => {
|
|
239
246
|
if (!alive)
|
|
@@ -284,7 +291,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
284
291
|
const measuredWidths = new Map(leafNodes.map((n) => [n.id, dims.get(n.id).width]));
|
|
285
292
|
const measuredHeights = new Map(leafNodes.map((n) => [n.id, dims.get(n.id).height]));
|
|
286
293
|
const gen = ++pass2GenRef.current;
|
|
287
|
-
void buildSubsystemGraph({ components,
|
|
294
|
+
void buildSubsystemGraph({ components, relations, walkthroughs }, { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights })
|
|
288
295
|
.then(({ nodes, edges: e }) => {
|
|
289
296
|
if (gen !== pass2GenRef.current)
|
|
290
297
|
return;
|
|
@@ -298,7 +305,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
298
305
|
// Reveal Pass 1 layout rather than leaving the cover up forever.
|
|
299
306
|
setLayoutReady(true);
|
|
300
307
|
});
|
|
301
|
-
}, [built.nodes, components,
|
|
308
|
+
}, [built.nodes, components, relations, walkthroughs, maxNodeWidth, showEdgeLabels]);
|
|
302
309
|
// After Pass 1 commits, try Pass 2 immediately with retained measurements.
|
|
303
310
|
// Same-id live updates often get no new React Flow `dimensions` events, so
|
|
304
311
|
// waiting only on onNodesChange leaves the cover stuck.
|
|
@@ -329,7 +336,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
329
336
|
// Node components call SUBSYSTEM_CALLBACKS.onSelect on click (their inner
|
|
330
337
|
// onClick stops React Flow propagation), so wire selection + width through it.
|
|
331
338
|
// Lookup from edge id → the authored SubsystemComponentEdge (for onEdgeSelect).
|
|
332
|
-
const edgeById = useMemo(() => new Map(
|
|
339
|
+
const edgeById = useMemo(() => new Map(graphEdges.map((e) => [e.id, e])), [graphEdges]);
|
|
333
340
|
// Shared edge-selection logic used by both React Flow's onEdgeClick and the
|
|
334
341
|
// clickable edge label (SUBSYSTEM_CALLBACKS.onEdgeSelect).
|
|
335
342
|
const selectEdge = useCallback((edgeId) => {
|
|
@@ -339,8 +346,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
339
346
|
}
|
|
340
347
|
const src = edgeById.get(edgeId);
|
|
341
348
|
setSelectedEdgeId(edgeId);
|
|
342
|
-
// A direct edge selection on the canvas supersedes any
|
|
343
|
-
|
|
349
|
+
// A direct edge selection on the canvas supersedes any walkthrough focus.
|
|
350
|
+
setFocusedWalkthroughId(null);
|
|
344
351
|
setFocusedStepIndex(null);
|
|
345
352
|
if (src)
|
|
346
353
|
onEdgeSelect?.(src);
|
|
@@ -358,7 +365,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
358
365
|
}
|
|
359
366
|
setSelected(comp);
|
|
360
367
|
setSelectedEdgeId(null);
|
|
361
|
-
|
|
368
|
+
setFocusedWalkthroughId(null);
|
|
362
369
|
setFocusedStepIndex(null);
|
|
363
370
|
onSelect?.(id);
|
|
364
371
|
}
|
|
@@ -382,50 +389,50 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
382
389
|
// without a stale closure value.
|
|
383
390
|
const selectedEdgeIdRef = useRef(null);
|
|
384
391
|
selectedEdgeIdRef.current = selectedEdgeId;
|
|
385
|
-
// Edge ids in
|
|
386
|
-
// step's edge). Used to frame the camera. `null` = no
|
|
392
|
+
// Edge ids in walkthrough focus (an active flow's edge set, or a single
|
|
393
|
+
// step's edge). Used to frame the camera. `null` = no walkthrough focus.
|
|
387
394
|
const focusEdgeIds = useMemo(() => {
|
|
388
|
-
if (
|
|
395
|
+
if (focusedWalkthroughId == null || !walkthroughs)
|
|
389
396
|
return null;
|
|
390
|
-
const tl =
|
|
397
|
+
const tl = walkthroughs.find((t) => t.id === focusedWalkthroughId);
|
|
391
398
|
if (!tl)
|
|
392
399
|
return null;
|
|
393
400
|
if (focusedStepIndex != null) {
|
|
394
401
|
const step = tl.steps[focusedStepIndex];
|
|
395
|
-
return step ? new Set([step
|
|
402
|
+
return step ? new Set([walkthroughStepGraphEdgeId(step)]) : null;
|
|
396
403
|
}
|
|
397
|
-
return new Set(tl.steps.map((s) => s
|
|
398
|
-
}, [
|
|
404
|
+
return new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s)));
|
|
405
|
+
}, [walkthroughs, focusedWalkthroughId, focusedStepIndex]);
|
|
399
406
|
// 1-based step numbers per edge of the active flow (focused or
|
|
400
407
|
// hover/autoplay-highlighted). An edge can appear in more than one step.
|
|
401
408
|
const selectedFlowStepNos = useMemo(() => {
|
|
402
|
-
const activeId =
|
|
403
|
-
if (activeId == null || !
|
|
409
|
+
const activeId = focusedWalkthroughId ?? hoveredWalkthroughStep?.walkthroughId;
|
|
410
|
+
if (activeId == null || !walkthroughs)
|
|
404
411
|
return null;
|
|
405
|
-
const tl =
|
|
412
|
+
const tl = walkthroughs.find((t) => t.id === activeId);
|
|
406
413
|
if (!tl)
|
|
407
414
|
return null;
|
|
408
415
|
const map = new Map();
|
|
409
416
|
tl.steps.forEach((s, i) => {
|
|
410
|
-
const list = map.get(s
|
|
417
|
+
const list = map.get(walkthroughStepGraphEdgeId(s)) ?? [];
|
|
411
418
|
list.push(i + 1);
|
|
412
|
-
map.set(s
|
|
419
|
+
map.set(walkthroughStepGraphEdgeId(s), list);
|
|
413
420
|
});
|
|
414
421
|
return map;
|
|
415
|
-
}, [
|
|
416
|
-
// Union of every expanded (opened)
|
|
422
|
+
}, [walkthroughs, focusedWalkthroughId, hoveredWalkthroughStep]);
|
|
423
|
+
// Union of every expanded (opened) walkthrough's edges — the visible set.
|
|
417
424
|
const openedEdgeIds = useMemo(() => {
|
|
418
|
-
if (!
|
|
425
|
+
if (!walkthroughs || expandedWalkthroughs.size === 0)
|
|
419
426
|
return null;
|
|
420
427
|
const ids = new Set();
|
|
421
|
-
for (const tl of
|
|
422
|
-
if (!
|
|
428
|
+
for (const tl of walkthroughs) {
|
|
429
|
+
if (!expandedWalkthroughs.has(tl.id))
|
|
423
430
|
continue;
|
|
424
431
|
for (const s of tl.steps)
|
|
425
|
-
ids.add(s
|
|
432
|
+
ids.add(walkthroughStepGraphEdgeId(s));
|
|
426
433
|
}
|
|
427
434
|
return ids.size > 0 ? ids : null;
|
|
428
|
-
}, [
|
|
435
|
+
}, [walkthroughs, expandedWalkthroughs]);
|
|
429
436
|
const endpointsOf = (edgeIds) => {
|
|
430
437
|
if (!edgeIds)
|
|
431
438
|
return null;
|
|
@@ -440,17 +447,17 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
440
447
|
};
|
|
441
448
|
// Edge/nodes involved in the hovered step or whole flow.
|
|
442
449
|
const hoverEdgeIds = useMemo(() => {
|
|
443
|
-
if (!
|
|
450
|
+
if (!hoveredWalkthroughStep || !walkthroughs)
|
|
444
451
|
return null;
|
|
445
|
-
const tl =
|
|
452
|
+
const tl = walkthroughs.find((t) => t.id === hoveredWalkthroughStep.walkthroughId);
|
|
446
453
|
if (!tl)
|
|
447
454
|
return null;
|
|
448
|
-
if (
|
|
449
|
-
return new Set(tl.steps.map((s) => s
|
|
455
|
+
if (hoveredWalkthroughStep.stepIndex == null) {
|
|
456
|
+
return new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s)));
|
|
450
457
|
}
|
|
451
|
-
const step = tl.steps[
|
|
452
|
-
return step ? new Set([step
|
|
453
|
-
}, [
|
|
458
|
+
const step = tl.steps[hoveredWalkthroughStep.stepIndex];
|
|
459
|
+
return step ? new Set([walkthroughStepGraphEdgeId(step)]) : null;
|
|
460
|
+
}, [walkthroughs, hoveredWalkthroughStep]);
|
|
454
461
|
// While hovering with a *step* already selected, brighten the union of
|
|
455
462
|
// selected + hovered participants. Whole-flow focus (or no focus) keeps
|
|
456
463
|
// the old hover-replace preview so a step hover still dims the rest.
|
|
@@ -497,8 +504,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
497
504
|
// React Flow's own selection state from updating.
|
|
498
505
|
const dispNodes = useMemo(() => {
|
|
499
506
|
// While a step/flow is hovered, selected + hovered participants stay
|
|
500
|
-
// bright; everything else among the currently-visible (opened-
|
|
501
|
-
// nodes is dimmed. Nodes outside the opened-
|
|
507
|
+
// bright; everything else among the currently-visible (opened-walkthrough)
|
|
508
|
+
// nodes is dimmed. Nodes outside the opened-walkthrough set stay hidden.
|
|
502
509
|
return xyflowNodesBase.map((n) => {
|
|
503
510
|
// Boundary frames follow their members: hidden when no member is
|
|
504
511
|
// visible, dimmed when members are dimmed. Never selectable.
|
|
@@ -613,7 +620,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
613
620
|
const onNodeClick = useCallback((_e, node) => {
|
|
614
621
|
const comp = node.data?.component;
|
|
615
622
|
setSelectedEdgeId(null);
|
|
616
|
-
|
|
623
|
+
setFocusedWalkthroughId(null);
|
|
617
624
|
setFocusedStepIndex(null);
|
|
618
625
|
if (node.type === 'subsystem-component' && comp) {
|
|
619
626
|
// Clicking the already-selected node unselects it (toggle off).
|
|
@@ -634,13 +641,13 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
634
641
|
const onPaneClick = useCallback(() => {
|
|
635
642
|
setSelected(null);
|
|
636
643
|
setSelectedEdgeId(null);
|
|
637
|
-
|
|
644
|
+
setFocusedWalkthroughId(null);
|
|
638
645
|
setFocusedStepIndex(null);
|
|
639
646
|
}, []);
|
|
640
647
|
// Sidebar file trees — one per repo on multi-repo graphs, each under its
|
|
641
648
|
// own owner-avatar header. Clicking a header collapses that repo's tree.
|
|
642
649
|
const repoGroups = useMemo(() => buildRepoGroups(components), [components]);
|
|
643
|
-
const
|
|
650
|
+
const hasWalkthroughs = useMemo(() => (walkthroughs?.length ?? 0) > 0, [walkthroughs]);
|
|
644
651
|
const [collapsedRepos, setCollapsedRepos] = useState(new Set());
|
|
645
652
|
const toggleRepoCollapsed = useCallback((key) => {
|
|
646
653
|
setCollapsedRepos((prev) => {
|
|
@@ -671,12 +678,12 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
671
678
|
setDrawerTarget({ kind: 'file', file, startLine });
|
|
672
679
|
onFileSelect?.(file);
|
|
673
680
|
}, [onFileSelect]);
|
|
674
|
-
// Camera helper shared by the
|
|
681
|
+
// Camera helper shared by the walkthrough interactions: frames the focused
|
|
675
682
|
// edges' endpoint nodes via `fitView({ nodes })`, which uses the store's
|
|
676
683
|
// live positions + measured dims — so the frame always includes BOTH
|
|
677
684
|
// components the edge attaches to (and therefore the edge line between them).
|
|
678
685
|
const fitFocusBounds = useCallback((ids) => {
|
|
679
|
-
if (!
|
|
686
|
+
if (!zoomOnWalkthroughFocus)
|
|
680
687
|
return;
|
|
681
688
|
const nodeIds = new Set();
|
|
682
689
|
for (const e of baseEdges) {
|
|
@@ -692,11 +699,11 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
692
699
|
padding: 0.25,
|
|
693
700
|
duration: 300,
|
|
694
701
|
});
|
|
695
|
-
}, [baseEdges, fitView,
|
|
696
|
-
// Zoom back out to the full diagram after the last expanded
|
|
702
|
+
}, [baseEdges, fitView, zoomOnWalkthroughFocus]);
|
|
703
|
+
// Zoom back out to the full diagram after the last expanded walkthrough
|
|
697
704
|
// closes (visibility restores non-flow nodes that were hidden).
|
|
698
705
|
const fitOverview = useCallback(() => {
|
|
699
|
-
if (!
|
|
706
|
+
if (!zoomOnWalkthroughFocus)
|
|
700
707
|
return;
|
|
701
708
|
fitView({
|
|
702
709
|
padding: 0.1,
|
|
@@ -705,62 +712,77 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
705
712
|
maxZoom: 2,
|
|
706
713
|
duration: 300,
|
|
707
714
|
});
|
|
708
|
-
}, [fitView,
|
|
715
|
+
}, [fitView, zoomOnWalkthroughFocus]);
|
|
709
716
|
// Focus an entire flow: hide everything but the flow's nodes and edges, and
|
|
710
717
|
// frame the flow on the canvas. Selection state is cleared — the graph now
|
|
711
718
|
// reads as the narrative. No drawer: the code view only opens on a step
|
|
712
|
-
// click. A stale
|
|
719
|
+
// click. A stale walkthrough drawer (from a previously focused flow's step)
|
|
713
720
|
// closes; an explicitly opened file drawer stays.
|
|
714
721
|
// In `dim` mode: clear step highlight and leave the full graph visible
|
|
715
722
|
// (no zoom / hide) — matching "step away from a hovered step".
|
|
716
|
-
const
|
|
723
|
+
const focusWalkthroughEdges = useCallback((tl) => {
|
|
717
724
|
setSelected(null);
|
|
718
725
|
setSelectedEdgeId(null);
|
|
719
|
-
|
|
720
|
-
if (
|
|
726
|
+
setHoveredWalkthroughStep(null);
|
|
727
|
+
if (walkthroughStepMode === 'dim') {
|
|
721
728
|
setFocusedStepIndex(null);
|
|
722
|
-
|
|
723
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
729
|
+
setFocusedWalkthroughId(null);
|
|
730
|
+
setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
|
|
724
731
|
return;
|
|
725
732
|
}
|
|
726
733
|
setFocusedStepIndex(null);
|
|
727
|
-
|
|
728
|
-
fitFocusBounds(new Set(tl.steps.map((s) => s
|
|
729
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
730
|
-
}, [fitFocusBounds,
|
|
731
|
-
const
|
|
732
|
-
|
|
734
|
+
setFocusedWalkthroughId(tl.id);
|
|
735
|
+
fitFocusBounds(new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s))));
|
|
736
|
+
setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
|
|
737
|
+
}, [fitFocusBounds, walkthroughStepMode]);
|
|
738
|
+
const clearWalkthroughFocus = useCallback(() => {
|
|
739
|
+
setFocusedWalkthroughId(null);
|
|
733
740
|
setFocusedStepIndex(null);
|
|
734
|
-
|
|
735
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
741
|
+
setHoveredWalkthroughStep(null);
|
|
742
|
+
setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
|
|
736
743
|
}, []);
|
|
737
|
-
// Focus a single step's edge on the canvas and open/scroll the
|
|
738
|
-
// drawer to that step's snippet.
|
|
744
|
+
// Focus a single step's edge on the canvas and open/scroll the walkthrough
|
|
745
|
+
// drawer to that step's snippet. Clicking the already-focused step clears
|
|
746
|
+
// step focus and returns to whole-flow (high-level) framing.
|
|
739
747
|
// In `dim` mode: only dim non-participants (same as hovering a step) —
|
|
740
748
|
// no camera move, no drawer, no hide.
|
|
741
|
-
const
|
|
749
|
+
const focusWalkthroughStep = useCallback((tl, stepIndex) => {
|
|
742
750
|
const step = tl.steps[stepIndex];
|
|
743
751
|
if (!step)
|
|
744
752
|
return;
|
|
745
753
|
setSelected(null);
|
|
746
754
|
setSelectedEdgeId(null);
|
|
747
|
-
if (
|
|
755
|
+
if (walkthroughStepMode === 'dim') {
|
|
756
|
+
setFocusedStepIndex(null);
|
|
757
|
+
setFocusedWalkthroughId(null);
|
|
758
|
+
setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex });
|
|
759
|
+
setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
// Toggle: clicking the selected step unselects it (back to whole flow).
|
|
763
|
+
if (focusedWalkthroughId === tl.id && focusedStepIndex === stepIndex) {
|
|
748
764
|
setFocusedStepIndex(null);
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
765
|
+
// Pointer is still over the row — keep hover preview so dimming doesn't
|
|
766
|
+
// flash off until mouseleave.
|
|
767
|
+
setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex });
|
|
768
|
+
setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
|
|
769
|
+
if (pendingFocusFitRef.current != null) {
|
|
770
|
+
window.clearTimeout(pendingFocusFitRef.current);
|
|
771
|
+
pendingFocusFitRef.current = null;
|
|
772
|
+
}
|
|
773
|
+
fitFocusBounds(new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s))));
|
|
752
774
|
return;
|
|
753
775
|
}
|
|
754
776
|
setFocusedStepIndex(stepIndex);
|
|
755
|
-
|
|
756
|
-
|
|
777
|
+
setFocusedWalkthroughId(tl.id);
|
|
778
|
+
setHoveredWalkthroughStep(null);
|
|
757
779
|
// Open the drawer before fitting. If it was closed, wait for its height
|
|
758
780
|
// transition so fitView uses the reduced canvas — not full height.
|
|
759
781
|
const drawerWasOpen = drawerOpenRef.current;
|
|
760
|
-
if (
|
|
782
|
+
if (renderWalkthroughViewer) {
|
|
761
783
|
setDrawerTarget({
|
|
762
|
-
kind: '
|
|
763
|
-
|
|
784
|
+
kind: 'walkthrough',
|
|
785
|
+
walkthroughId: tl.id,
|
|
764
786
|
stepIndex,
|
|
765
787
|
});
|
|
766
788
|
}
|
|
@@ -771,7 +793,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
771
793
|
startLine: step.line,
|
|
772
794
|
});
|
|
773
795
|
}
|
|
774
|
-
const edgeIds = new Set([step
|
|
796
|
+
const edgeIds = new Set([walkthroughStepGraphEdgeId(step)]);
|
|
775
797
|
if (pendingFocusFitRef.current != null) {
|
|
776
798
|
window.clearTimeout(pendingFocusFitRef.current);
|
|
777
799
|
pendingFocusFitRef.current = null;
|
|
@@ -785,7 +807,13 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
785
807
|
requestAnimationFrame(() => fitFocusBounds(edgeIds));
|
|
786
808
|
}, FILE_DRAWER_HEIGHT_MS + 20);
|
|
787
809
|
}
|
|
788
|
-
}, [
|
|
810
|
+
}, [
|
|
811
|
+
fitFocusBounds,
|
|
812
|
+
focusedStepIndex,
|
|
813
|
+
focusedWalkthroughId,
|
|
814
|
+
renderWalkthroughViewer,
|
|
815
|
+
walkthroughStepMode,
|
|
816
|
+
]);
|
|
789
817
|
useEffect(() => {
|
|
790
818
|
return () => {
|
|
791
819
|
if (pendingFocusFitRef.current != null) {
|
|
@@ -802,10 +830,10 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
802
830
|
useEffect(() => {
|
|
803
831
|
const prev = prevPreviewEdgeIdsRef.current;
|
|
804
832
|
prevPreviewEdgeIdsRef.current = previewEdgeIds;
|
|
805
|
-
if (!
|
|
833
|
+
if (!zoomOnWalkthroughFocus || walkthroughStepMode === 'dim')
|
|
806
834
|
return;
|
|
807
835
|
// Camera follows hover only when a specific step is already focused.
|
|
808
|
-
if (
|
|
836
|
+
if (focusedWalkthroughId == null || focusedStepIndex == null)
|
|
809
837
|
return;
|
|
810
838
|
if (previewEdgeIds) {
|
|
811
839
|
fitFocusBounds(previewEdgeIds);
|
|
@@ -817,33 +845,33 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
817
845
|
}, [
|
|
818
846
|
previewEdgeIds,
|
|
819
847
|
focusEdgeIds,
|
|
820
|
-
|
|
848
|
+
focusedWalkthroughId,
|
|
821
849
|
focusedStepIndex,
|
|
822
850
|
fitFocusBounds,
|
|
823
|
-
|
|
824
|
-
|
|
851
|
+
zoomOnWalkthroughFocus,
|
|
852
|
+
walkthroughStepMode,
|
|
825
853
|
]);
|
|
826
|
-
// Graph-only embeds: cycle
|
|
854
|
+
// Graph-only embeds: cycle walkthrough steps with hover-style dimming.
|
|
827
855
|
useEffect(() => {
|
|
828
|
-
if (!
|
|
856
|
+
if (!autoPlayWalkthroughs || !walkthroughs?.length || !layoutReady)
|
|
829
857
|
return;
|
|
830
|
-
const playable =
|
|
858
|
+
const playable = walkthroughs.filter((tl) => tl.steps.length > 0);
|
|
831
859
|
if (playable.length === 0)
|
|
832
860
|
return;
|
|
833
861
|
let cancelled = false;
|
|
834
862
|
let tlIdx = 0;
|
|
835
863
|
let stepIdx = 0;
|
|
836
864
|
let timer = null;
|
|
837
|
-
const interval = Math.max(400,
|
|
865
|
+
const interval = Math.max(400, walkthroughAutoPlayIntervalMs);
|
|
838
866
|
const tick = () => {
|
|
839
867
|
if (cancelled)
|
|
840
868
|
return;
|
|
841
869
|
const tl = playable[tlIdx];
|
|
842
870
|
setSelected(null);
|
|
843
871
|
setSelectedEdgeId(null);
|
|
844
|
-
|
|
872
|
+
setFocusedWalkthroughId(null);
|
|
845
873
|
setFocusedStepIndex(null);
|
|
846
|
-
|
|
874
|
+
setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex: stepIdx });
|
|
847
875
|
stepIdx += 1;
|
|
848
876
|
if (stepIdx >= tl.steps.length) {
|
|
849
877
|
stepIdx = 0;
|
|
@@ -856,16 +884,16 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
856
884
|
cancelled = true;
|
|
857
885
|
if (timer != null)
|
|
858
886
|
window.clearTimeout(timer);
|
|
859
|
-
|
|
887
|
+
setHoveredWalkthroughStep(null);
|
|
860
888
|
};
|
|
861
|
-
}, [
|
|
862
|
-
// Arrow keys step through the focused
|
|
889
|
+
}, [autoPlayWalkthroughs, walkthroughs, walkthroughAutoPlayIntervalMs, layoutReady]);
|
|
890
|
+
// Arrow keys step through the focused walkthrough once a step is active
|
|
863
891
|
// (sidebar click or drawer open). Ignores typing targets and chords.
|
|
864
892
|
useEffect(() => {
|
|
865
|
-
if (
|
|
893
|
+
if (focusedWalkthroughId == null || focusedStepIndex == null || !walkthroughs) {
|
|
866
894
|
return;
|
|
867
895
|
}
|
|
868
|
-
const tl =
|
|
896
|
+
const tl = walkthroughs.find((t) => t.id === focusedWalkthroughId);
|
|
869
897
|
if (!tl || tl.steps.length === 0)
|
|
870
898
|
return;
|
|
871
899
|
const onKeyDown = (e) => {
|
|
@@ -892,19 +920,19 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
892
920
|
if (next === focusedStepIndex)
|
|
893
921
|
return;
|
|
894
922
|
e.preventDefault();
|
|
895
|
-
|
|
923
|
+
focusWalkthroughStep(tl, next);
|
|
896
924
|
};
|
|
897
925
|
window.addEventListener('keydown', onKeyDown);
|
|
898
926
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
899
927
|
}, [
|
|
900
|
-
|
|
928
|
+
focusedWalkthroughId,
|
|
901
929
|
focusedStepIndex,
|
|
902
|
-
|
|
903
|
-
|
|
930
|
+
walkthroughs,
|
|
931
|
+
focusWalkthroughStep,
|
|
904
932
|
]);
|
|
905
|
-
const
|
|
906
|
-
const collapsingLast =
|
|
907
|
-
|
|
933
|
+
const toggleWalkthroughCollapsed = useCallback((tlId) => {
|
|
934
|
+
const collapsingLast = expandedWalkthroughs.has(tlId) && expandedWalkthroughs.size === 1;
|
|
935
|
+
setExpandedWalkthroughs((prev) => {
|
|
908
936
|
const next = new Set(prev);
|
|
909
937
|
if (next.has(tlId))
|
|
910
938
|
next.delete(tlId);
|
|
@@ -912,7 +940,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
912
940
|
next.add(tlId);
|
|
913
941
|
return next;
|
|
914
942
|
});
|
|
915
|
-
if (!collapsingLast || !
|
|
943
|
+
if (!collapsingLast || !zoomOnWalkthroughFocus)
|
|
916
944
|
return;
|
|
917
945
|
// Cancel any pending focus fit so it doesn't fight the overview zoom.
|
|
918
946
|
if (pendingFocusFitRef.current != null) {
|
|
@@ -927,7 +955,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
927
955
|
pendingFocusFitRef.current = null;
|
|
928
956
|
requestAnimationFrame(() => fitOverview());
|
|
929
957
|
}, drawerWasOpen ? FILE_DRAWER_HEIGHT_MS + 20 : 20);
|
|
930
|
-
}, [
|
|
958
|
+
}, [expandedWalkthroughs, fitOverview, zoomOnWalkthroughFocus]);
|
|
931
959
|
// Filename-badge clicks on nodes open the drawer through the same path as
|
|
932
960
|
// the declaration panel's file link (toggle + tree sync, no start line).
|
|
933
961
|
useEffect(() => {
|
|
@@ -952,7 +980,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
952
980
|
return;
|
|
953
981
|
setSelected(comp);
|
|
954
982
|
setSelectedEdgeId(null);
|
|
955
|
-
|
|
983
|
+
setFocusedWalkthroughId(null);
|
|
956
984
|
setFocusedStepIndex(null);
|
|
957
985
|
onSelect?.(comp.id);
|
|
958
986
|
}, [components, onSelect]);
|
|
@@ -1000,10 +1028,10 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1000
1028
|
const fileViewerRef = useRef(fileViewer);
|
|
1001
1029
|
fileViewerRef.current = fileViewer;
|
|
1002
1030
|
const renderDrawerContent = useCallback((file, opts) => fileViewerRef.current?.(file, opts) ?? null, []);
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
const
|
|
1006
|
-
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 ||
|
|
1031
|
+
const walkthroughViewerRef = useRef(renderWalkthroughViewer);
|
|
1032
|
+
walkthroughViewerRef.current = renderWalkthroughViewer;
|
|
1033
|
+
const renderWalkthroughDrawerContent = useCallback((ctx) => walkthroughViewerRef.current?.(ctx) ?? null, []);
|
|
1034
|
+
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 || hasWalkthroughs) && (_jsxs("div", { style: {
|
|
1007
1035
|
width: sidebarWidth,
|
|
1008
1036
|
minWidth: sidebarWidth,
|
|
1009
1037
|
borderRight: `1px solid ${theme.colors.border}`,
|
|
@@ -1041,7 +1069,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1041
1069
|
color: descToggleHover ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
|
|
1042
1070
|
cursor: 'pointer',
|
|
1043
1071
|
transition: 'background 120ms ease, color 120ms ease',
|
|
1044
|
-
}, children: descriptionVisible ? _jsx(ChevronUp, { size: 14 }) : _jsx(ChevronDown, { size: 14 }) }))] })), description && descriptionVisible && (_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, disableBasePadding: true, fontSizeScale: 0.9, enableKeyboardScrolling: false, autoFocusOnVisible: false }) })), sidebarAfterDescription] }), (treeFilePaths.length > 0 ||
|
|
1072
|
+
}, children: descriptionVisible ? _jsx(ChevronUp, { size: 14 }) : _jsx(ChevronDown, { size: 14 }) }))] })), description && descriptionVisible && (_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, disableBasePadding: true, fontSizeScale: 0.9, enableKeyboardScrolling: false, autoFocusOnVisible: false }) })), sidebarAfterDescription] }), (treeFilePaths.length > 0 || hasWalkthroughs) && (_jsxs("div", { style: {
|
|
1045
1073
|
...(showDesc ? { height: '50%' } : { flex: 1, minHeight: 0 }),
|
|
1046
1074
|
minHeight: 160,
|
|
1047
1075
|
flexShrink: 0,
|
|
@@ -1049,13 +1077,13 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1049
1077
|
display: 'flex',
|
|
1050
1078
|
flexDirection: 'column',
|
|
1051
1079
|
overflow: 'hidden',
|
|
1052
|
-
}, children: [
|
|
1080
|
+
}, children: [hasWalkthroughs && (_jsx("div", { role: "tablist", "aria-label": "Sidebar view", style: {
|
|
1053
1081
|
display: 'flex',
|
|
1054
1082
|
width: '100%',
|
|
1055
1083
|
flexShrink: 0,
|
|
1056
1084
|
borderBottom: `1px solid ${theme.colors.border}`,
|
|
1057
1085
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
1058
|
-
}, children: ['
|
|
1086
|
+
}, children: ['walkthroughs', 'files'].map((view) => (_jsx("button", { type: "button", role: "tab", "aria-selected": sidebarView === view, onClick: () => setSidebarView(view), style: {
|
|
1059
1087
|
flex: 1,
|
|
1060
1088
|
minWidth: 0,
|
|
1061
1089
|
padding: '8px 8px',
|
|
@@ -1069,16 +1097,16 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1069
1097
|
fontFamily: theme.fonts.monospace,
|
|
1070
1098
|
textTransform: 'capitalize',
|
|
1071
1099
|
cursor: 'pointer',
|
|
1072
|
-
}, children: view }, view))) })), sidebarView === '
|
|
1100
|
+
}, children: view === 'walkthroughs' ? 'Walkthroughs' : 'Files' }, view))) })), sidebarView === 'walkthroughs' && walkthroughs && walkthroughs.length > 0 ? (_jsx("div", { style: {
|
|
1073
1101
|
flex: 1,
|
|
1074
1102
|
minHeight: 0,
|
|
1075
1103
|
overflowY: 'auto',
|
|
1076
1104
|
padding: '0 0 12px',
|
|
1077
|
-
}, children:
|
|
1105
|
+
}, children: walkthroughs.map((tl) => (_jsx(WalkthroughFlow, { walkthrough: tl, collapsed: !expandedWalkthroughs.has(tl.id), active: focusedWalkthroughId === tl.id
|
|
1078
1106
|
? { stepIndex: focusedStepIndex }
|
|
1079
|
-
:
|
|
1080
|
-
? { stepIndex:
|
|
1081
|
-
: null, onToggleCollapsed:
|
|
1107
|
+
: hoveredWalkthroughStep?.walkthroughId === tl.id
|
|
1108
|
+
? { stepIndex: hoveredWalkthroughStep.stepIndex }
|
|
1109
|
+
: null, onToggleCollapsed: toggleWalkthroughCollapsed, onFocusFlow: focusWalkthroughEdges, onClearFocus: clearWalkthroughFocus, onFocusStep: focusWalkthroughStep, onHoverStep: (tl, i) => setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex: i }), onHoverFlow: (tl) => setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex: null }), onLeaveStep: () => setHoveredWalkthroughStep(null) }, tl.id))) })) : treeFilePaths.length > 0 ? (_jsx(_Fragment, { children: repoGroups.groups.map((group, i) => {
|
|
1082
1110
|
const groupKey = group.repoKey ?? '__no-repo__';
|
|
1083
1111
|
const collapsed = collapsedRepos.has(groupKey);
|
|
1084
1112
|
return (_jsxs("div", { style: {
|
|
@@ -1088,7 +1116,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1088
1116
|
flexDirection: 'column',
|
|
1089
1117
|
borderTop: i > 0 ? `1px solid ${theme.colors.border}` : undefined,
|
|
1090
1118
|
}, 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));
|
|
1091
|
-
}) })) : null] }))] })), !hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 ||
|
|
1119
|
+
}) })) : null] }))] })), !hideSidebar && (title || description || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (_jsx("div", { onMouseDown: onSidebarResizeStart, "aria-label": "Resize sidebar", title: "Drag to resize", style: {
|
|
1092
1120
|
width: 3,
|
|
1093
1121
|
flexShrink: 0,
|
|
1094
1122
|
cursor: 'col-resize',
|
|
@@ -1151,7 +1179,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1151
1179
|
flex: 1,
|
|
1152
1180
|
minHeight: 0,
|
|
1153
1181
|
background: theme.colors.background,
|
|
1154
|
-
}, children: [_jsx(Background, { variant: BackgroundVariant.Dots, gap: 16, size: 1 }), _jsx(Controls, { showZoom: true, showFitView: true, showInteractive: true })] }, `${baseNodesKey}-${baseEdgesKey}`), (graphTitle ||
|
|
1182
|
+
}, children: [_jsx(Background, { variant: BackgroundVariant.Dots, gap: 16, size: 1 }), _jsx(Controls, { showZoom: true, showFitView: true, showInteractive: true })] }, `${baseNodesKey}-${baseEdgesKey}`), (graphTitle || overlayWalkthroughTitle || overlayWalkthroughStep) && (_jsxs("div", { style: {
|
|
1155
1183
|
position: 'absolute',
|
|
1156
1184
|
top: 10,
|
|
1157
1185
|
left: '50%',
|
|
@@ -1177,9 +1205,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1177
1205
|
border: `1px solid ${theme.colors.border}`,
|
|
1178
1206
|
borderRadius: 6,
|
|
1179
1207
|
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
1180
|
-
}, children: graphTitle })),
|
|
1208
|
+
}, children: graphTitle })), overlayWalkthroughTitle && (_jsxs("div", { style: {
|
|
1181
1209
|
maxWidth: '100%',
|
|
1182
|
-
minWidth:
|
|
1210
|
+
minWidth: overlayWalkthroughStep ? 160 : undefined,
|
|
1183
1211
|
display: 'flex',
|
|
1184
1212
|
flexDirection: 'column',
|
|
1185
1213
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
@@ -1188,9 +1216,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1188
1216
|
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
1189
1217
|
overflow: 'hidden',
|
|
1190
1218
|
opacity: 0.95,
|
|
1191
|
-
}, "aria-label":
|
|
1192
|
-
? `${
|
|
1193
|
-
:
|
|
1219
|
+
}, "aria-label": overlayWalkthroughStep
|
|
1220
|
+
? `${overlayWalkthroughTitle}, step ${overlayWalkthroughStep.index} of ${overlayWalkthroughStep.total}`
|
|
1221
|
+
: overlayWalkthroughTitle, children: [_jsx("div", { style: {
|
|
1194
1222
|
padding: '5px 14px',
|
|
1195
1223
|
fontSize: theme.fontSizes[2] ?? theme.fontSizes[1],
|
|
1196
1224
|
fontWeight: 600,
|
|
@@ -1200,14 +1228,14 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1200
1228
|
textOverflow: 'ellipsis',
|
|
1201
1229
|
whiteSpace: 'nowrap',
|
|
1202
1230
|
textAlign: 'center',
|
|
1203
|
-
}, children:
|
|
1231
|
+
}, children: overlayWalkthroughTitle }), overlayWalkthroughStep && overlayWalkthroughStep.total > 0 && (_jsx("div", { style: {
|
|
1204
1232
|
display: 'flex',
|
|
1205
1233
|
gap: 3,
|
|
1206
1234
|
padding: '0 6px 5px',
|
|
1207
|
-
}, "aria-hidden": "true", children: Array.from({ length:
|
|
1235
|
+
}, "aria-hidden": "true", children: Array.from({ length: overlayWalkthroughStep.total }, (_, i) => {
|
|
1208
1236
|
const n = i + 1;
|
|
1209
|
-
const active = n ===
|
|
1210
|
-
const done = n <
|
|
1237
|
+
const active = n === overlayWalkthroughStep.index;
|
|
1238
|
+
const done = n < overlayWalkthroughStep.index;
|
|
1211
1239
|
return (_jsx("span", { style: {
|
|
1212
1240
|
flex: 1,
|
|
1213
1241
|
height: 2,
|
|
@@ -1220,7 +1248,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1220
1248
|
opacity: active ? 1 : done ? 0.75 : 0.4,
|
|
1221
1249
|
transition: 'opacity 120ms ease, background 120ms ease',
|
|
1222
1250
|
} }, n));
|
|
1223
|
-
}) }))] })),
|
|
1251
|
+
}) }))] })), overlayWalkthroughStep?.annotation && (_jsx("div", { style: {
|
|
1224
1252
|
maxWidth: '100%',
|
|
1225
1253
|
padding: '6px 14px',
|
|
1226
1254
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
@@ -1232,7 +1260,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1232
1260
|
fontFamily: theme.fonts.body,
|
|
1233
1261
|
color: theme.colors.textMuted ?? theme.colors.textSecondary,
|
|
1234
1262
|
lineHeight: 1.35,
|
|
1235
|
-
}, children:
|
|
1263
|
+
}, children: overlayWalkthroughStep.annotation }))] })), selected && (_jsx("div", { style: {
|
|
1236
1264
|
position: 'absolute',
|
|
1237
1265
|
top: 10,
|
|
1238
1266
|
right: 10,
|
|
@@ -1244,9 +1272,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1244
1272
|
border: `1px solid ${theme.colors.border}`,
|
|
1245
1273
|
borderRadius: 8,
|
|
1246
1274
|
boxShadow: '0 8px 24px rgba(0,0,0,0.35)',
|
|
1247
|
-
}, children: _jsx(ComponentDeclaration, { component: selected, onOpenFile: onOpenDeclarationFile, onRelatedSelect: resolveRelatedComponent, onVerify: onVerifyComponent, verification: componentVerification }) }))] }), _jsx(FileDrawer, { title: drawerTitle, onClose: () => setDrawerTarget(null), children: drawerTarget?.kind === '
|
|
1248
|
-
|
|
1249
|
-
|
|
1275
|
+
}, children: _jsx(ComponentDeclaration, { component: selected, onOpenFile: onOpenDeclarationFile, onRelatedSelect: resolveRelatedComponent, onVerify: onVerifyComponent, verification: componentVerification }) }))] }), _jsx(FileDrawer, { title: drawerTitle, onClose: () => setDrawerTarget(null), children: drawerTarget?.kind === 'walkthrough' &&
|
|
1276
|
+
focusedWalkthrough &&
|
|
1277
|
+
renderWalkthroughViewer ? (_jsx(WalkthroughDrawerContent, { render: renderWalkthroughDrawerContent, walkthrough: focusedWalkthrough, stepIndex: drawerTarget.stepIndex })) : drawerTarget?.kind === 'file' ? (_jsx(FileDrawerContent, { render: renderDrawerContent, file: drawerTarget.file, startLine: drawerTarget.startLine })) : null }), _jsx(GraphLayoutCover, { revealed: layoutReady }), canvasOverlay] })] }));
|
|
1250
1278
|
}
|
|
1251
1279
|
/** Sidebar header for one repo's tree: owner avatar + repo name + file count.
|
|
1252
1280
|
* Clicking toggles the tree's visibility. */
|
|
@@ -1288,15 +1316,14 @@ function RepoGroupHeader({ group, collapsed, onToggle, }) {
|
|
|
1288
1316
|
whiteSpace: 'nowrap',
|
|
1289
1317
|
}, children: label })] }));
|
|
1290
1318
|
}
|
|
1291
|
-
/** One collapsible
|
|
1319
|
+
/** One collapsible walkthrough in the sidebar's flows panel. Clicking the
|
|
1292
1320
|
* title: closed → open + select; open and unselected → select; open and
|
|
1293
1321
|
* selected → close + clear focus. The right-aligned close button collapses
|
|
1294
1322
|
* without selecting. A step row focuses that step's edge. */
|
|
1295
|
-
function
|
|
1323
|
+
function WalkthroughFlow({ walkthrough, collapsed, active, onToggleCollapsed, onFocusFlow, onClearFocus, onFocusStep, onHoverStep, onHoverFlow, onLeaveStep, }) {
|
|
1296
1324
|
const { theme } = useTheme();
|
|
1297
1325
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
1298
1326
|
const hoverBg = theme.colors.background;
|
|
1299
|
-
const edgeById = useMemo(() => new Map(edges.map((e) => [e.id, e])), [edges]);
|
|
1300
1327
|
const wholeFlowActive = active !== null && active.stepIndex === null;
|
|
1301
1328
|
const [headerHover, setHeaderHover] = useState(false);
|
|
1302
1329
|
const [closeHover, setCloseHover] = useState(false);
|
|
@@ -1318,10 +1345,10 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1318
1345
|
useEffect(() => () => stopPlaying(), []);
|
|
1319
1346
|
const startPlaying = useCallback(() => {
|
|
1320
1347
|
if (collapsed)
|
|
1321
|
-
onToggleCollapsed(
|
|
1348
|
+
onToggleCollapsed(walkthrough.id);
|
|
1322
1349
|
if (active === null || active.stepIndex !== null)
|
|
1323
|
-
onFocusFlow(
|
|
1324
|
-
const stepCount =
|
|
1350
|
+
onFocusFlow(walkthrough);
|
|
1351
|
+
const stepCount = walkthrough.steps.length;
|
|
1325
1352
|
if (stepCount === 0)
|
|
1326
1353
|
return;
|
|
1327
1354
|
setPlaying(true);
|
|
@@ -1332,12 +1359,12 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1332
1359
|
setPlaying(false);
|
|
1333
1360
|
return;
|
|
1334
1361
|
}
|
|
1335
|
-
onFocusStep(
|
|
1362
|
+
onFocusStep(walkthrough, i);
|
|
1336
1363
|
i += 1;
|
|
1337
|
-
playTimerRef.current = window.setTimeout(tick,
|
|
1364
|
+
playTimerRef.current = window.setTimeout(tick, WALKTHROUGH_PLAY_PAUSE_MS);
|
|
1338
1365
|
};
|
|
1339
1366
|
tick();
|
|
1340
|
-
}, [collapsed, active, onToggleCollapsed, onFocusFlow,
|
|
1367
|
+
}, [collapsed, active, onToggleCollapsed, onFocusFlow, walkthrough, onFocusStep]);
|
|
1341
1368
|
const togglePlay = useCallback(() => {
|
|
1342
1369
|
if (playing) {
|
|
1343
1370
|
stopPlaying();
|
|
@@ -1357,7 +1384,7 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1357
1384
|
return (_jsxs("div", { children: [_jsxs("div", { onMouseEnter: () => {
|
|
1358
1385
|
setHeaderHover(true);
|
|
1359
1386
|
if (collapsed)
|
|
1360
|
-
onHoverFlow(
|
|
1387
|
+
onHoverFlow(walkthrough);
|
|
1361
1388
|
}, onMouseLeave: () => {
|
|
1362
1389
|
setHeaderHover(false);
|
|
1363
1390
|
if (collapsed)
|
|
@@ -1371,14 +1398,14 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1371
1398
|
transition: 'background 120ms ease',
|
|
1372
1399
|
}, children: [_jsx("button", { type: "button", onClick: () => {
|
|
1373
1400
|
if (collapsed) {
|
|
1374
|
-
onToggleCollapsed(
|
|
1375
|
-
onFocusFlow(
|
|
1401
|
+
onToggleCollapsed(walkthrough.id);
|
|
1402
|
+
onFocusFlow(walkthrough);
|
|
1376
1403
|
}
|
|
1377
1404
|
else if (active === null) {
|
|
1378
|
-
onFocusFlow(
|
|
1405
|
+
onFocusFlow(walkthrough);
|
|
1379
1406
|
}
|
|
1380
1407
|
else {
|
|
1381
|
-
onToggleCollapsed(
|
|
1408
|
+
onToggleCollapsed(walkthrough.id);
|
|
1382
1409
|
onClearFocus();
|
|
1383
1410
|
}
|
|
1384
1411
|
}, style: {
|
|
@@ -1399,7 +1426,7 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1399
1426
|
fontFamily: theme.fonts.monospace,
|
|
1400
1427
|
fontWeight: 600,
|
|
1401
1428
|
color: theme.colors.text,
|
|
1402
|
-
}, children:
|
|
1429
|
+
}, children: walkthrough.title }) }), !collapsed && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", "aria-label": playing ? `Pause ${walkthrough.title} autoplay` : `Play ${walkthrough.title}`, title: playing ? 'Pause' : 'Play through steps', onMouseEnter: () => setPlayHover(true), onMouseLeave: () => setPlayHover(false), onClick: (e) => {
|
|
1403
1430
|
e.stopPropagation();
|
|
1404
1431
|
togglePlay();
|
|
1405
1432
|
}, style: {
|
|
@@ -1416,9 +1443,9 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1416
1443
|
color: playing || playHover ? theme.colors.text : muted,
|
|
1417
1444
|
cursor: 'pointer',
|
|
1418
1445
|
transition: 'background 120ms ease, color 120ms ease',
|
|
1419
|
-
}, children: playing ? _jsx(Pause, { size: 12, strokeWidth: 2 }) : _jsx(Play, { size: 12, strokeWidth: 2 }) }), _jsx("button", { type: "button", "aria-label": `Close ${
|
|
1446
|
+
}, children: playing ? _jsx(Pause, { size: 12, strokeWidth: 2 }) : _jsx(Play, { size: 12, strokeWidth: 2 }) }), _jsx("button", { type: "button", "aria-label": `Close ${walkthrough.title}`, onMouseEnter: () => setCloseHover(true), onMouseLeave: () => setCloseHover(false), onClick: (e) => {
|
|
1420
1447
|
e.stopPropagation();
|
|
1421
|
-
onToggleCollapsed(
|
|
1448
|
+
onToggleCollapsed(walkthrough.id);
|
|
1422
1449
|
if (active !== null)
|
|
1423
1450
|
onClearFocus();
|
|
1424
1451
|
}, style: {
|
|
@@ -1438,17 +1465,16 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1438
1465
|
}, children: _jsx(X, { size: 12, strokeWidth: 2 }) })] }))] }), !collapsed && (_jsx("div", { style: { display: 'flex', flexDirection: 'column' }, onMouseLeave: () => {
|
|
1439
1466
|
setHoveredStep(null);
|
|
1440
1467
|
onLeaveStep();
|
|
1441
|
-
}, children:
|
|
1442
|
-
const
|
|
1443
|
-
const mech = edge?.mechanism;
|
|
1468
|
+
}, children: walkthrough.steps.map((step, i) => {
|
|
1469
|
+
const mech = step.mechanism;
|
|
1444
1470
|
const color = mech ? MECHANISM_COLOR[mech] : muted;
|
|
1445
1471
|
const stepActive = active !== null && active.stepIndex === i;
|
|
1446
1472
|
return (_jsxs("button", { ref: (el) => {
|
|
1447
1473
|
stepButtonRefs.current[i] = el;
|
|
1448
1474
|
}, type: "button", onMouseEnter: () => {
|
|
1449
1475
|
setHoveredStep(i);
|
|
1450
|
-
onHoverStep(
|
|
1451
|
-
}, onClick: () => onFocusStep(
|
|
1476
|
+
onHoverStep(walkthrough, i);
|
|
1477
|
+
}, onClick: () => onFocusStep(walkthrough, i), style: {
|
|
1452
1478
|
display: 'flex',
|
|
1453
1479
|
alignItems: 'center',
|
|
1454
1480
|
gap: 8,
|
|
@@ -1485,7 +1511,7 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1485
1511
|
fontSize: theme.fontSizes[0],
|
|
1486
1512
|
fontFamily: theme.fonts.monospace,
|
|
1487
1513
|
color,
|
|
1488
|
-
}, children: mech ?? step.
|
|
1514
|
+
}, children: mech ?? step.mechanism }), _jsxs("span", { style: {
|
|
1489
1515
|
flex: 1,
|
|
1490
1516
|
minWidth: 0,
|
|
1491
1517
|
overflow: 'hidden',
|
|
@@ -1494,7 +1520,7 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1494
1520
|
fontSize: theme.fontSizes[0],
|
|
1495
1521
|
fontFamily: theme.fonts.monospace,
|
|
1496
1522
|
color: theme.colors.text,
|
|
1497
|
-
}, children: [step.file.split('/').pop(), _jsxs("span", { style: { opacity: 0.7 }, children: [":", step.line] })] })] }))] }, `${step
|
|
1523
|
+
}, children: [step.file.split('/').pop(), _jsxs("span", { style: { opacity: 0.7 }, children: [":", step.line] })] })] }))] }, `${walkthroughStepGraphEdgeId(step)}-${i}`));
|
|
1498
1524
|
}) }))] }));
|
|
1499
1525
|
}
|
|
1500
1526
|
export function SubsystemComponentGraph(props) {
|