@principal-ai/subsystems-react 0.51.2 → 0.51.3

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 (73) hide show
  1. package/dist/index.d.ts +5 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/subsystem/C4Graph.d.ts +12 -1
  6. package/dist/subsystem/C4Graph.d.ts.map +1 -1
  7. package/dist/subsystem/C4Graph.js +82 -14
  8. package/dist/subsystem/C4Graph.js.map +1 -1
  9. package/dist/subsystem/C4NodeCard.d.ts +22 -22
  10. package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
  11. package/dist/subsystem/C4NodeCard.js +56 -77
  12. package/dist/subsystem/C4NodeCard.js.map +1 -1
  13. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  14. package/dist/subsystem/SubsystemComponentGraph.js +0 -6
  15. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  16. package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
  17. package/dist/subsystem/TrailsPanel.js +8 -4
  18. package/dist/subsystem/TrailsPanel.js.map +1 -1
  19. package/dist/subsystem/c4.d.ts +24 -86
  20. package/dist/subsystem/c4.d.ts.map +1 -1
  21. package/dist/subsystem/c4.js +6 -285
  22. package/dist/subsystem/c4.js.map +1 -1
  23. package/dist/subsystem/c4Evidence.js +1 -1
  24. package/dist/subsystem/c4Evidence.js.map +1 -1
  25. package/dist/subsystem/model.d.ts +8 -0
  26. package/dist/subsystem/model.d.ts.map +1 -1
  27. package/dist/subsystem/model.js.map +1 -1
  28. package/dist/subsystem/techIcons/bun.d.ts +11 -0
  29. package/dist/subsystem/techIcons/bun.d.ts.map +1 -0
  30. package/dist/subsystem/techIcons/bun.js +11 -0
  31. package/dist/subsystem/techIcons/bun.js.map +1 -0
  32. package/dist/subsystem/techIcons/index.d.ts +36 -0
  33. package/dist/subsystem/techIcons/index.d.ts.map +1 -0
  34. package/dist/subsystem/techIcons/index.js +55 -0
  35. package/dist/subsystem/techIcons/index.js.map +1 -0
  36. package/dist/subsystem/techIcons/node.d.ts +12 -0
  37. package/dist/subsystem/techIcons/node.d.ts.map +1 -0
  38. package/dist/subsystem/techIcons/node.js +39 -0
  39. package/dist/subsystem/techIcons/node.js.map +1 -0
  40. package/dist/subsystem/techIcons/react.d.ts +12 -0
  41. package/dist/subsystem/techIcons/react.d.ts.map +1 -0
  42. package/dist/subsystem/techIcons/react.js +19 -0
  43. package/dist/subsystem/techIcons/react.js.map +1 -0
  44. package/dist/utils/elkLayout.d.ts.map +1 -1
  45. package/dist/utils/elkLayout.js +16 -5
  46. package/dist/utils/elkLayout.js.map +1 -1
  47. package/package.json +3 -3
  48. package/src/index.ts +4 -6
  49. package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +212 -0
  50. package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +20 -10
  51. package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +4 -2
  52. package/src/subsystem/C4Graph.tsx +127 -12
  53. package/src/subsystem/C4NodeCard.test.ts +71 -77
  54. package/src/subsystem/C4NodeCard.tsx +65 -95
  55. package/src/subsystem/SubsystemComponentGraph.tsx +0 -6
  56. package/src/subsystem/TrailsPanel.tsx +10 -5
  57. package/src/subsystem/c4.ts +23 -357
  58. package/src/subsystem/c4Evidence.test.ts +1 -1
  59. package/src/subsystem/c4Evidence.ts +1 -1
  60. package/src/subsystem/model.ts +8 -0
  61. package/src/subsystem/techIcons/bun.ts +10 -0
  62. package/src/subsystem/techIcons/index.tsx +73 -0
  63. package/src/subsystem/techIcons/node.ts +38 -0
  64. package/src/subsystem/techIcons/react.ts +18 -0
  65. package/src/subsystem/techIcons/techIcons.test.ts +39 -0
  66. package/src/utils/elkLayout.ts +16 -5
  67. package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts +0 -493
  68. package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts.map +0 -1
  69. package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js +0 -329
  70. package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js.map +0 -1
  71. package/src/stories/Subsystem/C4Graph/ProposedElements.stories.tsx +0 -293
  72. package/src/stories/Subsystem/C4Graph/c4ProposedElements.fixture.ts +0 -366
  73. package/src/subsystem/c4.test.ts +0 -573
@@ -7,24 +7,24 @@
7
7
  *
8
8
  * The encoding has one job: make confirmation state legible at a glance. A box
9
9
  * nobody has looked at must not read the same as a box a person signed off.
10
- * Kind says what the box is — shape, size, and a written tag — and everything
11
- * else in C4 notation (technology, description) is surfaced in the order a
12
- * reader wants it.
10
+ * Kind is carried by shape and size — plus an icon for a container's sort,
11
+ * application vs data-store — and C4 notation is surfaced in the order a reader
12
+ * wants it: technology on the top row, description under the label.
13
13
  */
14
14
 
15
15
  import { useTheme } from '@principal-ade/industry-theme';
16
- import { Cloud, Component, Container, UserRound } from 'lucide-react';
16
+ import { AppWindow, Database } from 'lucide-react';
17
17
  import type { LucideIcon } from 'lucide-react';
18
- import { describeConstructBreakdown } from './model';
18
+ import { TechMark, technologyBrand } from './techIcons';
19
19
  import type { C4Node } from './c4';
20
20
 
21
21
  /**
22
22
  * Card box for a container: a wide rectangle. A container carries four things
23
- * at once (tag, label, type + technology, and the notation gap it still has),
24
- * and a strip would force the last two to truncate.
23
+ * at once (technology row, label, description, and the notation gap it still
24
+ * has), and a strip would force the last two to wrap hard.
25
25
  */
26
- export const NODE_W = 210;
27
- export const NODE_H = 138;
26
+ export const NODE_W = 250;
27
+ export const NODE_H = 150;
28
28
 
29
29
  /**
30
30
  * A component is a proper square — equal sides, and visibly smaller than the
@@ -32,7 +32,7 @@ export const NODE_H = 138;
32
32
  * these are different C4 levels, and it survives grayscale and colour-blind
33
33
  * reading where a border-weight difference alone does not.
34
34
  */
35
- export const COMPONENT_SIZE = 132;
35
+ export const COMPONENT_SIZE = 140;
36
36
 
37
37
  /**
38
38
  * Per-level box size. The graph lays out against these, so a square component
@@ -45,12 +45,10 @@ export function nodeSize(node: C4Node): { width: number; height: number } {
45
45
  }
46
46
 
47
47
  /**
48
- * Fixed slot heights, so rows line up across every card regardless of which
49
- * optional fields are present. Without these a card missing its notation-gap
50
- * marker is two lines shorter and a row of cards reads as ragged.
48
+ * Fixed label slot, so the description below sits at the same y on every card
49
+ * even when a label is one line rather than two.
51
50
  */
52
51
  const LABEL_SLOT_H = 40;
53
- const GAP_SLOT_H = 18;
54
52
 
55
53
  type Theme = ReturnType<typeof useTheme>['theme'];
56
54
 
@@ -91,32 +89,18 @@ export function nodeStyle(node: C4Node, theme: Theme, selected = false): NodeSty
91
89
  // Dashed — an agent asked; a human has not answered.
92
90
  return { color: theme.colors.warning ?? '#e8a33a', dash: 'dashed', width: weight };
93
91
  case 'rejected':
94
- // Should never be drawn (buildC4Model drops it); belt-and-braces.
92
+ // Should never be drawn (a rejected element is not projected); belt-and-braces.
95
93
  return { color: muted, dash: 'dotted', width: 1 };
96
94
  }
97
95
  }
98
96
 
99
- function baseName(path: string): string {
100
- const parts = path.split('/').filter(Boolean);
101
- return parts[parts.length - 1] ?? path;
102
- }
103
-
104
97
  /**
105
- * The kind + technology line under the label.
106
- *
107
- * The container kind moved up to the icon row (see `nodeTypeLabel`), so it is
108
- * not repeated here. Technology is the remaining confirmed attribute; without
109
- * one we fall back to what the document told us.
98
+ * The line under the label: the element's one-line responsibility, which C4
99
+ * asks every element to carry. No fallback — when nothing is stated, the
100
+ * notation-gap row says so rather than the card inventing a summary.
110
101
  */
111
102
  export function nodeSubtitle(node: C4Node): string {
112
- if (node.technology) return node.technology;
113
-
114
- // No stated technology: fall back to what the document told us.
115
- if (node.kind === 'component') {
116
- return `${node.component?.construct ?? 'code'}${node.component?.file ? ` · ${baseName(node.component.file)}` : ''}`;
117
- }
118
- const count = node.members.length;
119
- return `${describeConstructBreakdown(node.constructs)}${count > 0 ? ` · ${count} component${count === 1 ? '' : 's'}` : ''}`;
103
+ return node.description ?? '';
120
104
  }
121
105
 
122
106
  /**
@@ -130,31 +114,26 @@ export function nodeTag(node: C4Node): string {
130
114
  }
131
115
 
132
116
  /**
133
- * The glyph for the C4 kind, shown in place of a word. The kind is already
134
- * carried by the box shape (see `nodeShape`); the icon lets the top row spend
135
- * its one piece of text on the container's kind instead of repeating
136
- * "container".
117
+ * The icon on the top row: a container's sort, **application vs data-store**.
118
+ *
119
+ * The C4 kind itself does not need a glyph — the box shape already carries it
120
+ * (square vs rectangle vs pill), and the row's one text slot is worth more
121
+ * spent on the technology. Only containers have a C4-recognised split, so the
122
+ * other three kinds return nothing.
137
123
  */
138
- export function nodeKindIcon(node: C4Node): LucideIcon {
139
- switch (node.kind) {
140
- case 'container':
141
- return Container;
142
- case 'component':
143
- return Component;
144
- case 'external-system':
145
- return Cloud;
146
- case 'person':
147
- return UserRound;
148
- }
124
+ export function nodeKindIcon(node: C4Node): LucideIcon | undefined {
125
+ if (node.kind !== 'container') return undefined;
126
+ return node.containerKind === 'data-store' ? Database : AppWindow;
149
127
  }
150
128
 
151
129
  /**
152
- * The word beside the kind icon: what sort of container this is — C4's two,
153
- * `application` or `data-store`. The other three kinds have no such subtype,
154
- * so they show nothing; the icon and shape are the whole label.
130
+ * The text on the top row: the technology the box is built with (Bun, React,
131
+ * Postgres…). Falls back to the container kind / C4 kind when no technology is
132
+ * stated, so the row is never blank.
155
133
  */
156
- export function nodeTypeLabel(node: C4Node): string | undefined {
157
- return node.kind === 'container' ? node.containerKind : undefined;
134
+ export function nodeTopLabel(node: C4Node): string {
135
+ if (node.technology) return node.technology;
136
+ return node.kind === 'container' ? (node.containerKind ?? 'container') : node.kind;
158
137
  }
159
138
 
160
139
  /** How a box is drawn. Shape carries the C4 kind, independent of colour. */
@@ -226,11 +205,15 @@ export function C4NodeCard({ node, selected = false, handles, onClick }: C4NodeC
226
205
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
227
206
  const style = nodeStyle(node, theme, selected);
228
207
  const shape = nodeShape(node);
229
- const missing = nodeMissing(node);
230
208
  const Icon = nodeKindIcon(node);
231
- const typeLabel = nodeTypeLabel(node);
209
+ const brand = technologyBrand(node.technology);
210
+ const topLabel = nodeTopLabel(node);
211
+ const subtitle = nodeSubtitle(node);
232
212
 
233
213
  const size = nodeSize(node);
214
+ // A pill (a person) has no straight left edge, so left-aligned text reads as
215
+ // spilling out of the oval. Centre the text and inset it from the curve.
216
+ const centered = shape.kind === 'pill';
234
217
 
235
218
  return (
236
219
  <div
@@ -246,7 +229,7 @@ export function C4NodeCard({ node, selected = false, handles, onClick }: C4NodeC
246
229
  // height on every card. In a graph grid that makes the row unscannable.
247
230
  justifyContent: 'flex-start',
248
231
  gap: 3,
249
- padding: '10px 12px',
232
+ padding: centered ? '10px 30px' : '10px 12px',
250
233
  borderRadius: shape.radius,
251
234
  background: theme.colors.backgroundSecondary ?? theme.colors.background,
252
235
  border: `${style.width} ${style.dash} ${style.color}`,
@@ -255,30 +238,26 @@ export function C4NodeCard({ node, selected = false, handles, onClick }: C4NodeC
255
238
  fontFamily: theme.fonts.body,
256
239
  }}
257
240
  >
258
- {/* The kind is carried by the icon and the box shape; the one word here
259
- is the container's kind. Confirmation state is carried by the border
260
- (see `nodeStyle`), not by a word. */}
241
+ {/* Left: the brand mark when we hold the official one, then the
242
+ technology. Right: a container's C4 sort, application vs data-store.
243
+ The kind itself stays on the shape, confirmation on the border (see
244
+ `nodeShape`, `nodeStyle`). */}
261
245
  <span
262
246
  style={{
263
247
  display: 'flex',
264
248
  alignItems: 'center',
249
+ justifyContent: centered ? 'center' : 'space-between',
265
250
  gap: 6,
266
251
  fontFamily: theme.fonts.monospace,
267
252
  fontSize: theme.fontSizes[0],
268
- color: style.color,
253
+ color: muted,
269
254
  letterSpacing: 0.5,
270
255
  textTransform: 'uppercase',
271
256
  minWidth: 0,
272
257
  }}
273
258
  >
274
- <span
275
- title={nodeTag(node)}
276
- aria-label={nodeTag(node)}
277
- style={{ display: 'inline-flex', flexShrink: 0 }}
278
- >
279
- <Icon size={13} strokeWidth={2.25} />
280
- </span>
281
- {typeLabel && (
259
+ <span style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
260
+ {brand && <TechMark brand={brand} />}
282
261
  <span
283
262
  style={{
284
263
  fontWeight: 600,
@@ -288,7 +267,16 @@ export function C4NodeCard({ node, selected = false, handles, onClick }: C4NodeC
288
267
  minWidth: 0,
289
268
  }}
290
269
  >
291
- {typeLabel}
270
+ {topLabel}
271
+ </span>
272
+ </span>
273
+ {Icon && (
274
+ <span
275
+ title={node.containerKind ?? node.kind}
276
+ aria-label={node.containerKind ?? node.kind}
277
+ style={{ display: 'inline-flex', flexShrink: 0, color: style.color }}
278
+ >
279
+ <Icon size={13} strokeWidth={2.25} />
292
280
  </span>
293
281
  )}
294
282
  </span>
@@ -307,43 +295,25 @@ export function C4NodeCard({ node, selected = false, handles, onClick }: C4NodeC
307
295
  WebkitLineClamp: 2,
308
296
  WebkitBoxOrient: 'vertical',
309
297
  overflow: 'hidden',
298
+ textAlign: centered ? 'center' : 'left',
310
299
  }}
311
300
  title={node.label}
312
301
  >
313
302
  {node.label}
314
303
  </span>
315
- <span
316
- style={{
317
- fontFamily: theme.fonts.monospace,
318
- fontSize: theme.fontSizes[0],
319
- color: muted,
320
- whiteSpace: 'nowrap',
321
- overflow: 'hidden',
322
- textOverflow: 'ellipsis',
323
- }}
324
- title={node.description ?? node.members.join(', ')}
325
- >
326
- {nodeSubtitle(node)}
327
- </span>
328
- {/* Reserved row: without it, a card with no notation gap is two lines
329
- shorter and the row of cards reads as ragged. */}
330
- <span style={{ height: GAP_SLOT_H }} aria-hidden="true" />
331
- {missing.length > 0 && (
304
+ {subtitle && (
332
305
  <span
333
- title={`C4 requires a ${missing.join(' and a ')} on every container`}
334
306
  style={{
335
307
  fontFamily: theme.fonts.monospace,
336
308
  fontSize: theme.fontSizes[0],
337
- color: theme.colors.warning ?? '#e8a33a',
338
- lineHeight: 1.3,
339
- height: GAP_SLOT_H,
340
- display: '-webkit-box',
341
- WebkitLineClamp: 2,
342
- WebkitBoxOrient: 'vertical',
343
- overflow: 'hidden',
309
+ color: muted,
310
+ lineHeight: 1.35,
311
+ whiteSpace: 'normal',
312
+ overflowWrap: 'anywhere',
313
+ textAlign: centered ? 'center' : 'left',
344
314
  }}
345
315
  >
346
- needs {missing.join(' + ')}
316
+ {subtitle}
347
317
  </span>
348
318
  )}
349
319
  {handles}
@@ -1190,18 +1190,15 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1190
1190
  const stepIssueBadgeByEdgeId = useMemo(() => {
1191
1191
  const map = new Map<string, { kind: 'step_unconfirmed' | 'step_stale'; severity: 'info' | 'error'; count: number }>();
1192
1192
  if (!issues?.length || !trails?.length) {
1193
- console.log('[stepIssueBadge] early return: issues=', issues?.length, 'trails=', trails?.length);
1194
1193
  return map;
1195
1194
  }
1196
1195
  for (const issue of issues) {
1197
1196
  if (issue.kind !== 'step_unconfirmed' && issue.kind !== 'step_stale') continue;
1198
1197
  const stepInfo = issueStep(issue);
1199
- console.log('[stepIssueBadge] issue', issue.id, 'kind=', issue.kind, 'stepInfo=', stepInfo);
1200
1198
  if (!stepInfo) continue;
1201
1199
  const step = stepInfo.trail.steps[stepInfo.stepIndex];
1202
1200
  if (!step) continue;
1203
1201
  const edgeId = trailStepGraphEdgeId(step);
1204
- console.log('[stepIssueBadge] -> edgeId=', edgeId);
1205
1202
  const prev = map.get(edgeId);
1206
1203
  // step_stale is worse than step_unconfirmed
1207
1204
  const isWorse = issue.kind === 'step_stale' && prev?.kind !== 'step_stale';
@@ -1211,7 +1208,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
1211
1208
  count: (prev?.count ?? 0) + 1,
1212
1209
  });
1213
1210
  }
1214
- console.log('[stepIssueBadge] final map size=', map.size);
1215
1211
  return map;
1216
1212
  }, [issues, trails, issueStep]);
1217
1213
 
@@ -2499,7 +2495,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2499
2495
  pathLength?: number;
2500
2496
  } | undefined;
2501
2497
  const stepIssue = stepIssueBadgeByEdgeId.get(e.id);
2502
- console.log('[edgeLabels] edge', e.id, 'stepIssue=', stepIssue);
2503
2498
  return {
2504
2499
  id: e.id,
2505
2500
  mechanism: d?.mechanism ?? 'uses',
@@ -2512,7 +2507,6 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
2512
2507
  stepIssue,
2513
2508
  };
2514
2509
  });
2515
- console.log('[edgeLabels] total labels=', labels.length, 'with stepIssue=', labels.filter(l => l.stepIssue).length);
2516
2510
  return labels;
2517
2511
  }, [dispEdges, selectedFlowStepNos, stepIssueBadgeByEdgeId]);
2518
2512
 
@@ -20,6 +20,7 @@ import {
20
20
  reorderTrails,
21
21
  trailStepGraphEdgeId,
22
22
  type SubsystemTrail,
23
+ type SubsystemTrailStep,
23
24
  } from './model';
24
25
  import { buildStepBrief } from './trailBrief';
25
26
 
@@ -90,10 +91,14 @@ function TrailFlow({
90
91
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
91
92
  const hoverBg = theme.colors.background;
92
93
  const wholeFlowActive = active !== null && active.stepIndex === null;
93
- // A step is "proposed" when either endpoint is a proposed component.
94
- const stepProposed = (from: string, to: string) =>
95
- proposedAliases != null && (proposedAliases.has(from) || proposedAliases.has(to));
96
- const touchesProposed = trail.steps.some((s) => stepProposed(s.from, s.to));
94
+ // A step is "proposed" when it says so itself OR either endpoint component
95
+ // is proposed — the step flag covers planned seams whose endpoints already
96
+ // exist as real components.
97
+ const stepProposed = (step: SubsystemTrailStep) =>
98
+ step.proposed === true ||
99
+ (proposedAliases != null &&
100
+ (proposedAliases.has(step.from) || proposedAliases.has(step.to)));
101
+ const touchesProposed = trail.steps.some((s) => stepProposed(s));
97
102
  const [headerHover, setHeaderHover] = useState(false);
98
103
  const [collapseHover, setCollapseHover] = useState(false);
99
104
  const [playHover, setPlayHover] = useState(false);
@@ -366,7 +371,7 @@ function TrailFlow({
366
371
  >
367
372
  {trail.steps.map((step, i) => {
368
373
  const stepActive = active !== null && active.stepIndex === i;
369
- const proposed = stepProposed(step.from, step.to);
374
+ const proposed = stepProposed(step);
370
375
  const revealed =
371
376
  stepActive || hoveredStep === i || focusedStep === i || copiedStep === i;
372
377
  const copied = copiedStep === i;