@principal-ai/subsystems-react 0.56.10 → 0.56.12
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/subsystem/DiagnosticToggle.d.ts.map +1 -1
- package/dist/subsystem/DiagnosticToggle.js +2 -1
- package/dist/subsystem/DiagnosticToggle.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +2 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +64 -27
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SubsystemFileTree.d.ts.map +1 -1
- package/dist/subsystem/SubsystemFileTree.js +2 -1
- package/dist/subsystem/SubsystemFileTree.js.map +1 -1
- package/dist/subsystem/TrailStepsNav.d.ts +3 -1
- package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
- package/dist/subsystem/TrailStepsNav.js +13 -10
- package/dist/subsystem/TrailStepsNav.js.map +1 -1
- package/dist/subsystem/TrailsPanel.d.ts +5 -3
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +44 -68
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/dist/subsystem/trailSidebarScope.d.ts +11 -0
- package/dist/subsystem/trailSidebarScope.d.ts.map +1 -0
- package/dist/subsystem/trailSidebarScope.js +47 -0
- package/dist/subsystem/trailSidebarScope.js.map +1 -0
- package/package.json +1 -1
- package/src/stories/Canvases/Subsystem/TrailSidebarViews.stories.tsx +68 -0
- package/src/subsystem/DiagnosticToggle.tsx +2 -1
- package/src/subsystem/SubsystemComponentGraph.sidebar.test.tsx +106 -5
- package/src/subsystem/SubsystemComponentGraph.tsx +87 -41
- package/src/subsystem/SubsystemFileTree.tsx +2 -1
- package/src/subsystem/TrailStepsNav.test.tsx +60 -0
- package/src/subsystem/TrailStepsNav.tsx +16 -10
- package/src/subsystem/TrailsPanel.test.tsx +99 -35
- package/src/subsystem/TrailsPanel.tsx +43 -86
- package/src/subsystem/trailSidebarScope.test.ts +89 -0
- package/src/subsystem/trailSidebarScope.ts +62 -0
|
@@ -82,7 +82,7 @@ import {
|
|
|
82
82
|
} from './IssueList';
|
|
83
83
|
import { GraphLayoutCover } from './GraphLayoutCover';
|
|
84
84
|
import { SubsystemFileTree } from './SubsystemFileTree';
|
|
85
|
-
import {
|
|
85
|
+
import { buildTrailSidebarScope, type TrailSidebarScope } from './trailSidebarScope';
|
|
86
86
|
import { GRAPH_CANVAS_CLASS, GRAPH_NAV_PROPS, GraphChrome, GraphLayerStyle, LAYOUT_FLIP_CLASS, LAYOUT_FLIP_EDGE_MS, LAYOUT_FLIP_NODE_MS } from './graphChrome';
|
|
87
87
|
import { ComponentDeclaration } from './ComponentDeclaration';
|
|
88
88
|
import type { ComponentVerificationState } from './ComponentDeclaration';
|
|
@@ -151,6 +151,7 @@ type DrawerTarget =
|
|
|
151
151
|
interface PersistedViewState {
|
|
152
152
|
expandedTrails?: string[];
|
|
153
153
|
focusedTrailId?: string | null;
|
|
154
|
+
sidebarTrailId?: string | null;
|
|
154
155
|
focusedStepIndex?: number | null;
|
|
155
156
|
/** Sidebar width moved to the shared GraphWithSidebar chrome (persisted
|
|
156
157
|
* under `principal.subsystems.sidebarWidth.<persistKey>`); reading the old
|
|
@@ -403,7 +404,8 @@ export interface SubsystemComponentGraphProps {
|
|
|
403
404
|
/**
|
|
404
405
|
* Verification issues for this graph (audit findings). When diagnostics are
|
|
405
406
|
* active the sidebar's bottom panel becomes the issue list instead of the
|
|
406
|
-
* file tree / trails.
|
|
407
|
+
* file tree / trails. Graph issue badges and issue-driven dimming are only
|
|
408
|
+
* shown in that mode.
|
|
407
409
|
*/
|
|
408
410
|
issues?: SubsystemIssue[];
|
|
409
411
|
/**
|
|
@@ -609,9 +611,11 @@ const TrailDrawerContent = memo(function TrailDrawerContent({
|
|
|
609
611
|
interface InnerProps extends SubsystemComponentGraphProps {
|
|
610
612
|
measured: { w: number; h: number } | null;
|
|
611
613
|
filesActive: boolean;
|
|
614
|
+
sidebarScope: TrailSidebarScope;
|
|
615
|
+
onSidebarTrailChange: (trailId: string | null) => void;
|
|
612
616
|
}
|
|
613
617
|
|
|
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) {
|
|
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, hideSidebar, trailStepMode = 'focus', autoPlayTrails = false, trailAutoPlayIntervalMs = TRAIL_PLAY_PAUSE_MS, zoomOnTrailFocus = true, trailFocusDurationMs = 300, graphTitle, showTrailTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, issues, showIssues, filesActive, sidebarScope, onSidebarTrailChange, focusIssueCategory, laneStatus, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileViewer, renderTrailViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, codeProcessKeys, hideDrawer = false, persistKey, liveEvents, agentsPanel }: InnerProps) {
|
|
615
619
|
const { theme } = useTheme();
|
|
616
620
|
const { fitView, fitBounds } = useReactFlow();
|
|
617
621
|
const viewport = useViewport();
|
|
@@ -1259,7 +1263,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1259
1263
|
// node says "diagnostics here" for every finding the sidebar lists.
|
|
1260
1264
|
const issueBadgeByAlias = useMemo(() => {
|
|
1261
1265
|
const map = new Map<string, SubsystemNodeIssue>();
|
|
1262
|
-
if (!issues?.length) return map;
|
|
1266
|
+
if (showIssues !== true || !issues?.length) return map;
|
|
1263
1267
|
for (const issue of issues) {
|
|
1264
1268
|
const rung = issueRung(issue.kind);
|
|
1265
1269
|
if (!rung && !ISSUE_KIND_ICON[issue.kind]) continue;
|
|
@@ -1294,7 +1298,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1294
1298
|
if (kinded) map.set(alias, { ...badge, kind: kinded.kind });
|
|
1295
1299
|
}
|
|
1296
1300
|
return map;
|
|
1297
|
-
}, [issues, issueComponent]);
|
|
1301
|
+
}, [showIssues, issues, issueComponent]);
|
|
1298
1302
|
|
|
1299
1303
|
// Per-frame diagnostics badge: the same fold, but for findings about a
|
|
1300
1304
|
// BOUNDARY rather than a construct. Keyed by the region's React Flow node
|
|
@@ -1305,7 +1309,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1305
1309
|
// borrowing its layer's icon would imply the frame is at fault for it.
|
|
1306
1310
|
const regionIssueByNodeId = useMemo(() => {
|
|
1307
1311
|
const map = new Map<string, SubsystemRegionIssue>();
|
|
1308
|
-
if (!issues?.length) return map;
|
|
1312
|
+
if (showIssues !== true || !issues?.length) return map;
|
|
1309
1313
|
for (const issue of issues) {
|
|
1310
1314
|
const target = issue.target;
|
|
1311
1315
|
if (!target || !ISSUE_KIND_ICON[issue.kind]) continue;
|
|
@@ -1326,7 +1330,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1326
1330
|
});
|
|
1327
1331
|
}
|
|
1328
1332
|
return map;
|
|
1329
|
-
}, [issues]);
|
|
1333
|
+
}, [showIssues, issues]);
|
|
1330
1334
|
|
|
1331
1335
|
// Per-edge step verification badge: maps edge IDs to step verification status.
|
|
1332
1336
|
// Keyed by the React Flow edge id (from → mechanism → to). An edge can have
|
|
@@ -1344,7 +1348,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1344
1348
|
step_span_stale: 1,
|
|
1345
1349
|
};
|
|
1346
1350
|
const map = new Map<string, { kind: StepIssueKind; severity: 'info' | 'error'; count: number }>();
|
|
1347
|
-
if (!issues?.length || !trails?.length) {
|
|
1351
|
+
if (showIssues !== true || !issues?.length || !trails?.length) {
|
|
1348
1352
|
return map;
|
|
1349
1353
|
}
|
|
1350
1354
|
for (const issue of issues) {
|
|
@@ -1371,7 +1375,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1371
1375
|
});
|
|
1372
1376
|
}
|
|
1373
1377
|
return map;
|
|
1374
|
-
}, [issues, trails, issueStep]);
|
|
1378
|
+
}, [showIssues, issues, trails, issueStep]);
|
|
1375
1379
|
|
|
1376
1380
|
// Expanded verification layers in the diagnostics list. When any layer with
|
|
1377
1381
|
// findings is expanded, the canvas dims everything that layer does not
|
|
@@ -1420,7 +1424,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1420
1424
|
// Returns null when nothing is expanded, no expanded layer has findings, or
|
|
1421
1425
|
// the findings implicate nothing — so no dimming is applied.
|
|
1422
1426
|
const issueFocus = useMemo(() => {
|
|
1423
|
-
if (!issues?.length || expandedIssueCategories.length === 0) return null;
|
|
1427
|
+
if (showIssues !== true || !issues?.length || expandedIssueCategories.length === 0) return null;
|
|
1424
1428
|
const active = new Set(expandedIssueCategories);
|
|
1425
1429
|
const activeIssues = issues.filter((i) => active.has(issueCategory(i)));
|
|
1426
1430
|
if (activeIssues.length === 0) return null;
|
|
@@ -1475,7 +1479,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1475
1479
|
if (nodeIds.has(e.source) && nodeIds.has(e.target)) edgeIds.add(e.id);
|
|
1476
1480
|
}
|
|
1477
1481
|
return { nodeIds, edgeIds };
|
|
1478
|
-
}, [issues, expandedIssueCategories, components, trails, resolveAlias]);
|
|
1482
|
+
}, [showIssues, issues, expandedIssueCategories, components, trails, resolveAlias]);
|
|
1479
1483
|
const issueFocusNodeIds = issueFocus?.nodeIds ?? null;
|
|
1480
1484
|
const issueFocusEdgeIds = issueFocus?.edgeIds ?? null;
|
|
1481
1485
|
|
|
@@ -1834,7 +1838,11 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
1834
1838
|
}, []);
|
|
1835
1839
|
|
|
1836
1840
|
const hasTrails = useMemo(() => (trails?.length ?? 0) > 0, [trails]);
|
|
1837
|
-
const treePaths
|
|
1841
|
+
const { treePaths, trail: sidebarTrail } = sidebarScope;
|
|
1842
|
+
const treeFilePaths = useMemo(() => treePaths.entries.map((entry) => entry.displayPath), [treePaths]);
|
|
1843
|
+
useEffect(() => {
|
|
1844
|
+
if (persistKey) writeViewState(persistKey, { sidebarTrailId: sidebarTrail?.id ?? null });
|
|
1845
|
+
}, [persistKey, sidebarTrail?.id]);
|
|
1838
1846
|
// Alias → component, so the Trails panel can show human hop names instead
|
|
1839
1847
|
// of model-local aliases (and the site/`via` distinction can read).
|
|
1840
1848
|
const componentByAlias = useMemo(
|
|
@@ -2123,13 +2131,14 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2123
2131
|
}
|
|
2124
2132
|
setExpandedTrails(new Set([tl.id]));
|
|
2125
2133
|
setLayoutTrailId(tl.id);
|
|
2134
|
+
onSidebarTrailChange(tl.id);
|
|
2126
2135
|
setFocusedStepIndex(null);
|
|
2127
2136
|
setFocusedTrailId(tl.id);
|
|
2128
2137
|
onTrailSelect?.({ trailId: tl.id });
|
|
2129
2138
|
fitFocusBounds(new Set(tl.steps.map((s) => trailStepGraphEdgeId(s))));
|
|
2130
2139
|
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
2131
2140
|
},
|
|
2132
|
-
[fitFocusBounds, trailStepMode, onTrailSelect],
|
|
2141
|
+
[fitFocusBounds, trailStepMode, onTrailSelect, onSidebarTrailChange],
|
|
2133
2142
|
);
|
|
2134
2143
|
|
|
2135
2144
|
const clearTrailFocus = useCallback(() => {
|
|
@@ -2139,25 +2148,25 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2139
2148
|
}
|
|
2140
2149
|
setExpandedTrails(new Set());
|
|
2141
2150
|
setLayoutTrailId(null);
|
|
2151
|
+
onSidebarTrailChange(null);
|
|
2142
2152
|
setFocusedTrailId(null);
|
|
2143
2153
|
setFocusedStepIndex(null);
|
|
2144
2154
|
setHoveredTrailStep(null);
|
|
2145
2155
|
setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
|
|
2146
2156
|
onTrailSelect?.(null);
|
|
2147
|
-
}, [onTrailSelect]);
|
|
2157
|
+
}, [onTrailSelect, onSidebarTrailChange]);
|
|
2148
2158
|
|
|
2149
|
-
//
|
|
2159
|
+
// Switching panels keeps the selected trail, but drops transient hover previews.
|
|
2150
2160
|
const filesWereActive = useRef(false);
|
|
2151
2161
|
useEffect(() => {
|
|
2152
2162
|
const enteringFiles = filesActive && !filesWereActive.current;
|
|
2153
2163
|
filesWereActive.current = filesActive;
|
|
2154
2164
|
if (!enteringFiles) return;
|
|
2155
|
-
clearTrailFocus();
|
|
2156
|
-
setExpandedTrails(new Set());
|
|
2165
|
+
if (!sidebarTrail) clearTrailFocus();
|
|
2157
2166
|
setSelectedEdgeId(null);
|
|
2158
|
-
|
|
2167
|
+
setHoveredTrailStep(null);
|
|
2159
2168
|
onTrailStepHover?.(null);
|
|
2160
|
-
}, [filesActive, clearTrailFocus, onTrailStepHover]);
|
|
2169
|
+
}, [filesActive, clearTrailFocus, onTrailStepHover, sidebarTrail]);
|
|
2161
2170
|
|
|
2162
2171
|
// Host deep-link: opening the graph from a trail row selects that
|
|
2163
2172
|
// flow — expand its steps and focus its edges.
|
|
@@ -2195,6 +2204,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2195
2204
|
return;
|
|
2196
2205
|
}
|
|
2197
2206
|
// Toggle: clicking the selected step unselects it (back to whole flow).
|
|
2207
|
+
onSidebarTrailChange(tl.id);
|
|
2198
2208
|
if (focusedTrailId === tl.id && focusedStepIndex === stepIndex) {
|
|
2199
2209
|
setFocusedStepIndex(null);
|
|
2200
2210
|
// Pointer is still over the row — keep hover preview so dimming doesn't
|
|
@@ -2250,6 +2260,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2250
2260
|
renderTrailViewer,
|
|
2251
2261
|
trailStepMode,
|
|
2252
2262
|
onTrailSelect,
|
|
2263
|
+
onSidebarTrailChange,
|
|
2253
2264
|
],
|
|
2254
2265
|
);
|
|
2255
2266
|
|
|
@@ -2705,7 +2716,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2705
2716
|
(description || filesActive || issuesActive || sidebarExtra || sidebarAfterDescription ||
|
|
2706
2717
|
hasTrails);
|
|
2707
2718
|
const sidebarBodyVisible = hasTrails || filesActive || issuesActive;
|
|
2708
|
-
const sidebarTabs: GraphSidebarTab[] | undefined = issuesActive
|
|
2719
|
+
const sidebarTabs: GraphSidebarTab[] | undefined = issuesActive && !sidebarTrail
|
|
2709
2720
|
? agentsPanel
|
|
2710
2721
|
? [
|
|
2711
2722
|
{ id: 'issues', label: 'Issues' },
|
|
@@ -2736,14 +2747,14 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2736
2747
|
</>
|
|
2737
2748
|
);
|
|
2738
2749
|
// Lower panel: diagnostics, the file tree, or the trail flow list.
|
|
2739
|
-
const
|
|
2740
|
-
diagnosticsTab === 'agents' && agentsPanel ? (
|
|
2750
|
+
const sidebarPanel = issuesActive ? (
|
|
2751
|
+
diagnosticsTab === 'agents' && agentsPanel && !sidebarTrail ? (
|
|
2741
2752
|
<SubsystemAgentsPanel {...agentsPanel} />
|
|
2742
2753
|
) : (
|
|
2743
2754
|
<SubsystemIssueList
|
|
2744
|
-
issues={issues
|
|
2755
|
+
issues={sidebarScope.issues}
|
|
2745
2756
|
focusCategory={focusIssueCategory}
|
|
2746
|
-
laneStatus={laneStatus}
|
|
2757
|
+
laneStatus={sidebarTrail ? undefined : laneStatus}
|
|
2747
2758
|
onSelectIssue={focusIssueTarget}
|
|
2748
2759
|
onDeselectIssue={unfocusIssueTarget}
|
|
2749
2760
|
onApplyFix={onApplyIssueFix}
|
|
@@ -2751,22 +2762,27 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2751
2762
|
onExpandedCategoriesChange={handleExpandedCategoriesChange}
|
|
2752
2763
|
/>
|
|
2753
2764
|
)
|
|
2754
|
-
) : filesActive ? (
|
|
2765
|
+
) : filesActive ? treePaths.entries.length === 0 ? (
|
|
2766
|
+
<div style={{ padding: 16, color: theme.colors.textSecondary }}>No files for this trail.</div>
|
|
2767
|
+
) : (
|
|
2755
2768
|
<SubsystemFileTree
|
|
2756
|
-
|
|
2769
|
+
headerless
|
|
2770
|
+
files={treeFilePaths}
|
|
2757
2771
|
selectedFile={openFile ? treePaths.toDisplay.get(openFile) : null}
|
|
2758
2772
|
onSelectFile={(path) => {
|
|
2759
2773
|
const file = treePaths.toFile.get(path);
|
|
2760
2774
|
if (file) onOpenDeclarationFile(file);
|
|
2761
2775
|
}}
|
|
2762
2776
|
/>
|
|
2763
|
-
) :
|
|
2777
|
+
) : null;
|
|
2778
|
+
const trailNavigation = trails && trails.length > 0 ? (
|
|
2764
2779
|
<TrailsPanel
|
|
2765
2780
|
drilldown={trailStepMode !== 'dim'}
|
|
2766
2781
|
trails={trails}
|
|
2767
2782
|
expandedTrails={expandedTrails}
|
|
2768
|
-
focusedTrailId={focusedTrailId}
|
|
2769
|
-
focusedStepIndex={focusedStepIndex}
|
|
2783
|
+
focusedTrailId={sidebarTrail?.id ?? focusedTrailId}
|
|
2784
|
+
focusedStepIndex={!sidebarTrail || focusedTrailId === sidebarTrail.id ? focusedStepIndex : null}
|
|
2785
|
+
detailContent={sidebarPanel}
|
|
2770
2786
|
hoveredTrailStep={hoveredTrailStep}
|
|
2771
2787
|
onToggleCollapsed={toggleTrailCollapsed}
|
|
2772
2788
|
onFocusFlow={focusTrailEdges}
|
|
@@ -2790,6 +2806,9 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
|
|
|
2790
2806
|
onRemoveStepEmphasis={onRemoveStepEmphasis}
|
|
2791
2807
|
/>
|
|
2792
2808
|
) : null;
|
|
2809
|
+
const sidebarBody = sidebarTrail && trailStepMode !== 'dim'
|
|
2810
|
+
? trailNavigation
|
|
2811
|
+
: sidebarPanel ?? trailNavigation;
|
|
2793
2812
|
const fileOverlayView = fileOverlay && (
|
|
2794
2813
|
<div
|
|
2795
2814
|
style={{
|
|
@@ -3385,20 +3404,43 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3385
3404
|
const wrapRef = useRef<HTMLDivElement>(null);
|
|
3386
3405
|
const [size, setSize] = useState<{ w: number; h: number } | null>(null);
|
|
3387
3406
|
const [filesOpen, setFilesOpen] = useState(false);
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3407
|
+
// Browsing a trail is independent of selecting one of its files or findings.
|
|
3408
|
+
const [sidebarTrailId, setSidebarTrailId] = useState<string | null>(() => {
|
|
3409
|
+
const saved = readViewState(props.persistKey);
|
|
3410
|
+
return saved.sidebarTrailId !== undefined ? saved.sidebarTrailId : saved.focusedTrailId ?? null;
|
|
3411
|
+
});
|
|
3412
|
+
const sidebarScope = useMemo(
|
|
3413
|
+
() => buildTrailSidebarScope(props.components, props.trails, props.issues, sidebarTrailId),
|
|
3414
|
+
[props.components, props.trails, props.issues, sidebarTrailId],
|
|
3391
3415
|
);
|
|
3392
|
-
const
|
|
3416
|
+
const fileCount = sidebarScope.treePaths.entries.length;
|
|
3417
|
+
const trailViewLabel = sidebarScope.trail ? 'Trail' : 'Trails';
|
|
3418
|
+
const trailCount = props.trails?.length ?? 0;
|
|
3419
|
+
const hasFiles = fileCount > 0 || sidebarScope.trail != null;
|
|
3420
|
+
const sidebarDiagnostic: SubsystemDiagnostic = sidebarScope.trail ? {
|
|
3421
|
+
...props.diagnostic,
|
|
3422
|
+
status: props.issues == null ? props.diagnostic?.status ?? 'unknown'
|
|
3423
|
+
: sidebarScope.issues.length > 0 ? 'issues' : 'ok',
|
|
3424
|
+
issueCount: sidebarScope.issues.length,
|
|
3425
|
+
fixableCount: sidebarScope.issues.filter((issue) => issue.fix != null).length,
|
|
3426
|
+
title: undefined,
|
|
3427
|
+
} : props.diagnostic ?? { status: 'unknown', issueCount: props.issues?.length };
|
|
3428
|
+
const hasIssues = (props.issues != null
|
|
3429
|
+
? sidebarScope.issues.length
|
|
3430
|
+
: sidebarDiagnostic.issueCount ?? 0) > 0;
|
|
3431
|
+
useEffect(() => {
|
|
3432
|
+
if (sidebarTrailId != null && !sidebarScope.trail) setSidebarTrailId(null);
|
|
3433
|
+
}, [sidebarTrailId, sidebarScope.trail]);
|
|
3393
3434
|
const [diagnosticsOpen, setDiagnosticsOpen] = useState<boolean>(
|
|
3394
3435
|
props.showIssues ?? (props.issues?.length ?? 0) > 0,
|
|
3395
3436
|
);
|
|
3396
|
-
const
|
|
3437
|
+
const issuesRequested = props.showIssues ?? diagnosticsOpen;
|
|
3438
|
+
const issuesActive = hasIssues && issuesRequested;
|
|
3397
3439
|
const filesActive = filesOpen && hasFiles && !issuesActive;
|
|
3398
3440
|
const selectSidebarView = (view: 'trails' | 'files' | 'issues') => {
|
|
3399
3441
|
const nextIssuesActive = view === 'issues';
|
|
3400
3442
|
setFilesOpen(view === 'files');
|
|
3401
|
-
if (nextIssuesActive !==
|
|
3443
|
+
if (nextIssuesActive !== issuesRequested) {
|
|
3402
3444
|
if (props.showIssues === undefined) setDiagnosticsOpen(nextIssuesActive);
|
|
3403
3445
|
props.diagnostic?.onToggle?.();
|
|
3404
3446
|
}
|
|
@@ -3409,7 +3451,7 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3409
3451
|
justifyContent: 'center',
|
|
3410
3452
|
gap: 4,
|
|
3411
3453
|
height: 28,
|
|
3412
|
-
padding: '4px
|
|
3454
|
+
padding: '4px 16px',
|
|
3413
3455
|
fontFamily: theme.fonts.monospace,
|
|
3414
3456
|
fontSize: theme.fontSizes[0],
|
|
3415
3457
|
border: 'none',
|
|
@@ -3500,13 +3542,14 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3500
3542
|
>
|
|
3501
3543
|
<button
|
|
3502
3544
|
type="button"
|
|
3503
|
-
aria-label=
|
|
3545
|
+
aria-label={sidebarScope.trail ? 'Trail' : `Trails (${trailCount} ${trailCount === 1 ? 'trail' : 'trails'})`}
|
|
3504
3546
|
aria-pressed={!filesActive && !issuesActive}
|
|
3505
3547
|
onClick={() => selectSidebarView('trails')}
|
|
3506
3548
|
style={viewButtonStyle(!filesActive && !issuesActive)}
|
|
3507
3549
|
>
|
|
3508
3550
|
<Route size={14} aria-hidden />
|
|
3509
|
-
<span>
|
|
3551
|
+
<span>{trailViewLabel}</span>
|
|
3552
|
+
{!sidebarScope.trail && <span style={{ fontWeight: 600, marginLeft: 4 }}>{trailCount}</span>}
|
|
3510
3553
|
</button>
|
|
3511
3554
|
{hasFiles && (
|
|
3512
3555
|
<button
|
|
@@ -3519,16 +3562,18 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3519
3562
|
>
|
|
3520
3563
|
<Files size={14} aria-hidden />
|
|
3521
3564
|
<span>Files</span>
|
|
3522
|
-
<span style={{ fontWeight: 600 }}>{fileCount}</span>
|
|
3565
|
+
<span style={{ fontWeight: 600, marginLeft: 4 }}>{fileCount}</span>
|
|
3523
3566
|
</button>
|
|
3524
3567
|
)}
|
|
3568
|
+
{hasIssues && (
|
|
3525
3569
|
<SubsystemDiagnosticToggle
|
|
3526
|
-
{...
|
|
3570
|
+
{...sidebarDiagnostic}
|
|
3527
3571
|
labelText="Issues"
|
|
3528
3572
|
segmented
|
|
3529
3573
|
active={issuesActive}
|
|
3530
3574
|
onToggle={() => selectSidebarView('issues')}
|
|
3531
3575
|
/>
|
|
3576
|
+
)}
|
|
3532
3577
|
</div>
|
|
3533
3578
|
</header>
|
|
3534
3579
|
)}
|
|
@@ -3552,7 +3597,8 @@ export function SubsystemComponentGraph(props: SubsystemComponentGraphProps) {
|
|
|
3552
3597
|
/>
|
|
3553
3598
|
) : (
|
|
3554
3599
|
<ReactFlowProvider>
|
|
3555
|
-
<Inner {...props} showIssues={issuesActive} filesActive={filesActive} measured={size}
|
|
3600
|
+
<Inner {...props} showIssues={issuesActive} filesActive={filesActive} measured={size}
|
|
3601
|
+
sidebarScope={sidebarScope} onSidebarTrailChange={setSidebarTrailId} />
|
|
3556
3602
|
</ReactFlowProvider>
|
|
3557
3603
|
)}
|
|
3558
3604
|
</div>
|
|
@@ -143,6 +143,7 @@ export function SubsystemFileTree({ files, selectedFile, hoveredFile, hoveredFol
|
|
|
143
143
|
ref={treeRef}
|
|
144
144
|
onClickCapture={handleContainerClick}
|
|
145
145
|
style={{
|
|
146
|
+
paddingTop: headerless ? 8 : undefined,
|
|
146
147
|
flex: 1,
|
|
147
148
|
minHeight: 0,
|
|
148
149
|
display: 'flex',
|
|
@@ -187,7 +188,7 @@ export function SubsystemFileTree({ files, selectedFile, hoveredFile, hoveredFol
|
|
|
187
188
|
'--trees-fg-muted-override': muted,
|
|
188
189
|
'--trees-accent-override': theme.colors.primary,
|
|
189
190
|
'--trees-font-family-override': theme.fonts.monospace,
|
|
190
|
-
'--trees-font-size-override': `${theme.fontSizes[
|
|
191
|
+
'--trees-font-size-override': `${theme.fontSizes[1]}px`,
|
|
191
192
|
'--trees-theme-list-hover-bg': theme.colors.border,
|
|
192
193
|
'--trees-theme-list-active-selection-bg': theme.colors.border,
|
|
193
194
|
} as CSSProperties
|
|
@@ -56,6 +56,61 @@ function renderNav(props: Record<string, unknown>) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
describe('TrailStepsNav', () => {
|
|
59
|
+
for (const title of ['Short flow', 'Generate the account report and deliver it to every subscribed recipient']) {
|
|
60
|
+
for (const selected of [false, true]) {
|
|
61
|
+
test(`keeps explicit title spacing in the ${selected ? 'selected header' : 'list'} for ${title}`, () => {
|
|
62
|
+
const { getByRole } = renderNav({
|
|
63
|
+
trails: [{ ...TRAILS[0], title }],
|
|
64
|
+
focusedTrailId: selected ? 't1' : null,
|
|
65
|
+
focusedStepIndex: null,
|
|
66
|
+
hoveredTrailStep: null,
|
|
67
|
+
onFocusFlow: () => {},
|
|
68
|
+
onClearFocus: () => {},
|
|
69
|
+
onFocusStep: () => {},
|
|
70
|
+
onHoverStep: () => {},
|
|
71
|
+
onHoverFlow: () => {},
|
|
72
|
+
onLeaveStep: () => {},
|
|
73
|
+
});
|
|
74
|
+
const button = getByRole('button', { name: selected ? title : new RegExp(title), exact: selected });
|
|
75
|
+
const row = selected ? button.parentElement! : button;
|
|
76
|
+
const label = selected ? button : button.children[1] as HTMLElement;
|
|
77
|
+
expect(row.style.paddingTop).toBe('14px');
|
|
78
|
+
expect(row.style.paddingBottom).toBe('14px');
|
|
79
|
+
expect(row.style.paddingLeft).toBe('12px');
|
|
80
|
+
expect(row.style.paddingRight).toBe(row.style.paddingLeft);
|
|
81
|
+
expect(row.style.alignItems).toBe('flex-start');
|
|
82
|
+
expect(label.style.lineHeight).toBe('20px');
|
|
83
|
+
expect((row.firstElementChild as SVGElement).style.marginTop).toBe('3px');
|
|
84
|
+
if (selected) {
|
|
85
|
+
expect(row.style.borderBottomStyle).toBe('');
|
|
86
|
+
expect(getByRole('button', { name: 'Back to trails' }).style.height).toBe('20px');
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
test('keeps the selected trail header when files or issues replace the step body', () => {
|
|
93
|
+
let cleared = false;
|
|
94
|
+
const { getByRole, getByText, queryByText } = renderNav({
|
|
95
|
+
trails: TRAILS,
|
|
96
|
+
focusedTrailId: 't1',
|
|
97
|
+
focusedStepIndex: null,
|
|
98
|
+
hoveredTrailStep: null,
|
|
99
|
+
onFocusFlow: () => {},
|
|
100
|
+
onClearFocus: () => { cleared = true; },
|
|
101
|
+
onFocusStep: () => {},
|
|
102
|
+
onHoverStep: () => {},
|
|
103
|
+
onHoverFlow: () => {},
|
|
104
|
+
onLeaveStep: () => {},
|
|
105
|
+
detailContent: createElement('div', null, 'Files for this trail'),
|
|
106
|
+
});
|
|
107
|
+
expect(getByRole('button', { name: 'First flow', exact: true })).toBeTruthy();
|
|
108
|
+
expect(getByText('Files for this trail')).toBeTruthy();
|
|
109
|
+
expect(queryByText('A.go')).toBeNull();
|
|
110
|
+
fireEvent.click(getByRole('button', { name: 'Back to trails' }));
|
|
111
|
+
expect(cleared).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
59
114
|
test('lists trails with step counts and focuses on click', () => {
|
|
60
115
|
let focused: string | null = null;
|
|
61
116
|
const { getByText, getByRole } = renderNav({
|
|
@@ -161,6 +216,11 @@ describe('TrailStepsNav', () => {
|
|
|
161
216
|
expect(getByText('B.go').style.fontFamily).toBe(defaultEditorTheme.fonts.body);
|
|
162
217
|
expect(getByRole('button', { name: /A\.go/ }).style.paddingLeft).toBe('12px');
|
|
163
218
|
expect(getByRole('button', { name: /B\.go/ }).style.paddingLeft).toBe('22px');
|
|
219
|
+
for (const name of [/A\.go/, /B\.go/]) {
|
|
220
|
+
const row = getByRole('button', { name });
|
|
221
|
+
expect(row.style.paddingTop).toBe('14px');
|
|
222
|
+
expect(row.style.paddingBottom).toBe('14px');
|
|
223
|
+
}
|
|
164
224
|
fireEvent.click(getByRole('button', { name: /B\.go/ }));
|
|
165
225
|
expect(focusedStep).toBe(1);
|
|
166
226
|
fireEvent.click(getByLabelText('Back to trails'));
|
|
@@ -54,6 +54,8 @@ export interface TrailStepsNavProps {
|
|
|
54
54
|
listContent?: ReactNode;
|
|
55
55
|
/** Optional step content, for hosts with copy and guidance controls. */
|
|
56
56
|
renderSteps?: (trail: SubsystemTrail) => ReactNode;
|
|
57
|
+
/** Replace the step body without replacing the selected trail's header. */
|
|
58
|
+
detailContent?: ReactNode;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
const PANE: CSSProperties = {
|
|
@@ -80,6 +82,7 @@ export function TrailStepsNav({
|
|
|
80
82
|
transitionMs = 340,
|
|
81
83
|
listContent,
|
|
82
84
|
renderSteps,
|
|
85
|
+
detailContent,
|
|
83
86
|
}: TrailStepsNavProps) {
|
|
84
87
|
const { theme } = useTheme();
|
|
85
88
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
@@ -149,11 +152,11 @@ export function TrailStepsNav({
|
|
|
149
152
|
onClick={() => onFocusFlow(trail)}
|
|
150
153
|
style={{
|
|
151
154
|
display: 'flex',
|
|
152
|
-
alignItems: '
|
|
155
|
+
alignItems: 'flex-start',
|
|
153
156
|
gap: 8,
|
|
154
157
|
width: '100%',
|
|
155
158
|
minWidth: 0,
|
|
156
|
-
padding: '
|
|
159
|
+
padding: '14px 12px',
|
|
157
160
|
border: 'none',
|
|
158
161
|
borderRadius: 0,
|
|
159
162
|
background: focusedRow || hovered ? hoverBg : 'transparent',
|
|
@@ -168,6 +171,7 @@ export function TrailStepsNav({
|
|
|
168
171
|
aria-hidden
|
|
169
172
|
style={{
|
|
170
173
|
flexShrink: 0,
|
|
174
|
+
marginTop: 3,
|
|
171
175
|
color: focusedRow || hovered ? theme.colors.primary : muted,
|
|
172
176
|
transition: 'color 120ms ease',
|
|
173
177
|
}}
|
|
@@ -180,6 +184,7 @@ export function TrailStepsNav({
|
|
|
180
184
|
fontSize: theme.fontSizes[1],
|
|
181
185
|
fontFamily: theme.fonts.monospace,
|
|
182
186
|
fontWeight: 600,
|
|
187
|
+
lineHeight: '20px',
|
|
183
188
|
color: theme.colors.text,
|
|
184
189
|
}}
|
|
185
190
|
>
|
|
@@ -189,6 +194,7 @@ export function TrailStepsNav({
|
|
|
189
194
|
style={{
|
|
190
195
|
flexShrink: 0,
|
|
191
196
|
fontSize: theme.fontSizes[0],
|
|
197
|
+
lineHeight: '20px',
|
|
192
198
|
fontFamily: theme.fonts.monospace,
|
|
193
199
|
color: muted,
|
|
194
200
|
}}
|
|
@@ -217,12 +223,11 @@ export function TrailStepsNav({
|
|
|
217
223
|
style={{
|
|
218
224
|
flexShrink: 0,
|
|
219
225
|
display: 'flex',
|
|
220
|
-
alignItems: '
|
|
226
|
+
alignItems: 'flex-start',
|
|
221
227
|
gap: 8,
|
|
222
228
|
// Left inset + leading cell + gap match the step rows below, so
|
|
223
229
|
// the trail icon and the step numbers share one column.
|
|
224
|
-
padding: '
|
|
225
|
-
borderBottom: `1px solid ${theme.colors.border}`,
|
|
230
|
+
padding: '14px 12px',
|
|
226
231
|
}}
|
|
227
232
|
>
|
|
228
233
|
<Route
|
|
@@ -231,7 +236,7 @@ export function TrailStepsNav({
|
|
|
231
236
|
aria-hidden
|
|
232
237
|
// The focused trail: the icon stays primary, matching the row's
|
|
233
238
|
// hover colour in the list it came from.
|
|
234
|
-
style={{ flexShrink: 0, color: theme.colors.primary }}
|
|
239
|
+
style={{ flexShrink: 0, marginTop: 3, color: theme.colors.primary }}
|
|
235
240
|
/>
|
|
236
241
|
{/* The title is the back affordance too: clicking it goes back to
|
|
237
242
|
the list, like the trailing close. */}
|
|
@@ -254,6 +259,7 @@ export function TrailStepsNav({
|
|
|
254
259
|
fontSize: theme.fontSizes[1],
|
|
255
260
|
fontFamily: theme.fonts.monospace,
|
|
256
261
|
fontWeight: 600,
|
|
262
|
+
lineHeight: '20px',
|
|
257
263
|
color: theme.colors.text,
|
|
258
264
|
}}
|
|
259
265
|
>
|
|
@@ -272,7 +278,7 @@ export function TrailStepsNav({
|
|
|
272
278
|
justifyContent: 'center',
|
|
273
279
|
flexShrink: 0,
|
|
274
280
|
width: 24,
|
|
275
|
-
height:
|
|
281
|
+
height: 20,
|
|
276
282
|
padding: 0,
|
|
277
283
|
border: 'none',
|
|
278
284
|
borderRadius: 4,
|
|
@@ -285,7 +291,7 @@ export function TrailStepsNav({
|
|
|
285
291
|
<X size={16} strokeWidth={2} />
|
|
286
292
|
</button>
|
|
287
293
|
</div>
|
|
288
|
-
<div
|
|
294
|
+
{detailContent ?? <div
|
|
289
295
|
style={{ flex: 1, minHeight: 0, overflowY: 'auto', padding: '0 0 12px' }}
|
|
290
296
|
onMouseLeave={onLeaveStep}
|
|
291
297
|
>
|
|
@@ -308,7 +314,7 @@ export function TrailStepsNav({
|
|
|
308
314
|
gap: 8,
|
|
309
315
|
width: '100%',
|
|
310
316
|
minWidth: 0,
|
|
311
|
-
padding: `
|
|
317
|
+
padding: `14px 16px 14px ${12 + Math.max(0, stackDepth - 1) * 10}px`,
|
|
312
318
|
border: 'none',
|
|
313
319
|
borderRadius: 0,
|
|
314
320
|
background: active || hovered ? hoverBg : 'transparent',
|
|
@@ -370,7 +376,7 @@ export function TrailStepsNav({
|
|
|
370
376
|
</button>
|
|
371
377
|
);
|
|
372
378
|
})}
|
|
373
|
-
</div>
|
|
379
|
+
</div>}
|
|
374
380
|
</>
|
|
375
381
|
)}
|
|
376
382
|
</div>
|