@principal-ai/subsystems-react 0.44.3 → 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 +307 -309
- 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 +30 -0
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +29 -0
- package/dist/subsystem/nodes.js.map +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 +400 -403
- 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.test.ts +42 -1
- package/src/subsystem/nodes.tsx +38 -0
- 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,8 +63,8 @@ import {
|
|
|
63
63
|
MaintainLivePanel,
|
|
64
64
|
type MaintainLivePanelProps,
|
|
65
65
|
} from '../components/maintain-events/MaintainLivePanel';
|
|
66
|
-
import type {
|
|
67
|
-
import { SubsystemComponentNode, SubsystemGroupNode, SubsystemEdge, SUBSYSTEM_CALLBACKS, hexWithAlpha, EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility } from './nodes';
|
|
66
|
+
import type { TrailSymbolQuery } from '../pierre/PierreTrailCodeView';
|
|
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 {
|
|
70
70
|
SubsystemIssueList,
|
|
@@ -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
|
|
@@ -1389,36 +1395,29 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1389
1395
|
// `isSelected` rides in data because the node's stopPropagation() keeps
|
|
1390
1396
|
// React Flow's own selection state from updating.
|
|
1391
1397
|
const dispNodes = useMemo(() => {
|
|
1392
|
-
// While a step/flow is hovered,
|
|
1393
|
-
//
|
|
1394
|
-
//
|
|
1395
|
-
//
|
|
1396
|
-
// nothing to hide toward — everything shows, and focus/hover only dims.
|
|
1397
|
-
const noOpenedFlow = openedEdgeIds == null;
|
|
1398
|
+
// While a step/flow is hovered, hovered participants stay bright; every
|
|
1399
|
+
// other node is dimmed (and, when a flow is expanded, hidden). Exception:
|
|
1400
|
+
// with no expanded flow at all (a sidebar-less embed) there is nothing to
|
|
1401
|
+
// hide toward — everything shows, and focus/hover only dims.
|
|
1398
1402
|
return xyflowNodesBase.map((n) => {
|
|
1399
1403
|
// Boundary frames follow their members: hidden when no member is
|
|
1400
1404
|
// visible, dimmed when members are dimmed. Never selectable.
|
|
1401
1405
|
if (n.type === 'subsystem-group') {
|
|
1402
1406
|
const region = (n.data as { region?: { key?: string; memberAliases?: string[] } } | undefined)?.region;
|
|
1403
1407
|
const memberAliases = region?.memberAliases ?? [];
|
|
1404
|
-
//
|
|
1405
|
-
|
|
1408
|
+
// `brightNodeIds` is the focused step's endpoints. It has to be a
|
|
1409
|
+
// spotlight member as well as a bright set: without an expanded flow
|
|
1410
|
+
// there is no opened set, so dimming has to be driven from the
|
|
1411
|
+
// participants or nothing outside the focused step would dim.
|
|
1412
|
+
const dimSource = previewNodeIds ?? brightNodeIds ?? issueFocusNodeIds;
|
|
1413
|
+
const { hidden, dimmed } = flowNodeVisibility({
|
|
1406
1414
|
inOpened: memberAliases.some((alias) => openedNodeIds?.has(alias) === true),
|
|
1407
1415
|
inSelected: memberAliases.some((alias) => brightNodeIds?.has(alias) === true),
|
|
1416
|
+
inSpotlight: memberAliases.some((alias) => dimSource?.has(alias) === true),
|
|
1408
1417
|
anyOpened: openedNodeIds != null,
|
|
1409
|
-
anySelected: brightNodeIds != null
|
|
1418
|
+
anySelected: brightNodeIds != null,
|
|
1419
|
+
anySpotlight: dimSource != null,
|
|
1410
1420
|
});
|
|
1411
|
-
// `brightNodeIds` is the focused step's endpoints. It has to be a dim
|
|
1412
|
-
// source, not just a bright set: without an expanded flow,
|
|
1413
|
-
// `flowElementVisibility` reports non-participants as *hidden*, and the
|
|
1414
|
-
// noOpenedFlow escape below discards that — leaving them neither hidden
|
|
1415
|
-
// nor dimmed. Naming the participants as the dim source dims everything
|
|
1416
|
-
// outside the focused step however the flow got focused.
|
|
1417
|
-
const dimSource = previewNodeIds ?? brightNodeIds ?? issueFocusNodeIds;
|
|
1418
|
-
const dimmed = dimSource
|
|
1419
|
-
? vis.hidden || !memberAliases.some((alias) => dimSource.has(alias))
|
|
1420
|
-
: vis.dimmed;
|
|
1421
|
-
const hidden = noOpenedFlow ? false : vis.hidden;
|
|
1422
1421
|
// Host override wins over the library's derived frame color.
|
|
1423
1422
|
const color = region?.key != null ? boundaryColors?.[region.key] : undefined;
|
|
1424
1423
|
// Boundary findings badge the FRAME, not a member — a region's shape is
|
|
@@ -1438,22 +1437,20 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1438
1437
|
}
|
|
1439
1438
|
const comp = (n.data as { component?: SubsystemComponent } | undefined)?.component;
|
|
1440
1439
|
const fileMatch = fileMatchForNode(comp?.file, openFile, focusNodeIds?.has(n.id) === true);
|
|
1441
|
-
//
|
|
1442
|
-
//
|
|
1443
|
-
// embed), only dim them. Bases selection on the focused step, not the
|
|
1444
|
-
// hover preview, so hovering doesn't strip the frame's `isSelected`.
|
|
1440
|
+
// Bases selection on the focused step, not the hover preview, so
|
|
1441
|
+
// hovering doesn't strip the frame's `isSelected`.
|
|
1445
1442
|
const isSelected = selected?.alias !== undefined && comp?.alias === selected.alias;
|
|
1446
|
-
|
|
1443
|
+
// `brightNodeIds` (the focused step's endpoints) is a spotlight member as
|
|
1444
|
+
// well as a bright set — see the group branch above.
|
|
1445
|
+
const dimSource = previewNodeIds ?? brightNodeIds ?? issueFocusNodeIds;
|
|
1446
|
+
const { hidden, dimmed } = flowNodeVisibility({
|
|
1447
1447
|
inOpened: openedNodeIds?.has(n.id) === true,
|
|
1448
1448
|
inSelected: brightNodeIds?.has(n.id) === true,
|
|
1449
|
+
inSpotlight: dimSource?.has(n.id) === true,
|
|
1449
1450
|
anyOpened: openedNodeIds != null,
|
|
1450
|
-
anySelected: brightNodeIds != null
|
|
1451
|
+
anySelected: brightNodeIds != null,
|
|
1452
|
+
anySpotlight: dimSource != null,
|
|
1451
1453
|
});
|
|
1452
|
-
// `brightNodeIds` (the focused step's endpoints) is a dim source as well
|
|
1453
|
-
// as a bright set — see the group branch above.
|
|
1454
|
-
const dimSource = previewNodeIds ?? brightNodeIds ?? issueFocusNodeIds;
|
|
1455
|
-
const dimmed = dimSource ? (vis.hidden || !dimSource.has(n.id)) : vis.dimmed;
|
|
1456
|
-
const hidden = noOpenedFlow ? false : vis.hidden;
|
|
1457
1454
|
// Component findings key by alias, boundary findings by region node id.
|
|
1458
1455
|
// The two id spaces are disjoint (`module:` / `process:` are prefixed), so
|
|
1459
1456
|
// one lookup covers both node kinds.
|
|
@@ -1498,7 +1495,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1498
1495
|
};
|
|
1499
1496
|
// Edges outside the selected view are hidden entirely (labels included).
|
|
1500
1497
|
// graphify-native edges belong to the graphify view (they are static
|
|
1501
|
-
// topology, not runtime
|
|
1498
|
+
// topology, not runtime steps) regardless of their verb.
|
|
1502
1499
|
const edgeInView = (e: Edge): boolean => {
|
|
1503
1500
|
const d = e.data as
|
|
1504
1501
|
| { mechanism?: string; provenance?: SubsystemEdgeProvenance }
|
|
@@ -1507,7 +1504,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1507
1504
|
if (resolvedEdgeView === 'graphify') {
|
|
1508
1505
|
return d?.provenance === 'graphify';
|
|
1509
1506
|
}
|
|
1510
|
-
return
|
|
1507
|
+
return isTrailMechanism(mechanism);
|
|
1511
1508
|
};
|
|
1512
1509
|
if (openedEdgeIds || focusEdgeIds || previewEdgeIds || issueFocusEdgeIds) {
|
|
1513
1510
|
return baseEdges.map((e) => {
|
|
@@ -1574,7 +1571,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1574
1571
|
(_e, node: Node) => {
|
|
1575
1572
|
const comp = (node.data as { component?: SubsystemComponent } | undefined)?.component;
|
|
1576
1573
|
setSelectedEdgeId(null);
|
|
1577
|
-
|
|
1574
|
+
setFocusedTrailId(null);
|
|
1578
1575
|
setFocusedStepIndex(null);
|
|
1579
1576
|
if (node.type === 'subsystem-component' && comp) {
|
|
1580
1577
|
// Clicking the already-selected node unselects it (toggle off).
|
|
@@ -1602,7 +1599,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1602
1599
|
const onPaneClick = useCallback(() => {
|
|
1603
1600
|
setSelected(null);
|
|
1604
1601
|
setSelectedEdgeId(null);
|
|
1605
|
-
|
|
1602
|
+
setFocusedTrailId(null);
|
|
1606
1603
|
setFocusedStepIndex(null);
|
|
1607
1604
|
issueModuleFocusRef.current = null;
|
|
1608
1605
|
}, []);
|
|
@@ -1610,8 +1607,8 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1610
1607
|
// Sidebar file trees — one per repo on multi-repo graphs, each under its
|
|
1611
1608
|
// own owner-avatar header. Clicking a header collapses that repo's tree.
|
|
1612
1609
|
const repoGroups = useMemo(() => buildRepoGroups(components), [components]);
|
|
1613
|
-
const
|
|
1614
|
-
// 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
|
|
1615
1612
|
// (and steps) that touch one.
|
|
1616
1613
|
const proposedAliases = useMemo(
|
|
1617
1614
|
() => new Set(components.filter((c) => c.proposed).map((c) => c.alias)),
|
|
@@ -1688,7 +1685,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1688
1685
|
const onNodeDoubleClick: NodeMouseHandler = useCallback(
|
|
1689
1686
|
(_e, node: Node) => {
|
|
1690
1687
|
// Boundary frame → frame its members on the canvas; double-click again to
|
|
1691
|
-
// zoom back out. User-initiated, so it ignores the
|
|
1688
|
+
// zoom back out. User-initiated, so it ignores the trail zoom gate.
|
|
1692
1689
|
if (node.type === 'subsystem-group') {
|
|
1693
1690
|
const key =
|
|
1694
1691
|
(node.data as { region?: { key?: string } } | undefined)?.region?.key;
|
|
@@ -1724,7 +1721,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1724
1721
|
[onOpenDeclarationFile, fitView, focusedBoundaryKey],
|
|
1725
1722
|
);
|
|
1726
1723
|
|
|
1727
|
-
const
|
|
1724
|
+
const onOpenFileFromTrail = useCallback(
|
|
1728
1725
|
(file: string, opts?: SubsystemOpenFileOptions) => {
|
|
1729
1726
|
setFileOverlay({ file, startLine: opts?.startLine });
|
|
1730
1727
|
onFileSelect?.(file);
|
|
@@ -1746,15 +1743,15 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1746
1743
|
const fileOverlayOpen = fileOverlay != null;
|
|
1747
1744
|
const collapseCanvas = drawerFillHeight || fileOverlayOpen;
|
|
1748
1745
|
|
|
1749
|
-
// Drop the full-file overlay when the underlying
|
|
1746
|
+
// Drop the full-file overlay when the underlying trail drawer changes
|
|
1750
1747
|
// or closes — open-file keeps the same drawerTarget so the snippets stay mounted.
|
|
1751
|
-
const
|
|
1752
|
-
drawerTarget?.kind === '
|
|
1753
|
-
? `${drawerTarget.
|
|
1748
|
+
const trailDrawerKey =
|
|
1749
|
+
drawerTarget?.kind === 'trail'
|
|
1750
|
+
? `${drawerTarget.trailId}:${drawerTarget.stepIndex}`
|
|
1754
1751
|
: null;
|
|
1755
1752
|
useEffect(() => {
|
|
1756
1753
|
setFileOverlay(null);
|
|
1757
|
-
}, [
|
|
1754
|
+
}, [trailDrawerKey]);
|
|
1758
1755
|
|
|
1759
1756
|
useEffect(() => {
|
|
1760
1757
|
if (!fileOverlayOpen) return;
|
|
@@ -1765,16 +1762,16 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1765
1762
|
return () => window.removeEventListener('keydown', onKey);
|
|
1766
1763
|
}, [fileOverlayOpen, closeFileOverlay]);
|
|
1767
1764
|
|
|
1768
|
-
// Camera helper shared by the
|
|
1765
|
+
// Camera helper shared by the trail interactions. Fits the union of
|
|
1769
1766
|
// the focused edges' endpoint rects AND their routed waypoints — the edge
|
|
1770
1767
|
// carries its full polyline (`elkPathPoints`, absolute flow coords), so the
|
|
1771
|
-
// frame is the line the
|
|
1768
|
+
// frame is the line the step actually traces. That keeps a step whose route
|
|
1772
1769
|
// bulges out around intervening nodes from being clipped at the viewport
|
|
1773
1770
|
// edge, and it is the edge's own geometry doing the guiding, not fudge
|
|
1774
1771
|
// padding.
|
|
1775
1772
|
const fitFocusBounds = useCallback(
|
|
1776
1773
|
(ids: ReadonlySet<string>) => {
|
|
1777
|
-
if (!
|
|
1774
|
+
if (!zoomOnTrailFocus) return;
|
|
1778
1775
|
const nodeIds = new Set<string>();
|
|
1779
1776
|
const points: { x: number; y: number }[] = [];
|
|
1780
1777
|
for (const e of baseEdges) {
|
|
@@ -1819,15 +1816,15 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1819
1816
|
};
|
|
1820
1817
|
fitBounds(bounds, {
|
|
1821
1818
|
padding: 0.15,
|
|
1822
|
-
duration:
|
|
1819
|
+
duration: trailFocusDurationMs,
|
|
1823
1820
|
});
|
|
1824
1821
|
},
|
|
1825
1822
|
[
|
|
1826
1823
|
baseEdges,
|
|
1827
1824
|
built.absoluteRects,
|
|
1828
1825
|
fitBounds,
|
|
1829
|
-
|
|
1830
|
-
|
|
1826
|
+
zoomOnTrailFocus,
|
|
1827
|
+
trailFocusDurationMs,
|
|
1831
1828
|
],
|
|
1832
1829
|
);
|
|
1833
1830
|
|
|
@@ -1885,16 +1882,16 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1885
1882
|
width: Math.max(1, maxX - minX),
|
|
1886
1883
|
height: Math.max(1, maxY - minY),
|
|
1887
1884
|
},
|
|
1888
|
-
{ padding, duration:
|
|
1885
|
+
{ padding, duration: trailFocusDurationMs },
|
|
1889
1886
|
);
|
|
1890
1887
|
},
|
|
1891
|
-
[rectForNode, fitBounds,
|
|
1888
|
+
[rectForNode, fitBounds, trailFocusDurationMs],
|
|
1892
1889
|
);
|
|
1893
1890
|
|
|
1894
|
-
// Zoom back out to the full diagram after the last expanded
|
|
1891
|
+
// Zoom back out to the full diagram after the last expanded trail
|
|
1895
1892
|
// closes (visibility restores non-flow nodes that were hidden).
|
|
1896
1893
|
const fitOverview = useCallback(() => {
|
|
1897
|
-
if (!
|
|
1894
|
+
if (!zoomOnTrailFocus) return;
|
|
1898
1895
|
fitView({
|
|
1899
1896
|
padding: 0.1,
|
|
1900
1897
|
includeHiddenNodes: false,
|
|
@@ -1902,115 +1899,115 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
1902
1899
|
maxZoom: 2,
|
|
1903
1900
|
duration: 300,
|
|
1904
1901
|
});
|
|
1905
|
-
}, [fitView,
|
|
1902
|
+
}, [fitView, zoomOnTrailFocus]);
|
|
1906
1903
|
|
|
1907
1904
|
// Focus an entire flow: hide everything but the flow's nodes and edges, and
|
|
1908
1905
|
// frame the flow on the canvas. Selection state is cleared — the graph now
|
|
1909
1906
|
// reads as the narrative. No drawer: the code view only opens on a step
|
|
1910
|
-
// click. A stale
|
|
1907
|
+
// click. A stale trail drawer (from a previously focused flow's step)
|
|
1911
1908
|
// closes; an explicitly opened file drawer stays.
|
|
1912
1909
|
// In `dim` mode: clear step highlight and leave the full graph visible
|
|
1913
1910
|
// (no zoom / hide) — matching "step away from a hovered step".
|
|
1914
|
-
const
|
|
1915
|
-
(tl:
|
|
1911
|
+
const focusTrailEdges = useCallback(
|
|
1912
|
+
(tl: SubsystemTrail) => {
|
|
1916
1913
|
setSelected(null);
|
|
1917
1914
|
setSelectedEdgeId(null);
|
|
1918
|
-
|
|
1919
|
-
if (
|
|
1915
|
+
setHoveredTrailStep(null);
|
|
1916
|
+
if (trailStepMode === 'dim') {
|
|
1920
1917
|
setFocusedStepIndex(null);
|
|
1921
|
-
|
|
1922
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1918
|
+
setFocusedTrailId(null);
|
|
1919
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1923
1920
|
return;
|
|
1924
1921
|
}
|
|
1925
1922
|
setFocusedStepIndex(null);
|
|
1926
|
-
|
|
1927
|
-
fitFocusBounds(new Set(tl.steps.map((s) =>
|
|
1928
|
-
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));
|
|
1929
1926
|
},
|
|
1930
|
-
[fitFocusBounds,
|
|
1927
|
+
[fitFocusBounds, trailStepMode],
|
|
1931
1928
|
);
|
|
1932
1929
|
|
|
1933
|
-
const
|
|
1934
|
-
|
|
1930
|
+
const clearTrailFocus = useCallback(() => {
|
|
1931
|
+
setFocusedTrailId(null);
|
|
1935
1932
|
setFocusedStepIndex(null);
|
|
1936
|
-
|
|
1937
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1933
|
+
setHoveredTrailStep(null);
|
|
1934
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1938
1935
|
}, []);
|
|
1939
1936
|
|
|
1940
|
-
// Host deep-link: opening the graph from a
|
|
1937
|
+
// Host deep-link: opening the graph from a trail row selects that
|
|
1941
1938
|
// flow — expand its steps, focus its edges, and switch the sidebar to
|
|
1942
|
-
//
|
|
1939
|
+
// Trails. Guarded by a ref so a later in-tab selection isn't yanked
|
|
1943
1940
|
// back; a remount (or a new id) re-applies it.
|
|
1944
|
-
const
|
|
1941
|
+
const appliedInitialTrailRef = useRef<string | null>(null);
|
|
1945
1942
|
useEffect(() => {
|
|
1946
|
-
if (!
|
|
1947
|
-
if (
|
|
1948
|
-
if (!layoutReady || !
|
|
1949
|
-
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);
|
|
1950
1947
|
if (!tl) return;
|
|
1951
|
-
|
|
1952
|
-
setSidebarView('
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
}, [
|
|
1948
|
+
appliedInitialTrailRef.current = initialTrailId;
|
|
1949
|
+
setSidebarView('trails');
|
|
1950
|
+
setExpandedTrails((prev) => new Set(prev).add(tl.id));
|
|
1951
|
+
focusTrailEdges(tl);
|
|
1952
|
+
}, [initialTrailId, layoutReady, trails, focusTrailEdges]);
|
|
1956
1953
|
|
|
1957
1954
|
// Switching sidebar panels also switches the edge vocabulary. Leaving the
|
|
1958
|
-
//
|
|
1955
|
+
// Trails panel drops its canvas state (focus, expanded flows, selected
|
|
1959
1956
|
// edge) so the Files view's topology edges aren't gated by a flow the user
|
|
1960
1957
|
// can no longer see or un-dim.
|
|
1961
1958
|
const changeSidebarView = useCallback(
|
|
1962
|
-
(view: 'files' | '
|
|
1959
|
+
(view: 'files' | 'trails') => {
|
|
1963
1960
|
setSidebarView(view);
|
|
1964
|
-
if (view === '
|
|
1965
|
-
|
|
1966
|
-
|
|
1961
|
+
if (view === 'trails') return;
|
|
1962
|
+
clearTrailFocus();
|
|
1963
|
+
setExpandedTrails(new Set());
|
|
1967
1964
|
setSelectedEdgeId(null);
|
|
1968
1965
|
},
|
|
1969
|
-
[
|
|
1966
|
+
[clearTrailFocus],
|
|
1970
1967
|
);
|
|
1971
1968
|
|
|
1972
|
-
// 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
|
|
1973
1970
|
// drawer to that step's snippet. Clicking the already-focused step clears
|
|
1974
1971
|
// step focus and returns to whole-flow (high-level) framing.
|
|
1975
1972
|
// In `dim` mode: only dim non-participants (same as hovering a step) —
|
|
1976
1973
|
// no camera move, no drawer, no hide.
|
|
1977
|
-
const
|
|
1978
|
-
(tl:
|
|
1974
|
+
const focusTrailStep = useCallback(
|
|
1975
|
+
(tl: SubsystemTrail, stepIndex: number) => {
|
|
1979
1976
|
const step = tl.steps[stepIndex];
|
|
1980
1977
|
if (!step) return;
|
|
1981
1978
|
setSelected(null);
|
|
1982
1979
|
setSelectedEdgeId(null);
|
|
1983
|
-
if (
|
|
1980
|
+
if (trailStepMode === 'dim') {
|
|
1984
1981
|
setFocusedStepIndex(null);
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1982
|
+
setFocusedTrailId(null);
|
|
1983
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex });
|
|
1984
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1988
1985
|
return;
|
|
1989
1986
|
}
|
|
1990
1987
|
// Toggle: clicking the selected step unselects it (back to whole flow).
|
|
1991
|
-
if (
|
|
1988
|
+
if (focusedTrailId === tl.id && focusedStepIndex === stepIndex) {
|
|
1992
1989
|
setFocusedStepIndex(null);
|
|
1993
1990
|
// Pointer is still over the row — keep hover preview so dimming doesn't
|
|
1994
1991
|
// flash off until mouseleave.
|
|
1995
|
-
|
|
1996
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
1992
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex });
|
|
1993
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
1997
1994
|
if (pendingFocusFitRef.current != null) {
|
|
1998
1995
|
window.clearTimeout(pendingFocusFitRef.current);
|
|
1999
1996
|
pendingFocusFitRef.current = null;
|
|
2000
1997
|
}
|
|
2001
|
-
fitFocusBounds(new Set(tl.steps.map((s) =>
|
|
1998
|
+
fitFocusBounds(new Set(tl.steps.map((s) => trailStepGraphEdgeId(s))));
|
|
2002
1999
|
return;
|
|
2003
2000
|
}
|
|
2004
2001
|
setFocusedStepIndex(stepIndex);
|
|
2005
|
-
|
|
2006
|
-
|
|
2002
|
+
setFocusedTrailId(tl.id);
|
|
2003
|
+
setHoveredTrailStep(null);
|
|
2007
2004
|
// Open the drawer before fitting. If it was closed, wait for its height
|
|
2008
2005
|
// transition so fitView uses the reduced canvas — not full height.
|
|
2009
2006
|
const drawerWasOpen = drawerOpenRef.current;
|
|
2010
|
-
if (
|
|
2007
|
+
if (renderTrailViewer) {
|
|
2011
2008
|
setDrawerTarget({
|
|
2012
|
-
kind: '
|
|
2013
|
-
|
|
2009
|
+
kind: 'trail',
|
|
2010
|
+
trailId: tl.id,
|
|
2014
2011
|
stepIndex,
|
|
2015
2012
|
});
|
|
2016
2013
|
} else {
|
|
@@ -2020,7 +2017,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2020
2017
|
startLine: step.line,
|
|
2021
2018
|
});
|
|
2022
2019
|
}
|
|
2023
|
-
const edgeIds = new Set([
|
|
2020
|
+
const edgeIds = new Set([trailStepGraphEdgeId(step)]);
|
|
2024
2021
|
if (pendingFocusFitRef.current != null) {
|
|
2025
2022
|
window.clearTimeout(pendingFocusFitRef.current);
|
|
2026
2023
|
pendingFocusFitRef.current = null;
|
|
@@ -2037,9 +2034,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2037
2034
|
[
|
|
2038
2035
|
fitFocusBounds,
|
|
2039
2036
|
focusedStepIndex,
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2037
|
+
focusedTrailId,
|
|
2038
|
+
renderTrailViewer,
|
|
2039
|
+
trailStepMode,
|
|
2043
2040
|
],
|
|
2044
2041
|
);
|
|
2045
2042
|
|
|
@@ -2061,10 +2058,10 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2061
2058
|
const prev = prevPreviewEdgeIdsRef.current;
|
|
2062
2059
|
prevPreviewEdgeIdsRef.current = previewEdgeIds;
|
|
2063
2060
|
|
|
2064
|
-
if (!
|
|
2061
|
+
if (!zoomOnTrailFocus || trailStepMode === 'dim') return;
|
|
2065
2062
|
// Camera follows hover only when a specific step is already focused (or
|
|
2066
2063
|
// autoplay is driving an auto-focus).
|
|
2067
|
-
if (!autoPlayFocus && (
|
|
2064
|
+
if (!autoPlayFocus && (focusedTrailId == null || focusedStepIndex == null)) {
|
|
2068
2065
|
return;
|
|
2069
2066
|
}
|
|
2070
2067
|
|
|
@@ -2078,26 +2075,26 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2078
2075
|
}, [
|
|
2079
2076
|
previewEdgeIds,
|
|
2080
2077
|
focusEdgeIds,
|
|
2081
|
-
|
|
2078
|
+
focusedTrailId,
|
|
2082
2079
|
focusedStepIndex,
|
|
2083
2080
|
autoPlayFocus,
|
|
2084
2081
|
fitFocusBounds,
|
|
2085
|
-
|
|
2086
|
-
|
|
2082
|
+
zoomOnTrailFocus,
|
|
2083
|
+
trailStepMode,
|
|
2087
2084
|
]);
|
|
2088
2085
|
|
|
2089
|
-
// Graph-only embeds: cycle
|
|
2090
|
-
// 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
|
|
2091
2088
|
// viewer is supplied, its snippet drawer opens; in `dim` mode it only
|
|
2092
2089
|
// dim-highlights the step (hover-style) with no camera move.
|
|
2093
2090
|
useEffect(() => {
|
|
2094
|
-
if (!
|
|
2091
|
+
if (!autoPlayTrails || trails == null || trails.length === 0) return;
|
|
2095
2092
|
if (!layoutReady) return;
|
|
2096
|
-
const playable =
|
|
2093
|
+
const playable = trails.filter((tl) => tl.steps.length > 0);
|
|
2097
2094
|
if (playable.length === 0) return;
|
|
2098
2095
|
// Only run the cycles that can actually be shown in the current edge view.
|
|
2099
2096
|
const inView = playable.filter((tl) =>
|
|
2100
|
-
tl.steps.some((s) =>
|
|
2097
|
+
tl.steps.some((s) => isTrailMechanism(s.mechanism)),
|
|
2101
2098
|
);
|
|
2102
2099
|
if (inView.length === 0) return;
|
|
2103
2100
|
const cyc = inView;
|
|
@@ -2106,8 +2103,8 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2106
2103
|
let tlIdx = 0;
|
|
2107
2104
|
let stepIdx = 0;
|
|
2108
2105
|
let timer: number | null = null;
|
|
2109
|
-
const interval = Math.max(400,
|
|
2110
|
-
const focusMode =
|
|
2106
|
+
const interval = Math.max(400, trailAutoPlayIntervalMs);
|
|
2107
|
+
const focusMode = trailStepMode !== 'dim';
|
|
2111
2108
|
let cleanup = () => {};
|
|
2112
2109
|
|
|
2113
2110
|
const tick = () => {
|
|
@@ -2116,15 +2113,15 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2116
2113
|
setSelected(null);
|
|
2117
2114
|
setSelectedEdgeId(null);
|
|
2118
2115
|
if (focusMode) {
|
|
2119
|
-
|
|
2120
|
-
|
|
2116
|
+
setHoveredTrailStep(null);
|
|
2117
|
+
setFocusedTrailId(null);
|
|
2121
2118
|
setFocusedStepIndex(null);
|
|
2122
|
-
setAutoPlayFocus({
|
|
2119
|
+
setAutoPlayFocus({ trailId: tl.id, stepIndex: stepIdx });
|
|
2123
2120
|
if (!hideDrawer) {
|
|
2124
2121
|
const step = tl.steps[stepIdx];
|
|
2125
2122
|
if (step) {
|
|
2126
|
-
if (
|
|
2127
|
-
setDrawerTarget({ kind: '
|
|
2123
|
+
if (renderTrailViewer) {
|
|
2124
|
+
setDrawerTarget({ kind: 'trail', trailId: tl.id, stepIndex: stepIdx });
|
|
2128
2125
|
} else {
|
|
2129
2126
|
setDrawerTarget({ kind: 'file', file: step.file, startLine: step.line });
|
|
2130
2127
|
}
|
|
@@ -2132,9 +2129,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2132
2129
|
}
|
|
2133
2130
|
} else {
|
|
2134
2131
|
setAutoPlayFocus(null);
|
|
2135
|
-
|
|
2132
|
+
setFocusedTrailId(null);
|
|
2136
2133
|
setFocusedStepIndex(null);
|
|
2137
|
-
|
|
2134
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex: stepIdx });
|
|
2138
2135
|
}
|
|
2139
2136
|
stepIdx += 1;
|
|
2140
2137
|
if (stepIdx >= tl.steps.length) {
|
|
@@ -2148,29 +2145,29 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2148
2145
|
cleanup = () => {
|
|
2149
2146
|
cancelled = true;
|
|
2150
2147
|
if (timer != null) window.clearTimeout(timer);
|
|
2151
|
-
|
|
2152
|
-
|
|
2148
|
+
setHoveredTrailStep(null);
|
|
2149
|
+
setFocusedTrailId(null);
|
|
2153
2150
|
setFocusedStepIndex(null);
|
|
2154
2151
|
setAutoPlayFocus(null);
|
|
2155
2152
|
};
|
|
2156
2153
|
return cleanup;
|
|
2157
2154
|
}, [
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2155
|
+
autoPlayTrails,
|
|
2156
|
+
trails,
|
|
2157
|
+
trailAutoPlayIntervalMs,
|
|
2161
2158
|
layoutReady,
|
|
2162
|
-
|
|
2163
|
-
|
|
2159
|
+
trailStepMode,
|
|
2160
|
+
renderTrailViewer,
|
|
2164
2161
|
hideDrawer,
|
|
2165
2162
|
]);
|
|
2166
2163
|
|
|
2167
|
-
// Arrow keys step through the focused
|
|
2164
|
+
// Arrow keys step through the focused trail once a step is active
|
|
2168
2165
|
// (sidebar click or drawer open). Ignores typing targets and chords.
|
|
2169
2166
|
useEffect(() => {
|
|
2170
|
-
if (
|
|
2167
|
+
if (focusedTrailId == null || focusedStepIndex == null || !trails) {
|
|
2171
2168
|
return;
|
|
2172
2169
|
}
|
|
2173
|
-
const tl =
|
|
2170
|
+
const tl = trails.find((t) => t.id === focusedTrailId);
|
|
2174
2171
|
if (!tl || tl.steps.length === 0) return;
|
|
2175
2172
|
|
|
2176
2173
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
@@ -2196,31 +2193,31 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2196
2193
|
}
|
|
2197
2194
|
if (next === focusedStepIndex) return;
|
|
2198
2195
|
e.preventDefault();
|
|
2199
|
-
|
|
2196
|
+
focusTrailStep(tl, next);
|
|
2200
2197
|
};
|
|
2201
2198
|
|
|
2202
2199
|
window.addEventListener('keydown', onKeyDown);
|
|
2203
2200
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
2204
2201
|
}, [
|
|
2205
|
-
|
|
2202
|
+
focusedTrailId,
|
|
2206
2203
|
focusedStepIndex,
|
|
2207
|
-
|
|
2208
|
-
|
|
2204
|
+
trails,
|
|
2205
|
+
focusTrailStep,
|
|
2209
2206
|
]);
|
|
2210
2207
|
|
|
2211
|
-
const
|
|
2208
|
+
const toggleTrailCollapsed = useCallback(
|
|
2212
2209
|
(tlId: string) => {
|
|
2213
2210
|
const collapsingLast =
|
|
2214
|
-
|
|
2211
|
+
expandedTrails.has(tlId) && expandedTrails.size === 1;
|
|
2215
2212
|
|
|
2216
|
-
|
|
2213
|
+
setExpandedTrails((prev) => {
|
|
2217
2214
|
const next = new Set(prev);
|
|
2218
2215
|
if (next.has(tlId)) next.delete(tlId);
|
|
2219
2216
|
else next.add(tlId);
|
|
2220
2217
|
return next;
|
|
2221
2218
|
});
|
|
2222
2219
|
|
|
2223
|
-
if (!collapsingLast || !
|
|
2220
|
+
if (!collapsingLast || !zoomOnTrailFocus) return;
|
|
2224
2221
|
|
|
2225
2222
|
// Cancel any pending focus fit so it doesn't fight the overview zoom.
|
|
2226
2223
|
if (pendingFocusFitRef.current != null) {
|
|
@@ -2240,7 +2237,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2240
2237
|
drawerWasOpen ? FILE_DRAWER_HEIGHT_MS + 20 : 20,
|
|
2241
2238
|
);
|
|
2242
2239
|
},
|
|
2243
|
-
[
|
|
2240
|
+
[expandedTrails, fitOverview, zoomOnTrailFocus],
|
|
2244
2241
|
);
|
|
2245
2242
|
|
|
2246
2243
|
// Detail-panel links: related-name clicks select the matching component —
|
|
@@ -2258,7 +2255,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2258
2255
|
if (!comp || comp.alias === selectedRef.current?.alias) return;
|
|
2259
2256
|
setSelected(comp);
|
|
2260
2257
|
setSelectedEdgeId(null);
|
|
2261
|
-
|
|
2258
|
+
setFocusedTrailId(null);
|
|
2262
2259
|
setFocusedStepIndex(null);
|
|
2263
2260
|
onSelect?.(comp.alias);
|
|
2264
2261
|
},
|
|
@@ -2277,9 +2274,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2277
2274
|
if (comp) {
|
|
2278
2275
|
setSelected(comp);
|
|
2279
2276
|
setSelectedEdgeId(null);
|
|
2280
|
-
|
|
2277
|
+
setFocusedTrailId(null);
|
|
2281
2278
|
setFocusedStepIndex(null);
|
|
2282
|
-
|
|
2279
|
+
setHoveredTrailStep(null);
|
|
2283
2280
|
issueModuleFocusRef.current = null;
|
|
2284
2281
|
onSelect?.(comp.alias);
|
|
2285
2282
|
requestAnimationFrame(() => {
|
|
@@ -2293,11 +2290,11 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2293
2290
|
});
|
|
2294
2291
|
} else if (step) {
|
|
2295
2292
|
// A step finding focuses the step itself, through the same entry point
|
|
2296
|
-
// the
|
|
2293
|
+
// the trail panel uses — so the drawer, the step numbering, the
|
|
2297
2294
|
// dim-mode hover preview, and the camera all behave identically whether
|
|
2298
2295
|
// the step was reached from the panel or from a diagnostics card.
|
|
2299
2296
|
issueModuleFocusRef.current = null;
|
|
2300
|
-
|
|
2297
|
+
focusTrailStep(step.trail, step.stepIndex);
|
|
2301
2298
|
} else {
|
|
2302
2299
|
// Nothing selectable — but a module target names a boundary frame, so
|
|
2303
2300
|
// frame that region.
|
|
@@ -2305,9 +2302,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2305
2302
|
if (moduleNodeId) {
|
|
2306
2303
|
setSelected(null);
|
|
2307
2304
|
setSelectedEdgeId(null);
|
|
2308
|
-
|
|
2305
|
+
setFocusedTrailId(null);
|
|
2309
2306
|
setFocusedStepIndex(null);
|
|
2310
|
-
|
|
2307
|
+
setHoveredTrailStep(null);
|
|
2311
2308
|
issueModuleFocusRef.current = moduleNodeId;
|
|
2312
2309
|
requestAnimationFrame(() => {
|
|
2313
2310
|
fitNodeRects(new Set([moduleNodeId]), 0.4);
|
|
@@ -2320,7 +2317,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2320
2317
|
issueComponent,
|
|
2321
2318
|
issueStep,
|
|
2322
2319
|
issueModuleNodeId,
|
|
2323
|
-
|
|
2320
|
+
focusTrailStep,
|
|
2324
2321
|
onSelect,
|
|
2325
2322
|
onSelectIssue,
|
|
2326
2323
|
fitView,
|
|
@@ -2350,14 +2347,14 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2350
2347
|
// not the card), and correctly leaves that alone here.
|
|
2351
2348
|
const owned = focusedStepRef.current;
|
|
2352
2349
|
if (
|
|
2353
|
-
owned?.
|
|
2350
|
+
owned?.trailId !== step.trail.id ||
|
|
2354
2351
|
owned?.stepIndex !== step.stepIndex
|
|
2355
2352
|
) {
|
|
2356
2353
|
return;
|
|
2357
2354
|
}
|
|
2358
|
-
|
|
2355
|
+
setFocusedTrailId(null);
|
|
2359
2356
|
setFocusedStepIndex(null);
|
|
2360
|
-
setDrawerTarget((prev) => (prev?.kind === '
|
|
2357
|
+
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
2361
2358
|
} else if (moduleNodeId) {
|
|
2362
2359
|
// Only unwind framing this card still owns.
|
|
2363
2360
|
if (issueModuleFocusRef.current !== moduleNodeId) return;
|
|
@@ -2373,14 +2370,14 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2373
2370
|
[issueComponent, issueStep, issueModuleNodeId, fitView],
|
|
2374
2371
|
);
|
|
2375
2372
|
|
|
2376
|
-
// Construct tokens inside a
|
|
2373
|
+
// Construct tokens inside a trail snippet. A step's line is an edge
|
|
2377
2374
|
// between its `from`/`to` components, so a token naming either of those
|
|
2378
2375
|
// constructs should navigate to that construct's declaration. Index the
|
|
2379
|
-
// matchable identifiers per step (`
|
|
2380
|
-
const
|
|
2376
|
+
// matchable identifiers per step (`trailId:index`).
|
|
2377
|
+
const trailSymbolIndex = useMemo(() => {
|
|
2381
2378
|
const byAlias = new Map(components.map((c) => [c.alias, c]));
|
|
2382
2379
|
const index = new Map<string, Map<string, string>>();
|
|
2383
|
-
for (const wt of
|
|
2380
|
+
for (const wt of trails ?? []) {
|
|
2384
2381
|
wt.steps.forEach((step, i) => {
|
|
2385
2382
|
const tokens = new Map<string, string>();
|
|
2386
2383
|
for (const alias of [step.from, step.to]) {
|
|
@@ -2392,19 +2389,19 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2392
2389
|
});
|
|
2393
2390
|
}
|
|
2394
2391
|
return index;
|
|
2395
|
-
}, [components,
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
// Stable resolver: reads the live index + focused
|
|
2400
|
-
// the memoized
|
|
2401
|
-
const
|
|
2402
|
-
(query:
|
|
2403
|
-
const
|
|
2404
|
-
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;
|
|
2405
2402
|
return (
|
|
2406
|
-
|
|
2407
|
-
.get(`${
|
|
2403
|
+
trailSymbolIndexRef.current
|
|
2404
|
+
.get(`${trailId}:${query.stepIndex}`)
|
|
2408
2405
|
?.get(query.tokenText) ?? null
|
|
2409
2406
|
);
|
|
2410
2407
|
},
|
|
@@ -2492,11 +2489,11 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2492
2489
|
[],
|
|
2493
2490
|
);
|
|
2494
2491
|
|
|
2495
|
-
const
|
|
2496
|
-
|
|
2497
|
-
const
|
|
2498
|
-
(ctx:
|
|
2499
|
-
|
|
2492
|
+
const trailViewerRef = useRef(renderTrailViewer);
|
|
2493
|
+
trailViewerRef.current = renderTrailViewer;
|
|
2494
|
+
const renderTrailDrawerContent = useCallback(
|
|
2495
|
+
(ctx: TrailViewerContext) =>
|
|
2496
|
+
trailViewerRef.current?.(ctx) ?? null,
|
|
2500
2497
|
[],
|
|
2501
2498
|
);
|
|
2502
2499
|
|
|
@@ -2505,7 +2502,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2505
2502
|
{/* Sidebar: scrollable title/description on top, files or flows pinned below.
|
|
2506
2503
|
The description hides by default so files/flows get the room; the
|
|
2507
2504
|
title-row toggle reveals it, and the lower panel yields back to 50%. */}
|
|
2508
|
-
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 ||
|
|
2505
|
+
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasTrails) && (
|
|
2509
2506
|
<div
|
|
2510
2507
|
style={{
|
|
2511
2508
|
width: sidebarWidth,
|
|
@@ -2612,7 +2609,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2612
2609
|
)}
|
|
2613
2610
|
{sidebarAfterDescription}
|
|
2614
2611
|
</div>
|
|
2615
|
-
{(treeFilePaths.length > 0 ||
|
|
2612
|
+
{(treeFilePaths.length > 0 || hasTrails || issuesActive) && (
|
|
2616
2613
|
<div
|
|
2617
2614
|
style={{
|
|
2618
2615
|
...(showDesc ? { height: '50%' as const } : { flex: 1, minHeight: 0 }),
|
|
@@ -2691,7 +2688,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2691
2688
|
)
|
|
2692
2689
|
) : (
|
|
2693
2690
|
<>
|
|
2694
|
-
{
|
|
2691
|
+
{hasTrails && (
|
|
2695
2692
|
<div
|
|
2696
2693
|
role="tablist"
|
|
2697
2694
|
aria-label="Sidebar view"
|
|
@@ -2703,7 +2700,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2703
2700
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
2704
2701
|
}}
|
|
2705
2702
|
>
|
|
2706
|
-
{(['
|
|
2703
|
+
{(['trails', 'files'] as const).map((view) => (
|
|
2707
2704
|
<button
|
|
2708
2705
|
key={view}
|
|
2709
2706
|
type="button"
|
|
@@ -2727,30 +2724,30 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2727
2724
|
cursor: 'pointer',
|
|
2728
2725
|
}}
|
|
2729
2726
|
>
|
|
2730
|
-
{view === '
|
|
2727
|
+
{view === 'trails' ? 'Trails' : 'Files'}
|
|
2731
2728
|
</button>
|
|
2732
2729
|
))}
|
|
2733
2730
|
</div>
|
|
2734
2731
|
)}
|
|
2735
|
-
{sidebarView === '
|
|
2736
|
-
<
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2732
|
+
{sidebarView === 'trails' && trails && trails.length > 0 ? (
|
|
2733
|
+
<TrailsPanel
|
|
2734
|
+
trails={trails}
|
|
2735
|
+
expandedTrails={expandedTrails}
|
|
2736
|
+
focusedTrailId={focusedTrailId}
|
|
2740
2737
|
focusedStepIndex={focusedStepIndex}
|
|
2741
|
-
|
|
2742
|
-
onToggleCollapsed={
|
|
2743
|
-
onFocusFlow={
|
|
2744
|
-
onClearFocus={
|
|
2745
|
-
onFocusStep={
|
|
2738
|
+
hoveredTrailStep={hoveredTrailStep}
|
|
2739
|
+
onToggleCollapsed={toggleTrailCollapsed}
|
|
2740
|
+
onFocusFlow={focusTrailEdges}
|
|
2741
|
+
onClearFocus={clearTrailFocus}
|
|
2742
|
+
onFocusStep={focusTrailStep}
|
|
2746
2743
|
onHoverStep={(tl, i) =>
|
|
2747
|
-
|
|
2744
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex: i })
|
|
2748
2745
|
}
|
|
2749
2746
|
onHoverFlow={(tl) =>
|
|
2750
|
-
|
|
2747
|
+
setHoveredTrailStep({ trailId: tl.id, stepIndex: null })
|
|
2751
2748
|
}
|
|
2752
|
-
onLeaveStep={() =>
|
|
2753
|
-
onReorder={
|
|
2749
|
+
onLeaveStep={() => setHoveredTrailStep(null)}
|
|
2750
|
+
onReorder={onReorderTrails}
|
|
2754
2751
|
proposedAliases={proposedAliases}
|
|
2755
2752
|
/>
|
|
2756
2753
|
) : treeFilePaths.length > 0 ? (
|
|
@@ -2799,7 +2796,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2799
2796
|
)}
|
|
2800
2797
|
|
|
2801
2798
|
{/* Drag handle between sidebar and canvas — resize the left panel. */}
|
|
2802
|
-
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 ||
|
|
2799
|
+
{!hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasTrails) && (
|
|
2803
2800
|
<div
|
|
2804
2801
|
onMouseDown={onSidebarResizeStart}
|
|
2805
2802
|
aria-label="Resize sidebar"
|
|
@@ -2980,9 +2977,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
2980
2977
|
>
|
|
2981
2978
|
<GraphChrome />
|
|
2982
2979
|
</ReactFlow>
|
|
2983
|
-
{/* Graph /
|
|
2980
|
+
{/* Graph / trail / step titles — non-interactive chips at the top
|
|
2984
2981
|
of the canvas. Graph-only embeds use these without opening the sidebar. */}
|
|
2985
|
-
{(graphTitle ||
|
|
2982
|
+
{(graphTitle || overlayTrailTitle || overlayTrailStep) && (
|
|
2986
2983
|
<div
|
|
2987
2984
|
style={{
|
|
2988
2985
|
position: 'absolute',
|
|
@@ -3019,11 +3016,11 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3019
3016
|
{graphTitle}
|
|
3020
3017
|
</div>
|
|
3021
3018
|
)}
|
|
3022
|
-
{
|
|
3019
|
+
{overlayTrailTitle && (
|
|
3023
3020
|
<div
|
|
3024
3021
|
style={{
|
|
3025
3022
|
maxWidth: '100%',
|
|
3026
|
-
minWidth:
|
|
3023
|
+
minWidth: overlayTrailStep ? 160 : undefined,
|
|
3027
3024
|
display: 'flex',
|
|
3028
3025
|
flexDirection: 'column',
|
|
3029
3026
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
@@ -3034,9 +3031,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3034
3031
|
opacity: 0.95,
|
|
3035
3032
|
}}
|
|
3036
3033
|
aria-label={
|
|
3037
|
-
|
|
3038
|
-
? `${
|
|
3039
|
-
:
|
|
3034
|
+
overlayTrailStep
|
|
3035
|
+
? `${overlayTrailTitle}, step ${overlayTrailStep.index} of ${overlayTrailStep.total}`
|
|
3036
|
+
: overlayTrailTitle
|
|
3040
3037
|
}
|
|
3041
3038
|
>
|
|
3042
3039
|
<div
|
|
@@ -3052,9 +3049,9 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3052
3049
|
textAlign: 'center',
|
|
3053
3050
|
}}
|
|
3054
3051
|
>
|
|
3055
|
-
{
|
|
3052
|
+
{overlayTrailTitle}
|
|
3056
3053
|
</div>
|
|
3057
|
-
{
|
|
3054
|
+
{overlayTrailStep && overlayTrailStep.total > 0 && (
|
|
3058
3055
|
<div
|
|
3059
3056
|
style={{
|
|
3060
3057
|
display: 'flex',
|
|
@@ -3063,10 +3060,10 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3063
3060
|
}}
|
|
3064
3061
|
aria-hidden="true"
|
|
3065
3062
|
>
|
|
3066
|
-
{Array.from({ length:
|
|
3063
|
+
{Array.from({ length: overlayTrailStep.total }, (_, i) => {
|
|
3067
3064
|
const n = i + 1;
|
|
3068
|
-
const active = n ===
|
|
3069
|
-
const done = n <
|
|
3065
|
+
const active = n === overlayTrailStep.index;
|
|
3066
|
+
const done = n < overlayTrailStep.index;
|
|
3070
3067
|
return (
|
|
3071
3068
|
<span
|
|
3072
3069
|
key={n}
|
|
@@ -3089,7 +3086,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3089
3086
|
)}
|
|
3090
3087
|
</div>
|
|
3091
3088
|
)}
|
|
3092
|
-
{
|
|
3089
|
+
{overlayTrailStep?.annotation && (
|
|
3093
3090
|
<div
|
|
3094
3091
|
style={{
|
|
3095
3092
|
maxWidth: '100%',
|
|
@@ -3105,7 +3102,7 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3105
3102
|
lineHeight: 1.35,
|
|
3106
3103
|
}}
|
|
3107
3104
|
>
|
|
3108
|
-
{
|
|
3105
|
+
{overlayTrailStep.annotation}
|
|
3109
3106
|
</div>
|
|
3110
3107
|
)}
|
|
3111
3108
|
</div>
|
|
@@ -3167,16 +3164,16 @@ function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initi
|
|
|
3167
3164
|
suppressEscape={fileOverlayOpen}
|
|
3168
3165
|
hidden={hideDrawer}
|
|
3169
3166
|
>
|
|
3170
|
-
{drawerTarget?.kind === '
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
<
|
|
3174
|
-
render={
|
|
3175
|
-
|
|
3167
|
+
{drawerTarget?.kind === 'trail' &&
|
|
3168
|
+
focusedTrail &&
|
|
3169
|
+
renderTrailViewer ? (
|
|
3170
|
+
<TrailDrawerContent
|
|
3171
|
+
render={renderTrailDrawerContent}
|
|
3172
|
+
trail={focusedTrail}
|
|
3176
3173
|
stepIndex={drawerTarget.stepIndex}
|
|
3177
|
-
onOpenFile={
|
|
3174
|
+
onOpenFile={onOpenFileFromTrail}
|
|
3178
3175
|
proposedAliases={proposedAliases}
|
|
3179
|
-
resolveSymbol={
|
|
3176
|
+
resolveSymbol={resolveTrailSymbol}
|
|
3180
3177
|
onSymbolClick={openConstructDeclaration}
|
|
3181
3178
|
/>
|
|
3182
3179
|
) : drawerTarget?.kind === 'file' ? (
|
|
@@ -3345,7 +3342,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3345
3342
|
|
|
3346
3343
|
const constructsOnly = isConstructsOnlyModel({
|
|
3347
3344
|
components: props.components,
|
|
3348
|
-
|
|
3345
|
+
trails: props.trails,
|
|
3349
3346
|
graphifyRelations: props.graphifyRelations,
|
|
3350
3347
|
});
|
|
3351
3348
|
|