@principal-ai/subsystems-react 0.44.4 → 0.45.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/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/pierre/{PierreWalkthroughCodeView.d.ts → PierreTrailCodeView.d.ts} +11 -11
- package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -0
- package/dist/pierre/{PierreWalkthroughCodeView.js → PierreTrailCodeView.js} +36 -36
- package/dist/pierre/PierreTrailCodeView.js.map +1 -0
- package/dist/pierre/index.d.ts +2 -2
- package/dist/pierre/index.d.ts.map +1 -1
- package/dist/pierre/index.js +1 -1
- package/dist/pierre/index.js.map +1 -1
- package/dist/stories/Subsystem/AggregateGraph/aggregateViewFixture.d.ts.map +1 -1
- package/dist/stories/Subsystem/AggregateGraph/aggregateViewFixture.js +1 -1
- package/dist/stories/Subsystem/AggregateGraph/aggregateViewFixture.js.map +1 -1
- package/dist/stories/Subsystem/C4Graph/c4Fixture.js +1 -1
- package/dist/stories/Subsystem/C4Graph/c4Fixture.js.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts +9 -9
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +12 -12
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts +2 -2
- package/dist/subsystem/C4Graph.js +2 -2
- package/dist/subsystem/ConstructsCatalog.d.ts +1 -1
- package/dist/subsystem/ConstructsCatalog.js +1 -1
- package/dist/subsystem/DiagnosticToggle.d.ts +1 -1
- package/dist/subsystem/DiagnosticToggle.js +1 -1
- package/dist/subsystem/FileDrawer.d.ts +2 -2
- package/dist/subsystem/FileDrawer.js +2 -2
- package/dist/subsystem/IssueList.d.ts +6 -6
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +9 -9
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemAggregateGraph.d.ts +3 -3
- package/dist/subsystem/SubsystemAggregateGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemAggregateGraph.js +2 -2
- package/dist/subsystem/SubsystemComponentGraph.d.ts +45 -45
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +284 -277
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SubsystemModelTransition.d.ts +10 -10
- package/dist/subsystem/SubsystemModelTransition.d.ts.map +1 -1
- package/dist/subsystem/SubsystemModelTransition.js +42 -42
- package/dist/subsystem/SubsystemModelTransition.js.map +1 -1
- package/dist/subsystem/TrailsPanel.d.ts +55 -0
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -0
- package/dist/subsystem/{WalkthroughsPanel.js → TrailsPanel.js} +51 -51
- package/dist/subsystem/TrailsPanel.js.map +1 -0
- package/dist/subsystem/declarationRef.d.ts +1 -1
- package/dist/subsystem/declarationRef.d.ts.map +1 -1
- package/dist/subsystem/model.d.ts +40 -40
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +17 -17
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +1 -1
- package/dist/subsystem/nodes.js +1 -1
- package/dist/subsystem/toC4.d.ts +2 -2
- package/dist/subsystem/toC4.d.ts.map +1 -1
- package/dist/subsystem/toC4.js +2 -2
- package/dist/subsystem/toC4.js.map +1 -1
- package/dist/subsystem/trailBrief.d.ts +18 -0
- package/dist/subsystem/trailBrief.d.ts.map +1 -0
- package/dist/subsystem/{walkthroughBrief.js → trailBrief.js} +8 -8
- package/dist/subsystem/trailBrief.js.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +15 -15
- package/src/pierre/{PierreWalkthroughCodeView.tsx → PierreTrailCodeView.tsx} +56 -56
- package/src/pierre/index.ts +2 -2
- package/src/stories/Pierre/CodeView.stories.tsx +3 -3
- package/src/stories/Subsystem/AggregateGraph/AggregateFrameGraph.stories.tsx +1 -1
- package/src/stories/Subsystem/AggregateGraph/aggregateViewFixture.ts +1 -1
- package/src/stories/Subsystem/C4Graph/c4Fixture.ts +1 -1
- package/src/stories/Subsystem/ComponentDeclarationAudit.stories.tsx +8 -8
- package/src/stories/Subsystem/ComponentGraph/AnimatedProgression.stories.tsx +13 -13
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +2 -2
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +5 -5
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +8 -8
- package/src/stories/Subsystem/ComponentGraph/CustomEntities.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +6 -6
- package/src/stories/Subsystem/ComponentGraph/EdgeViews.stories.tsx +12 -12
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +36 -36
- package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +7 -7
- package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +4 -4
- package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +8 -8
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +4 -4
- package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +5 -5
- package/src/stories/Subsystem/ComponentGraph/Packages.stories.tsx +6 -6
- package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentGraph/Proposed.stories.tsx +4 -4
- package/src/stories/Subsystem/ComponentGraph/{ProposedWalkthroughs.stories.tsx → ProposedTrails.stories.tsx} +9 -9
- package/src/stories/Subsystem/ComponentGraph/Reorder.stories.tsx +19 -19
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +21 -21
- package/src/stories/Subsystem/ComponentGraph/{WalkthroughAutoplay.stories.tsx → TrailAutoplay.stories.tsx} +22 -22
- package/src/stories/Subsystem/ComponentGraph/WorkspacePackageFrames.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +19 -19
- package/src/subsystem/C4Graph.tsx +2 -2
- package/src/subsystem/ConstructsCatalog.tsx +1 -1
- package/src/subsystem/DiagnosticToggle.tsx +1 -1
- package/src/subsystem/FileDrawer.tsx +2 -2
- package/src/subsystem/IssueList.focus.test.tsx +4 -4
- package/src/subsystem/IssueList.icon.test.tsx +3 -3
- package/src/subsystem/IssueList.tsx +14 -14
- package/src/subsystem/SubsystemAggregateGraph.tsx +3 -3
- package/src/subsystem/SubsystemComponentGraph.tsx +377 -371
- package/src/subsystem/SubsystemModelTransition.tsx +61 -61
- package/src/subsystem/{WalkthroughsPanel.test.tsx → TrailsPanel.test.tsx} +16 -16
- package/src/subsystem/{WalkthroughsPanel.tsx → TrailsPanel.tsx} +81 -81
- package/src/subsystem/declarationRef.ts +1 -1
- package/src/subsystem/formatDeclaration.test.ts +6 -6
- package/src/subsystem/model.test.ts +25 -25
- package/src/subsystem/model.ts +51 -51
- package/src/subsystem/nodes.tsx +1 -1
- package/src/subsystem/toC4.test.ts +1 -1
- package/src/subsystem/toC4.ts +3 -3
- package/src/subsystem/{walkthroughBrief.test.ts → trailBrief.test.ts} +10 -10
- package/src/subsystem/{walkthroughBrief.ts → trailBrief.ts} +8 -8
- package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +0 -1
- package/dist/pierre/PierreWalkthroughCodeView.js.map +0 -1
- package/dist/subsystem/WalkthroughsPanel.d.ts +0 -55
- package/dist/subsystem/WalkthroughsPanel.d.ts.map +0 -1
- package/dist/subsystem/WalkthroughsPanel.js.map +0 -1
- package/dist/subsystem/walkthroughBrief.d.ts +0 -18
- package/dist/subsystem/walkthroughBrief.d.ts.map +0 -1
- package/dist/subsystem/walkthroughBrief.js.map +0 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* inside one labeled boundary frame; nodes without one sit outside every
|
|
8
8
|
* boundary. Clicking a component invokes `onSelect`.
|
|
9
9
|
*
|
|
10
|
-
* When the model has components but no topology or
|
|
10
|
+
* When the model has components but no topology or trail edges, the
|
|
11
11
|
* canvas is a constructs catalog (list + signature) instead of a graph.
|
|
12
12
|
*
|
|
13
13
|
* This is a focused fork of the package's `GraphRenderer` pipeline (same ELK
|
|
@@ -39,11 +39,11 @@ import {
|
|
|
39
39
|
deriveGraphEdges,
|
|
40
40
|
isConstructsOnlyModel,
|
|
41
41
|
moduleGroupNodeId,
|
|
42
|
-
|
|
42
|
+
isTrailMechanism,
|
|
43
43
|
edgeColor,
|
|
44
44
|
MECHANISM_DESCRIPTIONS,
|
|
45
45
|
subsystemGraphLayoutKey,
|
|
46
|
-
|
|
46
|
+
trailStepGraphEdgeId,
|
|
47
47
|
type SubsystemComponentEdge,
|
|
48
48
|
type SubsystemComponent,
|
|
49
49
|
type SubsystemEdgeProvenance,
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
type SubsystemGraphifyRelation,
|
|
52
52
|
type SubsystemNodeIssue,
|
|
53
53
|
type SubsystemRegionIssue,
|
|
54
|
-
type
|
|
54
|
+
type SubsystemTrail,
|
|
55
55
|
} from './model';
|
|
56
56
|
import { ConstructsCatalog } from './ConstructsCatalog';
|
|
57
57
|
import type { SubsystemOpenFileOptions } from './declarationRef';
|
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
MaintainLivePanel,
|
|
64
64
|
type MaintainLivePanelProps,
|
|
65
65
|
} from '../components/maintain-events/MaintainLivePanel';
|
|
66
|
-
import type {
|
|
66
|
+
import type { TrailSymbolQuery } from '../pierre/PierreTrailCodeView';
|
|
67
67
|
import { SubsystemComponentNode, SubsystemGroupNode, SubsystemEdge, SUBSYSTEM_CALLBACKS, hexWithAlpha, EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility, flowNodeVisibility } from './nodes';
|
|
68
68
|
import { SubsystemDiagnosticToggle, type SubsystemDiagnostic } from './DiagnosticToggle';
|
|
69
69
|
import {
|
|
@@ -84,7 +84,7 @@ import type { ComponentVerificationState } from './ComponentDeclaration';
|
|
|
84
84
|
import type { DeclarationSymbolRef, SymbolInspection } from './symbolRefs';
|
|
85
85
|
import { FileDrawer, FILE_DRAWER_HEIGHT_MS } from './FileDrawer';
|
|
86
86
|
import { buildRepoGroups, repoAvatarUrl, type RepoGroup } from './paths';
|
|
87
|
-
import {
|
|
87
|
+
import { TrailsPanel, TRAIL_PLAY_PAUSE_MS } from './TrailsPanel';
|
|
88
88
|
import {
|
|
89
89
|
EDGE_LABEL_WIDTH,
|
|
90
90
|
EDGE_LABEL_HEIGHT,
|
|
@@ -93,12 +93,12 @@ import {
|
|
|
93
93
|
EDGE_LABEL_CLOUD_EXTRA_TOP,
|
|
94
94
|
} from '../utils/edgeLabel';
|
|
95
95
|
|
|
96
|
-
/** Context passed to `
|
|
97
|
-
export interface
|
|
98
|
-
|
|
96
|
+
/** Context passed to `renderTrailViewer` when a flow/step is focused. */
|
|
97
|
+
export interface TrailViewerContext {
|
|
98
|
+
trail: SubsystemTrail;
|
|
99
99
|
/** Focused step index; `null` means the whole flow (no specific step). */
|
|
100
100
|
stepIndex: number | null;
|
|
101
|
-
/** Open a step's full source file over the
|
|
101
|
+
/** Open a step's full source file over the trail drawer (keeps snippets mounted). */
|
|
102
102
|
onOpenFile: (path: string, opts?: SubsystemOpenFileOptions) => void;
|
|
103
103
|
/**
|
|
104
104
|
* Aliases of components marked `proposed`. A step whose file can't be read
|
|
@@ -111,9 +111,9 @@ export interface WalkthroughViewerContext {
|
|
|
111
111
|
* token names no touched construct. Forward to the code view so constructs
|
|
112
112
|
* read as clickable.
|
|
113
113
|
*/
|
|
114
|
-
resolveSymbol?: (query:
|
|
114
|
+
resolveSymbol?: (query: TrailSymbolQuery) => string | null;
|
|
115
115
|
/** A clicked construct token — open that construct's declaration line. */
|
|
116
|
-
onSymbolClick?: (symbol: string, query:
|
|
116
|
+
onSymbolClick?: (symbol: string, query: TrailSymbolQuery) => void;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/** Identifiers a token could match to name this component as a construct. */
|
|
@@ -131,21 +131,21 @@ function constructIdentifiers(comp: SubsystemComponent): string[] {
|
|
|
131
131
|
|
|
132
132
|
type DrawerTarget =
|
|
133
133
|
| { kind: 'file'; file: string; startLine?: number }
|
|
134
|
-
| { kind: '
|
|
134
|
+
| { kind: 'trail'; trailId: string; stepIndex: number | null };
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
137
|
* Per-model UI state persisted to `localStorage`, keyed by `persistKey`.
|
|
138
|
-
* Stores the
|
|
138
|
+
* Stores the trail working set (which flows are expanded and which
|
|
139
139
|
* flow/step is selected) and which sidebar/diagnostics tab is showing, so
|
|
140
140
|
* tabbing away from a model and back lands you where you left off. Transient
|
|
141
141
|
* things (hover, drag, camera) are not saved.
|
|
142
142
|
*/
|
|
143
143
|
interface PersistedViewState {
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
expandedTrails?: string[];
|
|
145
|
+
focusedTrailId?: string | null;
|
|
146
146
|
focusedStepIndex?: number | null;
|
|
147
147
|
sidebarWidth?: number;
|
|
148
|
-
sidebarView?: 'files' | '
|
|
148
|
+
sidebarView?: 'files' | 'trails';
|
|
149
149
|
diagnosticsTab?: 'issues' | 'agents';
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -181,15 +181,15 @@ function writeViewState(
|
|
|
181
181
|
export interface SubsystemComponentGraphProps {
|
|
182
182
|
components: SubsystemComponent[];
|
|
183
183
|
/**
|
|
184
|
-
* Ordered runtime
|
|
185
|
-
* sidebar's bottom half offers a Files/
|
|
186
|
-
* the flows panel lists each
|
|
184
|
+
* Ordered runtime trails — one trail per flow. When present the
|
|
185
|
+
* sidebar's bottom half offers a Files/Trails toggle:
|
|
186
|
+
* the flows panel lists each trail's steps (`symbol` or `file:line`);
|
|
187
187
|
* clicking a flow row toggles its steps; clicking a step focuses that
|
|
188
|
-
* step's edge and (when `
|
|
188
|
+
* step's edge and (when `renderTrailViewer` is set) opens the bottom
|
|
189
189
|
* drawer on that flow's snippets. Opened flows stay on the canvas
|
|
190
190
|
* (unselected ones dimmed); everything else is hidden.
|
|
191
191
|
*/
|
|
192
|
-
|
|
192
|
+
trails?: SubsystemTrail[];
|
|
193
193
|
/**
|
|
194
194
|
* graphify-native relations (raw static-graph edges: `imports`, `contains`,
|
|
195
195
|
* `re_exports`, …). Display-only, drawn with the separate
|
|
@@ -204,20 +204,20 @@ export interface SubsystemComponentGraphProps {
|
|
|
204
204
|
*/
|
|
205
205
|
orderByLine?: boolean;
|
|
206
206
|
/**
|
|
207
|
-
* Deep-link target: when set, the matching
|
|
207
|
+
* Deep-link target: when set, the matching trail is selected on mount
|
|
208
208
|
* — its steps expanded and its flow focused on the canvas. Hosts use this
|
|
209
|
-
* when opening the graph from a
|
|
209
|
+
* when opening the graph from a trail row in a list. Re-applies on a
|
|
210
210
|
* new id (or a remount); in-tab selection afterwards stays owned by the
|
|
211
211
|
* graph.
|
|
212
212
|
*/
|
|
213
|
-
|
|
213
|
+
initialTrailId?: string | null;
|
|
214
214
|
/**
|
|
215
|
-
* Called with the next
|
|
215
|
+
* Called with the next trail order after a drag in the sidebar's
|
|
216
216
|
* flows panel. When set, each row grows a drag grip and a drop emits the
|
|
217
217
|
* reordered array — the host owns persisting it (the graph stays controlled
|
|
218
218
|
* and never reorders its own prop).
|
|
219
219
|
*/
|
|
220
|
-
|
|
220
|
+
onReorderTrails?: (next: SubsystemTrail[]) => void;
|
|
221
221
|
onSelect?: (componentAlias: string) => void;
|
|
222
222
|
/** Called when an edge is clicked (relationship / mechanism + refs seam). */
|
|
223
223
|
onEdgeSelect?: (edge: SubsystemComponentEdge) => void;
|
|
@@ -242,50 +242,50 @@ export interface SubsystemComponentGraphProps {
|
|
|
242
242
|
*/
|
|
243
243
|
moduleNesting?: 'exact' | 'path';
|
|
244
244
|
/**
|
|
245
|
-
* Which edge source the canvas draws. The graphify and
|
|
245
|
+
* Which edge source the canvas draws. The graphify and trail sources
|
|
246
246
|
* are disjoint, so a graph carrying both shows one or the other — never
|
|
247
247
|
* both. Edges outside the view are hidden (labels go too).
|
|
248
248
|
* - `graphify`: graphify-native static edges (`imports`, `contains`, …)
|
|
249
|
-
* - `
|
|
249
|
+
* - `trails`: trail step edges (`calls`, `feeds`, …), including
|
|
250
250
|
* step numbers when a flow is focused/hovered
|
|
251
|
-
* Leave unset to let the sidebar's Files /
|
|
252
|
-
* draws graphify edges,
|
|
253
|
-
* tabs, defaults to `
|
|
251
|
+
* Leave unset to let the sidebar's Files / Trails tab drive it: Files
|
|
252
|
+
* draws graphify edges, Trails draws runtime steps. Without visible
|
|
253
|
+
* tabs, defaults to `trails`.
|
|
254
254
|
*/
|
|
255
255
|
edgeView?: SubsystemEdgeView;
|
|
256
256
|
/** Subsystem title displayed in the sidebar. */
|
|
257
257
|
title?: string;
|
|
258
258
|
/**
|
|
259
259
|
* Suppresses the sidebar entirely (title, description, file tree,
|
|
260
|
-
*
|
|
260
|
+
* trails) for graph-only embeds. Pair with `graphTitle` to keep the
|
|
261
261
|
* subsystem name visible as an overlay on the canvas.
|
|
262
262
|
*/
|
|
263
263
|
hideSidebar?: boolean;
|
|
264
264
|
/**
|
|
265
|
-
* How
|
|
265
|
+
* How trail step highlighting behaves on the canvas.
|
|
266
266
|
* - `focus` (default): zoom to the step, hide non-participants, open drawer
|
|
267
267
|
* - `dim`: keep the full graph, dim non-participants (same as hovering a step)
|
|
268
268
|
*/
|
|
269
|
-
|
|
269
|
+
trailStepMode?: 'focus' | 'dim';
|
|
270
270
|
/**
|
|
271
|
-
* When true, cycles
|
|
272
|
-
* (no zoom, no drawer). Loops across all
|
|
271
|
+
* When true, cycles trail steps automatically using `dim` highlighting
|
|
272
|
+
* (no zoom, no drawer). Loops across all trails that have steps.
|
|
273
273
|
* Useful for graph-only embeds (`hideSidebar`).
|
|
274
274
|
*/
|
|
275
|
-
|
|
275
|
+
autoPlayTrails?: boolean;
|
|
276
276
|
/** Pause between autoplay steps in ms. @default 2500 */
|
|
277
|
-
|
|
277
|
+
trailAutoPlayIntervalMs?: number;
|
|
278
278
|
/**
|
|
279
|
-
* When false, focusing a
|
|
279
|
+
* When false, focusing a trail/step does not call `fitView`.
|
|
280
280
|
* @default true
|
|
281
281
|
*/
|
|
282
|
-
|
|
282
|
+
zoomOnTrailFocus?: boolean;
|
|
283
283
|
/**
|
|
284
|
-
* Duration (ms) of the camera pan/zoom when a
|
|
284
|
+
* Duration (ms) of the camera pan/zoom when a trail step or flow is
|
|
285
285
|
* focused. Higher = a slower, more legible flight between steps.
|
|
286
286
|
* @default 300
|
|
287
287
|
*/
|
|
288
|
-
|
|
288
|
+
trailFocusDurationMs?: number;
|
|
289
289
|
/**
|
|
290
290
|
* Subsystem title rendered as a non-interactive overlay chip on the graph
|
|
291
291
|
* canvas (top-center). Does not trigger the sidebar — for graph-only
|
|
@@ -293,11 +293,11 @@ export interface SubsystemComponentGraphProps {
|
|
|
293
293
|
*/
|
|
294
294
|
graphTitle?: string;
|
|
295
295
|
/**
|
|
296
|
-
* When true, shows the active
|
|
296
|
+
* When true, shows the active trail's title as a non-interactive
|
|
297
297
|
* overlay chip on the canvas (under `graphTitle` when both are set).
|
|
298
|
-
* Uses the focused or hover-highlighted
|
|
298
|
+
* Uses the focused or hover-highlighted trail.
|
|
299
299
|
*/
|
|
300
|
-
|
|
300
|
+
showTrailTitle?: boolean;
|
|
301
301
|
/** Markdown description rendered in the sidebar. */
|
|
302
302
|
description?: string;
|
|
303
303
|
/**
|
|
@@ -327,13 +327,13 @@ export interface SubsystemComponentGraphProps {
|
|
|
327
327
|
* Diagnostics status chip in the sidebar title row (beside the description
|
|
328
328
|
* toggle). Shows the last verification pass's state as an icon color + count,
|
|
329
329
|
* and toggles the sidebar between the diagnostics list and the normal
|
|
330
|
-
* files/
|
|
330
|
+
* files/trails view. Omit to hide the chip.
|
|
331
331
|
*/
|
|
332
332
|
diagnostic?: SubsystemDiagnostic;
|
|
333
333
|
/**
|
|
334
334
|
* Verification issues for this graph (audit findings). When diagnostics are
|
|
335
335
|
* active the sidebar's bottom panel becomes the issue list instead of the
|
|
336
|
-
* file tree /
|
|
336
|
+
* file tree / trails.
|
|
337
337
|
*/
|
|
338
338
|
issues?: SubsystemIssue[];
|
|
339
339
|
/**
|
|
@@ -349,7 +349,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
349
349
|
focusIssueCategory?: SubsystemIssueCategory;
|
|
350
350
|
/**
|
|
351
351
|
* Click an issue. The graph first focuses the target on the canvas itself —
|
|
352
|
-
* a component target is selected and framed (any edge /
|
|
352
|
+
* a component target is selected and framed (any edge / trail focus
|
|
353
353
|
* that would hide it is cleared), and a
|
|
354
354
|
* module target frames that boundary frame — then this fires so the host can
|
|
355
355
|
* open its own detail. Collapsing the card again reverses that (deselect +
|
|
@@ -368,13 +368,13 @@ export interface SubsystemComponentGraphProps {
|
|
|
368
368
|
*/
|
|
369
369
|
renderFileViewer?: (file: string, opts?: SubsystemOpenFileOptions) => ReactNode;
|
|
370
370
|
/**
|
|
371
|
-
* Host-injected multi-snippet viewer for a focused
|
|
371
|
+
* Host-injected multi-snippet viewer for a focused trail. When set,
|
|
372
372
|
* clicking a flow step opens the bottom drawer with this content and
|
|
373
373
|
* updates it as the focused step changes.
|
|
374
374
|
*/
|
|
375
|
-
|
|
375
|
+
renderTrailViewer?: (ctx: TrailViewerContext) => ReactNode;
|
|
376
376
|
/**
|
|
377
|
-
* Suppress the bottom file/
|
|
377
|
+
* Suppress the bottom file/trail drawer entirely. Focusing a step then
|
|
378
378
|
* only frames it on the canvas (and dims the rest) without dropping a snippet
|
|
379
379
|
* panel below — for embeds that want the camera to tell the story. Defaults
|
|
380
380
|
* to false.
|
|
@@ -406,7 +406,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
406
406
|
ref: DeclarationSymbolRef;
|
|
407
407
|
}) => Promise<SymbolInspection | null> | SymbolInspection | null;
|
|
408
408
|
/**
|
|
409
|
-
* When set, the graph's
|
|
409
|
+
* When set, the graph's trail working set — expanded flows and the
|
|
410
410
|
* selected flow/step — is persisted to `localStorage` under this key and
|
|
411
411
|
* restored on mount. Hosts key it by the model id so each model remembers
|
|
412
412
|
* where the user left off. Omit to keep the state purely in-memory.
|
|
@@ -489,27 +489,27 @@ function FileOverlayCloseButton({ onClose }: { onClose: () => void }) {
|
|
|
489
489
|
);
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
const
|
|
492
|
+
const TrailDrawerContent = memo(function TrailDrawerContent({
|
|
493
493
|
render,
|
|
494
|
-
|
|
494
|
+
trail,
|
|
495
495
|
stepIndex,
|
|
496
496
|
onOpenFile,
|
|
497
497
|
proposedAliases,
|
|
498
498
|
resolveSymbol,
|
|
499
499
|
onSymbolClick,
|
|
500
500
|
}: {
|
|
501
|
-
render: (ctx:
|
|
502
|
-
|
|
501
|
+
render: (ctx: TrailViewerContext) => ReactNode;
|
|
502
|
+
trail: SubsystemTrail;
|
|
503
503
|
stepIndex: number | null;
|
|
504
504
|
onOpenFile: (path: string, opts?: SubsystemOpenFileOptions) => void;
|
|
505
505
|
proposedAliases: ReadonlySet<string>;
|
|
506
|
-
resolveSymbol?: (query:
|
|
507
|
-
onSymbolClick?: (symbol: string, query:
|
|
506
|
+
resolveSymbol?: (query: TrailSymbolQuery) => string | null;
|
|
507
|
+
onSymbolClick?: (symbol: string, query: TrailSymbolQuery) => void;
|
|
508
508
|
}) {
|
|
509
509
|
return (
|
|
510
510
|
<>
|
|
511
511
|
{render({
|
|
512
|
-
|
|
512
|
+
trail,
|
|
513
513
|
stepIndex,
|
|
514
514
|
onOpenFile,
|
|
515
515
|
proposedAliases,
|
|
@@ -524,7 +524,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
524
524
|
measured: { w: number; h: number } | null;
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
-
function Inner({ components,
|
|
527
|
+
function Inner({ components, trails, graphifyRelations, orderByLine, initialTrailId, onReorderTrails, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, moduleNesting, edgeView, title, hideSidebar, trailStepMode = 'focus', autoPlayTrails = false, trailAutoPlayIntervalMs = TRAIL_PLAY_PAUSE_MS, zoomOnTrailFocus = true, trailFocusDurationMs = 300, graphTitle, showTrailTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderTrailViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, hideDrawer = false, persistKey, liveEvents, agentsPanel }: InnerProps) {
|
|
528
528
|
const { theme } = useTheme();
|
|
529
529
|
const { fitView, fitBounds, screenToFlowPosition } = useReactFlow();
|
|
530
530
|
const viewport = useViewport();
|
|
@@ -532,8 +532,8 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
532
532
|
// graph is its own tab/mount, so `persistKey` is stable for a mount.
|
|
533
533
|
const persisted = useMemo(() => readViewState(persistKey), [persistKey]);
|
|
534
534
|
const graphEdges = useMemo(
|
|
535
|
-
() => deriveGraphEdges({
|
|
536
|
-
[
|
|
535
|
+
() => deriveGraphEdges({ trails }),
|
|
536
|
+
[trails],
|
|
537
537
|
);
|
|
538
538
|
const [built, setBuilt] = useState<{
|
|
539
539
|
nodes: Node[];
|
|
@@ -546,9 +546,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
546
546
|
});
|
|
547
547
|
const [layoutReady, setLayoutReady] = useState(false);
|
|
548
548
|
const [selected, setSelected] = useState<SubsystemComponent | null>(null);
|
|
549
|
-
/** Bottom drawer: single file or
|
|
549
|
+
/** Bottom drawer: single file or trail multi-snippet mode. */
|
|
550
550
|
const [drawerTarget, setDrawerTarget] = useState<DrawerTarget | null>(null);
|
|
551
|
-
/** Full-file layer over an open
|
|
551
|
+
/** Full-file layer over an open trail drawer — trail stays mounted. */
|
|
552
552
|
const [fileOverlay, setFileOverlay] = useState<{
|
|
553
553
|
file: string;
|
|
554
554
|
startLine?: number;
|
|
@@ -628,52 +628,58 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
628
628
|
if (raf) cancelAnimationFrame(raf);
|
|
629
629
|
};
|
|
630
630
|
}, [pathFrameRects, screenToFlowPosition]);
|
|
631
|
-
//
|
|
631
|
+
// Trail focus — selected flow (or step) is full strength; other
|
|
632
632
|
// opened-flow members stay visible but dimmed; everything else is hidden.
|
|
633
633
|
// Restored from the persisted view state when a `persistKey` is set.
|
|
634
|
-
const [
|
|
635
|
-
persisted.
|
|
634
|
+
const [focusedTrailId, setFocusedTrailId] = useState<string | null>(
|
|
635
|
+
persisted.focusedTrailId ?? null,
|
|
636
636
|
);
|
|
637
637
|
// `null` = whole flow focused; a number = that single step's edge focused.
|
|
638
638
|
const [focusedStepIndex, setFocusedStepIndex] = useState<number | null>(
|
|
639
639
|
persisted.focusedStepIndex ?? null,
|
|
640
640
|
);
|
|
641
|
-
// Hovered
|
|
641
|
+
// Hovered trail in the flows panel: dims every canvas node/edge not
|
|
642
642
|
// involved in the hover preview (or selected ∪ hovered when a step is
|
|
643
643
|
// focused). `stepIndex: null` = whole flow (collapsed title hover);
|
|
644
644
|
// a number = that step. Transient — no drawer. Camera only reframes when
|
|
645
645
|
// a step is already selected.
|
|
646
|
-
const [
|
|
647
|
-
|
|
646
|
+
const [hoveredTrailStep, setHoveredTrailStep] = useState<{
|
|
647
|
+
trailId: string;
|
|
648
648
|
stepIndex: number | null;
|
|
649
649
|
} | null>(null);
|
|
650
|
-
// Sidebar bottom half: which panel is shown when
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
650
|
+
// Sidebar bottom half: which panel is shown when trails exist.
|
|
651
|
+
// The persisted value is membership-checked rather than trusted: a stale
|
|
652
|
+
// localStorage entry from before the walkthrough->trail rename would match
|
|
653
|
+
// neither panel and render a blank sidebar with no selected tab.
|
|
654
|
+
const [sidebarView, setSidebarView] = useState<'files' | 'trails'>(() => {
|
|
655
|
+
const stored = persisted.sidebarView;
|
|
656
|
+
const restored: 'files' | 'trails' =
|
|
657
|
+
stored === 'files' || stored === 'trails' ? stored : 'trails';
|
|
658
|
+
return trails?.length ? restored : 'files';
|
|
659
|
+
});
|
|
654
660
|
// Diagnostics area tab: issues list vs the Maintain agent pipeline.
|
|
655
661
|
const [diagnosticsTab, setDiagnosticsTab] = useState<'issues' | 'agents'>(
|
|
656
662
|
() => persisted.diagnosticsTab ?? 'issues',
|
|
657
663
|
);
|
|
658
664
|
// One edge source at a time. When the caller doesn't pick, the sidebar's
|
|
659
|
-
// Files /
|
|
660
|
-
//
|
|
661
|
-
// or a sidebar-less embed) fall back to
|
|
662
|
-
const sidebarTabsVisible = !hideSidebar && (
|
|
665
|
+
// Files / Trails tab picks: Files draws graphify static edges,
|
|
666
|
+
// Trails draws runtime step edges. Without visible tabs (no trails,
|
|
667
|
+
// or a sidebar-less embed) fall back to trail edges.
|
|
668
|
+
const sidebarTabsVisible = !hideSidebar && (trails?.length ?? 0) > 0;
|
|
663
669
|
const resolvedEdgeView: SubsystemEdgeView =
|
|
664
670
|
edgeView ??
|
|
665
671
|
(sidebarTabsVisible
|
|
666
|
-
? (sidebarView === '
|
|
667
|
-
: (
|
|
668
|
-
? '
|
|
672
|
+
? (sidebarView === 'trails' ? 'trails' : 'graphify')
|
|
673
|
+
: (trails?.length ?? 0) > 0
|
|
674
|
+
? 'trails'
|
|
669
675
|
: (graphifyRelations?.length ?? 0) > 0
|
|
670
676
|
? 'graphify'
|
|
671
|
-
: '
|
|
672
|
-
//
|
|
677
|
+
: 'trails');
|
|
678
|
+
// Trail flows the user has expanded (via the title row). Closed by
|
|
673
679
|
// default so a graph with several flows doesn't dump every step list at once.
|
|
674
680
|
// Restored from the persisted view state when a `persistKey` is set.
|
|
675
|
-
const [
|
|
676
|
-
() => new Set(persisted.
|
|
681
|
+
const [expandedTrails, setExpandedTrails] = useState<Set<string>>(
|
|
682
|
+
() => new Set(persisted.expandedTrails ?? []),
|
|
677
683
|
);
|
|
678
684
|
// Sidebar description visibility. Hidden by default so the files/flows
|
|
679
685
|
// panel gets the vertical room; the title-row toggle reveals it.
|
|
@@ -732,18 +738,18 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
732
738
|
document.removeEventListener('mouseup', onUp);
|
|
733
739
|
};
|
|
734
740
|
}, [sidebarDrag, sidebarMaxWidth, fitView, persistKey]);
|
|
735
|
-
// Persist the
|
|
741
|
+
// Persist the trail working set per model. Each write merges into the
|
|
736
742
|
// stored blob, so the three fields never clobber one another.
|
|
737
743
|
useEffect(() => {
|
|
738
744
|
if (!persistKey) return;
|
|
739
745
|
writeViewState(persistKey, {
|
|
740
|
-
|
|
746
|
+
expandedTrails: Array.from(expandedTrails),
|
|
741
747
|
});
|
|
742
|
-
}, [persistKey,
|
|
748
|
+
}, [persistKey, expandedTrails]);
|
|
743
749
|
useEffect(() => {
|
|
744
750
|
if (!persistKey) return;
|
|
745
|
-
writeViewState(persistKey, {
|
|
746
|
-
}, [persistKey,
|
|
751
|
+
writeViewState(persistKey, { focusedTrailId });
|
|
752
|
+
}, [persistKey, focusedTrailId]);
|
|
747
753
|
useEffect(() => {
|
|
748
754
|
if (!persistKey) return;
|
|
749
755
|
writeViewState(persistKey, { focusedStepIndex });
|
|
@@ -776,59 +782,59 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
776
782
|
? { file: drawerTarget.file, startLine: drawerTarget.startLine }
|
|
777
783
|
: null;
|
|
778
784
|
|
|
779
|
-
const
|
|
780
|
-
if (drawerTarget?.kind !== '
|
|
781
|
-
return
|
|
782
|
-
}, [drawerTarget,
|
|
785
|
+
const focusedTrail = useMemo(() => {
|
|
786
|
+
if (drawerTarget?.kind !== 'trail' || !trails) return null;
|
|
787
|
+
return trails.find((t) => t.id === drawerTarget.trailId) ?? null;
|
|
788
|
+
}, [drawerTarget, trails]);
|
|
783
789
|
// Autoplay focus: a sidebar-less embed has no expanded flow to "open", so
|
|
784
790
|
// branding every step as a selection would hide all other nodes. Instead the
|
|
785
|
-
// autoplay sets this (flow id + step) to frame the
|
|
791
|
+
// autoplay sets this (flow id + step) to frame the step and dim the rest —
|
|
786
792
|
// same treatment as hover — without hiding anything. Declared before the
|
|
787
793
|
// overlay memos, which read it to title the canvas chip + step bar.
|
|
788
794
|
const [autoPlayFocus, setAutoPlayFocus] = useState<
|
|
789
|
-
{
|
|
795
|
+
{ trailId: string; stepIndex: number } | null
|
|
790
796
|
>(null);
|
|
791
|
-
// Ref mirror of the drawer's
|
|
797
|
+
// Ref mirror of the drawer's trail id so the symbol resolver stays
|
|
792
798
|
// stable across graph re-renders (the drawer is memoized on callback identity).
|
|
793
|
-
const
|
|
794
|
-
|
|
799
|
+
const drawerTrailIdRef = useRef<string | null>(null);
|
|
800
|
+
drawerTrailIdRef.current = focusedTrail?.id ?? null;
|
|
795
801
|
|
|
796
|
-
//
|
|
802
|
+
// Trail shown on the canvas title chip (focus, hover highlight, or
|
|
797
803
|
// autoplay focus).
|
|
798
|
-
const
|
|
799
|
-
if (!
|
|
804
|
+
const overlayTrailTitle = useMemo(() => {
|
|
805
|
+
if (!showTrailTitle || !trails?.length) return null;
|
|
800
806
|
const id =
|
|
801
|
-
|
|
802
|
-
autoPlayFocus?.
|
|
803
|
-
|
|
807
|
+
focusedTrailId ??
|
|
808
|
+
autoPlayFocus?.trailId ??
|
|
809
|
+
hoveredTrailStep?.trailId;
|
|
804
810
|
if (!id) return null;
|
|
805
|
-
return
|
|
811
|
+
return trails.find((t) => t.id === id)?.title ?? null;
|
|
806
812
|
}, [
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
813
|
+
showTrailTitle,
|
|
814
|
+
trails,
|
|
815
|
+
focusedTrailId,
|
|
810
816
|
autoPlayFocus,
|
|
811
|
-
|
|
817
|
+
hoveredTrailStep,
|
|
812
818
|
]);
|
|
813
819
|
|
|
814
820
|
// Active step for the bottom-of-title progress + annotation chip.
|
|
815
|
-
const
|
|
816
|
-
if (!
|
|
821
|
+
const overlayTrailStep = useMemo(() => {
|
|
822
|
+
if (!showTrailTitle || !trails?.length) return null;
|
|
817
823
|
const tlId =
|
|
818
|
-
|
|
819
|
-
autoPlayFocus?.
|
|
820
|
-
|
|
824
|
+
focusedTrailId ??
|
|
825
|
+
autoPlayFocus?.trailId ??
|
|
826
|
+
hoveredTrailStep?.trailId ??
|
|
821
827
|
null;
|
|
822
828
|
let stepIndex: number | null = null;
|
|
823
|
-
if (
|
|
829
|
+
if (focusedTrailId != null) {
|
|
824
830
|
stepIndex = focusedStepIndex;
|
|
825
831
|
} else if (autoPlayFocus != null) {
|
|
826
832
|
stepIndex = autoPlayFocus.stepIndex;
|
|
827
|
-
} else if (
|
|
828
|
-
stepIndex =
|
|
833
|
+
} else if (hoveredTrailStep != null) {
|
|
834
|
+
stepIndex = hoveredTrailStep.stepIndex;
|
|
829
835
|
}
|
|
830
836
|
if (tlId == null || stepIndex == null) return null;
|
|
831
|
-
const tl =
|
|
837
|
+
const tl = trails.find((t) => t.id === tlId);
|
|
832
838
|
const step = tl?.steps[stepIndex];
|
|
833
839
|
if (!step || !tl) return null;
|
|
834
840
|
return {
|
|
@@ -837,25 +843,25 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
837
843
|
annotation: step.annotation,
|
|
838
844
|
};
|
|
839
845
|
}, [
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
846
|
+
showTrailTitle,
|
|
847
|
+
trails,
|
|
848
|
+
focusedTrailId,
|
|
843
849
|
focusedStepIndex,
|
|
844
850
|
autoPlayFocus,
|
|
845
|
-
|
|
851
|
+
hoveredTrailStep,
|
|
846
852
|
]);
|
|
847
853
|
|
|
848
854
|
const drawerTitle = useMemo(() => {
|
|
849
855
|
if (!drawerTarget) return null;
|
|
850
856
|
if (drawerTarget.kind === 'file') return drawerTarget.file;
|
|
851
|
-
const tl =
|
|
857
|
+
const tl = focusedTrail;
|
|
852
858
|
if (!tl) return null;
|
|
853
859
|
if (drawerTarget.stepIndex == null) return tl.title;
|
|
854
860
|
const step = tl.steps[drawerTarget.stepIndex];
|
|
855
861
|
if (!step) return tl.title;
|
|
856
862
|
const site = `${step.file.split('/').pop() ?? step.file}:${step.line}`;
|
|
857
863
|
return `${tl.title} · ${site}`;
|
|
858
|
-
}, [drawerTarget,
|
|
864
|
+
}, [drawerTarget, focusedTrail]);
|
|
859
865
|
|
|
860
866
|
// Refresh selected component when the components list updates (e.g. verify
|
|
861
867
|
// writes back declarationRef).
|
|
@@ -869,14 +875,14 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
869
875
|
// The pane stays hidden until Pass 2 completes. Key off layout-affecting
|
|
870
876
|
// fields only — declarationRef updates after verify must not re-run ELK.
|
|
871
877
|
const layoutKey = useMemo(
|
|
872
|
-
() => subsystemGraphLayoutKey({ components,
|
|
873
|
-
[components,
|
|
878
|
+
() => subsystemGraphLayoutKey({ components, trails, graphifyRelations }),
|
|
879
|
+
[components, trails, graphifyRelations],
|
|
874
880
|
);
|
|
875
881
|
const componentsRef = useRef(components);
|
|
876
|
-
const
|
|
882
|
+
const trailsRef = useRef(trails);
|
|
877
883
|
const graphifyRelationsRef = useRef(graphifyRelations);
|
|
878
884
|
componentsRef.current = components;
|
|
879
|
-
|
|
885
|
+
trailsRef.current = trails;
|
|
880
886
|
graphifyRelationsRef.current = graphifyRelations;
|
|
881
887
|
|
|
882
888
|
// Track measured dimensions from React Flow's dimension changes.
|
|
@@ -896,7 +902,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
896
902
|
pass2GenRef.current += 1;
|
|
897
903
|
const doc = {
|
|
898
904
|
components: componentsRef.current,
|
|
899
|
-
|
|
905
|
+
trails: trailsRef.current,
|
|
900
906
|
};
|
|
901
907
|
void buildSubsystemGraph(doc, {
|
|
902
908
|
maxNodeWidth,
|
|
@@ -953,7 +959,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
953
959
|
const measuredHeights = new Map(leafNodes.map((n) => [n.id, dims.get(n.id)!.height]));
|
|
954
960
|
const gen = ++pass2GenRef.current;
|
|
955
961
|
void buildSubsystemGraph(
|
|
956
|
-
{ components,
|
|
962
|
+
{ components, trails },
|
|
957
963
|
{ maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, moduleNesting, graphifyRelations, orderByLine },
|
|
958
964
|
)
|
|
959
965
|
.then(({ nodes, edges: e, absoluteRects }) => {
|
|
@@ -967,7 +973,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
967
973
|
// Reveal Pass 1 layout rather than leaving the cover up forever.
|
|
968
974
|
setLayoutReady(true);
|
|
969
975
|
});
|
|
970
|
-
}, [built.nodes, components,
|
|
976
|
+
}, [built.nodes, components, trails, graphifyRelations, orderByLine, maxNodeWidth, showEdgeLabels, moduleNesting]);
|
|
971
977
|
|
|
972
978
|
// After Pass 1 commits, try Pass 2 immediately with retained measurements.
|
|
973
979
|
// Same-id live updates often get no new React Flow `dimensions` events, so
|
|
@@ -1008,8 +1014,8 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1008
1014
|
}
|
|
1009
1015
|
const src = edgeById.get(edgeId);
|
|
1010
1016
|
setSelectedEdgeId(edgeId);
|
|
1011
|
-
// A direct edge selection on the canvas supersedes any
|
|
1012
|
-
|
|
1017
|
+
// A direct edge selection on the canvas supersedes any trail focus.
|
|
1018
|
+
setFocusedTrailId(null);
|
|
1013
1019
|
setFocusedStepIndex(null);
|
|
1014
1020
|
if (src) onEdgeSelect?.(src);
|
|
1015
1021
|
},
|
|
@@ -1029,7 +1035,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1029
1035
|
}
|
|
1030
1036
|
setSelected(comp);
|
|
1031
1037
|
setSelectedEdgeId(null);
|
|
1032
|
-
|
|
1038
|
+
setFocusedTrailId(null);
|
|
1033
1039
|
setFocusedStepIndex(null);
|
|
1034
1040
|
onSelect?.(alias);
|
|
1035
1041
|
}
|
|
@@ -1083,22 +1089,22 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1083
1089
|
[components],
|
|
1084
1090
|
);
|
|
1085
1091
|
|
|
1086
|
-
// A `step` issue target names ONE step of a
|
|
1092
|
+
// A `step` issue target names ONE step of a trail — the trail id in
|
|
1087
1093
|
// `id`, the 0-based step position in `stepIndex`. Validated against the loaded
|
|
1088
|
-
//
|
|
1094
|
+
// trails so a finding left over from an edited flow resolves to null
|
|
1089
1095
|
// rather than framing whatever now happens to sit at that index.
|
|
1090
1096
|
const issueStep = useCallback(
|
|
1091
1097
|
(
|
|
1092
1098
|
issue: SubsystemIssue,
|
|
1093
|
-
): {
|
|
1099
|
+
): { trail: SubsystemTrail; stepIndex: number } | null => {
|
|
1094
1100
|
const target = issue.target;
|
|
1095
1101
|
if (target?.kind !== 'step') return null;
|
|
1096
1102
|
if (target.id == null || target.stepIndex == null) return null;
|
|
1097
|
-
const
|
|
1098
|
-
if (!
|
|
1099
|
-
return {
|
|
1103
|
+
const trail = trails?.find((t) => t.id === target.id);
|
|
1104
|
+
if (!trail?.steps[target.stepIndex]) return null;
|
|
1105
|
+
return { trail, stepIndex: target.stepIndex };
|
|
1100
1106
|
},
|
|
1101
|
-
[
|
|
1107
|
+
[trails],
|
|
1102
1108
|
);
|
|
1103
1109
|
|
|
1104
1110
|
// Per-node diagnostics badge: fold each component-targeted finding that maps
|
|
@@ -1194,7 +1200,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1194
1200
|
// target:
|
|
1195
1201
|
// component → that node
|
|
1196
1202
|
// module → every component in that module
|
|
1197
|
-
//
|
|
1203
|
+
// trail → the flow's nodes + its step edges
|
|
1198
1204
|
// repo → every component of that repo
|
|
1199
1205
|
// graph → whole-graph finding; implicates nothing specific
|
|
1200
1206
|
// Returns null when nothing is expanded, no expanded layer has findings, or
|
|
@@ -1225,15 +1231,15 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1225
1231
|
nodeIds.add(c.alias);
|
|
1226
1232
|
}
|
|
1227
1233
|
}
|
|
1228
|
-
} else if (t.kind === '
|
|
1229
|
-
const wt = (
|
|
1234
|
+
} else if (t.kind === 'trail') {
|
|
1235
|
+
const wt = (trails ?? []).find(
|
|
1230
1236
|
(w) => w.id === t.id || w.title === t.label,
|
|
1231
1237
|
);
|
|
1232
1238
|
if (wt) {
|
|
1233
1239
|
for (const s of wt.steps) {
|
|
1234
1240
|
nodeIds.add(s.from);
|
|
1235
1241
|
nodeIds.add(s.to);
|
|
1236
|
-
edgeIds.add(
|
|
1242
|
+
edgeIds.add(trailStepGraphEdgeId(s));
|
|
1237
1243
|
}
|
|
1238
1244
|
}
|
|
1239
1245
|
} else if (t.kind === 'repo') {
|
|
@@ -1250,7 +1256,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1250
1256
|
if (nodeIds.has(e.source) && nodeIds.has(e.target)) edgeIds.add(e.id);
|
|
1251
1257
|
}
|
|
1252
1258
|
return { nodeIds, edgeIds };
|
|
1253
|
-
}, [issues, expandedIssueCategories, components,
|
|
1259
|
+
}, [issues, expandedIssueCategories, components, trails, resolveAlias]);
|
|
1254
1260
|
const issueFocusNodeIds = issueFocus?.nodeIds ?? null;
|
|
1255
1261
|
const issueFocusEdgeIds = issueFocus?.edgeIds ?? null;
|
|
1256
1262
|
|
|
@@ -1262,58 +1268,58 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1262
1268
|
selectedEdgeIdRef.current = selectedEdgeId;
|
|
1263
1269
|
// Ref mirrors of the focused step, so `unfocusIssueTarget` (a useCallback
|
|
1264
1270
|
// over early deps) can tell a step focus it still owns from one the user has
|
|
1265
|
-
// since moved elsewhere in the
|
|
1266
|
-
const focusedStepRef = useRef<{
|
|
1271
|
+
// since moved elsewhere in the trail panel.
|
|
1272
|
+
const focusedStepRef = useRef<{ trailId: string; stepIndex: number } | null>(
|
|
1267
1273
|
null,
|
|
1268
1274
|
);
|
|
1269
1275
|
focusedStepRef.current =
|
|
1270
|
-
|
|
1271
|
-
? {
|
|
1276
|
+
focusedTrailId != null && focusedStepIndex != null
|
|
1277
|
+
? { trailId: focusedTrailId, stepIndex: focusedStepIndex }
|
|
1272
1278
|
: null;
|
|
1273
1279
|
|
|
1274
|
-
// Edge ids in
|
|
1275
|
-
// step's edge). Used to frame the camera. `null` = no
|
|
1280
|
+
// Edge ids in trail focus (an active flow's edge set, or a single
|
|
1281
|
+
// step's edge). Used to frame the camera. `null` = no trail focus.
|
|
1276
1282
|
const focusEdgeIds = useMemo(() => { if (autoPlayFocus) {
|
|
1277
|
-
const tl =
|
|
1283
|
+
const tl = trails?.find((t) => t.id === autoPlayFocus.trailId);
|
|
1278
1284
|
const step = tl?.steps[autoPlayFocus.stepIndex];
|
|
1279
|
-
return step ? new Set([
|
|
1285
|
+
return step ? new Set([trailStepGraphEdgeId(step)]) : null;
|
|
1280
1286
|
}
|
|
1281
|
-
if (
|
|
1282
|
-
const tl =
|
|
1287
|
+
if (focusedTrailId == null || !trails) return null;
|
|
1288
|
+
const tl = trails.find((t) => t.id === focusedTrailId);
|
|
1283
1289
|
if (!tl) return null;
|
|
1284
1290
|
if (focusedStepIndex != null) {
|
|
1285
1291
|
const step = tl.steps[focusedStepIndex];
|
|
1286
|
-
return step ? new Set([
|
|
1292
|
+
return step ? new Set([trailStepGraphEdgeId(step)]) : null;
|
|
1287
1293
|
}
|
|
1288
|
-
return new Set(tl.steps.map((s) =>
|
|
1289
|
-
}, [
|
|
1294
|
+
return new Set(tl.steps.map((s) => trailStepGraphEdgeId(s)));
|
|
1295
|
+
}, [trails, focusedTrailId, focusedStepIndex, autoPlayFocus]);
|
|
1290
1296
|
|
|
1291
1297
|
// 1-based step numbers per edge of the active flow (focused or
|
|
1292
1298
|
// hover/autoplay-highlighted). An edge can appear in more than one step.
|
|
1293
1299
|
const selectedFlowStepNos = useMemo(() => {
|
|
1294
|
-
const activeId =
|
|
1295
|
-
if (activeId == null || !
|
|
1296
|
-
const tl =
|
|
1300
|
+
const activeId = focusedTrailId ?? hoveredTrailStep?.trailId;
|
|
1301
|
+
if (activeId == null || !trails) return null;
|
|
1302
|
+
const tl = trails.find((t) => t.id === activeId);
|
|
1297
1303
|
if (!tl) return null;
|
|
1298
1304
|
const map = new Map<string, number[]>();
|
|
1299
1305
|
tl.steps.forEach((s, i) => {
|
|
1300
|
-
const list = map.get(
|
|
1306
|
+
const list = map.get(trailStepGraphEdgeId(s)) ?? [];
|
|
1301
1307
|
list.push(i + 1);
|
|
1302
|
-
map.set(
|
|
1308
|
+
map.set(trailStepGraphEdgeId(s), list);
|
|
1303
1309
|
});
|
|
1304
1310
|
return map;
|
|
1305
|
-
}, [
|
|
1311
|
+
}, [trails, focusedTrailId, hoveredTrailStep]);
|
|
1306
1312
|
|
|
1307
|
-
// Union of every expanded (opened)
|
|
1313
|
+
// Union of every expanded (opened) trail's edges — the visible set.
|
|
1308
1314
|
const openedEdgeIds = useMemo(() => {
|
|
1309
|
-
if (!
|
|
1315
|
+
if (!trails || expandedTrails.size === 0) return null;
|
|
1310
1316
|
const ids = new Set<string>();
|
|
1311
|
-
for (const tl of
|
|
1312
|
-
if (!
|
|
1313
|
-
for (const s of tl.steps) ids.add(
|
|
1317
|
+
for (const tl of trails) {
|
|
1318
|
+
if (!expandedTrails.has(tl.id)) continue;
|
|
1319
|
+
for (const s of tl.steps) ids.add(trailStepGraphEdgeId(s));
|
|
1314
1320
|
}
|
|
1315
1321
|
return ids.size > 0 ? ids : null;
|
|
1316
|
-
}, [
|
|
1322
|
+
}, [trails, expandedTrails]);
|
|
1317
1323
|
|
|
1318
1324
|
const endpointsOf = (edgeIds: ReadonlySet<string> | null): Set<string> | null => {
|
|
1319
1325
|
if (!edgeIds) return null;
|
|
@@ -1327,20 +1333,20 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1327
1333
|
};
|
|
1328
1334
|
// Edge/nodes involved in the hovered step or whole flow.
|
|
1329
1335
|
const hoverEdgeIds = useMemo(() => {
|
|
1330
|
-
if (!
|
|
1331
|
-
const tl =
|
|
1336
|
+
if (!hoveredTrailStep || !trails) return null;
|
|
1337
|
+
const tl = trails.find((t) => t.id === hoveredTrailStep.trailId);
|
|
1332
1338
|
if (!tl) return null;
|
|
1333
|
-
if (
|
|
1334
|
-
return new Set(tl.steps.map((s) =>
|
|
1339
|
+
if (hoveredTrailStep.stepIndex == null) {
|
|
1340
|
+
return new Set(tl.steps.map((s) => trailStepGraphEdgeId(s)));
|
|
1335
1341
|
}
|
|
1336
|
-
const step = tl.steps[
|
|
1337
|
-
return step ? new Set([
|
|
1338
|
-
}, [
|
|
1342
|
+
const step = tl.steps[hoveredTrailStep.stepIndex];
|
|
1343
|
+
return step ? new Set([trailStepGraphEdgeId(step)]) : null;
|
|
1344
|
+
}, [trails, hoveredTrailStep]);
|
|
1339
1345
|
// While hovering with a *step* already selected, brighten the union of
|
|
1340
1346
|
// selected + hovered participants. Whole-flow focus (or no focus) keeps
|
|
1341
1347
|
// the old hover-replace preview so a step hover still dims the rest.
|
|
1342
1348
|
const previewEdgeIds = useMemo(() => {
|
|
1343
|
-
// Autoplay drives focus without a hover, so its
|
|
1349
|
+
// Autoplay drives focus without a hover, so its step is the preview set.
|
|
1344
1350
|
if (autoPlayFocus) return focusEdgeIds;
|
|
1345
1351
|
if (!hoverEdgeIds) return null;
|
|
1346
1352
|
// While hovering with a *step* already selected, brighten the union of
|
|
@@ -1489,7 +1495,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1489
1495
|
};
|
|
1490
1496
|
// Edges outside the selected view are hidden entirely (labels included).
|
|
1491
1497
|
// graphify-native edges belong to the graphify view (they are static
|
|
1492
|
-
// topology, not runtime
|
|
1498
|
+
// topology, not runtime steps) regardless of their verb.
|
|
1493
1499
|
const edgeInView = (e: Edge): boolean => {
|
|
1494
1500
|
const d = e.data as
|
|
1495
1501
|
| { mechanism?: string; provenance?: SubsystemEdgeProvenance }
|
|
@@ -1498,7 +1504,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1498
1504
|
if (resolvedEdgeView === 'graphify') {
|
|
1499
1505
|
return d?.provenance === 'graphify';
|
|
1500
1506
|
}
|
|
1501
|
-
return
|
|
1507
|
+
return isTrailMechanism(mechanism);
|
|
1502
1508
|
};
|
|
1503
1509
|
if (openedEdgeIds || focusEdgeIds || previewEdgeIds || issueFocusEdgeIds) {
|
|
1504
1510
|
return baseEdges.map((e) => {
|
|
@@ -1565,7 +1571,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1565
1571
|
(_e, node: Node) => {
|
|
1566
1572
|
const comp = (node.data as { component?: SubsystemComponent } | undefined)?.component;
|
|
1567
1573
|
setSelectedEdgeId(null);
|
|
1568
|
-
|
|
1574
|
+
setFocusedTrailId(null);
|
|
1569
1575
|
setFocusedStepIndex(null);
|
|
1570
1576
|
if (node.type === 'subsystem-component' && comp) {
|
|
1571
1577
|
// Clicking the already-selected node unselects it (toggle off).
|
|
@@ -1593,7 +1599,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1593
1599
|
const onPaneClick = useCallback(() => {
|
|
1594
1600
|
setSelected(null);
|
|
1595
1601
|
setSelectedEdgeId(null);
|
|
1596
|
-
|
|
1602
|
+
setFocusedTrailId(null);
|
|
1597
1603
|
setFocusedStepIndex(null);
|
|
1598
1604
|
issueModuleFocusRef.current = null;
|
|
1599
1605
|
}, []);
|
|
@@ -1601,8 +1607,8 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1601
1607
|
// Sidebar file trees — one per repo on multi-repo graphs, each under its
|
|
1602
1608
|
// own owner-avatar header. Clicking a header collapses that repo's tree.
|
|
1603
1609
|
const repoGroups = useMemo(() => buildRepoGroups(components), [components]);
|
|
1604
|
-
const
|
|
1605
|
-
// Aliases of proposed components — the flows panel tints
|
|
1610
|
+
const hasTrails = useMemo(() => (trails?.length ?? 0) > 0, [trails]);
|
|
1611
|
+
// Aliases of proposed components — the flows panel tints trail titles
|
|
1606
1612
|
// (and steps) that touch one.
|
|
1607
1613
|
const proposedAliases = useMemo(
|
|
1608
1614
|
() => new Set(components.filter((c) => c.proposed).map((c) => c.alias)),
|
|
@@ -1679,7 +1685,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1679
1685
|
const onNodeDoubleClick: NodeMouseHandler = useCallback(
|
|
1680
1686
|
(_e, node: Node) => {
|
|
1681
1687
|
// Boundary frame → frame its members on the canvas; double-click again to
|
|
1682
|
-
// zoom back out. User-initiated, so it ignores the
|
|
1688
|
+
// zoom back out. User-initiated, so it ignores the trail zoom gate.
|
|
1683
1689
|
if (node.type === 'subsystem-group') {
|
|
1684
1690
|
const key =
|
|
1685
1691
|
(node.data as { region?: { key?: string } } | undefined)?.region?.key;
|
|
@@ -1715,7 +1721,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1715
1721
|
[onOpenDeclarationFile, fitView, focusedBoundaryKey],
|
|
1716
1722
|
);
|
|
1717
1723
|
|
|
1718
|
-
const
|
|
1724
|
+
const onOpenFileFromTrail = useCallback(
|
|
1719
1725
|
(file: string, opts?: SubsystemOpenFileOptions) => {
|
|
1720
1726
|
setFileOverlay({ file, startLine: opts?.startLine });
|
|
1721
1727
|
onFileSelect?.(file);
|
|
@@ -1737,15 +1743,15 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1737
1743
|
const fileOverlayOpen = fileOverlay != null;
|
|
1738
1744
|
const collapseCanvas = drawerFillHeight || fileOverlayOpen;
|
|
1739
1745
|
|
|
1740
|
-
// Drop the full-file overlay when the underlying
|
|
1746
|
+
// Drop the full-file overlay when the underlying trail drawer changes
|
|
1741
1747
|
// or closes — open-file keeps the same drawerTarget so the snippets stay mounted.
|
|
1742
|
-
const
|
|
1743
|
-
drawerTarget?.kind === '
|
|
1744
|
-
? `${drawerTarget.
|
|
1748
|
+
const trailDrawerKey =
|
|
1749
|
+
drawerTarget?.kind === 'trail'
|
|
1750
|
+
? `${drawerTarget.trailId}:${drawerTarget.stepIndex}`
|
|
1745
1751
|
: null;
|
|
1746
1752
|
useEffect(() => {
|
|
1747
1753
|
setFileOverlay(null);
|
|
1748
|
-
}, [
|
|
1754
|
+
}, [trailDrawerKey]);
|
|
1749
1755
|
|
|
1750
1756
|
useEffect(() => {
|
|
1751
1757
|
if (!fileOverlayOpen) return;
|
|
@@ -1756,16 +1762,16 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1756
1762
|
return () => window.removeEventListener('keydown', onKey);
|
|
1757
1763
|
}, [fileOverlayOpen, closeFileOverlay]);
|
|
1758
1764
|
|
|
1759
|
-
// Camera helper shared by the
|
|
1765
|
+
// Camera helper shared by the trail interactions. Fits the union of
|
|
1760
1766
|
// the focused edges' endpoint rects AND their routed waypoints — the edge
|
|
1761
1767
|
// carries its full polyline (`elkPathPoints`, absolute flow coords), so the
|
|
1762
|
-
// frame is the line the
|
|
1768
|
+
// frame is the line the step actually traces. That keeps a step whose route
|
|
1763
1769
|
// bulges out around intervening nodes from being clipped at the viewport
|
|
1764
1770
|
// edge, and it is the edge's own geometry doing the guiding, not fudge
|
|
1765
1771
|
// padding.
|
|
1766
1772
|
const fitFocusBounds = useCallback(
|
|
1767
1773
|
(ids: ReadonlySet<string>) => {
|
|
1768
|
-
if (!
|
|
1774
|
+
if (!zoomOnTrailFocus) return;
|
|
1769
1775
|
const nodeIds = new Set<string>();
|
|
1770
1776
|
const points: { x: number; y: number }[] = [];
|
|
1771
1777
|
for (const e of baseEdges) {
|
|
@@ -1810,15 +1816,15 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1810
1816
|
};
|
|
1811
1817
|
fitBounds(bounds, {
|
|
1812
1818
|
padding: 0.15,
|
|
1813
|
-
duration:
|
|
1819
|
+
duration: trailFocusDurationMs,
|
|
1814
1820
|
});
|
|
1815
1821
|
},
|
|
1816
1822
|
[
|
|
1817
1823
|
baseEdges,
|
|
1818
1824
|
built.absoluteRects,
|
|
1819
1825
|
fitBounds,
|
|
1820
|
-
|
|
1821
|
-
|
|
1826
|
+
zoomOnTrailFocus,
|
|
1827
|
+
trailFocusDurationMs,
|
|
1822
1828
|
],
|
|
1823
1829
|
);
|
|
1824
1830
|
|
|
@@ -1876,16 +1882,16 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1876
1882
|
width: Math.max(1, maxX - minX),
|
|
1877
1883
|
height: Math.max(1, maxY - minY),
|
|
1878
1884
|
},
|
|
1879
|
-
{ padding, duration:
|
|
1885
|
+
{ padding, duration: trailFocusDurationMs },
|
|
1880
1886
|
);
|
|
1881
1887
|
},
|
|
1882
|
-
[rectForNode, fitBounds,
|
|
1888
|
+
[rectForNode, fitBounds, trailFocusDurationMs],
|
|
1883
1889
|
);
|
|
1884
1890
|
|
|
1885
|
-
// Zoom back out to the full diagram after the last expanded
|
|
1891
|
+
// Zoom back out to the full diagram after the last expanded trail
|
|
1886
1892
|
// closes (visibility restores non-flow nodes that were hidden).
|
|
1887
1893
|
const fitOverview = useCallback(() => {
|
|
1888
|
-
if (!
|
|
1894
|
+
if (!zoomOnTrailFocus) return;
|
|
1889
1895
|
fitView({
|
|
1890
1896
|
padding: 0.1,
|
|
1891
1897
|
includeHiddenNodes: false,
|
|
@@ -1893,115 +1899,115 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1893
1899
|
maxZoom: 2,
|
|
1894
1900
|
duration: 300,
|
|
1895
1901
|
});
|
|
1896
|
-
}, [fitView,
|
|
1902
|
+
}, [fitView, zoomOnTrailFocus]);
|
|
1897
1903
|
|
|
1898
1904
|
// Focus an entire flow: hide everything but the flow's nodes and edges, and
|
|
1899
1905
|
// frame the flow on the canvas. Selection state is cleared — the graph now
|
|
1900
1906
|
// reads as the narrative. No drawer: the code view only opens on a step
|
|
1901
|
-
// click. A stale
|
|
1907
|
+
// click. A stale trail drawer (from a previously focused flow's step)
|
|
1902
1908
|
// closes; an explicitly opened file drawer stays.
|
|
1903
1909
|
// In `dim` mode: clear step highlight and leave the full graph visible
|
|
1904
1910
|
// (no zoom / hide) — matching "step away from a hovered step".
|
|
1905
|
-
const
|
|
1906
|
-
(tl:
|
|
1911
|
+
const focusTrailEdges = useCallback(
|
|
1912
|
+
(tl: SubsystemTrail) => {
|
|
1907
1913
|
setSelected(null);
|
|
1908
1914
|
setSelectedEdgeId(null);
|
|
1909
|
-
|
|
1910
|
-
if (
|
|
1915
|
+
setHoveredTrailStep(null);
|
|
1916
|
+
if (trailStepMode === 'dim') {
|
|
1911
1917
|
setFocusedStepIndex(null);
|
|
1912
|
-
|
|
1913
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1918
|
+
setFocusedTrailId(null);
|
|
1919
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1914
1920
|
return;
|
|
1915
1921
|
}
|
|
1916
1922
|
setFocusedStepIndex(null);
|
|
1917
|
-
|
|
1918
|
-
fitFocusBounds(new Set(tl.steps.map((s) =>
|
|
1919
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1923
|
+
setFocusedTrailId(tl.id);
|
|
1924
|
+
fitFocusBounds(new Set(tl.steps.map((s) => trailStepGraphEdgeId(s))));
|
|
1925
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1920
1926
|
},
|
|
1921
|
-
[fitFocusBounds,
|
|
1927
|
+
[fitFocusBounds, trailStepMode],
|
|
1922
1928
|
);
|
|
1923
1929
|
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1930
|
+
const clearTrailFocus = useCallback(() => {
|
|
1931
|
+
setFocusedTrailId(null);
|
|
1926
1932
|
setFocusedStepIndex(null);
|
|
1927
|
-
|
|
1928
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1933
|
+
setHoveredTrailStep(null);
|
|
1934
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1929
1935
|
}, []);
|
|
1930
1936
|
|
|
1931
|
-
// Host deep-link: opening the graph from a
|
|
1937
|
+
// Host deep-link: opening the graph from a trail row selects that
|
|
1932
1938
|
// flow — expand its steps, focus its edges, and switch the sidebar to
|
|
1933
|
-
//
|
|
1939
|
+
// Trails. Guarded by a ref so a later in-tab selection isn't yanked
|
|
1934
1940
|
// back; a remount (or a new id) re-applies it.
|
|
1935
|
-
const
|
|
1941
|
+
const appliedInitialTrailRef = useRef<string | null>(null);
|
|
1936
1942
|
useEffect(() => {
|
|
1937
|
-
if (!
|
|
1938
|
-
if (
|
|
1939
|
-
if (!layoutReady || !
|
|
1940
|
-
const tl =
|
|
1943
|
+
if (!initialTrailId) return;
|
|
1944
|
+
if (appliedInitialTrailRef.current === initialTrailId) return;
|
|
1945
|
+
if (!layoutReady || !trails?.length) return;
|
|
1946
|
+
const tl = trails.find((t) => t.id === initialTrailId);
|
|
1941
1947
|
if (!tl) return;
|
|
1942
|
-
|
|
1943
|
-
setSidebarView('
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
}, [
|
|
1948
|
+
appliedInitialTrailRef.current = initialTrailId;
|
|
1949
|
+
setSidebarView('trails');
|
|
1950
|
+
setExpandedTrails((prev) => new Set(prev).add(tl.id));
|
|
1951
|
+
focusTrailEdges(tl);
|
|
1952
|
+
}, [initialTrailId, layoutReady, trails, focusTrailEdges]);
|
|
1947
1953
|
|
|
1948
1954
|
// Switching sidebar panels also switches the edge vocabulary. Leaving the
|
|
1949
|
-
//
|
|
1955
|
+
// Trails panel drops its canvas state (focus, expanded flows, selected
|
|
1950
1956
|
// edge) so the Files view's topology edges aren't gated by a flow the user
|
|
1951
1957
|
// can no longer see or un-dim.
|
|
1952
1958
|
const changeSidebarView = useCallback(
|
|
1953
|
-
(view: 'files' | '
|
|
1959
|
+
(view: 'files' | 'trails') => {
|
|
1954
1960
|
setSidebarView(view);
|
|
1955
|
-
if (view === '
|
|
1956
|
-
|
|
1957
|
-
|
|
1961
|
+
if (view === 'trails') return;
|
|
1962
|
+
clearTrailFocus();
|
|
1963
|
+
setExpandedTrails(new Set());
|
|
1958
1964
|
setSelectedEdgeId(null);
|
|
1959
1965
|
},
|
|
1960
|
-
[
|
|
1966
|
+
[clearTrailFocus],
|
|
1961
1967
|
);
|
|
1962
1968
|
|
|
1963
|
-
// Focus a single step's edge on the canvas and open/scroll the
|
|
1969
|
+
// Focus a single step's edge on the canvas and open/scroll the trail
|
|
1964
1970
|
// drawer to that step's snippet. Clicking the already-focused step clears
|
|
1965
1971
|
// step focus and returns to whole-flow (high-level) framing.
|
|
1966
1972
|
// In `dim` mode: only dim non-participants (same as hovering a step) —
|
|
1967
1973
|
// no camera move, no drawer, no hide.
|
|
1968
|
-
const
|
|
1969
|
-
(tl:
|
|
1974
|
+
const focusTrailStep = useCallback(
|
|
1975
|
+
(tl: SubsystemTrail, stepIndex: number) => {
|
|
1970
1976
|
const step = tl.steps[stepIndex];
|
|
1971
1977
|
if (!step) return;
|
|
1972
1978
|
setSelected(null);
|
|
1973
1979
|
setSelectedEdgeId(null);
|
|
1974
|
-
if (
|
|
1980
|
+
if (trailStepMode === 'dim') {
|
|
1975
1981
|
setFocusedStepIndex(null);
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1982
|
+
setFocusedTrailId(null);
|
|
1983
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex });
|
|
1984
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1979
1985
|
return;
|
|
1980
1986
|
}
|
|
1981
1987
|
// Toggle: clicking the selected step unselects it (back to whole flow).
|
|
1982
|
-
if (
|
|
1988
|
+
if (focusedTrailId === tl.id && focusedStepIndex === stepIndex) {
|
|
1983
1989
|
setFocusedStepIndex(null);
|
|
1984
1990
|
// Pointer is still over the row — keep hover preview so dimming doesn't
|
|
1985
1991
|
// flash off until mouseleave.
|
|
1986
|
-
|
|
1987
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1992
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex });
|
|
1993
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1988
1994
|
if (pendingFocusFitRef.current != null) {
|
|
1989
1995
|
window.clearTimeout(pendingFocusFitRef.current);
|
|
1990
1996
|
pendingFocusFitRef.current = null;
|
|
1991
1997
|
}
|
|
1992
|
-
fitFocusBounds(new Set(tl.steps.map((s) =>
|
|
1998
|
+
fitFocusBounds(new Set(tl.steps.map((s) => trailStepGraphEdgeId(s))));
|
|
1993
1999
|
return;
|
|
1994
2000
|
}
|
|
1995
2001
|
setFocusedStepIndex(stepIndex);
|
|
1996
|
-
|
|
1997
|
-
|
|
2002
|
+
setFocusedTrailId(tl.id);
|
|
2003
|
+
setHoveredTrailStep(null);
|
|
1998
2004
|
// Open the drawer before fitting. If it was closed, wait for its height
|
|
1999
2005
|
// transition so fitView uses the reduced canvas — not full height.
|
|
2000
2006
|
const drawerWasOpen = drawerOpenRef.current;
|
|
2001
|
-
if (
|
|
2007
|
+
if (renderTrailViewer) {
|
|
2002
2008
|
setDrawerTarget({
|
|
2003
|
-
kind: '
|
|
2004
|
-
|
|
2009
|
+
kind: 'trail',
|
|
2010
|
+
trailId: tl.id,
|
|
2005
2011
|
stepIndex,
|
|
2006
2012
|
});
|
|
2007
2013
|
} else {
|
|
@@ -2011,7 +2017,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2011
2017
|
startLine: step.line,
|
|
2012
2018
|
});
|
|
2013
2019
|
}
|
|
2014
|
-
const edgeIds = new Set([
|
|
2020
|
+
const edgeIds = new Set([trailStepGraphEdgeId(step)]);
|
|
2015
2021
|
if (pendingFocusFitRef.current != null) {
|
|
2016
2022
|
window.clearTimeout(pendingFocusFitRef.current);
|
|
2017
2023
|
pendingFocusFitRef.current = null;
|
|
@@ -2028,9 +2034,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2028
2034
|
[
|
|
2029
2035
|
fitFocusBounds,
|
|
2030
2036
|
focusedStepIndex,
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2037
|
+
focusedTrailId,
|
|
2038
|
+
renderTrailViewer,
|
|
2039
|
+
trailStepMode,
|
|
2034
2040
|
],
|
|
2035
2041
|
);
|
|
2036
2042
|
|
|
@@ -2052,10 +2058,10 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2052
2058
|
const prev = prevPreviewEdgeIdsRef.current;
|
|
2053
2059
|
prevPreviewEdgeIdsRef.current = previewEdgeIds;
|
|
2054
2060
|
|
|
2055
|
-
if (!
|
|
2061
|
+
if (!zoomOnTrailFocus || trailStepMode === 'dim') return;
|
|
2056
2062
|
// Camera follows hover only when a specific step is already focused (or
|
|
2057
2063
|
// autoplay is driving an auto-focus).
|
|
2058
|
-
if (!autoPlayFocus && (
|
|
2064
|
+
if (!autoPlayFocus && (focusedTrailId == null || focusedStepIndex == null)) {
|
|
2059
2065
|
return;
|
|
2060
2066
|
}
|
|
2061
2067
|
|
|
@@ -2069,26 +2075,26 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2069
2075
|
}, [
|
|
2070
2076
|
previewEdgeIds,
|
|
2071
2077
|
focusEdgeIds,
|
|
2072
|
-
|
|
2078
|
+
focusedTrailId,
|
|
2073
2079
|
focusedStepIndex,
|
|
2074
2080
|
autoPlayFocus,
|
|
2075
2081
|
fitFocusBounds,
|
|
2076
|
-
|
|
2077
|
-
|
|
2082
|
+
zoomOnTrailFocus,
|
|
2083
|
+
trailStepMode,
|
|
2078
2084
|
]);
|
|
2079
2085
|
|
|
2080
|
-
// Graph-only embeds: cycle
|
|
2081
|
-
// selected (camera frames it via fitFocusBounds) and, when a
|
|
2086
|
+
// Graph-only embeds: cycle trail steps. In `focus` mode each step is
|
|
2087
|
+
// selected (camera frames it via fitFocusBounds) and, when a trail
|
|
2082
2088
|
// viewer is supplied, its snippet drawer opens; in `dim` mode it only
|
|
2083
2089
|
// dim-highlights the step (hover-style) with no camera move.
|
|
2084
2090
|
useEffect(() => {
|
|
2085
|
-
if (!
|
|
2091
|
+
if (!autoPlayTrails || trails == null || trails.length === 0) return;
|
|
2086
2092
|
if (!layoutReady) return;
|
|
2087
|
-
const playable =
|
|
2093
|
+
const playable = trails.filter((tl) => tl.steps.length > 0);
|
|
2088
2094
|
if (playable.length === 0) return;
|
|
2089
2095
|
// Only run the cycles that can actually be shown in the current edge view.
|
|
2090
2096
|
const inView = playable.filter((tl) =>
|
|
2091
|
-
tl.steps.some((s) =>
|
|
2097
|
+
tl.steps.some((s) => isTrailMechanism(s.mechanism)),
|
|
2092
2098
|
);
|
|
2093
2099
|
if (inView.length === 0) return;
|
|
2094
2100
|
const cyc = inView;
|
|
@@ -2097,8 +2103,8 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2097
2103
|
let tlIdx = 0;
|
|
2098
2104
|
let stepIdx = 0;
|
|
2099
2105
|
let timer: number | null = null;
|
|
2100
|
-
const interval = Math.max(400,
|
|
2101
|
-
const focusMode =
|
|
2106
|
+
const interval = Math.max(400, trailAutoPlayIntervalMs);
|
|
2107
|
+
const focusMode = trailStepMode !== 'dim';
|
|
2102
2108
|
let cleanup = () => {};
|
|
2103
2109
|
|
|
2104
2110
|
const tick = () => {
|
|
@@ -2107,15 +2113,15 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2107
2113
|
setSelected(null);
|
|
2108
2114
|
setSelectedEdgeId(null);
|
|
2109
2115
|
if (focusMode) {
|
|
2110
|
-
|
|
2111
|
-
|
|
2116
|
+
setHoveredTrailStep(null);
|
|
2117
|
+
setFocusedTrailId(null);
|
|
2112
2118
|
setFocusedStepIndex(null);
|
|
2113
|
-
setAutoPlayFocus({
|
|
2119
|
+
setAutoPlayFocus({ trailId: tl.id, stepIndex: stepIdx });
|
|
2114
2120
|
if (!hideDrawer) {
|
|
2115
2121
|
const step = tl.steps[stepIdx];
|
|
2116
2122
|
if (step) {
|
|
2117
|
-
if (
|
|
2118
|
-
setDrawerTarget({ kind: '
|
|
2123
|
+
if (renderTrailViewer) {
|
|
2124
|
+
setDrawerTarget({ kind: 'trail', trailId: tl.id, stepIndex: stepIdx });
|
|
2119
2125
|
} else {
|
|
2120
2126
|
setDrawerTarget({ kind: 'file', file: step.file, startLine: step.line });
|
|
2121
2127
|
}
|
|
@@ -2123,9 +2129,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2123
2129
|
}
|
|
2124
2130
|
} else {
|
|
2125
2131
|
setAutoPlayFocus(null);
|
|
2126
|
-
|
|
2132
|
+
setFocusedTrailId(null);
|
|
2127
2133
|
setFocusedStepIndex(null);
|
|
2128
|
-
|
|
2134
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex: stepIdx });
|
|
2129
2135
|
}
|
|
2130
2136
|
stepIdx += 1;
|
|
2131
2137
|
if (stepIdx >= tl.steps.length) {
|
|
@@ -2139,29 +2145,29 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2139
2145
|
cleanup = () => {
|
|
2140
2146
|
cancelled = true;
|
|
2141
2147
|
if (timer != null) window.clearTimeout(timer);
|
|
2142
|
-
|
|
2143
|
-
|
|
2148
|
+
setHoveredTrailStep(null);
|
|
2149
|
+
setFocusedTrailId(null);
|
|
2144
2150
|
setFocusedStepIndex(null);
|
|
2145
2151
|
setAutoPlayFocus(null);
|
|
2146
2152
|
};
|
|
2147
2153
|
return cleanup;
|
|
2148
2154
|
}, [
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2155
|
+
autoPlayTrails,
|
|
2156
|
+
trails,
|
|
2157
|
+
trailAutoPlayIntervalMs,
|
|
2152
2158
|
layoutReady,
|
|
2153
|
-
|
|
2154
|
-
|
|
2159
|
+
trailStepMode,
|
|
2160
|
+
renderTrailViewer,
|
|
2155
2161
|
hideDrawer,
|
|
2156
2162
|
]);
|
|
2157
2163
|
|
|
2158
|
-
// Arrow keys step through the focused
|
|
2164
|
+
// Arrow keys step through the focused trail once a step is active
|
|
2159
2165
|
// (sidebar click or drawer open). Ignores typing targets and chords.
|
|
2160
2166
|
useEffect(() => {
|
|
2161
|
-
if (
|
|
2167
|
+
if (focusedTrailId == null || focusedStepIndex == null || !trails) {
|
|
2162
2168
|
return;
|
|
2163
2169
|
}
|
|
2164
|
-
const tl =
|
|
2170
|
+
const tl = trails.find((t) => t.id === focusedTrailId);
|
|
2165
2171
|
if (!tl || tl.steps.length === 0) return;
|
|
2166
2172
|
|
|
2167
2173
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
@@ -2187,31 +2193,31 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2187
2193
|
}
|
|
2188
2194
|
if (next === focusedStepIndex) return;
|
|
2189
2195
|
e.preventDefault();
|
|
2190
|
-
|
|
2196
|
+
focusTrailStep(tl, next);
|
|
2191
2197
|
};
|
|
2192
2198
|
|
|
2193
2199
|
window.addEventListener('keydown', onKeyDown);
|
|
2194
2200
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
2195
2201
|
}, [
|
|
2196
|
-
|
|
2202
|
+
focusedTrailId,
|
|
2197
2203
|
focusedStepIndex,
|
|
2198
|
-
|
|
2199
|
-
|
|
2204
|
+
trails,
|
|
2205
|
+
focusTrailStep,
|
|
2200
2206
|
]);
|
|
2201
2207
|
|
|
2202
|
-
const
|
|
2208
|
+
const toggleTrailCollapsed = useCallback(
|
|
2203
2209
|
(tlId: string) => {
|
|
2204
2210
|
const collapsingLast =
|
|
2205
|
-
|
|
2211
|
+
expandedTrails.has(tlId) && expandedTrails.size === 1;
|
|
2206
2212
|
|
|
2207
|
-
|
|
2213
|
+
setExpandedTrails((prev) => {
|
|
2208
2214
|
const next = new Set(prev);
|
|
2209
2215
|
if (next.has(tlId)) next.delete(tlId);
|
|
2210
2216
|
else next.add(tlId);
|
|
2211
2217
|
return next;
|
|
2212
2218
|
});
|
|
2213
2219
|
|
|
2214
|
-
if (!collapsingLast || !
|
|
2220
|
+
if (!collapsingLast || !zoomOnTrailFocus) return;
|
|
2215
2221
|
|
|
2216
2222
|
// Cancel any pending focus fit so it doesn't fight the overview zoom.
|
|
2217
2223
|
if (pendingFocusFitRef.current != null) {
|
|
@@ -2231,7 +2237,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2231
2237
|
drawerWasOpen ? FILE_DRAWER_HEIGHT_MS + 20 : 20,
|
|
2232
2238
|
);
|
|
2233
2239
|
},
|
|
2234
|
-
[
|
|
2240
|
+
[expandedTrails, fitOverview, zoomOnTrailFocus],
|
|
2235
2241
|
);
|
|
2236
2242
|
|
|
2237
2243
|
// Detail-panel links: related-name clicks select the matching component —
|
|
@@ -2249,7 +2255,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2249
2255
|
if (!comp || comp.alias === selectedRef.current?.alias) return;
|
|
2250
2256
|
setSelected(comp);
|
|
2251
2257
|
setSelectedEdgeId(null);
|
|
2252
|
-
|
|
2258
|
+
setFocusedTrailId(null);
|
|
2253
2259
|
setFocusedStepIndex(null);
|
|
2254
2260
|
onSelect?.(comp.alias);
|
|
2255
2261
|
},
|
|
@@ -2268,9 +2274,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2268
2274
|
if (comp) {
|
|
2269
2275
|
setSelected(comp);
|
|
2270
2276
|
setSelectedEdgeId(null);
|
|
2271
|
-
|
|
2277
|
+
setFocusedTrailId(null);
|
|
2272
2278
|
setFocusedStepIndex(null);
|
|
2273
|
-
|
|
2279
|
+
setHoveredTrailStep(null);
|
|
2274
2280
|
issueModuleFocusRef.current = null;
|
|
2275
2281
|
onSelect?.(comp.alias);
|
|
2276
2282
|
requestAnimationFrame(() => {
|
|
@@ -2284,11 +2290,11 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2284
2290
|
});
|
|
2285
2291
|
} else if (step) {
|
|
2286
2292
|
// A step finding focuses the step itself, through the same entry point
|
|
2287
|
-
// the
|
|
2293
|
+
// the trail panel uses — so the drawer, the step numbering, the
|
|
2288
2294
|
// dim-mode hover preview, and the camera all behave identically whether
|
|
2289
2295
|
// the step was reached from the panel or from a diagnostics card.
|
|
2290
2296
|
issueModuleFocusRef.current = null;
|
|
2291
|
-
|
|
2297
|
+
focusTrailStep(step.trail, step.stepIndex);
|
|
2292
2298
|
} else {
|
|
2293
2299
|
// Nothing selectable — but a module target names a boundary frame, so
|
|
2294
2300
|
// frame that region.
|
|
@@ -2296,9 +2302,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2296
2302
|
if (moduleNodeId) {
|
|
2297
2303
|
setSelected(null);
|
|
2298
2304
|
setSelectedEdgeId(null);
|
|
2299
|
-
|
|
2305
|
+
setFocusedTrailId(null);
|
|
2300
2306
|
setFocusedStepIndex(null);
|
|
2301
|
-
|
|
2307
|
+
setHoveredTrailStep(null);
|
|
2302
2308
|
issueModuleFocusRef.current = moduleNodeId;
|
|
2303
2309
|
requestAnimationFrame(() => {
|
|
2304
2310
|
fitNodeRects(new Set([moduleNodeId]), 0.4);
|
|
@@ -2311,7 +2317,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2311
2317
|
issueComponent,
|
|
2312
2318
|
issueStep,
|
|
2313
2319
|
issueModuleNodeId,
|
|
2314
|
-
|
|
2320
|
+
focusTrailStep,
|
|
2315
2321
|
onSelect,
|
|
2316
2322
|
onSelectIssue,
|
|
2317
2323
|
fitView,
|
|
@@ -2341,14 +2347,14 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2341
2347
|
// not the card), and correctly leaves that alone here.
|
|
2342
2348
|
const owned = focusedStepRef.current;
|
|
2343
2349
|
if (
|
|
2344
|
-
owned?.
|
|
2350
|
+
owned?.trailId !== step.trail.id ||
|
|
2345
2351
|
owned?.stepIndex !== step.stepIndex
|
|
2346
2352
|
) {
|
|
2347
2353
|
return;
|
|
2348
2354
|
}
|
|
2349
|
-
|
|
2355
|
+
setFocusedTrailId(null);
|
|
2350
2356
|
setFocusedStepIndex(null);
|
|
2351
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
2357
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
2352
2358
|
} else if (moduleNodeId) {
|
|
2353
2359
|
// Only unwind framing this card still owns.
|
|
2354
2360
|
if (issueModuleFocusRef.current !== moduleNodeId) return;
|
|
@@ -2364,14 +2370,14 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2364
2370
|
[issueComponent, issueStep, issueModuleNodeId, fitView],
|
|
2365
2371
|
);
|
|
2366
2372
|
|
|
2367
|
-
// Construct tokens inside a
|
|
2373
|
+
// Construct tokens inside a trail snippet. A step's line is an edge
|
|
2368
2374
|
// between its `from`/`to` components, so a token naming either of those
|
|
2369
2375
|
// constructs should navigate to that construct's declaration. Index the
|
|
2370
|
-
// matchable identifiers per step (`
|
|
2371
|
-
const
|
|
2376
|
+
// matchable identifiers per step (`trailId:index`).
|
|
2377
|
+
const trailSymbolIndex = useMemo(() => {
|
|
2372
2378
|
const byAlias = new Map(components.map((c) => [c.alias, c]));
|
|
2373
2379
|
const index = new Map<string, Map<string, string>>();
|
|
2374
|
-
for (const wt of
|
|
2380
|
+
for (const wt of trails ?? []) {
|
|
2375
2381
|
wt.steps.forEach((step, i) => {
|
|
2376
2382
|
const tokens = new Map<string, string>();
|
|
2377
2383
|
for (const alias of [step.from, step.to]) {
|
|
@@ -2383,19 +2389,19 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2383
2389
|
});
|
|
2384
2390
|
}
|
|
2385
2391
|
return index;
|
|
2386
|
-
}, [components,
|
|
2387
|
-
const
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
// Stable resolver: reads the live index + focused
|
|
2391
|
-
// the memoized
|
|
2392
|
-
const
|
|
2393
|
-
(query:
|
|
2394
|
-
const
|
|
2395
|
-
if (
|
|
2392
|
+
}, [components, trails]);
|
|
2393
|
+
const trailSymbolIndexRef = useRef(trailSymbolIndex);
|
|
2394
|
+
trailSymbolIndexRef.current = trailSymbolIndex;
|
|
2395
|
+
|
|
2396
|
+
// Stable resolver: reads the live index + focused trail from refs so
|
|
2397
|
+
// the memoized trail drawer isn't rebuilt on every graph render.
|
|
2398
|
+
const resolveTrailSymbol = useCallback(
|
|
2399
|
+
(query: TrailSymbolQuery): string | null => {
|
|
2400
|
+
const trailId = drawerTrailIdRef.current;
|
|
2401
|
+
if (trailId == null) return null;
|
|
2396
2402
|
return (
|
|
2397
|
-
|
|
2398
|
-
.get(`${
|
|
2403
|
+
trailSymbolIndexRef.current
|
|
2404
|
+
.get(`${trailId}:${query.stepIndex}`)
|
|
2399
2405
|
?.get(query.tokenText) ?? null
|
|
2400
2406
|
);
|
|
2401
2407
|
},
|
|
@@ -2483,11 +2489,11 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2483
2489
|
[],
|
|
2484
2490
|
);
|
|
2485
2491
|
|
|
2486
|
-
const
|
|
2487
|
-
|
|
2488
|
-
const
|
|
2489
|
-
(ctx:
|
|
2490
|
-
|
|
2492
|
+
const trailViewerRef = useRef(renderTrailViewer);
|
|
2493
|
+
trailViewerRef.current = renderTrailViewer;
|
|
2494
|
+
const renderTrailDrawerContent = useCallback(
|
|
2495
|
+
(ctx: TrailViewerContext) =>
|
|
2496
|
+
trailViewerRef.current?.(ctx) ?? null,
|
|
2491
2497
|
[],
|
|
2492
2498
|
);
|
|
2493
2499
|
|
|
@@ -2496,7 +2502,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2496
2502
|
{/* Sidebar: scrollable title/description on top, files or flows pinned below.
|
|
2497
2503
|
The description hides by default so files/flows get the room; the
|
|
2498
2504
|
title-row toggle reveals it, and the lower panel yields back to 50%. */}
|
|
2499
|
-
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 ||
|
|
2505
|
+
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasTrails) && (
|
|
2500
2506
|
<div
|
|
2501
2507
|
style={{
|
|
2502
2508
|
width: sidebarWidth,
|
|
@@ -2603,7 +2609,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2603
2609
|
)}
|
|
2604
2610
|
{sidebarAfterDescription}
|
|
2605
2611
|
</div>
|
|
2606
|
-
{(treeFilePaths.length > 0 ||
|
|
2612
|
+
{(treeFilePaths.length > 0 || hasTrails || issuesActive) && (
|
|
2607
2613
|
<div
|
|
2608
2614
|
style={{
|
|
2609
2615
|
...(showDesc ? { height: '50%' as const } : { flex: 1, minHeight: 0 }),
|
|
@@ -2682,7 +2688,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2682
2688
|
)
|
|
2683
2689
|
) : (
|
|
2684
2690
|
<>
|
|
2685
|
-
{
|
|
2691
|
+
{hasTrails && (
|
|
2686
2692
|
<div
|
|
2687
2693
|
role="tablist"
|
|
2688
2694
|
aria-label="Sidebar view"
|
|
@@ -2694,7 +2700,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2694
2700
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
2695
2701
|
}}
|
|
2696
2702
|
>
|
|
2697
|
-
{(['
|
|
2703
|
+
{(['trails', 'files'] as const).map((view) => (
|
|
2698
2704
|
<button
|
|
2699
2705
|
key={view}
|
|
2700
2706
|
type="button"
|
|
@@ -2718,30 +2724,30 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2718
2724
|
cursor: 'pointer',
|
|
2719
2725
|
}}
|
|
2720
2726
|
>
|
|
2721
|
-
{view === '
|
|
2727
|
+
{view === 'trails' ? 'Trails' : 'Files'}
|
|
2722
2728
|
</button>
|
|
2723
2729
|
))}
|
|
2724
2730
|
</div>
|
|
2725
2731
|
)}
|
|
2726
|
-
{sidebarView === '
|
|
2727
|
-
<
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2732
|
+
{sidebarView === 'trails' && trails && trails.length > 0 ? (
|
|
2733
|
+
<TrailsPanel
|
|
2734
|
+
trails={trails}
|
|
2735
|
+
expandedTrails={expandedTrails}
|
|
2736
|
+
focusedTrailId={focusedTrailId}
|
|
2731
2737
|
focusedStepIndex={focusedStepIndex}
|
|
2732
|
-
|
|
2733
|
-
onToggleCollapsed={
|
|
2734
|
-
onFocusFlow={
|
|
2735
|
-
onClearFocus={
|
|
2736
|
-
onFocusStep={
|
|
2738
|
+
hoveredTrailStep={hoveredTrailStep}
|
|
2739
|
+
onToggleCollapsed={toggleTrailCollapsed}
|
|
2740
|
+
onFocusFlow={focusTrailEdges}
|
|
2741
|
+
onClearFocus={clearTrailFocus}
|
|
2742
|
+
onFocusStep={focusTrailStep}
|
|
2737
2743
|
onHoverStep={(tl, i) =>
|
|
2738
|
-
|
|
2744
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex: i })
|
|
2739
2745
|
}
|
|
2740
2746
|
onHoverFlow={(tl) =>
|
|
2741
|
-
|
|
2747
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex: null })
|
|
2742
2748
|
}
|
|
2743
|
-
onLeaveStep={() =>
|
|
2744
|
-
onReorder={
|
|
2749
|
+
onLeaveStep={() => setHoveredTrailStep(null)}
|
|
2750
|
+
onReorder={onReorderTrails}
|
|
2745
2751
|
proposedAliases={proposedAliases}
|
|
2746
2752
|
/>
|
|
2747
2753
|
) : treeFilePaths.length > 0 ? (
|
|
@@ -2790,7 +2796,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2790
2796
|
)}
|
|
2791
2797
|
|
|
2792
2798
|
{/* Drag handle between sidebar and canvas — resize the left panel. */}
|
|
2793
|
-
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 ||
|
|
2799
|
+
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasTrails) && (
|
|
2794
2800
|
<div
|
|
2795
2801
|
onMouseDown={onSidebarResizeStart}
|
|
2796
2802
|
aria-label="Resize sidebar"
|
|
@@ -2971,9 +2977,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2971
2977
|
>
|
|
2972
2978
|
<GraphChrome />
|
|
2973
2979
|
</ReactFlow>
|
|
2974
|
-
{/* Graph /
|
|
2980
|
+
{/* Graph / trail / step titles — non-interactive chips at the top
|
|
2975
2981
|
of the canvas. Graph-only embeds use these without opening the sidebar. */}
|
|
2976
|
-
{(graphTitle ||
|
|
2982
|
+
{(graphTitle || overlayTrailTitle || overlayTrailStep) && (
|
|
2977
2983
|
<div
|
|
2978
2984
|
style={{
|
|
2979
2985
|
position: 'absolute',
|
|
@@ -3010,11 +3016,11 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3010
3016
|
{graphTitle}
|
|
3011
3017
|
</div>
|
|
3012
3018
|
)}
|
|
3013
|
-
{
|
|
3019
|
+
{overlayTrailTitle && (
|
|
3014
3020
|
<div
|
|
3015
3021
|
style={{
|
|
3016
3022
|
maxWidth: '100%',
|
|
3017
|
-
minWidth:
|
|
3023
|
+
minWidth: overlayTrailStep ? 160 : undefined,
|
|
3018
3024
|
display: 'flex',
|
|
3019
3025
|
flexDirection: 'column',
|
|
3020
3026
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
@@ -3025,9 +3031,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3025
3031
|
opacity: 0.95,
|
|
3026
3032
|
}}
|
|
3027
3033
|
aria-label={
|
|
3028
|
-
|
|
3029
|
-
? `${
|
|
3030
|
-
:
|
|
3034
|
+
overlayTrailStep
|
|
3035
|
+
? `${overlayTrailTitle}, step ${overlayTrailStep.index} of ${overlayTrailStep.total}`
|
|
3036
|
+
: overlayTrailTitle
|
|
3031
3037
|
}
|
|
3032
3038
|
>
|
|
3033
3039
|
<div
|
|
@@ -3043,9 +3049,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3043
3049
|
textAlign: 'center',
|
|
3044
3050
|
}}
|
|
3045
3051
|
>
|
|
3046
|
-
{
|
|
3052
|
+
{overlayTrailTitle}
|
|
3047
3053
|
</div>
|
|
3048
|
-
{
|
|
3054
|
+
{overlayTrailStep && overlayTrailStep.total > 0 && (
|
|
3049
3055
|
<div
|
|
3050
3056
|
style={{
|
|
3051
3057
|
display: 'flex',
|
|
@@ -3054,10 +3060,10 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3054
3060
|
}}
|
|
3055
3061
|
aria-hidden="true"
|
|
3056
3062
|
>
|
|
3057
|
-
{Array.from({ length:
|
|
3063
|
+
{Array.from({ length: overlayTrailStep.total }, (_, i) => {
|
|
3058
3064
|
const n = i + 1;
|
|
3059
|
-
const active = n ===
|
|
3060
|
-
const done = n <
|
|
3065
|
+
const active = n === overlayTrailStep.index;
|
|
3066
|
+
const done = n < overlayTrailStep.index;
|
|
3061
3067
|
return (
|
|
3062
3068
|
<span
|
|
3063
3069
|
key={n}
|
|
@@ -3080,7 +3086,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3080
3086
|
)}
|
|
3081
3087
|
</div>
|
|
3082
3088
|
)}
|
|
3083
|
-
{
|
|
3089
|
+
{overlayTrailStep?.annotation && (
|
|
3084
3090
|
<div
|
|
3085
3091
|
style={{
|
|
3086
3092
|
maxWidth: '100%',
|
|
@@ -3096,7 +3102,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3096
3102
|
lineHeight: 1.35,
|
|
3097
3103
|
}}
|
|
3098
3104
|
>
|
|
3099
|
-
{
|
|
3105
|
+
{overlayTrailStep.annotation}
|
|
3100
3106
|
</div>
|
|
3101
3107
|
)}
|
|
3102
3108
|
</div>
|
|
@@ -3158,16 +3164,16 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3158
3164
|
suppressEscape={fileOverlayOpen}
|
|
3159
3165
|
hidden={hideDrawer}
|
|
3160
3166
|
>
|
|
3161
|
-
{drawerTarget?.kind === '
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
<
|
|
3165
|
-
render={
|
|
3166
|
-
|
|
3167
|
+
{drawerTarget?.kind === 'trail' &&
|
|
3168
|
+
focusedTrail &&
|
|
3169
|
+
renderTrailViewer ? (
|
|
3170
|
+
<TrailDrawerContent
|
|
3171
|
+
render={renderTrailDrawerContent}
|
|
3172
|
+
trail={focusedTrail}
|
|
3167
3173
|
stepIndex={drawerTarget.stepIndex}
|
|
3168
|
-
onOpenFile={
|
|
3174
|
+
onOpenFile={onOpenFileFromTrail}
|
|
3169
3175
|
proposedAliases={proposedAliases}
|
|
3170
|
-
resolveSymbol={
|
|
3176
|
+
resolveSymbol={resolveTrailSymbol}
|
|
3171
3177
|
onSymbolClick={openConstructDeclaration}
|
|
3172
3178
|
/>
|
|
3173
3179
|
) : drawerTarget?.kind === 'file' ? (
|
|
@@ -3336,7 +3342,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3336
3342
|
|
|
3337
3343
|
const constructsOnly = isConstructsOnlyModel({
|
|
3338
3344
|
components: props.components,
|
|
3339
|
-
|
|
3345
|
+
trails: props.trails,
|
|
3340
3346
|
graphifyRelations: props.graphifyRelations,
|
|
3341
3347
|
});
|
|
3342
3348
|
|