@principal-ai/subsystems-react 0.20.6 → 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/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- 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/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 +204 -199
- 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 +85 -35
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +85 -6
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +30 -13
- 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/index.ts +13 -5
- 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 +14 -14
- 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 +1 -1
- 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/FileDrawer.tsx +2 -2
- package/src/subsystem/SubsystemComponentGraph.tsx +268 -259
- package/src/subsystem/formatDeclaration.test.ts +76 -1
- package/src/subsystem/formatDeclaration.ts +57 -33
- package/src/subsystem/model.test.ts +27 -15
- package/src/subsystem/model.ts +175 -45
- package/src/subsystem/nodes.tsx +47 -13
- 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,77 +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
|
|
744
|
+
// Focus a single step's edge on the canvas and open/scroll the walkthrough
|
|
738
745
|
// drawer to that step's snippet. Clicking the already-focused step clears
|
|
739
746
|
// step focus and returns to whole-flow (high-level) framing.
|
|
740
747
|
// In `dim` mode: only dim non-participants (same as hovering a step) —
|
|
741
748
|
// no camera move, no drawer, no hide.
|
|
742
|
-
const
|
|
749
|
+
const focusWalkthroughStep = useCallback((tl, stepIndex) => {
|
|
743
750
|
const step = tl.steps[stepIndex];
|
|
744
751
|
if (!step)
|
|
745
752
|
return;
|
|
746
753
|
setSelected(null);
|
|
747
754
|
setSelectedEdgeId(null);
|
|
748
|
-
if (
|
|
755
|
+
if (walkthroughStepMode === 'dim') {
|
|
749
756
|
setFocusedStepIndex(null);
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
757
|
+
setFocusedWalkthroughId(null);
|
|
758
|
+
setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex });
|
|
759
|
+
setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
|
|
753
760
|
return;
|
|
754
761
|
}
|
|
755
762
|
// Toggle: clicking the selected step unselects it (back to whole flow).
|
|
756
|
-
if (
|
|
763
|
+
if (focusedWalkthroughId === tl.id && focusedStepIndex === stepIndex) {
|
|
757
764
|
setFocusedStepIndex(null);
|
|
758
765
|
// Pointer is still over the row — keep hover preview so dimming doesn't
|
|
759
766
|
// flash off until mouseleave.
|
|
760
|
-
|
|
761
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
767
|
+
setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex });
|
|
768
|
+
setDrawerTarget((prev) => (prev?.kind === 'walkthrough' ? null : prev));
|
|
762
769
|
if (pendingFocusFitRef.current != null) {
|
|
763
770
|
window.clearTimeout(pendingFocusFitRef.current);
|
|
764
771
|
pendingFocusFitRef.current = null;
|
|
765
772
|
}
|
|
766
|
-
fitFocusBounds(new Set(tl.steps.map((s) => s
|
|
773
|
+
fitFocusBounds(new Set(tl.steps.map((s) => walkthroughStepGraphEdgeId(s))));
|
|
767
774
|
return;
|
|
768
775
|
}
|
|
769
776
|
setFocusedStepIndex(stepIndex);
|
|
770
|
-
|
|
771
|
-
|
|
777
|
+
setFocusedWalkthroughId(tl.id);
|
|
778
|
+
setHoveredWalkthroughStep(null);
|
|
772
779
|
// Open the drawer before fitting. If it was closed, wait for its height
|
|
773
780
|
// transition so fitView uses the reduced canvas — not full height.
|
|
774
781
|
const drawerWasOpen = drawerOpenRef.current;
|
|
775
|
-
if (
|
|
782
|
+
if (renderWalkthroughViewer) {
|
|
776
783
|
setDrawerTarget({
|
|
777
|
-
kind: '
|
|
778
|
-
|
|
784
|
+
kind: 'walkthrough',
|
|
785
|
+
walkthroughId: tl.id,
|
|
779
786
|
stepIndex,
|
|
780
787
|
});
|
|
781
788
|
}
|
|
@@ -786,7 +793,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
786
793
|
startLine: step.line,
|
|
787
794
|
});
|
|
788
795
|
}
|
|
789
|
-
const edgeIds = new Set([step
|
|
796
|
+
const edgeIds = new Set([walkthroughStepGraphEdgeId(step)]);
|
|
790
797
|
if (pendingFocusFitRef.current != null) {
|
|
791
798
|
window.clearTimeout(pendingFocusFitRef.current);
|
|
792
799
|
pendingFocusFitRef.current = null;
|
|
@@ -803,9 +810,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
803
810
|
}, [
|
|
804
811
|
fitFocusBounds,
|
|
805
812
|
focusedStepIndex,
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
813
|
+
focusedWalkthroughId,
|
|
814
|
+
renderWalkthroughViewer,
|
|
815
|
+
walkthroughStepMode,
|
|
809
816
|
]);
|
|
810
817
|
useEffect(() => {
|
|
811
818
|
return () => {
|
|
@@ -823,10 +830,10 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
823
830
|
useEffect(() => {
|
|
824
831
|
const prev = prevPreviewEdgeIdsRef.current;
|
|
825
832
|
prevPreviewEdgeIdsRef.current = previewEdgeIds;
|
|
826
|
-
if (!
|
|
833
|
+
if (!zoomOnWalkthroughFocus || walkthroughStepMode === 'dim')
|
|
827
834
|
return;
|
|
828
835
|
// Camera follows hover only when a specific step is already focused.
|
|
829
|
-
if (
|
|
836
|
+
if (focusedWalkthroughId == null || focusedStepIndex == null)
|
|
830
837
|
return;
|
|
831
838
|
if (previewEdgeIds) {
|
|
832
839
|
fitFocusBounds(previewEdgeIds);
|
|
@@ -838,33 +845,33 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
838
845
|
}, [
|
|
839
846
|
previewEdgeIds,
|
|
840
847
|
focusEdgeIds,
|
|
841
|
-
|
|
848
|
+
focusedWalkthroughId,
|
|
842
849
|
focusedStepIndex,
|
|
843
850
|
fitFocusBounds,
|
|
844
|
-
|
|
845
|
-
|
|
851
|
+
zoomOnWalkthroughFocus,
|
|
852
|
+
walkthroughStepMode,
|
|
846
853
|
]);
|
|
847
|
-
// Graph-only embeds: cycle
|
|
854
|
+
// Graph-only embeds: cycle walkthrough steps with hover-style dimming.
|
|
848
855
|
useEffect(() => {
|
|
849
|
-
if (!
|
|
856
|
+
if (!autoPlayWalkthroughs || !walkthroughs?.length || !layoutReady)
|
|
850
857
|
return;
|
|
851
|
-
const playable =
|
|
858
|
+
const playable = walkthroughs.filter((tl) => tl.steps.length > 0);
|
|
852
859
|
if (playable.length === 0)
|
|
853
860
|
return;
|
|
854
861
|
let cancelled = false;
|
|
855
862
|
let tlIdx = 0;
|
|
856
863
|
let stepIdx = 0;
|
|
857
864
|
let timer = null;
|
|
858
|
-
const interval = Math.max(400,
|
|
865
|
+
const interval = Math.max(400, walkthroughAutoPlayIntervalMs);
|
|
859
866
|
const tick = () => {
|
|
860
867
|
if (cancelled)
|
|
861
868
|
return;
|
|
862
869
|
const tl = playable[tlIdx];
|
|
863
870
|
setSelected(null);
|
|
864
871
|
setSelectedEdgeId(null);
|
|
865
|
-
|
|
872
|
+
setFocusedWalkthroughId(null);
|
|
866
873
|
setFocusedStepIndex(null);
|
|
867
|
-
|
|
874
|
+
setHoveredWalkthroughStep({ walkthroughId: tl.id, stepIndex: stepIdx });
|
|
868
875
|
stepIdx += 1;
|
|
869
876
|
if (stepIdx >= tl.steps.length) {
|
|
870
877
|
stepIdx = 0;
|
|
@@ -877,16 +884,16 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
877
884
|
cancelled = true;
|
|
878
885
|
if (timer != null)
|
|
879
886
|
window.clearTimeout(timer);
|
|
880
|
-
|
|
887
|
+
setHoveredWalkthroughStep(null);
|
|
881
888
|
};
|
|
882
|
-
}, [
|
|
883
|
-
// Arrow keys step through the focused
|
|
889
|
+
}, [autoPlayWalkthroughs, walkthroughs, walkthroughAutoPlayIntervalMs, layoutReady]);
|
|
890
|
+
// Arrow keys step through the focused walkthrough once a step is active
|
|
884
891
|
// (sidebar click or drawer open). Ignores typing targets and chords.
|
|
885
892
|
useEffect(() => {
|
|
886
|
-
if (
|
|
893
|
+
if (focusedWalkthroughId == null || focusedStepIndex == null || !walkthroughs) {
|
|
887
894
|
return;
|
|
888
895
|
}
|
|
889
|
-
const tl =
|
|
896
|
+
const tl = walkthroughs.find((t) => t.id === focusedWalkthroughId);
|
|
890
897
|
if (!tl || tl.steps.length === 0)
|
|
891
898
|
return;
|
|
892
899
|
const onKeyDown = (e) => {
|
|
@@ -913,19 +920,19 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
913
920
|
if (next === focusedStepIndex)
|
|
914
921
|
return;
|
|
915
922
|
e.preventDefault();
|
|
916
|
-
|
|
923
|
+
focusWalkthroughStep(tl, next);
|
|
917
924
|
};
|
|
918
925
|
window.addEventListener('keydown', onKeyDown);
|
|
919
926
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
920
927
|
}, [
|
|
921
|
-
|
|
928
|
+
focusedWalkthroughId,
|
|
922
929
|
focusedStepIndex,
|
|
923
|
-
|
|
924
|
-
|
|
930
|
+
walkthroughs,
|
|
931
|
+
focusWalkthroughStep,
|
|
925
932
|
]);
|
|
926
|
-
const
|
|
927
|
-
const collapsingLast =
|
|
928
|
-
|
|
933
|
+
const toggleWalkthroughCollapsed = useCallback((tlId) => {
|
|
934
|
+
const collapsingLast = expandedWalkthroughs.has(tlId) && expandedWalkthroughs.size === 1;
|
|
935
|
+
setExpandedWalkthroughs((prev) => {
|
|
929
936
|
const next = new Set(prev);
|
|
930
937
|
if (next.has(tlId))
|
|
931
938
|
next.delete(tlId);
|
|
@@ -933,7 +940,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
933
940
|
next.add(tlId);
|
|
934
941
|
return next;
|
|
935
942
|
});
|
|
936
|
-
if (!collapsingLast || !
|
|
943
|
+
if (!collapsingLast || !zoomOnWalkthroughFocus)
|
|
937
944
|
return;
|
|
938
945
|
// Cancel any pending focus fit so it doesn't fight the overview zoom.
|
|
939
946
|
if (pendingFocusFitRef.current != null) {
|
|
@@ -948,7 +955,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
948
955
|
pendingFocusFitRef.current = null;
|
|
949
956
|
requestAnimationFrame(() => fitOverview());
|
|
950
957
|
}, drawerWasOpen ? FILE_DRAWER_HEIGHT_MS + 20 : 20);
|
|
951
|
-
}, [
|
|
958
|
+
}, [expandedWalkthroughs, fitOverview, zoomOnWalkthroughFocus]);
|
|
952
959
|
// Filename-badge clicks on nodes open the drawer through the same path as
|
|
953
960
|
// the declaration panel's file link (toggle + tree sync, no start line).
|
|
954
961
|
useEffect(() => {
|
|
@@ -973,7 +980,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
973
980
|
return;
|
|
974
981
|
setSelected(comp);
|
|
975
982
|
setSelectedEdgeId(null);
|
|
976
|
-
|
|
983
|
+
setFocusedWalkthroughId(null);
|
|
977
984
|
setFocusedStepIndex(null);
|
|
978
985
|
onSelect?.(comp.id);
|
|
979
986
|
}, [components, onSelect]);
|
|
@@ -1021,10 +1028,10 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1021
1028
|
const fileViewerRef = useRef(fileViewer);
|
|
1022
1029
|
fileViewerRef.current = fileViewer;
|
|
1023
1030
|
const renderDrawerContent = useCallback((file, opts) => fileViewerRef.current?.(file, opts) ?? null, []);
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
const
|
|
1027
|
-
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: {
|
|
1028
1035
|
width: sidebarWidth,
|
|
1029
1036
|
minWidth: sidebarWidth,
|
|
1030
1037
|
borderRight: `1px solid ${theme.colors.border}`,
|
|
@@ -1062,7 +1069,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1062
1069
|
color: descToggleHover ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
|
|
1063
1070
|
cursor: 'pointer',
|
|
1064
1071
|
transition: 'background 120ms ease, color 120ms ease',
|
|
1065
|
-
}, 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: {
|
|
1066
1073
|
...(showDesc ? { height: '50%' } : { flex: 1, minHeight: 0 }),
|
|
1067
1074
|
minHeight: 160,
|
|
1068
1075
|
flexShrink: 0,
|
|
@@ -1070,13 +1077,13 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1070
1077
|
display: 'flex',
|
|
1071
1078
|
flexDirection: 'column',
|
|
1072
1079
|
overflow: 'hidden',
|
|
1073
|
-
}, children: [
|
|
1080
|
+
}, children: [hasWalkthroughs && (_jsx("div", { role: "tablist", "aria-label": "Sidebar view", style: {
|
|
1074
1081
|
display: 'flex',
|
|
1075
1082
|
width: '100%',
|
|
1076
1083
|
flexShrink: 0,
|
|
1077
1084
|
borderBottom: `1px solid ${theme.colors.border}`,
|
|
1078
1085
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
1079
|
-
}, children: ['
|
|
1086
|
+
}, children: ['walkthroughs', 'files'].map((view) => (_jsx("button", { type: "button", role: "tab", "aria-selected": sidebarView === view, onClick: () => setSidebarView(view), style: {
|
|
1080
1087
|
flex: 1,
|
|
1081
1088
|
minWidth: 0,
|
|
1082
1089
|
padding: '8px 8px',
|
|
@@ -1090,16 +1097,16 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1090
1097
|
fontFamily: theme.fonts.monospace,
|
|
1091
1098
|
textTransform: 'capitalize',
|
|
1092
1099
|
cursor: 'pointer',
|
|
1093
|
-
}, children: view }, view))) })), sidebarView === '
|
|
1100
|
+
}, children: view === 'walkthroughs' ? 'Walkthroughs' : 'Files' }, view))) })), sidebarView === 'walkthroughs' && walkthroughs && walkthroughs.length > 0 ? (_jsx("div", { style: {
|
|
1094
1101
|
flex: 1,
|
|
1095
1102
|
minHeight: 0,
|
|
1096
1103
|
overflowY: 'auto',
|
|
1097
1104
|
padding: '0 0 12px',
|
|
1098
|
-
}, children:
|
|
1105
|
+
}, children: walkthroughs.map((tl) => (_jsx(WalkthroughFlow, { walkthrough: tl, collapsed: !expandedWalkthroughs.has(tl.id), active: focusedWalkthroughId === tl.id
|
|
1099
1106
|
? { stepIndex: focusedStepIndex }
|
|
1100
|
-
:
|
|
1101
|
-
? { stepIndex:
|
|
1102
|
-
: 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) => {
|
|
1103
1110
|
const groupKey = group.repoKey ?? '__no-repo__';
|
|
1104
1111
|
const collapsed = collapsedRepos.has(groupKey);
|
|
1105
1112
|
return (_jsxs("div", { style: {
|
|
@@ -1109,7 +1116,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1109
1116
|
flexDirection: 'column',
|
|
1110
1117
|
borderTop: i > 0 ? `1px solid ${theme.colors.border}` : undefined,
|
|
1111
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));
|
|
1112
|
-
}) })) : 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: {
|
|
1113
1120
|
width: 3,
|
|
1114
1121
|
flexShrink: 0,
|
|
1115
1122
|
cursor: 'col-resize',
|
|
@@ -1172,7 +1179,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1172
1179
|
flex: 1,
|
|
1173
1180
|
minHeight: 0,
|
|
1174
1181
|
background: theme.colors.background,
|
|
1175
|
-
}, 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: {
|
|
1176
1183
|
position: 'absolute',
|
|
1177
1184
|
top: 10,
|
|
1178
1185
|
left: '50%',
|
|
@@ -1198,9 +1205,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1198
1205
|
border: `1px solid ${theme.colors.border}`,
|
|
1199
1206
|
borderRadius: 6,
|
|
1200
1207
|
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
1201
|
-
}, children: graphTitle })),
|
|
1208
|
+
}, children: graphTitle })), overlayWalkthroughTitle && (_jsxs("div", { style: {
|
|
1202
1209
|
maxWidth: '100%',
|
|
1203
|
-
minWidth:
|
|
1210
|
+
minWidth: overlayWalkthroughStep ? 160 : undefined,
|
|
1204
1211
|
display: 'flex',
|
|
1205
1212
|
flexDirection: 'column',
|
|
1206
1213
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
@@ -1209,9 +1216,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1209
1216
|
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
1210
1217
|
overflow: 'hidden',
|
|
1211
1218
|
opacity: 0.95,
|
|
1212
|
-
}, "aria-label":
|
|
1213
|
-
? `${
|
|
1214
|
-
:
|
|
1219
|
+
}, "aria-label": overlayWalkthroughStep
|
|
1220
|
+
? `${overlayWalkthroughTitle}, step ${overlayWalkthroughStep.index} of ${overlayWalkthroughStep.total}`
|
|
1221
|
+
: overlayWalkthroughTitle, children: [_jsx("div", { style: {
|
|
1215
1222
|
padding: '5px 14px',
|
|
1216
1223
|
fontSize: theme.fontSizes[2] ?? theme.fontSizes[1],
|
|
1217
1224
|
fontWeight: 600,
|
|
@@ -1221,14 +1228,14 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1221
1228
|
textOverflow: 'ellipsis',
|
|
1222
1229
|
whiteSpace: 'nowrap',
|
|
1223
1230
|
textAlign: 'center',
|
|
1224
|
-
}, children:
|
|
1231
|
+
}, children: overlayWalkthroughTitle }), overlayWalkthroughStep && overlayWalkthroughStep.total > 0 && (_jsx("div", { style: {
|
|
1225
1232
|
display: 'flex',
|
|
1226
1233
|
gap: 3,
|
|
1227
1234
|
padding: '0 6px 5px',
|
|
1228
|
-
}, "aria-hidden": "true", children: Array.from({ length:
|
|
1235
|
+
}, "aria-hidden": "true", children: Array.from({ length: overlayWalkthroughStep.total }, (_, i) => {
|
|
1229
1236
|
const n = i + 1;
|
|
1230
|
-
const active = n ===
|
|
1231
|
-
const done = n <
|
|
1237
|
+
const active = n === overlayWalkthroughStep.index;
|
|
1238
|
+
const done = n < overlayWalkthroughStep.index;
|
|
1232
1239
|
return (_jsx("span", { style: {
|
|
1233
1240
|
flex: 1,
|
|
1234
1241
|
height: 2,
|
|
@@ -1241,7 +1248,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1241
1248
|
opacity: active ? 1 : done ? 0.75 : 0.4,
|
|
1242
1249
|
transition: 'opacity 120ms ease, background 120ms ease',
|
|
1243
1250
|
} }, n));
|
|
1244
|
-
}) }))] })),
|
|
1251
|
+
}) }))] })), overlayWalkthroughStep?.annotation && (_jsx("div", { style: {
|
|
1245
1252
|
maxWidth: '100%',
|
|
1246
1253
|
padding: '6px 14px',
|
|
1247
1254
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
@@ -1253,7 +1260,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1253
1260
|
fontFamily: theme.fonts.body,
|
|
1254
1261
|
color: theme.colors.textMuted ?? theme.colors.textSecondary,
|
|
1255
1262
|
lineHeight: 1.35,
|
|
1256
|
-
}, children:
|
|
1263
|
+
}, children: overlayWalkthroughStep.annotation }))] })), selected && (_jsx("div", { style: {
|
|
1257
1264
|
position: 'absolute',
|
|
1258
1265
|
top: 10,
|
|
1259
1266
|
right: 10,
|
|
@@ -1265,9 +1272,9 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1265
1272
|
border: `1px solid ${theme.colors.border}`,
|
|
1266
1273
|
borderRadius: 8,
|
|
1267
1274
|
boxShadow: '0 8px 24px rgba(0,0,0,0.35)',
|
|
1268
|
-
}, children: _jsx(ComponentDeclaration, { component: selected, onOpenFile: onOpenDeclarationFile, onRelatedSelect: resolveRelatedComponent, onVerify: onVerifyComponent, verification: componentVerification }) }))] }), _jsx(FileDrawer, { title: drawerTitle, onClose: () => setDrawerTarget(null), children: drawerTarget?.kind === '
|
|
1269
|
-
|
|
1270
|
-
|
|
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] })] }));
|
|
1271
1278
|
}
|
|
1272
1279
|
/** Sidebar header for one repo's tree: owner avatar + repo name + file count.
|
|
1273
1280
|
* Clicking toggles the tree's visibility. */
|
|
@@ -1309,15 +1316,14 @@ function RepoGroupHeader({ group, collapsed, onToggle, }) {
|
|
|
1309
1316
|
whiteSpace: 'nowrap',
|
|
1310
1317
|
}, children: label })] }));
|
|
1311
1318
|
}
|
|
1312
|
-
/** One collapsible
|
|
1319
|
+
/** One collapsible walkthrough in the sidebar's flows panel. Clicking the
|
|
1313
1320
|
* title: closed → open + select; open and unselected → select; open and
|
|
1314
1321
|
* selected → close + clear focus. The right-aligned close button collapses
|
|
1315
1322
|
* without selecting. A step row focuses that step's edge. */
|
|
1316
|
-
function
|
|
1323
|
+
function WalkthroughFlow({ walkthrough, collapsed, active, onToggleCollapsed, onFocusFlow, onClearFocus, onFocusStep, onHoverStep, onHoverFlow, onLeaveStep, }) {
|
|
1317
1324
|
const { theme } = useTheme();
|
|
1318
1325
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
1319
1326
|
const hoverBg = theme.colors.background;
|
|
1320
|
-
const edgeById = useMemo(() => new Map(edges.map((e) => [e.id, e])), [edges]);
|
|
1321
1327
|
const wholeFlowActive = active !== null && active.stepIndex === null;
|
|
1322
1328
|
const [headerHover, setHeaderHover] = useState(false);
|
|
1323
1329
|
const [closeHover, setCloseHover] = useState(false);
|
|
@@ -1339,10 +1345,10 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1339
1345
|
useEffect(() => () => stopPlaying(), []);
|
|
1340
1346
|
const startPlaying = useCallback(() => {
|
|
1341
1347
|
if (collapsed)
|
|
1342
|
-
onToggleCollapsed(
|
|
1348
|
+
onToggleCollapsed(walkthrough.id);
|
|
1343
1349
|
if (active === null || active.stepIndex !== null)
|
|
1344
|
-
onFocusFlow(
|
|
1345
|
-
const stepCount =
|
|
1350
|
+
onFocusFlow(walkthrough);
|
|
1351
|
+
const stepCount = walkthrough.steps.length;
|
|
1346
1352
|
if (stepCount === 0)
|
|
1347
1353
|
return;
|
|
1348
1354
|
setPlaying(true);
|
|
@@ -1353,12 +1359,12 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1353
1359
|
setPlaying(false);
|
|
1354
1360
|
return;
|
|
1355
1361
|
}
|
|
1356
|
-
onFocusStep(
|
|
1362
|
+
onFocusStep(walkthrough, i);
|
|
1357
1363
|
i += 1;
|
|
1358
|
-
playTimerRef.current = window.setTimeout(tick,
|
|
1364
|
+
playTimerRef.current = window.setTimeout(tick, WALKTHROUGH_PLAY_PAUSE_MS);
|
|
1359
1365
|
};
|
|
1360
1366
|
tick();
|
|
1361
|
-
}, [collapsed, active, onToggleCollapsed, onFocusFlow,
|
|
1367
|
+
}, [collapsed, active, onToggleCollapsed, onFocusFlow, walkthrough, onFocusStep]);
|
|
1362
1368
|
const togglePlay = useCallback(() => {
|
|
1363
1369
|
if (playing) {
|
|
1364
1370
|
stopPlaying();
|
|
@@ -1378,7 +1384,7 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1378
1384
|
return (_jsxs("div", { children: [_jsxs("div", { onMouseEnter: () => {
|
|
1379
1385
|
setHeaderHover(true);
|
|
1380
1386
|
if (collapsed)
|
|
1381
|
-
onHoverFlow(
|
|
1387
|
+
onHoverFlow(walkthrough);
|
|
1382
1388
|
}, onMouseLeave: () => {
|
|
1383
1389
|
setHeaderHover(false);
|
|
1384
1390
|
if (collapsed)
|
|
@@ -1392,14 +1398,14 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1392
1398
|
transition: 'background 120ms ease',
|
|
1393
1399
|
}, children: [_jsx("button", { type: "button", onClick: () => {
|
|
1394
1400
|
if (collapsed) {
|
|
1395
|
-
onToggleCollapsed(
|
|
1396
|
-
onFocusFlow(
|
|
1401
|
+
onToggleCollapsed(walkthrough.id);
|
|
1402
|
+
onFocusFlow(walkthrough);
|
|
1397
1403
|
}
|
|
1398
1404
|
else if (active === null) {
|
|
1399
|
-
onFocusFlow(
|
|
1405
|
+
onFocusFlow(walkthrough);
|
|
1400
1406
|
}
|
|
1401
1407
|
else {
|
|
1402
|
-
onToggleCollapsed(
|
|
1408
|
+
onToggleCollapsed(walkthrough.id);
|
|
1403
1409
|
onClearFocus();
|
|
1404
1410
|
}
|
|
1405
1411
|
}, style: {
|
|
@@ -1420,7 +1426,7 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1420
1426
|
fontFamily: theme.fonts.monospace,
|
|
1421
1427
|
fontWeight: 600,
|
|
1422
1428
|
color: theme.colors.text,
|
|
1423
|
-
}, 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) => {
|
|
1424
1430
|
e.stopPropagation();
|
|
1425
1431
|
togglePlay();
|
|
1426
1432
|
}, style: {
|
|
@@ -1437,9 +1443,9 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1437
1443
|
color: playing || playHover ? theme.colors.text : muted,
|
|
1438
1444
|
cursor: 'pointer',
|
|
1439
1445
|
transition: 'background 120ms ease, color 120ms ease',
|
|
1440
|
-
}, 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) => {
|
|
1441
1447
|
e.stopPropagation();
|
|
1442
|
-
onToggleCollapsed(
|
|
1448
|
+
onToggleCollapsed(walkthrough.id);
|
|
1443
1449
|
if (active !== null)
|
|
1444
1450
|
onClearFocus();
|
|
1445
1451
|
}, style: {
|
|
@@ -1459,17 +1465,16 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1459
1465
|
}, children: _jsx(X, { size: 12, strokeWidth: 2 }) })] }))] }), !collapsed && (_jsx("div", { style: { display: 'flex', flexDirection: 'column' }, onMouseLeave: () => {
|
|
1460
1466
|
setHoveredStep(null);
|
|
1461
1467
|
onLeaveStep();
|
|
1462
|
-
}, children:
|
|
1463
|
-
const
|
|
1464
|
-
const mech = edge?.mechanism;
|
|
1468
|
+
}, children: walkthrough.steps.map((step, i) => {
|
|
1469
|
+
const mech = step.mechanism;
|
|
1465
1470
|
const color = mech ? MECHANISM_COLOR[mech] : muted;
|
|
1466
1471
|
const stepActive = active !== null && active.stepIndex === i;
|
|
1467
1472
|
return (_jsxs("button", { ref: (el) => {
|
|
1468
1473
|
stepButtonRefs.current[i] = el;
|
|
1469
1474
|
}, type: "button", onMouseEnter: () => {
|
|
1470
1475
|
setHoveredStep(i);
|
|
1471
|
-
onHoverStep(
|
|
1472
|
-
}, onClick: () => onFocusStep(
|
|
1476
|
+
onHoverStep(walkthrough, i);
|
|
1477
|
+
}, onClick: () => onFocusStep(walkthrough, i), style: {
|
|
1473
1478
|
display: 'flex',
|
|
1474
1479
|
alignItems: 'center',
|
|
1475
1480
|
gap: 8,
|
|
@@ -1506,7 +1511,7 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1506
1511
|
fontSize: theme.fontSizes[0],
|
|
1507
1512
|
fontFamily: theme.fonts.monospace,
|
|
1508
1513
|
color,
|
|
1509
|
-
}, children: mech ?? step.
|
|
1514
|
+
}, children: mech ?? step.mechanism }), _jsxs("span", { style: {
|
|
1510
1515
|
flex: 1,
|
|
1511
1516
|
minWidth: 0,
|
|
1512
1517
|
overflow: 'hidden',
|
|
@@ -1515,7 +1520,7 @@ function ThroughlineFlow({ throughline, edges, collapsed, active, onToggleCollap
|
|
|
1515
1520
|
fontSize: theme.fontSizes[0],
|
|
1516
1521
|
fontFamily: theme.fonts.monospace,
|
|
1517
1522
|
color: theme.colors.text,
|
|
1518
|
-
}, 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}`));
|
|
1519
1524
|
}) }))] }));
|
|
1520
1525
|
}
|
|
1521
1526
|
export function SubsystemComponentGraph(props) {
|