@principal-ai/subsystems-react 0.44.4 → 0.46.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 +7 -11
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +10 -26
- 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 +2 -3
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
- package/dist/subsystem/ConstructsCatalog.js +3 -14
- package/dist/subsystem/ConstructsCatalog.js.map +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 -51
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +288 -292
- 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 +17 -37
- package/src/subsystem/C4Graph.tsx +2 -2
- package/src/subsystem/ConstructsCatalog.tsx +2 -16
- 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 +380 -393
- 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,24 +368,18 @@ 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.
|
|
381
381
|
*/
|
|
382
382
|
hideDrawer?: boolean;
|
|
383
|
-
/**
|
|
384
|
-
* Legacy component-keyed variant, kept for backward compatibility. When
|
|
385
|
-
* `renderFileViewer` is absent, drawer content resolves via the first
|
|
386
|
-
* component whose `file` matches the opened path.
|
|
387
|
-
*/
|
|
388
|
-
renderFileView?: (component: SubsystemComponent) => ReactNode;
|
|
389
383
|
/**
|
|
390
384
|
* Called when a file in the sidebar file tree is clicked (repo-root-relative
|
|
391
385
|
* path). The tree is derived from the components' `file` values.
|
|
@@ -406,7 +400,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
406
400
|
ref: DeclarationSymbolRef;
|
|
407
401
|
}) => Promise<SymbolInspection | null> | SymbolInspection | null;
|
|
408
402
|
/**
|
|
409
|
-
* When set, the graph's
|
|
403
|
+
* When set, the graph's trail working set — expanded flows and the
|
|
410
404
|
* selected flow/step — is persisted to `localStorage` under this key and
|
|
411
405
|
* restored on mount. Hosts key it by the model id so each model remembers
|
|
412
406
|
* where the user left off. Omit to keep the state purely in-memory.
|
|
@@ -489,27 +483,27 @@ function FileOverlayCloseButton({ onClose }: { onClose: () => void }) {
|
|
|
489
483
|
);
|
|
490
484
|
}
|
|
491
485
|
|
|
492
|
-
const
|
|
486
|
+
const TrailDrawerContent = memo(function TrailDrawerContent({
|
|
493
487
|
render,
|
|
494
|
-
|
|
488
|
+
trail,
|
|
495
489
|
stepIndex,
|
|
496
490
|
onOpenFile,
|
|
497
491
|
proposedAliases,
|
|
498
492
|
resolveSymbol,
|
|
499
493
|
onSymbolClick,
|
|
500
494
|
}: {
|
|
501
|
-
render: (ctx:
|
|
502
|
-
|
|
495
|
+
render: (ctx: TrailViewerContext) => ReactNode;
|
|
496
|
+
trail: SubsystemTrail;
|
|
503
497
|
stepIndex: number | null;
|
|
504
498
|
onOpenFile: (path: string, opts?: SubsystemOpenFileOptions) => void;
|
|
505
499
|
proposedAliases: ReadonlySet<string>;
|
|
506
|
-
resolveSymbol?: (query:
|
|
507
|
-
onSymbolClick?: (symbol: string, query:
|
|
500
|
+
resolveSymbol?: (query: TrailSymbolQuery) => string | null;
|
|
501
|
+
onSymbolClick?: (symbol: string, query: TrailSymbolQuery) => void;
|
|
508
502
|
}) {
|
|
509
503
|
return (
|
|
510
504
|
<>
|
|
511
505
|
{render({
|
|
512
|
-
|
|
506
|
+
trail,
|
|
513
507
|
stepIndex,
|
|
514
508
|
onOpenFile,
|
|
515
509
|
proposedAliases,
|
|
@@ -524,7 +518,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
524
518
|
measured: { w: number; h: number } | null;
|
|
525
519
|
}
|
|
526
520
|
|
|
527
|
-
function Inner({ components,
|
|
521
|
+
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, renderFileViewer, renderTrailViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, hideDrawer = false, persistKey, liveEvents, agentsPanel }: InnerProps) {
|
|
528
522
|
const { theme } = useTheme();
|
|
529
523
|
const { fitView, fitBounds, screenToFlowPosition } = useReactFlow();
|
|
530
524
|
const viewport = useViewport();
|
|
@@ -532,8 +526,8 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
532
526
|
// graph is its own tab/mount, so `persistKey` is stable for a mount.
|
|
533
527
|
const persisted = useMemo(() => readViewState(persistKey), [persistKey]);
|
|
534
528
|
const graphEdges = useMemo(
|
|
535
|
-
() => deriveGraphEdges({
|
|
536
|
-
[
|
|
529
|
+
() => deriveGraphEdges({ trails }),
|
|
530
|
+
[trails],
|
|
537
531
|
);
|
|
538
532
|
const [built, setBuilt] = useState<{
|
|
539
533
|
nodes: Node[];
|
|
@@ -546,9 +540,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
546
540
|
});
|
|
547
541
|
const [layoutReady, setLayoutReady] = useState(false);
|
|
548
542
|
const [selected, setSelected] = useState<SubsystemComponent | null>(null);
|
|
549
|
-
/** Bottom drawer: single file or
|
|
543
|
+
/** Bottom drawer: single file or trail multi-snippet mode. */
|
|
550
544
|
const [drawerTarget, setDrawerTarget] = useState<DrawerTarget | null>(null);
|
|
551
|
-
/** Full-file layer over an open
|
|
545
|
+
/** Full-file layer over an open trail drawer — trail stays mounted. */
|
|
552
546
|
const [fileOverlay, setFileOverlay] = useState<{
|
|
553
547
|
file: string;
|
|
554
548
|
startLine?: number;
|
|
@@ -628,52 +622,58 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
628
622
|
if (raf) cancelAnimationFrame(raf);
|
|
629
623
|
};
|
|
630
624
|
}, [pathFrameRects, screenToFlowPosition]);
|
|
631
|
-
//
|
|
625
|
+
// Trail focus — selected flow (or step) is full strength; other
|
|
632
626
|
// opened-flow members stay visible but dimmed; everything else is hidden.
|
|
633
627
|
// Restored from the persisted view state when a `persistKey` is set.
|
|
634
|
-
const [
|
|
635
|
-
persisted.
|
|
628
|
+
const [focusedTrailId, setFocusedTrailId] = useState<string | null>(
|
|
629
|
+
persisted.focusedTrailId ?? null,
|
|
636
630
|
);
|
|
637
631
|
// `null` = whole flow focused; a number = that single step's edge focused.
|
|
638
632
|
const [focusedStepIndex, setFocusedStepIndex] = useState<number | null>(
|
|
639
633
|
persisted.focusedStepIndex ?? null,
|
|
640
634
|
);
|
|
641
|
-
// Hovered
|
|
635
|
+
// Hovered trail in the flows panel: dims every canvas node/edge not
|
|
642
636
|
// involved in the hover preview (or selected ∪ hovered when a step is
|
|
643
637
|
// focused). `stepIndex: null` = whole flow (collapsed title hover);
|
|
644
638
|
// a number = that step. Transient — no drawer. Camera only reframes when
|
|
645
639
|
// a step is already selected.
|
|
646
|
-
const [
|
|
647
|
-
|
|
640
|
+
const [hoveredTrailStep, setHoveredTrailStep] = useState<{
|
|
641
|
+
trailId: string;
|
|
648
642
|
stepIndex: number | null;
|
|
649
643
|
} | null>(null);
|
|
650
|
-
// Sidebar bottom half: which panel is shown when
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
644
|
+
// Sidebar bottom half: which panel is shown when trails exist.
|
|
645
|
+
// The persisted value is membership-checked rather than trusted: a stale
|
|
646
|
+
// localStorage entry from before the walkthrough->trail rename would match
|
|
647
|
+
// neither panel and render a blank sidebar with no selected tab.
|
|
648
|
+
const [sidebarView, setSidebarView] = useState<'files' | 'trails'>(() => {
|
|
649
|
+
const stored = persisted.sidebarView;
|
|
650
|
+
const restored: 'files' | 'trails' =
|
|
651
|
+
stored === 'files' || stored === 'trails' ? stored : 'trails';
|
|
652
|
+
return trails?.length ? restored : 'files';
|
|
653
|
+
});
|
|
654
654
|
// Diagnostics area tab: issues list vs the Maintain agent pipeline.
|
|
655
655
|
const [diagnosticsTab, setDiagnosticsTab] = useState<'issues' | 'agents'>(
|
|
656
656
|
() => persisted.diagnosticsTab ?? 'issues',
|
|
657
657
|
);
|
|
658
658
|
// 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 && (
|
|
659
|
+
// Files / Trails tab picks: Files draws graphify static edges,
|
|
660
|
+
// Trails draws runtime step edges. Without visible tabs (no trails,
|
|
661
|
+
// or a sidebar-less embed) fall back to trail edges.
|
|
662
|
+
const sidebarTabsVisible = !hideSidebar && (trails?.length ?? 0) > 0;
|
|
663
663
|
const resolvedEdgeView: SubsystemEdgeView =
|
|
664
664
|
edgeView ??
|
|
665
665
|
(sidebarTabsVisible
|
|
666
|
-
? (sidebarView === '
|
|
667
|
-
: (
|
|
668
|
-
? '
|
|
666
|
+
? (sidebarView === 'trails' ? 'trails' : 'graphify')
|
|
667
|
+
: (trails?.length ?? 0) > 0
|
|
668
|
+
? 'trails'
|
|
669
669
|
: (graphifyRelations?.length ?? 0) > 0
|
|
670
670
|
? 'graphify'
|
|
671
|
-
: '
|
|
672
|
-
//
|
|
671
|
+
: 'trails');
|
|
672
|
+
// Trail flows the user has expanded (via the title row). Closed by
|
|
673
673
|
// default so a graph with several flows doesn't dump every step list at once.
|
|
674
674
|
// Restored from the persisted view state when a `persistKey` is set.
|
|
675
|
-
const [
|
|
676
|
-
() => new Set(persisted.
|
|
675
|
+
const [expandedTrails, setExpandedTrails] = useState<Set<string>>(
|
|
676
|
+
() => new Set(persisted.expandedTrails ?? []),
|
|
677
677
|
);
|
|
678
678
|
// Sidebar description visibility. Hidden by default so the files/flows
|
|
679
679
|
// panel gets the vertical room; the title-row toggle reveals it.
|
|
@@ -732,18 +732,18 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
732
732
|
document.removeEventListener('mouseup', onUp);
|
|
733
733
|
};
|
|
734
734
|
}, [sidebarDrag, sidebarMaxWidth, fitView, persistKey]);
|
|
735
|
-
// Persist the
|
|
735
|
+
// Persist the trail working set per model. Each write merges into the
|
|
736
736
|
// stored blob, so the three fields never clobber one another.
|
|
737
737
|
useEffect(() => {
|
|
738
738
|
if (!persistKey) return;
|
|
739
739
|
writeViewState(persistKey, {
|
|
740
|
-
|
|
740
|
+
expandedTrails: Array.from(expandedTrails),
|
|
741
741
|
});
|
|
742
|
-
}, [persistKey,
|
|
742
|
+
}, [persistKey, expandedTrails]);
|
|
743
743
|
useEffect(() => {
|
|
744
744
|
if (!persistKey) return;
|
|
745
|
-
writeViewState(persistKey, {
|
|
746
|
-
}, [persistKey,
|
|
745
|
+
writeViewState(persistKey, { focusedTrailId });
|
|
746
|
+
}, [persistKey, focusedTrailId]);
|
|
747
747
|
useEffect(() => {
|
|
748
748
|
if (!persistKey) return;
|
|
749
749
|
writeViewState(persistKey, { focusedStepIndex });
|
|
@@ -776,59 +776,59 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
776
776
|
? { file: drawerTarget.file, startLine: drawerTarget.startLine }
|
|
777
777
|
: null;
|
|
778
778
|
|
|
779
|
-
const
|
|
780
|
-
if (drawerTarget?.kind !== '
|
|
781
|
-
return
|
|
782
|
-
}, [drawerTarget,
|
|
779
|
+
const focusedTrail = useMemo(() => {
|
|
780
|
+
if (drawerTarget?.kind !== 'trail' || !trails) return null;
|
|
781
|
+
return trails.find((t) => t.id === drawerTarget.trailId) ?? null;
|
|
782
|
+
}, [drawerTarget, trails]);
|
|
783
783
|
// Autoplay focus: a sidebar-less embed has no expanded flow to "open", so
|
|
784
784
|
// branding every step as a selection would hide all other nodes. Instead the
|
|
785
|
-
// autoplay sets this (flow id + step) to frame the
|
|
785
|
+
// autoplay sets this (flow id + step) to frame the step and dim the rest —
|
|
786
786
|
// same treatment as hover — without hiding anything. Declared before the
|
|
787
787
|
// overlay memos, which read it to title the canvas chip + step bar.
|
|
788
788
|
const [autoPlayFocus, setAutoPlayFocus] = useState<
|
|
789
|
-
{
|
|
789
|
+
{ trailId: string; stepIndex: number } | null
|
|
790
790
|
>(null);
|
|
791
|
-
// Ref mirror of the drawer's
|
|
791
|
+
// Ref mirror of the drawer's trail id so the symbol resolver stays
|
|
792
792
|
// stable across graph re-renders (the drawer is memoized on callback identity).
|
|
793
|
-
const
|
|
794
|
-
|
|
793
|
+
const drawerTrailIdRef = useRef<string | null>(null);
|
|
794
|
+
drawerTrailIdRef.current = focusedTrail?.id ?? null;
|
|
795
795
|
|
|
796
|
-
//
|
|
796
|
+
// Trail shown on the canvas title chip (focus, hover highlight, or
|
|
797
797
|
// autoplay focus).
|
|
798
|
-
const
|
|
799
|
-
if (!
|
|
798
|
+
const overlayTrailTitle = useMemo(() => {
|
|
799
|
+
if (!showTrailTitle || !trails?.length) return null;
|
|
800
800
|
const id =
|
|
801
|
-
|
|
802
|
-
autoPlayFocus?.
|
|
803
|
-
|
|
801
|
+
focusedTrailId ??
|
|
802
|
+
autoPlayFocus?.trailId ??
|
|
803
|
+
hoveredTrailStep?.trailId;
|
|
804
804
|
if (!id) return null;
|
|
805
|
-
return
|
|
805
|
+
return trails.find((t) => t.id === id)?.title ?? null;
|
|
806
806
|
}, [
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
807
|
+
showTrailTitle,
|
|
808
|
+
trails,
|
|
809
|
+
focusedTrailId,
|
|
810
810
|
autoPlayFocus,
|
|
811
|
-
|
|
811
|
+
hoveredTrailStep,
|
|
812
812
|
]);
|
|
813
813
|
|
|
814
814
|
// Active step for the bottom-of-title progress + annotation chip.
|
|
815
|
-
const
|
|
816
|
-
if (!
|
|
815
|
+
const overlayTrailStep = useMemo(() => {
|
|
816
|
+
if (!showTrailTitle || !trails?.length) return null;
|
|
817
817
|
const tlId =
|
|
818
|
-
|
|
819
|
-
autoPlayFocus?.
|
|
820
|
-
|
|
818
|
+
focusedTrailId ??
|
|
819
|
+
autoPlayFocus?.trailId ??
|
|
820
|
+
hoveredTrailStep?.trailId ??
|
|
821
821
|
null;
|
|
822
822
|
let stepIndex: number | null = null;
|
|
823
|
-
if (
|
|
823
|
+
if (focusedTrailId != null) {
|
|
824
824
|
stepIndex = focusedStepIndex;
|
|
825
825
|
} else if (autoPlayFocus != null) {
|
|
826
826
|
stepIndex = autoPlayFocus.stepIndex;
|
|
827
|
-
} else if (
|
|
828
|
-
stepIndex =
|
|
827
|
+
} else if (hoveredTrailStep != null) {
|
|
828
|
+
stepIndex = hoveredTrailStep.stepIndex;
|
|
829
829
|
}
|
|
830
830
|
if (tlId == null || stepIndex == null) return null;
|
|
831
|
-
const tl =
|
|
831
|
+
const tl = trails.find((t) => t.id === tlId);
|
|
832
832
|
const step = tl?.steps[stepIndex];
|
|
833
833
|
if (!step || !tl) return null;
|
|
834
834
|
return {
|
|
@@ -837,25 +837,25 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
837
837
|
annotation: step.annotation,
|
|
838
838
|
};
|
|
839
839
|
}, [
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
840
|
+
showTrailTitle,
|
|
841
|
+
trails,
|
|
842
|
+
focusedTrailId,
|
|
843
843
|
focusedStepIndex,
|
|
844
844
|
autoPlayFocus,
|
|
845
|
-
|
|
845
|
+
hoveredTrailStep,
|
|
846
846
|
]);
|
|
847
847
|
|
|
848
848
|
const drawerTitle = useMemo(() => {
|
|
849
849
|
if (!drawerTarget) return null;
|
|
850
850
|
if (drawerTarget.kind === 'file') return drawerTarget.file;
|
|
851
|
-
const tl =
|
|
851
|
+
const tl = focusedTrail;
|
|
852
852
|
if (!tl) return null;
|
|
853
853
|
if (drawerTarget.stepIndex == null) return tl.title;
|
|
854
854
|
const step = tl.steps[drawerTarget.stepIndex];
|
|
855
855
|
if (!step) return tl.title;
|
|
856
856
|
const site = `${step.file.split('/').pop() ?? step.file}:${step.line}`;
|
|
857
857
|
return `${tl.title} · ${site}`;
|
|
858
|
-
}, [drawerTarget,
|
|
858
|
+
}, [drawerTarget, focusedTrail]);
|
|
859
859
|
|
|
860
860
|
// Refresh selected component when the components list updates (e.g. verify
|
|
861
861
|
// writes back declarationRef).
|
|
@@ -869,14 +869,14 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
869
869
|
// The pane stays hidden until Pass 2 completes. Key off layout-affecting
|
|
870
870
|
// fields only — declarationRef updates after verify must not re-run ELK.
|
|
871
871
|
const layoutKey = useMemo(
|
|
872
|
-
() => subsystemGraphLayoutKey({ components,
|
|
873
|
-
[components,
|
|
872
|
+
() => subsystemGraphLayoutKey({ components, trails, graphifyRelations }),
|
|
873
|
+
[components, trails, graphifyRelations],
|
|
874
874
|
);
|
|
875
875
|
const componentsRef = useRef(components);
|
|
876
|
-
const
|
|
876
|
+
const trailsRef = useRef(trails);
|
|
877
877
|
const graphifyRelationsRef = useRef(graphifyRelations);
|
|
878
878
|
componentsRef.current = components;
|
|
879
|
-
|
|
879
|
+
trailsRef.current = trails;
|
|
880
880
|
graphifyRelationsRef.current = graphifyRelations;
|
|
881
881
|
|
|
882
882
|
// Track measured dimensions from React Flow's dimension changes.
|
|
@@ -896,7 +896,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
896
896
|
pass2GenRef.current += 1;
|
|
897
897
|
const doc = {
|
|
898
898
|
components: componentsRef.current,
|
|
899
|
-
|
|
899
|
+
trails: trailsRef.current,
|
|
900
900
|
};
|
|
901
901
|
void buildSubsystemGraph(doc, {
|
|
902
902
|
maxNodeWidth,
|
|
@@ -953,7 +953,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
953
953
|
const measuredHeights = new Map(leafNodes.map((n) => [n.id, dims.get(n.id)!.height]));
|
|
954
954
|
const gen = ++pass2GenRef.current;
|
|
955
955
|
void buildSubsystemGraph(
|
|
956
|
-
{ components,
|
|
956
|
+
{ components, trails },
|
|
957
957
|
{ maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, moduleNesting, graphifyRelations, orderByLine },
|
|
958
958
|
)
|
|
959
959
|
.then(({ nodes, edges: e, absoluteRects }) => {
|
|
@@ -967,7 +967,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
967
967
|
// Reveal Pass 1 layout rather than leaving the cover up forever.
|
|
968
968
|
setLayoutReady(true);
|
|
969
969
|
});
|
|
970
|
-
}, [built.nodes, components,
|
|
970
|
+
}, [built.nodes, components, trails, graphifyRelations, orderByLine, maxNodeWidth, showEdgeLabels, moduleNesting]);
|
|
971
971
|
|
|
972
972
|
// After Pass 1 commits, try Pass 2 immediately with retained measurements.
|
|
973
973
|
// Same-id live updates often get no new React Flow `dimensions` events, so
|
|
@@ -1008,8 +1008,8 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1008
1008
|
}
|
|
1009
1009
|
const src = edgeById.get(edgeId);
|
|
1010
1010
|
setSelectedEdgeId(edgeId);
|
|
1011
|
-
// A direct edge selection on the canvas supersedes any
|
|
1012
|
-
|
|
1011
|
+
// A direct edge selection on the canvas supersedes any trail focus.
|
|
1012
|
+
setFocusedTrailId(null);
|
|
1013
1013
|
setFocusedStepIndex(null);
|
|
1014
1014
|
if (src) onEdgeSelect?.(src);
|
|
1015
1015
|
},
|
|
@@ -1029,7 +1029,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1029
1029
|
}
|
|
1030
1030
|
setSelected(comp);
|
|
1031
1031
|
setSelectedEdgeId(null);
|
|
1032
|
-
|
|
1032
|
+
setFocusedTrailId(null);
|
|
1033
1033
|
setFocusedStepIndex(null);
|
|
1034
1034
|
onSelect?.(alias);
|
|
1035
1035
|
}
|
|
@@ -1083,22 +1083,22 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1083
1083
|
[components],
|
|
1084
1084
|
);
|
|
1085
1085
|
|
|
1086
|
-
// A `step` issue target names ONE step of a
|
|
1086
|
+
// A `step` issue target names ONE step of a trail — the trail id in
|
|
1087
1087
|
// `id`, the 0-based step position in `stepIndex`. Validated against the loaded
|
|
1088
|
-
//
|
|
1088
|
+
// trails so a finding left over from an edited flow resolves to null
|
|
1089
1089
|
// rather than framing whatever now happens to sit at that index.
|
|
1090
1090
|
const issueStep = useCallback(
|
|
1091
1091
|
(
|
|
1092
1092
|
issue: SubsystemIssue,
|
|
1093
|
-
): {
|
|
1093
|
+
): { trail: SubsystemTrail; stepIndex: number } | null => {
|
|
1094
1094
|
const target = issue.target;
|
|
1095
1095
|
if (target?.kind !== 'step') return null;
|
|
1096
1096
|
if (target.id == null || target.stepIndex == null) return null;
|
|
1097
|
-
const
|
|
1098
|
-
if (!
|
|
1099
|
-
return {
|
|
1097
|
+
const trail = trails?.find((t) => t.id === target.id);
|
|
1098
|
+
if (!trail?.steps[target.stepIndex]) return null;
|
|
1099
|
+
return { trail, stepIndex: target.stepIndex };
|
|
1100
1100
|
},
|
|
1101
|
-
[
|
|
1101
|
+
[trails],
|
|
1102
1102
|
);
|
|
1103
1103
|
|
|
1104
1104
|
// Per-node diagnostics badge: fold each component-targeted finding that maps
|
|
@@ -1194,7 +1194,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1194
1194
|
// target:
|
|
1195
1195
|
// component → that node
|
|
1196
1196
|
// module → every component in that module
|
|
1197
|
-
//
|
|
1197
|
+
// trail → the flow's nodes + its step edges
|
|
1198
1198
|
// repo → every component of that repo
|
|
1199
1199
|
// graph → whole-graph finding; implicates nothing specific
|
|
1200
1200
|
// Returns null when nothing is expanded, no expanded layer has findings, or
|
|
@@ -1225,15 +1225,15 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1225
1225
|
nodeIds.add(c.alias);
|
|
1226
1226
|
}
|
|
1227
1227
|
}
|
|
1228
|
-
} else if (t.kind === '
|
|
1229
|
-
const wt = (
|
|
1228
|
+
} else if (t.kind === 'trail') {
|
|
1229
|
+
const wt = (trails ?? []).find(
|
|
1230
1230
|
(w) => w.id === t.id || w.title === t.label,
|
|
1231
1231
|
);
|
|
1232
1232
|
if (wt) {
|
|
1233
1233
|
for (const s of wt.steps) {
|
|
1234
1234
|
nodeIds.add(s.from);
|
|
1235
1235
|
nodeIds.add(s.to);
|
|
1236
|
-
edgeIds.add(
|
|
1236
|
+
edgeIds.add(trailStepGraphEdgeId(s));
|
|
1237
1237
|
}
|
|
1238
1238
|
}
|
|
1239
1239
|
} else if (t.kind === 'repo') {
|
|
@@ -1250,7 +1250,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1250
1250
|
if (nodeIds.has(e.source) && nodeIds.has(e.target)) edgeIds.add(e.id);
|
|
1251
1251
|
}
|
|
1252
1252
|
return { nodeIds, edgeIds };
|
|
1253
|
-
}, [issues, expandedIssueCategories, components,
|
|
1253
|
+
}, [issues, expandedIssueCategories, components, trails, resolveAlias]);
|
|
1254
1254
|
const issueFocusNodeIds = issueFocus?.nodeIds ?? null;
|
|
1255
1255
|
const issueFocusEdgeIds = issueFocus?.edgeIds ?? null;
|
|
1256
1256
|
|
|
@@ -1262,58 +1262,58 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1262
1262
|
selectedEdgeIdRef.current = selectedEdgeId;
|
|
1263
1263
|
// Ref mirrors of the focused step, so `unfocusIssueTarget` (a useCallback
|
|
1264
1264
|
// 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<{
|
|
1265
|
+
// since moved elsewhere in the trail panel.
|
|
1266
|
+
const focusedStepRef = useRef<{ trailId: string; stepIndex: number } | null>(
|
|
1267
1267
|
null,
|
|
1268
1268
|
);
|
|
1269
1269
|
focusedStepRef.current =
|
|
1270
|
-
|
|
1271
|
-
? {
|
|
1270
|
+
focusedTrailId != null && focusedStepIndex != null
|
|
1271
|
+
? { trailId: focusedTrailId, stepIndex: focusedStepIndex }
|
|
1272
1272
|
: null;
|
|
1273
1273
|
|
|
1274
|
-
// Edge ids in
|
|
1275
|
-
// step's edge). Used to frame the camera. `null` = no
|
|
1274
|
+
// Edge ids in trail focus (an active flow's edge set, or a single
|
|
1275
|
+
// step's edge). Used to frame the camera. `null` = no trail focus.
|
|
1276
1276
|
const focusEdgeIds = useMemo(() => { if (autoPlayFocus) {
|
|
1277
|
-
const tl =
|
|
1277
|
+
const tl = trails?.find((t) => t.id === autoPlayFocus.trailId);
|
|
1278
1278
|
const step = tl?.steps[autoPlayFocus.stepIndex];
|
|
1279
|
-
return step ? new Set([
|
|
1279
|
+
return step ? new Set([trailStepGraphEdgeId(step)]) : null;
|
|
1280
1280
|
}
|
|
1281
|
-
if (
|
|
1282
|
-
const tl =
|
|
1281
|
+
if (focusedTrailId == null || !trails) return null;
|
|
1282
|
+
const tl = trails.find((t) => t.id === focusedTrailId);
|
|
1283
1283
|
if (!tl) return null;
|
|
1284
1284
|
if (focusedStepIndex != null) {
|
|
1285
1285
|
const step = tl.steps[focusedStepIndex];
|
|
1286
|
-
return step ? new Set([
|
|
1286
|
+
return step ? new Set([trailStepGraphEdgeId(step)]) : null;
|
|
1287
1287
|
}
|
|
1288
|
-
return new Set(tl.steps.map((s) =>
|
|
1289
|
-
}, [
|
|
1288
|
+
return new Set(tl.steps.map((s) => trailStepGraphEdgeId(s)));
|
|
1289
|
+
}, [trails, focusedTrailId, focusedStepIndex, autoPlayFocus]);
|
|
1290
1290
|
|
|
1291
1291
|
// 1-based step numbers per edge of the active flow (focused or
|
|
1292
1292
|
// hover/autoplay-highlighted). An edge can appear in more than one step.
|
|
1293
1293
|
const selectedFlowStepNos = useMemo(() => {
|
|
1294
|
-
const activeId =
|
|
1295
|
-
if (activeId == null || !
|
|
1296
|
-
const tl =
|
|
1294
|
+
const activeId = focusedTrailId ?? hoveredTrailStep?.trailId;
|
|
1295
|
+
if (activeId == null || !trails) return null;
|
|
1296
|
+
const tl = trails.find((t) => t.id === activeId);
|
|
1297
1297
|
if (!tl) return null;
|
|
1298
1298
|
const map = new Map<string, number[]>();
|
|
1299
1299
|
tl.steps.forEach((s, i) => {
|
|
1300
|
-
const list = map.get(
|
|
1300
|
+
const list = map.get(trailStepGraphEdgeId(s)) ?? [];
|
|
1301
1301
|
list.push(i + 1);
|
|
1302
|
-
map.set(
|
|
1302
|
+
map.set(trailStepGraphEdgeId(s), list);
|
|
1303
1303
|
});
|
|
1304
1304
|
return map;
|
|
1305
|
-
}, [
|
|
1305
|
+
}, [trails, focusedTrailId, hoveredTrailStep]);
|
|
1306
1306
|
|
|
1307
|
-
// Union of every expanded (opened)
|
|
1307
|
+
// Union of every expanded (opened) trail's edges — the visible set.
|
|
1308
1308
|
const openedEdgeIds = useMemo(() => {
|
|
1309
|
-
if (!
|
|
1309
|
+
if (!trails || expandedTrails.size === 0) return null;
|
|
1310
1310
|
const ids = new Set<string>();
|
|
1311
|
-
for (const tl of
|
|
1312
|
-
if (!
|
|
1313
|
-
for (const s of tl.steps) ids.add(
|
|
1311
|
+
for (const tl of trails) {
|
|
1312
|
+
if (!expandedTrails.has(tl.id)) continue;
|
|
1313
|
+
for (const s of tl.steps) ids.add(trailStepGraphEdgeId(s));
|
|
1314
1314
|
}
|
|
1315
1315
|
return ids.size > 0 ? ids : null;
|
|
1316
|
-
}, [
|
|
1316
|
+
}, [trails, expandedTrails]);
|
|
1317
1317
|
|
|
1318
1318
|
const endpointsOf = (edgeIds: ReadonlySet<string> | null): Set<string> | null => {
|
|
1319
1319
|
if (!edgeIds) return null;
|
|
@@ -1327,20 +1327,20 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1327
1327
|
};
|
|
1328
1328
|
// Edge/nodes involved in the hovered step or whole flow.
|
|
1329
1329
|
const hoverEdgeIds = useMemo(() => {
|
|
1330
|
-
if (!
|
|
1331
|
-
const tl =
|
|
1330
|
+
if (!hoveredTrailStep || !trails) return null;
|
|
1331
|
+
const tl = trails.find((t) => t.id === hoveredTrailStep.trailId);
|
|
1332
1332
|
if (!tl) return null;
|
|
1333
|
-
if (
|
|
1334
|
-
return new Set(tl.steps.map((s) =>
|
|
1333
|
+
if (hoveredTrailStep.stepIndex == null) {
|
|
1334
|
+
return new Set(tl.steps.map((s) => trailStepGraphEdgeId(s)));
|
|
1335
1335
|
}
|
|
1336
|
-
const step = tl.steps[
|
|
1337
|
-
return step ? new Set([
|
|
1338
|
-
}, [
|
|
1336
|
+
const step = tl.steps[hoveredTrailStep.stepIndex];
|
|
1337
|
+
return step ? new Set([trailStepGraphEdgeId(step)]) : null;
|
|
1338
|
+
}, [trails, hoveredTrailStep]);
|
|
1339
1339
|
// While hovering with a *step* already selected, brighten the union of
|
|
1340
1340
|
// selected + hovered participants. Whole-flow focus (or no focus) keeps
|
|
1341
1341
|
// the old hover-replace preview so a step hover still dims the rest.
|
|
1342
1342
|
const previewEdgeIds = useMemo(() => {
|
|
1343
|
-
// Autoplay drives focus without a hover, so its
|
|
1343
|
+
// Autoplay drives focus without a hover, so its step is the preview set.
|
|
1344
1344
|
if (autoPlayFocus) return focusEdgeIds;
|
|
1345
1345
|
if (!hoverEdgeIds) return null;
|
|
1346
1346
|
// While hovering with a *step* already selected, brighten the union of
|
|
@@ -1489,7 +1489,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1489
1489
|
};
|
|
1490
1490
|
// Edges outside the selected view are hidden entirely (labels included).
|
|
1491
1491
|
// graphify-native edges belong to the graphify view (they are static
|
|
1492
|
-
// topology, not runtime
|
|
1492
|
+
// topology, not runtime steps) regardless of their verb.
|
|
1493
1493
|
const edgeInView = (e: Edge): boolean => {
|
|
1494
1494
|
const d = e.data as
|
|
1495
1495
|
| { mechanism?: string; provenance?: SubsystemEdgeProvenance }
|
|
@@ -1498,7 +1498,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1498
1498
|
if (resolvedEdgeView === 'graphify') {
|
|
1499
1499
|
return d?.provenance === 'graphify';
|
|
1500
1500
|
}
|
|
1501
|
-
return
|
|
1501
|
+
return isTrailMechanism(mechanism);
|
|
1502
1502
|
};
|
|
1503
1503
|
if (openedEdgeIds || focusEdgeIds || previewEdgeIds || issueFocusEdgeIds) {
|
|
1504
1504
|
return baseEdges.map((e) => {
|
|
@@ -1565,7 +1565,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1565
1565
|
(_e, node: Node) => {
|
|
1566
1566
|
const comp = (node.data as { component?: SubsystemComponent } | undefined)?.component;
|
|
1567
1567
|
setSelectedEdgeId(null);
|
|
1568
|
-
|
|
1568
|
+
setFocusedTrailId(null);
|
|
1569
1569
|
setFocusedStepIndex(null);
|
|
1570
1570
|
if (node.type === 'subsystem-component' && comp) {
|
|
1571
1571
|
// Clicking the already-selected node unselects it (toggle off).
|
|
@@ -1593,7 +1593,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1593
1593
|
const onPaneClick = useCallback(() => {
|
|
1594
1594
|
setSelected(null);
|
|
1595
1595
|
setSelectedEdgeId(null);
|
|
1596
|
-
|
|
1596
|
+
setFocusedTrailId(null);
|
|
1597
1597
|
setFocusedStepIndex(null);
|
|
1598
1598
|
issueModuleFocusRef.current = null;
|
|
1599
1599
|
}, []);
|
|
@@ -1601,8 +1601,8 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1601
1601
|
// Sidebar file trees — one per repo on multi-repo graphs, each under its
|
|
1602
1602
|
// own owner-avatar header. Clicking a header collapses that repo's tree.
|
|
1603
1603
|
const repoGroups = useMemo(() => buildRepoGroups(components), [components]);
|
|
1604
|
-
const
|
|
1605
|
-
// Aliases of proposed components — the flows panel tints
|
|
1604
|
+
const hasTrails = useMemo(() => (trails?.length ?? 0) > 0, [trails]);
|
|
1605
|
+
// Aliases of proposed components — the flows panel tints trail titles
|
|
1606
1606
|
// (and steps) that touch one.
|
|
1607
1607
|
const proposedAliases = useMemo(
|
|
1608
1608
|
() => new Set(components.filter((c) => c.proposed).map((c) => c.alias)),
|
|
@@ -1679,7 +1679,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1679
1679
|
const onNodeDoubleClick: NodeMouseHandler = useCallback(
|
|
1680
1680
|
(_e, node: Node) => {
|
|
1681
1681
|
// Boundary frame → frame its members on the canvas; double-click again to
|
|
1682
|
-
// zoom back out. User-initiated, so it ignores the
|
|
1682
|
+
// zoom back out. User-initiated, so it ignores the trail zoom gate.
|
|
1683
1683
|
if (node.type === 'subsystem-group') {
|
|
1684
1684
|
const key =
|
|
1685
1685
|
(node.data as { region?: { key?: string } } | undefined)?.region?.key;
|
|
@@ -1715,7 +1715,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1715
1715
|
[onOpenDeclarationFile, fitView, focusedBoundaryKey],
|
|
1716
1716
|
);
|
|
1717
1717
|
|
|
1718
|
-
const
|
|
1718
|
+
const onOpenFileFromTrail = useCallback(
|
|
1719
1719
|
(file: string, opts?: SubsystemOpenFileOptions) => {
|
|
1720
1720
|
setFileOverlay({ file, startLine: opts?.startLine });
|
|
1721
1721
|
onFileSelect?.(file);
|
|
@@ -1737,15 +1737,15 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1737
1737
|
const fileOverlayOpen = fileOverlay != null;
|
|
1738
1738
|
const collapseCanvas = drawerFillHeight || fileOverlayOpen;
|
|
1739
1739
|
|
|
1740
|
-
// Drop the full-file overlay when the underlying
|
|
1740
|
+
// Drop the full-file overlay when the underlying trail drawer changes
|
|
1741
1741
|
// or closes — open-file keeps the same drawerTarget so the snippets stay mounted.
|
|
1742
|
-
const
|
|
1743
|
-
drawerTarget?.kind === '
|
|
1744
|
-
? `${drawerTarget.
|
|
1742
|
+
const trailDrawerKey =
|
|
1743
|
+
drawerTarget?.kind === 'trail'
|
|
1744
|
+
? `${drawerTarget.trailId}:${drawerTarget.stepIndex}`
|
|
1745
1745
|
: null;
|
|
1746
1746
|
useEffect(() => {
|
|
1747
1747
|
setFileOverlay(null);
|
|
1748
|
-
}, [
|
|
1748
|
+
}, [trailDrawerKey]);
|
|
1749
1749
|
|
|
1750
1750
|
useEffect(() => {
|
|
1751
1751
|
if (!fileOverlayOpen) return;
|
|
@@ -1756,16 +1756,16 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1756
1756
|
return () => window.removeEventListener('keydown', onKey);
|
|
1757
1757
|
}, [fileOverlayOpen, closeFileOverlay]);
|
|
1758
1758
|
|
|
1759
|
-
// Camera helper shared by the
|
|
1759
|
+
// Camera helper shared by the trail interactions. Fits the union of
|
|
1760
1760
|
// the focused edges' endpoint rects AND their routed waypoints — the edge
|
|
1761
1761
|
// carries its full polyline (`elkPathPoints`, absolute flow coords), so the
|
|
1762
|
-
// frame is the line the
|
|
1762
|
+
// frame is the line the step actually traces. That keeps a step whose route
|
|
1763
1763
|
// bulges out around intervening nodes from being clipped at the viewport
|
|
1764
1764
|
// edge, and it is the edge's own geometry doing the guiding, not fudge
|
|
1765
1765
|
// padding.
|
|
1766
1766
|
const fitFocusBounds = useCallback(
|
|
1767
1767
|
(ids: ReadonlySet<string>) => {
|
|
1768
|
-
if (!
|
|
1768
|
+
if (!zoomOnTrailFocus) return;
|
|
1769
1769
|
const nodeIds = new Set<string>();
|
|
1770
1770
|
const points: { x: number; y: number }[] = [];
|
|
1771
1771
|
for (const e of baseEdges) {
|
|
@@ -1810,15 +1810,15 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1810
1810
|
};
|
|
1811
1811
|
fitBounds(bounds, {
|
|
1812
1812
|
padding: 0.15,
|
|
1813
|
-
duration:
|
|
1813
|
+
duration: trailFocusDurationMs,
|
|
1814
1814
|
});
|
|
1815
1815
|
},
|
|
1816
1816
|
[
|
|
1817
1817
|
baseEdges,
|
|
1818
1818
|
built.absoluteRects,
|
|
1819
1819
|
fitBounds,
|
|
1820
|
-
|
|
1821
|
-
|
|
1820
|
+
zoomOnTrailFocus,
|
|
1821
|
+
trailFocusDurationMs,
|
|
1822
1822
|
],
|
|
1823
1823
|
);
|
|
1824
1824
|
|
|
@@ -1876,16 +1876,16 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1876
1876
|
width: Math.max(1, maxX - minX),
|
|
1877
1877
|
height: Math.max(1, maxY - minY),
|
|
1878
1878
|
},
|
|
1879
|
-
{ padding, duration:
|
|
1879
|
+
{ padding, duration: trailFocusDurationMs },
|
|
1880
1880
|
);
|
|
1881
1881
|
},
|
|
1882
|
-
[rectForNode, fitBounds,
|
|
1882
|
+
[rectForNode, fitBounds, trailFocusDurationMs],
|
|
1883
1883
|
);
|
|
1884
1884
|
|
|
1885
|
-
// Zoom back out to the full diagram after the last expanded
|
|
1885
|
+
// Zoom back out to the full diagram after the last expanded trail
|
|
1886
1886
|
// closes (visibility restores non-flow nodes that were hidden).
|
|
1887
1887
|
const fitOverview = useCallback(() => {
|
|
1888
|
-
if (!
|
|
1888
|
+
if (!zoomOnTrailFocus) return;
|
|
1889
1889
|
fitView({
|
|
1890
1890
|
padding: 0.1,
|
|
1891
1891
|
includeHiddenNodes: false,
|
|
@@ -1893,115 +1893,115 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1893
1893
|
maxZoom: 2,
|
|
1894
1894
|
duration: 300,
|
|
1895
1895
|
});
|
|
1896
|
-
}, [fitView,
|
|
1896
|
+
}, [fitView, zoomOnTrailFocus]);
|
|
1897
1897
|
|
|
1898
1898
|
// Focus an entire flow: hide everything but the flow's nodes and edges, and
|
|
1899
1899
|
// frame the flow on the canvas. Selection state is cleared — the graph now
|
|
1900
1900
|
// reads as the narrative. No drawer: the code view only opens on a step
|
|
1901
|
-
// click. A stale
|
|
1901
|
+
// click. A stale trail drawer (from a previously focused flow's step)
|
|
1902
1902
|
// closes; an explicitly opened file drawer stays.
|
|
1903
1903
|
// In `dim` mode: clear step highlight and leave the full graph visible
|
|
1904
1904
|
// (no zoom / hide) — matching "step away from a hovered step".
|
|
1905
|
-
const
|
|
1906
|
-
(tl:
|
|
1905
|
+
const focusTrailEdges = useCallback(
|
|
1906
|
+
(tl: SubsystemTrail) => {
|
|
1907
1907
|
setSelected(null);
|
|
1908
1908
|
setSelectedEdgeId(null);
|
|
1909
|
-
|
|
1910
|
-
if (
|
|
1909
|
+
setHoveredTrailStep(null);
|
|
1910
|
+
if (trailStepMode === 'dim') {
|
|
1911
1911
|
setFocusedStepIndex(null);
|
|
1912
|
-
|
|
1913
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1912
|
+
setFocusedTrailId(null);
|
|
1913
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1914
1914
|
return;
|
|
1915
1915
|
}
|
|
1916
1916
|
setFocusedStepIndex(null);
|
|
1917
|
-
|
|
1918
|
-
fitFocusBounds(new Set(tl.steps.map((s) =>
|
|
1919
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1917
|
+
setFocusedTrailId(tl.id);
|
|
1918
|
+
fitFocusBounds(new Set(tl.steps.map((s) => trailStepGraphEdgeId(s))));
|
|
1919
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1920
1920
|
},
|
|
1921
|
-
[fitFocusBounds,
|
|
1921
|
+
[fitFocusBounds, trailStepMode],
|
|
1922
1922
|
);
|
|
1923
1923
|
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1924
|
+
const clearTrailFocus = useCallback(() => {
|
|
1925
|
+
setFocusedTrailId(null);
|
|
1926
1926
|
setFocusedStepIndex(null);
|
|
1927
|
-
|
|
1928
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1927
|
+
setHoveredTrailStep(null);
|
|
1928
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1929
1929
|
}, []);
|
|
1930
1930
|
|
|
1931
|
-
// Host deep-link: opening the graph from a
|
|
1931
|
+
// Host deep-link: opening the graph from a trail row selects that
|
|
1932
1932
|
// flow — expand its steps, focus its edges, and switch the sidebar to
|
|
1933
|
-
//
|
|
1933
|
+
// Trails. Guarded by a ref so a later in-tab selection isn't yanked
|
|
1934
1934
|
// back; a remount (or a new id) re-applies it.
|
|
1935
|
-
const
|
|
1935
|
+
const appliedInitialTrailRef = useRef<string | null>(null);
|
|
1936
1936
|
useEffect(() => {
|
|
1937
|
-
if (!
|
|
1938
|
-
if (
|
|
1939
|
-
if (!layoutReady || !
|
|
1940
|
-
const tl =
|
|
1937
|
+
if (!initialTrailId) return;
|
|
1938
|
+
if (appliedInitialTrailRef.current === initialTrailId) return;
|
|
1939
|
+
if (!layoutReady || !trails?.length) return;
|
|
1940
|
+
const tl = trails.find((t) => t.id === initialTrailId);
|
|
1941
1941
|
if (!tl) return;
|
|
1942
|
-
|
|
1943
|
-
setSidebarView('
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
}, [
|
|
1942
|
+
appliedInitialTrailRef.current = initialTrailId;
|
|
1943
|
+
setSidebarView('trails');
|
|
1944
|
+
setExpandedTrails((prev) => new Set(prev).add(tl.id));
|
|
1945
|
+
focusTrailEdges(tl);
|
|
1946
|
+
}, [initialTrailId, layoutReady, trails, focusTrailEdges]);
|
|
1947
1947
|
|
|
1948
1948
|
// Switching sidebar panels also switches the edge vocabulary. Leaving the
|
|
1949
|
-
//
|
|
1949
|
+
// Trails panel drops its canvas state (focus, expanded flows, selected
|
|
1950
1950
|
// edge) so the Files view's topology edges aren't gated by a flow the user
|
|
1951
1951
|
// can no longer see or un-dim.
|
|
1952
1952
|
const changeSidebarView = useCallback(
|
|
1953
|
-
(view: 'files' | '
|
|
1953
|
+
(view: 'files' | 'trails') => {
|
|
1954
1954
|
setSidebarView(view);
|
|
1955
|
-
if (view === '
|
|
1956
|
-
|
|
1957
|
-
|
|
1955
|
+
if (view === 'trails') return;
|
|
1956
|
+
clearTrailFocus();
|
|
1957
|
+
setExpandedTrails(new Set());
|
|
1958
1958
|
setSelectedEdgeId(null);
|
|
1959
1959
|
},
|
|
1960
|
-
[
|
|
1960
|
+
[clearTrailFocus],
|
|
1961
1961
|
);
|
|
1962
1962
|
|
|
1963
|
-
// Focus a single step's edge on the canvas and open/scroll the
|
|
1963
|
+
// Focus a single step's edge on the canvas and open/scroll the trail
|
|
1964
1964
|
// drawer to that step's snippet. Clicking the already-focused step clears
|
|
1965
1965
|
// step focus and returns to whole-flow (high-level) framing.
|
|
1966
1966
|
// In `dim` mode: only dim non-participants (same as hovering a step) —
|
|
1967
1967
|
// no camera move, no drawer, no hide.
|
|
1968
|
-
const
|
|
1969
|
-
(tl:
|
|
1968
|
+
const focusTrailStep = useCallback(
|
|
1969
|
+
(tl: SubsystemTrail, stepIndex: number) => {
|
|
1970
1970
|
const step = tl.steps[stepIndex];
|
|
1971
1971
|
if (!step) return;
|
|
1972
1972
|
setSelected(null);
|
|
1973
1973
|
setSelectedEdgeId(null);
|
|
1974
|
-
if (
|
|
1974
|
+
if (trailStepMode === 'dim') {
|
|
1975
1975
|
setFocusedStepIndex(null);
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1976
|
+
setFocusedTrailId(null);
|
|
1977
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex });
|
|
1978
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1979
1979
|
return;
|
|
1980
1980
|
}
|
|
1981
1981
|
// Toggle: clicking the selected step unselects it (back to whole flow).
|
|
1982
|
-
if (
|
|
1982
|
+
if (focusedTrailId === tl.id && focusedStepIndex === stepIndex) {
|
|
1983
1983
|
setFocusedStepIndex(null);
|
|
1984
1984
|
// Pointer is still over the row — keep hover preview so dimming doesn't
|
|
1985
1985
|
// flash off until mouseleave.
|
|
1986
|
-
|
|
1987
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1986
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex });
|
|
1987
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1988
1988
|
if (pendingFocusFitRef.current != null) {
|
|
1989
1989
|
window.clearTimeout(pendingFocusFitRef.current);
|
|
1990
1990
|
pendingFocusFitRef.current = null;
|
|
1991
1991
|
}
|
|
1992
|
-
fitFocusBounds(new Set(tl.steps.map((s) =>
|
|
1992
|
+
fitFocusBounds(new Set(tl.steps.map((s) => trailStepGraphEdgeId(s))));
|
|
1993
1993
|
return;
|
|
1994
1994
|
}
|
|
1995
1995
|
setFocusedStepIndex(stepIndex);
|
|
1996
|
-
|
|
1997
|
-
|
|
1996
|
+
setFocusedTrailId(tl.id);
|
|
1997
|
+
setHoveredTrailStep(null);
|
|
1998
1998
|
// Open the drawer before fitting. If it was closed, wait for its height
|
|
1999
1999
|
// transition so fitView uses the reduced canvas — not full height.
|
|
2000
2000
|
const drawerWasOpen = drawerOpenRef.current;
|
|
2001
|
-
if (
|
|
2001
|
+
if (renderTrailViewer) {
|
|
2002
2002
|
setDrawerTarget({
|
|
2003
|
-
kind: '
|
|
2004
|
-
|
|
2003
|
+
kind: 'trail',
|
|
2004
|
+
trailId: tl.id,
|
|
2005
2005
|
stepIndex,
|
|
2006
2006
|
});
|
|
2007
2007
|
} else {
|
|
@@ -2011,7 +2011,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2011
2011
|
startLine: step.line,
|
|
2012
2012
|
});
|
|
2013
2013
|
}
|
|
2014
|
-
const edgeIds = new Set([
|
|
2014
|
+
const edgeIds = new Set([trailStepGraphEdgeId(step)]);
|
|
2015
2015
|
if (pendingFocusFitRef.current != null) {
|
|
2016
2016
|
window.clearTimeout(pendingFocusFitRef.current);
|
|
2017
2017
|
pendingFocusFitRef.current = null;
|
|
@@ -2028,9 +2028,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2028
2028
|
[
|
|
2029
2029
|
fitFocusBounds,
|
|
2030
2030
|
focusedStepIndex,
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2031
|
+
focusedTrailId,
|
|
2032
|
+
renderTrailViewer,
|
|
2033
|
+
trailStepMode,
|
|
2034
2034
|
],
|
|
2035
2035
|
);
|
|
2036
2036
|
|
|
@@ -2052,10 +2052,10 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2052
2052
|
const prev = prevPreviewEdgeIdsRef.current;
|
|
2053
2053
|
prevPreviewEdgeIdsRef.current = previewEdgeIds;
|
|
2054
2054
|
|
|
2055
|
-
if (!
|
|
2055
|
+
if (!zoomOnTrailFocus || trailStepMode === 'dim') return;
|
|
2056
2056
|
// Camera follows hover only when a specific step is already focused (or
|
|
2057
2057
|
// autoplay is driving an auto-focus).
|
|
2058
|
-
if (!autoPlayFocus && (
|
|
2058
|
+
if (!autoPlayFocus && (focusedTrailId == null || focusedStepIndex == null)) {
|
|
2059
2059
|
return;
|
|
2060
2060
|
}
|
|
2061
2061
|
|
|
@@ -2069,26 +2069,26 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2069
2069
|
}, [
|
|
2070
2070
|
previewEdgeIds,
|
|
2071
2071
|
focusEdgeIds,
|
|
2072
|
-
|
|
2072
|
+
focusedTrailId,
|
|
2073
2073
|
focusedStepIndex,
|
|
2074
2074
|
autoPlayFocus,
|
|
2075
2075
|
fitFocusBounds,
|
|
2076
|
-
|
|
2077
|
-
|
|
2076
|
+
zoomOnTrailFocus,
|
|
2077
|
+
trailStepMode,
|
|
2078
2078
|
]);
|
|
2079
2079
|
|
|
2080
|
-
// Graph-only embeds: cycle
|
|
2081
|
-
// selected (camera frames it via fitFocusBounds) and, when a
|
|
2080
|
+
// Graph-only embeds: cycle trail steps. In `focus` mode each step is
|
|
2081
|
+
// selected (camera frames it via fitFocusBounds) and, when a trail
|
|
2082
2082
|
// viewer is supplied, its snippet drawer opens; in `dim` mode it only
|
|
2083
2083
|
// dim-highlights the step (hover-style) with no camera move.
|
|
2084
2084
|
useEffect(() => {
|
|
2085
|
-
if (!
|
|
2085
|
+
if (!autoPlayTrails || trails == null || trails.length === 0) return;
|
|
2086
2086
|
if (!layoutReady) return;
|
|
2087
|
-
const playable =
|
|
2087
|
+
const playable = trails.filter((tl) => tl.steps.length > 0);
|
|
2088
2088
|
if (playable.length === 0) return;
|
|
2089
2089
|
// Only run the cycles that can actually be shown in the current edge view.
|
|
2090
2090
|
const inView = playable.filter((tl) =>
|
|
2091
|
-
tl.steps.some((s) =>
|
|
2091
|
+
tl.steps.some((s) => isTrailMechanism(s.mechanism)),
|
|
2092
2092
|
);
|
|
2093
2093
|
if (inView.length === 0) return;
|
|
2094
2094
|
const cyc = inView;
|
|
@@ -2097,8 +2097,8 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2097
2097
|
let tlIdx = 0;
|
|
2098
2098
|
let stepIdx = 0;
|
|
2099
2099
|
let timer: number | null = null;
|
|
2100
|
-
const interval = Math.max(400,
|
|
2101
|
-
const focusMode =
|
|
2100
|
+
const interval = Math.max(400, trailAutoPlayIntervalMs);
|
|
2101
|
+
const focusMode = trailStepMode !== 'dim';
|
|
2102
2102
|
let cleanup = () => {};
|
|
2103
2103
|
|
|
2104
2104
|
const tick = () => {
|
|
@@ -2107,15 +2107,15 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2107
2107
|
setSelected(null);
|
|
2108
2108
|
setSelectedEdgeId(null);
|
|
2109
2109
|
if (focusMode) {
|
|
2110
|
-
|
|
2111
|
-
|
|
2110
|
+
setHoveredTrailStep(null);
|
|
2111
|
+
setFocusedTrailId(null);
|
|
2112
2112
|
setFocusedStepIndex(null);
|
|
2113
|
-
setAutoPlayFocus({
|
|
2113
|
+
setAutoPlayFocus({ trailId: tl.id, stepIndex: stepIdx });
|
|
2114
2114
|
if (!hideDrawer) {
|
|
2115
2115
|
const step = tl.steps[stepIdx];
|
|
2116
2116
|
if (step) {
|
|
2117
|
-
if (
|
|
2118
|
-
setDrawerTarget({ kind: '
|
|
2117
|
+
if (renderTrailViewer) {
|
|
2118
|
+
setDrawerTarget({ kind: 'trail', trailId: tl.id, stepIndex: stepIdx });
|
|
2119
2119
|
} else {
|
|
2120
2120
|
setDrawerTarget({ kind: 'file', file: step.file, startLine: step.line });
|
|
2121
2121
|
}
|
|
@@ -2123,9 +2123,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2123
2123
|
}
|
|
2124
2124
|
} else {
|
|
2125
2125
|
setAutoPlayFocus(null);
|
|
2126
|
-
|
|
2126
|
+
setFocusedTrailId(null);
|
|
2127
2127
|
setFocusedStepIndex(null);
|
|
2128
|
-
|
|
2128
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex: stepIdx });
|
|
2129
2129
|
}
|
|
2130
2130
|
stepIdx += 1;
|
|
2131
2131
|
if (stepIdx >= tl.steps.length) {
|
|
@@ -2139,29 +2139,29 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2139
2139
|
cleanup = () => {
|
|
2140
2140
|
cancelled = true;
|
|
2141
2141
|
if (timer != null) window.clearTimeout(timer);
|
|
2142
|
-
|
|
2143
|
-
|
|
2142
|
+
setHoveredTrailStep(null);
|
|
2143
|
+
setFocusedTrailId(null);
|
|
2144
2144
|
setFocusedStepIndex(null);
|
|
2145
2145
|
setAutoPlayFocus(null);
|
|
2146
2146
|
};
|
|
2147
2147
|
return cleanup;
|
|
2148
2148
|
}, [
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2149
|
+
autoPlayTrails,
|
|
2150
|
+
trails,
|
|
2151
|
+
trailAutoPlayIntervalMs,
|
|
2152
2152
|
layoutReady,
|
|
2153
|
-
|
|
2154
|
-
|
|
2153
|
+
trailStepMode,
|
|
2154
|
+
renderTrailViewer,
|
|
2155
2155
|
hideDrawer,
|
|
2156
2156
|
]);
|
|
2157
2157
|
|
|
2158
|
-
// Arrow keys step through the focused
|
|
2158
|
+
// Arrow keys step through the focused trail once a step is active
|
|
2159
2159
|
// (sidebar click or drawer open). Ignores typing targets and chords.
|
|
2160
2160
|
useEffect(() => {
|
|
2161
|
-
if (
|
|
2161
|
+
if (focusedTrailId == null || focusedStepIndex == null || !trails) {
|
|
2162
2162
|
return;
|
|
2163
2163
|
}
|
|
2164
|
-
const tl =
|
|
2164
|
+
const tl = trails.find((t) => t.id === focusedTrailId);
|
|
2165
2165
|
if (!tl || tl.steps.length === 0) return;
|
|
2166
2166
|
|
|
2167
2167
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
@@ -2187,31 +2187,31 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2187
2187
|
}
|
|
2188
2188
|
if (next === focusedStepIndex) return;
|
|
2189
2189
|
e.preventDefault();
|
|
2190
|
-
|
|
2190
|
+
focusTrailStep(tl, next);
|
|
2191
2191
|
};
|
|
2192
2192
|
|
|
2193
2193
|
window.addEventListener('keydown', onKeyDown);
|
|
2194
2194
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
2195
2195
|
}, [
|
|
2196
|
-
|
|
2196
|
+
focusedTrailId,
|
|
2197
2197
|
focusedStepIndex,
|
|
2198
|
-
|
|
2199
|
-
|
|
2198
|
+
trails,
|
|
2199
|
+
focusTrailStep,
|
|
2200
2200
|
]);
|
|
2201
2201
|
|
|
2202
|
-
const
|
|
2202
|
+
const toggleTrailCollapsed = useCallback(
|
|
2203
2203
|
(tlId: string) => {
|
|
2204
2204
|
const collapsingLast =
|
|
2205
|
-
|
|
2205
|
+
expandedTrails.has(tlId) && expandedTrails.size === 1;
|
|
2206
2206
|
|
|
2207
|
-
|
|
2207
|
+
setExpandedTrails((prev) => {
|
|
2208
2208
|
const next = new Set(prev);
|
|
2209
2209
|
if (next.has(tlId)) next.delete(tlId);
|
|
2210
2210
|
else next.add(tlId);
|
|
2211
2211
|
return next;
|
|
2212
2212
|
});
|
|
2213
2213
|
|
|
2214
|
-
if (!collapsingLast || !
|
|
2214
|
+
if (!collapsingLast || !zoomOnTrailFocus) return;
|
|
2215
2215
|
|
|
2216
2216
|
// Cancel any pending focus fit so it doesn't fight the overview zoom.
|
|
2217
2217
|
if (pendingFocusFitRef.current != null) {
|
|
@@ -2231,7 +2231,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2231
2231
|
drawerWasOpen ? FILE_DRAWER_HEIGHT_MS + 20 : 20,
|
|
2232
2232
|
);
|
|
2233
2233
|
},
|
|
2234
|
-
[
|
|
2234
|
+
[expandedTrails, fitOverview, zoomOnTrailFocus],
|
|
2235
2235
|
);
|
|
2236
2236
|
|
|
2237
2237
|
// Detail-panel links: related-name clicks select the matching component —
|
|
@@ -2249,7 +2249,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2249
2249
|
if (!comp || comp.alias === selectedRef.current?.alias) return;
|
|
2250
2250
|
setSelected(comp);
|
|
2251
2251
|
setSelectedEdgeId(null);
|
|
2252
|
-
|
|
2252
|
+
setFocusedTrailId(null);
|
|
2253
2253
|
setFocusedStepIndex(null);
|
|
2254
2254
|
onSelect?.(comp.alias);
|
|
2255
2255
|
},
|
|
@@ -2268,9 +2268,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2268
2268
|
if (comp) {
|
|
2269
2269
|
setSelected(comp);
|
|
2270
2270
|
setSelectedEdgeId(null);
|
|
2271
|
-
|
|
2271
|
+
setFocusedTrailId(null);
|
|
2272
2272
|
setFocusedStepIndex(null);
|
|
2273
|
-
|
|
2273
|
+
setHoveredTrailStep(null);
|
|
2274
2274
|
issueModuleFocusRef.current = null;
|
|
2275
2275
|
onSelect?.(comp.alias);
|
|
2276
2276
|
requestAnimationFrame(() => {
|
|
@@ -2284,11 +2284,11 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2284
2284
|
});
|
|
2285
2285
|
} else if (step) {
|
|
2286
2286
|
// A step finding focuses the step itself, through the same entry point
|
|
2287
|
-
// the
|
|
2287
|
+
// the trail panel uses — so the drawer, the step numbering, the
|
|
2288
2288
|
// dim-mode hover preview, and the camera all behave identically whether
|
|
2289
2289
|
// the step was reached from the panel or from a diagnostics card.
|
|
2290
2290
|
issueModuleFocusRef.current = null;
|
|
2291
|
-
|
|
2291
|
+
focusTrailStep(step.trail, step.stepIndex);
|
|
2292
2292
|
} else {
|
|
2293
2293
|
// Nothing selectable — but a module target names a boundary frame, so
|
|
2294
2294
|
// frame that region.
|
|
@@ -2296,9 +2296,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2296
2296
|
if (moduleNodeId) {
|
|
2297
2297
|
setSelected(null);
|
|
2298
2298
|
setSelectedEdgeId(null);
|
|
2299
|
-
|
|
2299
|
+
setFocusedTrailId(null);
|
|
2300
2300
|
setFocusedStepIndex(null);
|
|
2301
|
-
|
|
2301
|
+
setHoveredTrailStep(null);
|
|
2302
2302
|
issueModuleFocusRef.current = moduleNodeId;
|
|
2303
2303
|
requestAnimationFrame(() => {
|
|
2304
2304
|
fitNodeRects(new Set([moduleNodeId]), 0.4);
|
|
@@ -2311,7 +2311,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2311
2311
|
issueComponent,
|
|
2312
2312
|
issueStep,
|
|
2313
2313
|
issueModuleNodeId,
|
|
2314
|
-
|
|
2314
|
+
focusTrailStep,
|
|
2315
2315
|
onSelect,
|
|
2316
2316
|
onSelectIssue,
|
|
2317
2317
|
fitView,
|
|
@@ -2341,14 +2341,14 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2341
2341
|
// not the card), and correctly leaves that alone here.
|
|
2342
2342
|
const owned = focusedStepRef.current;
|
|
2343
2343
|
if (
|
|
2344
|
-
owned?.
|
|
2344
|
+
owned?.trailId !== step.trail.id ||
|
|
2345
2345
|
owned?.stepIndex !== step.stepIndex
|
|
2346
2346
|
) {
|
|
2347
2347
|
return;
|
|
2348
2348
|
}
|
|
2349
|
-
|
|
2349
|
+
setFocusedTrailId(null);
|
|
2350
2350
|
setFocusedStepIndex(null);
|
|
2351
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
2351
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
2352
2352
|
} else if (moduleNodeId) {
|
|
2353
2353
|
// Only unwind framing this card still owns.
|
|
2354
2354
|
if (issueModuleFocusRef.current !== moduleNodeId) return;
|
|
@@ -2364,14 +2364,14 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2364
2364
|
[issueComponent, issueStep, issueModuleNodeId, fitView],
|
|
2365
2365
|
);
|
|
2366
2366
|
|
|
2367
|
-
// Construct tokens inside a
|
|
2367
|
+
// Construct tokens inside a trail snippet. A step's line is an edge
|
|
2368
2368
|
// between its `from`/`to` components, so a token naming either of those
|
|
2369
2369
|
// constructs should navigate to that construct's declaration. Index the
|
|
2370
|
-
// matchable identifiers per step (`
|
|
2371
|
-
const
|
|
2370
|
+
// matchable identifiers per step (`trailId:index`).
|
|
2371
|
+
const trailSymbolIndex = useMemo(() => {
|
|
2372
2372
|
const byAlias = new Map(components.map((c) => [c.alias, c]));
|
|
2373
2373
|
const index = new Map<string, Map<string, string>>();
|
|
2374
|
-
for (const wt of
|
|
2374
|
+
for (const wt of trails ?? []) {
|
|
2375
2375
|
wt.steps.forEach((step, i) => {
|
|
2376
2376
|
const tokens = new Map<string, string>();
|
|
2377
2377
|
for (const alias of [step.from, step.to]) {
|
|
@@ -2383,19 +2383,19 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2383
2383
|
});
|
|
2384
2384
|
}
|
|
2385
2385
|
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 (
|
|
2386
|
+
}, [components, trails]);
|
|
2387
|
+
const trailSymbolIndexRef = useRef(trailSymbolIndex);
|
|
2388
|
+
trailSymbolIndexRef.current = trailSymbolIndex;
|
|
2389
|
+
|
|
2390
|
+
// Stable resolver: reads the live index + focused trail from refs so
|
|
2391
|
+
// the memoized trail drawer isn't rebuilt on every graph render.
|
|
2392
|
+
const resolveTrailSymbol = useCallback(
|
|
2393
|
+
(query: TrailSymbolQuery): string | null => {
|
|
2394
|
+
const trailId = drawerTrailIdRef.current;
|
|
2395
|
+
if (trailId == null) return null;
|
|
2396
2396
|
return (
|
|
2397
|
-
|
|
2398
|
-
.get(`${
|
|
2397
|
+
trailSymbolIndexRef.current
|
|
2398
|
+
.get(`${trailId}:${query.stepIndex}`)
|
|
2399
2399
|
?.get(query.tokenText) ?? null
|
|
2400
2400
|
);
|
|
2401
2401
|
},
|
|
@@ -2459,21 +2459,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2459
2459
|
return components.find((c) => c.alias === hoveredComponentAlias)?.file ?? null;
|
|
2460
2460
|
}, [components, hoveredComponentAlias]);
|
|
2461
2461
|
|
|
2462
|
-
// Drawer content renderer
|
|
2463
|
-
//
|
|
2464
|
-
const fileViewer =
|
|
2465
|
-
if (renderFileViewer) return renderFileViewer;
|
|
2466
|
-
if (renderFileView) {
|
|
2467
|
-
const byFile = new Map(
|
|
2468
|
-
components.filter((c) => c.file).map((c) => [c.file, c] as const),
|
|
2469
|
-
);
|
|
2470
|
-
return (file: string, _opts?: SubsystemOpenFileOptions) => {
|
|
2471
|
-
const comp = byFile.get(file);
|
|
2472
|
-
return comp ? renderFileView(comp) : null;
|
|
2473
|
-
};
|
|
2474
|
-
}
|
|
2475
|
-
return renderFileViewer;
|
|
2476
|
-
}, [renderFileViewer, renderFileView, components]);
|
|
2462
|
+
// Drawer content renderer. Path-keyed only — the component-keyed
|
|
2463
|
+
// `renderFileView` variant was removed; nothing in-repo passed it.
|
|
2464
|
+
const fileViewer = renderFileViewer;
|
|
2477
2465
|
|
|
2478
2466
|
const fileViewerRef = useRef(fileViewer);
|
|
2479
2467
|
fileViewerRef.current = fileViewer;
|
|
@@ -2483,11 +2471,11 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2483
2471
|
[],
|
|
2484
2472
|
);
|
|
2485
2473
|
|
|
2486
|
-
const
|
|
2487
|
-
|
|
2488
|
-
const
|
|
2489
|
-
(ctx:
|
|
2490
|
-
|
|
2474
|
+
const trailViewerRef = useRef(renderTrailViewer);
|
|
2475
|
+
trailViewerRef.current = renderTrailViewer;
|
|
2476
|
+
const renderTrailDrawerContent = useCallback(
|
|
2477
|
+
(ctx: TrailViewerContext) =>
|
|
2478
|
+
trailViewerRef.current?.(ctx) ?? null,
|
|
2491
2479
|
[],
|
|
2492
2480
|
);
|
|
2493
2481
|
|
|
@@ -2496,7 +2484,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2496
2484
|
{/* Sidebar: scrollable title/description on top, files or flows pinned below.
|
|
2497
2485
|
The description hides by default so files/flows get the room; the
|
|
2498
2486
|
title-row toggle reveals it, and the lower panel yields back to 50%. */}
|
|
2499
|
-
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 ||
|
|
2487
|
+
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasTrails) && (
|
|
2500
2488
|
<div
|
|
2501
2489
|
style={{
|
|
2502
2490
|
width: sidebarWidth,
|
|
@@ -2603,7 +2591,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2603
2591
|
)}
|
|
2604
2592
|
{sidebarAfterDescription}
|
|
2605
2593
|
</div>
|
|
2606
|
-
{(treeFilePaths.length > 0 ||
|
|
2594
|
+
{(treeFilePaths.length > 0 || hasTrails || issuesActive) && (
|
|
2607
2595
|
<div
|
|
2608
2596
|
style={{
|
|
2609
2597
|
...(showDesc ? { height: '50%' as const } : { flex: 1, minHeight: 0 }),
|
|
@@ -2682,7 +2670,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2682
2670
|
)
|
|
2683
2671
|
) : (
|
|
2684
2672
|
<>
|
|
2685
|
-
{
|
|
2673
|
+
{hasTrails && (
|
|
2686
2674
|
<div
|
|
2687
2675
|
role="tablist"
|
|
2688
2676
|
aria-label="Sidebar view"
|
|
@@ -2694,7 +2682,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2694
2682
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
2695
2683
|
}}
|
|
2696
2684
|
>
|
|
2697
|
-
{(['
|
|
2685
|
+
{(['trails', 'files'] as const).map((view) => (
|
|
2698
2686
|
<button
|
|
2699
2687
|
key={view}
|
|
2700
2688
|
type="button"
|
|
@@ -2718,30 +2706,30 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2718
2706
|
cursor: 'pointer',
|
|
2719
2707
|
}}
|
|
2720
2708
|
>
|
|
2721
|
-
{view === '
|
|
2709
|
+
{view === 'trails' ? 'Trails' : 'Files'}
|
|
2722
2710
|
</button>
|
|
2723
2711
|
))}
|
|
2724
2712
|
</div>
|
|
2725
2713
|
)}
|
|
2726
|
-
{sidebarView === '
|
|
2727
|
-
<
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2714
|
+
{sidebarView === 'trails' && trails && trails.length > 0 ? (
|
|
2715
|
+
<TrailsPanel
|
|
2716
|
+
trails={trails}
|
|
2717
|
+
expandedTrails={expandedTrails}
|
|
2718
|
+
focusedTrailId={focusedTrailId}
|
|
2731
2719
|
focusedStepIndex={focusedStepIndex}
|
|
2732
|
-
|
|
2733
|
-
onToggleCollapsed={
|
|
2734
|
-
onFocusFlow={
|
|
2735
|
-
onClearFocus={
|
|
2736
|
-
onFocusStep={
|
|
2720
|
+
hoveredTrailStep={hoveredTrailStep}
|
|
2721
|
+
onToggleCollapsed={toggleTrailCollapsed}
|
|
2722
|
+
onFocusFlow={focusTrailEdges}
|
|
2723
|
+
onClearFocus={clearTrailFocus}
|
|
2724
|
+
onFocusStep={focusTrailStep}
|
|
2737
2725
|
onHoverStep={(tl, i) =>
|
|
2738
|
-
|
|
2726
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex: i })
|
|
2739
2727
|
}
|
|
2740
2728
|
onHoverFlow={(tl) =>
|
|
2741
|
-
|
|
2729
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex: null })
|
|
2742
2730
|
}
|
|
2743
|
-
onLeaveStep={() =>
|
|
2744
|
-
onReorder={
|
|
2731
|
+
onLeaveStep={() => setHoveredTrailStep(null)}
|
|
2732
|
+
onReorder={onReorderTrails}
|
|
2745
2733
|
proposedAliases={proposedAliases}
|
|
2746
2734
|
/>
|
|
2747
2735
|
) : treeFilePaths.length > 0 ? (
|
|
@@ -2790,7 +2778,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2790
2778
|
)}
|
|
2791
2779
|
|
|
2792
2780
|
{/* Drag handle between sidebar and canvas — resize the left panel. */}
|
|
2793
|
-
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 ||
|
|
2781
|
+
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasTrails) && (
|
|
2794
2782
|
<div
|
|
2795
2783
|
onMouseDown={onSidebarResizeStart}
|
|
2796
2784
|
aria-label="Resize sidebar"
|
|
@@ -2971,9 +2959,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2971
2959
|
>
|
|
2972
2960
|
<GraphChrome />
|
|
2973
2961
|
</ReactFlow>
|
|
2974
|
-
{/* Graph /
|
|
2962
|
+
{/* Graph / trail / step titles — non-interactive chips at the top
|
|
2975
2963
|
of the canvas. Graph-only embeds use these without opening the sidebar. */}
|
|
2976
|
-
{(graphTitle ||
|
|
2964
|
+
{(graphTitle || overlayTrailTitle || overlayTrailStep) && (
|
|
2977
2965
|
<div
|
|
2978
2966
|
style={{
|
|
2979
2967
|
position: 'absolute',
|
|
@@ -3010,11 +2998,11 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3010
2998
|
{graphTitle}
|
|
3011
2999
|
</div>
|
|
3012
3000
|
)}
|
|
3013
|
-
{
|
|
3001
|
+
{overlayTrailTitle && (
|
|
3014
3002
|
<div
|
|
3015
3003
|
style={{
|
|
3016
3004
|
maxWidth: '100%',
|
|
3017
|
-
minWidth:
|
|
3005
|
+
minWidth: overlayTrailStep ? 160 : undefined,
|
|
3018
3006
|
display: 'flex',
|
|
3019
3007
|
flexDirection: 'column',
|
|
3020
3008
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
@@ -3025,9 +3013,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3025
3013
|
opacity: 0.95,
|
|
3026
3014
|
}}
|
|
3027
3015
|
aria-label={
|
|
3028
|
-
|
|
3029
|
-
? `${
|
|
3030
|
-
:
|
|
3016
|
+
overlayTrailStep
|
|
3017
|
+
? `${overlayTrailTitle}, step ${overlayTrailStep.index} of ${overlayTrailStep.total}`
|
|
3018
|
+
: overlayTrailTitle
|
|
3031
3019
|
}
|
|
3032
3020
|
>
|
|
3033
3021
|
<div
|
|
@@ -3043,9 +3031,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3043
3031
|
textAlign: 'center',
|
|
3044
3032
|
}}
|
|
3045
3033
|
>
|
|
3046
|
-
{
|
|
3034
|
+
{overlayTrailTitle}
|
|
3047
3035
|
</div>
|
|
3048
|
-
{
|
|
3036
|
+
{overlayTrailStep && overlayTrailStep.total > 0 && (
|
|
3049
3037
|
<div
|
|
3050
3038
|
style={{
|
|
3051
3039
|
display: 'flex',
|
|
@@ -3054,10 +3042,10 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3054
3042
|
}}
|
|
3055
3043
|
aria-hidden="true"
|
|
3056
3044
|
>
|
|
3057
|
-
{Array.from({ length:
|
|
3045
|
+
{Array.from({ length: overlayTrailStep.total }, (_, i) => {
|
|
3058
3046
|
const n = i + 1;
|
|
3059
|
-
const active = n ===
|
|
3060
|
-
const done = n <
|
|
3047
|
+
const active = n === overlayTrailStep.index;
|
|
3048
|
+
const done = n < overlayTrailStep.index;
|
|
3061
3049
|
return (
|
|
3062
3050
|
<span
|
|
3063
3051
|
key={n}
|
|
@@ -3080,7 +3068,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3080
3068
|
)}
|
|
3081
3069
|
</div>
|
|
3082
3070
|
)}
|
|
3083
|
-
{
|
|
3071
|
+
{overlayTrailStep?.annotation && (
|
|
3084
3072
|
<div
|
|
3085
3073
|
style={{
|
|
3086
3074
|
maxWidth: '100%',
|
|
@@ -3096,7 +3084,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3096
3084
|
lineHeight: 1.35,
|
|
3097
3085
|
}}
|
|
3098
3086
|
>
|
|
3099
|
-
{
|
|
3087
|
+
{overlayTrailStep.annotation}
|
|
3100
3088
|
</div>
|
|
3101
3089
|
)}
|
|
3102
3090
|
</div>
|
|
@@ -3158,16 +3146,16 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3158
3146
|
suppressEscape={fileOverlayOpen}
|
|
3159
3147
|
hidden={hideDrawer}
|
|
3160
3148
|
>
|
|
3161
|
-
{drawerTarget?.kind === '
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
<
|
|
3165
|
-
render={
|
|
3166
|
-
|
|
3149
|
+
{drawerTarget?.kind === 'trail' &&
|
|
3150
|
+
focusedTrail &&
|
|
3151
|
+
renderTrailViewer ? (
|
|
3152
|
+
<TrailDrawerContent
|
|
3153
|
+
render={renderTrailDrawerContent}
|
|
3154
|
+
trail={focusedTrail}
|
|
3167
3155
|
stepIndex={drawerTarget.stepIndex}
|
|
3168
|
-
onOpenFile={
|
|
3156
|
+
onOpenFile={onOpenFileFromTrail}
|
|
3169
3157
|
proposedAliases={proposedAliases}
|
|
3170
|
-
resolveSymbol={
|
|
3158
|
+
resolveSymbol={resolveTrailSymbol}
|
|
3171
3159
|
onSymbolClick={openConstructDeclaration}
|
|
3172
3160
|
/>
|
|
3173
3161
|
) : drawerTarget?.kind === 'file' ? (
|
|
@@ -3336,7 +3324,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3336
3324
|
|
|
3337
3325
|
const constructsOnly = isConstructsOnlyModel({
|
|
3338
3326
|
components: props.components,
|
|
3339
|
-
|
|
3327
|
+
trails: props.trails,
|
|
3340
3328
|
graphifyRelations: props.graphifyRelations,
|
|
3341
3329
|
});
|
|
3342
3330
|
|
|
@@ -3366,7 +3354,6 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3366
3354
|
sidebarExtra={props.sidebarExtra}
|
|
3367
3355
|
sidebarAfterDescription={props.sidebarAfterDescription}
|
|
3368
3356
|
renderFileViewer={props.renderFileViewer}
|
|
3369
|
-
renderFileView={props.renderFileView}
|
|
3370
3357
|
onFileSelect={props.onFileSelect}
|
|
3371
3358
|
componentVerification={props.componentVerification}
|
|
3372
3359
|
onInspectSymbol={props.onInspectSymbol}
|