@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
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
deriveGraphEdges,
|
|
40
40
|
isConstructsOnlyModel,
|
|
41
41
|
moduleGroupNodeId,
|
|
42
|
-
isRelationMechanism,
|
|
43
42
|
isWalkthroughMechanism,
|
|
44
43
|
edgeColor,
|
|
45
44
|
MECHANISM_DESCRIPTIONS,
|
|
@@ -52,7 +51,6 @@ import {
|
|
|
52
51
|
type SubsystemGraphifyRelation,
|
|
53
52
|
type SubsystemNodeIssue,
|
|
54
53
|
type SubsystemRegionIssue,
|
|
55
|
-
type SubsystemRelation,
|
|
56
54
|
type SubsystemWalkthrough,
|
|
57
55
|
} from './model';
|
|
58
56
|
import { ConstructsCatalog } from './ConstructsCatalog';
|
|
@@ -182,8 +180,6 @@ function writeViewState(
|
|
|
182
180
|
|
|
183
181
|
export interface SubsystemComponentGraphProps {
|
|
184
182
|
components: SubsystemComponent[];
|
|
185
|
-
/** Topology relations (structural / module / type). Display edges are derived with walkthrough hops. */
|
|
186
|
-
relations: SubsystemRelation[];
|
|
187
183
|
/**
|
|
188
184
|
* Ordered runtime walkthroughs — one walkthrough per flow. When present the
|
|
189
185
|
* sidebar's bottom half offers a Files/Walkthroughs toggle:
|
|
@@ -198,7 +194,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
198
194
|
* graphify-native relations (raw static-graph edges: `imports`, `contains`,
|
|
199
195
|
* `re_exports`, …). Display-only, drawn with the separate
|
|
200
196
|
* `GRAPHIFY_RELATION_COLOR` palette so they read as derived facts, distinct
|
|
201
|
-
* from authored subsystem mechanisms. Visible in the `
|
|
197
|
+
* from authored subsystem mechanisms. Visible in the `graphify` edge view.
|
|
202
198
|
*/
|
|
203
199
|
graphifyRelations?: readonly SubsystemGraphifyRelation[];
|
|
204
200
|
/**
|
|
@@ -239,16 +235,22 @@ export interface SubsystemComponentGraphProps {
|
|
|
239
235
|
*/
|
|
240
236
|
showSingletonFrames?: boolean;
|
|
241
237
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
238
|
+
* How module frames group.
|
|
239
|
+
* - `exact` (default): one frame per distinct `module` string.
|
|
240
|
+
* - `path`: derive directory frames from `module` path segments and nest
|
|
241
|
+
* module frames inside them (`src` → `src/session` → module).
|
|
242
|
+
*/
|
|
243
|
+
moduleNesting?: 'exact' | 'path';
|
|
244
|
+
/**
|
|
245
|
+
* Which edge source the canvas draws. The graphify and walkthrough sources
|
|
246
|
+
* are disjoint, so a graph carrying both shows one or the other — never
|
|
247
|
+
* both. Edges outside the view are hidden (labels go too).
|
|
248
|
+
* - `graphify`: graphify-native static edges (`imports`, `contains`, …)
|
|
246
249
|
* - `walkthroughs`: walkthrough hop edges (`calls`, `feeds`, …), including
|
|
247
250
|
* step numbers when a flow is focused/hovered
|
|
248
251
|
* Leave unset to let the sidebar's Files / Walkthroughs tab drive it: Files
|
|
249
|
-
* draws
|
|
250
|
-
* tabs, defaults to `walkthroughs
|
|
251
|
-
* no relations), otherwise `relations`.
|
|
252
|
+
* draws graphify edges, Walkthroughs draws runtime hops. Without visible
|
|
253
|
+
* tabs, defaults to `walkthroughs`.
|
|
252
254
|
*/
|
|
253
255
|
edgeView?: SubsystemEdgeView;
|
|
254
256
|
/** Subsystem title displayed in the sidebar. */
|
|
@@ -348,7 +350,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
348
350
|
/**
|
|
349
351
|
* Click an issue. The graph first focuses the target on the canvas itself —
|
|
350
352
|
* a component target is selected and framed (any edge / walkthrough focus
|
|
351
|
-
* that would hide it is cleared),
|
|
353
|
+
* that would hide it is cleared), and a
|
|
352
354
|
* module target frames that boundary frame — then this fires so the host can
|
|
353
355
|
* open its own detail. Collapsing the card again reverses that (deselect +
|
|
354
356
|
* zoom out). Other target kinds (flow / repo) have no node to frame and just
|
|
@@ -522,16 +524,16 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
522
524
|
measured: { w: number; h: number } | null;
|
|
523
525
|
}
|
|
524
526
|
|
|
525
|
-
function Inner({ components,
|
|
527
|
+
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 }: InnerProps) {
|
|
526
528
|
const { theme } = useTheme();
|
|
527
|
-
const { fitView, fitBounds } = useReactFlow();
|
|
529
|
+
const { fitView, fitBounds, screenToFlowPosition } = useReactFlow();
|
|
528
530
|
const viewport = useViewport();
|
|
529
531
|
// Restored once per mount from `localStorage` (see `readViewState`). Each
|
|
530
532
|
// graph is its own tab/mount, so `persistKey` is stable for a mount.
|
|
531
533
|
const persisted = useMemo(() => readViewState(persistKey), [persistKey]);
|
|
532
534
|
const graphEdges = useMemo(
|
|
533
|
-
() => deriveGraphEdges({
|
|
534
|
-
[
|
|
535
|
+
() => deriveGraphEdges({ walkthroughs }),
|
|
536
|
+
[walkthroughs],
|
|
535
537
|
);
|
|
536
538
|
const [built, setBuilt] = useState<{
|
|
537
539
|
nodes: Node[];
|
|
@@ -559,6 +561,73 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
559
561
|
const pendingFocusFitRef = useRef<number | null>(null);
|
|
560
562
|
// Component the pointer is over (null on leave) → transient tree highlight.
|
|
561
563
|
const [hoveredComponentAlias, setHoveredComponentId] = useState<string | null>(null);
|
|
564
|
+
// Boundary frame hovered on the canvas → highlight the matching folder row in
|
|
565
|
+
// the sidebar file tree, so a frame's place in the repo tree is visible.
|
|
566
|
+
const [hoveredRegion, setHoveredRegion] = useState<{
|
|
567
|
+
kind: string;
|
|
568
|
+
key: string;
|
|
569
|
+
} | null>(null);
|
|
570
|
+
const hoveredBoundaryPath =
|
|
571
|
+
hoveredRegion && (hoveredRegion.kind === 'module' || hoveredRegion.kind === 'directory')
|
|
572
|
+
? hoveredRegion.key
|
|
573
|
+
: null;
|
|
574
|
+
// Path frames (module / directory) as absolute flow-coord rects, hit-tested
|
|
575
|
+
// on pointer move. A pointer hit-test keeps the frames' interior hoverable
|
|
576
|
+
// WITHOUT an overlay that would block edges / child nodes.
|
|
577
|
+
const pathFrameRects = useMemo(() => {
|
|
578
|
+
const out: Array<{ kind: string; key: string; x: number; y: number; w: number; h: number }> = [];
|
|
579
|
+
for (const n of built.nodes) {
|
|
580
|
+
if (n.type !== 'subsystem-group') continue;
|
|
581
|
+
const region = (n.data as { region?: { kind?: string; key?: string } } | undefined)?.region;
|
|
582
|
+
if (!region?.key || (region.kind !== 'module' && region.kind !== 'directory')) continue;
|
|
583
|
+
const r = built.absoluteRects.get(n.id);
|
|
584
|
+
if (!r) continue;
|
|
585
|
+
out.push({ kind: region.kind, key: region.key, x: r.x, y: r.y, w: r.width, h: r.height });
|
|
586
|
+
}
|
|
587
|
+
return out;
|
|
588
|
+
}, [built.nodes, built.absoluteRects]);
|
|
589
|
+
useEffect(() => {
|
|
590
|
+
if (pathFrameRects.length === 0) return;
|
|
591
|
+
let raf = 0;
|
|
592
|
+
let last: { x: number; y: number } | null = null;
|
|
593
|
+
const overCanvas = (e: MouseEvent): boolean =>
|
|
594
|
+
e.composedPath().some(
|
|
595
|
+
(el) => el instanceof HTMLElement && el.classList.contains(GRAPH_CANVAS_CLASS),
|
|
596
|
+
);
|
|
597
|
+
const onMove = (e: MouseEvent) => {
|
|
598
|
+
if (!overCanvas(e)) {
|
|
599
|
+
last = null;
|
|
600
|
+
setHoveredRegion((prev) => (prev == null ? prev : null));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
last = { x: e.clientX, y: e.clientY };
|
|
604
|
+
if (raf) return;
|
|
605
|
+
raf = window.requestAnimationFrame(() => {
|
|
606
|
+
raf = 0;
|
|
607
|
+
if (!last) return;
|
|
608
|
+
const p = screenToFlowPosition(last);
|
|
609
|
+
let best: { kind: string; key: string } | null = null;
|
|
610
|
+
let bestArea = Number.POSITIVE_INFINITY;
|
|
611
|
+
for (const f of pathFrameRects) {
|
|
612
|
+
if (p.x >= f.x && p.x <= f.x + f.w && p.y >= f.y && p.y <= f.y + f.h) {
|
|
613
|
+
const area = f.w * f.h;
|
|
614
|
+
if (area < bestArea) {
|
|
615
|
+
bestArea = area;
|
|
616
|
+
best = { kind: f.kind, key: f.key };
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
setHoveredRegion((prev) =>
|
|
621
|
+
prev?.kind === best?.kind && prev?.key === best?.key ? prev : best,
|
|
622
|
+
);
|
|
623
|
+
});
|
|
624
|
+
};
|
|
625
|
+
window.addEventListener('mousemove', onMove);
|
|
626
|
+
return () => {
|
|
627
|
+
window.removeEventListener('mousemove', onMove);
|
|
628
|
+
if (raf) cancelAnimationFrame(raf);
|
|
629
|
+
};
|
|
630
|
+
}, [pathFrameRects, screenToFlowPosition]);
|
|
562
631
|
// Walkthrough focus — selected flow (or step) is full strength; other
|
|
563
632
|
// opened-flow members stay visible but dimmed; everything else is hidden.
|
|
564
633
|
// Restored from the persisted view state when a `persistKey` is set.
|
|
@@ -586,27 +655,20 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
586
655
|
const [diagnosticsTab, setDiagnosticsTab] = useState<'issues' | 'agents'>(
|
|
587
656
|
() => persisted.diagnosticsTab ?? 'issues',
|
|
588
657
|
);
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
// visible; it reverts when the focus is undone (see `unfocusIssueTarget`) or
|
|
592
|
-
// when the diagnostics list closes. Null = use the host-pinned / tab view.
|
|
593
|
-
const [edgeViewOverride, setEdgeViewOverride] = useState<SubsystemEdgeView | null>(
|
|
594
|
-
null,
|
|
595
|
-
);
|
|
596
|
-
// One edge vocabulary at a time. When the caller doesn't pick, the sidebar's
|
|
597
|
-
// Files / Walkthroughs tab picks: Files draws topology relation edges,
|
|
658
|
+
// One edge source at a time. When the caller doesn't pick, the sidebar's
|
|
659
|
+
// Files / Walkthroughs tab picks: Files draws graphify static edges,
|
|
598
660
|
// Walkthroughs draws runtime hop edges. Without visible tabs (no walkthroughs,
|
|
599
|
-
// or a sidebar-less embed) fall back to
|
|
600
|
-
// to walkthrough edges rather than empty, everything else to relations.
|
|
661
|
+
// or a sidebar-less embed) fall back to walkthrough edges.
|
|
601
662
|
const sidebarTabsVisible = !hideSidebar && (walkthroughs?.length ?? 0) > 0;
|
|
602
663
|
const resolvedEdgeView: SubsystemEdgeView =
|
|
603
|
-
edgeViewOverride ??
|
|
604
664
|
edgeView ??
|
|
605
665
|
(sidebarTabsVisible
|
|
606
|
-
? (sidebarView === 'walkthroughs' ? 'walkthroughs' : '
|
|
607
|
-
:
|
|
666
|
+
? (sidebarView === 'walkthroughs' ? 'walkthroughs' : 'graphify')
|
|
667
|
+
: (walkthroughs?.length ?? 0) > 0
|
|
608
668
|
? 'walkthroughs'
|
|
609
|
-
:
|
|
669
|
+
: (graphifyRelations?.length ?? 0) > 0
|
|
670
|
+
? 'graphify'
|
|
671
|
+
: 'walkthroughs');
|
|
610
672
|
// Walkthrough flows the user has expanded (via the title row). Closed by
|
|
611
673
|
// default so a graph with several flows doesn't dump every step list at once.
|
|
612
674
|
// Restored from the persisted view state when a `persistKey` is set.
|
|
@@ -807,15 +869,13 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
807
869
|
// The pane stays hidden until Pass 2 completes. Key off layout-affecting
|
|
808
870
|
// fields only — declarationRef updates after verify must not re-run ELK.
|
|
809
871
|
const layoutKey = useMemo(
|
|
810
|
-
() => subsystemGraphLayoutKey({ components,
|
|
811
|
-
[components,
|
|
872
|
+
() => subsystemGraphLayoutKey({ components, walkthroughs, graphifyRelations }),
|
|
873
|
+
[components, walkthroughs, graphifyRelations],
|
|
812
874
|
);
|
|
813
875
|
const componentsRef = useRef(components);
|
|
814
|
-
const relationsRef = useRef(relations);
|
|
815
876
|
const walkthroughsRef = useRef(walkthroughs);
|
|
816
877
|
const graphifyRelationsRef = useRef(graphifyRelations);
|
|
817
878
|
componentsRef.current = components;
|
|
818
|
-
relationsRef.current = relations;
|
|
819
879
|
walkthroughsRef.current = walkthroughs;
|
|
820
880
|
graphifyRelationsRef.current = graphifyRelations;
|
|
821
881
|
|
|
@@ -836,13 +896,13 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
836
896
|
pass2GenRef.current += 1;
|
|
837
897
|
const doc = {
|
|
838
898
|
components: componentsRef.current,
|
|
839
|
-
relations: relationsRef.current,
|
|
840
899
|
walkthroughs: walkthroughsRef.current,
|
|
841
900
|
};
|
|
842
901
|
void buildSubsystemGraph(doc, {
|
|
843
902
|
maxNodeWidth,
|
|
844
903
|
showEdgeLabels,
|
|
845
904
|
showSingletonFrames,
|
|
905
|
+
moduleNesting,
|
|
846
906
|
graphifyRelations: graphifyRelationsRef.current,
|
|
847
907
|
orderByLine,
|
|
848
908
|
})
|
|
@@ -867,7 +927,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
867
927
|
setLayoutReady(true);
|
|
868
928
|
});
|
|
869
929
|
return () => { alive = false; };
|
|
870
|
-
}, [layoutKey, maxNodeWidth, showEdgeLabels]);
|
|
930
|
+
}, [layoutKey, maxNodeWidth, showEdgeLabels, moduleNesting]);
|
|
871
931
|
|
|
872
932
|
// Pass 2: once every leaf node has a measured dimension, re-run ELK.
|
|
873
933
|
// Group parents are sized by ELK, not measured — exclude them or pass 2
|
|
@@ -893,8 +953,8 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
893
953
|
const measuredHeights = new Map(leafNodes.map((n) => [n.id, dims.get(n.id)!.height]));
|
|
894
954
|
const gen = ++pass2GenRef.current;
|
|
895
955
|
void buildSubsystemGraph(
|
|
896
|
-
{ components,
|
|
897
|
-
{ maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, graphifyRelations, orderByLine },
|
|
956
|
+
{ components, walkthroughs },
|
|
957
|
+
{ maxNodeWidth, showEdgeLabels, measuredWidths, measuredHeights, showSingletonFrames, moduleNesting, graphifyRelations, orderByLine },
|
|
898
958
|
)
|
|
899
959
|
.then(({ nodes, edges: e, absoluteRects }) => {
|
|
900
960
|
if (gen !== pass2GenRef.current) return;
|
|
@@ -907,7 +967,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
907
967
|
// Reveal Pass 1 layout rather than leaving the cover up forever.
|
|
908
968
|
setLayoutReady(true);
|
|
909
969
|
});
|
|
910
|
-
}, [built.nodes, components,
|
|
970
|
+
}, [built.nodes, components, walkthroughs, graphifyRelations, orderByLine, maxNodeWidth, showEdgeLabels, moduleNesting]);
|
|
911
971
|
|
|
912
972
|
// After Pass 1 commits, try Pass 2 immediately with retained measurements.
|
|
913
973
|
// Same-id live updates often get no new React Flow `dimensions` events, so
|
|
@@ -991,7 +1051,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
991
1051
|
|
|
992
1052
|
// Resolve an issue's component target to the matching node. `id` is the
|
|
993
1053
|
// stable alias; `label` may be an alias, name, or symbol. Non-component
|
|
994
|
-
// targets (
|
|
1054
|
+
// targets (module / flow / repo) have no single node → null.
|
|
995
1055
|
const issueComponent = useCallback(
|
|
996
1056
|
(issue: SubsystemIssue): SubsystemComponent | null => {
|
|
997
1057
|
const target = issue.target;
|
|
@@ -1009,33 +1069,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1009
1069
|
[components],
|
|
1010
1070
|
);
|
|
1011
1071
|
|
|
1012
|
-
// Resolve a `relation` issue target to the display edge it flags. The target
|
|
1013
|
-
// names the two endpoints (`from → to`); match an edge joining both, in
|
|
1014
|
-
// either direction, preferring the target's `detail` mechanism when it names
|
|
1015
|
-
// one. Null when no display edge matches.
|
|
1016
|
-
const issueEdge = useCallback(
|
|
1017
|
-
(issue: SubsystemIssue): Edge | null => {
|
|
1018
|
-
const target = issue.target;
|
|
1019
|
-
if (target?.kind !== 'relation') return null;
|
|
1020
|
-
const parts = `${target.id ?? ''} → ${target.label}`
|
|
1021
|
-
.split('→')
|
|
1022
|
-
.map((p) => p.replace(/\(\)$/, '').trim())
|
|
1023
|
-
.filter(Boolean);
|
|
1024
|
-
const joins = (e: Edge): boolean =>
|
|
1025
|
-
parts.includes(e.source) && parts.includes(e.target);
|
|
1026
|
-
const matches = baseEdges.filter(joins);
|
|
1027
|
-
const byMechanism = target.detail
|
|
1028
|
-
? matches.filter(
|
|
1029
|
-
(e) =>
|
|
1030
|
-
(e.data as { mechanism?: string } | undefined)?.mechanism ===
|
|
1031
|
-
target.detail,
|
|
1032
|
-
)
|
|
1033
|
-
: matches;
|
|
1034
|
-
return byMechanism[0] ?? matches[0] ?? null;
|
|
1035
|
-
},
|
|
1036
|
-
[baseEdges],
|
|
1037
|
-
);
|
|
1038
|
-
|
|
1039
1072
|
// A `module` issue target names a boundary frame, so focusing the card frames
|
|
1040
1073
|
// that region on the canvas. Null when the target names no frame (a singleton
|
|
1041
1074
|
// dropped from the layout, or a region key nothing declares).
|
|
@@ -1160,7 +1193,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1160
1193
|
// the canvas keeps while everything else dims. Derived from each finding's
|
|
1161
1194
|
// target:
|
|
1162
1195
|
// component → that node
|
|
1163
|
-
// relation → the edge it flags (+ its two endpoint nodes)
|
|
1164
1196
|
// module → every component in that module
|
|
1165
1197
|
// walkthrough → the flow's nodes + its hop edges
|
|
1166
1198
|
// repo → every component of that repo
|
|
@@ -1180,20 +1212,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1180
1212
|
if (t.kind === 'component') {
|
|
1181
1213
|
const a = resolveAlias(t.id ?? t.label);
|
|
1182
1214
|
if (a) nodeIds.add(a);
|
|
1183
|
-
} else if (t.kind === 'relation') {
|
|
1184
|
-
// The target names the two endpoints (`from → to`); light the edge and
|
|
1185
|
-
// its endpoints. Falls back to endpoints alone if no edge matches.
|
|
1186
|
-
const edge = issueEdge(issue);
|
|
1187
|
-
if (edge) {
|
|
1188
|
-
edgeIds.add(edge.id);
|
|
1189
|
-
nodeIds.add(edge.source);
|
|
1190
|
-
nodeIds.add(edge.target);
|
|
1191
|
-
} else {
|
|
1192
|
-
for (const part of `${t.id ?? ''} → ${t.label}`.split('→')) {
|
|
1193
|
-
const a = resolveAlias(part);
|
|
1194
|
-
if (a) nodeIds.add(a);
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
1215
|
} else if (t.kind === 'module') {
|
|
1198
1216
|
const key = t.id ?? t.label;
|
|
1199
1217
|
const prefix = `${key.replace(/\/$/, '')}/`;
|
|
@@ -1232,7 +1250,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1232
1250
|
if (nodeIds.has(e.source) && nodeIds.has(e.target)) edgeIds.add(e.id);
|
|
1233
1251
|
}
|
|
1234
1252
|
return { nodeIds, edgeIds };
|
|
1235
|
-
}, [issues, expandedIssueCategories, components, walkthroughs,
|
|
1253
|
+
}, [issues, expandedIssueCategories, components, walkthroughs, resolveAlias]);
|
|
1236
1254
|
const issueFocusNodeIds = issueFocus?.nodeIds ?? null;
|
|
1237
1255
|
const issueFocusEdgeIds = issueFocus?.edgeIds ?? null;
|
|
1238
1256
|
|
|
@@ -1479,15 +1497,15 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1479
1497
|
};
|
|
1480
1498
|
};
|
|
1481
1499
|
// Edges outside the selected view are hidden entirely (labels included).
|
|
1482
|
-
// graphify-native edges belong to the
|
|
1500
|
+
// graphify-native edges belong to the graphify view (they are static
|
|
1483
1501
|
// topology, not runtime hops) regardless of their verb.
|
|
1484
1502
|
const edgeInView = (e: Edge): boolean => {
|
|
1485
1503
|
const d = e.data as
|
|
1486
1504
|
| { mechanism?: string; provenance?: SubsystemEdgeProvenance }
|
|
1487
1505
|
| undefined;
|
|
1488
1506
|
const mechanism = d?.mechanism ?? 'uses';
|
|
1489
|
-
if (resolvedEdgeView === '
|
|
1490
|
-
return d?.provenance === 'graphify'
|
|
1507
|
+
if (resolvedEdgeView === 'graphify') {
|
|
1508
|
+
return d?.provenance === 'graphify';
|
|
1491
1509
|
}
|
|
1492
1510
|
return isWalkthroughMechanism(mechanism);
|
|
1493
1511
|
};
|
|
@@ -1586,7 +1604,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1586
1604
|
setSelectedEdgeId(null);
|
|
1587
1605
|
setFocusedWalkthroughId(null);
|
|
1588
1606
|
setFocusedStepIndex(null);
|
|
1589
|
-
setEdgeViewOverride(null);
|
|
1590
1607
|
issueModuleFocusRef.current = null;
|
|
1591
1608
|
}, []);
|
|
1592
1609
|
|
|
@@ -1612,7 +1629,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
1612
1629
|
useEffect(() => {
|
|
1613
1630
|
if (!issuesActive) {
|
|
1614
1631
|
setExpandedIssueCategories([]);
|
|
1615
|
-
setEdgeViewOverride(null);
|
|
1616
1632
|
}
|
|
1617
1633
|
}, [issuesActive]);
|
|
1618
1634
|
const toggleIssues = () => {
|
|
@@ -2081,11 +2097,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2081
2097
|
if (playable.length === 0) return;
|
|
2082
2098
|
// Only run the cycles that can actually be shown in the current edge view.
|
|
2083
2099
|
const inView = playable.filter((tl) =>
|
|
2084
|
-
tl.steps.some((s) =>
|
|
2085
|
-
resolvedEdgeView === 'relations'
|
|
2086
|
-
? isRelationMechanism(s.mechanism)
|
|
2087
|
-
: isWalkthroughMechanism(s.mechanism),
|
|
2088
|
-
),
|
|
2100
|
+
tl.steps.some((s) => isWalkthroughMechanism(s.mechanism)),
|
|
2089
2101
|
);
|
|
2090
2102
|
if (inView.length === 0) return;
|
|
2091
2103
|
const cyc = inView;
|
|
@@ -2254,16 +2266,14 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2254
2266
|
);
|
|
2255
2267
|
|
|
2256
2268
|
// Issue click → select + frame the target on the canvas. Component targets
|
|
2257
|
-
// frame their node;
|
|
2258
|
-
//
|
|
2259
|
-
//
|
|
2260
|
-
//
|
|
2261
|
-
// still fires afterwards for its own detail.
|
|
2269
|
+
// frame their node; other kinds (module / flow / repo) have no single element
|
|
2270
|
+
// and just forward. Selections / focus that would hide the target are cleared
|
|
2271
|
+
// first, then the camera flies to it on the next frame. The host's
|
|
2272
|
+
// `onSelectIssue` still fires afterwards for its own detail.
|
|
2262
2273
|
const focusIssueTarget = useCallback(
|
|
2263
2274
|
(issue: SubsystemIssue) => {
|
|
2264
2275
|
const comp = issueComponent(issue);
|
|
2265
|
-
const
|
|
2266
|
-
const step = comp || edge ? null : issueStep(issue);
|
|
2276
|
+
const step = comp ? null : issueStep(issue);
|
|
2267
2277
|
if (comp) {
|
|
2268
2278
|
setSelected(comp);
|
|
2269
2279
|
setSelectedEdgeId(null);
|
|
@@ -2281,27 +2291,6 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2281
2291
|
maxZoom: 1.5,
|
|
2282
2292
|
});
|
|
2283
2293
|
});
|
|
2284
|
-
} else if (edge) {
|
|
2285
|
-
setSelected(null);
|
|
2286
|
-
setFocusedWalkthroughId(null);
|
|
2287
|
-
setFocusedStepIndex(null);
|
|
2288
|
-
setHoveredWalkthroughStep(null);
|
|
2289
|
-
setSelectedEdgeId(edge.id);
|
|
2290
|
-
issueModuleFocusRef.current = null;
|
|
2291
|
-
// Flip to the vocabulary this edge belongs to, so a hop flagged in the
|
|
2292
|
-
// relations view (or vice versa) is actually drawn while focused.
|
|
2293
|
-
const d = edge.data as
|
|
2294
|
-
| { mechanism?: string; provenance?: SubsystemEdgeProvenance }
|
|
2295
|
-
| undefined;
|
|
2296
|
-
const mechanism = d?.mechanism ?? 'uses';
|
|
2297
|
-
setEdgeViewOverride(
|
|
2298
|
-
d?.provenance === 'graphify' || isRelationMechanism(mechanism)
|
|
2299
|
-
? 'relations'
|
|
2300
|
-
: 'walkthroughs',
|
|
2301
|
-
);
|
|
2302
|
-
requestAnimationFrame(() => {
|
|
2303
|
-
fitFocusBounds(new Set([edge.id]));
|
|
2304
|
-
});
|
|
2305
2294
|
} else if (step) {
|
|
2306
2295
|
// A step finding focuses the step itself, through the same entry point
|
|
2307
2296
|
// the walkthrough panel uses — so the drawer, the step numbering, the
|
|
@@ -2329,14 +2318,12 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2329
2318
|
},
|
|
2330
2319
|
[
|
|
2331
2320
|
issueComponent,
|
|
2332
|
-
issueEdge,
|
|
2333
2321
|
issueStep,
|
|
2334
2322
|
issueModuleNodeId,
|
|
2335
2323
|
focusWalkthroughStep,
|
|
2336
2324
|
onSelect,
|
|
2337
2325
|
onSelectIssue,
|
|
2338
2326
|
fitView,
|
|
2339
|
-
fitFocusBounds,
|
|
2340
2327
|
fitNodeRects,
|
|
2341
2328
|
],
|
|
2342
2329
|
);
|
|
@@ -2355,14 +2342,9 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2355
2342
|
setSelected(null);
|
|
2356
2343
|
setSelectedEdgeId(null);
|
|
2357
2344
|
} else {
|
|
2358
|
-
const
|
|
2359
|
-
const
|
|
2360
|
-
|
|
2361
|
-
if (edge) {
|
|
2362
|
-
if (selectedEdgeIdRef.current !== edge.id) return;
|
|
2363
|
-
setSelectedEdgeId(null);
|
|
2364
|
-
setEdgeViewOverride(null);
|
|
2365
|
-
} else if (step) {
|
|
2345
|
+
const step = issueStep(issue);
|
|
2346
|
+
const moduleNodeId = step ? null : issueModuleNodeId(issue);
|
|
2347
|
+
if (step) {
|
|
2366
2348
|
// Only unwind a step focus this card still owns. A dim-mode graph
|
|
2367
2349
|
// keeps its step focus in hover state instead (owned by the pointer,
|
|
2368
2350
|
// not the card), and correctly leaves that alone here.
|
|
@@ -2388,7 +2370,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2388
2370
|
fitView({ padding: 0.1, duration: 300, minZoom: 0.05, maxZoom: 2 });
|
|
2389
2371
|
});
|
|
2390
2372
|
},
|
|
2391
|
-
[issueComponent,
|
|
2373
|
+
[issueComponent, issueStep, issueModuleNodeId, fitView],
|
|
2392
2374
|
);
|
|
2393
2375
|
|
|
2394
2376
|
// Construct tokens inside a walkthrough snippet. A step's line is an edge
|
|
@@ -2800,6 +2782,7 @@ function Inner({ components, relations, walkthroughs, graphifyRelations, orderBy
|
|
|
2800
2782
|
selectedFile={selected?.file ?? openFile}
|
|
2801
2783
|
hoveredFile={hoveredFile}
|
|
2802
2784
|
onSelectFile={onTreeSelectFile}
|
|
2785
|
+
hoveredFolder={hoveredBoundaryPath}
|
|
2803
2786
|
headerless={repoGroups.multiRepo}
|
|
2804
2787
|
/>
|
|
2805
2788
|
)}
|
|
@@ -3362,7 +3345,6 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3362
3345
|
|
|
3363
3346
|
const constructsOnly = isConstructsOnlyModel({
|
|
3364
3347
|
components: props.components,
|
|
3365
|
-
relations: props.relations,
|
|
3366
3348
|
walkthroughs: props.walkthroughs,
|
|
3367
3349
|
graphifyRelations: props.graphifyRelations,
|
|
3368
3350
|
});
|
|
@@ -18,19 +18,19 @@ export interface SubsystemFileTreeProps {
|
|
|
18
18
|
/** File to transiently highlight while its node is hovered on the graph;
|
|
19
19
|
* falls back to `selectedFile` when null. */
|
|
20
20
|
hoveredFile?: string | null;
|
|
21
|
+
/** Boundary path (folder or file) to transiently highlight — e.g. hovering a
|
|
22
|
+
* module/directory frame on the canvas. Matched with or without a trailing
|
|
23
|
+
* slash. Takes a back seat to `hoveredFile`. */
|
|
24
|
+
hoveredFolder?: string | null;
|
|
21
25
|
/** Called when a file row is clicked; upstream toggles the drawer. */
|
|
22
26
|
onSelectFile?: (file: string) => void;
|
|
23
|
-
/** Called on hover in/out of a file row — directories and non-file rows
|
|
24
|
-
* emit null. Pierre exposes no hover API, so this rides a shadow-DOM
|
|
25
|
-
* `mouseover` listener (see below). */
|
|
26
|
-
onHoverFile?: (file: string | null) => void;
|
|
27
27
|
/** Hide the built-in "Files N" header row — used when an upstream repo
|
|
28
28
|
* header already labels the tree. */
|
|
29
29
|
headerless?: boolean;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/** Fills its parent height by design — pin it with a sized flex container. */
|
|
33
|
-
export function SubsystemFileTree({ files, selectedFile, hoveredFile,
|
|
33
|
+
export function SubsystemFileTree({ files, selectedFile, hoveredFile, hoveredFolder, onSelectFile, headerless }: SubsystemFileTreeProps) {
|
|
34
34
|
const { theme } = useTheme();
|
|
35
35
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
36
36
|
const paths = useMemo(() => Array.from(new Set(files)).sort(), [files]);
|
|
@@ -42,8 +42,6 @@ export function SubsystemFileTree({ files, selectedFile, hoveredFile, onSelectFi
|
|
|
42
42
|
pathSetRef.current = pathSet;
|
|
43
43
|
const onSelectFileRef = useRef(onSelectFile);
|
|
44
44
|
onSelectFileRef.current = onSelectFile;
|
|
45
|
-
const onHoverFileRef = useRef(onHoverFile);
|
|
46
|
-
onHoverFileRef.current = onHoverFile;
|
|
47
45
|
const selectedFileRef = useRef<string | null>(selectedFile ?? null);
|
|
48
46
|
selectedFileRef.current = selectedFile ?? null;
|
|
49
47
|
// Suppresses onSelectFile while selection is driven programmatically from
|
|
@@ -96,59 +94,28 @@ export function SubsystemFileTree({ files, selectedFile, hoveredFile, onSelectFi
|
|
|
96
94
|
useEffect(() => {
|
|
97
95
|
hoveredRowRef.current?.style.removeProperty('background');
|
|
98
96
|
hoveredRowRef.current = null;
|
|
99
|
-
const target = hoveredFile ?? null;
|
|
100
|
-
if (!target
|
|
97
|
+
const target = hoveredFile ?? hoveredFolder ?? null;
|
|
98
|
+
if (!target) return;
|
|
99
|
+
// Files are keyed exact; folder rows may carry a trailing slash. Try both.
|
|
100
|
+
const candidates = hoveredFile ? [target] : [target, `${target}/`];
|
|
101
101
|
|
|
102
102
|
// Depth-first walk across shadow roots — handles nested custom elements.
|
|
103
103
|
const roots: Array<Document | ShadowRoot> = [document];
|
|
104
104
|
while (roots.length > 0) {
|
|
105
105
|
const root = roots.shift()!;
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
for (const candidate of candidates) {
|
|
107
|
+
const row = root.querySelector(`[data-item-path="${CSS.escape(candidate)}"]`);
|
|
108
|
+
if (row instanceof HTMLElement) {
|
|
109
|
+
row.style.background = 'var(--trees-theme-list-hover-bg, rgba(128, 128, 128, 0.25))';
|
|
110
|
+
hoveredRowRef.current = row;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
111
113
|
}
|
|
112
114
|
for (const el of Array.from(root.querySelectorAll('*'))) {
|
|
113
115
|
if (el.shadowRoot) roots.push(el.shadowRoot);
|
|
114
116
|
}
|
|
115
117
|
}
|
|
116
|
-
}, [hoveredFile]);
|
|
117
|
-
|
|
118
|
-
// Hover notifications. Pierre exposes no hover callback hook, but its rows
|
|
119
|
-
// carry `data-item-path` / `data-item-type` inside an OPEN shadow root and
|
|
120
|
-
// it binds no `mouseover`/`mouseleave` handlers, so native mouse events
|
|
121
|
-
// bubble out to this wrapper. Resolve the row via `composedPath()` (which
|
|
122
|
-
// crosses the shadow boundary even though `event.target` is retargeted to
|
|
123
|
-
// the host), then emit the repo-root-relative path. Directories emit null.
|
|
124
|
-
const lastHoveredRef = useRef<string | null>(null);
|
|
125
|
-
useEffect(() => {
|
|
126
|
-
const host = treeRef.current;
|
|
127
|
-
if (!host) return;
|
|
128
|
-
const resolveFile = (e: MouseEvent): string | null => {
|
|
129
|
-
for (const el of e.composedPath()) {
|
|
130
|
-
if (!(el instanceof HTMLElement) || el.dataset.type !== 'item') continue;
|
|
131
|
-
if (el.dataset.itemType === 'folder') return null;
|
|
132
|
-
const raw = el.dataset.itemPath ?? '';
|
|
133
|
-
const path = raw.endsWith('/') ? raw.slice(0, -1) : raw;
|
|
134
|
-
return pathSetRef.current.has(path) ? path : null;
|
|
135
|
-
}
|
|
136
|
-
return null;
|
|
137
|
-
};
|
|
138
|
-
const emit = (file: string | null) => {
|
|
139
|
-
if (lastHoveredRef.current === file) return;
|
|
140
|
-
lastHoveredRef.current = file;
|
|
141
|
-
onHoverFileRef.current?.(file);
|
|
142
|
-
};
|
|
143
|
-
const handleOver = (e: MouseEvent) => emit(resolveFile(e));
|
|
144
|
-
const handleLeave = () => emit(null);
|
|
145
|
-
host.addEventListener('mouseover', handleOver);
|
|
146
|
-
host.addEventListener('mouseleave', handleLeave);
|
|
147
|
-
return () => {
|
|
148
|
-
host.removeEventListener('mouseover', handleOver);
|
|
149
|
-
host.removeEventListener('mouseleave', handleLeave);
|
|
150
|
-
};
|
|
151
|
-
}, []);
|
|
118
|
+
}, [hoveredFile, hoveredFolder]);
|
|
152
119
|
|
|
153
120
|
// Re-scope the tree in place when the subsystem's file set changes.
|
|
154
121
|
useEffect(() => {
|