@principal-ai/principal-view-react 0.16.49 → 0.16.50
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/graphify/anchor.d.ts +28 -0
- package/dist/graphify/anchor.d.ts.map +1 -0
- package/dist/graphify/anchor.js +132 -0
- package/dist/graphify/anchor.js.map +1 -0
- package/dist/graphify/ids.d.ts +19 -0
- package/dist/graphify/ids.d.ts.map +1 -0
- package/dist/graphify/ids.js +46 -0
- package/dist/graphify/ids.js.map +1 -0
- package/dist/graphify/index.d.ts +7 -0
- package/dist/graphify/index.d.ts.map +1 -1
- package/dist/graphify/index.js +4 -0
- package/dist/graphify/index.js.map +1 -1
- package/dist/graphify/kind.d.ts +23 -0
- package/dist/graphify/kind.d.ts.map +1 -0
- package/dist/graphify/kind.js +71 -0
- package/dist/graphify/kind.js.map +1 -0
- package/dist/graphify/signature.d.ts +87 -0
- package/dist/graphify/signature.d.ts.map +1 -0
- package/dist/graphify/signature.js +276 -0
- package/dist/graphify/signature.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +58 -3
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +168 -35
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +5 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +2 -2
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/package.json +1 -1
- package/src/graphify/anchor.test.ts +124 -0
- package/src/graphify/anchor.ts +160 -0
- package/src/graphify/ids.ts +48 -0
- package/src/graphify/index.ts +26 -0
- package/src/graphify/kind.test.ts +108 -0
- package/src/graphify/kind.ts +114 -0
- package/src/graphify/signature.test.ts +278 -0
- package/src/graphify/signature.ts +354 -0
- package/src/index.ts +26 -0
- package/src/subsystem/ComponentDeclaration.tsx +287 -40
- package/src/subsystem/SubsystemComponentGraph.tsx +8 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
10
|
-
import { AlignLeft, FileText } from 'lucide-react';
|
|
10
|
+
import { AlignLeft, FileText, ShieldCheck } from 'lucide-react';
|
|
11
11
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
12
12
|
import {
|
|
13
13
|
KIND_COLOR,
|
|
@@ -17,6 +17,53 @@ import {
|
|
|
17
17
|
} from './model';
|
|
18
18
|
import { tokenizeComponent } from './tokenizeComponent';
|
|
19
19
|
|
|
20
|
+
/** Live / result state for the declaration-panel Verify control. */
|
|
21
|
+
export type ComponentVerificationPhase =
|
|
22
|
+
| 'idle'
|
|
23
|
+
| 'checking'
|
|
24
|
+
| 'done'
|
|
25
|
+
| 'error';
|
|
26
|
+
|
|
27
|
+
export interface ComponentVerificationState {
|
|
28
|
+
phase: ComponentVerificationPhase;
|
|
29
|
+
message?: string;
|
|
30
|
+
/** Mirrors host `ok` when a structured verify result is available. */
|
|
31
|
+
ok?: boolean;
|
|
32
|
+
code?: string;
|
|
33
|
+
file?: {
|
|
34
|
+
exists: boolean;
|
|
35
|
+
symbolDeclared?: boolean | null;
|
|
36
|
+
};
|
|
37
|
+
cache?: {
|
|
38
|
+
status: 'ready' | 'missing' | 'unavailable';
|
|
39
|
+
purl: string;
|
|
40
|
+
};
|
|
41
|
+
anchor?: {
|
|
42
|
+
resolution: 'exact' | 'file-only' | 'ambiguous' | 'missing';
|
|
43
|
+
nodeId?: string;
|
|
44
|
+
label?: string;
|
|
45
|
+
source_file?: string;
|
|
46
|
+
source_location?: string;
|
|
47
|
+
candidates?: Array<{ nodeId: string; label: string; source_file?: string }>;
|
|
48
|
+
};
|
|
49
|
+
kind?: {
|
|
50
|
+
claimed: string;
|
|
51
|
+
inferred: string;
|
|
52
|
+
match: boolean;
|
|
53
|
+
evidence?: string[];
|
|
54
|
+
};
|
|
55
|
+
signature?: {
|
|
56
|
+
match: boolean;
|
|
57
|
+
skipped: boolean;
|
|
58
|
+
skipCode?: string;
|
|
59
|
+
reason?: string;
|
|
60
|
+
claimed: { parameterTypes: string[]; returnTypes: string[] };
|
|
61
|
+
inferred: { parameterTypes: string[]; returnTypes: string[] };
|
|
62
|
+
/** Graphify `inline_parameter` marker count (anonymous eager args). */
|
|
63
|
+
inlineParameters?: number;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
20
67
|
/** Shared affordance for clickable text pieces. */
|
|
21
68
|
const clickableStyle = {
|
|
22
69
|
cursor: 'pointer',
|
|
@@ -46,8 +93,8 @@ function DetailLink({ children, onClick }: { children: ReactNode; onClick?: () =
|
|
|
46
93
|
);
|
|
47
94
|
}
|
|
48
95
|
|
|
49
|
-
/** Panel props.
|
|
50
|
-
*
|
|
96
|
+
/** Panel props. Callbacks are wired by the graph; standalone usage without
|
|
97
|
+
* them renders the same panel with nothing clickable. */
|
|
51
98
|
export interface ComponentDeclarationProps {
|
|
52
99
|
component: SubsystemComponent;
|
|
53
100
|
/** File-path click → open that file in the bottom drawer. */
|
|
@@ -57,20 +104,153 @@ export interface ComponentDeclarationProps {
|
|
|
57
104
|
onRelatedSelect?: (ref: string) => void;
|
|
58
105
|
/** Max width of the declaration panel (CSS value). Defaults to none. */
|
|
59
106
|
maxWidth?: string | number;
|
|
107
|
+
/** When set, shows a Verify control that calls back with the component id. */
|
|
108
|
+
onVerify?: (componentId: string) => void;
|
|
109
|
+
/** Live verification status for the selected component. */
|
|
110
|
+
verification?: ComponentVerificationState | null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function verificationSummary(
|
|
114
|
+
v: ComponentVerificationState,
|
|
115
|
+
muted: string,
|
|
116
|
+
ok: string,
|
|
117
|
+
warn: string,
|
|
118
|
+
): { text: string; color: string } {
|
|
119
|
+
if (v.phase === 'checking') {
|
|
120
|
+
return { text: v.message ?? 'Verifying…', color: muted };
|
|
121
|
+
}
|
|
122
|
+
if (v.phase === 'error') {
|
|
123
|
+
return { text: v.message ?? 'Verification failed', color: '#e5534b' };
|
|
124
|
+
}
|
|
125
|
+
if (v.phase !== 'done') {
|
|
126
|
+
return { text: '', color: muted };
|
|
127
|
+
}
|
|
128
|
+
const bits: string[] = [];
|
|
129
|
+
if (v.file) {
|
|
130
|
+
if (!v.file.exists) bits.push('file missing');
|
|
131
|
+
else if (v.file.symbolDeclared === false) bits.push('symbol not declared in file');
|
|
132
|
+
else if (v.file.symbolDeclared === true) bits.push('file+symbol ok');
|
|
133
|
+
else bits.push('file exists');
|
|
134
|
+
}
|
|
135
|
+
if (v.cache?.status === 'missing') {
|
|
136
|
+
return {
|
|
137
|
+
text: [...bits, 'graphify cache not ready — run graphify from Subsystems list'].join(' · '),
|
|
138
|
+
color: warn,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (v.cache?.status === 'unavailable') {
|
|
142
|
+
return {
|
|
143
|
+
text: [...bits, 'no local checkout for this purl'].join(' · '),
|
|
144
|
+
color: muted,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const res = v.anchor?.resolution;
|
|
148
|
+
if (res === 'exact') {
|
|
149
|
+
const loc = v.anchor?.source_location ? ` @ ${v.anchor.source_location}` : '';
|
|
150
|
+
const anchorBit = `exact → ${v.anchor?.label ?? v.anchor?.nodeId ?? 'node'}${loc}`;
|
|
151
|
+
if (v.kind && !v.kind.match) {
|
|
152
|
+
const why =
|
|
153
|
+
v.kind.inferred === 'unknown'
|
|
154
|
+
? `kind unknown (claimed ${v.kind.claimed})`
|
|
155
|
+
: `kind mismatch: claimed ${v.kind.claimed}, inferred ${v.kind.inferred}`;
|
|
156
|
+
return {
|
|
157
|
+
text: [...bits, anchorBit, why].join(' · '),
|
|
158
|
+
color: '#e5534b',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (v.signature && !v.signature.skipped && !v.signature.match) {
|
|
162
|
+
const cParams = v.signature.claimed.parameterTypes.join(', ') || '∅';
|
|
163
|
+
const iParams = v.signature.inferred.parameterTypes.join(', ') || '∅';
|
|
164
|
+
const cRet = v.signature.claimed.returnTypes.join(', ') || '∅';
|
|
165
|
+
const iRet = v.signature.inferred.returnTypes.join(', ') || '∅';
|
|
166
|
+
return {
|
|
167
|
+
text: [
|
|
168
|
+
...bits,
|
|
169
|
+
anchorBit,
|
|
170
|
+
v.kind?.match ? `kind ${v.kind.inferred}` : undefined,
|
|
171
|
+
`signature mismatch: params [${cParams}]≠[${iParams}] return [${cRet}]≠[${iRet}]`,
|
|
172
|
+
]
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.join(' · '),
|
|
175
|
+
color: '#e5534b',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const kindBit =
|
|
179
|
+
v.kind?.match ? `kind ${v.kind.inferred}` : undefined;
|
|
180
|
+
const inlineBit =
|
|
181
|
+
v.signature?.inlineParameters
|
|
182
|
+
? `${v.signature.inlineParameters} inline param${v.signature.inlineParameters === 1 ? '' : 's'} verified`
|
|
183
|
+
: undefined;
|
|
184
|
+
if (v.signature?.skipped) {
|
|
185
|
+
const skipPhrase: Record<string, string> = {
|
|
186
|
+
no_claimed_types: 'no claimable named types (primitives/inline only)',
|
|
187
|
+
generic_arg_only:
|
|
188
|
+
'return only as generic_arg — wrapper types (Promise/Omit/Map/Array) not read as return edges',
|
|
189
|
+
partially_generic_arg:
|
|
190
|
+
'claimed types resolve only via generic_arg; rest unresolved',
|
|
191
|
+
unresolved_claimed_types:
|
|
192
|
+
'claimed return type has no graphify edge (npm/global/DOM type)',
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
text: [
|
|
196
|
+
...bits,
|
|
197
|
+
anchorBit,
|
|
198
|
+
...(kindBit ? [kindBit] : []),
|
|
199
|
+
...(inlineBit ? [inlineBit] : []),
|
|
200
|
+
v.signature.skipCode
|
|
201
|
+
? `params/return not fully checked — ${skipPhrase[v.signature.skipCode] ?? v.signature.skipCode}`
|
|
202
|
+
: 'params/return not checked',
|
|
203
|
+
].join(' · '),
|
|
204
|
+
color: warn,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const sigBit =
|
|
208
|
+
v.signature && v.signature.match ? 'params/return ok' : undefined;
|
|
209
|
+
return {
|
|
210
|
+
text: [...bits, anchorBit, ...(kindBit ? [kindBit] : []), ...(sigBit ? [sigBit] : [])].join(' · '),
|
|
211
|
+
color: ok,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (res === 'file-only') {
|
|
215
|
+
return {
|
|
216
|
+
text: [...bits, 'file in graph, symbol not uniquely matched'].join(' · '),
|
|
217
|
+
color: warn,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (res === 'ambiguous') {
|
|
221
|
+
const n = v.anchor?.candidates?.length ?? 0;
|
|
222
|
+
return {
|
|
223
|
+
text: [...bits, `ambiguous (${n} candidates)`].join(' · '),
|
|
224
|
+
color: warn,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (res === 'missing') {
|
|
228
|
+
return {
|
|
229
|
+
text: [...bits, 'no matching graphify node'].join(' · '),
|
|
230
|
+
color: '#e5534b',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { text: bits.join(' · ') || 'done', color: muted };
|
|
60
234
|
}
|
|
61
235
|
|
|
62
236
|
/** Declaration panel for the selected component — its definition, code-style. */
|
|
63
|
-
export function ComponentDeclaration({
|
|
237
|
+
export function ComponentDeclaration({
|
|
238
|
+
component,
|
|
239
|
+
onOpenFile,
|
|
240
|
+
onRelatedSelect: _onRelatedSelect,
|
|
241
|
+
maxWidth,
|
|
242
|
+
onVerify,
|
|
243
|
+
verification,
|
|
244
|
+
}: ComponentDeclarationProps) {
|
|
64
245
|
const { theme } = useTheme();
|
|
65
246
|
const [fileHovered, setFileHovered] = useState(false);
|
|
66
|
-
// Header visibility toggles — off by default so the panel opens as pure
|
|
67
|
-
// code; the repo-row icons bring the path and description back.
|
|
68
247
|
const [showFile, setShowFile] = useState(false);
|
|
69
248
|
const [showPurpose, setShowPurpose] = useState(false);
|
|
70
249
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
71
250
|
const color = KIND_COLOR[component.kind] ?? '#888';
|
|
251
|
+
const okColor = '#3d9a5f';
|
|
252
|
+
const warnColor = theme.colors.textSecondary;
|
|
72
253
|
|
|
73
|
-
// Token-kind → theme-color map (used by the token iterator below).
|
|
74
254
|
const tokenColor: Record<SubsystemDeclTokenKind, string> = {
|
|
75
255
|
keyword: theme.colors.secondary,
|
|
76
256
|
name: color,
|
|
@@ -78,7 +258,7 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
78
258
|
type: theme.colors.accent,
|
|
79
259
|
punctuation: muted,
|
|
80
260
|
string: theme.colors.success,
|
|
81
|
-
newline: '',
|
|
261
|
+
newline: '',
|
|
82
262
|
};
|
|
83
263
|
|
|
84
264
|
const line = (children: ReactNode, key?: string, indent?: boolean | number) => {
|
|
@@ -98,9 +278,6 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
98
278
|
key,
|
|
99
279
|
indent,
|
|
100
280
|
);
|
|
101
|
-
// Purpose is the panel's actual prose, not code — body font, readable
|
|
102
|
-
// size/contrast, capped measure. The `//` metadata lines above it stay
|
|
103
|
-
// muted mono on purpose.
|
|
104
281
|
const purposeStyle = {
|
|
105
282
|
color: theme.colors.text,
|
|
106
283
|
fontFamily: theme.fonts.body,
|
|
@@ -108,17 +285,9 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
108
285
|
lineHeight: 1.5,
|
|
109
286
|
maxWidth: '52ch',
|
|
110
287
|
} as const;
|
|
111
|
-
|
|
112
|
-
// `// implemented by:`, …) are deliberately not rendered — the graph's
|
|
113
|
-
// edges carry interactions, and duplicated name lists read as clutter. The
|
|
114
|
-
// data stays on the Graphify detail types for other surfaces.
|
|
288
|
+
|
|
115
289
|
const lines: ReactNode[] = [];
|
|
116
|
-
// Repo identity — owner avatar + name for GitHub-hosted purls; anything
|
|
117
|
-
// else falls back to the formatted purl as a quiet comment. The avatar
|
|
118
|
-
// hides itself on load failure (offline / blocked hosts).
|
|
119
290
|
const ghMatch = /^pkg:github\/([^/]+)\/([^/#?]+)/.exec(component.purl ?? '');
|
|
120
|
-
// Small ghost icon button for the header toggles — accent when its section
|
|
121
|
-
// is shown, muted when hidden.
|
|
122
291
|
const toggleBtn = (on: boolean, onClick: () => void, title: string, Icon: typeof FileText) => (
|
|
123
292
|
<button
|
|
124
293
|
type="button"
|
|
@@ -142,6 +311,44 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
142
311
|
<Icon size={13} />
|
|
143
312
|
</button>
|
|
144
313
|
);
|
|
314
|
+
const verifyBusy = verification?.phase === 'checking';
|
|
315
|
+
const headerActions = (
|
|
316
|
+
<span style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
317
|
+
{onVerify && (
|
|
318
|
+
<button
|
|
319
|
+
type="button"
|
|
320
|
+
title="Verify against graphify"
|
|
321
|
+
aria-label="Verify against graphify"
|
|
322
|
+
disabled={verifyBusy}
|
|
323
|
+
onClick={(e) => {
|
|
324
|
+
e.stopPropagation();
|
|
325
|
+
onVerify(component.id);
|
|
326
|
+
}}
|
|
327
|
+
style={{
|
|
328
|
+
display: 'flex',
|
|
329
|
+
alignItems: 'center',
|
|
330
|
+
gap: 4,
|
|
331
|
+
height: 20,
|
|
332
|
+
padding: '0 6px',
|
|
333
|
+
border: `1px solid ${theme.colors.border}`,
|
|
334
|
+
borderRadius: 4,
|
|
335
|
+
background: 'transparent',
|
|
336
|
+
color: verifyBusy ? muted : theme.colors.textSecondary,
|
|
337
|
+
cursor: verifyBusy ? 'wait' : 'pointer',
|
|
338
|
+
fontSize: theme.fontSizes[0],
|
|
339
|
+
fontFamily: theme.fonts.body,
|
|
340
|
+
opacity: verifyBusy ? 0.7 : 1,
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
<ShieldCheck size={12} />
|
|
344
|
+
{verifyBusy ? '…' : 'Verify'}
|
|
345
|
+
</button>
|
|
346
|
+
)}
|
|
347
|
+
{toggleBtn(showFile, () => setShowFile((v) => !v), 'Toggle file path', FileText)}
|
|
348
|
+
{toggleBtn(showPurpose, () => setShowPurpose((v) => !v), 'Toggle description', AlignLeft)}
|
|
349
|
+
</span>
|
|
350
|
+
);
|
|
351
|
+
|
|
145
352
|
if (ghMatch) {
|
|
146
353
|
const [, ghOwner, ghRepo] = ghMatch;
|
|
147
354
|
lines.push(
|
|
@@ -166,17 +373,68 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
166
373
|
>
|
|
167
374
|
{ghRepo}
|
|
168
375
|
</span>
|
|
169
|
-
|
|
170
|
-
{toggleBtn(showFile, () => setShowFile((v) => !v), 'Toggle file path', FileText)}
|
|
171
|
-
{toggleBtn(showPurpose, () => setShowPurpose((v) => !v), 'Toggle description', AlignLeft)}
|
|
172
|
-
</span>
|
|
376
|
+
{headerActions}
|
|
173
377
|
</span>,
|
|
174
378
|
'repo',
|
|
175
379
|
),
|
|
176
380
|
);
|
|
177
381
|
} else if (component.purl) {
|
|
178
|
-
lines.push(
|
|
382
|
+
lines.push(
|
|
383
|
+
line(
|
|
384
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 6, width: '100%' }}>
|
|
385
|
+
<span style={commentStyle}>{`// ${formatPurl(component.purl)}`}</span>
|
|
386
|
+
{headerActions}
|
|
387
|
+
</span>,
|
|
388
|
+
'purl',
|
|
389
|
+
),
|
|
390
|
+
);
|
|
391
|
+
} else if (onVerify) {
|
|
392
|
+
lines.push(line(headerActions, 'actions'));
|
|
179
393
|
}
|
|
394
|
+
|
|
395
|
+
if (verification && verification.phase !== 'idle') {
|
|
396
|
+
const summary = verificationSummary(verification, muted, okColor, warnColor);
|
|
397
|
+
if (summary.text) {
|
|
398
|
+
lines.push(
|
|
399
|
+
line(
|
|
400
|
+
<span
|
|
401
|
+
style={{
|
|
402
|
+
color: summary.color,
|
|
403
|
+
fontFamily: theme.fonts.body,
|
|
404
|
+
fontSize: theme.fontSizes[0],
|
|
405
|
+
whiteSpace: 'normal',
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
{summary.text}
|
|
409
|
+
</span>,
|
|
410
|
+
'verify-status',
|
|
411
|
+
),
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
if (
|
|
415
|
+
verification.anchor?.resolution === 'ambiguous' &&
|
|
416
|
+
verification.anchor.candidates &&
|
|
417
|
+
verification.anchor.candidates.length > 0
|
|
418
|
+
) {
|
|
419
|
+
for (const c of verification.anchor.candidates.slice(0, 5)) {
|
|
420
|
+
lines.push(
|
|
421
|
+
commentLine(
|
|
422
|
+
`// ${c.label}${c.source_file ? ` — ${c.source_file}` : ''}`,
|
|
423
|
+
`cand-${c.nodeId}`,
|
|
424
|
+
),
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (verification.signature?.skipped && verification.signature.reason) {
|
|
429
|
+
lines.push(
|
|
430
|
+
commentLine(
|
|
431
|
+
`// why: ${verification.signature.reason}`,
|
|
432
|
+
'verify-skip-reason',
|
|
433
|
+
),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
180
438
|
if (showFile && component.file)
|
|
181
439
|
lines.push(
|
|
182
440
|
line(
|
|
@@ -185,8 +443,6 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
185
443
|
onMouseEnter={() => setFileHovered(true)}
|
|
186
444
|
onMouseLeave={() => setFileHovered(false)}
|
|
187
445
|
style={{
|
|
188
|
-
// Path as UI text, not a code comment — matches the repo-name
|
|
189
|
-
// row. Quiet until hover; the underline only appears then.
|
|
190
446
|
color: theme.colors.textSecondary ?? muted,
|
|
191
447
|
fontFamily: theme.fonts.body,
|
|
192
448
|
fontSize: theme.fontSizes[1],
|
|
@@ -201,9 +457,6 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
201
457
|
),
|
|
202
458
|
);
|
|
203
459
|
|
|
204
|
-
// --- Container width measurement --------------------------------------
|
|
205
|
-
// Approximate character count from the container's clientWidth so Prettier
|
|
206
|
-
// wraps at the same column the code block will actually use.
|
|
207
460
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
208
461
|
const [printWidth, setPrintWidth] = useState(80);
|
|
209
462
|
|
|
@@ -212,8 +465,6 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
212
465
|
if (!el) return;
|
|
213
466
|
|
|
214
467
|
function measureWidth() {
|
|
215
|
-
// Measure a single `ch` using an offscreen element so we don't disturb
|
|
216
|
-
// the component's own DOM.
|
|
217
468
|
const probe = document.createElement('div');
|
|
218
469
|
probe.style.cssText =
|
|
219
470
|
'position:absolute;visibility:hidden;white-space:pre;' +
|
|
@@ -233,9 +484,6 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
233
484
|
return () => ro.disconnect();
|
|
234
485
|
}, []);
|
|
235
486
|
|
|
236
|
-
// --- Declaration tokens ------------------------------------------------
|
|
237
|
-
// Tokenize asynchronously via Prettier + highlight.js. Pre-tokenized
|
|
238
|
-
// tokens from the wire skip the pipeline entirely.
|
|
239
487
|
const [tokens, setTokens] = useState(component.tokens ?? []);
|
|
240
488
|
useEffect(() => {
|
|
241
489
|
if (component.tokens) {
|
|
@@ -246,7 +494,9 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
246
494
|
tokenizeComponent(component, printWidth).then((t) => {
|
|
247
495
|
if (!cancelled) setTokens(t);
|
|
248
496
|
});
|
|
249
|
-
return () => {
|
|
497
|
+
return () => {
|
|
498
|
+
cancelled = true;
|
|
499
|
+
};
|
|
250
500
|
}, [component, printWidth]);
|
|
251
501
|
const declLines: ReactNode[][] = [[]];
|
|
252
502
|
let di = 0;
|
|
@@ -271,12 +521,9 @@ export function ComponentDeclaration({ component, onOpenFile, onRelatedSelect: _
|
|
|
271
521
|
);
|
|
272
522
|
}
|
|
273
523
|
|
|
274
|
-
// Purpose prose follows the declaration — identity and syntax first, the
|
|
275
|
-
// explanation underneath. Newlines split into separate prose lines so a
|
|
276
|
-
// two-thought purpose reads as a short paragraph instead of one blob.
|
|
277
524
|
if (showPurpose && component.purpose) {
|
|
278
525
|
lines.push(line(<div style={{ height: 6 }} />, 'purpose-gap'));
|
|
279
|
-
component.purpose.split(
|
|
526
|
+
component.purpose.split('\n').forEach((purposeLine, i) =>
|
|
280
527
|
lines.push(
|
|
281
528
|
line(
|
|
282
529
|
<span style={{ ...purposeStyle, display: 'block' }} key={`purpose${i}`}>
|
|
@@ -45,6 +45,7 @@ import { SubsystemComponentNode, SubsystemEdge, SUBSYSTEM_CALLBACKS } from './no
|
|
|
45
45
|
import { SubsystemFileTree } from './SubsystemFileTree';
|
|
46
46
|
import { GraphLayoutCover } from './GraphLayoutCover';
|
|
47
47
|
import { ComponentDeclaration } from './ComponentDeclaration';
|
|
48
|
+
import type { ComponentVerificationState } from './ComponentDeclaration';
|
|
48
49
|
import { FileDrawer } from './FileDrawer';
|
|
49
50
|
import { EdgeLegendModal, MECHANISM_DESCRIPTIONS } from './EdgeLegendModal';
|
|
50
51
|
import { buildRepoGroups, repoAvatarUrl, type RepoGroup } from './paths';
|
|
@@ -88,6 +89,10 @@ export interface SubsystemComponentGraphProps {
|
|
|
88
89
|
* path). The tree is derived from the components' `file` values.
|
|
89
90
|
*/
|
|
90
91
|
onFileSelect?: (file: string) => void;
|
|
92
|
+
/** Verify the selected component against graphify (declaration panel). */
|
|
93
|
+
onVerifyComponent?: (componentId: string) => void;
|
|
94
|
+
/** Live verification status for the selected component. */
|
|
95
|
+
componentVerification?: ComponentVerificationState | null;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
const nodeTypes: NodeTypes = {
|
|
@@ -117,7 +122,7 @@ interface InnerProps extends SubsystemComponentGraphProps {
|
|
|
117
122
|
measured: { w: number; h: number } | null;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, onFileSelect }: InnerProps) {
|
|
125
|
+
function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, title, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, onFileSelect, onVerifyComponent, componentVerification }: InnerProps) {
|
|
121
126
|
const { theme } = useTheme();
|
|
122
127
|
const { fitView } = useReactFlow();
|
|
123
128
|
const viewport = useViewport();
|
|
@@ -699,6 +704,8 @@ function Inner({ components, edges, onSelect, onEdgeSelect, measured: _measured,
|
|
|
699
704
|
component={selected}
|
|
700
705
|
onOpenFile={onTreeSelectFile}
|
|
701
706
|
onRelatedSelect={resolveRelatedComponent}
|
|
707
|
+
onVerify={onVerifyComponent}
|
|
708
|
+
verification={componentVerification}
|
|
702
709
|
/>
|
|
703
710
|
</div>
|
|
704
711
|
)}
|