@principal-ai/subsystems-react 0.53.6 → 0.54.0

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 (104) hide show
  1. package/dist/index.d.ts +8 -2
  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/pierre/PierreTrailCodeView.d.ts.map +1 -1
  6. package/dist/pierre/PierreTrailCodeView.js +7 -5
  7. package/dist/pierre/PierreTrailCodeView.js.map +1 -1
  8. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  9. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +1 -2
  10. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  11. package/dist/subsystem/C4Graph.d.ts +144 -9
  12. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  13. package/dist/subsystem/C4Graph.js +924 -148
  14. package/dist/subsystem/C4Graph.js.map +1 -1
  15. package/dist/subsystem/C4NodeCard.d.ts +22 -1
  16. package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
  17. package/dist/subsystem/C4NodeCard.js +10 -2
  18. package/dist/subsystem/C4NodeCard.js.map +1 -1
  19. package/dist/subsystem/ComponentDeclaration.d.ts +5 -4
  20. package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
  21. package/dist/subsystem/ComponentDeclaration.js +12 -4
  22. package/dist/subsystem/ComponentDeclaration.js.map +1 -1
  23. package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
  24. package/dist/subsystem/ConstructsCatalog.js +10 -7
  25. package/dist/subsystem/ConstructsCatalog.js.map +1 -1
  26. package/dist/subsystem/GraphSidebar.d.ts +71 -0
  27. package/dist/subsystem/GraphSidebar.d.ts.map +1 -0
  28. package/dist/subsystem/GraphSidebar.js +145 -0
  29. package/dist/subsystem/GraphSidebar.js.map +1 -0
  30. package/dist/subsystem/GraphWithSidebar.d.ts +24 -0
  31. package/dist/subsystem/GraphWithSidebar.d.ts.map +1 -0
  32. package/dist/subsystem/GraphWithSidebar.js +25 -0
  33. package/dist/subsystem/GraphWithSidebar.js.map +1 -0
  34. package/dist/subsystem/SubsystemComponentGraph.d.ts +33 -0
  35. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  36. package/dist/subsystem/SubsystemComponentGraph.js +364 -411
  37. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  38. package/dist/subsystem/TrailStepsNav.d.ts +48 -0
  39. package/dist/subsystem/TrailStepsNav.d.ts.map +1 -0
  40. package/dist/subsystem/TrailStepsNav.js +189 -0
  41. package/dist/subsystem/TrailStepsNav.js.map +1 -0
  42. package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
  43. package/dist/subsystem/TrailsPanel.js +3 -4
  44. package/dist/subsystem/TrailsPanel.js.map +1 -1
  45. package/dist/subsystem/__fixtures__/c4Drilldown.d.ts +151 -0
  46. package/dist/subsystem/__fixtures__/c4Drilldown.d.ts.map +1 -0
  47. package/dist/subsystem/__fixtures__/c4Drilldown.js +783 -0
  48. package/dist/subsystem/__fixtures__/c4Drilldown.js.map +1 -0
  49. package/dist/subsystem/c4.d.ts +39 -8
  50. package/dist/subsystem/c4.d.ts.map +1 -1
  51. package/dist/subsystem/c4.js.map +1 -1
  52. package/dist/subsystem/declarationRef.d.ts +26 -18
  53. package/dist/subsystem/declarationRef.d.ts.map +1 -1
  54. package/dist/subsystem/declarationRef.js +9 -4
  55. package/dist/subsystem/declarationRef.js.map +1 -1
  56. package/dist/subsystem/model.d.ts +19 -1
  57. package/dist/subsystem/model.d.ts.map +1 -1
  58. package/dist/subsystem/model.js +12 -2
  59. package/dist/subsystem/model.js.map +1 -1
  60. package/dist/subsystem/nodes.d.ts.map +1 -1
  61. package/dist/subsystem/nodes.js +24 -4
  62. package/dist/subsystem/nodes.js.map +1 -1
  63. package/dist/subsystem/trailBrief.d.ts.map +1 -1
  64. package/dist/subsystem/trailBrief.js +6 -1
  65. package/dist/subsystem/trailBrief.js.map +1 -1
  66. package/package.json +3 -3
  67. package/src/index.ts +9 -2
  68. package/src/pierre/PierreTrailCodeView.tsx +9 -13
  69. package/src/stories/Subsystem/C4Graph/C4ToComponentDrill.stories.tsx +496 -0
  70. package/src/stories/Subsystem/ComponentGraph/AnimatedProgression.stories.tsx +7 -7
  71. package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +19 -25
  72. package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +8 -10
  73. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +19 -19
  74. package/src/stories/Subsystem/ComponentGraph/ProposedTrails.stories.tsx +6 -6
  75. package/src/stories/Subsystem/ComponentGraph/TrailAutoplay.stories.tsx +7 -7
  76. package/src/stories/Subsystem/ComponentGraph/Trails.stories.tsx +27 -30
  77. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +1 -2
  78. package/src/stories/Subsystem/ConstructsCatalog.stories.tsx +10 -13
  79. package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +3 -4
  80. package/src/stories/Subsystem/Sidebar/GraphSidebar.stories.tsx +206 -0
  81. package/src/subsystem/C4Graph.groups.test.ts +96 -2
  82. package/src/subsystem/C4Graph.tsx +1377 -165
  83. package/src/subsystem/C4NodeCard.test.ts +1 -2
  84. package/src/subsystem/C4NodeCard.tsx +24 -2
  85. package/src/subsystem/ComponentDeclaration.tsx +18 -8
  86. package/src/subsystem/ConstructsCatalog.tsx +23 -8
  87. package/src/subsystem/GraphSidebar.test.tsx +152 -0
  88. package/src/subsystem/GraphSidebar.tsx +300 -0
  89. package/src/subsystem/GraphWithSidebar.test.tsx +78 -0
  90. package/src/subsystem/GraphWithSidebar.tsx +60 -0
  91. package/src/subsystem/SubsystemComponentGraph.tsx +284 -370
  92. package/src/subsystem/TrailStepsNav.test.tsx +179 -0
  93. package/src/subsystem/TrailStepsNav.tsx +372 -0
  94. package/src/subsystem/TrailsPanel.tsx +3 -4
  95. package/src/subsystem/__fixtures__/c4Drilldown.test.ts +338 -0
  96. package/src/subsystem/__fixtures__/c4Drilldown.ts +847 -0
  97. package/src/subsystem/c4.ts +41 -8
  98. package/src/subsystem/declarationRef.ts +28 -19
  99. package/src/subsystem/model.test.ts +32 -12
  100. package/src/subsystem/model.ts +30 -1
  101. package/src/subsystem/nodes.group.test.tsx +82 -0
  102. package/src/subsystem/nodes.tsx +30 -2
  103. package/src/subsystem/trailBrief.test.ts +2 -2
  104. package/src/subsystem/trailBrief.ts +6 -1
@@ -0,0 +1,496 @@
1
+ import { useCallback, useMemo, useRef, useState } from 'react';
2
+ import '@xyflow/react/dist/style.css';
3
+ import type { Meta, StoryObj } from '@storybook/react';
4
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
5
+ import { FileText, LayoutDashboard } from 'lucide-react';
6
+ import { IndustryMarkdownSlide } from 'themed-markdown';
7
+ import { C4Graph } from '../../../subsystem/C4Graph';
8
+ import { GraphWithSidebar } from '../../../subsystem/GraphWithSidebar';
9
+ import { TrailStepsNav } from '../../../subsystem/TrailStepsNav';
10
+ import type { C4ConstructGraph } from '../../../subsystem/C4Graph';
11
+ import { deriveGraphEdges, trailStepGraphEdgeId } from '../../../subsystem/model';
12
+ import {
13
+ DRILLDOWN_C4_MODEL,
14
+ DRILLDOWN_COMPONENTS,
15
+ DRILLDOWN_DOC,
16
+ DRILLDOWN_ELEMENTS,
17
+ DRILLDOWN_MEMBERS,
18
+ DRILLDOWN_TRAILS,
19
+ deriveLitC4Edges,
20
+ deriveTrailPlayerElements,
21
+ } from '../../../subsystem/__fixtures__/c4Drilldown';
22
+
23
+ /** The model this story is grounded in — shown in the sidebar header. */
24
+ const MODEL_DESCRIPTION =
25
+ 'The subsystem this C4 view summarizes. Focusing a trail opens the containers that hold its cast and folds the trail in as real nodes and edges, nested under their C4 component.';
26
+
27
+ /**
28
+ * The C4 → construct drill-down, in one canvas.
29
+ *
30
+ * The C4 view is the high-level summary: containers, externals, and the
31
+ * rolled-up lines between them. Focusing a trail folds the implementation into
32
+ * it — the constructs (the component graph's own `subsystem-component` nodes
33
+ * and `subsystem-edge` edges) are laid out by ELK inside the opened containers
34
+ * and morphed in with them, so the C4 canvas carries both the summary and the
35
+ * implementation. The model is the REAL subsystem model this repo lives under
36
+ * (`sg-1788913589907-ieokymqg9` "Gist share from Studio": 19 components, 15/15
37
+ * verified claims, 7 walkthroughs checked).
38
+ *
39
+ * Interaction:
40
+ * - click a C4 edge → it selects, and the story focuses the first trail that
41
+ * backs the line — the "edge is the summary, the trail is the
42
+ * implementation" jump;
43
+ * - the C4 edge inspector lists every backing trail; clicking one focuses it;
44
+ * - focusing a trail HIDES every C4 element whose components don't appear in
45
+ * it, and OPENS the containers that hold the cast, so the participating
46
+ * components come in at Level 3 instead of staying a collapsed box;
47
+ * - each opened container hosts the focused trail's components as real
48
+ * `subsystem-component` nodes and `subsystem-edge` edges. The constructs
49
+ * nest under the authored C4 component that claims them (container →
50
+ * component → construct); one no component claims sits directly in the
51
+ * container. An edge between constructs in the same container draws as a
52
+ * subsystem edge; one that would cross containers is left to the C4
53
+ * container-level line. Clicking a construct opens its **functionality** —
54
+ * the rendered declaration and its purpose — in the bottom-left panel. The
55
+ * host resolves it (`constructGraphByContainer`) so the C4 view stays free
56
+ * of the document;
57
+ * - hovering a *step* row DIMS the constructs down to that single hop: every
58
+ * construct except the step's `from`/`to`, and every construct edge except
59
+ * the step's own, fades (`dimmedConstructIds` / `dimmedConstructEdgeIds`).
60
+ * Transient: falls back to the focused view on leave;
61
+ * - the C4 view is composed with its own sidebar through the shared
62
+ * `GraphWithSidebar` wrapper: the sidebar carries the **trails**. The C4
63
+ * selection inspector stays the graph's own floating overlay; the wrapper
64
+ * is the composition point, so the C4 graph itself stays overlay-only.
65
+ *
66
+ * The status bar below the C4 view shows the current selection, so the wiring
67
+ * is visible while it is being argued with.
68
+ */
69
+
70
+ const meta = {
71
+ title: 'Subsystem/C4Graph/C4ToComponentDrill',
72
+ component: C4Graph,
73
+ parameters: { layout: 'fullscreen' },
74
+ tags: ['autodocs'],
75
+ decorators: [
76
+ (Story) => (
77
+ <ThemeProvider theme={defaultEditorTheme}>
78
+ <Story />
79
+ </ThemeProvider>
80
+ ),
81
+ ],
82
+ } satisfies Meta<typeof C4Graph>;
83
+
84
+ export default meta;
85
+ type Story = StoryObj<typeof meta>;
86
+
87
+ function Demo() {
88
+ const [openIds, setOpenIds] = useState<string[]>([]);
89
+ const [focusedTrailId, setFocusedTrailId] = useState<string | null>(null);
90
+ // The trail (or step) currently hovered in the trails panel — its transient
91
+ // preview drives a step-scoped dim. `stepIndex: null` means the whole trail
92
+ // (a collapsed row hover).
93
+ const [hoveredStep, setHoveredStep] = useState<{
94
+ trailId: string;
95
+ stepIndex: number | null;
96
+ } | null>(null);
97
+ // The persistently-focused step (a step click in the sidebar). Drives the
98
+ // sticky construct spotlight and the C4 graph's code drawer; the hover below
99
+ // is the transient preview that shares the same dim.
100
+ const [focusedStep, setFocusedStep] = useState<{
101
+ trailId: string;
102
+ stepIndex: number;
103
+ } | null>(null);
104
+ // The header description, collapsible like the component graph's sidebar.
105
+ const [descriptionVisible, setDescriptionVisible] = useState(true);
106
+
107
+ /**
108
+ * EXPERIMENT: when a trail is focused, remove the elements it does not touch
109
+ * from the model outright, so ELK lays the diagram out to the cast alone
110
+ * instead of leaving gaps where the hidden boxes were. `null` = nothing
111
+ * focused, the whole model.
112
+ */
113
+ const trailPlayers = useMemo(() => {
114
+ if (!focusedTrailId) return null;
115
+ const trail = DRILLDOWN_TRAILS.find((t) => t.id === focusedTrailId);
116
+ return trail ? deriveTrailPlayerElements(trail.steps, DRILLDOWN_MEMBERS) : null;
117
+ }, [focusedTrailId]);
118
+
119
+ const model = useMemo(() => {
120
+ if (!trailPlayers) return { ...DRILLDOWN_C4_MODEL, openContainerIds: openIds };
121
+ const nodes = DRILLDOWN_C4_MODEL.nodes.filter((n) => trailPlayers.has(n.id));
122
+ const ids = new Set(nodes.map((n) => n.id));
123
+ const edges = DRILLDOWN_C4_MODEL.edges.filter(
124
+ (e) => ids.has(e.source) && ids.has(e.target),
125
+ );
126
+ return { ...DRILLDOWN_C4_MODEL, nodes, edges, openContainerIds: openIds };
127
+ }, [openIds, trailPlayers]);
128
+
129
+ const toggle = useCallback((id: string) => {
130
+ setOpenIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id].sort()));
131
+ }, []);
132
+
133
+ const allElementIds = useMemo(
134
+ () => DRILLDOWN_C4_MODEL.nodes.map((n) => n.id),
135
+ [],
136
+ );
137
+ const allEdgeIds = useMemo(
138
+ () => DRILLDOWN_C4_MODEL.edges.map((e) => e.id),
139
+ [],
140
+ );
141
+
142
+ /**
143
+ * The C4 containers that hold a component the trail touches — opened when the
144
+ * trail is focused so its cast comes in at Level 3 instead of staying a
145
+ * collapsed container box. A trail whose endpoints no container claims (the
146
+ * anti-library keys) opens nothing.
147
+ */
148
+ const containersForTrail = useCallback((trailId: string): string[] => {
149
+ const trail = DRILLDOWN_TRAILS.find((t) => t.id === trailId);
150
+ if (!trail) return [];
151
+ const aliases = new Set<string>();
152
+ for (const s of trail.steps) {
153
+ aliases.add(s.from);
154
+ aliases.add(s.to);
155
+ }
156
+ const out: string[] = [];
157
+ for (const el of DRILLDOWN_C4_MODEL.nodes) {
158
+ if (el.kind !== 'container') continue;
159
+ const members = DRILLDOWN_MEMBERS[el.id] ?? [];
160
+ if (members.some((a) => aliases.has(a))) out.push(el.id);
161
+ }
162
+ return out;
163
+ }, []);
164
+
165
+ /**
166
+ * The containers the *current* trail focus opened — remembered so clearing
167
+ * the focus (or moving to another trail) can close exactly those and leave any
168
+ * the reader opened by hand alone.
169
+ */
170
+ const trailOpenedRef = useRef<Set<string>>(new Set());
171
+
172
+ const openContainersForTrail = useCallback(
173
+ (trailId: string) => {
174
+ const ids = containersForTrail(trailId);
175
+ if (ids.length === 0) return;
176
+ trailOpenedRef.current = new Set([...trailOpenedRef.current, ...ids]);
177
+ setOpenIds((prev) => [...new Set([...prev, ...ids])].sort());
178
+ },
179
+ [containersForTrail],
180
+ );
181
+
182
+ /** Close the containers the current trail focus opened. */
183
+ const closeTrailContainers = useCallback(() => {
184
+ const opened = trailOpenedRef.current;
185
+ trailOpenedRef.current = new Set();
186
+ if (opened.size === 0) return;
187
+ setOpenIds((prev) => prev.filter((id) => !opened.has(id)));
188
+ }, []);
189
+
190
+ /**
191
+ * The single source of the C4 emphasis, derived from the two focus facts —
192
+ * mirroring the component graph:
193
+ *
194
+ * hidden — an EXPANDED/focused trail removes every element it does not
195
+ * touch (the analogue of the component graph's opened-flow hide),
196
+ * lines included; `null` when nothing is focused.
197
+ * dimmed — the HOVER spotlight (a step, or a whole trail for a collapsed
198
+ * row) fades everything outside it; `null` when nothing is hovered.
199
+ *
200
+ * A step internal to a container folds into no C4 line, so its hover dims
201
+ * every edge while the container stays bright — no drawn edge represents it.
202
+ *
203
+ * The spotlight is the *selected* step when one is focused (sticky), else the
204
+ * hovered one (transient) — so a step click keeps its implementation lit.
205
+ */
206
+ const activeStep = focusedStep ?? hoveredStep;
207
+ const emphasis = useMemo(() => {
208
+ let hiddenElementIds: ReadonlySet<string> | null = null;
209
+ let hiddenEdgeIds: ReadonlySet<string> | null = null;
210
+ if (focusedTrailId) {
211
+ const trail = DRILLDOWN_TRAILS.find((t) => t.id === focusedTrailId);
212
+ const players = trail
213
+ ? deriveTrailPlayerElements(trail.steps, DRILLDOWN_MEMBERS)
214
+ : new Set<string>();
215
+ hiddenElementIds = new Set(allElementIds.filter((id) => !players.has(id)));
216
+ const lit = deriveLitC4Edges({ trailId: focusedTrailId }, DRILLDOWN_C4_MODEL.edges);
217
+ hiddenEdgeIds = new Set(allEdgeIds.filter((id) => !lit.has(id)));
218
+ }
219
+
220
+ let dimmedElementIds: ReadonlySet<string> | null = null;
221
+ let dimmedEdgeIds: ReadonlySet<string> | null = null;
222
+ if (activeStep) {
223
+ const trail = DRILLDOWN_TRAILS.find((t) => t.id === activeStep.trailId);
224
+ const steps =
225
+ !trail
226
+ ? []
227
+ : activeStep.stepIndex != null
228
+ ? trail.steps.slice(activeStep.stepIndex, activeStep.stepIndex + 1)
229
+ : trail.steps;
230
+ const spotlight = deriveTrailPlayerElements(steps, DRILLDOWN_MEMBERS);
231
+ dimmedElementIds = new Set(allElementIds.filter((id) => !spotlight.has(id)));
232
+ const lit = deriveLitC4Edges(
233
+ {
234
+ trailId: activeStep.trailId,
235
+ ...(activeStep.stepIndex != null ? { stepIndex: activeStep.stepIndex } : {}),
236
+ },
237
+ DRILLDOWN_C4_MODEL.edges,
238
+ );
239
+ dimmedEdgeIds = new Set(allEdgeIds.filter((id) => !lit.has(id)));
240
+ }
241
+
242
+ return { hiddenElementIds, hiddenEdgeIds, dimmedElementIds, dimmedEdgeIds };
243
+ }, [focusedTrailId, activeStep, allElementIds, allEdgeIds]);
244
+
245
+ /**
246
+ * The construct-level subgraph each opened container hosts: the focused
247
+ * trail's referenced components, grouped by the authored C4 component that
248
+ * claims each (so constructs nest container → component → construct), with
249
+ * the trail's steps as their edges. Scoped per container, so an edge that
250
+ * crosses containers is dropped here and stays a C4 container-level line.
251
+ */
252
+ const constructGraphByContainer = useMemo(() => {
253
+ if (!focusedTrailId) return undefined;
254
+ const trail = DRILLDOWN_TRAILS.find((t) => t.id === focusedTrailId);
255
+ if (!trail) return undefined;
256
+ const byAlias = new Map(DRILLDOWN_COMPONENTS.map((c) => [c.alias, c]));
257
+ const referenced = new Set<string>();
258
+ for (const s of trail.steps) {
259
+ referenced.add(s.from);
260
+ referenced.add(s.to);
261
+ }
262
+ const componentOf = new Map<string, string>();
263
+ for (const el of DRILLDOWN_ELEMENTS) {
264
+ if (el.kind === 'component') for (const a of el.members ?? []) componentOf.set(a, el.id);
265
+ }
266
+ const out = new Map<string, C4ConstructGraph>();
267
+ for (const el of DRILLDOWN_ELEMENTS) {
268
+ if (el.kind !== 'container') continue;
269
+ const aliases = (DRILLDOWN_MEMBERS[el.id] ?? []).filter(
270
+ (a) => referenced.has(a) && byAlias.has(a),
271
+ );
272
+ if (aliases.length === 0) continue;
273
+ out.set(el.id, {
274
+ components: aliases.map((a) => byAlias.get(a)!),
275
+ trails: [trail],
276
+ componentOf,
277
+ });
278
+ }
279
+ return out;
280
+ }, [focusedTrailId]);
281
+
282
+ /**
283
+ * The construct-level spotlight for the active step, matching the component
284
+ * graph: every construct except the step's `from`/`to`, and every construct
285
+ * edge except the step's own, fades. The C4-element spotlight (`emphasis`)
286
+ * only reaches containers, so this is what narrows the constructs to the hop.
287
+ */
288
+ const constructEmphasis = useMemo(() => {
289
+ const none = { ids: null as ReadonlySet<string> | null, edgeIds: null as ReadonlySet<string> | null };
290
+ if (!activeStep) return none;
291
+ const trail = DRILLDOWN_TRAILS.find((t) => t.id === activeStep.trailId);
292
+ if (!trail) return none;
293
+ const steps =
294
+ activeStep.stepIndex != null ? [trail.steps[activeStep.stepIndex]] : trail.steps;
295
+ const spotlight = new Set<string>();
296
+ const litEdges = new Set<string>();
297
+ for (const s of steps) {
298
+ spotlight.add(s.from);
299
+ spotlight.add(s.to);
300
+ litEdges.add(trailStepGraphEdgeId(s));
301
+ }
302
+ const allAliases = new Set<string>();
303
+ const allEdgeIds = new Set<string>();
304
+ for (const cg of constructGraphByContainer?.values() ?? []) {
305
+ for (const c of cg.components) allAliases.add(c.alias);
306
+ for (const e of deriveGraphEdges({ trails: [...cg.trails] })) allEdgeIds.add(e.id);
307
+ }
308
+ return {
309
+ ids: new Set([...allAliases].filter((a) => !spotlight.has(a))),
310
+ edgeIds: new Set([...allEdgeIds].filter((id) => !litEdges.has(id))),
311
+ };
312
+ }, [activeStep, constructGraphByContainer]);
313
+
314
+ /**
315
+ * Focus a trail: the C4 hides non-members, opens the containers that hold
316
+ * them (so the cast shows at Level 3), and the component graph deep-links.
317
+ */
318
+ const focusTrail = useCallback(
319
+ (trailId: string) => {
320
+ setFocusedTrailId(trailId);
321
+ setHoveredStep(null);
322
+ // Swap the previous trail's opened containers for this one's, so moving
323
+ // between trails does not accumulate open boxes.
324
+ closeTrailContainers();
325
+ openContainersForTrail(trailId);
326
+ },
327
+ [openContainersForTrail, closeTrailContainers],
328
+ );
329
+
330
+ const clearFocus = useCallback(() => {
331
+ setFocusedTrailId(null);
332
+ setHoveredStep(null);
333
+ // Collapse the containers the focus opened, so Back leaves the diagram as
334
+ // it was rather than with the trail's boxes still expanded.
335
+ closeTrailContainers();
336
+ }, [closeTrailContainers]);
337
+
338
+ return (
339
+ <div
340
+ style={{
341
+ width: '100%',
342
+ height: '100vh',
343
+ display: 'flex',
344
+ flexDirection: 'column',
345
+ background: defaultEditorTheme.colors.background,
346
+ fontFamily: 'monospace',
347
+ }}
348
+ >
349
+ {/* C4 — the summary and, on focus, the construct implementation. */}
350
+ <div style={{ flex: 1, minHeight: 0, position: 'relative' }}>
351
+ <GraphWithSidebar
352
+ persistKey="c4-drill"
353
+ defaultWidth={320}
354
+ minWidth={240}
355
+ maxWidth={480}
356
+ header={
357
+ <>
358
+ <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }}>
359
+ {/* Icon + title centred as a group, so the icon stays vertically
360
+ centred when the title wraps. */}
361
+ <div style={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', gap: 8 }}>
362
+ <LayoutDashboard
363
+ size={15}
364
+ strokeWidth={2}
365
+ aria-hidden
366
+ style={{
367
+ flexShrink: 0,
368
+ color:
369
+ defaultEditorTheme.colors.textMuted ??
370
+ defaultEditorTheme.colors.textSecondary,
371
+ }}
372
+ />
373
+ <h2
374
+ style={{
375
+ margin: 0,
376
+ flex: 1,
377
+ minWidth: 0,
378
+ fontSize: defaultEditorTheme.fontSizes[2],
379
+ fontWeight: 600,
380
+ color: defaultEditorTheme.colors.primary,
381
+ fontFamily: defaultEditorTheme.fonts.monospace,
382
+ }}
383
+ >
384
+ {DRILLDOWN_DOC.title}
385
+ </h2>
386
+ </div>
387
+ <button
388
+ type="button"
389
+ aria-expanded={descriptionVisible}
390
+ aria-label={descriptionVisible ? 'Hide description' : 'Show description'}
391
+ title={descriptionVisible ? 'Hide description' : 'Show description'}
392
+ onClick={() => setDescriptionVisible((v) => !v)}
393
+ style={{
394
+ display: 'inline-flex',
395
+ alignItems: 'center',
396
+ justifyContent: 'center',
397
+ flexShrink: 0,
398
+ width: 22,
399
+ height: 22,
400
+ padding: 0,
401
+ border: 'none',
402
+ borderRadius: 4,
403
+ background: descriptionVisible ? '#333' : 'transparent',
404
+ color: descriptionVisible ? '#ddd' : '#999',
405
+ cursor: 'pointer',
406
+ }}
407
+ >
408
+ <FileText size={14} />
409
+ </button>
410
+ </div>
411
+ {descriptionVisible && (
412
+ <div style={{ fontSize: 12, lineHeight: 1.5 }}>
413
+ <IndustryMarkdownSlide
414
+ content={MODEL_DESCRIPTION}
415
+ slideIdPrefix="c4-desc"
416
+ slideIndex={0}
417
+ isVisible={true}
418
+ theme={defaultEditorTheme}
419
+ disableScroll={true}
420
+ disableBasePadding
421
+ enableKeyboardScrolling={false}
422
+ autoFocusOnVisible={false}
423
+ />
424
+ </div>
425
+ )}
426
+ </>
427
+ }
428
+ sidebarBody={
429
+ <TrailStepsNav
430
+ trails={DRILLDOWN_TRAILS}
431
+ focusedTrailId={focusedTrailId}
432
+ focusedStepIndex={
433
+ focusedStep && focusedStep.trailId === focusedTrailId
434
+ ? focusedStep.stepIndex
435
+ : null
436
+ }
437
+ hoveredTrailStep={hoveredStep}
438
+ onFocusFlow={(tl) => {
439
+ setFocusedStep(null);
440
+ focusTrail(tl.id);
441
+ }}
442
+ onClearFocus={() => {
443
+ setFocusedStep(null);
444
+ clearFocus();
445
+ }}
446
+ onFocusStep={(tl, stepIndex) => {
447
+ // Toggle: clicking the selected step again clears it.
448
+ setFocusedStep((prev) =>
449
+ prev && prev.trailId === tl.id && prev.stepIndex === stepIndex
450
+ ? null
451
+ : { trailId: tl.id, stepIndex },
452
+ );
453
+ focusTrail(tl.id);
454
+ }}
455
+ onHoverStep={(tl, stepIndex) => {
456
+ setHoveredStep({ trailId: tl.id, stepIndex });
457
+ }}
458
+ onHoverFlow={(tl) => {
459
+ setHoveredStep({ trailId: tl.id, stepIndex: null });
460
+ }}
461
+ onLeaveStep={() => {
462
+ setHoveredStep(null);
463
+ }}
464
+ />
465
+ }
466
+ >
467
+ <C4Graph
468
+ model={model}
469
+ onSelectEdge={(edge) => {
470
+ if (edge && (edge.trailRefs ?? []).length > 0) {
471
+ // Auto-drill: an edge click focuses its first backing trail.
472
+ focusTrail(edge.trailRefs![0]!.trailId);
473
+ }
474
+ }}
475
+ onTrailClick={(ref) => focusTrail(ref.trailId)}
476
+ onToggleContainer={toggle}
477
+ hiddenElementIds={emphasis.hiddenElementIds}
478
+ hiddenEdgeIds={emphasis.hiddenEdgeIds}
479
+ dimmedElementIds={emphasis.dimmedElementIds}
480
+ dimmedEdgeIds={emphasis.dimmedEdgeIds}
481
+ dimmedConstructIds={constructEmphasis.ids}
482
+ dimmedConstructEdgeIds={constructEmphasis.edgeIds}
483
+ constructGraphByContainer={constructGraphByContainer}
484
+ focusedStep={focusedStep}
485
+ onCloseStep={() => setFocusedStep(null)}
486
+ />
487
+ </GraphWithSidebar>
488
+ </div>
489
+
490
+ </div>
491
+ );
492
+ }
493
+
494
+ export const DrillIntoTrails: Story = {
495
+ render: () => <Demo />,
496
+ };
@@ -39,19 +39,19 @@ const trails: SubsystemTrail[] = [
39
39
  id: 'wt-book',
40
40
  title: 'Guest books',
41
41
  steps: [
42
- { from: 'booking-page', to: 'book-slot', mechanism: 'calls', file: 'app/book/page.tsx', line: 24, purl: PURL, symbol: 'bookSlot' },
43
- { from: 'book-slot', to: 'create-booking', mechanism: 'calls', file: 'app/book/actions.ts', line: 22, purl: PURL, symbol: 'createBooking' },
44
- { from: 'create-booking', to: 'Database', mechanism: 'writes', file: 'lib/createBooking.ts', line: 13, purl: PURL, symbol: 'insert' },
45
- { from: 'booking-page', to: 'capture-event', mechanism: 'calls', file: 'app/book/page.tsx', line: 26, purl: PURL, symbol: 'captureEvent' },
42
+ { from: 'booking-page', to: 'book-slot', mechanism: 'calls', file: 'app/book/page.tsx', span: { startLine: 24, endLine: 24, contentHash: 'fixture' }, purl: PURL, symbol: 'bookSlot' },
43
+ { from: 'book-slot', to: 'create-booking', mechanism: 'calls', file: 'app/book/actions.ts', span: { startLine: 22, endLine: 22, contentHash: 'fixture' }, purl: PURL, symbol: 'createBooking' },
44
+ { from: 'create-booking', to: 'Database', mechanism: 'writes', file: 'lib/createBooking.ts', span: { startLine: 13, endLine: 13, contentHash: 'fixture' }, purl: PURL, symbol: 'insert' },
45
+ { from: 'booking-page', to: 'capture-event', mechanism: 'calls', file: 'app/book/page.tsx', span: { startLine: 26, endLine: 26, contentHash: 'fixture' }, purl: PURL, symbol: 'captureEvent' },
46
46
  ],
47
47
  },
48
48
  {
49
49
  id: 'wt-pick',
50
50
  title: 'Guest picks a slot',
51
51
  steps: [
52
- { from: 'booking-page', to: 'list-open-slots', mechanism: 'calls', file: 'app/book/page.tsx', line: 17, purl: PURL, symbol: 'listOpenSlots' },
53
- { from: 'list-open-slots', to: 'list-slots', mechanism: 'calls', file: 'app/book/actions.ts', line: 14, purl: PURL, symbol: 'listSlots' },
54
- { from: 'list-slots', to: 'Database', mechanism: 'reads', file: 'lib/listSlots.ts', line: 9, purl: PURL, symbol: 'findOpen' },
52
+ { from: 'booking-page', to: 'list-open-slots', mechanism: 'calls', file: 'app/book/page.tsx', span: { startLine: 17, endLine: 17, contentHash: 'fixture' }, purl: PURL, symbol: 'listOpenSlots' },
53
+ { from: 'list-open-slots', to: 'list-slots', mechanism: 'calls', file: 'app/book/actions.ts', span: { startLine: 14, endLine: 14, contentHash: 'fixture' }, purl: PURL, symbol: 'listSlots' },
54
+ { from: 'list-slots', to: 'Database', mechanism: 'reads', file: 'lib/listSlots.ts', span: { startLine: 9, endLine: 9, contentHash: 'fixture' }, purl: PURL, symbol: 'findOpen' },
55
55
  ],
56
56
  },
57
57
  ];
@@ -40,11 +40,10 @@ const constructsOnly: SubsystemComponent[] = [
40
40
  purl: PURL,
41
41
  file: 'src/api/submitNote.ts',
42
42
  purpose: 'HTTP/handler entry — validates and hands off to the service.',
43
- declarationRef: {
44
- file: 'src/api/submitNote.ts',
43
+ declarationSpan: {
45
44
  startLine: 7,
46
- lineHash: 'storybook-placeholder',
47
- capturedAt: new Date(0).toISOString(),
45
+ endLine: 7,
46
+ contentHash: 'storybook-placeholder',
48
47
  },
49
48
  declaration: {
50
49
  kind: 'function',
@@ -65,11 +64,10 @@ const constructsOnly: SubsystemComponent[] = [
65
64
  purl: PURL,
66
65
  file: 'src/NoteService.ts',
67
66
  purpose: 'Owns intake rules; methods are separate nodes when they matter.',
68
- declarationRef: {
69
- file: 'src/NoteService.ts',
67
+ declarationSpan: {
70
68
  startLine: 3,
71
- lineHash: 'storybook-placeholder',
72
- capturedAt: new Date(0).toISOString(),
69
+ endLine: 3,
70
+ contentHash: 'storybook-placeholder',
73
71
  },
74
72
  declaration: {
75
73
  kind: 'class',
@@ -93,11 +91,10 @@ const constructsOnly: SubsystemComponent[] = [
93
91
  purl: PURL,
94
92
  file: 'src/NoteService.ts',
95
93
  purpose: 'Instance method on NoteService — written as its own construct.',
96
- declarationRef: {
97
- file: 'src/NoteService.ts',
94
+ declarationSpan: {
98
95
  startLine: 14,
99
- lineHash: 'storybook-placeholder',
100
- capturedAt: new Date(0).toISOString(),
96
+ endLine: 14,
97
+ contentHash: 'storybook-placeholder',
101
98
  },
102
99
  declaration: {
103
100
  kind: 'function',
@@ -115,11 +112,10 @@ const constructsOnly: SubsystemComponent[] = [
115
112
  purl: PURL,
116
113
  file: 'src/NoteRepository.ts',
117
114
  purpose: 'Persistence contract the service depends on.',
118
- declarationRef: {
119
- file: 'src/NoteRepository.ts',
115
+ declarationSpan: {
120
116
  startLine: 1,
121
- lineHash: 'storybook-placeholder',
122
- capturedAt: new Date(0).toISOString(),
117
+ endLine: 1,
118
+ contentHash: 'storybook-placeholder',
123
119
  },
124
120
  declaration: {
125
121
  kind: 'type',
@@ -138,11 +134,10 @@ const constructsOnly: SubsystemComponent[] = [
138
134
  purl: PURL,
139
135
  file: 'src/types.ts',
140
136
  purpose: 'Branded id type — a declaration, not a runtime instance.',
141
- declarationRef: {
142
- file: 'src/types.ts',
137
+ declarationSpan: {
143
138
  startLine: 1,
144
- lineHash: 'storybook-placeholder',
145
- capturedAt: new Date(0).toISOString(),
139
+ endLine: 1,
140
+ contentHash: 'storybook-placeholder',
146
141
  },
147
142
  declaration: {
148
143
  kind: 'type',
@@ -157,11 +152,10 @@ const constructsOnly: SubsystemComponent[] = [
157
152
  purl: PURL,
158
153
  file: 'src/types.ts',
159
154
  purpose: 'Closed set of note kinds (idea, task, …).',
160
- declarationRef: {
161
- file: 'src/types.ts',
155
+ declarationSpan: {
162
156
  startLine: 5,
163
- lineHash: 'storybook-placeholder',
164
- capturedAt: new Date(0).toISOString(),
157
+ endLine: 5,
158
+ contentHash: 'storybook-placeholder',
165
159
  },
166
160
  declaration: {
167
161
  kind: 'type',
@@ -237,7 +231,7 @@ function renderFileViewer(file: string, opts?: SubsystemOpenFileOptions) {
237
231
  filePath={file}
238
232
  fileName={file.split('/').pop() ?? file}
239
233
  startLine={startLine}
240
- endLine={startLine}
234
+ endLine={opts?.endLine ?? startLine}
241
235
  focusLine={startLine}
242
236
  contextLines={12}
243
237
  readFile={readStoryFile}
@@ -221,7 +221,7 @@ export const DetailKinds: Story = {
221
221
  };
222
222
 
223
223
  // ---------------------------------------------------------------------------
224
- // Declaration line open — Pierre snippet/file views + declarationRef.
224
+ // Declaration line open — Pierre snippet/file views + declarationSpan.
225
225
  // Click the ComponentDeclaration node → toggle file → click path or L251.
226
226
  // ---------------------------------------------------------------------------
227
227
  const storyFileContents: Record<string, string> = {
@@ -238,11 +238,10 @@ const declarationOpenComponents: SubsystemComponent[] = [
238
238
  purl: 'pkg:github/principal-ai/principal-view-core-library',
239
239
  purpose: 'declaration panel — click file or L251 to open the drawer at the export line',
240
240
  symbol: 'ComponentDeclaration',
241
- declarationRef: {
242
- file: 'packages/subsystems-react/src/subsystem/ComponentDeclaration.tsx',
241
+ declarationSpan: {
243
242
  startLine: 251,
244
- lineHash: 'storybook-placeholder',
245
- capturedAt: new Date(0).toISOString(),
243
+ endLine: 251,
244
+ contentHash: 'storybook-placeholder',
246
245
  },
247
246
  },
248
247
  {
@@ -253,11 +252,10 @@ const declarationOpenComponents: SubsystemComponent[] = [
253
252
  purl: 'pkg:github/principal-ai/principal-view-core-library',
254
253
  purpose: 'graphify type-ref resolver (second file for tree navigation)',
255
254
  symbol: 'resolveGraphifyTypeRef',
256
- declarationRef: {
257
- file: 'packages/subsystems-react/src/graphify/resolve.ts',
255
+ declarationSpan: {
258
256
  startLine: 1,
259
- lineHash: 'storybook-placeholder',
260
- capturedAt: new Date(0).toISOString(),
257
+ endLine: 1,
258
+ contentHash: 'storybook-placeholder',
261
259
  },
262
260
  },
263
261
  ];
@@ -279,7 +277,7 @@ function DeclarationLineOpenDemo() {
279
277
  filePath={file}
280
278
  fileName={file.split('/').pop() ?? file}
281
279
  startLine={startLine}
282
- endLine={startLine}
280
+ endLine={opts?.endLine ?? startLine}
283
281
  focusLine={startLine}
284
282
  contextLines={40}
285
283
  readFile={readStoryFile}