@principal-ai/subsystems-react 0.42.0 → 0.43.1
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/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.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 +6 -9
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +15 -50
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts +2 -3
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +2 -3
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts +5 -5
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +9 -18
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +16 -12
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +99 -113
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SubsystemFileTree.d.ts +5 -5
- package/dist/subsystem/SubsystemFileTree.d.ts.map +1 -1
- package/dist/subsystem/SubsystemFileTree.js +13 -49
- package/dist/subsystem/SubsystemFileTree.js.map +1 -1
- package/dist/subsystem/model.d.ts +67 -53
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +305 -55
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +11 -4
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/toC4.d.ts +4 -4
- package/dist/subsystem/toC4.d.ts.map +1 -1
- package/dist/subsystem/toC4.js +3 -7
- package/dist/subsystem/toC4.js.map +1 -1
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +7 -0
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +0 -4
- package/src/stories/Subsystem/AgentsPanel.stories.tsx +3 -5
- package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +1 -4
- package/src/stories/Subsystem/C4Graph/c4Fixture.ts +1 -1
- package/src/stories/Subsystem/ComponentGraph/Appearance.stories.tsx +2 -4
- package/src/stories/Subsystem/ComponentGraph/Basics.stories.tsx +8 -8
- package/src/stories/Subsystem/ComponentGraph/Captures.stories.tsx +19 -19
- package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +0 -1
- package/src/stories/Subsystem/ComponentGraph/CustomEntities.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +0 -2
- package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +2 -6
- package/src/stories/Subsystem/ComponentGraph/EdgeViews.stories.tsx +21 -21
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +0 -3
- package/src/stories/Subsystem/ComponentGraph/FrameworkStereotype.stories.tsx +30 -32
- package/src/stories/Subsystem/ComponentGraph/GraphTitle.stories.tsx +4 -4
- package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +1 -26
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +3 -25
- package/src/stories/Subsystem/ComponentGraph/ModuleBadges.stories.tsx +7 -8
- package/src/stories/Subsystem/ComponentGraph/ModulePathNesting.stories.tsx +116 -0
- package/src/stories/Subsystem/ComponentGraph/Modules.stories.tsx +9 -11
- package/src/stories/Subsystem/ComponentGraph/Packages.stories.tsx +3 -6
- package/src/stories/Subsystem/ComponentGraph/Processes.stories.tsx +3 -3
- package/src/stories/Subsystem/ComponentGraph/Proposed.stories.tsx +8 -6
- package/src/stories/Subsystem/ComponentGraph/ProposedWalkthroughs.stories.tsx +0 -1
- package/src/stories/Subsystem/ComponentGraph/Reorder.stories.tsx +0 -1
- package/src/stories/Subsystem/ComponentGraph/Scenarios.stories.tsx +26 -28
- package/src/stories/Subsystem/ComponentGraph/WalkthroughAutoplay.stories.tsx +0 -1
- package/src/stories/Subsystem/ComponentGraph/WorkspacePackageFrames.stories.tsx +2 -3
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +18 -58
- package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +0 -2
- package/src/subsystem/C4Graph.tsx +2 -3
- package/src/subsystem/IssueList.tsx +12 -22
- package/src/subsystem/SubsystemComponentGraph.tsx +117 -135
- package/src/subsystem/SubsystemFileTree.tsx +17 -50
- package/src/subsystem/model.test.ts +155 -24
- package/src/subsystem/model.ts +352 -108
- package/src/subsystem/nodes.tsx +13 -3
- package/src/subsystem/toC4.test.ts +5 -12
- package/src/subsystem/toC4.ts +5 -9
- package/src/utils/elkLayout.ts +10 -0
|
@@ -20,7 +20,7 @@ import { ReactFlow, ReactFlowProvider, useReactFlow, useViewport, applyNodeChang
|
|
|
20
20
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
21
21
|
import { FileText, X } from 'lucide-react';
|
|
22
22
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
23
|
-
import { buildSubsystemGraph, deriveGraphEdges, isConstructsOnlyModel, moduleGroupNodeId,
|
|
23
|
+
import { buildSubsystemGraph, deriveGraphEdges, isConstructsOnlyModel, moduleGroupNodeId, isWalkthroughMechanism, edgeColor, MECHANISM_DESCRIPTIONS, subsystemGraphLayoutKey, walkthroughStepGraphEdgeId, } from './model';
|
|
24
24
|
import { ConstructsCatalog } from './ConstructsCatalog';
|
|
25
25
|
import { SubsystemAgentsPanel, } from './AgentsPanel';
|
|
26
26
|
import { MaintainLivePanel, } from '../components/maintain-events/MaintainLivePanel';
|
|
@@ -122,14 +122,14 @@ const WalkthroughDrawerContent = memo(function WalkthroughDrawerContent({ render
|
|
|
122
122
|
onSymbolClick,
|
|
123
123
|
}) }));
|
|
124
124
|
});
|
|
125
|
-
function Inner({ components,
|
|
125
|
+
function Inner({ components, walkthroughs, graphifyRelations, orderByLine, initialWalkthroughId, onReorderWalkthroughs, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, moduleNesting, edgeView, title, hideSidebar, walkthroughStepMode = 'focus', autoPlayWalkthroughs = false, walkthroughAutoPlayIntervalMs = WALKTHROUGH_PLAY_PAUSE_MS, zoomOnWalkthroughFocus = true, walkthroughFocusDurationMs = 300, graphTitle, showWalkthroughTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileView, renderFileViewer, renderWalkthroughViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, hideDrawer = false, persistKey, liveEvents, agentsPanel }) {
|
|
126
126
|
const { theme } = useTheme();
|
|
127
|
-
const { fitView, fitBounds } = useReactFlow();
|
|
127
|
+
const { fitView, fitBounds, screenToFlowPosition } = useReactFlow();
|
|
128
128
|
const viewport = useViewport();
|
|
129
129
|
// Restored once per mount from `localStorage` (see `readViewState`). Each
|
|
130
130
|
// graph is its own tab/mount, so `persistKey` is stable for a mount.
|
|
131
131
|
const persisted = useMemo(() => readViewState(persistKey), [persistKey]);
|
|
132
|
-
const graphEdges = useMemo(() => deriveGraphEdges({
|
|
132
|
+
const graphEdges = useMemo(() => deriveGraphEdges({ walkthroughs }), [walkthroughs]);
|
|
133
133
|
const [built, setBuilt] = useState({
|
|
134
134
|
nodes: [],
|
|
135
135
|
edges: [],
|
|
@@ -149,6 +149,71 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
149
149
|
const pendingFocusFitRef = useRef(null);
|
|
150
150
|
// Component the pointer is over (null on leave) → transient tree highlight.
|
|
151
151
|
const [hoveredComponentAlias, setHoveredComponentId] = useState(null);
|
|
152
|
+
// Boundary frame hovered on the canvas → highlight the matching folder row in
|
|
153
|
+
// the sidebar file tree, so a frame's place in the repo tree is visible.
|
|
154
|
+
const [hoveredRegion, setHoveredRegion] = useState(null);
|
|
155
|
+
const hoveredBoundaryPath = hoveredRegion && (hoveredRegion.kind === 'module' || hoveredRegion.kind === 'directory')
|
|
156
|
+
? hoveredRegion.key
|
|
157
|
+
: null;
|
|
158
|
+
// Path frames (module / directory) as absolute flow-coord rects, hit-tested
|
|
159
|
+
// on pointer move. A pointer hit-test keeps the frames' interior hoverable
|
|
160
|
+
// WITHOUT an overlay that would block edges / child nodes.
|
|
161
|
+
const pathFrameRects = useMemo(() => {
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const n of built.nodes) {
|
|
164
|
+
if (n.type !== 'subsystem-group')
|
|
165
|
+
continue;
|
|
166
|
+
const region = n.data?.region;
|
|
167
|
+
if (!region?.key || (region.kind !== 'module' && region.kind !== 'directory'))
|
|
168
|
+
continue;
|
|
169
|
+
const r = built.absoluteRects.get(n.id);
|
|
170
|
+
if (!r)
|
|
171
|
+
continue;
|
|
172
|
+
out.push({ kind: region.kind, key: region.key, x: r.x, y: r.y, w: r.width, h: r.height });
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}, [built.nodes, built.absoluteRects]);
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (pathFrameRects.length === 0)
|
|
178
|
+
return;
|
|
179
|
+
let raf = 0;
|
|
180
|
+
let last = null;
|
|
181
|
+
const overCanvas = (e) => e.composedPath().some((el) => el instanceof HTMLElement && el.classList.contains(GRAPH_CANVAS_CLASS));
|
|
182
|
+
const onMove = (e) => {
|
|
183
|
+
if (!overCanvas(e)) {
|
|
184
|
+
last = null;
|
|
185
|
+
setHoveredRegion((prev) => (prev == null ? prev : null));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
last = { x: e.clientX, y: e.clientY };
|
|
189
|
+
if (raf)
|
|
190
|
+
return;
|
|
191
|
+
raf = window.requestAnimationFrame(() => {
|
|
192
|
+
raf = 0;
|
|
193
|
+
if (!last)
|
|
194
|
+
return;
|
|
195
|
+
const p = screenToFlowPosition(last);
|
|
196
|
+
let best = null;
|
|
197
|
+
let bestArea = Number.POSITIVE_INFINITY;
|
|
198
|
+
for (const f of pathFrameRects) {
|
|
199
|
+
if (p.x >= f.x && p.x <= f.x + f.w && p.y >= f.y && p.y <= f.y + f.h) {
|
|
200
|
+
const area = f.w * f.h;
|
|
201
|
+
if (area < bestArea) {
|
|
202
|
+
bestArea = area;
|
|
203
|
+
best = { kind: f.kind, key: f.key };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
setHoveredRegion((prev) => prev?.kind === best?.kind && prev?.key === best?.key ? prev : best);
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
window.addEventListener('mousemove', onMove);
|
|
211
|
+
return () => {
|
|
212
|
+
window.removeEventListener('mousemove', onMove);
|
|
213
|
+
if (raf)
|
|
214
|
+
cancelAnimationFrame(raf);
|
|
215
|
+
};
|
|
216
|
+
}, [pathFrameRects, screenToFlowPosition]);
|
|
152
217
|
// Walkthrough focus — selected flow (or step) is full strength; other
|
|
153
218
|
// opened-flow members stay visible but dimmed; everything else is hidden.
|
|
154
219
|
// Restored from the persisted view state when a `persistKey` is set.
|
|
@@ -165,24 +230,19 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
165
230
|
const [sidebarView, setSidebarView] = useState(() => persisted.sidebarView ?? (walkthroughs?.length ? 'walkthroughs' : 'files'));
|
|
166
231
|
// Diagnostics area tab: issues list vs the Maintain agent pipeline.
|
|
167
232
|
const [diagnosticsTab, setDiagnosticsTab] = useState(() => persisted.diagnosticsTab ?? 'issues');
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// visible; it reverts when the focus is undone (see `unfocusIssueTarget`) or
|
|
171
|
-
// when the diagnostics list closes. Null = use the host-pinned / tab view.
|
|
172
|
-
const [edgeViewOverride, setEdgeViewOverride] = useState(null);
|
|
173
|
-
// One edge vocabulary at a time. When the caller doesn't pick, the sidebar's
|
|
174
|
-
// Files / Walkthroughs tab picks: Files draws topology relation edges,
|
|
233
|
+
// One edge source at a time. When the caller doesn't pick, the sidebar's
|
|
234
|
+
// Files / Walkthroughs tab picks: Files draws graphify static edges,
|
|
175
235
|
// Walkthroughs draws runtime hop edges. Without visible tabs (no walkthroughs,
|
|
176
|
-
// or a sidebar-less embed) fall back to
|
|
177
|
-
// to walkthrough edges rather than empty, everything else to relations.
|
|
236
|
+
// or a sidebar-less embed) fall back to walkthrough edges.
|
|
178
237
|
const sidebarTabsVisible = !hideSidebar && (walkthroughs?.length ?? 0) > 0;
|
|
179
|
-
const resolvedEdgeView =
|
|
180
|
-
edgeView ??
|
|
238
|
+
const resolvedEdgeView = edgeView ??
|
|
181
239
|
(sidebarTabsVisible
|
|
182
|
-
? (sidebarView === 'walkthroughs' ? 'walkthroughs' : '
|
|
183
|
-
:
|
|
240
|
+
? (sidebarView === 'walkthroughs' ? 'walkthroughs' : 'graphify')
|
|
241
|
+
: (walkthroughs?.length ?? 0) > 0
|
|
184
242
|
? 'walkthroughs'
|
|
185
|
-
:
|
|
243
|
+
: (graphifyRelations?.length ?? 0) > 0
|
|
244
|
+
? 'graphify'
|
|
245
|
+
: 'walkthroughs');
|
|
186
246
|
// Walkthrough flows the user has expanded (via the title row). Closed by
|
|
187
247
|
// default so a graph with several flows doesn't dump every step list at once.
|
|
188
248
|
// Restored from the persisted view state when a `persistKey` is set.
|
|
@@ -381,13 +441,11 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
381
441
|
// Pass 1: build with estimated widths so React Flow can measure the DOM.
|
|
382
442
|
// The pane stays hidden until Pass 2 completes. Key off layout-affecting
|
|
383
443
|
// fields only — declarationRef updates after verify must not re-run ELK.
|
|
384
|
-
const layoutKey = useMemo(() => subsystemGraphLayoutKey({ components,
|
|
444
|
+
const layoutKey = useMemo(() => subsystemGraphLayoutKey({ components, walkthroughs, graphifyRelations }), [components, walkthroughs, graphifyRelations]);
|
|
385
445
|
const componentsRef = useRef(components);
|
|
386
|
-
const relationsRef = useRef(relations);
|
|
387
446
|
const walkthroughsRef = useRef(walkthroughs);
|
|
388
447
|
const graphifyRelationsRef = useRef(graphifyRelations);
|
|
389
448
|
componentsRef.current = components;
|
|
390
|
-
relationsRef.current = relations;
|
|
391
449
|
walkthroughsRef.current = walkthroughs;
|
|
392
450
|
graphifyRelationsRef.current = graphifyRelations;
|
|
393
451
|
// Track measured dimensions from React Flow's dimension changes.
|
|
@@ -406,13 +464,13 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
406
464
|
pass2GenRef.current += 1;
|
|
407
465
|
const doc = {
|
|
408
466
|
components: componentsRef.current,
|
|
409
|
-
relations: relationsRef.current,
|
|
410
467
|
walkthroughs: walkthroughsRef.current,
|
|
411
468
|
};
|
|
412
469
|
void buildSubsystemGraph(doc, {
|
|
413
470
|
maxNodeWidth,
|
|
414
471
|
showEdgeLabels,
|
|
415
472
|
showSingletonFrames,
|
|
473
|
+
moduleNesting,
|
|
416
474
|
graphifyRelations: graphifyRelationsRef.current,
|
|
417
475
|
orderByLine,
|
|
418
476
|
})
|
|
@@ -438,7 +496,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
438
496
|
setLayoutReady(true);
|
|
439
497
|
});
|
|
440
498
|
return () => { alive = false; };
|
|
441
|
-
}, [layoutKey, maxNodeWidth, showEdgeLabels]);
|
|
499
|
+
}, [layoutKey, maxNodeWidth, showEdgeLabels, moduleNesting]);
|
|
442
500
|
// Pass 2: once every leaf node has a measured dimension, re-run ELK.
|
|
443
501
|
// Group parents are sized by ELK, not measured — exclude them or pass 2
|
|
444
502
|
// would wait forever for dimensions that never arrive.
|
|
@@ -465,7 +523,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
465
523
|
const measuredWidths = new Map(leafNodes.map((n) => [n.id, dims.get(n.id).width]));
|
|
466
524
|
const measuredHeights = new Map(leafNodes.map((n) => [n.id, dims.get(n.id).height]));
|
|
467
525
|
const gen = ++pass2GenRef.current;
|
|
468
|
-
void buildSubsystemGraph({ components,
|
|
526
|
+
void buildSubsystemGraph({ components, walkthroughs }, { maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, moduleNesting, graphifyRelations, orderByLine })
|
|
469
527
|
.then(({ nodes, edges: e, absoluteRects }) => {
|
|
470
528
|
if (gen !== pass2GenRef.current)
|
|
471
529
|
return;
|
|
@@ -479,7 +537,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
479
537
|
// Reveal Pass 1 layout rather than leaving the cover up forever.
|
|
480
538
|
setLayoutReady(true);
|
|
481
539
|
});
|
|
482
|
-
}, [built.nodes, components,
|
|
540
|
+
}, [built.nodes, components, walkthroughs, graphifyRelations, orderByLine, maxNodeWidth, showEdgeLabels, moduleNesting]);
|
|
483
541
|
// After Pass 1 commits, try Pass 2 immediately with retained measurements.
|
|
484
542
|
// Same-id live updates often get no new React Flow `dimensions` events, so
|
|
485
543
|
// waiting only on onNodesChange leaves the cover stuck.
|
|
@@ -559,7 +617,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
559
617
|
const baseEdges = convertedEdges;
|
|
560
618
|
// Resolve an issue's component target to the matching node. `id` is the
|
|
561
619
|
// stable alias; `label` may be an alias, name, or symbol. Non-component
|
|
562
|
-
// targets (
|
|
620
|
+
// targets (module / flow / repo) have no single node → null.
|
|
563
621
|
const issueComponent = useCallback((issue) => {
|
|
564
622
|
const target = issue.target;
|
|
565
623
|
if (target?.kind !== 'component')
|
|
@@ -569,26 +627,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
569
627
|
c.name === target.label ||
|
|
570
628
|
c.symbol === target.label) ?? null);
|
|
571
629
|
}, [components]);
|
|
572
|
-
// Resolve a `relation` issue target to the display edge it flags. The target
|
|
573
|
-
// names the two endpoints (`from → to`); match an edge joining both, in
|
|
574
|
-
// either direction, preferring the target's `detail` mechanism when it names
|
|
575
|
-
// one. Null when no display edge matches.
|
|
576
|
-
const issueEdge = useCallback((issue) => {
|
|
577
|
-
const target = issue.target;
|
|
578
|
-
if (target?.kind !== 'relation')
|
|
579
|
-
return null;
|
|
580
|
-
const parts = `${target.id ?? ''} → ${target.label}`
|
|
581
|
-
.split('→')
|
|
582
|
-
.map((p) => p.replace(/\(\)$/, '').trim())
|
|
583
|
-
.filter(Boolean);
|
|
584
|
-
const joins = (e) => parts.includes(e.source) && parts.includes(e.target);
|
|
585
|
-
const matches = baseEdges.filter(joins);
|
|
586
|
-
const byMechanism = target.detail
|
|
587
|
-
? matches.filter((e) => e.data?.mechanism ===
|
|
588
|
-
target.detail)
|
|
589
|
-
: matches;
|
|
590
|
-
return byMechanism[0] ?? matches[0] ?? null;
|
|
591
|
-
}, [baseEdges]);
|
|
592
630
|
// A `module` issue target names a boundary frame, so focusing the card frames
|
|
593
631
|
// that region on the canvas. Null when the target names no frame (a singleton
|
|
594
632
|
// dropped from the layout, or a region key nothing declares).
|
|
@@ -692,7 +730,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
692
730
|
// the canvas keeps while everything else dims. Derived from each finding's
|
|
693
731
|
// target:
|
|
694
732
|
// component → that node
|
|
695
|
-
// relation → the edge it flags (+ its two endpoint nodes)
|
|
696
733
|
// module → every component in that module
|
|
697
734
|
// walkthrough → the flow's nodes + its hop edges
|
|
698
735
|
// repo → every component of that repo
|
|
@@ -717,23 +754,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
717
754
|
if (a)
|
|
718
755
|
nodeIds.add(a);
|
|
719
756
|
}
|
|
720
|
-
else if (t.kind === 'relation') {
|
|
721
|
-
// The target names the two endpoints (`from → to`); light the edge and
|
|
722
|
-
// its endpoints. Falls back to endpoints alone if no edge matches.
|
|
723
|
-
const edge = issueEdge(issue);
|
|
724
|
-
if (edge) {
|
|
725
|
-
edgeIds.add(edge.id);
|
|
726
|
-
nodeIds.add(edge.source);
|
|
727
|
-
nodeIds.add(edge.target);
|
|
728
|
-
}
|
|
729
|
-
else {
|
|
730
|
-
for (const part of `${t.id ?? ''} → ${t.label}`.split('→')) {
|
|
731
|
-
const a = resolveAlias(part);
|
|
732
|
-
if (a)
|
|
733
|
-
nodeIds.add(a);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
757
|
else if (t.kind === 'module') {
|
|
738
758
|
const key = t.id ?? t.label;
|
|
739
759
|
const prefix = `${key.replace(/\/$/, '')}/`;
|
|
@@ -773,7 +793,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
773
793
|
edgeIds.add(e.id);
|
|
774
794
|
}
|
|
775
795
|
return { nodeIds, edgeIds };
|
|
776
|
-
}, [issues, expandedIssueCategories, components, walkthroughs,
|
|
796
|
+
}, [issues, expandedIssueCategories, components, walkthroughs, resolveAlias]);
|
|
777
797
|
const issueFocusNodeIds = issueFocus?.nodeIds ?? null;
|
|
778
798
|
const issueFocusEdgeIds = issueFocus?.edgeIds ?? null;
|
|
779
799
|
// When an edge is selected, dim every other edge + its label to focus it.
|
|
@@ -1016,13 +1036,13 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1016
1036
|
};
|
|
1017
1037
|
};
|
|
1018
1038
|
// Edges outside the selected view are hidden entirely (labels included).
|
|
1019
|
-
// graphify-native edges belong to the
|
|
1039
|
+
// graphify-native edges belong to the graphify view (they are static
|
|
1020
1040
|
// topology, not runtime hops) regardless of their verb.
|
|
1021
1041
|
const edgeInView = (e) => {
|
|
1022
1042
|
const d = e.data;
|
|
1023
1043
|
const mechanism = d?.mechanism ?? 'uses';
|
|
1024
|
-
if (resolvedEdgeView === '
|
|
1025
|
-
return d?.provenance === 'graphify'
|
|
1044
|
+
if (resolvedEdgeView === 'graphify') {
|
|
1045
|
+
return d?.provenance === 'graphify';
|
|
1026
1046
|
}
|
|
1027
1047
|
return isWalkthroughMechanism(mechanism);
|
|
1028
1048
|
};
|
|
@@ -1103,7 +1123,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1103
1123
|
setSelectedEdgeId(null);
|
|
1104
1124
|
setFocusedWalkthroughId(null);
|
|
1105
1125
|
setFocusedStepIndex(null);
|
|
1106
|
-
setEdgeViewOverride(null);
|
|
1107
1126
|
issueModuleFocusRef.current = null;
|
|
1108
1127
|
}, []);
|
|
1109
1128
|
// Sidebar file trees — one per repo on multi-repo graphs, each under its
|
|
@@ -1123,7 +1142,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1123
1142
|
useEffect(() => {
|
|
1124
1143
|
if (!issuesActive) {
|
|
1125
1144
|
setExpandedIssueCategories([]);
|
|
1126
|
-
setEdgeViewOverride(null);
|
|
1127
1145
|
}
|
|
1128
1146
|
}, [issuesActive]);
|
|
1129
1147
|
const toggleIssues = () => {
|
|
@@ -1551,9 +1569,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1551
1569
|
if (playable.length === 0)
|
|
1552
1570
|
return;
|
|
1553
1571
|
// Only run the cycles that can actually be shown in the current edge view.
|
|
1554
|
-
const inView = playable.filter((tl) => tl.steps.some((s) =>
|
|
1555
|
-
? isRelationMechanism(s.mechanism)
|
|
1556
|
-
: isWalkthroughMechanism(s.mechanism)));
|
|
1572
|
+
const inView = playable.filter((tl) => tl.steps.some((s) => isWalkthroughMechanism(s.mechanism)));
|
|
1557
1573
|
if (inView.length === 0)
|
|
1558
1574
|
return;
|
|
1559
1575
|
const cyc = inView;
|
|
@@ -1706,15 +1722,13 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1706
1722
|
onSelect?.(comp.alias);
|
|
1707
1723
|
}, [components, onSelect]);
|
|
1708
1724
|
// Issue click → select + frame the target on the canvas. Component targets
|
|
1709
|
-
// frame their node;
|
|
1710
|
-
//
|
|
1711
|
-
//
|
|
1712
|
-
//
|
|
1713
|
-
// still fires afterwards for its own detail.
|
|
1725
|
+
// frame their node; other kinds (module / flow / repo) have no single element
|
|
1726
|
+
// and just forward. Selections / focus that would hide the target are cleared
|
|
1727
|
+
// first, then the camera flies to it on the next frame. The host's
|
|
1728
|
+
// `onSelectIssue` still fires afterwards for its own detail.
|
|
1714
1729
|
const focusIssueTarget = useCallback((issue) => {
|
|
1715
1730
|
const comp = issueComponent(issue);
|
|
1716
|
-
const
|
|
1717
|
-
const step = comp || edge ? null : issueStep(issue);
|
|
1731
|
+
const step = comp ? null : issueStep(issue);
|
|
1718
1732
|
if (comp) {
|
|
1719
1733
|
setSelected(comp);
|
|
1720
1734
|
setSelectedEdgeId(null);
|
|
@@ -1733,24 +1747,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1733
1747
|
});
|
|
1734
1748
|
});
|
|
1735
1749
|
}
|
|
1736
|
-
else if (edge) {
|
|
1737
|
-
setSelected(null);
|
|
1738
|
-
setFocusedWalkthroughId(null);
|
|
1739
|
-
setFocusedStepIndex(null);
|
|
1740
|
-
setHoveredWalkthroughStep(null);
|
|
1741
|
-
setSelectedEdgeId(edge.id);
|
|
1742
|
-
issueModuleFocusRef.current = null;
|
|
1743
|
-
// Flip to the vocabulary this edge belongs to, so a hop flagged in the
|
|
1744
|
-
// relations view (or vice versa) is actually drawn while focused.
|
|
1745
|
-
const d = edge.data;
|
|
1746
|
-
const mechanism = d?.mechanism ?? 'uses';
|
|
1747
|
-
setEdgeViewOverride(d?.provenance === 'graphify' || isRelationMechanism(mechanism)
|
|
1748
|
-
? 'relations'
|
|
1749
|
-
: 'walkthroughs');
|
|
1750
|
-
requestAnimationFrame(() => {
|
|
1751
|
-
fitFocusBounds(new Set([edge.id]));
|
|
1752
|
-
});
|
|
1753
|
-
}
|
|
1754
1750
|
else if (step) {
|
|
1755
1751
|
// A step finding focuses the step itself, through the same entry point
|
|
1756
1752
|
// the walkthrough panel uses — so the drawer, the step numbering, the
|
|
@@ -1778,14 +1774,12 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1778
1774
|
onSelectIssue?.(issue);
|
|
1779
1775
|
}, [
|
|
1780
1776
|
issueComponent,
|
|
1781
|
-
issueEdge,
|
|
1782
1777
|
issueStep,
|
|
1783
1778
|
issueModuleNodeId,
|
|
1784
1779
|
focusWalkthroughStep,
|
|
1785
1780
|
onSelect,
|
|
1786
1781
|
onSelectIssue,
|
|
1787
1782
|
fitView,
|
|
1788
|
-
fitFocusBounds,
|
|
1789
1783
|
fitNodeRects,
|
|
1790
1784
|
]);
|
|
1791
1785
|
// Collapsing the issue card again undoes the focus: drop the node / edge
|
|
@@ -1803,16 +1797,9 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1803
1797
|
setSelectedEdgeId(null);
|
|
1804
1798
|
}
|
|
1805
1799
|
else {
|
|
1806
|
-
const
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
if (edge) {
|
|
1810
|
-
if (selectedEdgeIdRef.current !== edge.id)
|
|
1811
|
-
return;
|
|
1812
|
-
setSelectedEdgeId(null);
|
|
1813
|
-
setEdgeViewOverride(null);
|
|
1814
|
-
}
|
|
1815
|
-
else if (step) {
|
|
1800
|
+
const step = issueStep(issue);
|
|
1801
|
+
const moduleNodeId = step ? null : issueModuleNodeId(issue);
|
|
1802
|
+
if (step) {
|
|
1816
1803
|
// Only unwind a step focus this card still owns. A dim-mode graph
|
|
1817
1804
|
// keeps its step focus in hover state instead (owned by the pointer,
|
|
1818
1805
|
// not the card), and correctly leaves that alone here.
|
|
@@ -1838,7 +1825,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1838
1825
|
requestAnimationFrame(() => {
|
|
1839
1826
|
fitView({ padding: 0.1, duration: 300, minZoom: 0.05, maxZoom: 2 });
|
|
1840
1827
|
});
|
|
1841
|
-
}, [issueComponent,
|
|
1828
|
+
}, [issueComponent, issueStep, issueModuleNodeId, fitView]);
|
|
1842
1829
|
// Construct tokens inside a walkthrough snippet. A step's line is an edge
|
|
1843
1830
|
// between its `from`/`to` components, so a token naming either of those
|
|
1844
1831
|
// constructs should navigate to that construct's declaration. Index the
|
|
@@ -2037,7 +2024,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2037
2024
|
display: 'flex',
|
|
2038
2025
|
flexDirection: 'column',
|
|
2039
2026
|
borderTop: i > 0 ? `1px solid ${theme.colors.border}` : undefined,
|
|
2040
|
-
}, children: [repoGroups.multiRepo && (_jsx(RepoGroupHeader, { group: group, collapsed: collapsed, onToggle: () => toggleRepoCollapsed(groupKey) })), !collapsed && (_jsx(SubsystemFileTree, { files: group.entries.map((e) => e.displayPath), selectedFile: selected?.file ?? openFile, hoveredFile: hoveredFile, onSelectFile: onTreeSelectFile, headerless: repoGroups.multiRepo }))] }, groupKey));
|
|
2027
|
+
}, children: [repoGroups.multiRepo && (_jsx(RepoGroupHeader, { group: group, collapsed: collapsed, onToggle: () => toggleRepoCollapsed(groupKey) })), !collapsed && (_jsx(SubsystemFileTree, { files: group.entries.map((e) => e.displayPath), selectedFile: selected?.file ?? openFile, hoveredFile: hoveredFile, onSelectFile: onTreeSelectFile, hoveredFolder: hoveredBoundaryPath, headerless: repoGroups.multiRepo }))] }, groupKey));
|
|
2041
2028
|
}) })) : null] })) }))] })), !hideSidebar && (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription || treeFilePaths.length > 0 || hasWalkthroughs) && (_jsx("div", { onMouseDown: onSidebarResizeStart, "aria-label": "Resize sidebar", title: "Drag to resize", style: {
|
|
2042
2029
|
width: 3,
|
|
2043
2030
|
flexShrink: 0,
|
|
@@ -2326,7 +2313,6 @@ export function SubsystemComponentGraph(props) {
|
|
|
2326
2313
|
}, []);
|
|
2327
2314
|
const constructsOnly = isConstructsOnlyModel({
|
|
2328
2315
|
components: props.components,
|
|
2329
|
-
relations: props.relations,
|
|
2330
2316
|
walkthroughs: props.walkthroughs,
|
|
2331
2317
|
graphifyRelations: props.graphifyRelations,
|
|
2332
2318
|
});
|