@principal-ai/subsystems-react 0.24.8 → 0.24.10
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/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +47 -36
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/DiagnosticToggle.d.ts +9 -6
- package/dist/subsystem/DiagnosticToggle.d.ts.map +1 -1
- package/dist/subsystem/DiagnosticToggle.js +16 -9
- package/dist/subsystem/DiagnosticToggle.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +7 -6
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +11 -5
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/package.json +1 -1
- package/src/stories/Subsystem/ComponentGraph/Diagnostics.stories.tsx +5 -5
- package/src/stories/Subsystem/ComponentGraph/Issues.stories.tsx +1 -1
- package/src/stories/Subsystem/ComponentGraph/NodeAnatomy.stories.tsx +508 -0
- package/src/subsystem/ComponentDeclaration.tsx +64 -42
- package/src/subsystem/DiagnosticToggle.tsx +22 -12
- package/src/subsystem/SubsystemComponentGraph.tsx +25 -11
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '@xyflow/react/dist/style.css';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import { ReactFlowProvider, type NodeProps } from '@xyflow/react';
|
|
5
|
+
import { ThemeProvider, defaultEditorTheme, useTheme } from '@principal-ade/industry-theme';
|
|
6
|
+
import { FileX, Search, MapPin, Shapes, Sigma } from 'lucide-react';
|
|
7
|
+
import { SubsystemComponentNode } from '../../../subsystem/nodes';
|
|
8
|
+
import type { GraphifyComponentDetail } from '../../../graphify';
|
|
9
|
+
import type { SubsystemComponent, SubsystemGraphNode } from '../../../subsystem/model';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* NodeAnatomy — a dissection of the component node's visual language.
|
|
13
|
+
*
|
|
14
|
+
* Section 1 lays out every channel the node already spends (border, corners,
|
|
15
|
+
* left badge, right badge cluster, fill) so an added channel can be chosen
|
|
16
|
+
* against it. Section 2 shows the *proposed* diagnostics layer (severity ring +
|
|
17
|
+
* earliest-rung corner chip) composed over real nodes. Nothing here is wired
|
|
18
|
+
* into the production node yet — this is the reference to design against.
|
|
19
|
+
*/
|
|
20
|
+
const meta = {
|
|
21
|
+
title: 'Subsystem/ComponentGraph/NodeAnatomy',
|
|
22
|
+
parameters: { layout: 'fullscreen' },
|
|
23
|
+
tags: ['autodocs'],
|
|
24
|
+
decorators: [
|
|
25
|
+
(Story) => (
|
|
26
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
27
|
+
<ReactFlowProvider>
|
|
28
|
+
<Story />
|
|
29
|
+
</ReactFlowProvider>
|
|
30
|
+
</ThemeProvider>
|
|
31
|
+
),
|
|
32
|
+
],
|
|
33
|
+
} satisfies Meta;
|
|
34
|
+
|
|
35
|
+
export default meta;
|
|
36
|
+
type Story = StoryObj<typeof meta>;
|
|
37
|
+
|
|
38
|
+
const PURL = 'pkg:github/principal-ai/subsystem-modeling';
|
|
39
|
+
const NODE_W = 230;
|
|
40
|
+
const NODE_H = 84;
|
|
41
|
+
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
|
|
42
|
+
|
|
43
|
+
function nodeProps(
|
|
44
|
+
component: SubsystemComponent,
|
|
45
|
+
opts?: { selected?: boolean; width?: number; height?: number },
|
|
46
|
+
): NodeProps<SubsystemGraphNode> {
|
|
47
|
+
return {
|
|
48
|
+
data: { component },
|
|
49
|
+
selected: opts?.selected ?? false,
|
|
50
|
+
width: opts?.width ?? NODE_W,
|
|
51
|
+
height: opts?.height ?? NODE_H,
|
|
52
|
+
} as unknown as NodeProps<SubsystemGraphNode>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Section 1 — the channels the node already spends
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
type Channel = {
|
|
60
|
+
label: string;
|
|
61
|
+
note: string;
|
|
62
|
+
component: SubsystemComponent;
|
|
63
|
+
selected?: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const storeDetail: GraphifyComponentDetail = {
|
|
67
|
+
kind: 'store',
|
|
68
|
+
storage: 'memory',
|
|
69
|
+
} as GraphifyComponentDetail;
|
|
70
|
+
|
|
71
|
+
const CHANNELS: Channel[] = [
|
|
72
|
+
{
|
|
73
|
+
label: 'baseline — function',
|
|
74
|
+
note: 'border color = construct (function-indigo); left badge = construct.',
|
|
75
|
+
component: {
|
|
76
|
+
id: 'fn',
|
|
77
|
+
name: 'auditSubsystemModel',
|
|
78
|
+
construct: 'function',
|
|
79
|
+
file: 'src/bun/verify-subsystem-component.ts',
|
|
80
|
+
purl: PURL,
|
|
81
|
+
symbol: 'auditSubsystemModel',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
label: 'class',
|
|
86
|
+
note: 'construct color changes per kind; nothing else moves.',
|
|
87
|
+
component: {
|
|
88
|
+
id: 'cls',
|
|
89
|
+
name: 'MaintainModelAgent',
|
|
90
|
+
construct: 'class',
|
|
91
|
+
file: 'src/bun/maintain-model.ts',
|
|
92
|
+
purl: PURL,
|
|
93
|
+
symbol: 'MaintainModelAgent',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
label: 'external',
|
|
98
|
+
note: 'outside the system — square corners, no file.',
|
|
99
|
+
component: {
|
|
100
|
+
id: 'ext',
|
|
101
|
+
name: '@opencode-ai/cli',
|
|
102
|
+
construct: 'external',
|
|
103
|
+
file: '',
|
|
104
|
+
purl: 'pkg:npm/@opencode-ai/cli',
|
|
105
|
+
symbol: '',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
label: 'role badge',
|
|
110
|
+
note: 'top-right badge = role.',
|
|
111
|
+
component: {
|
|
112
|
+
id: 'role',
|
|
113
|
+
name: 'handleRequest',
|
|
114
|
+
construct: 'function',
|
|
115
|
+
file: 'src/bun/http-server.ts',
|
|
116
|
+
purl: PURL,
|
|
117
|
+
symbol: 'handleRequest',
|
|
118
|
+
role: 'entry',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
label: 'store + storage badge',
|
|
123
|
+
note: 'store backs a storage badge beside the role badge.',
|
|
124
|
+
component: {
|
|
125
|
+
id: 'store',
|
|
126
|
+
name: 'sessionCache',
|
|
127
|
+
construct: 'store',
|
|
128
|
+
file: 'src/bun/session-cache.ts',
|
|
129
|
+
purl: PURL,
|
|
130
|
+
symbol: 'sessionCache',
|
|
131
|
+
declaration: storeDetail,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
label: 'proposed',
|
|
136
|
+
note: 'dashed goldenrod border + right badge; fileless.',
|
|
137
|
+
component: {
|
|
138
|
+
id: 'prop',
|
|
139
|
+
name: 'OpenCodeV2Lifecycle',
|
|
140
|
+
construct: 'function',
|
|
141
|
+
file: '',
|
|
142
|
+
purl: PURL,
|
|
143
|
+
symbol: 'OpenCodeV2Lifecycle',
|
|
144
|
+
proposed: true,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
label: 'proposed + role',
|
|
149
|
+
note: 'proposed wins the right badge; role stays on the model.',
|
|
150
|
+
component: {
|
|
151
|
+
id: 'prop-role',
|
|
152
|
+
name: 'startMaintainFlow',
|
|
153
|
+
construct: 'function',
|
|
154
|
+
file: '',
|
|
155
|
+
purl: PURL,
|
|
156
|
+
symbol: 'startMaintainFlow',
|
|
157
|
+
proposed: true,
|
|
158
|
+
role: 'service',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
label: 'framework stereotype',
|
|
163
|
+
note: 'left badge prefers "framework · stereotype"; border stays construct.',
|
|
164
|
+
component: {
|
|
165
|
+
id: 'react',
|
|
166
|
+
name: 'SubsystemComponentNode',
|
|
167
|
+
construct: 'function',
|
|
168
|
+
file: 'src/subsystem/nodes.tsx',
|
|
169
|
+
purl: PURL,
|
|
170
|
+
symbol: 'SubsystemComponentNode',
|
|
171
|
+
framework: 'react',
|
|
172
|
+
stereotype: 'component',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
label: 'custom entity',
|
|
177
|
+
note: 'left badge wears the entityKind.',
|
|
178
|
+
component: {
|
|
179
|
+
id: 'entity',
|
|
180
|
+
name: 'WorkRequester',
|
|
181
|
+
construct: 'custom_entity',
|
|
182
|
+
file: '',
|
|
183
|
+
purl: 'external',
|
|
184
|
+
entityKind: 'Person',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
label: 'selected',
|
|
189
|
+
note: 'selection only thickens the border (2 → 4); color stays construct.',
|
|
190
|
+
component: {
|
|
191
|
+
id: 'sel',
|
|
192
|
+
name: 'runMaintainModel',
|
|
193
|
+
construct: 'function',
|
|
194
|
+
file: 'src/bun/maintain-model.ts',
|
|
195
|
+
purl: PURL,
|
|
196
|
+
symbol: 'runMaintainModel',
|
|
197
|
+
},
|
|
198
|
+
selected: true,
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
function ChannelCell({ channel }: { channel: Channel }) {
|
|
203
|
+
const { theme } = useTheme();
|
|
204
|
+
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
205
|
+
return (
|
|
206
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, width: 280 }}>
|
|
207
|
+
<div style={{ fontFamily: MONO, fontSize: 11, color: muted, minHeight: 28 }}>
|
|
208
|
+
{channel.label}
|
|
209
|
+
</div>
|
|
210
|
+
<div style={{ position: 'relative', width: NODE_W, height: NODE_H, margin: '14px 0 16px' }}>
|
|
211
|
+
<SubsystemComponentNode {...nodeProps(channel.component, { selected: channel.selected })} />
|
|
212
|
+
</div>
|
|
213
|
+
<div style={{ fontSize: 10, fontFamily: MONO, color: muted, lineHeight: 1.5 }}>
|
|
214
|
+
{channel.note}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const LEGEND: Array<[string, string]> = [
|
|
221
|
+
['border color', 'construct kind (Pierre palette) — or primary when a file is open (spotlight)'],
|
|
222
|
+
['border style', 'solid normally; dashed goldenrod when `proposed`'],
|
|
223
|
+
['border width', '2px; 4px when selected or file-matched'],
|
|
224
|
+
['corners', '8px rounded; square for `external`'],
|
|
225
|
+
['left badge (top edge)', 'construct / framework stereotype / entityKind'],
|
|
226
|
+
['right badge (top edge)', 'role, or `proposed` (wins); storage sits beside it'],
|
|
227
|
+
['bottom strip (bottom edge)', 'filename — hover only, or while that file is open'],
|
|
228
|
+
['fill', 'backgroundSecondary; opacity 0.18 when dimmed'],
|
|
229
|
+
['OCCUPIED', 'top-left · top-right · bottom-center · border · fill'],
|
|
230
|
+
['FREE', 'an outer ring/halo, and the bottom-left / bottom-right corners'],
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
/** The node's existing channels, one per cell, with a legend. */
|
|
234
|
+
export const Channels: Story = {
|
|
235
|
+
render: () => {
|
|
236
|
+
return (
|
|
237
|
+
<div style={{ padding: 32, minHeight: '100vh', boxSizing: 'border-box' }}>
|
|
238
|
+
<Legend />
|
|
239
|
+
<div
|
|
240
|
+
style={{
|
|
241
|
+
display: 'flex',
|
|
242
|
+
flexWrap: 'wrap',
|
|
243
|
+
gap: '40px 28px',
|
|
244
|
+
alignItems: 'flex-start',
|
|
245
|
+
marginTop: 28,
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
{CHANNELS.map((c) => (
|
|
249
|
+
<ChannelCell key={c.component.id} channel={c} />
|
|
250
|
+
))}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
function Legend() {
|
|
258
|
+
const { theme } = useTheme();
|
|
259
|
+
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
260
|
+
return (
|
|
261
|
+
<div style={{ maxWidth: 720 }}>
|
|
262
|
+
<div
|
|
263
|
+
style={{
|
|
264
|
+
fontSize: 12,
|
|
265
|
+
fontFamily: MONO,
|
|
266
|
+
textTransform: 'uppercase',
|
|
267
|
+
letterSpacing: 0.4,
|
|
268
|
+
color: muted,
|
|
269
|
+
marginBottom: 10,
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
Channel legend
|
|
273
|
+
</div>
|
|
274
|
+
<div style={{ display: 'grid', gridTemplateColumns: '180px 1fr', rowGap: 4, columnGap: 12 }}>
|
|
275
|
+
{LEGEND.map(([k, v]) => (
|
|
276
|
+
<React.Fragment key={k}>
|
|
277
|
+
<span style={{ fontFamily: MONO, fontSize: 11, color: theme.colors.text }}>{k}</span>
|
|
278
|
+
<span style={{ fontSize: 11, color: muted, lineHeight: 1.5 }}>{v}</span>
|
|
279
|
+
</React.Fragment>
|
|
280
|
+
))}
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Section 2 — proposed diagnostics layer: severity ring + earliest-rung chip
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
type Severity = 'error' | 'warn' | 'info';
|
|
291
|
+
type Rung = 'file' | 'symbol' | 'declaration' | 'type' | 'signature';
|
|
292
|
+
|
|
293
|
+
const RUNG_ICON = {
|
|
294
|
+
file: FileX,
|
|
295
|
+
symbol: Search,
|
|
296
|
+
declaration: MapPin,
|
|
297
|
+
type: Shapes,
|
|
298
|
+
signature: Sigma,
|
|
299
|
+
} as const;
|
|
300
|
+
|
|
301
|
+
const RUNG_LABEL: Record<Rung, string> = {
|
|
302
|
+
file: 'file',
|
|
303
|
+
symbol: 'symbol',
|
|
304
|
+
declaration: 'declaration',
|
|
305
|
+
type: 'type',
|
|
306
|
+
signature: 'signature',
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
function IssueOverlayNode({
|
|
310
|
+
component,
|
|
311
|
+
severity,
|
|
312
|
+
rung,
|
|
313
|
+
count = 1,
|
|
314
|
+
}: {
|
|
315
|
+
component: SubsystemComponent;
|
|
316
|
+
severity: Severity;
|
|
317
|
+
rung: Rung;
|
|
318
|
+
count?: number;
|
|
319
|
+
}) {
|
|
320
|
+
const { theme } = useTheme();
|
|
321
|
+
const color =
|
|
322
|
+
severity === 'error'
|
|
323
|
+
? (theme.colors.error ?? '#e5534b')
|
|
324
|
+
: severity === 'warn'
|
|
325
|
+
? (theme.colors.warning ?? '#d4a017')
|
|
326
|
+
: (theme.colors.info ?? '#0893d2');
|
|
327
|
+
// Gaps (info) read hollow/dashed — "couldn't confirm"; errors/warns solid.
|
|
328
|
+
const dashed = severity === 'info';
|
|
329
|
+
const Icon = RUNG_ICON[rung];
|
|
330
|
+
const badgeBg = theme.colors.backgroundSecondary ?? theme.colors.background;
|
|
331
|
+
return (
|
|
332
|
+
<div style={{ position: 'relative', width: NODE_W, height: NODE_H }}>
|
|
333
|
+
<SubsystemComponentNode {...nodeProps(component)} />
|
|
334
|
+
{/* severity ring — additive, outside the construct-colored border */}
|
|
335
|
+
<div
|
|
336
|
+
style={{
|
|
337
|
+
position: 'absolute',
|
|
338
|
+
inset: -5,
|
|
339
|
+
borderRadius: 11,
|
|
340
|
+
border: `2px ${dashed ? 'dashed' : 'solid'} ${color}`,
|
|
341
|
+
pointerEvents: 'none',
|
|
342
|
+
}}
|
|
343
|
+
/>
|
|
344
|
+
{/* earliest-rung corner chip + count */}
|
|
345
|
+
<div
|
|
346
|
+
style={{
|
|
347
|
+
position: 'absolute',
|
|
348
|
+
right: -8,
|
|
349
|
+
bottom: -7,
|
|
350
|
+
display: 'inline-flex',
|
|
351
|
+
alignItems: 'center',
|
|
352
|
+
gap: 3,
|
|
353
|
+
padding: '1px 6px',
|
|
354
|
+
borderRadius: 999,
|
|
355
|
+
border: `1.5px solid ${color}`,
|
|
356
|
+
background: badgeBg,
|
|
357
|
+
color,
|
|
358
|
+
fontFamily: MONO,
|
|
359
|
+
fontSize: 10,
|
|
360
|
+
fontWeight: 600,
|
|
361
|
+
lineHeight: 1.4,
|
|
362
|
+
}}
|
|
363
|
+
>
|
|
364
|
+
<Icon size={11} color={color} />
|
|
365
|
+
{count > 1 ? <span>{count}</span> : null}
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function IssueCell({
|
|
372
|
+
caption,
|
|
373
|
+
clean,
|
|
374
|
+
...node
|
|
375
|
+
}: {
|
|
376
|
+
caption: string;
|
|
377
|
+
component: SubsystemComponent;
|
|
378
|
+
severity: Severity;
|
|
379
|
+
rung: Rung;
|
|
380
|
+
count?: number;
|
|
381
|
+
clean?: boolean;
|
|
382
|
+
}) {
|
|
383
|
+
const { theme } = useTheme();
|
|
384
|
+
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
385
|
+
return (
|
|
386
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, width: 280 }}>
|
|
387
|
+
<div style={{ fontFamily: MONO, fontSize: 11, color: muted, minHeight: 28 }}>{caption}</div>
|
|
388
|
+
<div style={{ position: 'relative', width: NODE_W, height: NODE_H, margin: '16px 0 18px' }}>
|
|
389
|
+
{clean ? (
|
|
390
|
+
<SubsystemComponentNode {...nodeProps(node.component)} />
|
|
391
|
+
) : (
|
|
392
|
+
<IssueOverlayNode
|
|
393
|
+
component={node.component}
|
|
394
|
+
severity={node.severity}
|
|
395
|
+
rung={node.rung}
|
|
396
|
+
count={node.count}
|
|
397
|
+
/>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
<div style={{ fontSize: 10, fontFamily: MONO, color: muted }}>
|
|
401
|
+
{clean
|
|
402
|
+
? 'no issues'
|
|
403
|
+
: `${node.severity} · ${RUNG_LABEL[node.rung]}${
|
|
404
|
+
node.count && node.count > 1 ? ` · ${node.count} issues` : ''
|
|
405
|
+
}`}
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const ISSUE_EXAMPLES: Array<{
|
|
412
|
+
caption: string;
|
|
413
|
+
component: SubsystemComponent;
|
|
414
|
+
severity: Severity;
|
|
415
|
+
rung: Rung;
|
|
416
|
+
count?: number;
|
|
417
|
+
clean?: boolean;
|
|
418
|
+
}> = [
|
|
419
|
+
{
|
|
420
|
+
caption: 'clean node — no ring, no chip',
|
|
421
|
+
component: CHANNELS[0]!.component,
|
|
422
|
+
severity: 'error',
|
|
423
|
+
rung: 'file',
|
|
424
|
+
clean: true,
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
caption: 'error · earliest rung is file (declaration file missing)',
|
|
428
|
+
component: { ...CHANNELS[0]!.component, id: 'e-file' },
|
|
429
|
+
severity: 'error',
|
|
430
|
+
rung: 'file',
|
|
431
|
+
count: 2,
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
caption: 'warn · earliest rung is symbol (multiple symbol matches)',
|
|
435
|
+
component: { ...CHANNELS[1]!.component, id: 'e-sym' },
|
|
436
|
+
severity: 'warn',
|
|
437
|
+
rung: 'symbol',
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
caption: 'info/gap · dashed ring · declaration drift',
|
|
441
|
+
component: { ...CHANNELS[1]!.component, id: 'e-decl' },
|
|
442
|
+
severity: 'info',
|
|
443
|
+
rung: 'declaration',
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
caption: 'error · type mismatch',
|
|
447
|
+
component: { ...CHANNELS[7]!.component, id: 'e-type' },
|
|
448
|
+
severity: 'error',
|
|
449
|
+
rung: 'type',
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
caption: 'error · signature mismatch (deepest rung)',
|
|
453
|
+
component: { ...CHANNELS[0]!.component, id: 'e-sig' },
|
|
454
|
+
severity: 'error',
|
|
455
|
+
rung: 'signature',
|
|
456
|
+
},
|
|
457
|
+
];
|
|
458
|
+
|
|
459
|
+
/** Proposed diagnostics layer over real nodes — ring by severity, chip = earliest rung. */
|
|
460
|
+
export const IssueOverlayProposal: Story = {
|
|
461
|
+
render: () => {
|
|
462
|
+
return (
|
|
463
|
+
<div style={{ padding: 32, minHeight: '100vh', boxSizing: 'border-box' }}>
|
|
464
|
+
<div style={{ maxWidth: 720 }}>
|
|
465
|
+
<div
|
|
466
|
+
style={{
|
|
467
|
+
fontSize: 12,
|
|
468
|
+
fontFamily: MONO,
|
|
469
|
+
textTransform: 'uppercase',
|
|
470
|
+
letterSpacing: 0.4,
|
|
471
|
+
color: '#a0a0a0',
|
|
472
|
+
marginBottom: 10,
|
|
473
|
+
}}
|
|
474
|
+
>
|
|
475
|
+
Proposed · not wired yet
|
|
476
|
+
</div>
|
|
477
|
+
<ul style={{ margin: 0, paddingLeft: 18, fontSize: 12, lineHeight: 1.7, color: '#b0b0b0' }}>
|
|
478
|
+
<li>
|
|
479
|
+
<b>Ring</b> (outside the construct border): <b>solid</b> = error/warn (contradicts
|
|
480
|
+
source), <b>dashed</b> = info/gap (couldn't confirm). Color = severity.
|
|
481
|
+
</li>
|
|
482
|
+
<li>
|
|
483
|
+
<b>Corner chip</b> = the <i>earliest failing rung</i> by verification order (file →
|
|
484
|
+
symbol → declaration → type → signature), with a count when a node has more than one.
|
|
485
|
+
</li>
|
|
486
|
+
<li>
|
|
487
|
+
First cell is the clean node for contrast — no ring, no chip. Compose with selection,
|
|
488
|
+
file-open spotlight, and dimming without touching the border.
|
|
489
|
+
</li>
|
|
490
|
+
</ul>
|
|
491
|
+
</div>
|
|
492
|
+
<div
|
|
493
|
+
style={{
|
|
494
|
+
display: 'flex',
|
|
495
|
+
flexWrap: 'wrap',
|
|
496
|
+
gap: '40px 28px',
|
|
497
|
+
alignItems: 'flex-start',
|
|
498
|
+
marginTop: 28,
|
|
499
|
+
}}
|
|
500
|
+
>
|
|
501
|
+
{ISSUE_EXAMPLES.map((ex) => (
|
|
502
|
+
<IssueCell key={ex.caption} {...ex} />
|
|
503
|
+
))}
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
);
|
|
507
|
+
},
|
|
508
|
+
};
|
|
@@ -511,69 +511,91 @@ export function ComponentDeclaration({
|
|
|
511
511
|
|
|
512
512
|
if (showFile && component.file) {
|
|
513
513
|
const fileInteractive = !!onOpenFile;
|
|
514
|
+
const segBorder =
|
|
515
|
+
fileHovered && fileInteractive
|
|
516
|
+
? theme.colors.accent ?? color
|
|
517
|
+
: theme.colors.border;
|
|
518
|
+
const openAt = fileInteractive
|
|
519
|
+
? (e: { stopPropagation: () => void }) => {
|
|
520
|
+
e.stopPropagation();
|
|
521
|
+
openDeclarationFile();
|
|
522
|
+
}
|
|
523
|
+
: undefined;
|
|
514
524
|
lines.push(
|
|
515
525
|
line(
|
|
516
526
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
|
517
527
|
<span
|
|
518
|
-
role={fileInteractive ? 'button' : undefined}
|
|
519
|
-
tabIndex={fileInteractive ? 0 : undefined}
|
|
520
|
-
title={fileInteractive ? `Open ${component.file}` : component.file}
|
|
521
528
|
onMouseEnter={() => setFileHovered(true)}
|
|
522
529
|
onMouseLeave={() => setFileHovered(false)}
|
|
523
|
-
onClick={
|
|
524
|
-
fileInteractive
|
|
525
|
-
? (e) => {
|
|
526
|
-
e.stopPropagation();
|
|
527
|
-
openDeclarationFile();
|
|
528
|
-
}
|
|
529
|
-
: undefined
|
|
530
|
-
}
|
|
531
530
|
style={{
|
|
532
531
|
display: 'inline-flex',
|
|
533
|
-
alignItems: '
|
|
534
|
-
padding: '1px 8px',
|
|
532
|
+
alignItems: 'stretch',
|
|
535
533
|
borderRadius: 6,
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
? theme.colors.accent ?? color
|
|
539
|
-
: theme.colors.border
|
|
540
|
-
}`,
|
|
534
|
+
overflow: 'hidden',
|
|
535
|
+
border: `1px solid ${segBorder}`,
|
|
541
536
|
background: theme.colors.backgroundSecondary ?? 'transparent',
|
|
542
|
-
color:
|
|
543
|
-
fileHovered && fileInteractive
|
|
544
|
-
? theme.colors.text
|
|
545
|
-
: theme.colors.textSecondary ?? muted,
|
|
546
|
-
fontFamily: theme.fonts.monospace,
|
|
547
|
-
fontSize: theme.fontSizes[0],
|
|
548
537
|
whiteSpace: 'nowrap',
|
|
549
|
-
cursor: fileInteractive ? 'pointer' : 'default',
|
|
550
538
|
}}
|
|
551
539
|
>
|
|
552
|
-
{component.file}
|
|
553
|
-
</span>
|
|
554
|
-
{fileOpen && (
|
|
555
540
|
<span
|
|
556
|
-
|
|
541
|
+
role={fileInteractive ? 'button' : undefined}
|
|
542
|
+
tabIndex={fileInteractive ? 0 : undefined}
|
|
543
|
+
title={fileInteractive ? `Open ${component.file}` : component.file}
|
|
544
|
+
onClick={openAt}
|
|
557
545
|
style={{
|
|
558
546
|
display: 'inline-flex',
|
|
559
547
|
alignItems: 'center',
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
color: theme.colors.accent ?? color,
|
|
548
|
+
padding: '1px 8px',
|
|
549
|
+
color:
|
|
550
|
+
fileHovered && fileInteractive
|
|
551
|
+
? theme.colors.text
|
|
552
|
+
: theme.colors.textSecondary ?? muted,
|
|
566
553
|
fontFamily: theme.fonts.monospace,
|
|
567
|
-
fontSize: theme.fontSizes[0]
|
|
568
|
-
|
|
569
|
-
textTransform: 'uppercase',
|
|
570
|
-
whiteSpace: 'nowrap',
|
|
554
|
+
fontSize: theme.fontSizes[0],
|
|
555
|
+
cursor: fileInteractive ? 'pointer' : 'default',
|
|
571
556
|
}}
|
|
572
557
|
>
|
|
573
|
-
|
|
558
|
+
{component.file}
|
|
574
559
|
</span>
|
|
575
|
-
|
|
576
|
-
|
|
560
|
+
{declarationStartLine != null && (
|
|
561
|
+
<span
|
|
562
|
+
role={fileInteractive ? 'button' : undefined}
|
|
563
|
+
tabIndex={fileInteractive ? 0 : undefined}
|
|
564
|
+
title="Open at declaration line"
|
|
565
|
+
onClick={openAt}
|
|
566
|
+
style={{
|
|
567
|
+
display: 'inline-flex',
|
|
568
|
+
alignItems: 'center',
|
|
569
|
+
padding: '1px 6px',
|
|
570
|
+
borderLeft: `1px solid ${segBorder}`,
|
|
571
|
+
color: theme.colors.accent ?? color,
|
|
572
|
+
fontFamily: theme.fonts.monospace,
|
|
573
|
+
fontSize: theme.fontSizes[0],
|
|
574
|
+
cursor: fileInteractive ? 'pointer' : 'default',
|
|
575
|
+
}}
|
|
576
|
+
>
|
|
577
|
+
L{declarationStartLine}
|
|
578
|
+
</span>
|
|
579
|
+
)}
|
|
580
|
+
{fileOpen && (
|
|
581
|
+
<span
|
|
582
|
+
title="This file is open below"
|
|
583
|
+
style={{
|
|
584
|
+
display: 'inline-flex',
|
|
585
|
+
alignItems: 'center',
|
|
586
|
+
padding: '1px 6px',
|
|
587
|
+
borderLeft: `1px solid ${segBorder}`,
|
|
588
|
+
color: theme.colors.accent ?? color,
|
|
589
|
+
fontFamily: theme.fonts.monospace,
|
|
590
|
+
fontSize: theme.fontSizes[0] * 0.85,
|
|
591
|
+
letterSpacing: 0.4,
|
|
592
|
+
textTransform: 'uppercase',
|
|
593
|
+
}}
|
|
594
|
+
>
|
|
595
|
+
open
|
|
596
|
+
</span>
|
|
597
|
+
)}
|
|
598
|
+
</span>
|
|
577
599
|
</span>,
|
|
578
600
|
'file',
|
|
579
601
|
),
|