@principal-ai/subsystems-react 0.56.13 → 0.56.14
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 +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/subsystem/C4ProjectionView.d.ts +60 -0
- package/dist/subsystem/C4ProjectionView.d.ts.map +1 -0
- package/dist/subsystem/C4ProjectionView.js +336 -0
- package/dist/subsystem/C4ProjectionView.js.map +1 -0
- package/dist/subsystem/C4SketchView.d.ts +23 -0
- package/dist/subsystem/C4SketchView.d.ts.map +1 -0
- package/dist/subsystem/C4SketchView.js +119 -0
- package/dist/subsystem/C4SketchView.js.map +1 -0
- package/dist/subsystem/__fixtures__/c4Drilldown.d.ts +4 -27
- package/dist/subsystem/__fixtures__/c4Drilldown.d.ts.map +1 -1
- package/dist/subsystem/__fixtures__/c4Drilldown.js +4 -56
- package/dist/subsystem/__fixtures__/c4Drilldown.js.map +1 -1
- package/dist/subsystem/__fixtures__/c4Sketch.d.ts +9 -0
- package/dist/subsystem/__fixtures__/c4Sketch.d.ts.map +1 -0
- package/dist/subsystem/__fixtures__/c4Sketch.js +89 -0
- package/dist/subsystem/__fixtures__/c4Sketch.js.map +1 -0
- package/dist/subsystem/c4Sketch.d.ts +74 -0
- package/dist/subsystem/c4Sketch.d.ts.map +1 -0
- package/dist/subsystem/c4Sketch.js +9 -0
- package/dist/subsystem/c4Sketch.js.map +1 -0
- package/dist/subsystem/c4TrailFocus.d.ts +30 -0
- package/dist/subsystem/c4TrailFocus.d.ts.map +1 -0
- package/dist/subsystem/c4TrailFocus.js +57 -0
- package/dist/subsystem/c4TrailFocus.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +14 -0
- package/src/stories/Canvases/C4/C4Sketch.stories.tsx +36 -0
- package/src/stories/Canvases/C4/C4ToComponentDrill.stories.tsx +18 -429
- package/src/subsystem/C4ProjectionView.tsx +595 -0
- package/src/subsystem/C4SketchView.tsx +241 -0
- package/src/subsystem/__fixtures__/c4Drilldown.ts +7 -58
- package/src/subsystem/__fixtures__/c4Sketch.ts +94 -0
- package/src/subsystem/c4Sketch.ts +78 -0
- package/src/subsystem/c4TrailFocus.ts +62 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C4ProjectionView — a model-derived C4 diagram with its trails in a left
|
|
3
|
+
* sidebar.
|
|
4
|
+
*
|
|
5
|
+
* The host supplies the authored/composed `C4Model`, the subsystem components
|
|
6
|
+
* and trails it was projected from, and the element → member-alias map. This
|
|
7
|
+
* component owns the trail focus, the resulting emphasis, the construct-level
|
|
8
|
+
* drill-down, and the open-container state (controlled or uncontrolled).
|
|
9
|
+
*
|
|
10
|
+
* It is deliberately data-only: no RPC, no store reads. Studio passes a
|
|
11
|
+
* projection built from a stored model; Storybook passes a fixture.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
15
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
16
|
+
import { useTheme } from '@principal-ade/industry-theme';
|
|
17
|
+
import { IndustryMarkdownSlide } from 'themed-markdown';
|
|
18
|
+
import { C4Graph } from './C4Graph';
|
|
19
|
+
import type { C4ConstructGraph, C4GraphProps } from './C4Graph';
|
|
20
|
+
import { GraphWithSidebar } from './GraphWithSidebar';
|
|
21
|
+
import { TrailStepsNav } from './TrailStepsNav';
|
|
22
|
+
import {
|
|
23
|
+
deriveLitC4Edges,
|
|
24
|
+
deriveTrailPlayerElements,
|
|
25
|
+
} from './c4TrailFocus';
|
|
26
|
+
import { deriveGraphEdges, trailStepGraphEdgeId } from './model';
|
|
27
|
+
import type { C4Element, C4Model } from './c4';
|
|
28
|
+
import type { SubsystemComponent, SubsystemTrail } from './model';
|
|
29
|
+
import type { TrailComponentLabels } from './trailStepDisplay';
|
|
30
|
+
|
|
31
|
+
export interface C4ProjectionViewProps {
|
|
32
|
+
/** The C4 projection to draw. */
|
|
33
|
+
model: C4Model;
|
|
34
|
+
/** Subsystem components behind the model, for the construct drill-down. */
|
|
35
|
+
components: readonly SubsystemComponent[];
|
|
36
|
+
/** Ordered trails shown in the left sidebar. */
|
|
37
|
+
trails: readonly SubsystemTrail[];
|
|
38
|
+
/** C4 element id → member aliases. Container membership comes from `process`. */
|
|
39
|
+
memberships: Readonly<Record<string, readonly string[]>>;
|
|
40
|
+
/** Sidebar heading. Falls back to the system label. */
|
|
41
|
+
title?: string;
|
|
42
|
+
/** Optional Markdown description under the heading. */
|
|
43
|
+
description?: string;
|
|
44
|
+
/** Extra controls in the sidebar header (source badge, refresh, …). */
|
|
45
|
+
headerActions?: ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* Controlled open-container ids. Omit to let the view own them. Pass an empty
|
|
48
|
+
* array to start collapsed.
|
|
49
|
+
*/
|
|
50
|
+
openContainerIds?: readonly string[];
|
|
51
|
+
onOpenContainerIdsChange?: (ids: string[]) => void;
|
|
52
|
+
/** Fired when a trail is focused or cleared. */
|
|
53
|
+
onTrailSelect?: (
|
|
54
|
+
ref: { trailId: string; stepIndices?: number[] } | null,
|
|
55
|
+
) => void;
|
|
56
|
+
/** Fired when a trail step is hovered or unhovered. */
|
|
57
|
+
onTrailStepHover?: (
|
|
58
|
+
ref: { trailId: string; stepIndex: number } | null,
|
|
59
|
+
) => void;
|
|
60
|
+
/** Host code viewer for a focused step. */
|
|
61
|
+
renderFileViewer?: C4GraphProps['renderFileViewer'];
|
|
62
|
+
/** Host trail viewer; replaces the file viewer when supplied. */
|
|
63
|
+
renderTrailViewer?: C4GraphProps['renderTrailViewer'];
|
|
64
|
+
/** Suppress the C4 step drawer entirely. */
|
|
65
|
+
hideDrawer?: boolean;
|
|
66
|
+
/** Sidebar width persistence key. */
|
|
67
|
+
persistKey?: string;
|
|
68
|
+
/** Outer style for the flex fill. */
|
|
69
|
+
style?: CSSProperties;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parentIdsOf(model: C4Model): Map<string, string> {
|
|
73
|
+
const parents = new Map<string, string>();
|
|
74
|
+
for (const node of model.nodes) {
|
|
75
|
+
const parent =
|
|
76
|
+
node.kind === 'component' ? (node.container ?? node.parentId) : node.parentId;
|
|
77
|
+
if (parent) parents.set(node.id, parent);
|
|
78
|
+
}
|
|
79
|
+
return parents;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Include every ancestor of a player, so a filtered model never orphans a child. */
|
|
83
|
+
function withAncestors(
|
|
84
|
+
players: ReadonlySet<string>,
|
|
85
|
+
parents: ReadonlyMap<string, string>,
|
|
86
|
+
): Set<string> {
|
|
87
|
+
const out = new Set(players);
|
|
88
|
+
for (const id of players) {
|
|
89
|
+
let parent = parents.get(id);
|
|
90
|
+
while (parent && !out.has(parent)) {
|
|
91
|
+
out.add(parent);
|
|
92
|
+
parent = parents.get(parent);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function containersForTrail(
|
|
99
|
+
trail: SubsystemTrail | undefined,
|
|
100
|
+
model: C4Model,
|
|
101
|
+
memberships: C4ProjectionViewProps['memberships'],
|
|
102
|
+
): string[] {
|
|
103
|
+
if (!trail) return [];
|
|
104
|
+
const aliases = new Set<string>();
|
|
105
|
+
for (const step of trail.steps) {
|
|
106
|
+
aliases.add(step.from);
|
|
107
|
+
aliases.add(step.to);
|
|
108
|
+
}
|
|
109
|
+
return model.nodes
|
|
110
|
+
.filter(
|
|
111
|
+
(node): node is Extract<C4Element, { kind: 'container' }> =>
|
|
112
|
+
node.kind === 'container' &&
|
|
113
|
+
(memberships[node.id] ?? []).some((alias) => aliases.has(alias)),
|
|
114
|
+
)
|
|
115
|
+
.map((node) => node.id);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildConstructGraphs(opts: {
|
|
119
|
+
model: C4Model;
|
|
120
|
+
components: readonly SubsystemComponent[];
|
|
121
|
+
trails: readonly SubsystemTrail[];
|
|
122
|
+
memberships: C4ProjectionViewProps['memberships'];
|
|
123
|
+
focusedTrailId: string | null;
|
|
124
|
+
}): ReadonlyMap<string, C4ConstructGraph> | undefined {
|
|
125
|
+
if (!opts.focusedTrailId) return undefined;
|
|
126
|
+
const trail = opts.trails.find((t) => t.id === opts.focusedTrailId);
|
|
127
|
+
if (!trail) return undefined;
|
|
128
|
+
|
|
129
|
+
const byAlias = new Map(opts.components.map((c) => [c.alias, c]));
|
|
130
|
+
const referenced = new Set<string>();
|
|
131
|
+
for (const step of trail.steps) {
|
|
132
|
+
referenced.add(step.from);
|
|
133
|
+
referenced.add(step.to);
|
|
134
|
+
}
|
|
135
|
+
const componentOf = new Map<string, string>();
|
|
136
|
+
for (const element of opts.model.nodes) {
|
|
137
|
+
if (element.kind !== 'component') continue;
|
|
138
|
+
for (const alias of element.members ?? []) componentOf.set(alias, element.id);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const out = new Map<string, C4ConstructGraph>();
|
|
142
|
+
for (const element of opts.model.nodes) {
|
|
143
|
+
if (element.kind !== 'container') continue;
|
|
144
|
+
const aliases = (opts.memberships[element.id] ?? []).filter(
|
|
145
|
+
(alias) => referenced.has(alias) && byAlias.has(alias),
|
|
146
|
+
);
|
|
147
|
+
if (aliases.length === 0) continue;
|
|
148
|
+
out.set(element.id, {
|
|
149
|
+
components: aliases.map((alias) => byAlias.get(alias)!),
|
|
150
|
+
trails: [trail],
|
|
151
|
+
componentOf,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return out.size > 0 ? out : undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function constructEmphasis(
|
|
158
|
+
activeStep: { trailId: string; stepIndex: number } | null,
|
|
159
|
+
trails: readonly SubsystemTrail[],
|
|
160
|
+
constructGraphs: ReadonlyMap<string, C4ConstructGraph> | undefined,
|
|
161
|
+
): {
|
|
162
|
+
ids: ReadonlySet<string> | null;
|
|
163
|
+
edgeIds: ReadonlySet<string> | null;
|
|
164
|
+
} {
|
|
165
|
+
if (!activeStep) return { ids: null, edgeIds: null };
|
|
166
|
+
const trail = trails.find((t) => t.id === activeStep.trailId);
|
|
167
|
+
if (!trail) return { ids: null, edgeIds: null };
|
|
168
|
+
const step = trail.steps[activeStep.stepIndex];
|
|
169
|
+
if (!step) return { ids: null, edgeIds: null };
|
|
170
|
+
const steps = [step];
|
|
171
|
+
const spotlight = new Set<string>();
|
|
172
|
+
const litEdges = new Set<string>();
|
|
173
|
+
for (const step of steps) {
|
|
174
|
+
spotlight.add(step.from);
|
|
175
|
+
spotlight.add(step.to);
|
|
176
|
+
litEdges.add(trailStepGraphEdgeId(step));
|
|
177
|
+
}
|
|
178
|
+
if (!constructGraphs) return { ids: null, edgeIds: null };
|
|
179
|
+
|
|
180
|
+
const allAliases = new Set<string>();
|
|
181
|
+
const allEdgeIds = new Set<string>();
|
|
182
|
+
for (const graph of constructGraphs.values()) {
|
|
183
|
+
for (const component of graph.components) allAliases.add(component.alias);
|
|
184
|
+
for (const edge of deriveGraphEdges({ trails: [...graph.trails] })) {
|
|
185
|
+
allEdgeIds.add(edge.id);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
ids: new Set([...allAliases].filter((alias) => !spotlight.has(alias))),
|
|
190
|
+
edgeIds: new Set([...allEdgeIds].filter((id) => !litEdges.has(id))),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function C4ProjectionView({
|
|
195
|
+
model,
|
|
196
|
+
components,
|
|
197
|
+
trails,
|
|
198
|
+
memberships,
|
|
199
|
+
title,
|
|
200
|
+
description,
|
|
201
|
+
headerActions,
|
|
202
|
+
openContainerIds,
|
|
203
|
+
onOpenContainerIdsChange,
|
|
204
|
+
onTrailSelect,
|
|
205
|
+
onTrailStepHover,
|
|
206
|
+
renderFileViewer,
|
|
207
|
+
renderTrailViewer,
|
|
208
|
+
hideDrawer = false,
|
|
209
|
+
persistKey,
|
|
210
|
+
style,
|
|
211
|
+
}: C4ProjectionViewProps) {
|
|
212
|
+
const { theme } = useTheme();
|
|
213
|
+
const [internalOpenIds, setInternalOpenIds] = useState<string[]>(() =>
|
|
214
|
+
[...(openContainerIds ?? model.openContainerIds ?? [])].sort(),
|
|
215
|
+
);
|
|
216
|
+
const resolvedOpenIds = openContainerIds
|
|
217
|
+
? [...openContainerIds]
|
|
218
|
+
: internalOpenIds;
|
|
219
|
+
const setOpenIds = useCallback(
|
|
220
|
+
(next: string[]) => {
|
|
221
|
+
if (!openContainerIds) setInternalOpenIds(next);
|
|
222
|
+
onOpenContainerIdsChange?.(next);
|
|
223
|
+
},
|
|
224
|
+
[onOpenContainerIdsChange, openContainerIds],
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const [focusedTrailId, setFocusedTrailId] = useState<string | null>(null);
|
|
228
|
+
const [hoveredStep, setHoveredStep] = useState<{
|
|
229
|
+
trailId: string;
|
|
230
|
+
stepIndex: number | null;
|
|
231
|
+
} | null>(null);
|
|
232
|
+
const [focusedStep, setFocusedStep] = useState<{
|
|
233
|
+
trailId: string;
|
|
234
|
+
stepIndex: number;
|
|
235
|
+
} | null>(null);
|
|
236
|
+
const trailOpenedRef = useRef<Set<string>>(new Set());
|
|
237
|
+
|
|
238
|
+
const parents = useMemo(() => parentIdsOf(model), [model]);
|
|
239
|
+
const allElementIds = useMemo(
|
|
240
|
+
() => model.nodes.map((node) => node.id),
|
|
241
|
+
[model],
|
|
242
|
+
);
|
|
243
|
+
const allEdgeIds = useMemo(
|
|
244
|
+
() => model.edges.map((edge) => edge.id),
|
|
245
|
+
[model],
|
|
246
|
+
);
|
|
247
|
+
const componentByAlias = useMemo<TrailComponentLabels>(
|
|
248
|
+
() => new Map(components.map((component) => [component.alias, component])),
|
|
249
|
+
[components],
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const focusedTrail = focusedTrailId
|
|
253
|
+
? trails.find((trail) => trail.id === focusedTrailId)
|
|
254
|
+
: undefined;
|
|
255
|
+
const trailPlayers = useMemo(
|
|
256
|
+
() =>
|
|
257
|
+
focusedTrail
|
|
258
|
+
? withAncestors(
|
|
259
|
+
deriveTrailPlayerElements(focusedTrail.steps, memberships),
|
|
260
|
+
parents,
|
|
261
|
+
)
|
|
262
|
+
: null,
|
|
263
|
+
[focusedTrail, memberships, parents],
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const renderedModel = useMemo(() => {
|
|
267
|
+
if (!trailPlayers) {
|
|
268
|
+
return { ...model, openContainerIds: resolvedOpenIds };
|
|
269
|
+
}
|
|
270
|
+
const nodes = model.nodes.filter((node) => trailPlayers.has(node.id));
|
|
271
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
272
|
+
const edges = model.edges.filter(
|
|
273
|
+
(edge) => ids.has(edge.source) && ids.has(edge.target),
|
|
274
|
+
);
|
|
275
|
+
return { ...model, nodes, edges, openContainerIds: resolvedOpenIds };
|
|
276
|
+
}, [model, resolvedOpenIds, trailPlayers]);
|
|
277
|
+
|
|
278
|
+
const containers = useCallback(
|
|
279
|
+
(trailId: string) => {
|
|
280
|
+
const trail = trails.find((t) => t.id === trailId);
|
|
281
|
+
return containersForTrail(trail, model, memberships);
|
|
282
|
+
},
|
|
283
|
+
[memberships, model, trails],
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const focusTrail = useCallback(
|
|
287
|
+
(trailId: string) => {
|
|
288
|
+
const previouslyOpened = trailOpenedRef.current;
|
|
289
|
+
const nextTrailContainers = containers(trailId);
|
|
290
|
+
trailOpenedRef.current = new Set(nextTrailContainers);
|
|
291
|
+
const next = [
|
|
292
|
+
...new Set([
|
|
293
|
+
...resolvedOpenIds.filter((id) => !previouslyOpened.has(id)),
|
|
294
|
+
...nextTrailContainers,
|
|
295
|
+
]),
|
|
296
|
+
].sort();
|
|
297
|
+
setFocusedTrailId(trailId);
|
|
298
|
+
setHoveredStep(null);
|
|
299
|
+
setOpenIds(next);
|
|
300
|
+
onTrailSelect?.({ trailId });
|
|
301
|
+
},
|
|
302
|
+
[containers, onTrailSelect, resolvedOpenIds, setOpenIds],
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const clearFocus = useCallback(() => {
|
|
306
|
+
const opened = trailOpenedRef.current;
|
|
307
|
+
trailOpenedRef.current = new Set();
|
|
308
|
+
setFocusedTrailId(null);
|
|
309
|
+
setHoveredStep(null);
|
|
310
|
+
if (opened.size > 0) {
|
|
311
|
+
setOpenIds(resolvedOpenIds.filter((id) => !opened.has(id)));
|
|
312
|
+
}
|
|
313
|
+
onTrailSelect?.(null);
|
|
314
|
+
}, [onTrailSelect, resolvedOpenIds, setOpenIds]);
|
|
315
|
+
|
|
316
|
+
const toggleContainer = useCallback(
|
|
317
|
+
(id: string) => {
|
|
318
|
+
setOpenIds(
|
|
319
|
+
resolvedOpenIds.includes(id)
|
|
320
|
+
? resolvedOpenIds.filter((openId) => openId !== id)
|
|
321
|
+
: [...resolvedOpenIds, id].sort(),
|
|
322
|
+
);
|
|
323
|
+
},
|
|
324
|
+
[resolvedOpenIds, setOpenIds],
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const activeStep = focusedStep ?? hoveredStep;
|
|
328
|
+
const emphasis = useMemo(() => {
|
|
329
|
+
let hiddenElementIds: ReadonlySet<string> | null = null;
|
|
330
|
+
let hiddenEdgeIds: ReadonlySet<string> | null = null;
|
|
331
|
+
if (focusedTrail) {
|
|
332
|
+
const players = trailPlayers ?? new Set<string>();
|
|
333
|
+
hiddenElementIds = new Set(
|
|
334
|
+
allElementIds.filter((id) => !players.has(id)),
|
|
335
|
+
);
|
|
336
|
+
const lit = deriveLitC4Edges(
|
|
337
|
+
{ trailId: focusedTrail.id },
|
|
338
|
+
model.edges,
|
|
339
|
+
);
|
|
340
|
+
hiddenEdgeIds = new Set(allEdgeIds.filter((id) => !lit.has(id)));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let dimmedElementIds: ReadonlySet<string> | null = null;
|
|
344
|
+
let dimmedEdgeIds: ReadonlySet<string> | null = null;
|
|
345
|
+
if (activeStep) {
|
|
346
|
+
const trail = trails.find((t) => t.id === activeStep.trailId);
|
|
347
|
+
const steps =
|
|
348
|
+
!trail
|
|
349
|
+
? []
|
|
350
|
+
: activeStep.stepIndex != null
|
|
351
|
+
? trail.steps.slice(activeStep.stepIndex, activeStep.stepIndex + 1)
|
|
352
|
+
: trail.steps;
|
|
353
|
+
const spotlight = deriveTrailPlayerElements(steps, memberships);
|
|
354
|
+
dimmedElementIds = new Set(
|
|
355
|
+
allElementIds.filter((id) => !spotlight.has(id)),
|
|
356
|
+
);
|
|
357
|
+
const lit = deriveLitC4Edges(
|
|
358
|
+
{
|
|
359
|
+
trailId: activeStep.trailId,
|
|
360
|
+
...(activeStep.stepIndex != null
|
|
361
|
+
? { stepIndex: activeStep.stepIndex }
|
|
362
|
+
: {}),
|
|
363
|
+
},
|
|
364
|
+
model.edges,
|
|
365
|
+
);
|
|
366
|
+
dimmedEdgeIds = new Set(allEdgeIds.filter((id) => !lit.has(id)));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
hiddenElementIds,
|
|
371
|
+
hiddenEdgeIds,
|
|
372
|
+
dimmedElementIds,
|
|
373
|
+
dimmedEdgeIds,
|
|
374
|
+
};
|
|
375
|
+
}, [
|
|
376
|
+
activeStep,
|
|
377
|
+
allEdgeIds,
|
|
378
|
+
allElementIds,
|
|
379
|
+
focusedTrail,
|
|
380
|
+
memberships,
|
|
381
|
+
model.edges,
|
|
382
|
+
trailPlayers,
|
|
383
|
+
trails,
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
const constructGraphs = useMemo(
|
|
387
|
+
() =>
|
|
388
|
+
buildConstructGraphs({
|
|
389
|
+
model,
|
|
390
|
+
components,
|
|
391
|
+
trails,
|
|
392
|
+
memberships,
|
|
393
|
+
focusedTrailId,
|
|
394
|
+
}),
|
|
395
|
+
[components, focusedTrailId, memberships, model, trails],
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const constructStage = useMemo(
|
|
399
|
+
() =>
|
|
400
|
+
constructEmphasis(
|
|
401
|
+
activeStep && activeStep.stepIndex != null
|
|
402
|
+
? { trailId: activeStep.trailId, stepIndex: activeStep.stepIndex }
|
|
403
|
+
: null,
|
|
404
|
+
trails,
|
|
405
|
+
constructGraphs,
|
|
406
|
+
),
|
|
407
|
+
[activeStep, constructGraphs, trails],
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const openAll = useCallback(() => {
|
|
411
|
+
setOpenIds(
|
|
412
|
+
model.nodes
|
|
413
|
+
.filter((node) => node.kind === 'container')
|
|
414
|
+
.map((node) => node.id)
|
|
415
|
+
.sort(),
|
|
416
|
+
);
|
|
417
|
+
}, [model, setOpenIds]);
|
|
418
|
+
|
|
419
|
+
const collapseAll = useCallback(() => {
|
|
420
|
+
setOpenIds([]);
|
|
421
|
+
}, [setOpenIds]);
|
|
422
|
+
|
|
423
|
+
const sidebarHeader = description ? (
|
|
424
|
+
<div style={{ fontSize: theme.fontSizes[0], lineHeight: 1.5 }}>
|
|
425
|
+
<IndustryMarkdownSlide
|
|
426
|
+
content={description}
|
|
427
|
+
slideIdPrefix="c4-projection-desc"
|
|
428
|
+
slideIndex={0}
|
|
429
|
+
isVisible={true}
|
|
430
|
+
theme={theme}
|
|
431
|
+
disableScroll={true}
|
|
432
|
+
disableBasePadding
|
|
433
|
+
enableKeyboardScrolling={false}
|
|
434
|
+
autoFocusOnVisible={false}
|
|
435
|
+
/>
|
|
436
|
+
</div>
|
|
437
|
+
) : null;
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<div
|
|
441
|
+
style={{
|
|
442
|
+
width: '100%',
|
|
443
|
+
height: '100%',
|
|
444
|
+
display: 'flex',
|
|
445
|
+
flexDirection: 'column',
|
|
446
|
+
background: theme.colors.background,
|
|
447
|
+
...style,
|
|
448
|
+
}}
|
|
449
|
+
>
|
|
450
|
+
<header
|
|
451
|
+
style={{
|
|
452
|
+
flexShrink: 0,
|
|
453
|
+
display: 'flex',
|
|
454
|
+
alignItems: 'center',
|
|
455
|
+
gap: 12,
|
|
456
|
+
padding: '12px 16px',
|
|
457
|
+
background: theme.colors.background,
|
|
458
|
+
borderBottom: `1px solid ${theme.colors.border}`,
|
|
459
|
+
}}
|
|
460
|
+
>
|
|
461
|
+
<h2
|
|
462
|
+
style={{
|
|
463
|
+
margin: 0,
|
|
464
|
+
minWidth: 0,
|
|
465
|
+
flex: 1,
|
|
466
|
+
fontSize: theme.fontSizes[2],
|
|
467
|
+
fontWeight: 600,
|
|
468
|
+
color: theme.colors.text,
|
|
469
|
+
fontFamily: theme.fonts.monospace,
|
|
470
|
+
overflowWrap: 'anywhere',
|
|
471
|
+
}}
|
|
472
|
+
>
|
|
473
|
+
{title ?? model.system.label}
|
|
474
|
+
</h2>
|
|
475
|
+
<div
|
|
476
|
+
style={{
|
|
477
|
+
marginLeft: 'auto',
|
|
478
|
+
flexShrink: 0,
|
|
479
|
+
display: 'flex',
|
|
480
|
+
alignItems: 'center',
|
|
481
|
+
gap: 6,
|
|
482
|
+
}}
|
|
483
|
+
>
|
|
484
|
+
{headerActions}
|
|
485
|
+
<button type="button" onClick={openAll} style={smallButton(theme)}>
|
|
486
|
+
Open all
|
|
487
|
+
</button>
|
|
488
|
+
<button
|
|
489
|
+
type="button"
|
|
490
|
+
onClick={collapseAll}
|
|
491
|
+
style={smallButton(theme)}
|
|
492
|
+
>
|
|
493
|
+
Collapse all
|
|
494
|
+
</button>
|
|
495
|
+
</div>
|
|
496
|
+
</header>
|
|
497
|
+
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
|
498
|
+
<GraphWithSidebar
|
|
499
|
+
persistKey={persistKey}
|
|
500
|
+
defaultWidth={320}
|
|
501
|
+
minWidth={240}
|
|
502
|
+
maxWidth={480}
|
|
503
|
+
headerMaxHeight="30%"
|
|
504
|
+
header={sidebarHeader}
|
|
505
|
+
sidebarBody={
|
|
506
|
+
<TrailStepsNav
|
|
507
|
+
trails={[...trails]}
|
|
508
|
+
focusedTrailId={focusedTrailId}
|
|
509
|
+
focusedStepIndex={
|
|
510
|
+
focusedStep && focusedStep.trailId === focusedTrailId
|
|
511
|
+
? focusedStep.stepIndex
|
|
512
|
+
: null
|
|
513
|
+
}
|
|
514
|
+
hoveredTrailStep={hoveredStep}
|
|
515
|
+
componentByAlias={componentByAlias}
|
|
516
|
+
onFocusFlow={(trail) => {
|
|
517
|
+
setFocusedStep(null);
|
|
518
|
+
focusTrail(trail.id);
|
|
519
|
+
}}
|
|
520
|
+
onClearFocus={() => {
|
|
521
|
+
setFocusedStep(null);
|
|
522
|
+
clearFocus();
|
|
523
|
+
}}
|
|
524
|
+
onFocusStep={(trail, stepIndex) => {
|
|
525
|
+
setFocusedStep((prev) =>
|
|
526
|
+
prev &&
|
|
527
|
+
prev.trailId === trail.id &&
|
|
528
|
+
prev.stepIndex === stepIndex
|
|
529
|
+
? null
|
|
530
|
+
: { trailId: trail.id, stepIndex },
|
|
531
|
+
);
|
|
532
|
+
focusTrail(trail.id);
|
|
533
|
+
}}
|
|
534
|
+
onHoverStep={(trail, stepIndex) => {
|
|
535
|
+
setHoveredStep({ trailId: trail.id, stepIndex });
|
|
536
|
+
onTrailStepHover?.({ trailId: trail.id, stepIndex });
|
|
537
|
+
}}
|
|
538
|
+
onHoverFlow={(trail) => {
|
|
539
|
+
setHoveredStep({ trailId: trail.id, stepIndex: null });
|
|
540
|
+
onTrailStepHover?.(null);
|
|
541
|
+
}}
|
|
542
|
+
onLeaveStep={() => {
|
|
543
|
+
setHoveredStep(null);
|
|
544
|
+
onTrailStepHover?.(null);
|
|
545
|
+
}}
|
|
546
|
+
/>
|
|
547
|
+
}
|
|
548
|
+
>
|
|
549
|
+
<C4Graph
|
|
550
|
+
model={renderedModel}
|
|
551
|
+
onSelectEdge={(edge) => {
|
|
552
|
+
const refs = edge?.trailRefs ?? [];
|
|
553
|
+
if (refs.length > 0) focusTrail(refs[0]!.trailId);
|
|
554
|
+
}}
|
|
555
|
+
onTrailClick={(ref) => focusTrail(ref.trailId)}
|
|
556
|
+
onToggleContainer={toggleContainer}
|
|
557
|
+
hiddenElementIds={emphasis.hiddenElementIds}
|
|
558
|
+
hiddenEdgeIds={emphasis.hiddenEdgeIds}
|
|
559
|
+
dimmedElementIds={emphasis.dimmedElementIds}
|
|
560
|
+
dimmedEdgeIds={emphasis.dimmedEdgeIds}
|
|
561
|
+
dimmedConstructIds={constructStage.ids}
|
|
562
|
+
dimmedConstructEdgeIds={constructStage.edgeIds}
|
|
563
|
+
constructGraphByContainer={constructGraphs}
|
|
564
|
+
focusedStep={focusedStep}
|
|
565
|
+
onCloseStep={() => setFocusedStep(null)}
|
|
566
|
+
renderFileViewer={renderFileViewer}
|
|
567
|
+
renderTrailViewer={renderTrailViewer}
|
|
568
|
+
hideDrawer={hideDrawer}
|
|
569
|
+
/>
|
|
570
|
+
</GraphWithSidebar>
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function smallButton(theme: ReturnType<typeof useTheme>['theme']) {
|
|
577
|
+
return {
|
|
578
|
+
flexShrink: 0,
|
|
579
|
+
display: 'inline-flex',
|
|
580
|
+
alignItems: 'center',
|
|
581
|
+
justifyContent: 'center',
|
|
582
|
+
width: 132,
|
|
583
|
+
height: 34,
|
|
584
|
+
padding: '0 12px',
|
|
585
|
+
boxSizing: 'border-box',
|
|
586
|
+
whiteSpace: 'nowrap',
|
|
587
|
+
borderRadius: 6,
|
|
588
|
+
border: `1px solid ${theme.colors.border}`,
|
|
589
|
+
background: 'transparent',
|
|
590
|
+
color: theme.colors.textSecondary,
|
|
591
|
+
fontFamily: theme.fonts.body,
|
|
592
|
+
fontSize: theme.fontSizes[1],
|
|
593
|
+
cursor: 'pointer',
|
|
594
|
+
} as const;
|
|
595
|
+
}
|