@principal-ai/subsystems-react 0.55.1 → 0.55.3

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 (105) hide show
  1. package/README.md +2 -1
  2. package/dist/graphify/index.d.ts +2 -0
  3. package/dist/graphify/index.d.ts.map +1 -1
  4. package/dist/graphify/index.js +1 -0
  5. package/dist/graphify/index.js.map +1 -1
  6. package/dist/graphify/symbolFocus.d.ts +49 -0
  7. package/dist/graphify/symbolFocus.d.ts.map +1 -0
  8. package/dist/graphify/symbolFocus.js +139 -0
  9. package/dist/graphify/symbolFocus.js.map +1 -0
  10. package/dist/index.d.ts +5 -3
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/stories/Canvases/Aggregate/aggregateViewFixture.d.ts.map +1 -0
  15. package/dist/stories/Canvases/Aggregate/aggregateViewFixture.js.map +1 -0
  16. package/dist/stories/Canvases/Subsystem/fixtures.d.ts.map +1 -0
  17. package/dist/stories/Canvases/Subsystem/fixtures.js.map +1 -0
  18. package/dist/subsystem/SubsystemComponentGraph.d.ts +35 -1
  19. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  20. package/dist/subsystem/SubsystemComponentGraph.js +248 -33
  21. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  22. package/dist/subsystem/SymbolFocusedView.d.ts +30 -0
  23. package/dist/subsystem/SymbolFocusedView.d.ts.map +1 -0
  24. package/dist/subsystem/SymbolFocusedView.js +24 -0
  25. package/dist/subsystem/SymbolFocusedView.js.map +1 -0
  26. package/dist/subsystem/TrailsPanel.d.ts +16 -1
  27. package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
  28. package/dist/subsystem/TrailsPanel.js +64 -8
  29. package/dist/subsystem/TrailsPanel.js.map +1 -1
  30. package/dist/subsystem/graphChrome.d.ts +13 -0
  31. package/dist/subsystem/graphChrome.d.ts.map +1 -1
  32. package/dist/subsystem/graphChrome.js +19 -1
  33. package/dist/subsystem/graphChrome.js.map +1 -1
  34. package/dist/subsystem/model.d.ts +40 -4
  35. package/dist/subsystem/model.d.ts.map +1 -1
  36. package/dist/subsystem/model.js +38 -7
  37. package/dist/subsystem/model.js.map +1 -1
  38. package/package.json +2 -2
  39. package/src/graphify/index.ts +6 -0
  40. package/src/graphify/symbolFocus.test.ts +163 -0
  41. package/src/graphify/symbolFocus.ts +190 -0
  42. package/src/index.ts +7 -0
  43. package/src/stories/{Subsystem/AggregateGraph → Canvases/Aggregate}/AggregateFrameGraph.stories.tsx +1 -1
  44. package/src/stories/{Subsystem/C4Graph → Canvases/C4}/C4CardGrow.stories.tsx +1 -1
  45. package/src/stories/{Subsystem/C4Graph → Canvases/C4}/C4Component.stories.tsx +1 -1
  46. package/src/stories/{Subsystem/C4Graph → Canvases/C4}/C4Container.stories.tsx +1 -1
  47. package/src/stories/{Subsystem/C4Graph → Canvases/C4}/C4NodeGallery.stories.tsx +1 -1
  48. package/src/stories/{Subsystem/C4Graph → Canvases/C4}/C4ToComponentDrill.stories.tsx +1 -1
  49. package/src/stories/{Subsystem/C4Graph → Canvases/C4}/C4Transition.stories.tsx +1 -1
  50. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/AnimatedProgression.stories.tsx +1 -1
  51. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Appearance.stories.tsx +1 -1
  52. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Basics.stories.tsx +1 -1
  53. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Captures.stories.tsx +1 -1
  54. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Constructs.stories.tsx +1 -1
  55. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/CustomEntities.stories.tsx +1 -1
  56. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Deprecated.stories.tsx +1 -1
  57. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/DetailPanel.stories.tsx +1 -1
  58. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Diagnostics.stories.tsx +1 -1
  59. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/EdgeViews.stories.tsx +1 -1
  60. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/FrameworkStereotype.stories.tsx +1 -1
  61. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/GraphTitle.stories.tsx +1 -1
  62. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Issues.stories.tsx +1 -1
  63. package/src/stories/Canvases/Subsystem/LayoutDirection.stories.tsx +122 -0
  64. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/ModuleBadges.stories.tsx +1 -1
  65. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/ModulePathNesting.stories.tsx +1 -1
  66. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Modules.stories.tsx +1 -1
  67. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/NodeAnatomy.stories.tsx +1 -1
  68. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Packages.stories.tsx +1 -1
  69. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Processes.stories.tsx +1 -1
  70. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Proposed.stories.tsx +1 -1
  71. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/ProposedTrails.stories.tsx +1 -1
  72. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Reorder.stories.tsx +1 -1
  73. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Scenarios.stories.tsx +1 -1
  74. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Spotlights.stories.tsx +1 -1
  75. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/StoreValueType.stories.tsx +1 -1
  76. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/TrailAutoplay.stories.tsx +1 -1
  77. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/Trails.stories.tsx +1 -1
  78. package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/WorkspacePackageFrames.stories.tsx +1 -1
  79. package/src/stories/Canvases/SymbolFocusedView/SymbolFocusedView.stories.tsx +163 -0
  80. package/src/stories/{Subsystem → Components}/AgentsPanel.stories.tsx +1 -1
  81. package/src/stories/{Subsystem/ComponentDeclarationAudit.stories.tsx → Components/ComponentDeclaration/Audit.stories.tsx} +4 -4
  82. package/src/stories/{Subsystem/ComponentDeclarationSymbols.stories.tsx → Components/ComponentDeclaration/Symbols.stories.tsx} +5 -5
  83. package/src/stories/{Subsystem → Components}/ConstructsCatalog.stories.tsx +1 -1
  84. package/src/stories/{Subsystem/Sidebar → Components}/GraphSidebar.stories.tsx +4 -4
  85. package/src/stories/{Subsystem/IssueIcons.stories.tsx → Components/IssueList.stories.tsx} +1 -1
  86. package/src/stories/{Subsystem/StoreValueTypeDeclaration.stories.tsx → Components/StoreValueType.stories.tsx} +1 -1
  87. package/src/subsystem/SubsystemComponentGraph.tsx +331 -25
  88. package/src/subsystem/SymbolFocusedView.tsx +80 -0
  89. package/src/subsystem/TrailsPanel.tsx +107 -4
  90. package/src/subsystem/graphChrome.tsx +22 -1
  91. package/src/subsystem/layoutDirection.test.ts +26 -0
  92. package/src/subsystem/model.test.ts +17 -0
  93. package/src/subsystem/model.ts +70 -10
  94. package/dist/stories/Subsystem/AggregateGraph/aggregateViewFixture.d.ts.map +0 -1
  95. package/dist/stories/Subsystem/AggregateGraph/aggregateViewFixture.js.map +0 -1
  96. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +0 -1
  97. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +0 -1
  98. package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +0 -351
  99. /package/dist/stories/{Subsystem/AggregateGraph → Canvases/Aggregate}/aggregateViewFixture.d.ts +0 -0
  100. /package/dist/stories/{Subsystem/AggregateGraph → Canvases/Aggregate}/aggregateViewFixture.js +0 -0
  101. /package/dist/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/fixtures.d.ts +0 -0
  102. /package/dist/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/fixtures.js +0 -0
  103. /package/src/graphify/__fixtures__/{ego-samples.json → symbol-focused-samples.json} +0 -0
  104. /package/src/stories/{Subsystem/AggregateGraph → Canvases/Aggregate}/aggregateViewFixture.ts +0 -0
  105. /package/src/stories/{Subsystem/ComponentGraph → Canvases/Subsystem}/fixtures.ts +0 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * SymbolFocusedView — one graphify symbol and its radius-1 neighborhood on the
3
+ * subsystem canvas.
4
+ *
5
+ * This is the public entry point for the "focused symbol" canvas: the host
6
+ * hands over the center node plus the neighbors and edges graphify already
7
+ * extracted, and the view projects them onto the shared component graph
8
+ * (`toSymbolFocusedView`). Edges keep their raw graphify verbs and draw from
9
+ * the graphify palette, so derived facts stay visually distinct from authored
10
+ * subsystem mechanisms.
11
+ *
12
+ * No trails, no drawers for a model: a symbol focus is read, not walked. Hover
13
+ * and select still work, and double-clicking a node opens its file like any
14
+ * other component node.
15
+ */
16
+
17
+ import { useMemo } from 'react';
18
+ import type {
19
+ SymbolFocusedViewInput,
20
+ SymbolFocusedViewOptions,
21
+ } from '../graphify/symbolFocus';
22
+ import { toSymbolFocusedView } from '../graphify/symbolFocus';
23
+ import type { SubsystemComponentGraphProps } from './SubsystemComponentGraph';
24
+ import { SubsystemComponentGraph } from './SubsystemComponentGraph';
25
+
26
+ /**
27
+ * Canvas chrome that is meaningful for a derived, trail-less projection. The
28
+ * authored-model inputs (`components`, `trails`, `graphifyRelations`) and the
29
+ * trail deep-link/playback props are deliberately not forwarded: the view
30
+ * owns them.
31
+ */
32
+ type SymbolFocusedViewChrome = Pick<
33
+ SubsystemComponentGraphProps,
34
+ | 'title'
35
+ | 'description'
36
+ | 'orderByLine'
37
+ | 'showEdgeLabels'
38
+ | 'showSingletonFrames'
39
+ | 'hideSidebar'
40
+ | 'hideDrawer'
41
+ | 'maxNodeWidth'
42
+ | 'persistKey'
43
+ | 'onSelect'
44
+ | 'onEdgeSelect'
45
+ | 'onFileSelect'
46
+ | 'componentVerification'
47
+ | 'onInspectSymbol'
48
+ | 'canvasOverlay'
49
+ >;
50
+
51
+ export interface SymbolFocusedViewProps
52
+ extends SymbolFocusedViewInput,
53
+ SymbolFocusedViewOptions,
54
+ SymbolFocusedViewChrome {}
55
+
56
+ export function SymbolFocusedView({
57
+ center,
58
+ neighbors,
59
+ edges,
60
+ repoPurl,
61
+ centerConstruct,
62
+ ...chrome
63
+ }: SymbolFocusedViewProps) {
64
+ const { components, graphifyRelations } = useMemo(
65
+ () =>
66
+ toSymbolFocusedView(
67
+ { center, neighbors, edges },
68
+ { repoPurl, ...(centerConstruct ? { centerConstruct } : {}) },
69
+ ),
70
+ [center, neighbors, edges, repoPurl, centerConstruct],
71
+ );
72
+
73
+ return (
74
+ <SubsystemComponentGraph
75
+ components={components}
76
+ graphifyRelations={graphifyRelations}
77
+ {...chrome}
78
+ />
79
+ );
80
+ }
@@ -13,7 +13,7 @@
13
13
  import { useCallback, useEffect, useRef, useState } from 'react';
14
14
  import type { DragEvent as ReactDragEvent } from 'react';
15
15
  import { useTheme } from '@principal-ade/industry-theme';
16
- import { Check, ChevronDown, Copy, GripVertical, Pause, Play } from 'lucide-react';
16
+ import { Check, ChevronDown, Copy, Focus, GripVertical, Minimize2, Pause, Play } from 'lucide-react';
17
17
  import {
18
18
  PROPOSED_COLOR,
19
19
  reorderTargetIndex,
@@ -27,6 +27,15 @@ import { buildStepBrief } from './trailBrief';
27
27
  /** Pause (ms) between steps when a trail autoplays. */
28
28
  export const TRAIL_PLAY_PAUSE_MS = 2500;
29
29
 
30
+ /**
31
+ * Dwell time (ms) before a hover preview reaches the canvas.
32
+ *
33
+ * The row's own highlight stays immediate; the canvas preview (dimming +
34
+ * camera frame) waits this long, so sweeping the pointer across steps does not
35
+ * fire a reframe per step. Leaving hover clears immediately.
36
+ */
37
+ export const TRAIL_HOVER_PREVIEW_MS = 150;
38
+
30
39
  /** How long a step's copy button flashes its "copied" checkmark (ms). */
31
40
  export const STEP_COPY_FEEDBACK_MS = 2000;
32
41
 
@@ -61,6 +70,8 @@ function TrailFlow({
61
70
  dragActive,
62
71
  isHoverSuppressed,
63
72
  proposedAliases,
73
+ layoutFocused,
74
+ onToggleLayoutFocus,
64
75
  }: {
65
76
  trail: SubsystemTrail;
66
77
  collapsed: boolean;
@@ -86,6 +97,10 @@ function TrailFlow({
86
97
  * (darkgoldenrod, same as the proposed node treatment).
87
98
  */
88
99
  proposedAliases?: ReadonlySet<string>;
100
+ /** This flow's components are the only ones laid out right now. */
101
+ layoutFocused?: boolean;
102
+ /** Solo this flow's components in the layout, or restore the full model. */
103
+ onToggleLayoutFocus?: (tl: SubsystemTrail) => void;
89
104
  }) {
90
105
  const { theme } = useTheme();
91
106
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
@@ -102,8 +117,30 @@ function TrailFlow({
102
117
  const [headerHover, setHeaderHover] = useState(false);
103
118
  const [collapseHover, setCollapseHover] = useState(false);
104
119
  const [playHover, setPlayHover] = useState(false);
120
+ const [focusHover, setFocusHover] = useState(false);
105
121
  const [gripHover, setGripHover] = useState(false);
106
122
  const [hoveredStep, setHoveredStep] = useState<number | null>(null);
123
+ // Dwell timer for the canvas preview. The row highlight is immediate; only
124
+ // the `onHover*` callbacks (dim + camera) are delayed.
125
+ const hoverPreviewTimerRef = useRef<number | null>(null);
126
+ const cancelHoverPreview = useCallback(() => {
127
+ if (hoverPreviewTimerRef.current != null) {
128
+ window.clearTimeout(hoverPreviewTimerRef.current);
129
+ hoverPreviewTimerRef.current = null;
130
+ }
131
+ }, []);
132
+ const scheduleHoverPreview = useCallback(
133
+ (run: () => void) => {
134
+ cancelHoverPreview();
135
+ hoverPreviewTimerRef.current = window.setTimeout(() => {
136
+ hoverPreviewTimerRef.current = null;
137
+ run();
138
+ }, TRAIL_HOVER_PREVIEW_MS);
139
+ },
140
+ [cancelHoverPreview],
141
+ );
142
+ // A pending preview must not fire after the row unmounts or collapses.
143
+ useEffect(() => () => cancelHoverPreview(), [cancelHoverPreview]);
107
144
  const stepButtonRefs = useRef<(HTMLButtonElement | null)[]>([]);
108
145
  // Per-step copy affordance: revealed while a row is hovered or holds focus.
109
146
  // `focusedStep` tracks focus-within (button or its copy control) so the icon
@@ -171,6 +208,12 @@ function TrailFlow({
171
208
  setHoveredStep(null);
172
209
  }, [dragActive]);
173
210
 
211
+ // Expanding / collapsing swaps which preview applies (header flow vs step
212
+ // list). Drop any pending preview so it cannot fire against the new shape.
213
+ useEffect(() => {
214
+ cancelHoverPreview();
215
+ }, [collapsed, cancelHoverPreview]);
216
+
174
217
  const startPlaying = useCallback(() => {
175
218
  if (collapsed) onToggleCollapsed(trail.id);
176
219
  if (active === null || active.stepIndex !== null) onFocusFlow(trail);
@@ -213,11 +256,14 @@ function TrailFlow({
213
256
  onMouseEnter={() => {
214
257
  if (isHoverSuppressed?.()) return;
215
258
  setHeaderHover(true);
216
- if (collapsed) onHoverFlow(trail);
259
+ if (collapsed) scheduleHoverPreview(() => onHoverFlow(trail));
217
260
  }}
218
261
  onMouseLeave={() => {
219
262
  setHeaderHover(false);
220
- if (collapsed) onLeaveStep();
263
+ if (collapsed) {
264
+ cancelHoverPreview();
265
+ onLeaveStep();
266
+ }
221
267
  }}
222
268
  style={{
223
269
  display: 'flex',
@@ -278,6 +324,49 @@ function TrailFlow({
278
324
  </button>
279
325
  {!collapsed && (
280
326
  <>
327
+ {onToggleLayoutFocus && trail.steps.length > 0 && (
328
+ <button
329
+ type="button"
330
+ aria-label={
331
+ layoutFocused
332
+ ? `Exit focus layout for ${trail.title}`
333
+ : `Focus layout on ${trail.title}`
334
+ }
335
+ aria-pressed={!!layoutFocused}
336
+ title={
337
+ layoutFocused
338
+ ? 'Show the full model again'
339
+ : 'Lay out only this flow’s components'
340
+ }
341
+ onMouseEnter={() => setFocusHover(true)}
342
+ onMouseLeave={() => setFocusHover(false)}
343
+ onClick={(e) => {
344
+ e.stopPropagation();
345
+ onToggleLayoutFocus(trail);
346
+ }}
347
+ style={{
348
+ display: 'inline-flex',
349
+ alignItems: 'center',
350
+ justifyContent: 'center',
351
+ flexShrink: 0,
352
+ alignSelf: 'stretch',
353
+ width: TRAIL_CONTROL_SIZE,
354
+ padding: 0,
355
+ border: 'none',
356
+ borderRadius: 0,
357
+ background: layoutFocused || focusHover ? theme.colors.border : 'transparent',
358
+ color: layoutFocused || focusHover ? theme.colors.text : muted,
359
+ cursor: 'pointer',
360
+ transition: 'background 120ms ease, color 120ms ease',
361
+ }}
362
+ >
363
+ {layoutFocused ? (
364
+ <Minimize2 size={12} strokeWidth={2} />
365
+ ) : (
366
+ <Focus size={13} strokeWidth={2} />
367
+ )}
368
+ </button>
369
+ )}
281
370
  <button
282
371
  type="button"
283
372
  aria-label={playing ? `Pause ${trail.title} autoplay` : `Play ${trail.title}`}
@@ -370,6 +459,7 @@ function TrailFlow({
370
459
  <div
371
460
  style={{ display: 'flex', flexDirection: 'column' }}
372
461
  onMouseLeave={() => {
462
+ cancelHoverPreview();
373
463
  setHoveredStep(null);
374
464
  onLeaveStep();
375
465
  }}
@@ -400,7 +490,7 @@ function TrailFlow({
400
490
  title={proposed ? 'Step touches a proposed component' : undefined}
401
491
  onMouseEnter={() => {
402
492
  setHoveredStep(i);
403
- onHoverStep(trail, i);
493
+ scheduleHoverPreview(() => onHoverStep(trail, i));
404
494
  }}
405
495
  onClick={() => onFocusStep(trail, i)}
406
496
  style={{
@@ -530,6 +620,13 @@ export interface TrailsPanelProps {
530
620
  * (darkgoldenrod, same as the proposed node treatment).
531
621
  */
532
622
  proposedAliases?: ReadonlySet<string>;
623
+ /** Trail whose components are the only ones laid out on the canvas, if any. */
624
+ layoutFocusedTrailId?: string | null;
625
+ /**
626
+ * Toggle the solo layout for a flow: relayout the canvas with just that
627
+ * flow's components and edges. Omit to hide the per-row focus control.
628
+ */
629
+ onFocusLayout?: (trailId: string) => void;
533
630
  }
534
631
 
535
632
  /**
@@ -552,6 +649,8 @@ export function TrailsPanel({
552
649
  onLeaveStep,
553
650
  onReorder,
554
651
  proposedAliases,
652
+ layoutFocusedTrailId,
653
+ onFocusLayout,
555
654
  }: TrailsPanelProps) {
556
655
  const { theme } = useTheme();
557
656
  // Reordering is offered only while every row is collapsed: the compact list
@@ -732,6 +831,10 @@ export function TrailsPanel({
732
831
  dragActive={dragIndex != null}
733
832
  isHoverSuppressed={isHoverSuppressed}
734
833
  proposedAliases={proposedAliases}
834
+ layoutFocused={layoutFocusedTrailId === tl.id}
835
+ onToggleLayoutFocus={
836
+ onFocusLayout ? (t) => onFocusLayout(t.id) : undefined
837
+ }
735
838
  />
736
839
  </div>
737
840
  );
@@ -43,7 +43,28 @@ export const GRAPH_NAV_PROPS = {
43
43
  */
44
44
  export const GRAPH_CANVAS_CLASS = 'subsystem-graph-canvas';
45
45
 
46
- export const GRAPH_LAYER_CSS = `.${GRAPH_CANVAS_CLASS} .react-flow__nodes { z-index: 1; }`;
46
+ /**
47
+ * Applied to the canvas box while a layout-direction flip is in progress.
48
+ *
49
+ * The flip is sequenced in three beats: edges fade out (`LAYOUT_FLIP_EDGE_MS`),
50
+ * the graph re-lays out with nodes sliding to their new slots
51
+ * (`LAYOUT_FLIP_NODE_MS`), then edges fade back in. Hiding the edges first
52
+ * keeps the old routes from cutting across the new layout mid-flight.
53
+ */
54
+ export const LAYOUT_FLIP_CLASS = 'subsystem-layout-flipping';
55
+
56
+ /** Edge fade-out/in duration around a layout flip, ms. */
57
+ export const LAYOUT_FLIP_EDGE_MS = 180;
58
+
59
+ /** Node slide duration to the new layout, ms. */
60
+ export const LAYOUT_FLIP_NODE_MS = 320;
61
+
62
+ export const GRAPH_LAYER_CSS = `
63
+ .${GRAPH_CANVAS_CLASS} .react-flow__nodes { z-index: 1; }
64
+ .${GRAPH_CANVAS_CLASS} .react-flow__edges { transition: opacity ${LAYOUT_FLIP_EDGE_MS}ms ease; }
65
+ .${LAYOUT_FLIP_CLASS} .react-flow__edges { opacity: 0; }
66
+ .${LAYOUT_FLIP_CLASS} .react-flow__node { transition: transform ${LAYOUT_FLIP_NODE_MS}ms cubic-bezier(0.4, 0, 0.2, 1); }
67
+ `;
47
68
 
48
69
  /** Scoped style tag for `GRAPH_LAYER_CSS`; render once beside `<ReactFlow>`. */
49
70
  export function GraphLayerStyle() {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Direction wiring for the layered layout.
3
+ *
4
+ * ELK itself can't run under bun's test runner (elkjs wants a Web Worker, so
5
+ * `computeElkLayout` falls back to manual positions), which makes the rendered
6
+ * geometry identical in every direction. What is testable — and what actually
7
+ * decides the axis — is the mapping from the canvas option onto
8
+ * `elk.direction`, so that is what these tests pin.
9
+ */
10
+ import { describe, expect, test } from 'bun:test';
11
+ import { elkDirectionFor } from './model';
12
+
13
+ describe('elkDirectionFor', () => {
14
+ test('horizontal flow lays layers left-to-right', () => {
15
+ expect(elkDirectionFor('horizontal', true)).toBe('RIGHT');
16
+ });
17
+
18
+ test('vertical flow lays layers top-to-bottom', () => {
19
+ expect(elkDirectionFor('vertical', true)).toBe('DOWN');
20
+ });
21
+
22
+ test('edge-less containment keeps its DOWN + grid treatment either way', () => {
23
+ expect(elkDirectionFor('horizontal', false)).toBe('DOWN');
24
+ expect(elkDirectionFor('vertical', false)).toBe('DOWN');
25
+ });
26
+ });
@@ -26,6 +26,7 @@ import {
26
26
  buildBoundaryLayoutGroups,
27
27
  buildBoundaryLayoutGroupsByPath,
28
28
  buildSubsystemGraph,
29
+ unroutedEdgeIds,
29
30
  componentPackageKey,
30
31
  deriveNameFromSymbol,
31
32
  isConstructsOnlyModel,
@@ -1227,3 +1228,19 @@ describe('graphify-native edges', () => {
1227
1228
  expect((g?.data as { mechanism?: string })?.mechanism).toBe('imports');
1228
1229
  });
1229
1230
  });
1231
+
1232
+ describe('unroutedEdgeIds', () => {
1233
+ test('lists shown edges with no elkPath', () => {
1234
+ const ids = unroutedEdgeIds([
1235
+ { id: 'routed', data: { elkPath: 'M 0 0 L 10 0' } },
1236
+ { id: 'no-path', data: { mechanism: 'calls' } },
1237
+ { id: 'no-data' },
1238
+ { id: 'empty-path', data: { elkPath: '' } },
1239
+ ]);
1240
+ expect(ids).toEqual(['no-path', 'no-data', 'empty-path']);
1241
+ });
1242
+
1243
+ test('skips hidden edges', () => {
1244
+ expect(unroutedEdgeIds([{ id: 'hidden', hidden: true, data: {} }])).toEqual([]);
1245
+ });
1246
+ });
@@ -136,6 +136,17 @@ export interface SubsystemComponent extends CoreSubsystemComponent {
136
136
  */
137
137
  export type SubsystemEdgeView = 'graphify' | 'trails';
138
138
 
139
+ /**
140
+ * Flow direction of the layered layout.
141
+ *
142
+ * - `horizontal`: layers run left-to-right (ELK `RIGHT`)
143
+ * - `vertical`: layers run top-to-bottom (ELK `DOWN`)
144
+ *
145
+ * Edge-less containment graphs (pure module/process frames) keep their
146
+ * existing DOWN + grid treatment in either mode — there is no flow to orient.
147
+ */
148
+ export type SubsystemLayoutDirection = 'horizontal' | 'vertical';
149
+
139
150
  /**
140
151
  * Where a display edge came from.
141
152
  *
@@ -167,8 +178,8 @@ export interface SubsystemGraphifyRelation {
167
178
  refs?: string[];
168
179
  /**
169
180
  * 1-based source line of the relation site. Optional — available on
170
- * `calls`/`references`-style edges; used to order an ego graph's callees by
171
- * call site when a host opts into line-ordered layering.
181
+ * `calls`/`references`-style edges; used to order a symbol-focused view's
182
+ * callees by call site when a host opts into line-ordered layering.
172
183
  */
173
184
  line?: number;
174
185
  /** graphify's provenance tag (`EXTRACTED` / `INFERRED` / `AMBIGUOUS`). */
@@ -1866,6 +1877,22 @@ export function subsystemGraphLayoutKey(
1866
1877
  return `${components}|${edgeKey}`;
1867
1878
  }
1868
1879
 
1880
+ /**
1881
+ * ELK `elk.direction` for a requested flow direction.
1882
+ *
1883
+ * Edge-less containment graphs have no flow to orient: a RIGHT layout stacks
1884
+ * disconnected top-level frames in one long vertical column, while DOWN places
1885
+ * those sibling frames along the horizontal axis and stacks each frame's
1886
+ * members vertically. That containment treatment wins in both modes.
1887
+ */
1888
+ export function elkDirectionFor(
1889
+ layoutDirection: SubsystemLayoutDirection,
1890
+ hasEdges: boolean,
1891
+ ): 'RIGHT' | 'DOWN' {
1892
+ if (!hasEdges) return 'DOWN';
1893
+ return layoutDirection === 'vertical' ? 'DOWN' : 'RIGHT';
1894
+ }
1895
+
1869
1896
  /**
1870
1897
  * Build the full React Flow graph (nodes + edges) using **ELK auto-layout** to
1871
1898
  * position every node (including external stub targets), so the graph is
@@ -1885,11 +1912,18 @@ export async function buildSubsystemGraph(
1885
1912
  * Order same-layer nodes by `component.line` (ascending) instead of ELK's
1886
1913
  * crossing-minimizer. Only meaningful when components carry a `line` — and
1887
1914
  * callers are expected to stamp `line` with the RELEVANT site for their
1888
- * graph (e.g. an ego graph stamps each callee with the center's call-site
1889
- * line, not the callee's declaration line).
1915
+ * graph (e.g. a symbol-focused view stamps each callee with the center's
1916
+ * call-site line, not the callee's declaration line).
1890
1917
  * @default false
1891
1918
  */
1892
1919
  orderByLine?: boolean;
1920
+ /**
1921
+ * Flow direction of the layered layout. `horizontal` (default) lays layers
1922
+ * left-to-right, `vertical` top-to-bottom. Edge-less containment graphs
1923
+ * keep their DOWN + grid treatment either way.
1924
+ * @default 'horizontal'
1925
+ */
1926
+ layoutDirection?: SubsystemLayoutDirection;
1893
1927
  } & BoundaryFrameOptions = {},
1894
1928
  ): Promise<{
1895
1929
  nodes: SubsystemGraphNode[];
@@ -1912,6 +1946,7 @@ export async function buildSubsystemGraph(
1912
1946
  moduleNesting,
1913
1947
  graphifyRelations,
1914
1948
  orderByLine = false,
1949
+ layoutDirection = 'horizontal',
1915
1950
  codeProcessKeys,
1916
1951
  } = opts;
1917
1952
  const frameOpts: BoundaryFrameOptions = {
@@ -2020,12 +2055,14 @@ export async function buildSubsystemGraph(
2020
2055
  try {
2021
2056
  const result = await computeElkLayout(nodes, edges, {
2022
2057
  routingStyle: 'orthogonal',
2023
- // Edge-driven graphs flow left-to-right. With no edges (pure
2024
- // containment, e.g. the static-topology layer) a RIGHT layout stacks
2025
- // disconnected top-level frames in one vertical column; DOWN makes the
2026
- // layered pass place those sibling frames along the horizontal axis
2027
- // while each frame's members stack vertically.
2028
- direction: edges.length > 0 ? 'RIGHT' : 'DOWN',
2058
+ // Edge-driven graphs flow left-to-right (or top-to-bottom when the
2059
+ // host asks for a vertical layout). With no edges (pure containment,
2060
+ // e.g. the static-topology layer) a RIGHT layout stacks disconnected
2061
+ // top-level frames in one vertical column; DOWN makes the layered pass
2062
+ // place those sibling frames along the horizontal axis while each
2063
+ // frame's members stack vertically. There is no flow to orient there,
2064
+ // so the containment treatment wins in both modes.
2065
+ direction: elkDirectionFor(layoutDirection, edges.length > 0),
2029
2066
  // Edge-less containment: fold the root frames into a column-major grid
2030
2067
  // so the shape reads closer to square than one long strip.
2031
2068
  reflowGrid: edges.length === 0,
@@ -2157,5 +2194,28 @@ export async function buildSubsystemGraph(
2157
2194
  }
2158
2195
  }
2159
2196
 
2197
+ // ELK can return an edge with no sections without throwing, which nothing
2198
+ // else reports.
2199
+ const unrouted = unroutedEdgeIds(edges);
2200
+ if (unrouted.length > 0) {
2201
+ console.warn(
2202
+ `[subsystem-graph] ${unrouted.length}/${edges.length} edge(s) have no ELK path and will not render:`,
2203
+ unrouted,
2204
+ );
2205
+ }
2206
+
2160
2207
  return { nodes: placedNodes, edges, regions, absoluteRects };
2161
2208
  }
2209
+
2210
+ /**
2211
+ * Ids of shown edges that ELK left without a path. `SubsystemEdge` draws
2212
+ * `elkPath` with no fallback, so these edges are invisible on the canvas.
2213
+ * Hidden edges are skipped: they would not render either way.
2214
+ */
2215
+ export function unroutedEdgeIds(
2216
+ edges: readonly { id: string; hidden?: boolean; data?: unknown }[],
2217
+ ): string[] {
2218
+ return edges
2219
+ .filter((e) => !e.hidden && !(e.data as SubsystemGraphEdgeData | undefined)?.elkPath)
2220
+ .map((e) => e.id);
2221
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"aggregateViewFixture.d.ts","sourceRoot":"","sources":["../../../../src/stories/Subsystem/AggregateGraph/aggregateViewFixture.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,4CAA4C,CAAC;AAEvH,eAAO,MAAM,eAAe,EAAE,kBAAkB,EAA8n/C,CAAC;AAE/q/C,eAAO,MAAM,cAAc,EAAE,kBAAkB,EAAo88B,CAAC;AAEp/8B,eAAO,MAAM,aAAa,EAAE,YAAY,EAAutD,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"aggregateViewFixture.js","sourceRoot":"","sources":["../../../../src/stories/Subsystem/AggregateGraph/aggregateViewFixture.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,eAAe,GAAyB,IAAI,CAAC,KAAK,CAAC,6m/CAA6m/C,CAAC,CAAC;AAE/q/C,MAAM,CAAC,MAAM,cAAc,GAAyB,IAAI,CAAC,KAAK,CAAC,m78BAAm78B,CAAC,CAAC;AAEp/8B,MAAM,CAAC,MAAM,aAAa,GAAmB,IAAI,CAAC,KAAK,CAAC,ssDAAssD,CAAC,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../../../../src/stories/Subsystem/ComponentGraph/fixtures.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,sBAAsB,EACtB,cAAc,EACd,uBAAuB,EACxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAKjE,wBAAgB,UAAU,CACxB,IAAI,EAAE,KAAK,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,uBAAuB,CAAC,CAAC,GAC3L,kBAAkB,EAAE,CAWtB;AAED,sEAAsE;AACtE,wBAAgB,cAAc,CAC5B,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,uBAAuB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,GACxH,cAAc,CAchB;AAED,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,sBAAsB,CAAC,WAAW,CAAC,CAAC,CAAC,GACtF;IAAE,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;CAAE,CAe/B;AAED,eAAO,MAAM,YAAY,EAAE,uBAY1B,CAAC;AAMF,eAAO,MAAM,yBAAyB,EAAE,kBAAkB,EAqDzD,CAAC;AASF,eAAO,MAAM,qBAAqB,8BAAyB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"fixtures.js","sourceRoot":"","sources":["../../../../src/stories/Subsystem/ComponentGraph/fixtures.ts"],"names":[],"mappings":"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"}