@principal-ai/subsystems-react 0.56.11 → 0.56.13

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.
Files changed (38) hide show
  1. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  2. package/dist/subsystem/C4Graph.js +10 -4
  3. package/dist/subsystem/C4Graph.js.map +1 -1
  4. package/dist/subsystem/DescriptionOverlay.d.ts +6 -0
  5. package/dist/subsystem/DescriptionOverlay.d.ts.map +1 -0
  6. package/dist/subsystem/DescriptionOverlay.js +58 -0
  7. package/dist/subsystem/DescriptionOverlay.js.map +1 -0
  8. package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -1
  9. package/dist/subsystem/DiagnosticToggle.js +2 -1
  10. package/dist/subsystem/DiagnosticToggle.js.map +1 -1
  11. package/dist/subsystem/SubsystemComponentGraph.d.ts +2 -1
  12. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  13. package/dist/subsystem/SubsystemComponentGraph.js +307 -284
  14. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  15. package/dist/subsystem/SubsystemFileTree.d.ts.map +1 -1
  16. package/dist/subsystem/SubsystemFileTree.js +2 -1
  17. package/dist/subsystem/SubsystemFileTree.js.map +1 -1
  18. package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
  19. package/dist/subsystem/TrailStepsNav.js +11 -8
  20. package/dist/subsystem/TrailStepsNav.js.map +1 -1
  21. package/dist/subsystem/TrailsPanel.d.ts +1 -1
  22. package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
  23. package/dist/subsystem/TrailsPanel.js +42 -66
  24. package/dist/subsystem/TrailsPanel.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/stories/Canvases/C4/C4Draft.stories.tsx +238 -0
  27. package/src/stories/Canvases/Subsystem/TrailSidebarViews.stories.tsx +2 -2
  28. package/src/subsystem/C4Graph.tsx +10 -4
  29. package/src/subsystem/DescriptionOverlay.test.tsx +76 -0
  30. package/src/subsystem/DescriptionOverlay.tsx +101 -0
  31. package/src/subsystem/DiagnosticToggle.tsx +2 -1
  32. package/src/subsystem/SubsystemComponentGraph.sidebar.test.tsx +81 -10
  33. package/src/subsystem/SubsystemComponentGraph.tsx +68 -26
  34. package/src/subsystem/SubsystemFileTree.tsx +2 -1
  35. package/src/subsystem/TrailStepsNav.test.tsx +38 -0
  36. package/src/subsystem/TrailStepsNav.tsx +11 -8
  37. package/src/subsystem/TrailsPanel.test.tsx +99 -35
  38. package/src/subsystem/TrailsPanel.tsx +38 -85
@@ -34,6 +34,7 @@ import { GRAPH_CANVAS_CLASS, GRAPH_NAV_PROPS, GraphChrome, GraphLayerStyle, LAYO
34
34
  import { ComponentDeclaration } from './ComponentDeclaration';
35
35
  import { FileDrawer, FILE_DRAWER_HEIGHT_MS } from './FileDrawer';
36
36
  import { GraphWithSidebar } from './GraphWithSidebar';
37
+ import { DescriptionOverlay } from './DescriptionOverlay';
37
38
  import { TrailsPanel, TRAIL_PLAY_PAUSE_MS } from './TrailsPanel';
38
39
  import { EDGE_LABEL_WIDTH, EDGE_LABEL_HEIGHT, EDGE_LABEL_FONT_SIZE, EDGE_LABEL_CLOUD_PATH, EDGE_LABEL_CLOUD_EXTRA_TOP, } from '../utils/edgeLabel';
39
40
  /** Identifiers a token could match to name this component as a construct. */
@@ -130,6 +131,7 @@ const TrailDrawerContent = memo(function TrailDrawerContent({ render, trail, ste
130
131
  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 }) {
131
132
  const { theme } = useTheme();
132
133
  const { fitView, fitBounds } = useReactFlow();
134
+ const [descriptionOpen, setDescriptionOpen] = useState(false);
133
135
  const viewport = useViewport();
134
136
  // Restored once per mount from `localStorage` (see `readViewState`). Each
135
137
  // graph is its own tab/mount, so `persistKey` is stable for a mount.
@@ -725,7 +727,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
725
727
  // node says "diagnostics here" for every finding the sidebar lists.
726
728
  const issueBadgeByAlias = useMemo(() => {
727
729
  const map = new Map();
728
- if (!issues?.length)
730
+ if (showIssues !== true || !issues?.length)
729
731
  return map;
730
732
  for (const issue of issues) {
731
733
  const rung = issueRung(issue.kind);
@@ -760,7 +762,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
760
762
  map.set(alias, { ...badge, kind: kinded.kind });
761
763
  }
762
764
  return map;
763
- }, [issues, issueComponent]);
765
+ }, [showIssues, issues, issueComponent]);
764
766
  // Per-frame diagnostics badge: the same fold, but for findings about a
765
767
  // BOUNDARY rather than a construct. Keyed by the region's React Flow node
766
768
  // id — a `module` target names a module frame, a `process` target a process
@@ -770,7 +772,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
770
772
  // borrowing its layer's icon would imply the frame is at fault for it.
771
773
  const regionIssueByNodeId = useMemo(() => {
772
774
  const map = new Map();
773
- if (!issues?.length)
775
+ if (showIssues !== true || !issues?.length)
774
776
  return map;
775
777
  for (const issue of issues) {
776
778
  const target = issue.target;
@@ -791,7 +793,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
791
793
  });
792
794
  }
793
795
  return map;
794
- }, [issues]);
796
+ }, [showIssues, issues]);
795
797
  // Per-edge step verification badge: maps edge IDs to step verification status.
796
798
  // Keyed by the React Flow edge id (from → mechanism → to). An edge can have
797
799
  // multiple steps with issues; we show the worst status.
@@ -804,7 +806,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
804
806
  step_span_stale: 1,
805
807
  };
806
808
  const map = new Map();
807
- if (!issues?.length || !trails?.length) {
809
+ if (showIssues !== true || !issues?.length || !trails?.length) {
808
810
  return map;
809
811
  }
810
812
  for (const issue of issues) {
@@ -831,7 +833,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
831
833
  });
832
834
  }
833
835
  return map;
834
- }, [issues, trails, issueStep]);
836
+ }, [showIssues, issues, trails, issueStep]);
835
837
  // Expanded verification layers in the diagnostics list. When any layer with
836
838
  // findings is expanded, the canvas dims everything that layer does not
837
839
  // implicate (see `issueFocus`). The list publishes this through
@@ -864,7 +866,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
864
866
  // Returns null when nothing is expanded, no expanded layer has findings, or
865
867
  // the findings implicate nothing — so no dimming is applied.
866
868
  const issueFocus = useMemo(() => {
867
- if (!issues?.length || expandedIssueCategories.length === 0)
869
+ if (showIssues !== true || !issues?.length || expandedIssueCategories.length === 0)
868
870
  return null;
869
871
  const active = new Set(expandedIssueCategories);
870
872
  const activeIssues = issues.filter((i) => active.has(issueCategory(i)));
@@ -927,7 +929,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
927
929
  edgeIds.add(e.id);
928
930
  }
929
931
  return { nodeIds, edgeIds };
930
- }, [issues, expandedIssueCategories, components, trails, resolveAlias]);
932
+ }, [showIssues, issues, expandedIssueCategories, components, trails, resolveAlias]);
931
933
  const issueFocusNodeIds = issueFocus?.nodeIds ?? null;
932
934
  const issueFocusEdgeIds = issueFocus?.edgeIds ?? null;
933
935
  // When an edge is selected, dim every other edge + its label to focus it.
@@ -2057,9 +2059,25 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
2057
2059
  : undefined
2058
2060
  : undefined;
2059
2061
  // Header block: extras, then the description.
2060
- const sidebarHeader = (_jsxs(_Fragment, { children: [sidebarExtra, description && (_jsx("div", { style: { fontSize: theme.fontSizes[0], lineHeight: 1.5 }, children: _jsx(IndustryMarkdownSlide, { content: description, slideIdPrefix: "subsystem-desc", slideIndex: 0, isVisible: true, theme: theme, disableScroll: true, disableBasePadding: true, enableKeyboardScrolling: false, autoFocusOnVisible: false }) })), sidebarAfterDescription] }));
2062
+ const sidebarHeader = (_jsxs(_Fragment, { children: [sidebarExtra, description && (_jsxs("div", { style: { fontSize: theme.fontSizes[0], lineHeight: 1.5 }, children: [_jsx("div", { style: {
2063
+ display: '-webkit-box',
2064
+ WebkitBoxOrient: 'vertical',
2065
+ WebkitLineClamp: 6,
2066
+ maxHeight: '9em',
2067
+ overflow: 'hidden',
2068
+ }, children: _jsx(IndustryMarkdownSlide, { content: description, slideIdPrefix: "subsystem-desc", slideIndex: 0, isVisible: true, theme: theme, disableScroll: true, disableBasePadding: true, enableKeyboardScrolling: false, autoFocusOnVisible: false }) }), _jsx("button", { type: "button", "aria-label": "Read full description", "aria-haspopup": "dialog", "aria-expanded": descriptionOpen, onClick: () => setDescriptionOpen(true), style: {
2069
+ marginTop: 6,
2070
+ padding: 0,
2071
+ border: 'none',
2072
+ background: 'transparent',
2073
+ color: theme.colors.textSecondary,
2074
+ fontFamily: theme.fonts.body,
2075
+ fontSize: theme.fontSizes[0],
2076
+ textDecoration: 'underline',
2077
+ cursor: 'pointer',
2078
+ }, children: "More" })] })), sidebarAfterDescription] }));
2061
2079
  // Lower panel: diagnostics, the file tree, or the trail flow list.
2062
- const sidebarPanel = issuesActive ? (diagnosticsTab === 'agents' && agentsPanel && !sidebarTrail ? (_jsx(SubsystemAgentsPanel, { ...agentsPanel })) : (_jsx(SubsystemIssueList, { issues: sidebarScope.issues, focusCategory: focusIssueCategory, laneStatus: sidebarTrail ? undefined : laneStatus, onSelectIssue: focusIssueTarget, onDeselectIssue: unfocusIssueTarget, onApplyFix: onApplyIssueFix, onHoverIssue: onHoverIssue, onExpandedCategoriesChange: handleExpandedCategoriesChange }))) : filesActive ? treePaths.entries.length === 0 ? (_jsx("div", { style: { padding: 16, color: theme.colors.textSecondary }, children: "No files for this trail." })) : (_jsx(SubsystemFileTree, { files: treeFilePaths, selectedFile: openFile ? treePaths.toDisplay.get(openFile) : null, onSelectFile: (path) => {
2080
+ const sidebarPanel = issuesActive ? (diagnosticsTab === 'agents' && agentsPanel && !sidebarTrail ? (_jsx(SubsystemAgentsPanel, { ...agentsPanel })) : (_jsx(SubsystemIssueList, { issues: sidebarScope.issues, focusCategory: focusIssueCategory, laneStatus: sidebarTrail ? undefined : laneStatus, onSelectIssue: focusIssueTarget, onDeselectIssue: unfocusIssueTarget, onApplyFix: onApplyIssueFix, onHoverIssue: onHoverIssue, onExpandedCategoriesChange: handleExpandedCategoriesChange }))) : filesActive ? treePaths.entries.length === 0 ? (_jsx("div", { style: { padding: 16, color: theme.colors.textSecondary }, children: "No files for this trail." })) : (_jsx(SubsystemFileTree, { headerless: true, files: treeFilePaths, selectedFile: openFile ? treePaths.toDisplay.get(openFile) : null, onSelectFile: (path) => {
2063
2081
  const file = treePaths.toFile.get(path);
2064
2082
  if (file)
2065
2083
  onOpenDeclarationFile(file);
@@ -2102,279 +2120,279 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
2102
2120
  fontSize: theme.fontSizes[0],
2103
2121
  color: theme.colors.textMuted ?? theme.colors.textSecondary,
2104
2122
  }, children: fileOverlay.file }), _jsx(FileOverlayCloseButton, { onClose: closeFileOverlay })] }), _jsx("div", { style: { flex: 1, minHeight: 0, overflow: 'auto' }, children: _jsx(FileDrawerContent, { render: renderDrawerContent, file: fileOverlay.file, startLine: fileOverlay.startLine, endLine: fileOverlay.endLine, fullFile: true }) })] }));
2105
- return (_jsx(GraphWithSidebar, { hidden: !sidebarVisible, bodyHidden: !sidebarBodyVisible, headerMaxHeight: sidebarBodyVisible ? '30%' : '100%', header: sidebarHeader, tabs: sidebarTabs, activeTab: diagnosticsTab, onTabChange: (id) => setDiagnosticsTab(id), sidebarBody: sidebarBody, tabListLabel: issuesActive ? 'Diagnostics view' : 'Sidebar view', defaultWidth: persisted.sidebarWidth ?? 450, minWidth: 240, maxWidth: sidebarMaxWidth, persistKey: persistKey, onResizeEnd: () => requestAnimationFrame(() => fitView()), children: _jsxs("div", { style: { flex: 1, position: 'relative', display: 'flex', flexDirection: committedDirection === 'vertical' ? 'row' : 'column', minHeight: 0, minWidth: 0 }, children: [_jsxs("div", { className: layoutFlipping ? LAYOUT_FLIP_CLASS : undefined, style: {
2106
- position: 'relative',
2107
- flex: collapseCanvas ? 0 : 1,
2108
- minHeight: 0,
2109
- minWidth: 0,
2110
- height: collapseCanvas ? 0 : undefined,
2111
- overflow: 'hidden',
2112
- display: 'flex',
2113
- flexDirection: 'column',
2114
- }, children: [showEdgeLabels !== false && (_jsx("div", { style: {
2115
- position: 'absolute',
2116
- inset: 0,
2117
- overflow: 'hidden',
2118
- pointerEvents: 'none',
2119
- zIndex: 5,
2120
- opacity: layoutFlipping ? 0 : 1,
2121
- transition: `opacity ${LAYOUT_FLIP_EDGE_MS}ms ease`,
2122
- }, children: edgeLabels.map((lbl) => {
2123
- const mechanism = lbl.mechanism;
2124
- const isGraphify = lbl.provenance === 'graphify';
2125
- const color = edgeColor({ mechanism, provenance: lbl.provenance });
2126
- // Label chrome stays the crisp rounded box for every edge — graphify
2127
- // provenance is carried by hue (GRAPHIFY_RELATION_COLOR), the dashed
2128
- // stroke, and the tooltip, NOT by the "ambiguous" cloud silhouette
2129
- // (that idiom is reserved for soft authored mechanisms).
2130
- const verifiable = true;
2131
- const labelTitle = isGraphify
2132
- ? 'Derived from graphify (static symbol graph)'
2133
- : MECHANISM_DESCRIPTIONS.find(([m]) => m === mechanism)?.[2] === false
2134
- ? 'Not directly verifiable with graphify'
2135
- : undefined;
2136
- const screenX = lbl.midX * viewport.zoom + viewport.x;
2137
- const screenY = lbl.midY * viewport.zoom + viewport.y;
2138
- const text = lbl.stepNos?.length
2139
- ? `${lbl.stepNos.map((n) => `${n}:`).join(' ')} ${lbl.mechanism}`
2140
- : lbl.mechanism;
2141
- return (_jsxs("div", { "data-edge-label": lbl.id, title: labelTitle, onClick: (e) => {
2142
- e.stopPropagation();
2143
- selectEdge(lbl.id);
2144
- }, style: {
2145
- position: 'absolute',
2146
- left: screenX,
2147
- top: screenY,
2148
- // Center on the flow-space midpoint, and scale with the graph
2149
- // (flow-space size): labels shrink as you zoom out instead of
2150
- // staying fixed-screen-size and dominating the smaller graph.
2151
- transform: `translate(-50%, -50%) scale(${viewport.zoom})`,
2152
- transformOrigin: 'center center',
2153
- display: 'flex',
2154
- alignItems: 'center',
2155
- justifyContent: 'center',
2156
- width: EDGE_LABEL_WIDTH,
2157
- height: EDGE_LABEL_HEIGHT,
2158
- boxSizing: 'border-box',
2159
- padding: '7px 8px',
2160
- cursor: 'pointer',
2161
- pointerEvents: 'auto',
2162
- opacity: lbl.dimmed ? 0.15 : 1,
2163
- }, children: [verifiable ? (_jsx("span", { "aria-hidden": true, style: {
2164
- position: 'absolute',
2165
- inset: 0,
2166
- borderRadius: 4,
2167
- border: `0.5px solid ${color}`,
2168
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
2169
- pointerEvents: 'none',
2170
- } })) : (_jsx("svg", { "aria-hidden": true, width: EDGE_LABEL_WIDTH, height: EDGE_LABEL_HEIGHT + EDGE_LABEL_CLOUD_EXTRA_TOP, viewBox: `0 0 ${EDGE_LABEL_WIDTH} ${EDGE_LABEL_HEIGHT + EDGE_LABEL_CLOUD_EXTRA_TOP}`, style: {
2171
- position: 'absolute',
2172
- left: 0,
2173
- top: -EDGE_LABEL_CLOUD_EXTRA_TOP,
2174
- pointerEvents: 'none',
2175
- }, children: _jsx("path", { d: EDGE_LABEL_CLOUD_PATH, fill: theme.colors.backgroundSecondary ?? theme.colors.background, stroke: hexWithAlpha(color, 0.75), strokeWidth: 1.2, strokeLinejoin: "round" }) })), _jsx("span", { style: {
2176
- position: 'relative',
2177
- display: 'block',
2178
- maxWidth: '100%',
2179
- overflow: 'hidden',
2180
- textOverflow: 'ellipsis',
2181
- whiteSpace: 'nowrap',
2182
- fontSize: EDGE_LABEL_FONT_SIZE,
2183
- lineHeight: 1,
2184
- fontFamily: theme.fonts.monospace,
2185
- fontWeight: 500,
2186
- color,
2187
- }, children: text }), lbl.stepIssue && (() => {
2188
- const StepIssueIcon = ISSUE_KIND_ICON[lbl.stepIssue.kind];
2189
- // Match IssueList: error severity = red, info severity = amber
2190
- const issueColor = lbl.stepIssue.severity === 'error'
2191
- ? (theme.colors.error ?? '#e5534b')
2192
- : (theme.colors.warning ?? '#d4a017');
2193
- const badgeBg = theme.colors.backgroundSecondary ?? theme.colors.background;
2194
- return (_jsxs("span", { "aria-hidden": true, title: lbl.stepIssue.kind === 'step_span_stale'
2195
- ? 'Call site changed since verification'
2196
- : lbl.stepIssue.kind === 'step_site_outside_from'
2197
- ? 'Calls site is outside its source component'
2198
- : 'Call site not yet verified', style: {
2123
+ return (_jsxs(GraphWithSidebar, { hidden: !sidebarVisible, bodyHidden: !sidebarBodyVisible, header: sidebarHeader, tabs: sidebarTabs, activeTab: diagnosticsTab, onTabChange: (id) => setDiagnosticsTab(id), sidebarBody: sidebarBody, tabListLabel: issuesActive ? 'Diagnostics view' : 'Sidebar view', defaultWidth: persisted.sidebarWidth ?? 450, minWidth: 240, maxWidth: sidebarMaxWidth, persistKey: persistKey, onResizeEnd: () => requestAnimationFrame(() => fitView()), children: [!hideSidebar && description && descriptionOpen && (_jsx(DescriptionOverlay, { description: description, onClose: () => setDescriptionOpen(false) })), _jsxs("div", { style: { flex: 1, position: 'relative', display: 'flex', flexDirection: committedDirection === 'vertical' ? 'row' : 'column', minHeight: 0, minWidth: 0 }, children: [_jsxs("div", { className: layoutFlipping ? LAYOUT_FLIP_CLASS : undefined, style: {
2124
+ position: 'relative',
2125
+ flex: collapseCanvas ? 0 : 1,
2126
+ minHeight: 0,
2127
+ minWidth: 0,
2128
+ height: collapseCanvas ? 0 : undefined,
2129
+ overflow: 'hidden',
2130
+ display: 'flex',
2131
+ flexDirection: 'column',
2132
+ }, children: [showEdgeLabels !== false && (_jsx("div", { style: {
2133
+ position: 'absolute',
2134
+ inset: 0,
2135
+ overflow: 'hidden',
2136
+ pointerEvents: 'none',
2137
+ zIndex: 5,
2138
+ opacity: layoutFlipping ? 0 : 1,
2139
+ transition: `opacity ${LAYOUT_FLIP_EDGE_MS}ms ease`,
2140
+ }, children: edgeLabels.map((lbl) => {
2141
+ const mechanism = lbl.mechanism;
2142
+ const isGraphify = lbl.provenance === 'graphify';
2143
+ const color = edgeColor({ mechanism, provenance: lbl.provenance });
2144
+ // Label chrome stays the crisp rounded box for every edge — graphify
2145
+ // provenance is carried by hue (GRAPHIFY_RELATION_COLOR), the dashed
2146
+ // stroke, and the tooltip, NOT by the "ambiguous" cloud silhouette
2147
+ // (that idiom is reserved for soft authored mechanisms).
2148
+ const verifiable = true;
2149
+ const labelTitle = isGraphify
2150
+ ? 'Derived from graphify (static symbol graph)'
2151
+ : MECHANISM_DESCRIPTIONS.find(([m]) => m === mechanism)?.[2] === false
2152
+ ? 'Not directly verifiable with graphify'
2153
+ : undefined;
2154
+ const screenX = lbl.midX * viewport.zoom + viewport.x;
2155
+ const screenY = lbl.midY * viewport.zoom + viewport.y;
2156
+ const text = lbl.stepNos?.length
2157
+ ? `${lbl.stepNos.map((n) => `${n}:`).join(' ')} ${lbl.mechanism}`
2158
+ : lbl.mechanism;
2159
+ return (_jsxs("div", { "data-edge-label": lbl.id, title: labelTitle, onClick: (e) => {
2160
+ e.stopPropagation();
2161
+ selectEdge(lbl.id);
2162
+ }, style: {
2163
+ position: 'absolute',
2164
+ left: screenX,
2165
+ top: screenY,
2166
+ // Center on the flow-space midpoint, and scale with the graph
2167
+ // (flow-space size): labels shrink as you zoom out instead of
2168
+ // staying fixed-screen-size and dominating the smaller graph.
2169
+ transform: `translate(-50%, -50%) scale(${viewport.zoom})`,
2170
+ transformOrigin: 'center center',
2171
+ display: 'flex',
2172
+ alignItems: 'center',
2173
+ justifyContent: 'center',
2174
+ width: EDGE_LABEL_WIDTH,
2175
+ height: EDGE_LABEL_HEIGHT,
2176
+ boxSizing: 'border-box',
2177
+ padding: '7px 8px',
2178
+ cursor: 'pointer',
2179
+ pointerEvents: 'auto',
2180
+ opacity: lbl.dimmed ? 0.15 : 1,
2181
+ }, children: [verifiable ? (_jsx("span", { "aria-hidden": true, style: {
2199
2182
  position: 'absolute',
2200
- right: -7,
2201
- bottom: -6,
2202
- zIndex: 2,
2203
- display: 'inline-flex',
2204
- alignItems: 'center',
2205
- justifyContent: 'center',
2206
- gap: 3,
2207
- minWidth: 18,
2208
- padding: '2px 4px',
2183
+ inset: 0,
2209
2184
  borderRadius: 4,
2210
- border: `1.5px solid ${issueColor}`,
2211
- background: badgeBg,
2185
+ border: `0.5px solid ${color}`,
2186
+ background: theme.colors.backgroundSecondary ?? theme.colors.background,
2187
+ pointerEvents: 'none',
2188
+ } })) : (_jsx("svg", { "aria-hidden": true, width: EDGE_LABEL_WIDTH, height: EDGE_LABEL_HEIGHT + EDGE_LABEL_CLOUD_EXTRA_TOP, viewBox: `0 0 ${EDGE_LABEL_WIDTH} ${EDGE_LABEL_HEIGHT + EDGE_LABEL_CLOUD_EXTRA_TOP}`, style: {
2189
+ position: 'absolute',
2190
+ left: 0,
2191
+ top: -EDGE_LABEL_CLOUD_EXTRA_TOP,
2212
2192
  pointerEvents: 'none',
2213
- }, children: [StepIssueIcon && (_jsx(StepIssueIcon, { size: 12, color: issueColor })), lbl.stepIssue.count > 1 && (_jsx("span", { style: {
2214
- color: issueColor,
2215
- fontFamily: theme.fonts.monospace,
2216
- fontSize: 10,
2217
- fontWeight: 700,
2218
- lineHeight: 1,
2219
- }, children: lbl.stepIssue.count }))] }));
2220
- })()] }, lbl.id));
2221
- }) })), _jsx(GraphLayerStyle, {}), _jsx(ReactFlow, { nodes: dispNodes, edges: dispEdges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, className: GRAPH_CANVAS_CLASS,
2222
- // The key above remounts ReactFlow when the node/edge SET changes (a
2223
- // trail-focus layout, a filtered model). A remount otherwise resets the
2224
- // viewport to identity — a visible jump to scale 1 before the post-flip
2225
- // refit. Seed the new instance with the viewport the old one had.
2226
- defaultViewport: viewport, ...GRAPH_NAV_PROPS, onNodeClick: onNodeClick, onNodeDoubleClick: onNodeDoubleClick, onEdgeClick: onEdgeClick, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, proOptions: { hideAttribution: true }, onPaneClick: onPaneClick, style: {
2227
- width: '100%',
2228
- height: '100%',
2229
- flex: 1,
2230
- minHeight: 0,
2231
- background: theme.colors.background,
2232
- }, children: _jsx(GraphChrome, {}) }, `${baseNodesKey}-${baseEdgesKey}`), !hideLayoutToggle && (_jsxs("button", { type: "button", onClick: () => changeLayoutDirection(requestedDirection === 'horizontal' ? 'vertical' : 'horizontal'), disabled: layoutFlipping, title: requestedDirection === 'horizontal'
2233
- ? 'Switch to a vertical layout (top-to-bottom)'
2234
- : 'Switch to a horizontal layout (left-to-right)', "aria-label": requestedDirection === 'horizontal'
2235
- ? 'Switch to vertical layout'
2236
- : 'Switch to horizontal layout', style: {
2237
- position: 'absolute',
2238
- top: 10,
2239
- right: 10,
2240
- zIndex: 6,
2241
- display: 'inline-flex',
2242
- alignItems: 'center',
2243
- gap: 5,
2244
- padding: '4px 9px',
2245
- border: `1px solid ${theme.colors.border}`,
2246
- borderRadius: 6,
2247
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
2248
- color: theme.colors.textSecondary,
2249
- fontFamily: theme.fonts.monospace,
2250
- fontSize: theme.fontSizes[0],
2251
- cursor: layoutFlipping ? 'default' : 'pointer',
2252
- opacity: layoutFlipping ? 0.55 : 1,
2253
- }, children: [requestedDirection === 'horizontal' ? (_jsx(ArrowDown, { size: 13, "aria-hidden": "true" })) : (_jsx(ArrowRight, { size: 13, "aria-hidden": "true" })), requestedDirection === 'horizontal' ? 'Vertical' : 'Horizontal'] })), unroutedIds.length > 0 && (_jsxs("div", { role: "status", title: `Edges with no routed path (not drawn):\n${unroutedIds.join('\n')}`, style: {
2254
- position: 'absolute',
2255
- top: 10,
2256
- left: 10,
2257
- zIndex: 6,
2258
- display: 'inline-flex',
2259
- alignItems: 'center',
2260
- gap: 6,
2261
- padding: '4px 10px',
2262
- fontSize: theme.fontSizes[0],
2263
- fontFamily: theme.fonts.body,
2264
- color: theme.colors.text,
2265
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
2266
- border: `1px solid ${theme.colors.warning ?? '#d4a017'}`,
2267
- borderRadius: 6,
2268
- boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
2269
- }, children: [_jsx(Unlink, { size: 13, color: theme.colors.warning ?? '#d4a017', "aria-hidden": true }), unroutedIds.length, " ", unroutedIds.length === 1 ? 'edge' : 'edges', " not drawn"] })), (graphTitle || overlayTrailTitle || overlayTrailStep) && (_jsxs("div", { style: {
2270
- position: 'absolute',
2271
- top: 10,
2272
- left: '50%',
2273
- transform: 'translateX(-50%)',
2274
- zIndex: 6,
2275
- display: 'flex',
2276
- flexDirection: 'column',
2277
- alignItems: 'center',
2278
- gap: 6,
2279
- maxWidth: '70%',
2280
- pointerEvents: 'none',
2281
- }, children: [graphTitle && (_jsx("div", { style: {
2282
- maxWidth: '100%',
2283
- overflow: 'hidden',
2284
- textOverflow: 'ellipsis',
2285
- whiteSpace: 'nowrap',
2286
- padding: '6px 18px',
2287
- fontSize: theme.fontSizes[3],
2288
- fontWeight: 600,
2289
- fontFamily: theme.fonts.monospace,
2290
- color: theme.colors.text,
2291
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
2292
- border: `1px solid ${theme.colors.border}`,
2293
- borderRadius: 6,
2294
- boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
2295
- }, children: graphTitle })), overlayTrailTitle && (_jsxs("div", { style: {
2296
- maxWidth: '100%',
2297
- minWidth: overlayTrailStep ? 160 : undefined,
2298
- display: 'flex',
2299
- flexDirection: 'column',
2300
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
2301
- border: `1px solid ${theme.colors.border}`,
2302
- borderRadius: 6,
2303
- boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
2304
- overflow: 'hidden',
2305
- opacity: 0.95,
2306
- }, "aria-label": overlayTrailStep
2307
- ? `${overlayTrailTitle}, step ${overlayTrailStep.index} of ${overlayTrailStep.total}`
2308
- : overlayTrailTitle, children: [_jsx("div", { style: {
2309
- padding: '5px 14px',
2310
- fontSize: theme.fontSizes[2] ?? theme.fontSizes[1],
2311
- fontWeight: 600,
2312
- fontFamily: theme.fonts.monospace,
2313
- color: theme.colors.text,
2314
- overflow: 'hidden',
2315
- textOverflow: 'ellipsis',
2316
- whiteSpace: 'nowrap',
2317
- textAlign: 'center',
2318
- }, children: overlayTrailTitle }), overlayTrailStep && overlayTrailStep.total > 0 && (_jsx("div", { style: {
2319
- display: 'flex',
2320
- gap: 3,
2321
- padding: '0 6px 5px',
2322
- }, "aria-hidden": "true", children: Array.from({ length: overlayTrailStep.total }, (_, i) => {
2323
- const n = i + 1;
2324
- const active = n === overlayTrailStep.index;
2325
- const done = n < overlayTrailStep.index;
2326
- return (_jsx("span", { style: {
2327
- flex: 1,
2328
- height: 2,
2329
- borderRadius: 1,
2330
- background: active
2331
- ? (theme.colors.accent ?? theme.colors.primary ?? theme.colors.text)
2332
- : done
2333
- ? (theme.colors.textSecondary ?? theme.colors.text)
2334
- : (theme.colors.border ?? 'rgba(127,127,127,0.45)'),
2335
- opacity: active ? 1 : done ? 0.75 : 0.4,
2336
- transition: 'opacity 120ms ease, background 120ms ease',
2337
- } }, n));
2338
- }) }))] })), overlayTrailStep?.annotation && (_jsx("div", { style: {
2339
- maxWidth: '100%',
2340
- padding: '6px 14px',
2341
- background: theme.colors.backgroundSecondary ?? theme.colors.background,
2342
- border: `1px solid ${theme.colors.border}`,
2343
- borderRadius: 6,
2344
- boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
2345
- textAlign: 'center',
2346
- fontSize: theme.fontSizes[0],
2347
- fontFamily: theme.fonts.body,
2348
- color: theme.colors.textMuted ?? theme.colors.textSecondary,
2349
- lineHeight: 1.35,
2350
- }, children: overlayTrailStep.annotation }))] })), selected && (_jsx("div", { style: {
2351
- position: 'absolute',
2352
- top: 10,
2353
- right: 10,
2354
- zIndex: 8,
2355
- width: 'min(560px, 70%)',
2356
- maxHeight: '46%',
2357
- overflowY: 'auto',
2358
- background: theme.colors.background,
2359
- border: `1px solid ${theme.colors.border}`,
2360
- borderRadius: 8,
2361
- boxShadow: '0 8px 24px rgba(0,0,0,0.35)',
2362
- }, children: _jsx(ComponentDeclaration, { component: selected, onOpenFile: onOpenDeclarationFile, onRelatedSelect: resolveRelatedComponent, onInspectSymbol: onInspectSymbol
2363
- ? (symbol, ref) => onInspectSymbol({
2364
- purl: selected.purl,
2365
- file: selected.file,
2366
- symbol,
2367
- ref,
2368
- })
2369
- : undefined, verification: componentVerification, fileOpen: drawerTarget?.kind === 'file' &&
2370
- !!selected.file &&
2371
- selected.file === drawerTarget.file, declarationOpen: drawerTarget?.kind === 'file' &&
2372
- drawerTarget.startLine != null &&
2373
- !!selected.file &&
2374
- selected.file === drawerTarget.file &&
2375
- selected.declarationSpan?.startLine === drawerTarget.startLine }) })), committedDirection === 'vertical' && fileOverlayView] }), _jsx(FileDrawer, { title: drawerTitle, onClose: closeDrawer, placement: committedDirection === 'vertical' ? 'left' : 'bottom', onResizeEnd: () => requestAnimationFrame(() => fitView()), fillHeight: drawerFillHeight, suppressEscape: fileOverlayOpen, hidden: hideDrawer, progress: drawerProgress, children: drawerTarget?.kind === 'trail' &&
2376
- focusedTrail &&
2377
- renderTrailViewer ? (_jsx(TrailDrawerContent, { render: renderTrailDrawerContent, trail: focusedTrail, stepIndex: drawerTarget.stepIndex, onOpenFile: onOpenFileFromTrail, resolveSymbol: resolveTrailSymbol, onSymbolClick: openConstructDeclaration, onVisibleStepChange: setVisibleStepIndex })) : drawerTarget?.kind === 'file' ? (_jsx(FileDrawerContent, { render: renderDrawerContent, file: drawerTarget.file, startLine: drawerTarget.startLine, endLine: drawerTarget.endLine })) : null }), committedDirection !== 'vertical' && fileOverlayView, _jsx(GraphLayoutCover, { revealed: layoutReady || layoutFlipping }), canvasOverlay, liveEvents ? _jsx(MaintainLivePanel, { ...liveEvents }) : null] }) }));
2193
+ }, children: _jsx("path", { d: EDGE_LABEL_CLOUD_PATH, fill: theme.colors.backgroundSecondary ?? theme.colors.background, stroke: hexWithAlpha(color, 0.75), strokeWidth: 1.2, strokeLinejoin: "round" }) })), _jsx("span", { style: {
2194
+ position: 'relative',
2195
+ display: 'block',
2196
+ maxWidth: '100%',
2197
+ overflow: 'hidden',
2198
+ textOverflow: 'ellipsis',
2199
+ whiteSpace: 'nowrap',
2200
+ fontSize: EDGE_LABEL_FONT_SIZE,
2201
+ lineHeight: 1,
2202
+ fontFamily: theme.fonts.monospace,
2203
+ fontWeight: 500,
2204
+ color,
2205
+ }, children: text }), lbl.stepIssue && (() => {
2206
+ const StepIssueIcon = ISSUE_KIND_ICON[lbl.stepIssue.kind];
2207
+ // Match IssueList: error severity = red, info severity = amber
2208
+ const issueColor = lbl.stepIssue.severity === 'error'
2209
+ ? (theme.colors.error ?? '#e5534b')
2210
+ : (theme.colors.warning ?? '#d4a017');
2211
+ const badgeBg = theme.colors.backgroundSecondary ?? theme.colors.background;
2212
+ return (_jsxs("span", { "aria-hidden": true, title: lbl.stepIssue.kind === 'step_span_stale'
2213
+ ? 'Call site changed since verification'
2214
+ : lbl.stepIssue.kind === 'step_site_outside_from'
2215
+ ? 'Calls site is outside its source component'
2216
+ : 'Call site not yet verified', style: {
2217
+ position: 'absolute',
2218
+ right: -7,
2219
+ bottom: -6,
2220
+ zIndex: 2,
2221
+ display: 'inline-flex',
2222
+ alignItems: 'center',
2223
+ justifyContent: 'center',
2224
+ gap: 3,
2225
+ minWidth: 18,
2226
+ padding: '2px 4px',
2227
+ borderRadius: 4,
2228
+ border: `1.5px solid ${issueColor}`,
2229
+ background: badgeBg,
2230
+ pointerEvents: 'none',
2231
+ }, children: [StepIssueIcon && (_jsx(StepIssueIcon, { size: 12, color: issueColor })), lbl.stepIssue.count > 1 && (_jsx("span", { style: {
2232
+ color: issueColor,
2233
+ fontFamily: theme.fonts.monospace,
2234
+ fontSize: 10,
2235
+ fontWeight: 700,
2236
+ lineHeight: 1,
2237
+ }, children: lbl.stepIssue.count }))] }));
2238
+ })()] }, lbl.id));
2239
+ }) })), _jsx(GraphLayerStyle, {}), _jsx(ReactFlow, { nodes: dispNodes, edges: dispEdges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, className: GRAPH_CANVAS_CLASS,
2240
+ // The key above remounts ReactFlow when the node/edge SET changes (a
2241
+ // trail-focus layout, a filtered model). A remount otherwise resets the
2242
+ // viewport to identity — a visible jump to scale 1 before the post-flip
2243
+ // refit. Seed the new instance with the viewport the old one had.
2244
+ defaultViewport: viewport, ...GRAPH_NAV_PROPS, onNodeClick: onNodeClick, onNodeDoubleClick: onNodeDoubleClick, onEdgeClick: onEdgeClick, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, proOptions: { hideAttribution: true }, onPaneClick: onPaneClick, style: {
2245
+ width: '100%',
2246
+ height: '100%',
2247
+ flex: 1,
2248
+ minHeight: 0,
2249
+ background: theme.colors.background,
2250
+ }, children: _jsx(GraphChrome, {}) }, `${baseNodesKey}-${baseEdgesKey}`), !hideLayoutToggle && (_jsxs("button", { type: "button", onClick: () => changeLayoutDirection(requestedDirection === 'horizontal' ? 'vertical' : 'horizontal'), disabled: layoutFlipping, title: requestedDirection === 'horizontal'
2251
+ ? 'Switch to a vertical layout (top-to-bottom)'
2252
+ : 'Switch to a horizontal layout (left-to-right)', "aria-label": requestedDirection === 'horizontal'
2253
+ ? 'Switch to vertical layout'
2254
+ : 'Switch to horizontal layout', style: {
2255
+ position: 'absolute',
2256
+ top: 10,
2257
+ right: 10,
2258
+ zIndex: 6,
2259
+ display: 'inline-flex',
2260
+ alignItems: 'center',
2261
+ gap: 5,
2262
+ padding: '4px 9px',
2263
+ border: `1px solid ${theme.colors.border}`,
2264
+ borderRadius: 6,
2265
+ background: theme.colors.backgroundSecondary ?? theme.colors.background,
2266
+ color: theme.colors.textSecondary,
2267
+ fontFamily: theme.fonts.monospace,
2268
+ fontSize: theme.fontSizes[0],
2269
+ cursor: layoutFlipping ? 'default' : 'pointer',
2270
+ opacity: layoutFlipping ? 0.55 : 1,
2271
+ }, children: [requestedDirection === 'horizontal' ? (_jsx(ArrowDown, { size: 13, "aria-hidden": "true" })) : (_jsx(ArrowRight, { size: 13, "aria-hidden": "true" })), requestedDirection === 'horizontal' ? 'Vertical' : 'Horizontal'] })), unroutedIds.length > 0 && (_jsxs("div", { role: "status", title: `Edges with no routed path (not drawn):\n${unroutedIds.join('\n')}`, style: {
2272
+ position: 'absolute',
2273
+ top: 10,
2274
+ left: 10,
2275
+ zIndex: 6,
2276
+ display: 'inline-flex',
2277
+ alignItems: 'center',
2278
+ gap: 6,
2279
+ padding: '4px 10px',
2280
+ fontSize: theme.fontSizes[0],
2281
+ fontFamily: theme.fonts.body,
2282
+ color: theme.colors.text,
2283
+ background: theme.colors.backgroundSecondary ?? theme.colors.background,
2284
+ border: `1px solid ${theme.colors.warning ?? '#d4a017'}`,
2285
+ borderRadius: 6,
2286
+ boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
2287
+ }, children: [_jsx(Unlink, { size: 13, color: theme.colors.warning ?? '#d4a017', "aria-hidden": true }), unroutedIds.length, " ", unroutedIds.length === 1 ? 'edge' : 'edges', " not drawn"] })), (graphTitle || overlayTrailTitle || overlayTrailStep) && (_jsxs("div", { style: {
2288
+ position: 'absolute',
2289
+ top: 10,
2290
+ left: '50%',
2291
+ transform: 'translateX(-50%)',
2292
+ zIndex: 6,
2293
+ display: 'flex',
2294
+ flexDirection: 'column',
2295
+ alignItems: 'center',
2296
+ gap: 6,
2297
+ maxWidth: '70%',
2298
+ pointerEvents: 'none',
2299
+ }, children: [graphTitle && (_jsx("div", { style: {
2300
+ maxWidth: '100%',
2301
+ overflow: 'hidden',
2302
+ textOverflow: 'ellipsis',
2303
+ whiteSpace: 'nowrap',
2304
+ padding: '6px 18px',
2305
+ fontSize: theme.fontSizes[3],
2306
+ fontWeight: 600,
2307
+ fontFamily: theme.fonts.monospace,
2308
+ color: theme.colors.text,
2309
+ background: theme.colors.backgroundSecondary ?? theme.colors.background,
2310
+ border: `1px solid ${theme.colors.border}`,
2311
+ borderRadius: 6,
2312
+ boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
2313
+ }, children: graphTitle })), overlayTrailTitle && (_jsxs("div", { style: {
2314
+ maxWidth: '100%',
2315
+ minWidth: overlayTrailStep ? 160 : undefined,
2316
+ display: 'flex',
2317
+ flexDirection: 'column',
2318
+ background: theme.colors.backgroundSecondary ?? theme.colors.background,
2319
+ border: `1px solid ${theme.colors.border}`,
2320
+ borderRadius: 6,
2321
+ boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
2322
+ overflow: 'hidden',
2323
+ opacity: 0.95,
2324
+ }, "aria-label": overlayTrailStep
2325
+ ? `${overlayTrailTitle}, step ${overlayTrailStep.index} of ${overlayTrailStep.total}`
2326
+ : overlayTrailTitle, children: [_jsx("div", { style: {
2327
+ padding: '5px 14px',
2328
+ fontSize: theme.fontSizes[2] ?? theme.fontSizes[1],
2329
+ fontWeight: 600,
2330
+ fontFamily: theme.fonts.monospace,
2331
+ color: theme.colors.text,
2332
+ overflow: 'hidden',
2333
+ textOverflow: 'ellipsis',
2334
+ whiteSpace: 'nowrap',
2335
+ textAlign: 'center',
2336
+ }, children: overlayTrailTitle }), overlayTrailStep && overlayTrailStep.total > 0 && (_jsx("div", { style: {
2337
+ display: 'flex',
2338
+ gap: 3,
2339
+ padding: '0 6px 5px',
2340
+ }, "aria-hidden": "true", children: Array.from({ length: overlayTrailStep.total }, (_, i) => {
2341
+ const n = i + 1;
2342
+ const active = n === overlayTrailStep.index;
2343
+ const done = n < overlayTrailStep.index;
2344
+ return (_jsx("span", { style: {
2345
+ flex: 1,
2346
+ height: 2,
2347
+ borderRadius: 1,
2348
+ background: active
2349
+ ? (theme.colors.accent ?? theme.colors.primary ?? theme.colors.text)
2350
+ : done
2351
+ ? (theme.colors.textSecondary ?? theme.colors.text)
2352
+ : (theme.colors.border ?? 'rgba(127,127,127,0.45)'),
2353
+ opacity: active ? 1 : done ? 0.75 : 0.4,
2354
+ transition: 'opacity 120ms ease, background 120ms ease',
2355
+ } }, n));
2356
+ }) }))] })), overlayTrailStep?.annotation && (_jsx("div", { style: {
2357
+ maxWidth: '100%',
2358
+ padding: '6px 14px',
2359
+ background: theme.colors.backgroundSecondary ?? theme.colors.background,
2360
+ border: `1px solid ${theme.colors.border}`,
2361
+ borderRadius: 6,
2362
+ boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
2363
+ textAlign: 'center',
2364
+ fontSize: theme.fontSizes[0],
2365
+ fontFamily: theme.fonts.body,
2366
+ color: theme.colors.textMuted ?? theme.colors.textSecondary,
2367
+ lineHeight: 1.35,
2368
+ }, children: overlayTrailStep.annotation }))] })), selected && (_jsx("div", { style: {
2369
+ position: 'absolute',
2370
+ top: 10,
2371
+ right: 10,
2372
+ zIndex: 8,
2373
+ width: 'min(560px, 70%)',
2374
+ maxHeight: '46%',
2375
+ overflowY: 'auto',
2376
+ background: theme.colors.background,
2377
+ border: `1px solid ${theme.colors.border}`,
2378
+ borderRadius: 8,
2379
+ boxShadow: '0 8px 24px rgba(0,0,0,0.35)',
2380
+ }, children: _jsx(ComponentDeclaration, { component: selected, onOpenFile: onOpenDeclarationFile, onRelatedSelect: resolveRelatedComponent, onInspectSymbol: onInspectSymbol
2381
+ ? (symbol, ref) => onInspectSymbol({
2382
+ purl: selected.purl,
2383
+ file: selected.file,
2384
+ symbol,
2385
+ ref,
2386
+ })
2387
+ : undefined, verification: componentVerification, fileOpen: drawerTarget?.kind === 'file' &&
2388
+ !!selected.file &&
2389
+ selected.file === drawerTarget.file, declarationOpen: drawerTarget?.kind === 'file' &&
2390
+ drawerTarget.startLine != null &&
2391
+ !!selected.file &&
2392
+ selected.file === drawerTarget.file &&
2393
+ selected.declarationSpan?.startLine === drawerTarget.startLine }) })), committedDirection === 'vertical' && fileOverlayView] }), _jsx(FileDrawer, { title: drawerTitle, onClose: closeDrawer, placement: committedDirection === 'vertical' ? 'left' : 'bottom', onResizeEnd: () => requestAnimationFrame(() => fitView()), fillHeight: drawerFillHeight, suppressEscape: fileOverlayOpen, hidden: hideDrawer, progress: drawerProgress, children: drawerTarget?.kind === 'trail' &&
2394
+ focusedTrail &&
2395
+ renderTrailViewer ? (_jsx(TrailDrawerContent, { render: renderTrailDrawerContent, trail: focusedTrail, stepIndex: drawerTarget.stepIndex, onOpenFile: onOpenFileFromTrail, resolveSymbol: resolveTrailSymbol, onSymbolClick: openConstructDeclaration, onVisibleStepChange: setVisibleStepIndex })) : drawerTarget?.kind === 'file' ? (_jsx(FileDrawerContent, { render: renderDrawerContent, file: drawerTarget.file, startLine: drawerTarget.startLine, endLine: drawerTarget.endLine })) : null }), committedDirection !== 'vertical' && fileOverlayView, _jsx(GraphLayoutCover, { revealed: layoutReady || layoutFlipping }), canvasOverlay, liveEvents ? _jsx(MaintainLivePanel, { ...liveEvents }) : null] })] }));
2378
2396
  }
2379
2397
  export function SubsystemComponentGraph(props) {
2380
2398
  const { theme } = useTheme();
@@ -2389,6 +2407,7 @@ export function SubsystemComponentGraph(props) {
2389
2407
  const sidebarScope = useMemo(() => buildTrailSidebarScope(props.components, props.trails, props.issues, sidebarTrailId), [props.components, props.trails, props.issues, sidebarTrailId]);
2390
2408
  const fileCount = sidebarScope.treePaths.entries.length;
2391
2409
  const trailViewLabel = sidebarScope.trail ? 'Trail' : 'Trails';
2410
+ const trailCount = props.trails?.length ?? 0;
2392
2411
  const hasFiles = fileCount > 0 || sidebarScope.trail != null;
2393
2412
  const sidebarDiagnostic = sidebarScope.trail ? {
2394
2413
  ...props.diagnostic,
@@ -2398,17 +2417,21 @@ export function SubsystemComponentGraph(props) {
2398
2417
  fixableCount: sidebarScope.issues.filter((issue) => issue.fix != null).length,
2399
2418
  title: undefined,
2400
2419
  } : props.diagnostic ?? { status: 'unknown', issueCount: props.issues?.length };
2420
+ const hasIssues = (props.issues != null
2421
+ ? sidebarScope.issues.length
2422
+ : sidebarDiagnostic.issueCount ?? 0) > 0;
2401
2423
  useEffect(() => {
2402
2424
  if (sidebarTrailId != null && !sidebarScope.trail)
2403
2425
  setSidebarTrailId(null);
2404
2426
  }, [sidebarTrailId, sidebarScope.trail]);
2405
2427
  const [diagnosticsOpen, setDiagnosticsOpen] = useState(props.showIssues ?? (props.issues?.length ?? 0) > 0);
2406
- const issuesActive = props.showIssues ?? diagnosticsOpen;
2428
+ const issuesRequested = props.showIssues ?? diagnosticsOpen;
2429
+ const issuesActive = hasIssues && issuesRequested;
2407
2430
  const filesActive = filesOpen && hasFiles && !issuesActive;
2408
2431
  const selectSidebarView = (view) => {
2409
2432
  const nextIssuesActive = view === 'issues';
2410
2433
  setFilesOpen(view === 'files');
2411
- if (nextIssuesActive !== issuesActive) {
2434
+ if (nextIssuesActive !== issuesRequested) {
2412
2435
  if (props.showIssues === undefined)
2413
2436
  setDiagnosticsOpen(nextIssuesActive);
2414
2437
  props.diagnostic?.onToggle?.();
@@ -2420,7 +2443,7 @@ export function SubsystemComponentGraph(props) {
2420
2443
  justifyContent: 'center',
2421
2444
  gap: 4,
2422
2445
  height: 28,
2423
- padding: '4px 7px',
2446
+ padding: '4px 16px',
2424
2447
  fontFamily: theme.fonts.monospace,
2425
2448
  fontSize: theme.fontSizes[0],
2426
2449
  border: 'none',
@@ -2488,6 +2511,6 @@ export function SubsystemComponentGraph(props) {
2488
2511
  padding: 2,
2489
2512
  border: `1px solid ${theme.colors.border}`,
2490
2513
  borderRadius: 6,
2491
- }, children: [_jsxs("button", { type: "button", "aria-label": trailViewLabel, "aria-pressed": !filesActive && !issuesActive, onClick: () => selectSidebarView('trails'), style: viewButtonStyle(!filesActive && !issuesActive), children: [_jsx(Route, { size: 14, "aria-hidden": true }), _jsx("span", { children: trailViewLabel })] }), hasFiles && (_jsxs("button", { type: "button", "aria-label": `Files (${fileCount} files)`, title: "Show files", "aria-pressed": filesActive, onClick: () => selectSidebarView('files'), style: viewButtonStyle(filesActive), children: [_jsx(Files, { size: 14, "aria-hidden": true }), _jsx("span", { children: "Files" }), _jsx("span", { style: { fontWeight: 600 }, children: fileCount })] })), _jsx(SubsystemDiagnosticToggle, { ...sidebarDiagnostic, labelText: "Issues", segmented: true, active: issuesActive, onToggle: () => selectSidebarView('issues') })] })] })), _jsx("div", { ref: wrapRef, style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }, children: constructsOnly ? (_jsx(ConstructsCatalog, { components: props.components, onSelect: props.onSelect, title: props.title, hideSidebar: props.hideSidebar, description: props.description, descriptionOpen: props.descriptionOpen, onDescriptionOpenChange: props.onDescriptionOpenChange, diagnostic: props.diagnostic, sidebarExtra: props.sidebarExtra, sidebarAfterDescription: props.sidebarAfterDescription, renderFileViewer: props.renderFileViewer, onFileSelect: props.onFileSelect, componentVerification: props.componentVerification, onInspectSymbol: props.onInspectSymbol })) : (_jsx(ReactFlowProvider, { children: _jsx(Inner, { ...props, showIssues: issuesActive, filesActive: filesActive, measured: size, sidebarScope: sidebarScope, onSidebarTrailChange: setSidebarTrailId }) })) })] }));
2514
+ }, children: [_jsxs("button", { type: "button", "aria-label": sidebarScope.trail ? 'Trail' : `Trails (${trailCount} ${trailCount === 1 ? 'trail' : 'trails'})`, "aria-pressed": !filesActive && !issuesActive, onClick: () => selectSidebarView('trails'), style: viewButtonStyle(!filesActive && !issuesActive), children: [_jsx(Route, { size: 14, "aria-hidden": true }), _jsx("span", { children: trailViewLabel }), !sidebarScope.trail && _jsx("span", { style: { fontWeight: 600, marginLeft: 4 }, children: trailCount })] }), hasFiles && (_jsxs("button", { type: "button", "aria-label": `Files (${fileCount} files)`, title: "Show files", "aria-pressed": filesActive, onClick: () => selectSidebarView('files'), style: viewButtonStyle(filesActive), children: [_jsx(Files, { size: 14, "aria-hidden": true }), _jsx("span", { children: "Files" }), _jsx("span", { style: { fontWeight: 600, marginLeft: 4 }, children: fileCount })] })), hasIssues && (_jsx(SubsystemDiagnosticToggle, { ...sidebarDiagnostic, labelText: "Issues", segmented: true, active: issuesActive, onToggle: () => selectSidebarView('issues') }))] })] })), _jsx("div", { ref: wrapRef, style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }, children: constructsOnly ? (_jsx(ConstructsCatalog, { components: props.components, onSelect: props.onSelect, title: props.title, hideSidebar: props.hideSidebar, description: props.description, descriptionOpen: props.descriptionOpen, onDescriptionOpenChange: props.onDescriptionOpenChange, diagnostic: props.diagnostic, sidebarExtra: props.sidebarExtra, sidebarAfterDescription: props.sidebarAfterDescription, renderFileViewer: props.renderFileViewer, onFileSelect: props.onFileSelect, componentVerification: props.componentVerification, onInspectSymbol: props.onInspectSymbol })) : (_jsx(ReactFlowProvider, { children: _jsx(Inner, { ...props, showIssues: issuesActive, filesActive: filesActive, measured: size, sidebarScope: sidebarScope, onSidebarTrailChange: setSidebarTrailId }) })) })] }));
2492
2515
  }
2493
2516
  //# sourceMappingURL=SubsystemComponentGraph.js.map