@principal-ai/subsystems-react 0.50.0 → 0.51.1
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 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreTrailCodeView.d.ts +1 -1
- package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreTrailCodeView.js +44 -21
- package/dist/pierre/PierreTrailCodeView.js.map +1 -1
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts +493 -0
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts.map +1 -0
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js +329 -0
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js.map +1 -0
- package/dist/subsystem/C4Graph.d.ts +2 -2
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +38 -73
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/C4NodeCard.d.ts +119 -0
- package/dist/subsystem/C4NodeCard.d.ts.map +1 -0
- package/dist/subsystem/C4NodeCard.js +223 -0
- package/dist/subsystem/C4NodeCard.js.map +1 -0
- package/dist/subsystem/DiagnosticToggle.d.ts +8 -1
- package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -1
- package/dist/subsystem/DiagnosticToggle.js +40 -6
- package/dist/subsystem/DiagnosticToggle.js.map +1 -1
- package/dist/subsystem/FileDrawer.d.ts.map +1 -1
- package/dist/subsystem/FileDrawer.js +20 -21
- package/dist/subsystem/FileDrawer.js.map +1 -1
- package/dist/subsystem/IssueList.js +1 -1
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +13 -4
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/c4.d.ts +272 -0
- package/dist/subsystem/c4.d.ts.map +1 -0
- package/dist/subsystem/c4.js +322 -0
- package/dist/subsystem/c4.js.map +1 -0
- package/dist/subsystem/c4Evidence.d.ts +143 -0
- package/dist/subsystem/c4Evidence.d.ts.map +1 -0
- package/dist/subsystem/c4Evidence.js +240 -0
- package/dist/subsystem/c4Evidence.js.map +1 -0
- package/dist/subsystem/model.d.ts +27 -7
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +15 -0
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +78 -7
- package/dist/subsystem/nodes.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +37 -0
- package/src/pierre/PierreTrailCodeView.tsx +49 -21
- package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +413 -0
- package/src/stories/Subsystem/C4Graph/ProposedElements.stories.tsx +293 -0
- package/src/stories/Subsystem/C4Graph/c4ProposedElements.fixture.ts +366 -0
- package/src/stories/Subsystem/ComponentGraph/Deprecated.stories.tsx +358 -0
- package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +27 -2
- package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +1 -1
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +1 -1
- package/src/stories/Subsystem/ComponentGraph/{Flows.stories.tsx → Trails.stories.tsx} +4 -4
- package/src/subsystem/C4Graph.tsx +81 -107
- package/src/subsystem/C4NodeCard.test.ts +301 -0
- package/src/subsystem/C4NodeCard.tsx +301 -0
- package/src/subsystem/DiagnosticToggle.tsx +58 -4
- package/src/subsystem/FileDrawer.tsx +32 -34
- package/src/subsystem/IssueList.tsx +1 -1
- package/src/subsystem/SubsystemComponentGraph.tsx +13 -5
- package/src/subsystem/c4.test.ts +573 -0
- package/src/subsystem/c4.ts +558 -0
- package/src/subsystem/c4Evidence.test.ts +386 -0
- package/src/subsystem/c4Evidence.ts +329 -0
- package/src/subsystem/model.ts +36 -7
- package/src/subsystem/nodes.tsx +124 -5
- package/dist/stories/Subsystem/C4Graph/c4Fixture.d.ts +0 -3
- package/dist/stories/Subsystem/C4Graph/c4Fixture.d.ts.map +0 -1
- package/dist/stories/Subsystem/C4Graph/c4Fixture.js +0 -2
- package/dist/stories/Subsystem/C4Graph/c4Fixture.js.map +0 -1
- package/dist/subsystem/toC4.d.ts +0 -95
- package/dist/subsystem/toC4.d.ts.map +0 -1
- package/dist/subsystem/toC4.js +0 -192
- package/dist/subsystem/toC4.js.map +0 -1
- package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +0 -101
- package/src/stories/Subsystem/C4Graph/c4Fixture.ts +0 -6
- package/src/subsystem/toC4.test.ts +0 -114
- package/src/subsystem/toC4.ts +0 -273
|
@@ -68,15 +68,10 @@ function stepIndexFromItemContext(context: unknown): number | null {
|
|
|
68
68
|
return Number.isFinite(index) ? index : null;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* line-height 1.15 plus 2px vertical padding) — CodeView sizes its
|
|
76
|
-
* virtualization window and sticky offset from this, and the library's own
|
|
77
|
-
* default of 44 is more than double the compact header.
|
|
78
|
-
*/
|
|
79
|
-
const TRAIL_HEADER_HEIGHT = 18;
|
|
71
|
+
/** Line-height multiplier for the compact per-file header. */
|
|
72
|
+
const TRAIL_HEADER_LINE_HEIGHT = 1.15;
|
|
73
|
+
/** Vertical padding on the compact per-file header, px (each side). */
|
|
74
|
+
const TRAIL_HEADER_PAD_Y = 2;
|
|
80
75
|
|
|
81
76
|
/** Pointer/dotted-underline affordance for a clickable construct token. */
|
|
82
77
|
function paintSymbolToken(el: HTMLElement | undefined, active: boolean): void {
|
|
@@ -208,7 +203,7 @@ export function PierreTrailCodeView({
|
|
|
208
203
|
stepIndex,
|
|
209
204
|
readFile,
|
|
210
205
|
contextLines = 8,
|
|
211
|
-
background,
|
|
206
|
+
background: backgroundProp,
|
|
212
207
|
onOpenFile,
|
|
213
208
|
onVisibleStepChange,
|
|
214
209
|
proposedAliases,
|
|
@@ -216,6 +211,14 @@ export function PierreTrailCodeView({
|
|
|
216
211
|
onSymbolClick,
|
|
217
212
|
}: PierreTrailCodeViewProps) {
|
|
218
213
|
const { theme, mode } = useTheme();
|
|
214
|
+
const background = backgroundProp ?? theme.colors.background;
|
|
215
|
+
// The per-file header renders the step's file path (Pierre's `[data-title]`),
|
|
216
|
+
// so this font size is what the path reads at. It also drives CodeView's
|
|
217
|
+
// virtualization window and sticky offset via `diffHeaderHeight`, so derive
|
|
218
|
+
// the header height from it rather than tracking two numbers by hand.
|
|
219
|
+
const headerFontSize = theme.fontSizes[1];
|
|
220
|
+
const headerHeight =
|
|
221
|
+
Math.round(headerFontSize * TRAIL_HEADER_LINE_HEIGHT) + 2 * TRAIL_HEADER_PAD_Y;
|
|
219
222
|
const viewRef = useRef<CodeViewHandle<undefined>>(null);
|
|
220
223
|
// CodeView's scroll container — also the box we measure items against.
|
|
221
224
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -261,10 +264,34 @@ export function PierreTrailCodeView({
|
|
|
261
264
|
onVisibleStepChangeRef.current?.(next);
|
|
262
265
|
}, []);
|
|
263
266
|
|
|
264
|
-
|
|
265
|
-
|
|
267
|
+
// Coalesce measurement to one report per frame, after layout settles.
|
|
268
|
+
// `onPostRender` is a CodeView *shared file callback*: it fires for every item
|
|
269
|
+
// as virtualization mounts/unmounts them mid-scroll. Measuring synchronously
|
|
270
|
+
// there read geometry before the browser had laid the newly-mounted items out,
|
|
271
|
+
// so the "topmost visible" step bounced between values instead of advancing
|
|
272
|
+
// one step at a time. Deferring to rAF measures settled geometry once.
|
|
273
|
+
const visibleReportRaf = useRef<number | null>(null);
|
|
274
|
+
const scheduleVisibleStepReport = useCallback(() => {
|
|
275
|
+
if (visibleReportRaf.current != null) return;
|
|
276
|
+
visibleReportRaf.current = window.requestAnimationFrame(() => {
|
|
277
|
+
visibleReportRaf.current = null;
|
|
278
|
+
reportVisibleStep();
|
|
279
|
+
});
|
|
266
280
|
}, [reportVisibleStep]);
|
|
267
281
|
|
|
282
|
+
useEffect(
|
|
283
|
+
() => () => {
|
|
284
|
+
if (visibleReportRaf.current != null) {
|
|
285
|
+
window.cancelAnimationFrame(visibleReportRaf.current);
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
[],
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const onCodeViewScroll = useCallback(() => {
|
|
292
|
+
scheduleVisibleStepReport();
|
|
293
|
+
}, [scheduleVisibleStepReport]);
|
|
294
|
+
|
|
268
295
|
// A step onto a proposed component is planned work; label its missing file
|
|
269
296
|
// accordingly instead of showing a bare "not found".
|
|
270
297
|
const isProposedStep = useCallback(
|
|
@@ -404,14 +431,15 @@ export function PierreTrailCodeView({
|
|
|
404
431
|
) => {
|
|
405
432
|
const id = context?.item?.id;
|
|
406
433
|
// Item-level renders don't move the viewport, but the first paint does
|
|
407
|
-
// decide which step is on screen.
|
|
408
|
-
|
|
434
|
+
// decide which step is on screen. Schedule rather than measure here (see
|
|
435
|
+
// `scheduleVisibleStepReport`).
|
|
436
|
+
scheduleVisibleStepReport();
|
|
409
437
|
if (id == null) return;
|
|
410
438
|
const sliceStart = sliceStartByItemId.get(id);
|
|
411
439
|
if (sliceStart == null) return;
|
|
412
440
|
remapSnippetLineNumbers(node, sliceStart);
|
|
413
441
|
},
|
|
414
|
-
[sliceStartByItemId,
|
|
442
|
+
[sliceStartByItemId, scheduleVisibleStepReport],
|
|
415
443
|
);
|
|
416
444
|
|
|
417
445
|
// No step numbers in the file header: the sidebar already numbers the steps,
|
|
@@ -546,22 +574,22 @@ export function PierreTrailCodeView({
|
|
|
546
574
|
// PIERRE_BASE_OPTIONS and `disableFileHeader` must follow it.
|
|
547
575
|
...buildPierreOptions(background, {
|
|
548
576
|
background: theme.colors.backgroundSecondary,
|
|
549
|
-
fontSize: `${
|
|
550
|
-
lineHeight:
|
|
551
|
-
padding:
|
|
577
|
+
fontSize: `${headerFontSize}px`,
|
|
578
|
+
lineHeight: String(TRAIL_HEADER_LINE_HEIGHT),
|
|
579
|
+
padding: `${TRAIL_HEADER_PAD_Y}px 8px`,
|
|
552
580
|
}),
|
|
553
581
|
disableFileHeader: false,
|
|
554
582
|
// The library assumes a 44px header (DEFAULT_VIRTUAL_FILE_METRICS); the
|
|
555
|
-
// compact chrome above is
|
|
583
|
+
// compact chrome above is `headerHeight`. CodeView uses this for its
|
|
556
584
|
// virtualization window and sticky offset, so leaving it at 44 drifts.
|
|
557
|
-
itemMetrics: { diffHeaderHeight:
|
|
585
|
+
itemMetrics: { diffHeaderHeight: headerHeight },
|
|
558
586
|
layout: { paddingTop: 0, paddingBottom: 0, gap: 4 },
|
|
559
587
|
onPostRender,
|
|
560
588
|
...(onOpenFile ? { onLineClick } : {}),
|
|
561
589
|
...symbolHandlers,
|
|
562
590
|
...(mode === 'light' || mode === 'dark' ? { themeType: mode } : {}),
|
|
563
591
|
};
|
|
564
|
-
}, [background, theme.colors.backgroundSecondary,
|
|
592
|
+
}, [background, headerFontSize, headerHeight, theme.colors.backgroundSecondary, mode, onPostRender, onOpenFile, onLineClick, symbolHandlers]);
|
|
565
593
|
|
|
566
594
|
useEffect(() => {
|
|
567
595
|
if (load.status !== 'ready' || stepIndex == null) return;
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { ThemeProvider, defaultEditorTheme, useTheme } from '@principal-ade/industry-theme';
|
|
4
|
+
import {
|
|
5
|
+
C4NodeCard,
|
|
6
|
+
nodeStyle,
|
|
7
|
+
nodeSubtitle,
|
|
8
|
+
nodeTag,
|
|
9
|
+
nodeShape,
|
|
10
|
+
nodeSize,
|
|
11
|
+
nodeMissing,
|
|
12
|
+
NODE_W,
|
|
13
|
+
} from '../../../subsystem/C4NodeCard';
|
|
14
|
+
import type { C4Node } from '../../../subsystem/c4';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// A card gallery: every C4 box variant, outside the graph.
|
|
18
|
+
//
|
|
19
|
+
// The graph answers "what does this system look like". This answers "what does
|
|
20
|
+
// a box MEAN" — which encoding maps to which C4 concept, and which gaps the
|
|
21
|
+
// notation still wants filled.
|
|
22
|
+
//
|
|
23
|
+
// Rendering the card directly (not through React Flow) is the point: the node
|
|
24
|
+
// is a plain presentational component, so it can be reviewed and diffed
|
|
25
|
+
// without a layout engine in the way.
|
|
26
|
+
//
|
|
27
|
+
// Every card here is a flattened `C4Node` — exactly what `buildC4Model`
|
|
28
|
+
// produces from an authored element, with the per-kind fields already resolved
|
|
29
|
+
// to the one shape the card draws with.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const meta = {
|
|
33
|
+
title: 'Subsystem/C4Graph/C4NodeGallery',
|
|
34
|
+
parameters: { layout: 'fullscreen' },
|
|
35
|
+
tags: ['autodocs'],
|
|
36
|
+
decorators: [
|
|
37
|
+
(Story) => (
|
|
38
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
39
|
+
<Story />
|
|
40
|
+
</ThemeProvider>
|
|
41
|
+
),
|
|
42
|
+
],
|
|
43
|
+
} satisfies Meta;
|
|
44
|
+
|
|
45
|
+
export default meta;
|
|
46
|
+
type Story = StoryObj<typeof meta>;
|
|
47
|
+
|
|
48
|
+
// --- sample nodes ---------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function node(over: Partial<C4Node> & { id: string; label: string }): C4Node {
|
|
51
|
+
return {
|
|
52
|
+
kind: 'container',
|
|
53
|
+
members: [],
|
|
54
|
+
constructs: [],
|
|
55
|
+
state: 'proposed',
|
|
56
|
+
...over,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const VARIANTS: Array<{ group: string; note: string; node: C4Node }> = [
|
|
61
|
+
// `id` on each variant is its caption key, not a component field.
|
|
62
|
+
|
|
63
|
+
// --- confirmation state: the axis that matters most --------------------
|
|
64
|
+
{
|
|
65
|
+
group: 'Confirmation state',
|
|
66
|
+
note: 'Solid = a person signed off. Dashed = an agent asked and nobody answered. This is the only field that changes how a box is drawn, so it is the only one a confirm UI needs to move.',
|
|
67
|
+
node: node({
|
|
68
|
+
id: 'a1',
|
|
69
|
+
label: 'Subsystem Studio — host',
|
|
70
|
+
members: ['a', 'b', 'c'],
|
|
71
|
+
constructs: ['function', 'store'],
|
|
72
|
+
containerKind: 'application',
|
|
73
|
+
technology: 'Bun + Electrobun',
|
|
74
|
+
description: 'Runs the audit pipeline, owns the model store, serves the RPC surface.',
|
|
75
|
+
state: 'accepted',
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
group: '',
|
|
80
|
+
note: '',
|
|
81
|
+
node: node({
|
|
82
|
+
id: 'a2',
|
|
83
|
+
label: 'subsystems-react (proposed container?)',
|
|
84
|
+
members: ['x', 'y'],
|
|
85
|
+
constructs: ['function', 'interface'],
|
|
86
|
+
containerKind: 'application',
|
|
87
|
+
technology: 'React',
|
|
88
|
+
state: 'proposed',
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
group: '',
|
|
93
|
+
note: '',
|
|
94
|
+
node: node({
|
|
95
|
+
id: 'a3',
|
|
96
|
+
label: 'Subsystem Modeling CLI',
|
|
97
|
+
members: ['cli-create'],
|
|
98
|
+
constructs: ['function'],
|
|
99
|
+
containerKind: 'application',
|
|
100
|
+
technology: 'Node',
|
|
101
|
+
description: 'Creates and updates models from the terminal.',
|
|
102
|
+
state: 'accepted',
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
group: '',
|
|
107
|
+
note: 'Rejected elements are not drawn at all. This card exists to prove the fallback branch exists, not because a diagram ever shows one.',
|
|
108
|
+
node: node({
|
|
109
|
+
id: 'a4',
|
|
110
|
+
label: 'graphify/extract',
|
|
111
|
+
members: ['extract'],
|
|
112
|
+
constructs: ['function'],
|
|
113
|
+
containerKind: 'application',
|
|
114
|
+
technology: 'Bun',
|
|
115
|
+
state: 'rejected',
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
group: '',
|
|
120
|
+
note: 'A box that consolidated two derived keys says so — the inputs are hidden once merged, and silently losing them would be a lie about provenance.',
|
|
121
|
+
node: node({
|
|
122
|
+
id: 'a5',
|
|
123
|
+
label: 'Subsystem Studio — host',
|
|
124
|
+
derivedFrom: ['subsystems-studio/host', 'principal-studio/host'],
|
|
125
|
+
members: ['a'],
|
|
126
|
+
constructs: ['function'],
|
|
127
|
+
containerKind: 'application',
|
|
128
|
+
technology: 'Bun + Electrobun',
|
|
129
|
+
state: 'accepted',
|
|
130
|
+
}),
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// --- the four kinds ------------------------------------------------------
|
|
134
|
+
{
|
|
135
|
+
group: 'The four kinds',
|
|
136
|
+
note: 'C4 has a closed vocabulary: software system, container, component, code. Outside the boundary it adds people. There is no "library" kind — c4model says a module typically is not an element at all, so a library is a review comment, not a fifth box.',
|
|
137
|
+
node: node({
|
|
138
|
+
id: 'b1',
|
|
139
|
+
label: 'Subsystem model store',
|
|
140
|
+
members: ['s1'],
|
|
141
|
+
constructs: ['store'],
|
|
142
|
+
containerKind: 'data-store',
|
|
143
|
+
technology: 'JSON files on disk',
|
|
144
|
+
description: 'Every stored model plus its index.',
|
|
145
|
+
state: 'accepted',
|
|
146
|
+
}),
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
group: '',
|
|
150
|
+
note: '',
|
|
151
|
+
node: node({
|
|
152
|
+
id: 'b2',
|
|
153
|
+
label: 'Audit correction queue',
|
|
154
|
+
members: ['s2'],
|
|
155
|
+
constructs: ['store'],
|
|
156
|
+
containerKind: 'queue',
|
|
157
|
+
technology: 'agent session transport',
|
|
158
|
+
description: 'Proposals awaiting a human decision.',
|
|
159
|
+
state: 'accepted',
|
|
160
|
+
}),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
group: '',
|
|
164
|
+
note: 'A component is a real square, not a smaller rectangle. Border weight drops to 1px as a second signal, so the level survives grayscale.',
|
|
165
|
+
node: node({
|
|
166
|
+
id: 'b3',
|
|
167
|
+
label: 'Boundary layout builder',
|
|
168
|
+
kind: 'component',
|
|
169
|
+
parentId: 'container:shared-ui',
|
|
170
|
+
members: ['h1', 'h2'],
|
|
171
|
+
constructs: ['function'],
|
|
172
|
+
technology: 'React + ELK',
|
|
173
|
+
description: 'Turns claims into nested compound frames.',
|
|
174
|
+
state: 'accepted',
|
|
175
|
+
}),
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
group: '',
|
|
179
|
+
note: 'An external system keeps the rectangle but goes dashed — it is outside the boundary, which is a different statement from "inside, and unconfirmed".',
|
|
180
|
+
node: node({
|
|
181
|
+
id: 'b4',
|
|
182
|
+
label: 'OpenCode v2 service',
|
|
183
|
+
kind: 'external-system',
|
|
184
|
+
members: ['oc2'],
|
|
185
|
+
constructs: ['external'],
|
|
186
|
+
technology: 'HTTP',
|
|
187
|
+
description: 'Runs maintain agent sessions.',
|
|
188
|
+
state: 'accepted',
|
|
189
|
+
}),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
group: '',
|
|
193
|
+
note: '',
|
|
194
|
+
node: node({
|
|
195
|
+
id: 'b5',
|
|
196
|
+
label: 'Maintain operator',
|
|
197
|
+
kind: 'person',
|
|
198
|
+
members: ['a'],
|
|
199
|
+
constructs: ['custom_entity'],
|
|
200
|
+
description: 'Reviews and accepts correction proposals.',
|
|
201
|
+
state: 'proposed',
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// --- the gaps C4 keeps asking about ------------------------------------
|
|
206
|
+
{
|
|
207
|
+
group: 'Notation gaps',
|
|
208
|
+
note: 'C4 requires technology and description on every container. A box that never got either says so on its face rather than looking finished — and `technology` is required by the type, so the only way to have the gap is an empty string.',
|
|
209
|
+
node: node({
|
|
210
|
+
id: 'c1',
|
|
211
|
+
label: 'In-memory renderer state',
|
|
212
|
+
members: ['w1', 'w2', 'w3'],
|
|
213
|
+
constructs: ['store'],
|
|
214
|
+
containerKind: 'application',
|
|
215
|
+
technology: '',
|
|
216
|
+
state: 'accepted',
|
|
217
|
+
}),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
group: '',
|
|
221
|
+
note: '',
|
|
222
|
+
node: node({
|
|
223
|
+
id: 'c2',
|
|
224
|
+
label: 'Subsystem Studio — renderer',
|
|
225
|
+
members: ['r1', 'r2'],
|
|
226
|
+
constructs: ['function'],
|
|
227
|
+
containerKind: 'application',
|
|
228
|
+
technology: 'React 19',
|
|
229
|
+
state: 'accepted',
|
|
230
|
+
}),
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
// --- the pair that was confusing, side by side ------------------------
|
|
234
|
+
{
|
|
235
|
+
group: 'Container vs component',
|
|
236
|
+
note: 'The case that started this. Same border colour and same state, previously identical. Now a wide rectangle says "container" and a square says "component", so the level is readable before the text is.',
|
|
237
|
+
node: node({
|
|
238
|
+
id: 'e1',
|
|
239
|
+
label: 'Subsystem Studio — host',
|
|
240
|
+
members: ['a', 'b', 'c'],
|
|
241
|
+
constructs: ['function', 'store'],
|
|
242
|
+
containerKind: 'application',
|
|
243
|
+
technology: 'Bun + Electrobun',
|
|
244
|
+
description: 'Runs the audit pipeline.',
|
|
245
|
+
state: 'accepted',
|
|
246
|
+
}),
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
group: '',
|
|
250
|
+
note: '',
|
|
251
|
+
node: node({
|
|
252
|
+
id: 'e2',
|
|
253
|
+
label: 'Studio Host',
|
|
254
|
+
kind: 'component',
|
|
255
|
+
parentId: 'container:studio-host',
|
|
256
|
+
members: ['h1'],
|
|
257
|
+
constructs: ['function'],
|
|
258
|
+
technology: 'Bun',
|
|
259
|
+
description: 'Resolves the model root and registers repos.',
|
|
260
|
+
state: 'accepted',
|
|
261
|
+
component: {
|
|
262
|
+
alias: 'h1',
|
|
263
|
+
name: 'StudioHost',
|
|
264
|
+
construct: 'function',
|
|
265
|
+
file: 'packages/subsystems-studio/src/bun/index.ts',
|
|
266
|
+
purl: 'pkg:x#packages/subsystems-studio/src/bun/index.ts',
|
|
267
|
+
},
|
|
268
|
+
}),
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
function Section({ title, note }: { title?: string; note?: string }) {
|
|
273
|
+
if (!title) return null;
|
|
274
|
+
return (
|
|
275
|
+
<div style={{ marginBottom: 10, maxWidth: 720 }}>
|
|
276
|
+
<h2 style={{ margin: '0 0 3px', fontSize: 15, color: '#e8e8ea' }}>{title}</h2>
|
|
277
|
+
{note ? <p style={{ margin: 0, fontSize: 12, lineHeight: 1.45, color: '#9a9aa0' }}>{note}</p> : null}
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function Row({ children }: { children: React.ReactNode }) {
|
|
283
|
+
return (
|
|
284
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 14, alignItems: 'flex-start', marginBottom: 6 }}>
|
|
285
|
+
{children}
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Label under a card, so a reviewer can name the case they are looking at. */
|
|
291
|
+
function Case({ caption, children }: { caption: string; children: React.ReactNode }) {
|
|
292
|
+
return (
|
|
293
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 5, alignItems: 'flex-start' }}>
|
|
294
|
+
{children}
|
|
295
|
+
{/* Caption spans the container box so every card's caption starts at the
|
|
296
|
+
same x — otherwise the narrower square column looks misaligned. */}
|
|
297
|
+
<span
|
|
298
|
+
style={{
|
|
299
|
+
fontFamily: 'monospace',
|
|
300
|
+
fontSize: 10,
|
|
301
|
+
lineHeight: 1.4,
|
|
302
|
+
color: '#6a6a70',
|
|
303
|
+
width: NODE_W,
|
|
304
|
+
}}
|
|
305
|
+
>
|
|
306
|
+
{caption}
|
|
307
|
+
</span>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const KINDS = ['container', 'component', 'external-system', 'person'] as const;
|
|
313
|
+
|
|
314
|
+
function Gallery() {
|
|
315
|
+
const { theme } = useTheme();
|
|
316
|
+
const muted = theme.colors.textMuted ?? '#9a9aa0';
|
|
317
|
+
|
|
318
|
+
const legend: Array<[string, string]> = [
|
|
319
|
+
['accepted', nodeStyle(node({ id: 'x', label: '', state: 'accepted' }), theme).color],
|
|
320
|
+
['proposed', nodeStyle(node({ id: 'x', label: '', state: 'proposed' }), theme).color],
|
|
321
|
+
['rejected', nodeStyle(node({ id: 'x', label: '', state: 'rejected' }), theme).color],
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
let currentGroup = '';
|
|
325
|
+
return (
|
|
326
|
+
<div style={{ padding: 28, background: theme.colors.background, minHeight: '100vh' }}>
|
|
327
|
+
<h1 style={{ margin: '0 0 4px', fontSize: 22, color: theme.colors.text }}>C4 node gallery</h1>
|
|
328
|
+
<p style={{ margin: '0 0 22px', fontSize: 13, color: muted, maxWidth: 720, lineHeight: 1.5 }}>
|
|
329
|
+
Every box the graph can draw, rendered on its own. The graph answers “what does this system
|
|
330
|
+
look like”; this answers “what does a box mean”. No layout engine, no edges — just the
|
|
331
|
+
encoding.
|
|
332
|
+
</p>
|
|
333
|
+
|
|
334
|
+
<div style={{ fontFamily: 'monospace', fontSize: 11, marginBottom: 26, lineHeight: 1.9 }}>
|
|
335
|
+
<div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }}>
|
|
336
|
+
<span style={{ color: '#e8e8ea' }}>KIND — shape and border weight</span>
|
|
337
|
+
</div>
|
|
338
|
+
<div style={{ display: 'flex', gap: 18, flexWrap: 'wrap', marginTop: 4 }}>
|
|
339
|
+
{KINDS.map((kind) => {
|
|
340
|
+
const s = nodeShape(node({ id: 'x', label: '', kind }));
|
|
341
|
+
const size = nodeSize(node({ id: 'x', label: '', kind }));
|
|
342
|
+
const color = nodeStyle(node({ id: 'x', label: '', kind }), theme).color;
|
|
343
|
+
const weight = kind === 'component' ? 1 : 2;
|
|
344
|
+
// Drawn at the real aspect ratio — the whole signal is square vs
|
|
345
|
+
// rectangle, so a legend that flattened both would lie.
|
|
346
|
+
return (
|
|
347
|
+
<span key={kind} style={{ display: 'flex', alignItems: 'center', gap: 6, color: muted }}>
|
|
348
|
+
<span
|
|
349
|
+
style={{
|
|
350
|
+
width: 28,
|
|
351
|
+
height: Math.round((28 * size.height) / size.width),
|
|
352
|
+
borderRadius: Math.min(s.radius, 9),
|
|
353
|
+
border: `${weight} ${s.dash} ${color}`,
|
|
354
|
+
display: 'inline-block',
|
|
355
|
+
}}
|
|
356
|
+
/>
|
|
357
|
+
{kind}
|
|
358
|
+
</span>
|
|
359
|
+
);
|
|
360
|
+
})}
|
|
361
|
+
</div>
|
|
362
|
+
<div style={{ display: 'flex', gap: 20, flexWrap: 'wrap', marginTop: 14 }}>
|
|
363
|
+
<span style={{ color: '#e8e8ea' }}>CONFIRMATION — colour and dash, never the shape</span>
|
|
364
|
+
</div>
|
|
365
|
+
<div style={{ display: 'flex', gap: 18, flexWrap: 'wrap', marginTop: 4 }}>
|
|
366
|
+
{legend.map(([name, color]) => (
|
|
367
|
+
<span key={name} style={{ display: 'flex', alignItems: 'center', gap: 6, color: muted }}>
|
|
368
|
+
<span style={{ width: 22, height: 0, borderTop: `3px solid ${color}`, display: 'inline-block' }} />
|
|
369
|
+
{name}
|
|
370
|
+
</span>
|
|
371
|
+
))}
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<Row>
|
|
376
|
+
{VARIANTS.map((v) => {
|
|
377
|
+
const showHeader = v.group && v.group !== currentGroup;
|
|
378
|
+
if (v.group) currentGroup = v.group;
|
|
379
|
+
return (
|
|
380
|
+
<React.Fragment key={v.node.id}>
|
|
381
|
+
{showHeader && (
|
|
382
|
+
<div style={{ width: '100%' }}>
|
|
383
|
+
<Section title={v.group} note={v.note} />
|
|
384
|
+
</div>
|
|
385
|
+
)}
|
|
386
|
+
<Case
|
|
387
|
+
caption={`${v.node.id} · "${nodeTag(v.node)}" · ${v.node.state} · ${nodeShape(v.node).kind} ${
|
|
388
|
+
nodeSize(v.node).width
|
|
389
|
+
}×${nodeSize(v.node).height} · ${nodeSubtitle(v.node)}${
|
|
390
|
+
nodeMissing(v.node).length ? ` · missing ${nodeMissing(v.node).join('+')}` : ''
|
|
391
|
+
}`}
|
|
392
|
+
>
|
|
393
|
+
<C4NodeCard node={v.node} />
|
|
394
|
+
</Case>
|
|
395
|
+
</React.Fragment>
|
|
396
|
+
);
|
|
397
|
+
})}
|
|
398
|
+
</Row>
|
|
399
|
+
|
|
400
|
+
<div style={{ marginTop: 22, maxWidth: 720 }}>
|
|
401
|
+
<Section
|
|
402
|
+
title="Selected"
|
|
403
|
+
note="A selected node overrides its confirmation styling, so a click target reads differently from the graph around it."
|
|
404
|
+
/>
|
|
405
|
+
<C4NodeCard node={VARIANTS[0]!.node} selected />
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export const All: Story = {
|
|
412
|
+
render: () => <Gallery />,
|
|
413
|
+
};
|