@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
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C4NodeCard — the box an element draws as, with nothing else.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `C4Graph.tsx` so the visual encoding can be inspected,
|
|
5
|
+
* reviewed, and unit-tested without standing up a graph. React Flow supplies
|
|
6
|
+
* edges and hit-testing; this supplies the reading surface.
|
|
7
|
+
*
|
|
8
|
+
* The encoding has one job: make confirmation state legible at a glance. A box
|
|
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.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useTheme } from '@principal-ade/industry-theme';
|
|
16
|
+
import { describeConstructBreakdown } from './model';
|
|
17
|
+
import type { C4Node } from './c4';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Card box for a container: a wide rectangle. A container carries four things
|
|
21
|
+
* at once (tag, label, type + technology, and the notation gap it still has),
|
|
22
|
+
* and a strip would force the last two to truncate.
|
|
23
|
+
*/
|
|
24
|
+
export const NODE_W = 210;
|
|
25
|
+
export const NODE_H = 138;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A component is a proper square — equal sides, and visibly smaller than the
|
|
29
|
+
* container it sits inside. Square-vs-rectangle is the clearest signal that
|
|
30
|
+
* these are different C4 levels, and it survives grayscale and colour-blind
|
|
31
|
+
* reading where a border-weight difference alone does not.
|
|
32
|
+
*/
|
|
33
|
+
export const COMPONENT_SIZE = 132;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Per-level box size. The graph lays out against these, so a square component
|
|
37
|
+
* and a rectangular container do not collide at a shared nominal width.
|
|
38
|
+
*/
|
|
39
|
+
export function nodeSize(node: C4Node): { width: number; height: number } {
|
|
40
|
+
return node.kind === 'component'
|
|
41
|
+
? { width: COMPONENT_SIZE, height: COMPONENT_SIZE }
|
|
42
|
+
: { width: NODE_W, height: NODE_H };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fixed slot heights, so rows line up across every card regardless of which
|
|
47
|
+
* optional fields are present. Without these a card missing its notation-gap
|
|
48
|
+
* marker is two lines shorter and a row of cards reads as ragged.
|
|
49
|
+
*/
|
|
50
|
+
const LABEL_SLOT_H = 40;
|
|
51
|
+
const GAP_SLOT_H = 18;
|
|
52
|
+
|
|
53
|
+
type Theme = ReturnType<typeof useTheme>['theme'];
|
|
54
|
+
|
|
55
|
+
export interface NodeStyle {
|
|
56
|
+
color: string;
|
|
57
|
+
dash: 'solid' | 'dashed' | 'dotted';
|
|
58
|
+
width: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Border style for one node: colour, dash and weight, from confirmation state
|
|
63
|
+
* and level.
|
|
64
|
+
*
|
|
65
|
+
* One axis per channel, so nothing is said twice and nothing is contradicted:
|
|
66
|
+
*
|
|
67
|
+
* colour + dash → confirmation state (the primary axis)
|
|
68
|
+
* weight → C4 level, as a grayscale redundancy for the square
|
|
69
|
+
* shape + size → C4 kind (`nodeShape`, `nodeSize`)
|
|
70
|
+
*
|
|
71
|
+
* State owning the dash is deliberate. An earlier build gave the dash to kind
|
|
72
|
+
* and drew proposed boxes solid, quietly contradicting the one thing the
|
|
73
|
+
* confirm UI needs a reader to see. Kind now says itself through shape, and
|
|
74
|
+
* the border says who vouched for it.
|
|
75
|
+
*/
|
|
76
|
+
export function nodeStyle(node: C4Node, theme: Theme, selected = false): NodeStyle {
|
|
77
|
+
const muted = theme.colors.border ?? '#555';
|
|
78
|
+
if (selected) return { color: theme.colors.primary ?? '#5aa', dash: 'solid', width: 3 };
|
|
79
|
+
|
|
80
|
+
// Border weight carries the level, so a component stays a component even
|
|
81
|
+
// in grayscale or for a reader who cannot separate the hues.
|
|
82
|
+
const weight = node.kind === 'component' ? 1 : 2;
|
|
83
|
+
|
|
84
|
+
switch (node.state) {
|
|
85
|
+
case 'accepted':
|
|
86
|
+
// Solid and saturated — part of the architecture now.
|
|
87
|
+
return { color: theme.colors.primary ?? '#4ec9b0', dash: 'solid', width: weight };
|
|
88
|
+
case 'proposed':
|
|
89
|
+
// Dashed — an agent asked; a human has not answered.
|
|
90
|
+
return { color: theme.colors.warning ?? '#e8a33a', dash: 'dashed', width: weight };
|
|
91
|
+
case 'rejected':
|
|
92
|
+
// Should never be drawn (buildC4Model drops it); belt-and-braces.
|
|
93
|
+
return { color: muted, dash: 'dotted', width: 1 };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function baseName(path: string): string {
|
|
98
|
+
const parts = path.split('/').filter(Boolean);
|
|
99
|
+
return parts[parts.length - 1] ?? path;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The kind + technology line under the label.
|
|
104
|
+
*
|
|
105
|
+
* A container shows its `containerKind` (application / data-store / queue);
|
|
106
|
+
* C4 has no kind for the other three, so they show technology or nothing.
|
|
107
|
+
*/
|
|
108
|
+
export function nodeSubtitle(node: C4Node): string {
|
|
109
|
+
const parts = [node.containerKind, node.technology].filter(Boolean);
|
|
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'}` : ''}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* What the box IS — its C4 kind. An accepted container is still a container;
|
|
122
|
+
* collapsing the two is what once made containers and components unreadable.
|
|
123
|
+
* Confirmation state is a separate axis carried by the border (see
|
|
124
|
+
* `nodeStyle`), not by a word on the card.
|
|
125
|
+
*/
|
|
126
|
+
export function nodeTag(node: C4Node): string {
|
|
127
|
+
return node.kind;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** How a box is drawn. Shape carries the C4 kind, independent of colour. */
|
|
131
|
+
export type C4ShapeKind = 'rect' | 'square' | 'pill';
|
|
132
|
+
|
|
133
|
+
export interface NodeShape {
|
|
134
|
+
kind: C4ShapeKind;
|
|
135
|
+
/** Corner radius, for the shapes that use one. */
|
|
136
|
+
radius: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Shape cue for the C4 kind, independent of colour and dash — both of which
|
|
141
|
+
* belong to confirmation state.
|
|
142
|
+
*
|
|
143
|
+
* The convention:
|
|
144
|
+
*
|
|
145
|
+
* container wide rectangle, sharp corners — a runtime boundary
|
|
146
|
+
* component square — a part inside one (see `nodeSize`)
|
|
147
|
+
* external system wide rectangle, rounded — someone else's system
|
|
148
|
+
* person pill — not a box at all
|
|
149
|
+
*
|
|
150
|
+
* An external system is separated from a container by radius alone, which is
|
|
151
|
+
* a weak cue. That is deliberate rather than an oversight: the border dash is
|
|
152
|
+
* the only channel that could say "outside our control" loudly, and it is
|
|
153
|
+
* already spent on proposed-vs-accepted. The kind tag says it in words, and
|
|
154
|
+
* the frame position says it structurally.
|
|
155
|
+
*/
|
|
156
|
+
export function nodeShape(node: C4Node): NodeShape {
|
|
157
|
+
switch (node.kind) {
|
|
158
|
+
case 'person':
|
|
159
|
+
return { kind: 'pill', radius: NODE_H / 2 };
|
|
160
|
+
case 'external-system':
|
|
161
|
+
return { kind: 'rect', radius: 14 };
|
|
162
|
+
case 'component':
|
|
163
|
+
return { kind: 'square', radius: 6 };
|
|
164
|
+
default:
|
|
165
|
+
return { kind: 'rect', radius: 4 };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The gaps the C4 notation still wants on this element, named rather than left
|
|
171
|
+
* to be noticed.
|
|
172
|
+
*
|
|
173
|
+
* Containers are where the notation is strictest: it requires a technology and
|
|
174
|
+
* a description on every one. A component needs a technology too, but a
|
|
175
|
+
* component drawn without a parent already reports `unknown_container`, and
|
|
176
|
+
* that is the question worth asking first.
|
|
177
|
+
*/
|
|
178
|
+
export function nodeMissing(node: C4Node): string[] {
|
|
179
|
+
if (node.kind !== 'container') return [];
|
|
180
|
+
const missing: string[] = [];
|
|
181
|
+
if (!node.technology) missing.push('technology');
|
|
182
|
+
if (!node.description) missing.push('description');
|
|
183
|
+
return missing;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface C4NodeCardProps {
|
|
187
|
+
node: C4Node;
|
|
188
|
+
selected?: boolean;
|
|
189
|
+
/** Draw React Flow's edge handles. Off when rendering a bare gallery. */
|
|
190
|
+
handles?: React.ReactNode;
|
|
191
|
+
onClick?: () => void;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function C4NodeCard({ node, selected = false, handles, onClick }: C4NodeCardProps) {
|
|
195
|
+
const { theme } = useTheme();
|
|
196
|
+
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
197
|
+
const style = nodeStyle(node, theme, selected);
|
|
198
|
+
const shape = nodeShape(node);
|
|
199
|
+
const missing = nodeMissing(node);
|
|
200
|
+
|
|
201
|
+
const size = nodeSize(node);
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div
|
|
205
|
+
onClick={onClick}
|
|
206
|
+
style={{
|
|
207
|
+
width: size.width,
|
|
208
|
+
minHeight: size.height,
|
|
209
|
+
boxSizing: 'border-box',
|
|
210
|
+
display: 'flex',
|
|
211
|
+
flexDirection: 'column',
|
|
212
|
+
// Top-aligned, not centred: the number of lines varies (the notation-gap
|
|
213
|
+
// marker is conditional), and centring pushes the tag to a different
|
|
214
|
+
// height on every card. In a graph grid that makes the row unscannable.
|
|
215
|
+
justifyContent: 'flex-start',
|
|
216
|
+
gap: 3,
|
|
217
|
+
padding: '10px 12px',
|
|
218
|
+
borderRadius: shape.radius,
|
|
219
|
+
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
220
|
+
border: `${style.width} ${style.dash} ${style.color}`,
|
|
221
|
+
boxShadow: '0 1px 4px rgba(0,0,0,0.25)',
|
|
222
|
+
cursor: onClick ? 'pointer' : 'default',
|
|
223
|
+
fontFamily: theme.fonts.body,
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
{/* The kind is what the box IS. Confirmation state is carried by the
|
|
227
|
+
border (see `nodeStyle`), not by a word: the card says what it is,
|
|
228
|
+
and the styling says who vouched for it. */}
|
|
229
|
+
<span
|
|
230
|
+
style={{
|
|
231
|
+
fontFamily: theme.fonts.monospace,
|
|
232
|
+
fontSize: theme.fontSizes[0],
|
|
233
|
+
color: style.color,
|
|
234
|
+
fontWeight: 600,
|
|
235
|
+
letterSpacing: 0.5,
|
|
236
|
+
textTransform: 'uppercase',
|
|
237
|
+
whiteSpace: 'nowrap',
|
|
238
|
+
overflow: 'hidden',
|
|
239
|
+
textOverflow: 'ellipsis',
|
|
240
|
+
}}
|
|
241
|
+
>
|
|
242
|
+
{nodeTag(node)}
|
|
243
|
+
</span>
|
|
244
|
+
{/* Two lines rather than an ellipsis: a truncated label hides the very
|
|
245
|
+
thing that distinguishes two containers from each other. The fixed
|
|
246
|
+
height reserves both lines even for a one-line label, so the subtitle
|
|
247
|
+
below sits at the same y on every card. */}
|
|
248
|
+
<span
|
|
249
|
+
style={{
|
|
250
|
+
fontWeight: 600,
|
|
251
|
+
fontSize: theme.fontSizes[2],
|
|
252
|
+
color: theme.colors.text,
|
|
253
|
+
lineHeight: 1.25,
|
|
254
|
+
height: LABEL_SLOT_H,
|
|
255
|
+
display: '-webkit-box',
|
|
256
|
+
WebkitLineClamp: 2,
|
|
257
|
+
WebkitBoxOrient: 'vertical',
|
|
258
|
+
overflow: 'hidden',
|
|
259
|
+
}}
|
|
260
|
+
title={node.label}
|
|
261
|
+
>
|
|
262
|
+
{node.label}
|
|
263
|
+
</span>
|
|
264
|
+
<span
|
|
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 && (
|
|
281
|
+
<span
|
|
282
|
+
title={`C4 requires a ${missing.join(' and a ')} on every container`}
|
|
283
|
+
style={{
|
|
284
|
+
fontFamily: theme.fonts.monospace,
|
|
285
|
+
fontSize: theme.fontSizes[0],
|
|
286
|
+
color: theme.colors.warning ?? '#e8a33a',
|
|
287
|
+
lineHeight: 1.3,
|
|
288
|
+
height: GAP_SLOT_H,
|
|
289
|
+
display: '-webkit-box',
|
|
290
|
+
WebkitLineClamp: 2,
|
|
291
|
+
WebkitBoxOrient: 'vertical',
|
|
292
|
+
overflow: 'hidden',
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
needs {missing.join(' + ')}
|
|
296
|
+
</span>
|
|
297
|
+
)}
|
|
298
|
+
{handles}
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
@@ -28,6 +28,13 @@ export interface SubsystemDiagnostic {
|
|
|
28
28
|
status: SubsystemDiagnosticStatus;
|
|
29
29
|
/** Count shown beside the icon (e.g. error + warn findings). Hidden when 0/undefined. */
|
|
30
30
|
issueCount?: number;
|
|
31
|
+
/**
|
|
32
|
+
* How many of the open findings carry a one-click deterministic fix (a
|
|
33
|
+
* re-pin, an adoptable signature fill, a unique Graphify file relocate).
|
|
34
|
+
* Badged on the icon so it's visible without expanding the list — the fixes
|
|
35
|
+
* live inside collapsed categories, so the count was previously invisible.
|
|
36
|
+
*/
|
|
37
|
+
fixableCount?: number;
|
|
31
38
|
/** Inputs changed since the report was produced. */
|
|
32
39
|
stale?: boolean;
|
|
33
40
|
/** A verification pass is running right now. */
|
|
@@ -55,7 +62,7 @@ export function diagnosticStatusColor(
|
|
|
55
62
|
/** Human tooltip for the current diagnostic state. */
|
|
56
63
|
export function diagnosticTitle(diagnostic: SubsystemDiagnostic): string {
|
|
57
64
|
if (diagnostic.title) return diagnostic.title;
|
|
58
|
-
const { status, issueCount, stale, busy } = diagnostic;
|
|
65
|
+
const { status, issueCount, fixableCount, stale, busy } = diagnostic;
|
|
59
66
|
let head: string;
|
|
60
67
|
if (status === 'ok') head = 'Diagnostics: no issues';
|
|
61
68
|
else if (status === 'issues')
|
|
@@ -69,9 +76,15 @@ export function diagnosticTitle(diagnostic: SubsystemDiagnostic): string {
|
|
|
69
76
|
? `Diagnostics: ${issueCount} unconfirmed`
|
|
70
77
|
: 'Diagnostics: unconfirmed claims';
|
|
71
78
|
else head = 'Diagnostics: not checked yet';
|
|
79
|
+
// Name the one-click count first when there is work a click can close —
|
|
80
|
+
// that's the actionable part of the list.
|
|
81
|
+
const fixable =
|
|
82
|
+
fixableCount != null && fixableCount > 0
|
|
83
|
+
? `${fixableCount} fixable with one click`
|
|
84
|
+
: '';
|
|
72
85
|
if (busy) return `${head} — checking…`;
|
|
73
|
-
if (stale) return `${head} (stale) — toggle the list`;
|
|
74
|
-
return `${head} — toggle the list`;
|
|
86
|
+
if (stale) return `${head} (stale) — toggle the list${fixable ? `, ${fixable}` : ''}`;
|
|
87
|
+
return `${head} — toggle the list${fixable ? `, ${fixable}` : ''}`;
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
export interface SubsystemDiagnosticToggleProps extends SubsystemDiagnostic {
|
|
@@ -82,6 +95,7 @@ export interface SubsystemDiagnosticToggleProps extends SubsystemDiagnostic {
|
|
|
82
95
|
export function SubsystemDiagnosticToggle({
|
|
83
96
|
status,
|
|
84
97
|
issueCount,
|
|
98
|
+
fixableCount = 0,
|
|
85
99
|
stale = false,
|
|
86
100
|
busy = false,
|
|
87
101
|
active = false,
|
|
@@ -93,8 +107,20 @@ export function SubsystemDiagnosticToggle({
|
|
|
93
107
|
const [hover, setHover] = useState(false);
|
|
94
108
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
95
109
|
const color = diagnosticStatusColor(status, theme.colors, muted);
|
|
96
|
-
const label = diagnosticTitle({
|
|
110
|
+
const label = diagnosticTitle({
|
|
111
|
+
status,
|
|
112
|
+
issueCount,
|
|
113
|
+
fixableCount,
|
|
114
|
+
stale,
|
|
115
|
+
busy,
|
|
116
|
+
active,
|
|
117
|
+
...rest,
|
|
118
|
+
});
|
|
97
119
|
const showCount = issueCount != null && issueCount > 0;
|
|
120
|
+
// Green regardless of status: this marks "a click fixes this", not severity,
|
|
121
|
+
// and green is the same accent the per-finding Apply button uses.
|
|
122
|
+
const showFixable = fixableCount > 0 && !busy;
|
|
123
|
+
const fixableColor = theme.colors.success ?? '#2da44e';
|
|
98
124
|
const background = active
|
|
99
125
|
? hexWithAlpha(color, hover ? 0.24 : 0.16)
|
|
100
126
|
: hover
|
|
@@ -151,6 +177,34 @@ export function SubsystemDiagnosticToggle({
|
|
|
151
177
|
}}
|
|
152
178
|
/>
|
|
153
179
|
)}
|
|
180
|
+
{/* Fixable badge — superscript count on the icon's trailing edge.
|
|
181
|
+
Offset up-and-out from the `stale` dot so both read at once;
|
|
182
|
+
the badge is the call to action, the dot only a freshness note. */}
|
|
183
|
+
{showFixable && (
|
|
184
|
+
<span
|
|
185
|
+
aria-hidden
|
|
186
|
+
style={{
|
|
187
|
+
position: 'absolute',
|
|
188
|
+
top: -5,
|
|
189
|
+
right: -8,
|
|
190
|
+
minWidth: 12,
|
|
191
|
+
height: 12,
|
|
192
|
+
padding: '0 3px',
|
|
193
|
+
borderRadius: 6,
|
|
194
|
+
display: 'inline-flex',
|
|
195
|
+
alignItems: 'center',
|
|
196
|
+
justifyContent: 'center',
|
|
197
|
+
background: fixableColor,
|
|
198
|
+
color: theme.colors.background,
|
|
199
|
+
fontFamily: theme.fonts.monospace,
|
|
200
|
+
fontSize: 9,
|
|
201
|
+
fontWeight: 700,
|
|
202
|
+
lineHeight: 1,
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
{fixableCount > 99 ? '99+' : fixableCount}
|
|
206
|
+
</span>
|
|
207
|
+
)}
|
|
154
208
|
</span>
|
|
155
209
|
{showCount && (
|
|
156
210
|
<span
|
|
@@ -117,21 +117,46 @@ export function FileDrawer({
|
|
|
117
117
|
userSelect: "none",
|
|
118
118
|
}}
|
|
119
119
|
>
|
|
120
|
+
{showProgress && progress != null && (
|
|
121
|
+
<div
|
|
122
|
+
role="img"
|
|
123
|
+
aria-label={`Step ${progress.index + 1} of ${progress.total}`}
|
|
124
|
+
style={{
|
|
125
|
+
display: "flex",
|
|
126
|
+
gap: 2,
|
|
127
|
+
flexShrink: 0,
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{Array.from({ length: progress.total }, (_, i) => (
|
|
131
|
+
<span
|
|
132
|
+
key={i}
|
|
133
|
+
style={{
|
|
134
|
+
flex: 1,
|
|
135
|
+
height: 2,
|
|
136
|
+
borderRadius: 1,
|
|
137
|
+
background:
|
|
138
|
+
i <= progress.index
|
|
139
|
+
? theme.colors.primary
|
|
140
|
+
: theme.colors.border,
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
120
146
|
<div
|
|
121
147
|
style={{
|
|
122
148
|
display: "flex",
|
|
123
149
|
alignItems: "center",
|
|
124
150
|
gap: 8,
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
// Top runs deeper than the bottom so the title/label sits
|
|
152
|
+
// optically centered inside the sticky top edge.
|
|
153
|
+
padding: "10px 10px 8px",
|
|
154
|
+
// The header row is the last chrome row now, so its hairline is
|
|
155
|
+
// the boundary with the code below.
|
|
156
|
+
borderBottom: `1px solid ${theme.colors.border}`,
|
|
131
157
|
}}
|
|
132
158
|
>
|
|
133
159
|
<span
|
|
134
|
-
title={title ?? undefined}
|
|
135
160
|
style={{
|
|
136
161
|
flex: 1,
|
|
137
162
|
minWidth: 0,
|
|
@@ -197,33 +222,6 @@ export function FileDrawer({
|
|
|
197
222
|
<X size={14} />
|
|
198
223
|
</button>
|
|
199
224
|
</div>
|
|
200
|
-
{showProgress && progress != null && (
|
|
201
|
-
<div
|
|
202
|
-
role="img"
|
|
203
|
-
aria-label={`Step ${progress.index + 1} of ${progress.total}`}
|
|
204
|
-
style={{
|
|
205
|
-
display: "flex",
|
|
206
|
-
gap: 2,
|
|
207
|
-
flexShrink: 0,
|
|
208
|
-
padding: "0 10px 6px",
|
|
209
|
-
}}
|
|
210
|
-
>
|
|
211
|
-
{Array.from({ length: progress.total }, (_, i) => (
|
|
212
|
-
<span
|
|
213
|
-
key={i}
|
|
214
|
-
style={{
|
|
215
|
-
flex: 1,
|
|
216
|
-
height: 2,
|
|
217
|
-
borderRadius: 1,
|
|
218
|
-
background:
|
|
219
|
-
i <= progress.index
|
|
220
|
-
? theme.colors.primary
|
|
221
|
-
: theme.colors.border,
|
|
222
|
-
}}
|
|
223
|
-
/>
|
|
224
|
-
))}
|
|
225
|
-
</div>
|
|
226
|
-
)}
|
|
227
225
|
</div>
|
|
228
226
|
<div style={{ flex: 1, minHeight: 0, overflow: "auto" }}>
|
|
229
227
|
{children}
|
|
@@ -233,7 +233,7 @@ export function humanizeIssueKind(kind: string): string {
|
|
|
233
233
|
const KIND_LABEL: Record<string, string> = {
|
|
234
234
|
missing_file: 'Construct declaration file missing',
|
|
235
235
|
symbol_ambiguous: 'Multiple construct symbol matches',
|
|
236
|
-
symbol_unmatched: 'Construct symbol
|
|
236
|
+
symbol_unmatched: 'Construct symbol missing',
|
|
237
237
|
stale_declaration: 'Construct declaration drift',
|
|
238
238
|
construct_mismatch: 'Construct type mismatch',
|
|
239
239
|
construct_unconfirmed: 'Construct type unconfirmed',
|
|
@@ -869,16 +869,24 @@ function Inner({ components, trails, graphifyRelations, orderByLine, initialTrai
|
|
|
869
869
|
// the selected step until it does.
|
|
870
870
|
const [visibleStepIndex, setVisibleStepIndex] = useState<number | null>(null);
|
|
871
871
|
const drawerStepIndex = drawerTarget?.kind === 'trail' ? drawerTarget.stepIndex : null;
|
|
872
|
+
// Reset when the drawer switches trail (or closes) — synchronously, so the bar
|
|
873
|
+
// never paints the previous trail's position. A step change *within* the same
|
|
874
|
+
// trail is deliberately not reset: the code view scrolls to the new step and
|
|
875
|
+
// the viewport tracker carries the fill across that scroll. Resetting on step
|
|
876
|
+
// change flashed — the post-paint effect snapped the fill to the target, then
|
|
877
|
+
// Pierre's smooth scroll re-reported the old viewport, bouncing target → old →
|
|
878
|
+
// target.
|
|
879
|
+
const activeTrailId = focusedTrail?.id ?? null;
|
|
880
|
+
const [visibleTrailId, setVisibleTrailId] = useState(activeTrailId);
|
|
881
|
+
if (activeTrailId !== visibleTrailId) {
|
|
882
|
+
setVisibleTrailId(activeTrailId);
|
|
883
|
+
setVisibleStepIndex(null);
|
|
884
|
+
}
|
|
872
885
|
const drawerProgress = useMemo(() => {
|
|
873
886
|
if (drawerStepIndex == null || !focusedTrail) return null;
|
|
874
887
|
return { index: visibleStepIndex ?? drawerStepIndex, total: focusedTrail.steps.length };
|
|
875
888
|
}, [drawerStepIndex, focusedTrail, visibleStepIndex]);
|
|
876
889
|
|
|
877
|
-
// A new trail or step invalidates whatever the last scroll reported.
|
|
878
|
-
useEffect(() => {
|
|
879
|
-
setVisibleStepIndex(null);
|
|
880
|
-
}, [focusedTrail?.id, drawerStepIndex]);
|
|
881
|
-
|
|
882
890
|
// Refresh selected component when the components list updates (e.g. verify
|
|
883
891
|
// writes back declarationRef).
|
|
884
892
|
useEffect(() => {
|