@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.
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreTrailCodeView.js +7 -5
- package/dist/pierre/PierreTrailCodeView.js.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +1 -2
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts +144 -9
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +924 -148
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/C4NodeCard.d.ts +22 -1
- package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
- package/dist/subsystem/C4NodeCard.js +10 -2
- package/dist/subsystem/C4NodeCard.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +5 -4
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +12 -4
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
- package/dist/subsystem/ConstructsCatalog.js +10 -7
- package/dist/subsystem/ConstructsCatalog.js.map +1 -1
- package/dist/subsystem/GraphSidebar.d.ts +71 -0
- package/dist/subsystem/GraphSidebar.d.ts.map +1 -0
- package/dist/subsystem/GraphSidebar.js +145 -0
- package/dist/subsystem/GraphSidebar.js.map +1 -0
- package/dist/subsystem/GraphWithSidebar.d.ts +24 -0
- package/dist/subsystem/GraphWithSidebar.d.ts.map +1 -0
- package/dist/subsystem/GraphWithSidebar.js +25 -0
- package/dist/subsystem/GraphWithSidebar.js.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts +33 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +364 -411
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/TrailStepsNav.d.ts +48 -0
- package/dist/subsystem/TrailStepsNav.d.ts.map +1 -0
- package/dist/subsystem/TrailStepsNav.js +189 -0
- package/dist/subsystem/TrailStepsNav.js.map +1 -0
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +3 -4
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/dist/subsystem/__fixtures__/c4Drilldown.d.ts +151 -0
- package/dist/subsystem/__fixtures__/c4Drilldown.d.ts.map +1 -0
- package/dist/subsystem/__fixtures__/c4Drilldown.js +783 -0
- package/dist/subsystem/__fixtures__/c4Drilldown.js.map +1 -0
- package/dist/subsystem/c4.d.ts +39 -8
- package/dist/subsystem/c4.d.ts.map +1 -1
- package/dist/subsystem/c4.js.map +1 -1
- package/dist/subsystem/declarationRef.d.ts +26 -18
- package/dist/subsystem/declarationRef.d.ts.map +1 -1
- package/dist/subsystem/declarationRef.js +9 -4
- package/dist/subsystem/declarationRef.js.map +1 -1
- package/dist/subsystem/model.d.ts +19 -1
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +12 -2
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +24 -4
- package/dist/subsystem/nodes.js.map +1 -1
- package/dist/subsystem/trailBrief.d.ts.map +1 -1
- package/dist/subsystem/trailBrief.js +6 -1
- package/dist/subsystem/trailBrief.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +9 -2
- package/src/pierre/PierreTrailCodeView.tsx +9 -13
- package/src/stories/Subsystem/C4Graph/C4ToComponentDrill.stories.tsx +496 -0
- package/src/stories/Subsystem/ComponentGraph/AnimatedProgression.stories.tsx +7 -7
- package/src/stories/Subsystem/ComponentGraph/Constructs.stories.tsx +19 -25
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +8 -10
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +19 -19
- package/src/stories/Subsystem/ComponentGraph/ProposedTrails.stories.tsx +6 -6
- package/src/stories/Subsystem/ComponentGraph/TrailAutoplay.stories.tsx +7 -7
- package/src/stories/Subsystem/ComponentGraph/Trails.stories.tsx +27 -30
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +1 -2
- package/src/stories/Subsystem/ConstructsCatalog.stories.tsx +10 -13
- package/src/stories/Subsystem/EgoGraph/EgoGraph.stories.tsx +3 -4
- package/src/stories/Subsystem/Sidebar/GraphSidebar.stories.tsx +206 -0
- package/src/subsystem/C4Graph.groups.test.ts +96 -2
- package/src/subsystem/C4Graph.tsx +1377 -165
- package/src/subsystem/C4NodeCard.test.ts +1 -2
- package/src/subsystem/C4NodeCard.tsx +24 -2
- package/src/subsystem/ComponentDeclaration.tsx +18 -8
- package/src/subsystem/ConstructsCatalog.tsx +23 -8
- package/src/subsystem/GraphSidebar.test.tsx +152 -0
- package/src/subsystem/GraphSidebar.tsx +300 -0
- package/src/subsystem/GraphWithSidebar.test.tsx +78 -0
- package/src/subsystem/GraphWithSidebar.tsx +60 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +284 -370
- package/src/subsystem/TrailStepsNav.test.tsx +179 -0
- package/src/subsystem/TrailStepsNav.tsx +372 -0
- package/src/subsystem/TrailsPanel.tsx +3 -4
- package/src/subsystem/__fixtures__/c4Drilldown.test.ts +338 -0
- package/src/subsystem/__fixtures__/c4Drilldown.ts +847 -0
- package/src/subsystem/c4.ts +41 -8
- package/src/subsystem/declarationRef.ts +28 -19
- package/src/subsystem/model.test.ts +32 -12
- package/src/subsystem/model.ts +30 -1
- package/src/subsystem/nodes.group.test.tsx +82 -0
- package/src/subsystem/nodes.tsx +30 -2
- package/src/subsystem/trailBrief.test.ts +2 -2
- 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',
|
|
43
|
-
{ from: 'book-slot', to: 'create-booking', mechanism: 'calls', file: 'app/book/actions.ts',
|
|
44
|
-
{ from: 'create-booking', to: 'Database', mechanism: 'writes', file: 'lib/createBooking.ts',
|
|
45
|
-
{ from: 'booking-page', to: 'capture-event', mechanism: 'calls', file: 'app/book/page.tsx',
|
|
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',
|
|
53
|
-
{ from: 'list-open-slots', to: 'list-slots', mechanism: 'calls', file: 'app/book/actions.ts',
|
|
54
|
-
{ from: 'list-slots', to: 'Database', mechanism: 'reads', file: 'lib/listSlots.ts',
|
|
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
|
-
|
|
44
|
-
file: 'src/api/submitNote.ts',
|
|
43
|
+
declarationSpan: {
|
|
45
44
|
startLine: 7,
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
69
|
-
file: 'src/NoteService.ts',
|
|
67
|
+
declarationSpan: {
|
|
70
68
|
startLine: 3,
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
97
|
-
file: 'src/NoteService.ts',
|
|
94
|
+
declarationSpan: {
|
|
98
95
|
startLine: 14,
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
119
|
-
file: 'src/NoteRepository.ts',
|
|
115
|
+
declarationSpan: {
|
|
120
116
|
startLine: 1,
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
142
|
-
file: 'src/types.ts',
|
|
137
|
+
declarationSpan: {
|
|
143
138
|
startLine: 1,
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
161
|
-
file: 'src/types.ts',
|
|
155
|
+
declarationSpan: {
|
|
162
156
|
startLine: 5,
|
|
163
|
-
|
|
164
|
-
|
|
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 +
|
|
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
|
-
|
|
242
|
-
file: 'packages/subsystems-react/src/subsystem/ComponentDeclaration.tsx',
|
|
241
|
+
declarationSpan: {
|
|
243
242
|
startLine: 251,
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
257
|
-
file: 'packages/subsystems-react/src/graphify/resolve.ts',
|
|
255
|
+
declarationSpan: {
|
|
258
256
|
startLine: 1,
|
|
259
|
-
|
|
260
|
-
|
|
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}
|