@principal-ai/subsystems-react 0.51.0 → 0.51.2

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 (88) hide show
  1. package/dist/index.d.ts +6 -6
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +6 -4
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/PierreTrailCodeView.d.ts +1 -1
  6. package/dist/pierre/PierreTrailCodeView.d.ts.map +1 -1
  7. package/dist/pierre/PierreTrailCodeView.js +44 -21
  8. package/dist/pierre/PierreTrailCodeView.js.map +1 -1
  9. package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts +493 -0
  10. package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts.map +1 -0
  11. package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js +329 -0
  12. package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js.map +1 -0
  13. package/dist/subsystem/C4Graph.d.ts +2 -2
  14. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  15. package/dist/subsystem/C4Graph.js +25 -16
  16. package/dist/subsystem/C4Graph.js.map +1 -1
  17. package/dist/subsystem/C4NodeCard.d.ts +85 -35
  18. package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
  19. package/dist/subsystem/C4NodeCard.js +112 -83
  20. package/dist/subsystem/C4NodeCard.js.map +1 -1
  21. package/dist/subsystem/FileDrawer.d.ts.map +1 -1
  22. package/dist/subsystem/FileDrawer.js +20 -21
  23. package/dist/subsystem/FileDrawer.js.map +1 -1
  24. package/dist/subsystem/IssueList.d.ts.map +1 -1
  25. package/dist/subsystem/IssueList.js +12 -3
  26. package/dist/subsystem/IssueList.js.map +1 -1
  27. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  28. package/dist/subsystem/SubsystemComponentGraph.js +85 -7
  29. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  30. package/dist/subsystem/c4.d.ts +273 -0
  31. package/dist/subsystem/c4.d.ts.map +1 -0
  32. package/dist/subsystem/c4.js +322 -0
  33. package/dist/subsystem/c4.js.map +1 -0
  34. package/dist/subsystem/c4Evidence.d.ts +143 -0
  35. package/dist/subsystem/c4Evidence.d.ts.map +1 -0
  36. package/dist/subsystem/c4Evidence.js +240 -0
  37. package/dist/subsystem/c4Evidence.js.map +1 -0
  38. package/dist/subsystem/model.d.ts +2 -6
  39. package/dist/subsystem/model.d.ts.map +1 -1
  40. package/dist/subsystem/model.js.map +1 -1
  41. package/package.json +5 -4
  42. package/src/index.ts +26 -28
  43. package/src/pierre/PierreTrailCodeView.tsx +49 -21
  44. package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +171 -80
  45. package/src/stories/Subsystem/C4Graph/ProposedElements.stories.tsx +293 -0
  46. package/src/stories/Subsystem/C4Graph/c4ProposedElements.fixture.ts +366 -0
  47. package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +1 -1
  48. package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +169 -3
  49. package/src/subsystem/C4Graph.tsx +39 -24
  50. package/src/subsystem/C4NodeCard.test.ts +204 -62
  51. package/src/subsystem/C4NodeCard.tsx +146 -101
  52. package/src/subsystem/FileDrawer.tsx +32 -34
  53. package/src/subsystem/IssueList.tsx +13 -2
  54. package/src/subsystem/SubsystemComponentGraph.tsx +98 -7
  55. package/src/subsystem/c4.test.ts +573 -0
  56. package/src/subsystem/c4.ts +559 -0
  57. package/src/subsystem/c4Evidence.test.ts +386 -0
  58. package/src/subsystem/c4Evidence.ts +329 -0
  59. package/src/subsystem/model.ts +2 -6
  60. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.d.ts +0 -95
  61. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.d.ts.map +0 -1
  62. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.js +0 -340
  63. package/dist/stories/Subsystem/C4Graph/c4Associations.fixture.js.map +0 -1
  64. package/dist/stories/Subsystem/C4Graph/c4Fixture.d.ts +0 -7
  65. package/dist/stories/Subsystem/C4Graph/c4Fixture.d.ts.map +0 -1
  66. package/dist/stories/Subsystem/C4Graph/c4Fixture.js +0 -7335
  67. package/dist/stories/Subsystem/C4Graph/c4Fixture.js.map +0 -1
  68. package/dist/stories/Subsystem/C4Graph/generateFixture.d.ts +0 -2
  69. package/dist/stories/Subsystem/C4Graph/generateFixture.d.ts.map +0 -1
  70. package/dist/stories/Subsystem/C4Graph/generateFixture.js +0 -145
  71. package/dist/stories/Subsystem/C4Graph/generateFixture.js.map +0 -1
  72. package/dist/subsystem/c4Associations.d.ts +0 -186
  73. package/dist/subsystem/c4Associations.d.ts.map +0 -1
  74. package/dist/subsystem/c4Associations.js +0 -284
  75. package/dist/subsystem/c4Associations.js.map +0 -1
  76. package/dist/subsystem/toC4.d.ts +0 -214
  77. package/dist/subsystem/toC4.d.ts.map +0 -1
  78. package/dist/subsystem/toC4.js +0 -282
  79. package/dist/subsystem/toC4.js.map +0 -1
  80. package/src/stories/Subsystem/C4Graph/C4Associations.stories.tsx +0 -273
  81. package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +0 -101
  82. package/src/stories/Subsystem/C4Graph/c4Associations.fixture.ts +0 -344
  83. package/src/stories/Subsystem/C4Graph/c4Fixture.ts +0 -7339
  84. package/src/stories/Subsystem/C4Graph/generateFixture.ts +0 -161
  85. package/src/subsystem/c4Associations.test.ts +0 -484
  86. package/src/subsystem/c4Associations.ts +0 -398
  87. package/src/subsystem/toC4.test.ts +0 -114
  88. package/src/subsystem/toC4.ts +0 -478
@@ -1,8 +1,18 @@
1
1
  import React from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react';
3
3
  import { ThemeProvider, defaultEditorTheme, useTheme } from '@principal-ade/industry-theme';
4
- import { C4NodeCard, nodeStyle, nodeSubtitle, nodeTag, nodeMissing, NODE_W } from '../../../subsystem/C4NodeCard';
5
- import type { C4Node, C4ElementType } from '../../../subsystem/toC4';
4
+ import {
5
+ C4NodeCard,
6
+ nodeStyle,
7
+ nodeSubtitle,
8
+ nodeTag,
9
+ nodeTypeLabel,
10
+ nodeShape,
11
+ nodeSize,
12
+ nodeMissing,
13
+ NODE_W,
14
+ } from '../../../subsystem/C4NodeCard';
15
+ import type { C4Node } from '../../../subsystem/c4';
6
16
 
7
17
  // ---------------------------------------------------------------------------
8
18
  // A card gallery: every C4 box variant, outside the graph.
@@ -14,6 +24,10 @@ import type { C4Node, C4ElementType } from '../../../subsystem/toC4';
14
24
  // Rendering the card directly (not through React Flow) is the point: the node
15
25
  // is a plain presentational component, so it can be reviewed and diffed
16
26
  // without a layout engine in the way.
27
+ //
28
+ // Every card here is a flattened `C4Node` — exactly what `buildC4Model`
29
+ // produces from an authored element, with the per-kind fields already resolved
30
+ // to the one shape the card draws with.
17
31
  // ---------------------------------------------------------------------------
18
32
 
19
33
  const meta = {
@@ -39,33 +53,27 @@ function node(over: Partial<C4Node> & { id: string; label: string }): C4Node {
39
53
  kind: 'container',
40
54
  members: [],
41
55
  constructs: [],
42
- isStore: false,
56
+ state: 'proposed',
43
57
  ...over,
44
58
  };
45
59
  }
46
60
 
47
- const CONFIRMED = {
48
- id: 'container:studio-host',
49
- label: 'Studio Host',
50
- type: 'application' as C4ElementType,
51
- technology: 'Bun + Electrobun',
52
- description: 'Runs the audit pipeline, owns the model store, serves the RPC surface.',
53
- state: 'accepted' as const,
54
- };
55
-
56
61
  const VARIANTS: Array<{ group: string; note: string; node: C4Node }> = [
57
62
  // `id` on each variant is its caption key, not a component field.
63
+
58
64
  // --- confirmation state: the axis that matters most --------------------
59
65
  {
60
66
  group: 'Confirmation state',
61
- note: 'The whole point of the association layer. Solid = a person signed off. Dashed = an agent asked, nobody answered. Muted = raw derivation, never reviewed.',
67
+ 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.',
62
68
  node: node({
63
69
  id: 'a1',
64
- label: 'subsystems-studio/host',
65
- key: 'subsystems-studio/host',
70
+ label: 'Subsystem Studio — host',
66
71
  members: ['a', 'b', 'c'],
67
72
  constructs: ['function', 'store'],
68
- decoration: CONFIRMED,
73
+ containerKind: 'application',
74
+ technology: 'Bun + Electrobun',
75
+ description: 'Runs the audit pipeline, owns the model store, serves the RPC surface.',
76
+ state: 'accepted',
69
77
  }),
70
78
  },
71
79
  {
@@ -73,16 +81,12 @@ const VARIANTS: Array<{ group: string; note: string; node: C4Node }> = [
73
81
  note: '',
74
82
  node: node({
75
83
  id: 'a2',
76
- label: 'subsystems-core/types',
77
- key: 'subsystems-core/types',
84
+ label: 'subsystems-react (proposed container?)',
78
85
  members: ['x', 'y'],
79
- constructs: ['interface', 'type_alias'],
80
- decoration: {
81
- id: 'container:subsystems-core/types',
82
- label: 'subsystems-core/types',
83
- type: 'library',
84
- state: 'proposed',
85
- },
86
+ constructs: ['function', 'interface'],
87
+ containerKind: 'application',
88
+ technology: 'React',
89
+ state: 'proposed',
86
90
  }),
87
91
  },
88
92
  {
@@ -90,79 +94,99 @@ const VARIANTS: Array<{ group: string; note: string; node: C4Node }> = [
90
94
  note: '',
91
95
  node: node({
92
96
  id: 'a3',
93
- label: 'principal-studio-cli',
94
- key: 'principal-studio-cli',
97
+ label: 'Subsystem Modeling CLI',
95
98
  members: ['cli-create'],
96
99
  constructs: ['function'],
100
+ containerKind: 'application',
101
+ technology: 'Node',
102
+ description: 'Creates and updates models from the terminal.',
103
+ state: 'accepted',
97
104
  }),
98
105
  },
99
106
  {
100
107
  group: '',
101
- note: '',
108
+ note: 'Rejected elements are not drawn at all. This card exists to prove the fallback branch exists, not because a diagram ever shows one.',
102
109
  node: node({
103
110
  id: 'a4',
104
111
  label: 'graphify/extract',
105
- key: 'graphify/extract',
106
112
  members: ['extract'],
107
113
  constructs: ['function'],
108
- decoration: {
109
- id: 'container:graphify/extract',
110
- label: 'graphify/extract',
111
- type: 'application',
112
- state: 'rejected',
113
- },
114
+ containerKind: 'application',
115
+ technology: 'Bun',
116
+ state: 'rejected',
114
117
  }),
115
118
  },
116
119
  {
117
120
  group: '',
118
- note: '',
119
- node: node({ id: 'a5', label: 'Studio Host', key: 'p1 + p2', sourceKeys: ['p1', 'p2'], members: ['a'], constructs: ['function'], decoration: CONFIRMED }),
121
+ 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.',
122
+ node: node({
123
+ id: 'a5',
124
+ label: 'Subsystem Studio — host',
125
+ derivedFrom: ['subsystems-studio/host', 'principal-studio/host'],
126
+ members: ['a'],
127
+ constructs: ['function'],
128
+ containerKind: 'application',
129
+ technology: 'Bun + Electrobun',
130
+ state: 'accepted',
131
+ }),
120
132
  },
121
133
 
122
- // --- C4 element type ----------------------------------------------------
134
+ // --- the four kinds ------------------------------------------------------
123
135
  {
124
- group: 'C4 element type',
125
- note: 'C4 requires a type on every element. The document has no such field, so this is always a confirmation.',
136
+ group: 'The four kinds',
137
+ 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.',
126
138
  node: node({
127
139
  id: 'b1',
128
140
  label: 'Subsystem model store',
129
141
  members: ['s1'],
130
142
  constructs: ['store'],
131
- decoration: { id: 'container:ds', label: 'Subsystem model store', type: 'data-store', technology: 'JSON files on disk', state: 'accepted', description: 'Every stored model plus its index.' },
143
+ containerKind: 'data-store',
144
+ technology: 'JSON files on disk',
145
+ description: 'Every stored model plus its index.',
146
+ state: 'accepted',
132
147
  }),
133
148
  },
134
149
  {
135
150
  group: '',
136
- note: '',
151
+ note: 'A queue or topic is a data store, not its own kind. C4 says an individual queue is a bucket of messages — a data store — and that the message bus itself is not a container at all. The queue-ness lives in the label and technology.',
137
152
  node: node({
138
153
  id: 'b2',
139
154
  label: 'Audit correction queue',
140
155
  members: ['s2'],
141
156
  constructs: ['store'],
142
- decoration: { id: 'container:q', label: 'Audit correction queue', type: 'queue', technology: 'agent session transport', state: 'accepted', description: 'Proposals awaiting a human decision.' },
157
+ containerKind: 'data-store',
158
+ technology: 'agent session transport',
159
+ description: 'Proposals awaiting a human decision.',
160
+ state: 'accepted',
143
161
  }),
144
162
  },
145
163
  {
146
164
  group: '',
147
- note: '',
165
+ 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.',
148
166
  node: node({
149
167
  id: 'b3',
150
- label: 'subsystems-core',
151
- members: ['i1', 'i2'],
152
- constructs: ['interface', 'function'],
153
- decoration: { id: 'container:lib', label: 'subsystems-core', type: 'library', technology: 'TypeScript', state: 'accepted', description: 'Schema and validation. Not a deployable unit.' },
168
+ label: 'Boundary layout builder',
169
+ kind: 'component',
170
+ parentId: 'container:shared-ui',
171
+ members: ['h1', 'h2'],
172
+ constructs: ['function'],
173
+ technology: 'React + ELK',
174
+ description: 'Turns claims into nested compound frames.',
175
+ state: 'accepted',
154
176
  }),
155
177
  },
156
178
  {
157
179
  group: '',
158
- note: '',
180
+ note: 'An external system keeps the rectangle but goes dashed — it is outside the boundary, which is a different statement from "inside, and unconfirmed".',
159
181
  node: node({
160
182
  id: 'b4',
161
183
  label: 'OpenCode v2 service',
162
- kind: 'external',
184
+ kind: 'external-system',
163
185
  members: ['oc2'],
164
186
  constructs: ['external'],
165
- decoration: { id: 'external:oc2', label: 'OpenCode v2 service', type: 'software-system', technology: 'HTTP', state: 'accepted', description: 'Runs maintain agent sessions.' },
187
+ technology: 'HTTP',
188
+ description: 'Runs maintain agent sessions.',
189
+ state: 'accepted',
166
190
  }),
167
191
  },
168
192
  {
@@ -171,24 +195,26 @@ const VARIANTS: Array<{ group: string; note: string; node: C4Node }> = [
171
195
  node: node({
172
196
  id: 'b5',
173
197
  label: 'Maintain operator',
174
- kind: 'actor',
198
+ kind: 'person',
175
199
  members: ['a'],
176
200
  constructs: ['custom_entity'],
177
- decoration: { id: 'actor:a', label: 'Maintain operator', type: 'person', state: 'proposed', description: 'Reviews and accepts correction proposals.' },
201
+ description: 'Reviews and accepts correction proposals.',
202
+ state: 'proposed',
178
203
  }),
179
204
  },
180
205
 
181
206
  // --- the gaps C4 keeps asking about ------------------------------------
182
207
  {
183
208
  group: 'Notation gaps',
184
- note: 'C4 requires technology and description on every container. A confirmed box that never got either says so on its face rather than looking finished.',
209
+ 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.',
185
210
  node: node({
186
211
  id: 'c1',
187
- label: 'site/web',
188
- key: 'site/web',
212
+ label: 'In-memory renderer state',
189
213
  members: ['w1', 'w2', 'w3'],
190
- constructs: ['function'],
191
- decoration: { id: 'container:site/web', label: 'site/web', type: 'application', state: 'accepted' },
214
+ constructs: ['store'],
215
+ containerKind: 'application',
216
+ technology: '',
217
+ state: 'accepted',
192
218
  }),
193
219
  },
194
220
  {
@@ -196,25 +222,50 @@ const VARIANTS: Array<{ group: string; note: string; node: C4Node }> = [
196
222
  note: '',
197
223
  node: node({
198
224
  id: 'c2',
199
- label: 'subsystems-studio/renderer',
200
- key: 'subsystems-studio/renderer',
225
+ label: 'Subsystem Studio — renderer',
201
226
  members: ['r1', 'r2'],
202
227
  constructs: ['function'],
203
- decoration: { id: 'container:renderer', label: 'subsystems-studio/renderer', type: 'application', technology: 'React', state: 'accepted' },
228
+ containerKind: 'application',
229
+ technology: 'React 19',
230
+ state: 'accepted',
204
231
  }),
205
232
  },
206
233
 
207
- // --- component level ---------------------------------------------------
234
+ // --- the pair that was confusing, side by side ------------------------
235
+ {
236
+ group: 'Container vs component',
237
+ 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.',
238
+ node: node({
239
+ id: 'e1',
240
+ label: 'Subsystem Studio — host',
241
+ members: ['a', 'b', 'c'],
242
+ constructs: ['function', 'store'],
243
+ containerKind: 'application',
244
+ technology: 'Bun + Electrobun',
245
+ description: 'Runs the audit pipeline.',
246
+ state: 'accepted',
247
+ }),
248
+ },
208
249
  {
209
- group: 'Component level',
210
- note: 'Same card, one level down. A component has no type requirement — it inherits from its container.',
250
+ group: '',
251
+ note: '',
211
252
  node: node({
212
- id: 'd1',
213
- label: 'buildBoundaryLayoutGroups',
253
+ id: 'e2',
254
+ label: 'Studio Host',
214
255
  kind: 'component',
215
- members: ['b1'],
256
+ parentId: 'container:studio-host',
257
+ members: ['h1'],
216
258
  constructs: ['function'],
217
- component: { alias: 'b1', name: 'buildBoundaryLayoutGroups', construct: 'function', file: 'src/subsystem/model.ts', purl: 'pkg:x#src/subsystem/model.ts' },
259
+ technology: 'Bun',
260
+ description: 'Resolves the model root and registers repos.',
261
+ state: 'accepted',
262
+ component: {
263
+ alias: 'h1',
264
+ name: 'StudioHost',
265
+ construct: 'function',
266
+ file: 'packages/subsystems-studio/src/bun/index.ts',
267
+ purl: 'pkg:x#packages/subsystems-studio/src/bun/index.ts',
268
+ },
218
269
  }),
219
270
  },
220
271
  ];
@@ -242,6 +293,8 @@ function Case({ caption, children }: { caption: string; children: React.ReactNod
242
293
  return (
243
294
  <div style={{ display: 'flex', flexDirection: 'column', gap: 5, alignItems: 'flex-start' }}>
244
295
  {children}
296
+ {/* Caption spans the container box so every card's caption starts at the
297
+ same x — otherwise the narrower square column looks misaligned. */}
245
298
  <span
246
299
  style={{
247
300
  fontFamily: 'monospace',
@@ -257,14 +310,16 @@ function Case({ caption, children }: { caption: string; children: React.ReactNod
257
310
  );
258
311
  }
259
312
 
313
+ const KINDS = ['container', 'component', 'external-system', 'person'] as const;
314
+
260
315
  function Gallery() {
261
316
  const { theme } = useTheme();
262
317
  const muted = theme.colors.textMuted ?? '#9a9aa0';
263
318
 
264
319
  const legend: Array<[string, string]> = [
265
- ['accepted', nodeStyle({ id: 'x', kind: 'container', label: '', members: [], constructs: [], isStore: false, decoration: CONFIRMED } as C4Node, theme).color],
266
- ['proposed', nodeStyle({ id: 'x', kind: 'container', label: '', members: [], constructs: [], isStore: false, decoration: { id: 'y', state: 'proposed' } } as C4Node, theme).color],
267
- ['unconfirmed', nodeStyle({ id: 'x', kind: 'container', label: '', members: [], constructs: [], isStore: false } as C4Node, theme).color],
320
+ ['accepted', nodeStyle(node({ id: 'x', label: '', state: 'accepted' }), theme).color],
321
+ ['proposed', nodeStyle(node({ id: 'x', label: '', state: 'proposed' }), theme).color],
322
+ ['rejected', nodeStyle(node({ id: 'x', label: '', state: 'rejected' }), theme).color],
268
323
  ];
269
324
 
270
325
  let currentGroup = '';
@@ -277,13 +332,45 @@ function Gallery() {
277
332
  encoding.
278
333
  </p>
279
334
 
280
- <div style={{ display: 'flex', gap: 16, fontFamily: 'monospace', fontSize: 11, marginBottom: 26 }}>
281
- {legend.map(([name, color]) => (
282
- <span key={name} style={{ display: 'flex', alignItems: 'center', gap: 6, color: muted }}>
283
- <span style={{ width: 22, height: 0, borderTop: `3px solid ${color}`, display: 'inline-block' }} />
284
- {name}
285
- </span>
286
- ))}
335
+ <div style={{ fontFamily: 'monospace', fontSize: 11, marginBottom: 26, lineHeight: 1.9 }}>
336
+ <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }}>
337
+ <span style={{ color: '#e8e8ea' }}>KIND — shape, icon, and border weight</span>
338
+ </div>
339
+ <div style={{ display: 'flex', gap: 18, flexWrap: 'wrap', marginTop: 4 }}>
340
+ {KINDS.map((kind) => {
341
+ const s = nodeShape(node({ id: 'x', label: '', kind }));
342
+ const size = nodeSize(node({ id: 'x', label: '', kind }));
343
+ const color = nodeStyle(node({ id: 'x', label: '', kind }), theme).color;
344
+ const weight = kind === 'component' ? 1 : 2;
345
+ // Drawn at the real aspect ratio — the whole signal is square vs
346
+ // rectangle, so a legend that flattened both would lie.
347
+ return (
348
+ <span key={kind} style={{ display: 'flex', alignItems: 'center', gap: 6, color: muted }}>
349
+ <span
350
+ style={{
351
+ width: 28,
352
+ height: Math.round((28 * size.height) / size.width),
353
+ borderRadius: Math.min(s.radius, 9),
354
+ border: `${weight} ${s.dash} ${color}`,
355
+ display: 'inline-block',
356
+ }}
357
+ />
358
+ {kind}
359
+ </span>
360
+ );
361
+ })}
362
+ </div>
363
+ <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap', marginTop: 14 }}>
364
+ <span style={{ color: '#e8e8ea' }}>CONFIRMATION — colour and dash, never the shape</span>
365
+ </div>
366
+ <div style={{ display: 'flex', gap: 18, flexWrap: 'wrap', marginTop: 4 }}>
367
+ {legend.map(([name, color]) => (
368
+ <span key={name} style={{ display: 'flex', alignItems: 'center', gap: 6, color: muted }}>
369
+ <span style={{ width: 22, height: 0, borderTop: `3px solid ${color}`, display: 'inline-block' }} />
370
+ {name}
371
+ </span>
372
+ ))}
373
+ </div>
287
374
  </div>
288
375
 
289
376
  <Row>
@@ -291,14 +378,18 @@ function Gallery() {
291
378
  const showHeader = v.group && v.group !== currentGroup;
292
379
  if (v.group) currentGroup = v.group;
293
380
  return (
294
- <React.Fragment key={v.id ?? v.node.id}>
381
+ <React.Fragment key={v.node.id}>
295
382
  {showHeader && (
296
383
  <div style={{ width: '100%' }}>
297
384
  <Section title={v.group} note={v.note} />
298
385
  </div>
299
386
  )}
300
387
  <Case
301
- caption={`${v.node.id} · tag "${nodeTag(v.node)}" · ${nodeSubtitle(v.node)}${
388
+ caption={`${v.node.id} · "${nodeTag(v.node)}"${
389
+ nodeTypeLabel(v.node) ? ` / ${nodeTypeLabel(v.node)}` : ''
390
+ } · ${v.node.state} · ${nodeShape(v.node).kind} ${
391
+ nodeSize(v.node).width
392
+ }×${nodeSize(v.node).height} · ${nodeSubtitle(v.node)}${
302
393
  nodeMissing(v.node).length ? ` · missing ${nodeMissing(v.node).join('+')}` : ''
303
394
  }`}
304
395
  >