@principal-ai/subsystems-react 0.56.3 → 0.56.4

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 (50) hide show
  1. package/dist/index.d.ts +6 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/subsystem/DecorationPanel.d.ts +37 -0
  6. package/dist/subsystem/DecorationPanel.d.ts.map +1 -0
  7. package/dist/subsystem/DecorationPanel.js +147 -0
  8. package/dist/subsystem/DecorationPanel.js.map +1 -0
  9. package/dist/subsystem/DiagnosticToggle.d.ts +7 -1
  10. package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -1
  11. package/dist/subsystem/DiagnosticToggle.js +8 -7
  12. package/dist/subsystem/DiagnosticToggle.js.map +1 -1
  13. package/dist/subsystem/GraphSidebar.d.ts +4 -2
  14. package/dist/subsystem/GraphSidebar.d.ts.map +1 -1
  15. package/dist/subsystem/GraphSidebar.js +3 -1
  16. package/dist/subsystem/GraphSidebar.js.map +1 -1
  17. package/dist/subsystem/SubsystemComponentGraph.d.ts +10 -13
  18. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  19. package/dist/subsystem/SubsystemComponentGraph.js +112 -272
  20. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  21. package/dist/subsystem/TrailStepsNav.d.ts.map +1 -1
  22. package/dist/subsystem/TrailStepsNav.js +6 -4
  23. package/dist/subsystem/TrailStepsNav.js.map +1 -1
  24. package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
  25. package/dist/subsystem/TrailsPanel.js +14 -10
  26. package/dist/subsystem/TrailsPanel.js.map +1 -1
  27. package/dist/subsystem/decoration.d.ts +112 -0
  28. package/dist/subsystem/decoration.d.ts.map +1 -0
  29. package/dist/subsystem/decoration.js +54 -0
  30. package/dist/subsystem/decoration.js.map +1 -0
  31. package/package.json +3 -3
  32. package/src/index.ts +37 -0
  33. package/src/stories/Canvases/C4/C4ToComponentDrill.stories.tsx +38 -71
  34. package/src/stories/Canvases/Subsystem/GraphTitle.stories.tsx +1 -2
  35. package/src/stories/Canvases/Subsystem/StepEmphasis.stories.tsx +528 -0
  36. package/src/stories/Components/DecorationPanel.stories.tsx +160 -0
  37. package/src/subsystem/DecorationPanel.test.tsx +128 -0
  38. package/src/subsystem/DecorationPanel.tsx +334 -0
  39. package/src/subsystem/DiagnosticToggle.tsx +20 -5
  40. package/src/subsystem/GraphSidebar.test.tsx +18 -0
  41. package/src/subsystem/GraphSidebar.tsx +6 -1
  42. package/src/subsystem/SubsystemComponentGraph.sidebar.test.tsx +210 -0
  43. package/src/subsystem/SubsystemComponentGraph.tsx +184 -414
  44. package/src/subsystem/TrailStepsNav.test.tsx +32 -0
  45. package/src/subsystem/TrailStepsNav.tsx +6 -4
  46. package/src/subsystem/TrailsPanel.test.tsx +20 -0
  47. package/src/subsystem/TrailsPanel.tsx +11 -1
  48. package/src/subsystem/decoration.test.ts +72 -0
  49. package/src/subsystem/decoration.ts +151 -0
  50. package/src/subsystem/stepEmphasis.test.ts +110 -0
@@ -18,7 +18,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
18
18
  import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
19
19
  import { ReactFlow, ReactFlowProvider, useReactFlow, useViewport, applyNodeChanges, } from '@xyflow/react';
20
20
  import { useTheme } from '@principal-ade/industry-theme';
21
- import { FileText, ArrowDown, ArrowRight, Unlink, X } from 'lucide-react';
21
+ import { ArrowDown, ArrowRight, Files, Route, Unlink, X } from 'lucide-react';
22
22
  import { IndustryMarkdownSlide } from 'themed-markdown';
23
23
  import { buildSubsystemGraph, deriveGraphEdges, isConstructsOnlyModel, moduleGroupNodeId, processGroupNodeId, isTrailMechanism, edgeColor, MECHANISM_DESCRIPTIONS, subsystemGraphLayoutKey, trailStepGraphEdgeId, unroutedEdgeIds, } from './model';
24
24
  import { ConstructsCatalog } from './ConstructsCatalog';
@@ -27,15 +27,14 @@ import { MaintainLivePanel, } from '../components/maintain-events/MaintainLivePa
27
27
  import { SubsystemComponentNode, SubsystemGroupNode, SubsystemEdge, SUBSYSTEM_CALLBACKS, hexWithAlpha, EDGE_DIM_ALPHA, fileMatchForNode, flowElementVisibility, flowNodeVisibility } from './nodes';
28
28
  import { SubsystemDiagnosticToggle } from './DiagnosticToggle';
29
29
  import { SubsystemIssueList, issueCategory, issueKindOrder, issueRung, ISSUE_KIND_ICON, ISSUE_RUNG_ORDER, } from './IssueList';
30
- import { SubsystemFileTree } from './SubsystemFileTree';
31
30
  import { GraphLayoutCover } from './GraphLayoutCover';
31
+ import { SubsystemFileTree } from './SubsystemFileTree';
32
+ import { buildTreePathMapping } from './paths';
32
33
  import { GRAPH_CANVAS_CLASS, GRAPH_NAV_PROPS, GraphChrome, GraphLayerStyle, LAYOUT_FLIP_CLASS, LAYOUT_FLIP_EDGE_MS, LAYOUT_FLIP_NODE_MS } from './graphChrome';
33
34
  import { ComponentDeclaration } from './ComponentDeclaration';
34
35
  import { FileDrawer, FILE_DRAWER_HEIGHT_MS } from './FileDrawer';
35
36
  import { GraphWithSidebar } from './GraphWithSidebar';
36
- import { buildRepoGroups, repoAvatarUrl } from './paths';
37
37
  import { TrailsPanel, TRAIL_PLAY_PAUSE_MS } from './TrailsPanel';
38
- import { TrailStackBreadcrumb } from './TrailStackBreadcrumb';
39
38
  import { EDGE_LABEL_WIDTH, EDGE_LABEL_HEIGHT, EDGE_LABEL_FONT_SIZE, EDGE_LABEL_CLOUD_PATH, EDGE_LABEL_CLOUD_EXTRA_TOP, } from '../utils/edgeLabel';
40
39
  /** Identifiers a token could match to name this component as a construct. */
41
40
  function constructIdentifiers(comp) {
@@ -128,9 +127,9 @@ const TrailDrawerContent = memo(function TrailDrawerContent({ render, trail, ste
128
127
  onVisibleStepChange,
129
128
  }) }));
130
129
  });
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, title, hideSidebar, trailStepMode = 'focus', autoPlayTrails = false, trailAutoPlayIntervalMs = TRAIL_PLAY_PAUSE_MS, zoomOnTrailFocus = true, trailFocusDurationMs = 300, graphTitle, showTrailTitle = false, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, diagnostic, issues, showIssues, focusIssueCategory, laneStatus, onSelectIssue, onApplyIssueFix, onHoverIssue, renderFileViewer, renderTrailViewer, onFileSelect, componentVerification, onInspectSymbol, boundaryColors, codeProcessKeys, hideDrawer = false, persistKey, liveEvents, agentsPanel }) {
130
+ 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 }) {
132
131
  const { theme } = useTheme();
133
- const { fitView, fitBounds, screenToFlowPosition } = useReactFlow();
132
+ const { fitView, fitBounds } = useReactFlow();
134
133
  const viewport = useViewport();
135
134
  // Restored once per mount from `localStorage` (see `readViewState`). Each
136
135
  // graph is its own tab/mount, so `persistKey` is stable for a mount.
@@ -286,73 +285,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
286
285
  drawerOpenRef.current = drawerTarget != null;
287
286
  // Pending deferred fit after opening the drawer; cancelled on re-entry / unmount.
288
287
  const pendingFocusFitRef = useRef(null);
289
- // Component the pointer is over (null on leave) → transient tree highlight.
290
- const [hoveredComponentAlias, setHoveredComponentId] = useState(null);
291
- // Boundary frame hovered on the canvas → highlight the matching folder row in
292
- // the sidebar file tree, so a frame's place in the repo tree is visible.
293
- const [hoveredRegion, setHoveredRegion] = useState(null);
294
- const hoveredBoundaryPath = hoveredRegion && (hoveredRegion.kind === 'module' || hoveredRegion.kind === 'directory')
295
- ? hoveredRegion.key
296
- : null;
297
- // Path frames (module / directory) as absolute flow-coord rects, hit-tested
298
- // on pointer move. A pointer hit-test keeps the frames' interior hoverable
299
- // WITHOUT an overlay that would block edges / child nodes.
300
- const pathFrameRects = useMemo(() => {
301
- const out = [];
302
- for (const n of built.nodes) {
303
- if (n.type !== 'subsystem-group')
304
- continue;
305
- const region = n.data?.region;
306
- if (!region?.key || (region.kind !== 'module' && region.kind !== 'directory'))
307
- continue;
308
- const r = built.absoluteRects.get(n.id);
309
- if (!r)
310
- continue;
311
- out.push({ kind: region.kind, key: region.key, x: r.x, y: r.y, w: r.width, h: r.height });
312
- }
313
- return out;
314
- }, [built.nodes, built.absoluteRects]);
315
- useEffect(() => {
316
- if (pathFrameRects.length === 0)
317
- return;
318
- let raf = 0;
319
- let last = null;
320
- const overCanvas = (e) => e.composedPath().some((el) => el instanceof HTMLElement && el.classList.contains(GRAPH_CANVAS_CLASS));
321
- const onMove = (e) => {
322
- if (!overCanvas(e)) {
323
- last = null;
324
- setHoveredRegion((prev) => (prev == null ? prev : null));
325
- return;
326
- }
327
- last = { x: e.clientX, y: e.clientY };
328
- if (raf)
329
- return;
330
- raf = window.requestAnimationFrame(() => {
331
- raf = 0;
332
- if (!last)
333
- return;
334
- const p = screenToFlowPosition(last);
335
- let best = null;
336
- let bestArea = Number.POSITIVE_INFINITY;
337
- for (const f of pathFrameRects) {
338
- if (p.x >= f.x && p.x <= f.x + f.w && p.y >= f.y && p.y <= f.y + f.h) {
339
- const area = f.w * f.h;
340
- if (area < bestArea) {
341
- bestArea = area;
342
- best = { kind: f.kind, key: f.key };
343
- }
344
- }
345
- }
346
- setHoveredRegion((prev) => prev?.kind === best?.kind && prev?.key === best?.key ? prev : best);
347
- });
348
- };
349
- window.addEventListener('mousemove', onMove);
350
- return () => {
351
- window.removeEventListener('mousemove', onMove);
352
- if (raf)
353
- cancelAnimationFrame(raf);
354
- };
355
- }, [pathFrameRects, screenToFlowPosition]);
356
288
  // Trail focus — selected flow (or step) is full strength; other
357
289
  // opened-flow members stay visible but dimmed; everything else is hidden.
358
290
  // Restored from the persisted view state when a `persistKey` is set.
@@ -365,44 +297,24 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
365
297
  // a number = that step. Transient — no drawer. Camera only reframes when
366
298
  // a step is already selected.
367
299
  const [hoveredTrailStep, setHoveredTrailStep] = useState(null);
368
- // Sidebar bottom half: which panel is shown when trails exist.
369
- // The persisted value is membership-checked rather than trusted: a stale
370
- // localStorage entry from before the walkthrough->trail rename would match
371
- // neither panel and render a blank sidebar with no selected tab.
372
- const [sidebarView, setSidebarView] = useState(() => {
373
- const stored = persisted.sidebarView;
374
- const restored = stored === 'files' || stored === 'trails' ? stored : 'trails';
375
- return trails?.length ? restored : 'files';
376
- });
377
300
  // Diagnostics area tab: issues list vs the Maintain agent pipeline.
378
301
  const [diagnosticsTab, setDiagnosticsTab] = useState(() => persisted.diagnosticsTab ?? 'issues');
379
- // One edge source at a time. When the caller doesn't pick, the sidebar's
380
- // Files / Trails tab picks: Files draws graphify static edges,
381
- // Trails draws runtime step edges. Without visible tabs (no trails,
382
- // or a sidebar-less embed) fall back to trail edges.
383
- const sidebarTabsVisible = !hideSidebar && (trails?.length ?? 0) > 0;
302
+ // One edge source at a time. Prefer runtime steps when trails exist;
303
+ // otherwise draw static relations, unless the caller picks explicitly.
384
304
  // A solo trail layout only draws that flow's steps, so the trail edge source
385
305
  // is the only one that can render anything.
386
306
  const resolvedEdgeView = committedTrailId
387
307
  ? 'trails'
388
308
  : edgeView ??
389
- (sidebarTabsVisible
390
- ? (sidebarView === 'trails' ? 'trails' : 'graphify')
391
- : (trails?.length ?? 0) > 0
392
- ? 'trails'
393
- : (graphifyRelations?.length ?? 0) > 0
394
- ? 'graphify'
395
- : 'trails');
309
+ ((trails?.length ?? 0) > 0
310
+ ? 'trails'
311
+ : (graphifyRelations?.length ?? 0) > 0
312
+ ? 'graphify'
313
+ : 'trails');
396
314
  // Trail flows the user has expanded (via the title row). Closed by
397
315
  // default so a graph with several flows doesn't dump every step list at once.
398
316
  // Restored from the persisted view state when a `persistKey` is set.
399
317
  const [expandedTrails, setExpandedTrails] = useState(() => new Set(persisted.expandedTrails ?? []));
400
- // Sidebar description visibility. Hidden by default so the files/flows
401
- // panel gets the vertical room; the title-row toggle reveals it.
402
- const [descriptionVisible, setDescriptionVisible] = useState(false);
403
- const [descToggleHover, setDescToggleHover] = useState(false);
404
- // `true` only when a description exists AND the user opened it.
405
- const showDesc = !!description && descriptionVisible;
406
318
  // Sidebar width lives in the shared GraphWithSidebar chrome — draggable and
407
319
  // persisted under `principal.subsystems.sidebarWidth.<persistKey>`. The graph
408
320
  // caps it at half the view width so the canvas keeps room to breathe;
@@ -427,11 +339,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
427
339
  return;
428
340
  writeViewState(persistKey, { focusedStepIndex });
429
341
  }, [persistKey, focusedStepIndex]);
430
- useEffect(() => {
431
- if (!persistKey)
432
- return;
433
- writeViewState(persistKey, { sidebarView });
434
- }, [persistKey, sidebarView]);
435
342
  useEffect(() => {
436
343
  if (!persistKey)
437
344
  return;
@@ -757,12 +664,10 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
757
664
  };
758
665
  SUBSYSTEM_CALLBACKS.onEdgeSelect = selectEdge;
759
666
  SUBSYSTEM_CALLBACKS.maxNodeWidth = maxNodeWidth;
760
- SUBSYSTEM_CALLBACKS.onHover = setHoveredComponentId;
761
667
  return () => {
762
668
  SUBSYSTEM_CALLBACKS.onSelect = undefined;
763
669
  SUBSYSTEM_CALLBACKS.onEdgeSelect = undefined;
764
670
  SUBSYSTEM_CALLBACKS.maxNodeWidth = undefined;
765
- SUBSYSTEM_CALLBACKS.onHover = undefined;
766
671
  };
767
672
  }, [components, onSelect, maxNodeWidth, selectEdge]);
768
673
  const { nodes, edges: convertedEdges } = built;
@@ -1355,18 +1260,13 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
1355
1260
  setFocusedStepIndex(null);
1356
1261
  issueModuleFocusRef.current = null;
1357
1262
  }, []);
1358
- // Sidebar file trees — one per repo on multi-repo graphs, each under its
1359
- // own owner-avatar header. Clicking a header collapses that repo's tree.
1360
- const repoGroups = useMemo(() => buildRepoGroups(components), [components]);
1361
1263
  const hasTrails = useMemo(() => (trails?.length ?? 0) > 0, [trails]);
1264
+ const treePaths = useMemo(() => buildTreePathMapping(components), [components]);
1362
1265
  // Alias → component, so the Trails panel can show human hop names instead
1363
1266
  // of model-local aliases (and the site/`via` distinction can read).
1364
1267
  const componentByAlias = useMemo(() => new Map(components.map((c) => [c.alias, c])), [components]);
1365
- // Diagnostics view in the sidebar. Uncontrolled unless the host pins
1366
- // `showIssues`: the title-row chip toggles it, seeded from issues presence.
1367
- const [diagnosticsOpen, setDiagnosticsOpen] = useState(showIssues ?? (issues?.length ?? 0) > 0);
1368
- const controlledIssues = showIssues !== undefined;
1369
- const issuesActive = controlledIssues ? showIssues : diagnosticsOpen;
1268
+ // The outer header owns the diagnostics toggle and passes its resolved state.
1269
+ const issuesActive = showIssues === true;
1370
1270
  // Layer focus only applies while the diagnostics list is on screen — closing
1371
1271
  // it clears any dimming the expanded layers were driving.
1372
1272
  useEffect(() => {
@@ -1374,32 +1274,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
1374
1274
  setExpandedIssueCategories([]);
1375
1275
  }
1376
1276
  }, [issuesActive]);
1377
- const toggleIssues = () => {
1378
- if (!controlledIssues)
1379
- setDiagnosticsOpen((v) => !v);
1380
- diagnostic?.onToggle?.();
1381
- };
1382
- const [collapsedRepos, setCollapsedRepos] = useState(new Set());
1383
- const toggleRepoCollapsed = useCallback((key) => {
1384
- setCollapsedRepos((prev) => {
1385
- const next = new Set(prev);
1386
- if (next.has(key))
1387
- next.delete(key);
1388
- else
1389
- next.add(key);
1390
- return next;
1391
- });
1392
- }, []);
1393
- const treeFiles = useMemo(() => repoGroups.groups.flatMap((g) => g.entries.map((e) => e.file)), [repoGroups]);
1394
- const onTreeSelectFile = useCallback((file) => {
1395
- setFileOverlay(null);
1396
- if (openFileRef.current?.file === file && openFileRef.current.startLine == null) {
1397
- setDrawerTarget(null);
1398
- return;
1399
- }
1400
- setDrawerTarget({ kind: 'file', file });
1401
- onFileSelect?.(file);
1402
- }, [onFileSelect]);
1403
1277
  const onOpenDeclarationFile = useCallback((file, opts) => {
1404
1278
  setFileOverlay(null);
1405
1279
  const startLine = opts?.startLine;
@@ -1648,9 +1522,22 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
1648
1522
  setDrawerTarget((prev) => (prev?.kind === 'trail' ? null : prev));
1649
1523
  onTrailSelect?.(null);
1650
1524
  }, [onTrailSelect]);
1525
+ // A hidden trail must not leave the file view filtered to its cast.
1526
+ const filesWereActive = useRef(false);
1527
+ useEffect(() => {
1528
+ const enteringFiles = filesActive && !filesWereActive.current;
1529
+ filesWereActive.current = filesActive;
1530
+ if (!enteringFiles)
1531
+ return;
1532
+ clearTrailFocus();
1533
+ setExpandedTrails(new Set());
1534
+ setSelectedEdgeId(null);
1535
+ setLayoutTrailId(null);
1536
+ onTrailStepHover?.(null);
1537
+ }, [filesActive, clearTrailFocus, onTrailStepHover]);
1651
1538
  // Host deep-link: opening the graph from a trail row selects that
1652
- // flow — expand its steps, focus its edges, and switch the sidebar to
1653
- // Trails. Guarded by a ref so a later in-tab selection isn't yanked
1539
+ // flow — expand its steps and focus its edges.
1540
+ // Guarded by a ref so a later in-tab selection isn't yanked
1654
1541
  // back; a remount (or a new id) re-applies it.
1655
1542
  const appliedInitialTrailRef = useRef(null);
1656
1543
  useEffect(() => {
@@ -1664,22 +1551,9 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
1664
1551
  if (!tl)
1665
1552
  return;
1666
1553
  appliedInitialTrailRef.current = initialTrailId;
1667
- setSidebarView('trails');
1668
1554
  setExpandedTrails((prev) => new Set(prev).add(tl.id));
1669
1555
  focusTrailEdges(tl);
1670
1556
  }, [initialTrailId, layoutReady, trails, focusTrailEdges]);
1671
- // Switching sidebar panels also switches the edge vocabulary. Leaving the
1672
- // Trails panel drops its canvas state (focus, expanded flows, selected
1673
- // edge) so the Files view's topology edges aren't gated by a flow the user
1674
- // can no longer see or un-dim.
1675
- const changeSidebarView = useCallback((view) => {
1676
- setSidebarView(view);
1677
- if (view === 'trails')
1678
- return;
1679
- clearTrailFocus();
1680
- setExpandedTrails(new Set());
1681
- setSelectedEdgeId(null);
1682
- }, [clearTrailFocus]);
1683
1557
  // Focus a single step's edge on the canvas and open/scroll the trail
1684
1558
  // drawer to that step's snippet. Clicking the already-focused step clears
1685
1559
  // step focus and returns to whole-flow (high-level) framing.
@@ -2142,14 +2016,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
2142
2016
  });
2143
2017
  return labels;
2144
2018
  }, [dispEdges, selectedFlowStepNos, stepIssueBadgeByEdgeId]);
2145
- // Unique source files across components → sidebar file trees.
2146
- const treeFilePaths = treeFiles;
2147
- // The hovered node's file (null when not hovering / file-less component).
2148
- const hoveredFile = useMemo(() => {
2149
- if (!hoveredComponentAlias)
2150
- return null;
2151
- return components.find((c) => c.alias === hoveredComponentAlias)?.file ?? null;
2152
- }, [components, hoveredComponentAlias]);
2153
2019
  // Drawer content renderer. Path-keyed only — the component-keyed
2154
2020
  // `renderFileView` variant was removed; nothing in-repo passed it.
2155
2021
  const fileViewer = renderFileViewer;
@@ -2159,15 +2025,15 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
2159
2025
  const trailViewerRef = useRef(renderTrailViewer);
2160
2026
  trailViewerRef.current = renderTrailViewer;
2161
2027
  const renderTrailDrawerContent = useCallback((ctx) => trailViewerRef.current?.(ctx) ?? null, []);
2162
- // Sidebar chrome — title/description header, Files/Trails (or Issues/Agents)
2163
- // tab strip, and the lower panels. Everything renders through the shared
2028
+ // Sidebar chrome — description header, files/trails, and optional Issues/Agents
2029
+ // diagnostics. Everything renders through the shared
2164
2030
  // GraphWithSidebar; the graph keeps owning every bit of *state* (focused
2165
- // trail, active tab, description visibility) because the canvas dims from
2031
+ // trail, diagnostics tab) because the canvas dims from
2166
2032
  // the same facts the sidebar renders.
2167
2033
  const sidebarVisible = !hideSidebar &&
2168
- (title || description || diagnostic || issuesActive || sidebarExtra || sidebarAfterDescription ||
2169
- treeFilePaths.length > 0 || hasTrails);
2170
- const sidebarBodyVisible = treeFilePaths.length > 0 || hasTrails || issuesActive;
2034
+ (description || filesActive || issuesActive || sidebarExtra || sidebarAfterDescription ||
2035
+ hasTrails);
2036
+ const sidebarBodyVisible = hasTrails || filesActive || issuesActive;
2171
2037
  const sidebarTabs = issuesActive
2172
2038
  ? agentsPanel
2173
2039
  ? [
@@ -2175,49 +2041,15 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
2175
2041
  { id: 'agents', label: 'Agents' },
2176
2042
  ]
2177
2043
  : undefined
2178
- : hasTrails
2179
- ? [
2180
- { id: 'trails', label: 'Trails' },
2181
- { id: 'files', label: 'Files' },
2182
- ]
2183
- : undefined;
2184
- const activeSidebarTab = issuesActive ? diagnosticsTab : sidebarView;
2185
- // Header block: the extras, then the title/actions row (diagnostic chip +
2186
- // description toggle), then the description that toggles out from under it.
2187
- const sidebarHeader = (_jsxs(_Fragment, { children: [sidebarExtra, (title || description || diagnostic) && (_jsxs("div", { style: { display: 'flex', alignItems: 'flex-start', gap: 8 }, children: [title && (_jsx("h2", { style: {
2188
- margin: 0,
2189
- flex: 1,
2190
- minWidth: 0,
2191
- fontSize: theme.fontSizes[2],
2192
- fontWeight: 600,
2193
- color: theme.colors.text,
2194
- fontFamily: theme.fonts.monospace,
2195
- }, children: title })), _jsxs("div", { style: {
2196
- display: 'flex',
2197
- alignItems: 'center',
2198
- gap: 4,
2199
- flexShrink: 0,
2200
- // Title's flex:1 already pushes this cluster right; without a
2201
- // title, keep the actions flush right instead of flush-left.
2202
- marginLeft: title ? undefined : 'auto',
2203
- }, children: [diagnostic && (_jsx(SubsystemDiagnosticToggle, { ...diagnostic, active: issuesActive, onToggle: toggleIssues })), description && (_jsx("button", { type: "button", "aria-expanded": descriptionVisible, "aria-label": descriptionVisible ? 'Hide description' : 'Show description', title: descriptionVisible ? 'Hide description' : 'Show description', onMouseEnter: () => setDescToggleHover(true), onMouseLeave: () => setDescToggleHover(false), onClick: () => setDescriptionVisible((v) => !v), style: {
2204
- display: 'inline-flex',
2205
- alignItems: 'center',
2206
- justifyContent: 'center',
2207
- flexShrink: 0,
2208
- width: 22,
2209
- height: 22,
2210
- padding: 0,
2211
- border: 'none',
2212
- borderRadius: 4,
2213
- background: descriptionVisible || descToggleHover ? theme.colors.border : 'transparent',
2214
- color: descriptionVisible || descToggleHover ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
2215
- cursor: 'pointer',
2216
- transition: 'background 120ms ease, color 120ms ease',
2217
- }, children: _jsx(FileText, { size: 14 }) }))] })] })), description && descriptionVisible && (_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] }));
2218
- // Lower panel: the Issues/Agents diagnostics stack, or the Files/Trails
2219
- // stack with the trail flow list / repo file tree.
2220
- const sidebarBody = issuesActive ? (diagnosticsTab === 'agents' && agentsPanel ? (_jsx(SubsystemAgentsPanel, { ...agentsPanel })) : (_jsx(SubsystemIssueList, { issues: issues ?? [], focusCategory: focusIssueCategory, laneStatus: laneStatus, onSelectIssue: focusIssueTarget, onDeselectIssue: unfocusIssueTarget, onApplyFix: onApplyIssueFix, onHoverIssue: onHoverIssue, onExpandedCategoriesChange: handleExpandedCategoriesChange }))) : sidebarView === 'trails' && trails && trails.length > 0 ? (_jsx(TrailsPanel, { trails: trails, expandedTrails: expandedTrails, focusedTrailId: focusedTrailId, focusedStepIndex: focusedStepIndex, hoveredTrailStep: hoveredTrailStep, onToggleCollapsed: toggleTrailCollapsed, onFocusFlow: (tl) => {
2044
+ : undefined;
2045
+ // Header block: extras, then the description.
2046
+ 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] }));
2047
+ // Lower panel: diagnostics, the file tree, or the trail flow list.
2048
+ const sidebarBody = issuesActive ? (diagnosticsTab === 'agents' && agentsPanel ? (_jsx(SubsystemAgentsPanel, { ...agentsPanel })) : (_jsx(SubsystemIssueList, { issues: issues ?? [], focusCategory: focusIssueCategory, laneStatus: laneStatus, onSelectIssue: focusIssueTarget, onDeselectIssue: unfocusIssueTarget, onApplyFix: onApplyIssueFix, onHoverIssue: onHoverIssue, onExpandedCategoriesChange: handleExpandedCategoriesChange }))) : filesActive ? (_jsx(SubsystemFileTree, { files: treePaths.entries.map((entry) => entry.displayPath), selectedFile: openFile ? treePaths.toDisplay.get(openFile) : null, onSelectFile: (path) => {
2049
+ const file = treePaths.toFile.get(path);
2050
+ if (file)
2051
+ onOpenDeclarationFile(file);
2052
+ } })) : trails && trails.length > 0 ? (_jsx(TrailsPanel, { trails: trails, expandedTrails: expandedTrails, focusedTrailId: focusedTrailId, focusedStepIndex: focusedStepIndex, hoveredTrailStep: hoveredTrailStep, onToggleCollapsed: toggleTrailCollapsed, onFocusFlow: (tl) => {
2221
2053
  // While a solo layout is on, following the selection keeps the canvas
2222
2054
  // and the highlighted flow in agreement instead of leaving the graph
2223
2055
  // pinned to a flow the user just navigated away from.
@@ -2233,17 +2065,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
2233
2065
  }, onLeaveStep: () => {
2234
2066
  setHoveredTrailStep(null);
2235
2067
  onTrailStepHover?.(null);
2236
- }, onReorder: onReorderTrails, componentByAlias: componentByAlias, stepEmphasis: stepEmphasis, onRemoveStepEmphasis: onRemoveStepEmphasis, layoutFocusedTrailId: layoutTrailId, onFocusLayout: toggleLayoutFocus })) : treeFilePaths.length > 0 ? (_jsx(_Fragment, { children: repoGroups.groups.map((group, i) => {
2237
- const groupKey = group.repoKey ?? '__no-repo__';
2238
- const collapsed = collapsedRepos.has(groupKey);
2239
- return (_jsxs("div", { style: {
2240
- flex: collapsed ? '0 0 auto' : 1,
2241
- minHeight: collapsed ? 0 : undefined,
2242
- display: 'flex',
2243
- flexDirection: 'column',
2244
- borderTop: i > 0 ? `1px solid ${theme.colors.border}` : undefined,
2245
- }, children: [repoGroups.multiRepo && (_jsx(RepoGroupHeader, { group: group, collapsed: collapsed, onToggle: () => toggleRepoCollapsed(groupKey) })), !collapsed && (_jsx(SubsystemFileTree, { files: group.entries.map((e) => e.displayPath), selectedFile: selected?.file ?? openFile, hoveredFile: hoveredFile, onSelectFile: onTreeSelectFile, hoveredFolder: hoveredBoundaryPath, headerless: repoGroups.multiRepo }))] }, groupKey));
2246
- }) })) : null;
2068
+ }, onReorder: onReorderTrails, componentByAlias: componentByAlias, stepEmphasis: stepEmphasis, onRemoveStepEmphasis: onRemoveStepEmphasis, layoutFocusedTrailId: layoutTrailId, onFocusLayout: toggleLayoutFocus })) : null;
2247
2069
  const fileOverlayView = fileOverlay && (_jsxs("div", { style: {
2248
2070
  position: 'absolute',
2249
2071
  inset: 0,
@@ -2269,9 +2091,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
2269
2091
  fontSize: theme.fontSizes[0],
2270
2092
  color: theme.colors.textMuted ?? theme.colors.textSecondary,
2271
2093
  }, 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 }) })] }));
2272
- return (_jsx(GraphWithSidebar, { hidden: !sidebarVisible, bodyHidden: !sidebarBodyVisible, stretchHeader: showDesc, header: sidebarHeader, tabs: sidebarTabs, activeTab: activeSidebarTab, onTabChange: (id) => issuesActive
2273
- ? setDiagnosticsTab(id)
2274
- : changeSidebarView(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: {
2094
+ return (_jsx(GraphWithSidebar, { hidden: !sidebarVisible, bodyHidden: !sidebarBodyVisible, headerMaxHeight: sidebarBodyVisible ? '50%' : '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: {
2275
2095
  position: 'relative',
2276
2096
  flex: collapseCanvas ? 0 : 1,
2277
2097
  minHeight: 0,
@@ -2541,55 +2361,48 @@ function Inner({ components, trails, graphifyRelations, orderByLine, layoutDirec
2541
2361
  drawerTarget.startLine != null &&
2542
2362
  !!selected.file &&
2543
2363
  selected.file === drawerTarget.file &&
2544
- 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, subheader: drawerTarget?.kind === 'trail' &&
2545
- focusedTrail &&
2546
- drawerTarget.stepIndex != null ? (_jsx(TrailStackBreadcrumb, { trail: focusedTrail, stepIndex: drawerTarget.stepIndex, componentByAlias: componentByAlias, onFocusStep: (index) => focusTrailStep(focusedTrail, index) })) : null, children: drawerTarget?.kind === 'trail' &&
2364
+ 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' &&
2547
2365
  focusedTrail &&
2548
2366
  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] }) }));
2549
2367
  }
2550
- /** Sidebar header for one repo's tree: owner avatar + repo name + file count.
2551
- * Clicking toggles the tree's visibility. */
2552
- function RepoGroupHeader({ group, collapsed, onToggle, }) {
2553
- const { theme } = useTheme();
2554
- const avatar = repoAvatarUrl(group.repoKey);
2555
- const label = group.repo ?? 'No repo';
2556
- return (_jsxs("div", { role: "button", "aria-expanded": !collapsed, onClick: (e) => {
2557
- e.stopPropagation();
2558
- onToggle();
2559
- }, title: collapsed ? 'Show files' : 'Hide files', style: {
2560
- display: 'flex',
2561
- alignItems: 'center',
2562
- gap: 8,
2563
- padding: '10px 16px 4px',
2564
- flexShrink: 0,
2565
- cursor: 'pointer',
2566
- userSelect: 'none',
2567
- }, children: [avatar ? (_jsx("img", { src: avatar, alt: "", width: 16, height: 16, loading: "lazy", style: { borderRadius: 4, flexShrink: 0, background: theme.colors.border } })) : group.owner ? (_jsx("span", { style: {
2568
- width: 16,
2569
- height: 16,
2570
- borderRadius: 4,
2571
- flexShrink: 0,
2572
- display: 'inline-flex',
2573
- alignItems: 'center',
2574
- justifyContent: 'center',
2575
- fontSize: 9,
2576
- fontWeight: 700,
2577
- fontFamily: theme.fonts.monospace,
2578
- color: theme.colors.text,
2579
- background: theme.colors.border,
2580
- }, children: group.owner.charAt(0).toUpperCase() })) : null, _jsx("span", { style: {
2581
- fontSize: theme.fontSizes[1],
2582
- fontFamily: theme.fonts.monospace,
2583
- color: theme.colors.text,
2584
- fontWeight: 600,
2585
- overflow: 'hidden',
2586
- textOverflow: 'ellipsis',
2587
- whiteSpace: 'nowrap',
2588
- }, children: label })] }));
2589
- }
2590
2368
  export function SubsystemComponentGraph(props) {
2369
+ const { theme } = useTheme();
2591
2370
  const wrapRef = useRef(null);
2592
2371
  const [size, setSize] = useState(null);
2372
+ const [filesOpen, setFilesOpen] = useState(false);
2373
+ const fileCount = useMemo(() => buildTreePathMapping(props.components).entries.length, [props.components]);
2374
+ const hasFiles = fileCount > 0;
2375
+ const [diagnosticsOpen, setDiagnosticsOpen] = useState(props.showIssues ?? (props.issues?.length ?? 0) > 0);
2376
+ const issuesActive = props.showIssues ?? diagnosticsOpen;
2377
+ const filesActive = filesOpen && hasFiles && !issuesActive;
2378
+ const selectSidebarView = (view) => {
2379
+ const nextIssuesActive = view === 'issues';
2380
+ setFilesOpen(view === 'files');
2381
+ if (nextIssuesActive !== issuesActive) {
2382
+ if (props.showIssues === undefined)
2383
+ setDiagnosticsOpen(nextIssuesActive);
2384
+ props.diagnostic?.onToggle?.();
2385
+ }
2386
+ };
2387
+ const viewButtonStyle = (active) => ({
2388
+ display: 'inline-flex',
2389
+ alignItems: 'center',
2390
+ justifyContent: 'center',
2391
+ gap: 4,
2392
+ height: 28,
2393
+ padding: '4px 7px',
2394
+ fontFamily: theme.fonts.monospace,
2395
+ fontSize: theme.fontSizes[0],
2396
+ border: 'none',
2397
+ borderRadius: 4,
2398
+ background: active ? theme.colors.border : 'transparent',
2399
+ color: active ? theme.colors.text : (theme.colors.textMuted ?? theme.colors.textSecondary),
2400
+ cursor: 'pointer',
2401
+ });
2402
+ useEffect(() => {
2403
+ if (props.initialTrailId)
2404
+ setFilesOpen(false);
2405
+ }, [props.initialTrailId]);
2593
2406
  // Measure the container so React Flow always has a real, non-zero pixel size
2594
2407
  // (responsive to the parent; avoids the %→0 blank canvas).
2595
2408
  useEffect(() => {
@@ -2611,13 +2424,40 @@ export function SubsystemComponentGraph(props) {
2611
2424
  trails: props.trails,
2612
2425
  graphifyRelations: props.graphifyRelations,
2613
2426
  });
2614
- return (_jsx("div", { ref: wrapRef, style: {
2427
+ return (_jsxs("div", { style: {
2615
2428
  position: 'relative',
2616
2429
  width: '100%',
2617
2430
  height: '100%',
2618
2431
  minHeight: 0,
2619
2432
  display: 'flex',
2620
2433
  flexDirection: 'column',
2621
- }, children: _jsx("div", { 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, measured: size }) })) }) }));
2434
+ }, children: [!constructsOnly && !props.hideSidebar && (_jsxs("header", { style: {
2435
+ flexShrink: 0,
2436
+ display: 'flex',
2437
+ alignItems: 'center',
2438
+ gap: 12,
2439
+ padding: '12px 16px',
2440
+ background: theme.colors.background,
2441
+ borderBottom: `1px solid ${theme.colors.border}`,
2442
+ }, children: [props.title && (_jsx("h2", { style: {
2443
+ margin: 0,
2444
+ minWidth: 0,
2445
+ fontSize: theme.fontSizes[2],
2446
+ fontWeight: 600,
2447
+ color: theme.colors.text,
2448
+ fontFamily: theme.fonts.monospace,
2449
+ overflowWrap: 'anywhere',
2450
+ }, children: props.title })), _jsxs("div", { role: "group", "aria-label": "Sidebar view", style: {
2451
+ marginLeft: 'auto',
2452
+ flexShrink: 0,
2453
+ display: 'grid',
2454
+ gridAutoFlow: 'column',
2455
+ gridAutoColumns: '1fr',
2456
+ alignItems: 'center',
2457
+ gap: 2,
2458
+ padding: 2,
2459
+ border: `1px solid ${theme.colors.border}`,
2460
+ borderRadius: 6,
2461
+ }, children: [_jsxs("button", { type: "button", "aria-label": "Trails", "aria-pressed": !filesActive && !issuesActive, onClick: () => selectSidebarView('trails'), style: viewButtonStyle(!filesActive && !issuesActive), children: [_jsx(Route, { size: 14, "aria-hidden": true }), _jsx("span", { children: "Trails" })] }), 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, { ...(props.diagnostic ?? { status: 'unknown', issueCount: props.issues?.length }), 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 }) })) })] }));
2622
2462
  }
2623
2463
  //# sourceMappingURL=SubsystemComponentGraph.js.map