@principal-ai/subsystems-react 0.56.3 → 0.56.5
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 +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/subsystem/DecorationPanel.d.ts +37 -0
- package/dist/subsystem/DecorationPanel.d.ts.map +1 -0
- package/dist/subsystem/DecorationPanel.js +147 -0
- package/dist/subsystem/DecorationPanel.js.map +1 -0
- package/dist/subsystem/DiagnosticToggle.d.ts +7 -1
- package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -1
- package/dist/subsystem/DiagnosticToggle.js +8 -7
- package/dist/subsystem/DiagnosticToggle.js.map +1 -1
- package/dist/subsystem/GraphSidebar.d.ts +4 -2
- package/dist/subsystem/GraphSidebar.d.ts.map +1 -1
- package/dist/subsystem/GraphSidebar.js +3 -1
- package/dist/subsystem/GraphSidebar.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +10 -13
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +112 -272
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
- package/dist/subsystem/TrailStepsNav.js +6 -4
- package/dist/subsystem/TrailStepsNav.js.map +1 -1
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +14 -10
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/dist/subsystem/decoration.d.ts +112 -0
- package/dist/subsystem/decoration.d.ts.map +1 -0
- package/dist/subsystem/decoration.js +54 -0
- package/dist/subsystem/decoration.js.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +37 -0
- package/src/stories/Canvases/C4/C4ToComponentDrill.stories.tsx +38 -71
- package/src/stories/Canvases/Subsystem/GraphTitle.stories.tsx +1 -2
- package/src/stories/Canvases/Subsystem/StepEmphasis.stories.tsx +528 -0
- package/src/stories/Components/DecorationPanel.stories.tsx +160 -0
- package/src/subsystem/DecorationPanel.test.tsx +128 -0
- package/src/subsystem/DecorationPanel.tsx +334 -0
- package/src/subsystem/DiagnosticToggle.tsx +20 -5
- package/src/subsystem/GraphSidebar.test.tsx +18 -0
- package/src/subsystem/GraphSidebar.tsx +6 -1
- package/src/subsystem/SubsystemComponentGraph.sidebar.test.tsx +218 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +184 -414
- package/src/subsystem/TrailStepsNav.test.tsx +32 -0
- package/src/subsystem/TrailStepsNav.tsx +6 -4
- package/src/subsystem/TrailsPanel.test.tsx +20 -0
- package/src/subsystem/TrailsPanel.tsx +11 -1
- package/src/subsystem/decoration.test.ts +72 -0
- package/src/subsystem/decoration.ts +151 -0
- package/src/subsystem/stepEmphasis.test.ts +110 -0
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
19
|
-
import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
|
19
|
+
import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
|
20
20
|
import {
|
|
21
21
|
ReactFlow,
|
|
22
22
|
ReactFlowProvider,
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
applyNodeChanges,
|
|
33
33
|
} from '@xyflow/react';
|
|
34
34
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
35
|
-
import {
|
|
35
|
+
import { ArrowDown, ArrowRight, Files, Route, Unlink, X } from 'lucide-react';
|
|
36
36
|
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
37
37
|
import {
|
|
38
38
|
buildSubsystemGraph,
|
|
@@ -80,8 +80,9 @@ import {
|
|
|
80
80
|
type SubsystemIssue,
|
|
81
81
|
type SubsystemIssueCategory,
|
|
82
82
|
} from './IssueList';
|
|
83
|
-
import { SubsystemFileTree } from './SubsystemFileTree';
|
|
84
83
|
import { GraphLayoutCover } from './GraphLayoutCover';
|
|
84
|
+
import { SubsystemFileTree } from './SubsystemFileTree';
|
|
85
|
+
import { buildTreePathMapping } from './paths';
|
|
85
86
|
import { GRAPH_CANVAS_CLASS, GRAPH_NAV_PROPS, GraphChrome, GraphLayerStyle, LAYOUT_FLIP_CLASS, LAYOUT_FLIP_EDGE_MS, LAYOUT_FLIP_NODE_MS } from './graphChrome';
|
|
86
87
|
import { ComponentDeclaration } from './ComponentDeclaration';
|
|
87
88
|
import type { ComponentVerificationState } from './ComponentDeclaration';
|
|
@@ -89,9 +90,7 @@ import type { DeclarationSymbolRef, SymbolInspection } from './symbolRefs';
|
|
|
89
90
|
import { FileDrawer, FILE_DRAWER_HEIGHT_MS } from './FileDrawer';
|
|
90
91
|
import { GraphWithSidebar } from './GraphWithSidebar';
|
|
91
92
|
import type { GraphSidebarTab } from './GraphSidebar';
|
|
92
|
-
import { buildRepoGroups, repoAvatarUrl, type RepoGroup } from './paths';
|
|
93
93
|
import { TrailsPanel, TRAIL_PLAY_PAUSE_MS } from './TrailsPanel';
|
|
94
|
-
import { TrailStackBreadcrumb } from './TrailStackBreadcrumb';
|
|
95
94
|
import type { StepEmphasisMap } from './stepEmphasis';
|
|
96
95
|
import {
|
|
97
96
|
EDGE_LABEL_WIDTH,
|
|
@@ -145,7 +144,7 @@ type DrawerTarget =
|
|
|
145
144
|
/**
|
|
146
145
|
* Per-model UI state persisted to `localStorage`, keyed by `persistKey`.
|
|
147
146
|
* Stores the trail working set (which flows are expanded and which
|
|
148
|
-
* flow/step is selected) and which
|
|
147
|
+
* flow/step is selected) and which diagnostics tab is showing, so
|
|
149
148
|
* tabbing away from a model and back lands you where you left off. Transient
|
|
150
149
|
* things (hover, drag, camera) are not saved.
|
|
151
150
|
*/
|
|
@@ -157,7 +156,6 @@ interface PersistedViewState {
|
|
|
157
156
|
* under `principal.subsystems.sidebarWidth.<persistKey>`); reading the old
|
|
158
157
|
* field still restores a width saved before that move. */
|
|
159
158
|
sidebarWidth?: number;
|
|
160
|
-
sidebarView?: 'files' | 'trails';
|
|
161
159
|
diagnosticsTab?: 'issues' | 'agents';
|
|
162
160
|
/** Flow direction chosen with the canvas toggle, restored on the next visit. */
|
|
163
161
|
layoutDirection?: SubsystemLayoutDirection;
|
|
@@ -196,8 +194,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
196
194
|
components: SubsystemComponent[];
|
|
197
195
|
/**
|
|
198
196
|
* Ordered runtime trails — one trail per flow. When present the
|
|
199
|
-
* sidebar's bottom half
|
|
200
|
-
* the flows panel lists each trail's steps (`symbol` or `file:line`);
|
|
197
|
+
* sidebar's bottom half lists each trail's steps (`symbol` or `file:line`);
|
|
201
198
|
* clicking a flow row toggles its steps; clicking a step focuses that
|
|
202
199
|
* step's edge and (when `renderTrailViewer` is set) opens the code
|
|
203
200
|
* drawer on that flow's snippets. Opened flows stay on the canvas
|
|
@@ -322,15 +319,14 @@ export interface SubsystemComponentGraphProps {
|
|
|
322
319
|
* - `graphify`: graphify-native static edges (`imports`, `contains`, …)
|
|
323
320
|
* - `trails`: trail step edges (`calls`, `feeds`, …), including
|
|
324
321
|
* step numbers when a flow is focused/hovered
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* tabs, defaults to `trails`.
|
|
322
|
+
* Defaults to `trails` when trails exist, otherwise `graphify` when
|
|
323
|
+
* static relations exist.
|
|
328
324
|
*/
|
|
329
325
|
edgeView?: SubsystemEdgeView;
|
|
330
|
-
/** Subsystem title displayed
|
|
326
|
+
/** Subsystem title displayed above the sidebar and graph. */
|
|
331
327
|
title?: string;
|
|
332
328
|
/**
|
|
333
|
-
* Suppresses the sidebar entirely (title, description,
|
|
329
|
+
* Suppresses the header and sidebar entirely (title, description,
|
|
334
330
|
* trails) for graph-only embeds. Pair with `graphTitle` to keep the
|
|
335
331
|
* subsystem name visible as an overlay on the canvas.
|
|
336
332
|
*/
|
|
@@ -398,10 +394,10 @@ export interface SubsystemComponentGraphProps {
|
|
|
398
394
|
/** Rendered in the sidebar under the description (e.g. selection inspector). */
|
|
399
395
|
sidebarAfterDescription?: ReactNode;
|
|
400
396
|
/**
|
|
401
|
-
* Diagnostics status chip in the
|
|
402
|
-
*
|
|
397
|
+
* Diagnostics status chip in the full-width graph header.
|
|
398
|
+
* Shows the last verification pass's state as an icon color + count,
|
|
403
399
|
* and toggles the sidebar between the diagnostics list and the normal
|
|
404
|
-
*
|
|
400
|
+
* trails view. Omit to hide the chip.
|
|
405
401
|
*/
|
|
406
402
|
diagnostic?: SubsystemDiagnostic;
|
|
407
403
|
/**
|
|
@@ -412,7 +408,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
412
408
|
issues?: SubsystemIssue[];
|
|
413
409
|
/**
|
|
414
410
|
* Pin diagnostics mode (controlled). When omitted, the sidebar seeds
|
|
415
|
-
* diagnostics on iff `issues` is non-empty, and the
|
|
411
|
+
* diagnostics on iff `issues` is non-empty, and the header chip toggles it.
|
|
416
412
|
*/
|
|
417
413
|
showIssues?: boolean;
|
|
418
414
|
/**
|
|
@@ -463,8 +459,7 @@ export interface SubsystemComponentGraphProps {
|
|
|
463
459
|
*/
|
|
464
460
|
hideDrawer?: boolean;
|
|
465
461
|
/**
|
|
466
|
-
* Called when a
|
|
467
|
-
* path). The tree is derived from the components' `file` values.
|
|
462
|
+
* Called when a source file is opened (repo-root-relative path).
|
|
468
463
|
*/
|
|
469
464
|
onFileSelect?: (file: string) => void;
|
|
470
465
|
/** Live verification status for the selected component. */
|
|
@@ -613,11 +608,12 @@ const TrailDrawerContent = memo(function TrailDrawerContent({
|
|
|
613
608
|
|
|
614
609
|
interface InnerProps extends SubsystemComponentGraphProps {
|
|
615
610
|
measured: { w: number; h: number } | null;
|
|
611
|
+
filesActive: boolean;
|
|
616
612
|
}
|
|
617
613
|
|
|
618
|
-
function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirection: layoutDirectionProp, defaultLayoutDirection = 'horizontal', onLayoutDirectionChange, hideLayoutToggle = false, refocusOnLayoutFlip = true, initialTrailId, onTrailSelect, onTrailStepHover, onReorderTrails, stepEmphasis, onRemoveStepEmphasis, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, moduleNesting, edgeView,
|
|
614
|
+
function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirection: layoutDirectionProp, defaultLayoutDirection = 'horizontal', onLayoutDirectionChange, hideLayoutToggle = false, refocusOnLayoutFlip = true, initialTrailId, onTrailSelect, onTrailStepHover, onReorderTrails, stepEmphasis, onRemoveStepEmphasis, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showSingletonFrames = true, moduleNesting, edgeView, hideSidebar, trailStepMode = 'focus', autoPlayTrails = false, trailAutoPlayIntervalMs = TRAIL_PLAY_PAUSE_MS, zoomOnTrailFocus = true, trailFocusDurationMs = 300, graphTitle, showTrailTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, issues, showIssues, filesActive, focusIssueCategory, laneStatus, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileViewer, renderTrailViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, codeProcessKeys, hideDrawer = false, persistKey, liveEvents, agentsPanel }: InnerProps) {
|
|
619
615
|
const { theme } = useTheme();
|
|
620
|
-
const { fitView, fitBounds
|
|
616
|
+
const { fitView, fitBounds } = useReactFlow();
|
|
621
617
|
const viewport = useViewport();
|
|
622
618
|
// Restored once per mount from `localStorage` (see `readViewState`). Each
|
|
623
619
|
// graph is its own tab/mount, so `persistKey` is stable for a mount.
|
|
@@ -796,75 +792,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
796
792
|
drawerOpenRef.current = drawerTarget != null;
|
|
797
793
|
// Pending deferred fit after opening the drawer; cancelled on re-entry / unmount.
|
|
798
794
|
const pendingFocusFitRef = useRef<number | null>(null);
|
|
799
|
-
// Component the pointer is over (null on leave) → transient tree highlight.
|
|
800
|
-
const [hoveredComponentAlias, setHoveredComponentId] = useState<string | null>(null);
|
|
801
|
-
// Boundary frame hovered on the canvas → highlight the matching folder row in
|
|
802
|
-
// the sidebar file tree, so a frame's place in the repo tree is visible.
|
|
803
|
-
const [hoveredRegion, setHoveredRegion] = useState<{
|
|
804
|
-
kind: string;
|
|
805
|
-
key: string;
|
|
806
|
-
} | null>(null);
|
|
807
|
-
const hoveredBoundaryPath =
|
|
808
|
-
hoveredRegion && (hoveredRegion.kind === 'module' || hoveredRegion.kind === 'directory')
|
|
809
|
-
? hoveredRegion.key
|
|
810
|
-
: null;
|
|
811
|
-
// Path frames (module / directory) as absolute flow-coord rects, hit-tested
|
|
812
|
-
// on pointer move. A pointer hit-test keeps the frames' interior hoverable
|
|
813
|
-
// WITHOUT an overlay that would block edges / child nodes.
|
|
814
|
-
const pathFrameRects = useMemo(() => {
|
|
815
|
-
const out: Array<{ kind: string; key: string; x: number; y: number; w: number; h: number }> = [];
|
|
816
|
-
for (const n of built.nodes) {
|
|
817
|
-
if (n.type !== 'subsystem-group') continue;
|
|
818
|
-
const region = (n.data as { region?: { kind?: string; key?: string } } | undefined)?.region;
|
|
819
|
-
if (!region?.key || (region.kind !== 'module' && region.kind !== 'directory')) continue;
|
|
820
|
-
const r = built.absoluteRects.get(n.id);
|
|
821
|
-
if (!r) continue;
|
|
822
|
-
out.push({ kind: region.kind, key: region.key, x: r.x, y: r.y, w: r.width, h: r.height });
|
|
823
|
-
}
|
|
824
|
-
return out;
|
|
825
|
-
}, [built.nodes, built.absoluteRects]);
|
|
826
|
-
useEffect(() => {
|
|
827
|
-
if (pathFrameRects.length === 0) return;
|
|
828
|
-
let raf = 0;
|
|
829
|
-
let last: { x: number; y: number } | null = null;
|
|
830
|
-
const overCanvas = (e: MouseEvent): boolean =>
|
|
831
|
-
e.composedPath().some(
|
|
832
|
-
(el) => el instanceof HTMLElement && el.classList.contains(GRAPH_CANVAS_CLASS),
|
|
833
|
-
);
|
|
834
|
-
const onMove = (e: MouseEvent) => {
|
|
835
|
-
if (!overCanvas(e)) {
|
|
836
|
-
last = null;
|
|
837
|
-
setHoveredRegion((prev) => (prev == null ? prev : null));
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
last = { x: e.clientX, y: e.clientY };
|
|
841
|
-
if (raf) return;
|
|
842
|
-
raf = window.requestAnimationFrame(() => {
|
|
843
|
-
raf = 0;
|
|
844
|
-
if (!last) return;
|
|
845
|
-
const p = screenToFlowPosition(last);
|
|
846
|
-
let best: { kind: string; key: string } | null = null;
|
|
847
|
-
let bestArea = Number.POSITIVE_INFINITY;
|
|
848
|
-
for (const f of pathFrameRects) {
|
|
849
|
-
if (p.x >= f.x && p.x <= f.x + f.w && p.y >= f.y && p.y <= f.y + f.h) {
|
|
850
|
-
const area = f.w * f.h;
|
|
851
|
-
if (area < bestArea) {
|
|
852
|
-
bestArea = area;
|
|
853
|
-
best = { kind: f.kind, key: f.key };
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
setHoveredRegion((prev) =>
|
|
858
|
-
prev?.kind === best?.kind && prev?.key === best?.key ? prev : best,
|
|
859
|
-
);
|
|
860
|
-
});
|
|
861
|
-
};
|
|
862
|
-
window.addEventListener('mousemove', onMove);
|
|
863
|
-
return () => {
|
|
864
|
-
window.removeEventListener('mousemove', onMove);
|
|
865
|
-
if (raf) cancelAnimationFrame(raf);
|
|
866
|
-
};
|
|
867
|
-
}, [pathFrameRects, screenToFlowPosition]);
|
|
868
795
|
// Trail focus — selected flow (or step) is full strength; other
|
|
869
796
|
// opened-flow members stay visible but dimmed; everything else is hidden.
|
|
870
797
|
// Restored from the persisted view state when a `persistKey` is set.
|
|
@@ -884,49 +811,28 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
884
811
|
trailId: string;
|
|
885
812
|
stepIndex: number | null;
|
|
886
813
|
} | null>(null);
|
|
887
|
-
// Sidebar bottom half: which panel is shown when trails exist.
|
|
888
|
-
// The persisted value is membership-checked rather than trusted: a stale
|
|
889
|
-
// localStorage entry from before the walkthrough->trail rename would match
|
|
890
|
-
// neither panel and render a blank sidebar with no selected tab.
|
|
891
|
-
const [sidebarView, setSidebarView] = useState<'files' | 'trails'>(() => {
|
|
892
|
-
const stored = persisted.sidebarView;
|
|
893
|
-
const restored: 'files' | 'trails' =
|
|
894
|
-
stored === 'files' || stored === 'trails' ? stored : 'trails';
|
|
895
|
-
return trails?.length ? restored : 'files';
|
|
896
|
-
});
|
|
897
814
|
// Diagnostics area tab: issues list vs the Maintain agent pipeline.
|
|
898
815
|
const [diagnosticsTab, setDiagnosticsTab] = useState<'issues' | 'agents'>(
|
|
899
816
|
() => persisted.diagnosticsTab ?? 'issues',
|
|
900
817
|
);
|
|
901
|
-
// One edge source at a time.
|
|
902
|
-
//
|
|
903
|
-
// Trails draws runtime step edges. Without visible tabs (no trails,
|
|
904
|
-
// or a sidebar-less embed) fall back to trail edges.
|
|
905
|
-
const sidebarTabsVisible = !hideSidebar && (trails?.length ?? 0) > 0;
|
|
818
|
+
// One edge source at a time. Prefer runtime steps when trails exist;
|
|
819
|
+
// otherwise draw static relations, unless the caller picks explicitly.
|
|
906
820
|
// A solo trail layout only draws that flow's steps, so the trail edge source
|
|
907
821
|
// is the only one that can render anything.
|
|
908
822
|
const resolvedEdgeView: SubsystemEdgeView = committedTrailId
|
|
909
823
|
? 'trails'
|
|
910
824
|
: edgeView ??
|
|
911
|
-
(
|
|
912
|
-
?
|
|
913
|
-
: (
|
|
914
|
-
? '
|
|
915
|
-
:
|
|
916
|
-
? 'graphify'
|
|
917
|
-
: 'trails');
|
|
825
|
+
((trails?.length ?? 0) > 0
|
|
826
|
+
? 'trails'
|
|
827
|
+
: (graphifyRelations?.length ?? 0) > 0
|
|
828
|
+
? 'graphify'
|
|
829
|
+
: 'trails');
|
|
918
830
|
// Trail flows the user has expanded (via the title row). Closed by
|
|
919
831
|
// default so a graph with several flows doesn't dump every step list at once.
|
|
920
832
|
// Restored from the persisted view state when a `persistKey` is set.
|
|
921
833
|
const [expandedTrails, setExpandedTrails] = useState<Set<string>>(
|
|
922
834
|
() => new Set(persisted.expandedTrails ?? []),
|
|
923
835
|
);
|
|
924
|
-
// Sidebar description visibility. Hidden by default so the files/flows
|
|
925
|
-
// panel gets the vertical room; the title-row toggle reveals it.
|
|
926
|
-
const [descriptionVisible, setDescriptionVisible] = useState(false);
|
|
927
|
-
const [descToggleHover, setDescToggleHover] = useState(false);
|
|
928
|
-
// `true` only when a description exists AND the user opened it.
|
|
929
|
-
const showDesc = !!description && descriptionVisible;
|
|
930
836
|
// Sidebar width lives in the shared GraphWithSidebar chrome — draggable and
|
|
931
837
|
// persisted under `principal.subsystems.sidebarWidth.<persistKey>`. The graph
|
|
932
838
|
// caps it at half the view width so the canvas keeps room to breathe;
|
|
@@ -951,10 +857,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
951
857
|
if (!persistKey) return;
|
|
952
858
|
writeViewState(persistKey, { focusedStepIndex });
|
|
953
859
|
}, [persistKey, focusedStepIndex]);
|
|
954
|
-
useEffect(() => {
|
|
955
|
-
if (!persistKey) return;
|
|
956
|
-
writeViewState(persistKey, { sidebarView });
|
|
957
|
-
}, [persistKey, sidebarView]);
|
|
958
860
|
useEffect(() => {
|
|
959
861
|
if (!persistKey) return;
|
|
960
862
|
writeViewState(persistKey, { diagnosticsTab });
|
|
@@ -1282,12 +1184,10 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1282
1184
|
};
|
|
1283
1185
|
SUBSYSTEM_CALLBACKS.onEdgeSelect = selectEdge;
|
|
1284
1186
|
SUBSYSTEM_CALLBACKS.maxNodeWidth = maxNodeWidth;
|
|
1285
|
-
SUBSYSTEM_CALLBACKS.onHover = setHoveredComponentId;
|
|
1286
1187
|
return () => {
|
|
1287
1188
|
SUBSYSTEM_CALLBACKS.onSelect = undefined;
|
|
1288
1189
|
SUBSYSTEM_CALLBACKS.onEdgeSelect = undefined;
|
|
1289
1190
|
SUBSYSTEM_CALLBACKS.maxNodeWidth = undefined;
|
|
1290
|
-
SUBSYSTEM_CALLBACKS.onHover = undefined;
|
|
1291
1191
|
};
|
|
1292
1192
|
}, [components, onSelect, maxNodeWidth, selectEdge]);
|
|
1293
1193
|
|
|
@@ -1937,23 +1837,16 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1937
1837
|
issueModuleFocusRef.current = null;
|
|
1938
1838
|
}, []);
|
|
1939
1839
|
|
|
1940
|
-
// Sidebar file trees — one per repo on multi-repo graphs, each under its
|
|
1941
|
-
// own owner-avatar header. Clicking a header collapses that repo's tree.
|
|
1942
|
-
const repoGroups = useMemo(() => buildRepoGroups(components), [components]);
|
|
1943
1840
|
const hasTrails = useMemo(() => (trails?.length ?? 0) > 0, [trails]);
|
|
1841
|
+
const treePaths = useMemo(() => buildTreePathMapping(components), [components]);
|
|
1944
1842
|
// Alias → component, so the Trails panel can show human hop names instead
|
|
1945
1843
|
// of model-local aliases (and the site/`via` distinction can read).
|
|
1946
1844
|
const componentByAlias = useMemo(
|
|
1947
1845
|
() => new Map(components.map((c) => [c.alias, c])),
|
|
1948
1846
|
[components],
|
|
1949
1847
|
);
|
|
1950
|
-
//
|
|
1951
|
-
|
|
1952
|
-
const [diagnosticsOpen, setDiagnosticsOpen] = useState<boolean>(
|
|
1953
|
-
showIssues ?? (issues?.length ?? 0) > 0,
|
|
1954
|
-
);
|
|
1955
|
-
const controlledIssues = showIssues !== undefined;
|
|
1956
|
-
const issuesActive = controlledIssues ? showIssues : diagnosticsOpen;
|
|
1848
|
+
// The outer header owns the diagnostics toggle and passes its resolved state.
|
|
1849
|
+
const issuesActive = showIssues === true;
|
|
1957
1850
|
// Layer focus only applies while the diagnostics list is on screen — closing
|
|
1958
1851
|
// it clears any dimming the expanded layers were driving.
|
|
1959
1852
|
useEffect(() => {
|
|
@@ -1961,35 +1854,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1961
1854
|
setExpandedIssueCategories([]);
|
|
1962
1855
|
}
|
|
1963
1856
|
}, [issuesActive]);
|
|
1964
|
-
const toggleIssues = () => {
|
|
1965
|
-
if (!controlledIssues) setDiagnosticsOpen((v) => !v);
|
|
1966
|
-
diagnostic?.onToggle?.();
|
|
1967
|
-
};
|
|
1968
|
-
const [collapsedRepos, setCollapsedRepos] = useState<Set<string>>(new Set());
|
|
1969
|
-
const toggleRepoCollapsed = useCallback((key: string) => {
|
|
1970
|
-
setCollapsedRepos((prev) => {
|
|
1971
|
-
const next = new Set(prev);
|
|
1972
|
-
if (next.has(key)) next.delete(key);
|
|
1973
|
-
else next.add(key);
|
|
1974
|
-
return next;
|
|
1975
|
-
});
|
|
1976
|
-
}, []);
|
|
1977
|
-
const treeFiles = useMemo(
|
|
1978
|
-
() => repoGroups.groups.flatMap((g) => g.entries.map((e) => e.file)),
|
|
1979
|
-
[repoGroups],
|
|
1980
|
-
);
|
|
1981
|
-
const onTreeSelectFile = useCallback(
|
|
1982
|
-
(file: string) => {
|
|
1983
|
-
setFileOverlay(null);
|
|
1984
|
-
if (openFileRef.current?.file === file && openFileRef.current.startLine == null) {
|
|
1985
|
-
setDrawerTarget(null);
|
|
1986
|
-
return;
|
|
1987
|
-
}
|
|
1988
|
-
setDrawerTarget({ kind: 'file', file });
|
|
1989
|
-
onFileSelect?.(file);
|
|
1990
|
-
},
|
|
1991
|
-
[onFileSelect],
|
|
1992
|
-
);
|
|
1993
1857
|
|
|
1994
1858
|
const onOpenDeclarationFile = useCallback(
|
|
1995
1859
|
(file: string, opts?: SubsystemOpenFileOptions) => {
|
|
@@ -2278,9 +2142,22 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2278
2142
|
onTrailSelect?.(null);
|
|
2279
2143
|
}, [onTrailSelect]);
|
|
2280
2144
|
|
|
2145
|
+
// A hidden trail must not leave the file view filtered to its cast.
|
|
2146
|
+
const filesWereActive = useRef(false);
|
|
2147
|
+
useEffect(() => {
|
|
2148
|
+
const enteringFiles = filesActive && !filesWereActive.current;
|
|
2149
|
+
filesWereActive.current = filesActive;
|
|
2150
|
+
if (!enteringFiles) return;
|
|
2151
|
+
clearTrailFocus();
|
|
2152
|
+
setExpandedTrails(new Set());
|
|
2153
|
+
setSelectedEdgeId(null);
|
|
2154
|
+
setLayoutTrailId(null);
|
|
2155
|
+
onTrailStepHover?.(null);
|
|
2156
|
+
}, [filesActive, clearTrailFocus, onTrailStepHover]);
|
|
2157
|
+
|
|
2281
2158
|
// Host deep-link: opening the graph from a trail row selects that
|
|
2282
|
-
// flow — expand its steps
|
|
2283
|
-
//
|
|
2159
|
+
// flow — expand its steps and focus its edges.
|
|
2160
|
+
// Guarded by a ref so a later in-tab selection isn't yanked
|
|
2284
2161
|
// back; a remount (or a new id) re-applies it.
|
|
2285
2162
|
const appliedInitialTrailRef = useRef<string | null>(null);
|
|
2286
2163
|
useEffect(() => {
|
|
@@ -2290,26 +2167,10 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2290
2167
|
const tl = trails.find((t) => t.id === initialTrailId);
|
|
2291
2168
|
if (!tl) return;
|
|
2292
2169
|
appliedInitialTrailRef.current = initialTrailId;
|
|
2293
|
-
setSidebarView('trails');
|
|
2294
2170
|
setExpandedTrails((prev) => new Set(prev).add(tl.id));
|
|
2295
2171
|
focusTrailEdges(tl);
|
|
2296
2172
|
}, [initialTrailId, layoutReady, trails, focusTrailEdges]);
|
|
2297
2173
|
|
|
2298
|
-
// Switching sidebar panels also switches the edge vocabulary. Leaving the
|
|
2299
|
-
// Trails panel drops its canvas state (focus, expanded flows, selected
|
|
2300
|
-
// edge) so the Files view's topology edges aren't gated by a flow the user
|
|
2301
|
-
// can no longer see or un-dim.
|
|
2302
|
-
const changeSidebarView = useCallback(
|
|
2303
|
-
(view: 'files' | 'trails') => {
|
|
2304
|
-
setSidebarView(view);
|
|
2305
|
-
if (view === 'trails') return;
|
|
2306
|
-
clearTrailFocus();
|
|
2307
|
-
setExpandedTrails(new Set());
|
|
2308
|
-
setSelectedEdgeId(null);
|
|
2309
|
-
},
|
|
2310
|
-
[clearTrailFocus],
|
|
2311
|
-
);
|
|
2312
|
-
|
|
2313
2174
|
// Focus a single step's edge on the canvas and open/scroll the trail
|
|
2314
2175
|
// drawer to that step's snippet. Clicking the already-focused step clears
|
|
2315
2176
|
// step focus and returns to whole-flow (high-level) framing.
|
|
@@ -2810,15 +2671,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2810
2671
|
return labels;
|
|
2811
2672
|
}, [dispEdges, selectedFlowStepNos, stepIssueBadgeByEdgeId]);
|
|
2812
2673
|
|
|
2813
|
-
// Unique source files across components → sidebar file trees.
|
|
2814
|
-
const treeFilePaths = treeFiles;
|
|
2815
|
-
|
|
2816
|
-
// The hovered node's file (null when not hovering / file-less component).
|
|
2817
|
-
const hoveredFile = useMemo(() => {
|
|
2818
|
-
if (!hoveredComponentAlias) return null;
|
|
2819
|
-
return components.find((c) => c.alias === hoveredComponentAlias)?.file ?? null;
|
|
2820
|
-
}, [components, hoveredComponentAlias]);
|
|
2821
|
-
|
|
2822
2674
|
// Drawer content renderer. Path-keyed only — the component-keyed
|
|
2823
2675
|
// `renderFileView` variant was removed; nothing in-repo passed it.
|
|
2824
2676
|
const fileViewer = renderFileViewer;
|
|
@@ -2839,16 +2691,16 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2839
2691
|
[],
|
|
2840
2692
|
);
|
|
2841
2693
|
|
|
2842
|
-
// Sidebar chrome —
|
|
2843
|
-
//
|
|
2694
|
+
// Sidebar chrome — description header, files/trails, and optional Issues/Agents
|
|
2695
|
+
// diagnostics. Everything renders through the shared
|
|
2844
2696
|
// GraphWithSidebar; the graph keeps owning every bit of *state* (focused
|
|
2845
|
-
// trail,
|
|
2697
|
+
// trail, diagnostics tab) because the canvas dims from
|
|
2846
2698
|
// the same facts the sidebar renders.
|
|
2847
2699
|
const sidebarVisible =
|
|
2848
2700
|
!hideSidebar &&
|
|
2849
|
-
(
|
|
2850
|
-
|
|
2851
|
-
const sidebarBodyVisible =
|
|
2701
|
+
(description || filesActive || issuesActive || sidebarExtra || sidebarAfterDescription ||
|
|
2702
|
+
hasTrails);
|
|
2703
|
+
const sidebarBodyVisible = hasTrails || filesActive || issuesActive;
|
|
2852
2704
|
const sidebarTabs: GraphSidebarTab[] | undefined = issuesActive
|
|
2853
2705
|
? agentsPanel
|
|
2854
2706
|
? [
|
|
@@ -2856,85 +2708,12 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2856
2708
|
{ id: 'agents', label: 'Agents' },
|
|
2857
2709
|
]
|
|
2858
2710
|
: undefined
|
|
2859
|
-
:
|
|
2860
|
-
|
|
2861
|
-
{ id: 'trails', label: 'Trails' },
|
|
2862
|
-
{ id: 'files', label: 'Files' },
|
|
2863
|
-
]
|
|
2864
|
-
: undefined;
|
|
2865
|
-
const activeSidebarTab = issuesActive ? diagnosticsTab : sidebarView;
|
|
2866
|
-
// Header block: the extras, then the title/actions row (diagnostic chip +
|
|
2867
|
-
// description toggle), then the description that toggles out from under it.
|
|
2711
|
+
: undefined;
|
|
2712
|
+
// Header block: extras, then the description.
|
|
2868
2713
|
const sidebarHeader = (
|
|
2869
2714
|
<>
|
|
2870
2715
|
{sidebarExtra}
|
|
2871
|
-
{
|
|
2872
|
-
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }}>
|
|
2873
|
-
{title && (
|
|
2874
|
-
<h2
|
|
2875
|
-
style={{
|
|
2876
|
-
margin: 0,
|
|
2877
|
-
flex: 1,
|
|
2878
|
-
minWidth: 0,
|
|
2879
|
-
fontSize: theme.fontSizes[2],
|
|
2880
|
-
fontWeight: 600,
|
|
2881
|
-
color: theme.colors.text,
|
|
2882
|
-
fontFamily: theme.fonts.monospace,
|
|
2883
|
-
}}
|
|
2884
|
-
>
|
|
2885
|
-
{title}
|
|
2886
|
-
</h2>
|
|
2887
|
-
)}
|
|
2888
|
-
<div
|
|
2889
|
-
style={{
|
|
2890
|
-
display: 'flex',
|
|
2891
|
-
alignItems: 'center',
|
|
2892
|
-
gap: 4,
|
|
2893
|
-
flexShrink: 0,
|
|
2894
|
-
// Title's flex:1 already pushes this cluster right; without a
|
|
2895
|
-
// title, keep the actions flush right instead of flush-left.
|
|
2896
|
-
marginLeft: title ? undefined : 'auto',
|
|
2897
|
-
}}
|
|
2898
|
-
>
|
|
2899
|
-
{diagnostic && (
|
|
2900
|
-
<SubsystemDiagnosticToggle
|
|
2901
|
-
{...diagnostic}
|
|
2902
|
-
active={issuesActive}
|
|
2903
|
-
onToggle={toggleIssues}
|
|
2904
|
-
/>
|
|
2905
|
-
)}
|
|
2906
|
-
{description && (
|
|
2907
|
-
<button
|
|
2908
|
-
type="button"
|
|
2909
|
-
aria-expanded={descriptionVisible}
|
|
2910
|
-
aria-label={descriptionVisible ? 'Hide description' : 'Show description'}
|
|
2911
|
-
title={descriptionVisible ? 'Hide description' : 'Show description'}
|
|
2912
|
-
onMouseEnter={() => setDescToggleHover(true)}
|
|
2913
|
-
onMouseLeave={() => setDescToggleHover(false)}
|
|
2914
|
-
onClick={() => setDescriptionVisible((v) => !v)}
|
|
2915
|
-
style={{
|
|
2916
|
-
display: 'inline-flex',
|
|
2917
|
-
alignItems: 'center',
|
|
2918
|
-
justifyContent: 'center',
|
|
2919
|
-
flexShrink: 0,
|
|
2920
|
-
width: 22,
|
|
2921
|
-
height: 22,
|
|
2922
|
-
padding: 0,
|
|
2923
|
-
border: 'none',
|
|
2924
|
-
borderRadius: 4,
|
|
2925
|
-
background: descriptionVisible || descToggleHover ? theme.colors.border : 'transparent',
|
|
2926
|
-
color: descriptionVisible || descToggleHover ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
|
|
2927
|
-
cursor: 'pointer',
|
|
2928
|
-
transition: 'background 120ms ease, color 120ms ease',
|
|
2929
|
-
}}
|
|
2930
|
-
>
|
|
2931
|
-
<FileText size={14} />
|
|
2932
|
-
</button>
|
|
2933
|
-
)}
|
|
2934
|
-
</div>
|
|
2935
|
-
</div>
|
|
2936
|
-
)}
|
|
2937
|
-
{description && descriptionVisible && (
|
|
2716
|
+
{description && (
|
|
2938
2717
|
<div style={{ fontSize: theme.fontSizes[0], lineHeight: 1.5 }}>
|
|
2939
2718
|
<IndustryMarkdownSlide
|
|
2940
2719
|
content={description}
|
|
@@ -2952,8 +2731,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2952
2731
|
{sidebarAfterDescription}
|
|
2953
2732
|
</>
|
|
2954
2733
|
);
|
|
2955
|
-
// Lower panel: the
|
|
2956
|
-
// stack with the trail flow list / repo file tree.
|
|
2734
|
+
// Lower panel: diagnostics, the file tree, or the trail flow list.
|
|
2957
2735
|
const sidebarBody = issuesActive ? (
|
|
2958
2736
|
diagnosticsTab === 'agents' && agentsPanel ? (
|
|
2959
2737
|
<SubsystemAgentsPanel {...agentsPanel} />
|
|
@@ -2969,7 +2747,16 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2969
2747
|
onExpandedCategoriesChange={handleExpandedCategoriesChange}
|
|
2970
2748
|
/>
|
|
2971
2749
|
)
|
|
2972
|
-
) :
|
|
2750
|
+
) : filesActive ? (
|
|
2751
|
+
<SubsystemFileTree
|
|
2752
|
+
files={treePaths.entries.map((entry) => entry.displayPath)}
|
|
2753
|
+
selectedFile={openFile ? treePaths.toDisplay.get(openFile) : null}
|
|
2754
|
+
onSelectFile={(path) => {
|
|
2755
|
+
const file = treePaths.toFile.get(path);
|
|
2756
|
+
if (file) onOpenDeclarationFile(file);
|
|
2757
|
+
}}
|
|
2758
|
+
/>
|
|
2759
|
+
) : trails && trails.length > 0 ? (
|
|
2973
2760
|
<TrailsPanel
|
|
2974
2761
|
trails={trails}
|
|
2975
2762
|
expandedTrails={expandedTrails}
|
|
@@ -3005,43 +2792,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
3005
2792
|
layoutFocusedTrailId={layoutTrailId}
|
|
3006
2793
|
onFocusLayout={toggleLayoutFocus}
|
|
3007
2794
|
/>
|
|
3008
|
-
) : treeFilePaths.length > 0 ? (
|
|
3009
|
-
<>
|
|
3010
|
-
{repoGroups.groups.map((group, i) => {
|
|
3011
|
-
const groupKey = group.repoKey ?? '__no-repo__';
|
|
3012
|
-
const collapsed = collapsedRepos.has(groupKey);
|
|
3013
|
-
return (
|
|
3014
|
-
<div
|
|
3015
|
-
key={groupKey}
|
|
3016
|
-
style={{
|
|
3017
|
-
flex: collapsed ? '0 0 auto' : 1,
|
|
3018
|
-
minHeight: collapsed ? 0 : undefined,
|
|
3019
|
-
display: 'flex',
|
|
3020
|
-
flexDirection: 'column',
|
|
3021
|
-
borderTop: i > 0 ? `1px solid ${theme.colors.border}` : undefined,
|
|
3022
|
-
}}
|
|
3023
|
-
>
|
|
3024
|
-
{repoGroups.multiRepo && (
|
|
3025
|
-
<RepoGroupHeader
|
|
3026
|
-
group={group}
|
|
3027
|
-
collapsed={collapsed}
|
|
3028
|
-
onToggle={() => toggleRepoCollapsed(groupKey)}
|
|
3029
|
-
/>
|
|
3030
|
-
)}
|
|
3031
|
-
{!collapsed && (
|
|
3032
|
-
<SubsystemFileTree
|
|
3033
|
-
files={group.entries.map((e) => e.displayPath)}
|
|
3034
|
-
selectedFile={selected?.file ?? openFile}
|
|
3035
|
-
hoveredFile={hoveredFile}
|
|
3036
|
-
onSelectFile={onTreeSelectFile}
|
|
3037
|
-
hoveredFolder={hoveredBoundaryPath}
|
|
3038
|
-
headerless={repoGroups.multiRepo}
|
|
3039
|
-
/>
|
|
3040
|
-
)}
|
|
3041
|
-
</div>
|
|
3042
|
-
);
|
|
3043
|
-
})}
|
|
3044
|
-
</>
|
|
3045
2795
|
) : null;
|
|
3046
2796
|
const fileOverlayView = fileOverlay && (
|
|
3047
2797
|
<div
|
|
@@ -3098,15 +2848,11 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
3098
2848
|
<GraphWithSidebar
|
|
3099
2849
|
hidden={!sidebarVisible}
|
|
3100
2850
|
bodyHidden={!sidebarBodyVisible}
|
|
3101
|
-
|
|
2851
|
+
headerMaxHeight={sidebarBodyVisible ? '30%' : '100%'}
|
|
3102
2852
|
header={sidebarHeader}
|
|
3103
2853
|
tabs={sidebarTabs}
|
|
3104
|
-
activeTab={
|
|
3105
|
-
onTabChange={(id) =>
|
|
3106
|
-
issuesActive
|
|
3107
|
-
? setDiagnosticsTab(id as 'issues' | 'agents')
|
|
3108
|
-
: changeSidebarView(id as 'files' | 'trails')
|
|
3109
|
-
}
|
|
2854
|
+
activeTab={diagnosticsTab}
|
|
2855
|
+
onTabChange={(id) => setDiagnosticsTab(id as 'issues' | 'agents')}
|
|
3110
2856
|
sidebarBody={sidebarBody}
|
|
3111
2857
|
tabListLabel={issuesActive ? 'Diagnostics view' : 'Sidebar view'}
|
|
3112
2858
|
defaultWidth={persisted.sidebarWidth ?? 450}
|
|
@@ -3603,18 +3349,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
3603
3349
|
suppressEscape={fileOverlayOpen}
|
|
3604
3350
|
hidden={hideDrawer}
|
|
3605
3351
|
progress={drawerProgress}
|
|
3606
|
-
subheader={
|
|
3607
|
-
drawerTarget?.kind === 'trail' &&
|
|
3608
|
-
focusedTrail &&
|
|
3609
|
-
drawerTarget.stepIndex != null ? (
|
|
3610
|
-
<TrailStackBreadcrumb
|
|
3611
|
-
trail={focusedTrail}
|
|
3612
|
-
stepIndex={drawerTarget.stepIndex}
|
|
3613
|
-
componentByAlias={componentByAlias}
|
|
3614
|
-
onFocusStep={(index) => focusTrailStep(focusedTrail, index)}
|
|
3615
|
-
/>
|
|
3616
|
-
) : null
|
|
3617
|
-
}
|
|
3618
3352
|
>
|
|
3619
3353
|
{drawerTarget?.kind === 'trail' &&
|
|
3620
3354
|
focusedTrail &&
|
|
@@ -3649,88 +3383,48 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
3649
3383
|
);
|
|
3650
3384
|
}
|
|
3651
3385
|
|
|
3652
|
-
/** Sidebar header for one repo's tree: owner avatar + repo name + file count.
|
|
3653
|
-
* Clicking toggles the tree's visibility. */
|
|
3654
|
-
function RepoGroupHeader({
|
|
3655
|
-
group,
|
|
3656
|
-
collapsed,
|
|
3657
|
-
onToggle,
|
|
3658
|
-
}: {
|
|
3659
|
-
group: RepoGroup;
|
|
3660
|
-
collapsed: boolean;
|
|
3661
|
-
onToggle: () => void;
|
|
3662
|
-
}) {
|
|
3663
|
-
const { theme } = useTheme();
|
|
3664
|
-
const avatar = repoAvatarUrl(group.repoKey);
|
|
3665
|
-
const label = group.repo ?? 'No repo';
|
|
3666
|
-
return (
|
|
3667
|
-
<div
|
|
3668
|
-
role="button"
|
|
3669
|
-
aria-expanded={!collapsed}
|
|
3670
|
-
onClick={(e) => {
|
|
3671
|
-
e.stopPropagation();
|
|
3672
|
-
onToggle();
|
|
3673
|
-
}}
|
|
3674
|
-
title={collapsed ? 'Show files' : 'Hide files'}
|
|
3675
|
-
style={{
|
|
3676
|
-
display: 'flex',
|
|
3677
|
-
alignItems: 'center',
|
|
3678
|
-
gap: 8,
|
|
3679
|
-
padding: '10px 16px 4px',
|
|
3680
|
-
flexShrink: 0,
|
|
3681
|
-
cursor: 'pointer',
|
|
3682
|
-
userSelect: 'none',
|
|
3683
|
-
}}
|
|
3684
|
-
>
|
|
3685
|
-
{avatar ? (
|
|
3686
|
-
<img
|
|
3687
|
-
src={avatar}
|
|
3688
|
-
alt=""
|
|
3689
|
-
width={16}
|
|
3690
|
-
height={16}
|
|
3691
|
-
loading="lazy"
|
|
3692
|
-
style={{ borderRadius: 4, flexShrink: 0, background: theme.colors.border }}
|
|
3693
|
-
/>
|
|
3694
|
-
) : group.owner ? (
|
|
3695
|
-
<span
|
|
3696
|
-
style={{
|
|
3697
|
-
width: 16,
|
|
3698
|
-
height: 16,
|
|
3699
|
-
borderRadius: 4,
|
|
3700
|
-
flexShrink: 0,
|
|
3701
|
-
display: 'inline-flex',
|
|
3702
|
-
alignItems: 'center',
|
|
3703
|
-
justifyContent: 'center',
|
|
3704
|
-
fontSize: 9,
|
|
3705
|
-
fontWeight: 700,
|
|
3706
|
-
fontFamily: theme.fonts.monospace,
|
|
3707
|
-
color: theme.colors.text,
|
|
3708
|
-
background: theme.colors.border,
|
|
3709
|
-
}}
|
|
3710
|
-
>
|
|
3711
|
-
{group.owner.charAt(0).toUpperCase()}
|
|
3712
|
-
</span>
|
|
3713
|
-
) : null}
|
|
3714
|
-
<span
|
|
3715
|
-
style={{
|
|
3716
|
-
fontSize: theme.fontSizes[1],
|
|
3717
|
-
fontFamily: theme.fonts.monospace,
|
|
3718
|
-
color: theme.colors.text,
|
|
3719
|
-
fontWeight: 600,
|
|
3720
|
-
overflow: 'hidden',
|
|
3721
|
-
textOverflow: 'ellipsis',
|
|
3722
|
-
whiteSpace: 'nowrap',
|
|
3723
|
-
}}
|
|
3724
|
-
>
|
|
3725
|
-
{label}
|
|
3726
|
-
</span>
|
|
3727
|
-
</div>
|
|
3728
|
-
);
|
|
3729
|
-
}
|
|
3730
|
-
|
|
3731
3386
|
export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
3387
|
+
const { theme } = useTheme();
|
|
3732
3388
|
const wrapRef = useRef<HTMLDivElement>(null);
|
|
3733
3389
|
const [size, setSize] = useState<{ w: number; h: number } | null>(null);
|
|
3390
|
+
const [filesOpen, setFilesOpen] = useState(false);
|
|
3391
|
+
const fileCount = useMemo(
|
|
3392
|
+
() => buildTreePathMapping(props.components).entries.length,
|
|
3393
|
+
[props.components],
|
|
3394
|
+
);
|
|
3395
|
+
const hasFiles = fileCount > 0;
|
|
3396
|
+
const [diagnosticsOpen, setDiagnosticsOpen] = useState<boolean>(
|
|
3397
|
+
props.showIssues ?? (props.issues?.length ?? 0) > 0,
|
|
3398
|
+
);
|
|
3399
|
+
const issuesActive = props.showIssues ?? diagnosticsOpen;
|
|
3400
|
+
const filesActive = filesOpen && hasFiles && !issuesActive;
|
|
3401
|
+
const selectSidebarView = (view: 'trails' | 'files' | 'issues') => {
|
|
3402
|
+
const nextIssuesActive = view === 'issues';
|
|
3403
|
+
setFilesOpen(view === 'files');
|
|
3404
|
+
if (nextIssuesActive !== issuesActive) {
|
|
3405
|
+
if (props.showIssues === undefined) setDiagnosticsOpen(nextIssuesActive);
|
|
3406
|
+
props.diagnostic?.onToggle?.();
|
|
3407
|
+
}
|
|
3408
|
+
};
|
|
3409
|
+
const viewButtonStyle = (active: boolean): CSSProperties => ({
|
|
3410
|
+
display: 'inline-flex',
|
|
3411
|
+
alignItems: 'center',
|
|
3412
|
+
justifyContent: 'center',
|
|
3413
|
+
gap: 4,
|
|
3414
|
+
height: 28,
|
|
3415
|
+
padding: '4px 7px',
|
|
3416
|
+
fontFamily: theme.fonts.monospace,
|
|
3417
|
+
fontSize: theme.fontSizes[0],
|
|
3418
|
+
border: 'none',
|
|
3419
|
+
borderRadius: 4,
|
|
3420
|
+
background: active ? theme.colors.border : 'transparent',
|
|
3421
|
+
color: active ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
|
|
3422
|
+
cursor: 'pointer',
|
|
3423
|
+
});
|
|
3424
|
+
|
|
3425
|
+
useEffect(() => {
|
|
3426
|
+
if (props.initialTrailId) setFilesOpen(false);
|
|
3427
|
+
}, [props.initialTrailId]);
|
|
3734
3428
|
|
|
3735
3429
|
// Measure the container so React Flow always has a real, non-zero pixel size
|
|
3736
3430
|
// (responsive to the parent; avoids the %→0 blank canvas).
|
|
@@ -3755,7 +3449,6 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3755
3449
|
|
|
3756
3450
|
return (
|
|
3757
3451
|
<div
|
|
3758
|
-
ref={wrapRef}
|
|
3759
3452
|
style={{
|
|
3760
3453
|
position: 'relative',
|
|
3761
3454
|
width: '100%',
|
|
@@ -3765,7 +3458,84 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3765
3458
|
flexDirection: 'column',
|
|
3766
3459
|
}}
|
|
3767
3460
|
>
|
|
3768
|
-
|
|
3461
|
+
{!constructsOnly && !props.hideSidebar && (
|
|
3462
|
+
<header
|
|
3463
|
+
style={{
|
|
3464
|
+
flexShrink: 0,
|
|
3465
|
+
display: 'flex',
|
|
3466
|
+
alignItems: 'center',
|
|
3467
|
+
gap: 12,
|
|
3468
|
+
padding: '12px 16px',
|
|
3469
|
+
background: theme.colors.background,
|
|
3470
|
+
borderBottom: `1px solid ${theme.colors.border}`,
|
|
3471
|
+
}}
|
|
3472
|
+
>
|
|
3473
|
+
{props.title && (
|
|
3474
|
+
<h2
|
|
3475
|
+
style={{
|
|
3476
|
+
margin: 0,
|
|
3477
|
+
minWidth: 0,
|
|
3478
|
+
fontSize: theme.fontSizes[2],
|
|
3479
|
+
fontWeight: 600,
|
|
3480
|
+
color: theme.colors.text,
|
|
3481
|
+
fontFamily: theme.fonts.monospace,
|
|
3482
|
+
overflowWrap: 'anywhere',
|
|
3483
|
+
}}
|
|
3484
|
+
>
|
|
3485
|
+
{props.title}
|
|
3486
|
+
</h2>
|
|
3487
|
+
)}
|
|
3488
|
+
<div
|
|
3489
|
+
role="group"
|
|
3490
|
+
aria-label="Sidebar view"
|
|
3491
|
+
style={{
|
|
3492
|
+
marginLeft: 'auto',
|
|
3493
|
+
flexShrink: 0,
|
|
3494
|
+
display: 'grid',
|
|
3495
|
+
gridAutoFlow: 'column',
|
|
3496
|
+
gridAutoColumns: '1fr',
|
|
3497
|
+
alignItems: 'center',
|
|
3498
|
+
gap: 2,
|
|
3499
|
+
padding: 2,
|
|
3500
|
+
border: `1px solid ${theme.colors.border}`,
|
|
3501
|
+
borderRadius: 6,
|
|
3502
|
+
}}
|
|
3503
|
+
>
|
|
3504
|
+
<button
|
|
3505
|
+
type="button"
|
|
3506
|
+
aria-label="Trails"
|
|
3507
|
+
aria-pressed={!filesActive && !issuesActive}
|
|
3508
|
+
onClick={() => selectSidebarView('trails')}
|
|
3509
|
+
style={viewButtonStyle(!filesActive && !issuesActive)}
|
|
3510
|
+
>
|
|
3511
|
+
<Route size={14} aria-hidden />
|
|
3512
|
+
<span>Trails</span>
|
|
3513
|
+
</button>
|
|
3514
|
+
{hasFiles && (
|
|
3515
|
+
<button
|
|
3516
|
+
type="button"
|
|
3517
|
+
aria-label={`Files (${fileCount} files)`}
|
|
3518
|
+
title="Show files"
|
|
3519
|
+
aria-pressed={filesActive}
|
|
3520
|
+
onClick={() => selectSidebarView('files')}
|
|
3521
|
+
style={viewButtonStyle(filesActive)}
|
|
3522
|
+
>
|
|
3523
|
+
<Files size={14} aria-hidden />
|
|
3524
|
+
<span>Files</span>
|
|
3525
|
+
<span style={{ fontWeight: 600 }}>{fileCount}</span>
|
|
3526
|
+
</button>
|
|
3527
|
+
)}
|
|
3528
|
+
<SubsystemDiagnosticToggle
|
|
3529
|
+
{...(props.diagnostic ?? { status: 'unknown', issueCount: props.issues?.length })}
|
|
3530
|
+
labelText="Issues"
|
|
3531
|
+
segmented
|
|
3532
|
+
active={issuesActive}
|
|
3533
|
+
onToggle={() => selectSidebarView('issues')}
|
|
3534
|
+
/>
|
|
3535
|
+
</div>
|
|
3536
|
+
</header>
|
|
3537
|
+
)}
|
|
3538
|
+
<div ref={wrapRef} style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
|
3769
3539
|
{constructsOnly ? (
|
|
3770
3540
|
<ConstructsCatalog
|
|
3771
3541
|
components={props.components}
|
|
@@ -3785,7 +3555,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3785
3555
|
/>
|
|
3786
3556
|
) : (
|
|
3787
3557
|
<ReactFlowProvider>
|
|
3788
|
-
<Inner {...props} measured={size} />
|
|
3558
|
+
<Inner {...props} showIssues={issuesActive} filesActive={filesActive} measured={size} />
|
|
3789
3559
|
</ReactFlowProvider>
|
|
3790
3560
|
)}
|
|
3791
3561
|
</div>
|