@principal-ai/subsystems-react 0.51.1 → 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.
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/subsystem/C4Graph.d.ts +12 -1
- package/dist/subsystem/C4Graph.d.ts.map +1 -1
- package/dist/subsystem/C4Graph.js +82 -14
- package/dist/subsystem/C4Graph.js.map +1 -1
- package/dist/subsystem/C4NodeCard.d.ts +29 -14
- package/dist/subsystem/C4NodeCard.d.ts.map +1 -1
- package/dist/subsystem/C4NodeCard.js +71 -57
- package/dist/subsystem/C4NodeCard.js.map +1 -1
- package/dist/subsystem/IssueList.d.ts.map +1 -1
- package/dist/subsystem/IssueList.js +11 -2
- package/dist/subsystem/IssueList.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +66 -3
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/TrailsPanel.d.ts.map +1 -1
- package/dist/subsystem/TrailsPanel.js +8 -4
- package/dist/subsystem/TrailsPanel.js.map +1 -1
- package/dist/subsystem/c4.d.ts +31 -92
- package/dist/subsystem/c4.d.ts.map +1 -1
- package/dist/subsystem/c4.js +6 -285
- package/dist/subsystem/c4.js.map +1 -1
- package/dist/subsystem/c4Evidence.d.ts +1 -1
- package/dist/subsystem/c4Evidence.js +2 -2
- package/dist/subsystem/c4Evidence.js.map +1 -1
- package/dist/subsystem/model.d.ts +8 -0
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/techIcons/bun.d.ts +11 -0
- package/dist/subsystem/techIcons/bun.d.ts.map +1 -0
- package/dist/subsystem/techIcons/bun.js +11 -0
- package/dist/subsystem/techIcons/bun.js.map +1 -0
- package/dist/subsystem/techIcons/index.d.ts +36 -0
- package/dist/subsystem/techIcons/index.d.ts.map +1 -0
- package/dist/subsystem/techIcons/index.js +55 -0
- package/dist/subsystem/techIcons/index.js.map +1 -0
- package/dist/subsystem/techIcons/node.d.ts +12 -0
- package/dist/subsystem/techIcons/node.d.ts.map +1 -0
- package/dist/subsystem/techIcons/node.js +39 -0
- package/dist/subsystem/techIcons/node.js.map +1 -0
- package/dist/subsystem/techIcons/react.d.ts +12 -0
- package/dist/subsystem/techIcons/react.d.ts.map +1 -0
- package/dist/subsystem/techIcons/react.js +19 -0
- package/dist/subsystem/techIcons/react.js.map +1 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +16 -5
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +4 -6
- package/src/stories/Subsystem/C4Graph/C4Container.stories.tsx +212 -0
- package/src/stories/Subsystem/C4Graph/C4NodeGallery.stories.tsx +21 -8
- package/src/stories/Subsystem/ComponentGraph/IssueOverlay.stories.tsx +4 -2
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +168 -2
- package/src/subsystem/C4Graph.tsx +127 -12
- package/src/subsystem/C4NodeCard.test.ts +86 -55
- package/src/subsystem/C4NodeCard.tsx +92 -71
- package/src/subsystem/IssueList.tsx +12 -1
- package/src/subsystem/SubsystemComponentGraph.tsx +79 -2
- package/src/subsystem/TrailsPanel.tsx +10 -5
- package/src/subsystem/c4.ts +30 -363
- package/src/subsystem/c4Evidence.test.ts +5 -5
- package/src/subsystem/c4Evidence.ts +2 -2
- package/src/subsystem/model.ts +8 -0
- package/src/subsystem/techIcons/bun.ts +10 -0
- package/src/subsystem/techIcons/index.tsx +73 -0
- package/src/subsystem/techIcons/node.ts +38 -0
- package/src/subsystem/techIcons/react.ts +18 -0
- package/src/subsystem/techIcons/techIcons.test.ts +39 -0
- package/src/utils/elkLayout.ts +16 -5
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts +0 -493
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.d.ts.map +0 -1
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js +0 -329
- package/dist/stories/Subsystem/C4Graph/c4ProposedElements.fixture.js.map +0 -1
- package/src/stories/Subsystem/C4Graph/ProposedElements.stories.tsx +0 -293
- package/src/stories/Subsystem/C4Graph/c4ProposedElements.fixture.ts +0 -366
- package/src/subsystem/c4.test.ts +0 -573
|
@@ -7,22 +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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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 {
|
|
16
|
+
import { AppWindow, Database } from 'lucide-react';
|
|
17
|
+
import type { LucideIcon } from 'lucide-react';
|
|
18
|
+
import { TechMark, technologyBrand } from './techIcons';
|
|
17
19
|
import type { C4Node } from './c4';
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Card box for a container: a wide rectangle. A container carries four things
|
|
21
|
-
* at once (
|
|
22
|
-
* and a strip would force the last two to
|
|
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.
|
|
23
25
|
*/
|
|
24
|
-
export const NODE_W =
|
|
25
|
-
export const NODE_H =
|
|
26
|
+
export const NODE_W = 250;
|
|
27
|
+
export const NODE_H = 150;
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* A component is a proper square — equal sides, and visibly smaller than the
|
|
@@ -30,7 +32,7 @@ export const NODE_H = 138;
|
|
|
30
32
|
* these are different C4 levels, and it survives grayscale and colour-blind
|
|
31
33
|
* reading where a border-weight difference alone does not.
|
|
32
34
|
*/
|
|
33
|
-
export const COMPONENT_SIZE =
|
|
35
|
+
export const COMPONENT_SIZE = 140;
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* Per-level box size. The graph lays out against these, so a square component
|
|
@@ -43,12 +45,10 @@ export function nodeSize(node: C4Node): { width: number; height: number } {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
|
-
* Fixed slot
|
|
47
|
-
*
|
|
48
|
-
* 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.
|
|
49
50
|
*/
|
|
50
51
|
const LABEL_SLOT_H = 40;
|
|
51
|
-
const GAP_SLOT_H = 18;
|
|
52
52
|
|
|
53
53
|
type Theme = ReturnType<typeof useTheme>['theme'];
|
|
54
54
|
|
|
@@ -89,32 +89,18 @@ export function nodeStyle(node: C4Node, theme: Theme, selected = false): NodeSty
|
|
|
89
89
|
// Dashed — an agent asked; a human has not answered.
|
|
90
90
|
return { color: theme.colors.warning ?? '#e8a33a', dash: 'dashed', width: weight };
|
|
91
91
|
case 'rejected':
|
|
92
|
-
// Should never be drawn (
|
|
92
|
+
// Should never be drawn (a rejected element is not projected); belt-and-braces.
|
|
93
93
|
return { color: muted, dash: 'dotted', width: 1 };
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function baseName(path: string): string {
|
|
98
|
-
const parts = path.split('/').filter(Boolean);
|
|
99
|
-
return parts[parts.length - 1] ?? path;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
97
|
/**
|
|
103
|
-
* The
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* C4 has no kind for the other three, so they show technology or nothing.
|
|
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.
|
|
107
101
|
*/
|
|
108
102
|
export function nodeSubtitle(node: C4Node): string {
|
|
109
|
-
|
|
110
|
-
if (parts.length > 0) return parts.join(' · ');
|
|
111
|
-
|
|
112
|
-
// No confirmed attributes: fall back to what the document told us.
|
|
113
|
-
if (node.kind === 'component') {
|
|
114
|
-
return `${node.component?.construct ?? 'code'}${node.component?.file ? ` · ${baseName(node.component.file)}` : ''}`;
|
|
115
|
-
}
|
|
116
|
-
const count = node.members.length;
|
|
117
|
-
return `${describeConstructBreakdown(node.constructs)}${count > 0 ? ` · ${count} component${count === 1 ? '' : 's'}` : ''}`;
|
|
103
|
+
return node.description ?? '';
|
|
118
104
|
}
|
|
119
105
|
|
|
120
106
|
/**
|
|
@@ -127,6 +113,29 @@ export function nodeTag(node: C4Node): string {
|
|
|
127
113
|
return node.kind;
|
|
128
114
|
}
|
|
129
115
|
|
|
116
|
+
/**
|
|
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.
|
|
123
|
+
*/
|
|
124
|
+
export function nodeKindIcon(node: C4Node): LucideIcon | undefined {
|
|
125
|
+
if (node.kind !== 'container') return undefined;
|
|
126
|
+
return node.containerKind === 'data-store' ? Database : AppWindow;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
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.
|
|
133
|
+
*/
|
|
134
|
+
export function nodeTopLabel(node: C4Node): string {
|
|
135
|
+
if (node.technology) return node.technology;
|
|
136
|
+
return node.kind === 'container' ? (node.containerKind ?? 'container') : node.kind;
|
|
137
|
+
}
|
|
138
|
+
|
|
130
139
|
/** How a box is drawn. Shape carries the C4 kind, independent of colour. */
|
|
131
140
|
export type C4ShapeKind = 'rect' | 'square' | 'pill';
|
|
132
141
|
|
|
@@ -150,8 +159,8 @@ export interface NodeShape {
|
|
|
150
159
|
* An external system is separated from a container by radius alone, which is
|
|
151
160
|
* a weak cue. That is deliberate rather than an oversight: the border dash is
|
|
152
161
|
* the only channel that could say "outside our control" loudly, and it is
|
|
153
|
-
* already spent on proposed-vs-accepted. The kind
|
|
154
|
-
*
|
|
162
|
+
* already spent on proposed-vs-accepted. The kind icon says it, and the frame
|
|
163
|
+
* position says it structurally.
|
|
155
164
|
*/
|
|
156
165
|
export function nodeShape(node: C4Node): NodeShape {
|
|
157
166
|
switch (node.kind) {
|
|
@@ -196,9 +205,15 @@ export function C4NodeCard({ node, selected = false, handles, onClick }: C4NodeC
|
|
|
196
205
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
197
206
|
const style = nodeStyle(node, theme, selected);
|
|
198
207
|
const shape = nodeShape(node);
|
|
199
|
-
const
|
|
208
|
+
const Icon = nodeKindIcon(node);
|
|
209
|
+
const brand = technologyBrand(node.technology);
|
|
210
|
+
const topLabel = nodeTopLabel(node);
|
|
211
|
+
const subtitle = nodeSubtitle(node);
|
|
200
212
|
|
|
201
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';
|
|
202
217
|
|
|
203
218
|
return (
|
|
204
219
|
<div
|
|
@@ -214,7 +229,7 @@ export function C4NodeCard({ node, selected = false, handles, onClick }: C4NodeC
|
|
|
214
229
|
// height on every card. In a graph grid that makes the row unscannable.
|
|
215
230
|
justifyContent: 'flex-start',
|
|
216
231
|
gap: 3,
|
|
217
|
-
padding: '10px 12px',
|
|
232
|
+
padding: centered ? '10px 30px' : '10px 12px',
|
|
218
233
|
borderRadius: shape.radius,
|
|
219
234
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
220
235
|
border: `${style.width} ${style.dash} ${style.color}`,
|
|
@@ -223,23 +238,47 @@ export function C4NodeCard({ node, selected = false, handles, onClick }: C4NodeC
|
|
|
223
238
|
fontFamily: theme.fonts.body,
|
|
224
239
|
}}
|
|
225
240
|
>
|
|
226
|
-
{/*
|
|
227
|
-
|
|
228
|
-
|
|
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`). */}
|
|
229
245
|
<span
|
|
230
246
|
style={{
|
|
247
|
+
display: 'flex',
|
|
248
|
+
alignItems: 'center',
|
|
249
|
+
justifyContent: centered ? 'center' : 'space-between',
|
|
250
|
+
gap: 6,
|
|
231
251
|
fontFamily: theme.fonts.monospace,
|
|
232
252
|
fontSize: theme.fontSizes[0],
|
|
233
|
-
color:
|
|
234
|
-
fontWeight: 600,
|
|
253
|
+
color: muted,
|
|
235
254
|
letterSpacing: 0.5,
|
|
236
255
|
textTransform: 'uppercase',
|
|
237
|
-
|
|
238
|
-
overflow: 'hidden',
|
|
239
|
-
textOverflow: 'ellipsis',
|
|
256
|
+
minWidth: 0,
|
|
240
257
|
}}
|
|
241
258
|
>
|
|
242
|
-
{
|
|
259
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0 }}>
|
|
260
|
+
{brand && <TechMark brand={brand} />}
|
|
261
|
+
<span
|
|
262
|
+
style={{
|
|
263
|
+
fontWeight: 600,
|
|
264
|
+
whiteSpace: 'nowrap',
|
|
265
|
+
overflow: 'hidden',
|
|
266
|
+
textOverflow: 'ellipsis',
|
|
267
|
+
minWidth: 0,
|
|
268
|
+
}}
|
|
269
|
+
>
|
|
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} />
|
|
280
|
+
</span>
|
|
281
|
+
)}
|
|
243
282
|
</span>
|
|
244
283
|
{/* Two lines rather than an ellipsis: a truncated label hides the very
|
|
245
284
|
thing that distinguishes two containers from each other. The fixed
|
|
@@ -256,43 +295,25 @@ export function C4NodeCard({ node, selected = false, handles, onClick }: C4NodeC
|
|
|
256
295
|
WebkitLineClamp: 2,
|
|
257
296
|
WebkitBoxOrient: 'vertical',
|
|
258
297
|
overflow: 'hidden',
|
|
298
|
+
textAlign: centered ? 'center' : 'left',
|
|
259
299
|
}}
|
|
260
300
|
title={node.label}
|
|
261
301
|
>
|
|
262
302
|
{node.label}
|
|
263
303
|
</span>
|
|
264
|
-
|
|
265
|
-
style={{
|
|
266
|
-
fontFamily: theme.fonts.monospace,
|
|
267
|
-
fontSize: theme.fontSizes[0],
|
|
268
|
-
color: muted,
|
|
269
|
-
whiteSpace: 'nowrap',
|
|
270
|
-
overflow: 'hidden',
|
|
271
|
-
textOverflow: 'ellipsis',
|
|
272
|
-
}}
|
|
273
|
-
title={node.description ?? node.members.join(', ')}
|
|
274
|
-
>
|
|
275
|
-
{nodeSubtitle(node)}
|
|
276
|
-
</span>
|
|
277
|
-
{/* Reserved row: without it, a card with no notation gap is two lines
|
|
278
|
-
shorter and the row of cards reads as ragged. */}
|
|
279
|
-
<span style={{ height: GAP_SLOT_H }} aria-hidden="true" />
|
|
280
|
-
{missing.length > 0 && (
|
|
304
|
+
{subtitle && (
|
|
281
305
|
<span
|
|
282
|
-
title={`C4 requires a ${missing.join(' and a ')} on every container`}
|
|
283
306
|
style={{
|
|
284
307
|
fontFamily: theme.fonts.monospace,
|
|
285
308
|
fontSize: theme.fontSizes[0],
|
|
286
|
-
color:
|
|
287
|
-
lineHeight: 1.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
WebkitBoxOrient: 'vertical',
|
|
292
|
-
overflow: 'hidden',
|
|
309
|
+
color: muted,
|
|
310
|
+
lineHeight: 1.35,
|
|
311
|
+
whiteSpace: 'normal',
|
|
312
|
+
overflowWrap: 'anywhere',
|
|
313
|
+
textAlign: centered ? 'center' : 'left',
|
|
293
314
|
}}
|
|
294
315
|
>
|
|
295
|
-
|
|
316
|
+
{subtitle}
|
|
296
317
|
</span>
|
|
297
318
|
)}
|
|
298
319
|
{handles}
|
|
@@ -17,10 +17,12 @@ import {
|
|
|
17
17
|
ChevronDown,
|
|
18
18
|
ChevronRight,
|
|
19
19
|
CircleCheck,
|
|
20
|
+
CircleHelp,
|
|
20
21
|
Component,
|
|
21
22
|
FileX,
|
|
22
23
|
FolderGit2,
|
|
23
24
|
Footprints,
|
|
25
|
+
History,
|
|
24
26
|
MapPin,
|
|
25
27
|
Network,
|
|
26
28
|
Route,
|
|
@@ -104,6 +106,8 @@ const KIND_CATEGORY: Record<string, SubsystemIssueCategory> = {
|
|
|
104
106
|
boundary_module_file_mismatch: 'static-topology',
|
|
105
107
|
boundary_process_nest_disagree: 'dynamic-topology',
|
|
106
108
|
trail: 'trail',
|
|
109
|
+
step_unconfirmed: 'trail',
|
|
110
|
+
step_stale: 'trail',
|
|
107
111
|
};
|
|
108
112
|
|
|
109
113
|
/**
|
|
@@ -164,6 +168,9 @@ export const ISSUE_KIND_ICON: Record<string, LucideIcon> = {
|
|
|
164
168
|
// One module, more than one claimed process parent — a containment that can't
|
|
165
169
|
// be drawn, which is why the region sits at the root instead of nested.
|
|
166
170
|
boundary_process_nest_disagree: Split,
|
|
171
|
+
// Trail step verification: unconfirmed = no augmentation yet, stale = code changed
|
|
172
|
+
step_unconfirmed: CircleHelp,
|
|
173
|
+
step_stale: History,
|
|
167
174
|
};
|
|
168
175
|
|
|
169
176
|
/**
|
|
@@ -244,6 +251,8 @@ const KIND_LABEL: Record<string, string> = {
|
|
|
244
251
|
boundary_module_file_mismatch: 'Declaration file outside module',
|
|
245
252
|
boundary_process_nest_disagree: 'Module spans multiple process contexts',
|
|
246
253
|
trail: 'Step issue',
|
|
254
|
+
step_unconfirmed: 'Call site not verified',
|
|
255
|
+
step_stale: 'Call site changed',
|
|
247
256
|
};
|
|
248
257
|
|
|
249
258
|
/** Label for an issue's kind. Explicit `kindLabel` wins; unknown kinds humanize. */
|
|
@@ -275,8 +284,10 @@ const KIND_ORDER: Record<string, number> = {
|
|
|
275
284
|
boundary_module_file_mismatch: 23,
|
|
276
285
|
// Runtime topology — process containment
|
|
277
286
|
boundary_process_nest_disagree: 30,
|
|
278
|
-
// Trail
|
|
287
|
+
// Trail — mechanical issues first, then step verification
|
|
279
288
|
trail: 40,
|
|
289
|
+
step_unconfirmed: 41,
|
|
290
|
+
step_stale: 42,
|
|
280
291
|
};
|
|
281
292
|
|
|
282
293
|
const KIND_ORDER_FALLBACK = 100;
|
|
@@ -1184,6 +1184,33 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
1184
1184
|
return map;
|
|
1185
1185
|
}, [issues]);
|
|
1186
1186
|
|
|
1187
|
+
// Per-edge step verification badge: maps edge IDs to step verification status.
|
|
1188
|
+
// Keyed by the React Flow edge id (from → mechanism → to). An edge can have
|
|
1189
|
+
// multiple steps with issues; we show the worst status (stale > unconfirmed).
|
|
1190
|
+
const stepIssueBadgeByEdgeId = useMemo(() => {
|
|
1191
|
+
const map = new Map<string, { kind: 'step_unconfirmed' | 'step_stale'; severity: 'info' | 'error'; count: number }>();
|
|
1192
|
+
if (!issues?.length || !trails?.length) {
|
|
1193
|
+
return map;
|
|
1194
|
+
}
|
|
1195
|
+
for (const issue of issues) {
|
|
1196
|
+
if (issue.kind !== 'step_unconfirmed' && issue.kind !== 'step_stale') continue;
|
|
1197
|
+
const stepInfo = issueStep(issue);
|
|
1198
|
+
if (!stepInfo) continue;
|
|
1199
|
+
const step = stepInfo.trail.steps[stepInfo.stepIndex];
|
|
1200
|
+
if (!step) continue;
|
|
1201
|
+
const edgeId = trailStepGraphEdgeId(step);
|
|
1202
|
+
const prev = map.get(edgeId);
|
|
1203
|
+
// step_stale is worse than step_unconfirmed
|
|
1204
|
+
const isWorse = issue.kind === 'step_stale' && prev?.kind !== 'step_stale';
|
|
1205
|
+
map.set(edgeId, {
|
|
1206
|
+
kind: isWorse || !prev ? issue.kind as 'step_unconfirmed' | 'step_stale' : prev.kind,
|
|
1207
|
+
severity: prev?.severity === 'error' || issue.severity === 'error' ? 'error' : 'info',
|
|
1208
|
+
count: (prev?.count ?? 0) + 1,
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
return map;
|
|
1212
|
+
}, [issues, trails, issueStep]);
|
|
1213
|
+
|
|
1187
1214
|
// Expanded verification layers in the diagnostics list. When any layer with
|
|
1188
1215
|
// findings is expanded, the canvas dims everything that layer does not
|
|
1189
1216
|
// implicate (see `issueFocus`). The list publishes this through
|
|
@@ -2452,7 +2479,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
2452
2479
|
// doesn't intercept pointer events). Uses ELK-computed label midpoints from
|
|
2453
2480
|
// the actual edge path (not node-center approximations).
|
|
2454
2481
|
const edgeLabels = useMemo(() => {
|
|
2455
|
-
|
|
2482
|
+
const labels = dispEdges
|
|
2456
2483
|
.filter((e) => {
|
|
2457
2484
|
if (e.hidden) return false;
|
|
2458
2485
|
const d = e?.data as { dimmed?: boolean } | undefined;
|
|
@@ -2467,6 +2494,7 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
2467
2494
|
labelY?: number;
|
|
2468
2495
|
pathLength?: number;
|
|
2469
2496
|
} | undefined;
|
|
2497
|
+
const stepIssue = stepIssueBadgeByEdgeId.get(e.id);
|
|
2470
2498
|
return {
|
|
2471
2499
|
id: e.id,
|
|
2472
2500
|
mechanism: d?.mechanism ?? 'uses',
|
|
@@ -2476,9 +2504,11 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
2476
2504
|
midY: d?.labelY ?? 0,
|
|
2477
2505
|
pathLength: d?.pathLength ?? 0,
|
|
2478
2506
|
stepNos: selectedFlowStepNos?.get(e.id),
|
|
2507
|
+
stepIssue,
|
|
2479
2508
|
};
|
|
2480
2509
|
});
|
|
2481
|
-
|
|
2510
|
+
return labels;
|
|
2511
|
+
}, [dispEdges, selectedFlowStepNos, stepIssueBadgeByEdgeId]);
|
|
2482
2512
|
|
|
2483
2513
|
// Unique source files across components → sidebar file trees.
|
|
2484
2514
|
const treeFilePaths = treeFiles;
|
|
@@ -2958,6 +2988,53 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
2958
2988
|
>
|
|
2959
2989
|
{text}
|
|
2960
2990
|
</span>
|
|
2991
|
+
{lbl.stepIssue && (() => {
|
|
2992
|
+
const StepIssueIcon = ISSUE_KIND_ICON[lbl.stepIssue.kind];
|
|
2993
|
+
// Match IssueList: error severity = red, info severity = amber
|
|
2994
|
+
const issueColor = lbl.stepIssue.severity === 'error'
|
|
2995
|
+
? (theme.colors.error ?? '#e5534b')
|
|
2996
|
+
: (theme.colors.warning ?? '#d4a017');
|
|
2997
|
+
const badgeBg = theme.colors.backgroundSecondary ?? theme.colors.background;
|
|
2998
|
+
return (
|
|
2999
|
+
<span
|
|
3000
|
+
aria-hidden
|
|
3001
|
+
title={lbl.stepIssue.kind === 'step_stale'
|
|
3002
|
+
? 'Call site changed since verification'
|
|
3003
|
+
: 'Call site not yet verified'}
|
|
3004
|
+
style={{
|
|
3005
|
+
position: 'absolute',
|
|
3006
|
+
right: -7,
|
|
3007
|
+
bottom: -6,
|
|
3008
|
+
zIndex: 2,
|
|
3009
|
+
display: 'inline-flex',
|
|
3010
|
+
alignItems: 'center',
|
|
3011
|
+
justifyContent: 'center',
|
|
3012
|
+
gap: 3,
|
|
3013
|
+
minWidth: 18,
|
|
3014
|
+
padding: '2px 4px',
|
|
3015
|
+
borderRadius: 4,
|
|
3016
|
+
border: `1.5px solid ${issueColor}`,
|
|
3017
|
+
background: badgeBg,
|
|
3018
|
+
pointerEvents: 'none',
|
|
3019
|
+
}}
|
|
3020
|
+
>
|
|
3021
|
+
{StepIssueIcon && (
|
|
3022
|
+
<StepIssueIcon size={12} color={issueColor} />
|
|
3023
|
+
)}
|
|
3024
|
+
{lbl.stepIssue.count > 1 && (
|
|
3025
|
+
<span style={{
|
|
3026
|
+
color: issueColor,
|
|
3027
|
+
fontFamily: theme.fonts.monospace,
|
|
3028
|
+
fontSize: 10,
|
|
3029
|
+
fontWeight: 700,
|
|
3030
|
+
lineHeight: 1,
|
|
3031
|
+
}}>
|
|
3032
|
+
{lbl.stepIssue.count}
|
|
3033
|
+
</span>
|
|
3034
|
+
)}
|
|
3035
|
+
</span>
|
|
3036
|
+
);
|
|
3037
|
+
})()}
|
|
2961
3038
|
</div>
|
|
2962
3039
|
);
|
|
2963
3040
|
})}
|
|
@@ -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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
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
|
|
374
|
+
const proposed = stepProposed(step);
|
|
370
375
|
const revealed =
|
|
371
376
|
stepActive || hoveredStep === i || focusedStep === i || copiedStep === i;
|
|
372
377
|
const copied = copiedStep === i;
|