@principal-ai/principal-view-react 0.16.35 → 0.16.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/graphify/consolidated.d.ts +17 -3
  2. package/dist/graphify/consolidated.d.ts.map +1 -1
  3. package/dist/graphify/index.d.ts +2 -0
  4. package/dist/graphify/index.d.ts.map +1 -1
  5. package/dist/graphify/index.js +1 -1
  6. package/dist/graphify/index.js.map +1 -1
  7. package/dist/graphify/resolve.d.ts +48 -0
  8. package/dist/graphify/resolve.d.ts.map +1 -0
  9. package/dist/graphify/resolve.js +83 -0
  10. package/dist/graphify/resolve.js.map +1 -0
  11. package/dist/index.d.ts +5 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +3 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/subsystem/ComponentDetail.d.ts +16 -7
  16. package/dist/subsystem/ComponentDetail.d.ts.map +1 -1
  17. package/dist/subsystem/ComponentDetail.js +124 -82
  18. package/dist/subsystem/ComponentDetail.js.map +1 -1
  19. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  20. package/dist/subsystem/SubsystemComponentGraph.js +47 -10
  21. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  22. package/dist/subsystem/model.d.ts +14 -0
  23. package/dist/subsystem/model.d.ts.map +1 -1
  24. package/dist/subsystem/model.js +26 -1
  25. package/dist/subsystem/model.js.map +1 -1
  26. package/dist/subsystem/paths.d.ts +38 -0
  27. package/dist/subsystem/paths.d.ts.map +1 -0
  28. package/dist/subsystem/paths.js +52 -0
  29. package/dist/subsystem/paths.js.map +1 -0
  30. package/package.json +1 -1
  31. package/src/graphify/consolidated.ts +17 -3
  32. package/src/graphify/index.ts +9 -0
  33. package/src/graphify/resolve.test.ts +89 -0
  34. package/src/graphify/resolve.ts +115 -0
  35. package/src/index.ts +19 -0
  36. package/src/stories/SubsystemComponentGraph.stories.tsx +331 -11
  37. package/src/subsystem/ComponentDetail.tsx +355 -156
  38. package/src/subsystem/SubsystemComponentGraph.tsx +76 -15
  39. package/src/subsystem/model.test.ts +11 -0
  40. package/src/subsystem/model.ts +25 -1
  41. package/src/subsystem/paths.test.ts +62 -0
  42. package/src/subsystem/paths.ts +69 -0
@@ -1,187 +1,386 @@
1
1
  /**
2
- * ComponentDetail — selection detail panel for a subsystem component: the
3
- * verifiable identity (symbol/file/purpose/purl) plus graphify drill-down
4
- * sections. File content lives in the bottom FileDrawer, not here.
2
+ * ComponentDetail — selection detail panel for a subsystem component, rendered
3
+ * as it would appear in code: a file-path comment + doc comment, then the kind's
4
+ * declaration (`class X extends Y { ... }`, `function f(): T`, ...) with its
5
+ * members as signatures. Graphify relationship drill-downs appear as trailing
6
+ * comments. File content lives in the bottom FileDrawer, not here.
5
7
  */
6
8
 
7
- import type { ReactNode } from 'react';
9
+ import { Fragment, type ReactNode } from 'react';
8
10
  import { useTheme } from '@principal-ade/industry-theme';
9
11
  import {
10
12
  KIND_COLOR,
11
13
  deriveNameFromSymbol,
14
+ formatPurl,
12
15
  type SubsystemComponent,
13
16
  } from './model';
14
- import type { GraphifyComponentDetail } from '../graphify';
15
17
 
16
- /** Detail panel for the selected component — the verifiable identity + sources
17
- * moved off the canvas so nodes stay minimal. */
18
- export function ComponentDetail({ component }: { component: SubsystemComponent }) {
19
- const { theme } = useTheme();
20
- const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
21
- const color = KIND_COLOR[component.kind] ?? '#888';
22
- const displayName = deriveNameFromSymbol(component.symbol, component.kind, component.name, component.file);
23
- const rows: Array<[string, string]> = [];
24
- if (component.symbol) rows.push(['Symbol', component.symbol]);
25
- if (component.file) rows.push(['File', component.file]);
26
- if (component.purpose) rows.push(['Purpose', component.purpose]);
27
- rows.push(['PURL', component.purl]);
18
+ /** Shared affordance for clickable text pieces. */
19
+ const clickableStyle = {
20
+ cursor: 'pointer',
21
+ textDecorationLine: 'underline',
22
+ textDecorationStyle: 'dotted',
23
+ textUnderlineOffset: 2,
24
+ } as const;
28
25
 
26
+ /** Clickable detail-panel piece — dotted underline signals interactivity. */
27
+ function DetailLink({ children, onClick }: { children: ReactNode; onClick?: () => void }) {
28
+ if (!onClick) return <>{children}</>;
29
29
  return (
30
- <div
30
+ <span
31
+ onClick={(e) => {
32
+ e.stopPropagation();
33
+ onClick();
34
+ }}
31
35
  style={{
32
- borderTop: `1px solid ${theme.colors.border}`,
33
- background: theme.colors.background,
34
- padding: '10px 12px',
35
- fontFamily: theme.fonts.body,
36
+ cursor: 'pointer',
37
+ textDecorationLine: 'underline',
38
+ textDecorationStyle: 'dotted',
39
+ textUnderlineOffset: 2,
36
40
  }}
37
41
  >
38
- <div
39
- style={{
40
- display: 'flex',
41
- alignItems: 'center',
42
- gap: 8,
43
- fontSize: theme.fontSizes[1],
44
- fontWeight: 600,
45
- marginBottom: 6,
46
- }}
47
- >
48
- <span style={{ color }}>{displayName}</span>
49
- <span
50
- style={{
51
- fontSize: theme.fontSizes[0] * 0.8,
52
- fontFamily: theme.fonts.monospace,
53
- textTransform: 'uppercase',
54
- color,
55
- }}
56
- >
57
- {component.kind}
58
- </span>
59
- </div>
60
- {rows.map(([k, v]) => (
61
- <div
62
- key={k}
63
- style={{ display: 'flex', gap: 8, fontSize: theme.fontSizes[0], lineHeight: 1.5 }}
64
- >
65
- <span
66
- style={{
67
- width: 64,
68
- flexShrink: 0,
69
- color: muted,
70
- fontFamily: theme.fonts.monospace,
71
- textTransform: 'uppercase',
72
- fontSize: theme.fontSizes[0] * 0.8,
73
- }}
74
- >
75
- {k}
76
- </span>
77
- <span style={{ color: theme.colors.text, fontFamily: theme.fonts.monospace }}>
78
- {v}
79
- </span>
80
- </div>
81
- ))}
82
- {component.detail && <GraphifyDetailSections detail={component.detail} />}
83
- </div>
42
+ {children}
43
+ </span>
84
44
  );
85
45
  }
86
46
 
87
- /** Renders the graphify drill-down payload for a component. */
88
- function GraphifyDetailSections({ detail }: { detail: GraphifyComponentDetail }) {
47
+ /** Panel props. The two callbacks are wired by the graph; standalone usage
48
+ * without them renders the same panel with nothing clickable. */
49
+ export interface ComponentDetailProps {
50
+ component: SubsystemComponent;
51
+ /** File-path click → open that file in the bottom drawer. */
52
+ onOpenFile?: (file: string) => void;
53
+ /** Related-name click (types, callers/callees, implementors, …) → select
54
+ * that component if one matches; unmatched refs no-op. */
55
+ onRelatedSelect?: (ref: string) => void;
56
+ }
57
+
58
+ /** Detail panel for the selected component — its declaration, code-style. */
59
+ export function ComponentDetail({ component, onOpenFile, onRelatedSelect }: ComponentDetailProps) {
89
60
  const { theme } = useTheme();
90
61
  const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
62
+ const color = KIND_COLOR[component.kind] ?? '#888';
63
+ const displayName = deriveNameFromSymbol(component.symbol, component.kind, component.name, component.file);
91
64
 
92
- const label = (k: string) => (
93
- <span
94
- style={{
95
- width: 64,
96
- flexShrink: 0,
97
- color: muted,
98
- fontFamily: theme.fonts.monospace,
99
- textTransform: 'uppercase',
100
- fontSize: theme.fontSizes[0] * 0.8,
101
- }}
102
- >
103
- {k}
104
- </span>
65
+ // Token styles keyword / name / member / type / string / punctuation.
66
+ const kw = (text: string) => <span style={{ color: theme.colors.secondary }}>{text}</span>;
67
+ const nm = (text: string) => <span style={{ color }}>{text}</span>;
68
+ const mb = (text: string) => <span style={{ color: theme.colors.info }}>{text}</span>;
69
+ // Type tokens are links into the graph (other components often own them).
70
+ const ty = (text: string) => (
71
+ <DetailLink onClick={onRelatedSelect ? () => onRelatedSelect(text) : undefined}>
72
+ <span style={{ color: theme.colors.accent }}>{text}</span>
73
+ </DetailLink>
105
74
  );
106
- const chip = (name: string, meta?: string, key?: string | number) => (
107
- <span
108
- key={key}
109
- style={{
110
- fontFamily: theme.fonts.monospace,
111
- fontSize: theme.fontSizes[0],
112
- color: theme.colors.text,
113
- border: `1px solid ${theme.colors.border}`,
114
- borderRadius: 4,
115
- padding: '1px 6px',
116
- marginRight: 6,
117
- marginBottom: 4,
118
- display: 'inline-block',
119
- }}
120
- >
121
- {name}
122
- {meta ? <span style={{ color: muted }}> {meta}</span> : ''}
123
- </span>
124
- );
125
- const row = (k: string, children: ReactNode) => (
126
- <div
127
- style={{ display: 'flex', gap: 8, fontSize: theme.fontSizes[0], lineHeight: 1.5, marginBottom: 4 }}
128
- >
129
- {label(k)}
130
- <div style={{ minWidth: 0 }}>{children}</div>
75
+ const str = (text: string) => <span style={{ color: theme.colors.success }}>{text}</span>;
76
+ const pn = (text: string) => <span style={{ color: muted }}>{text}</span>;
77
+
78
+ const line = (children: ReactNode, key?: string, indent?: boolean) => (
79
+ <div key={key} style={indent ? { paddingLeft: 16 } : undefined}>
80
+ {children}
131
81
  </div>
132
82
  );
83
+ const commentStyle = { color: muted, fontStyle: 'italic' } as const;
84
+ const commentLine = (text: string, key?: string, indent?: boolean, onClick?: () => void) =>
85
+ line(
86
+ <DetailLink onClick={onClick}>
87
+ <span style={{ ...commentStyle, ...(onClick ? clickableStyle : undefined) }}>{text}</span>
88
+ </DetailLink>,
89
+ key,
90
+ indent,
91
+ );
92
+ // Trailing relationship comments whose names are individually clickable.
93
+ const commentListLine = (prefix: string, names: string[], key?: string) =>
94
+ line(
95
+ <>
96
+ <span style={commentStyle}>{prefix}</span>
97
+ {list(
98
+ names.map((n, i) => (
99
+ <Fragment key={i}>
100
+ <DetailLink onClick={onRelatedSelect ? () => onRelatedSelect(n) : undefined}>
101
+ <span style={{ ...commentStyle, ...(onRelatedSelect ? clickableStyle : undefined) }}>{n}</span>
102
+ </DetailLink>
103
+ </Fragment>
104
+ )),
105
+ )}
106
+ </>,
107
+ key,
108
+ );
109
+ // Comma-join rendered tokens without raw-array key churn.
110
+ const list = (items: ReactNode[]) =>
111
+ items.map((item, i) => (
112
+ <Fragment key={i}>
113
+ {i > 0 ? ', ' : null}
114
+ {item}
115
+ </Fragment>
116
+ ));
133
117
 
134
- switch (detail.kind) {
135
- case 'class':
136
- return (
137
- <>
138
- {detail.methods.length > 0 &&
139
- row('methods', detail.methods.map((m, i) => chip(m.name, m.returnType, i)))}
140
- {detail.properties.length > 0 &&
141
- row('props', detail.properties.map((p, i) => chip(p.name, p.type, i)))}
142
- {detail.extends.length > 0 && row('extends', detail.extends.map((e, i) => chip(e, undefined, i)))}
143
- {detail.implements.length > 0 && row('impl', detail.implements.map((e, i) => chip(e, undefined, i)))}
144
- {detail.instantiations.length > 0 &&
145
- row('insts', detail.instantiations.map((i, idx) => chip(i.name, undefined, idx)))}
146
- {detail.references.length > 0 &&
147
- row('refs', detail.references.map((r, idx) => chip(r.name, undefined, idx)))}
148
- </>
118
+ const lines: ReactNode[] = [];
119
+ if (component.purl)
120
+ lines.push(commentLine(`// ${formatPurl(component.purl)}`, 'purl'));
121
+ if (component.file)
122
+ lines.push(commentLine(`// ${component.file}`, 'file', false, onOpenFile ? () => onOpenFile(component.file) : undefined));
123
+ if (component.purpose) lines.push(commentLine(`/** ${component.purpose} */`, 'purpose'));
124
+
125
+ const detail = component.detail;
126
+ switch (detail?.kind ?? component.kind) {
127
+ case 'class': {
128
+ const members: ReactNode[] = [];
129
+ if (detail?.kind === 'class') {
130
+ detail.methods.forEach((m, i) =>
131
+ members.push(
132
+ line(
133
+ <>
134
+ {mb(m.name)}
135
+ {pn('(')}
136
+ {(m.parameters ?? []).length > 0 &&
137
+ list(
138
+ (m.parameters ?? []).map((p, j) => (
139
+ <Fragment key={j}>
140
+ {p.name ? (
141
+ <>
142
+ {mb(p.name)}
143
+ {pn(': ')}
144
+ </>
145
+ ) : null}
146
+ {ty(p.type)}
147
+ </Fragment>
148
+ )),
149
+ )}
150
+ {pn(')')}
151
+ {m.returnType ? (
152
+ <>
153
+ {pn(': ')}
154
+ {ty(m.returnType)}
155
+ </>
156
+ ) : null}
157
+ </>,
158
+ `m${i}`,
159
+ true,
160
+ ),
161
+ ),
162
+ );
163
+ detail.properties.forEach((prop, i) =>
164
+ members.push(
165
+ line(
166
+ <>
167
+ {mb(prop.name)}
168
+ {prop.type ? (
169
+ <>
170
+ {pn(': ')}
171
+ {ty(prop.type)}
172
+ </>
173
+ ) : null}
174
+ </>,
175
+ `p${i}`,
176
+ true,
177
+ ),
178
+ ),
179
+ );
180
+ }
181
+ const cls = detail?.kind === 'class' ? detail : undefined;
182
+ const hasBody = members.length > 0;
183
+ lines.push(
184
+ line(
185
+ <>
186
+ {kw('class')}
187
+ {' '}
188
+ {nm(displayName)}
189
+ {cls && cls.extends.length > 0 && (
190
+ <>
191
+ {' '}
192
+ {kw('extends')}
193
+ {' '}
194
+ {list(cls.extends.map((e, i) => <Fragment key={i}>{ty(e)}</Fragment>))}
195
+ </>
196
+ )}
197
+ {cls && cls.implements.length > 0 && (
198
+ <>
199
+ {' '}
200
+ {kw('implements')}
201
+ {' '}
202
+ {list(cls.implements.map((e, i) => <Fragment key={i}>{ty(e)}</Fragment>))}
203
+ </>
204
+ )}
205
+ {hasBody ? pn(' {') : null}
206
+ </>,
207
+ 'decl',
208
+ ),
149
209
  );
150
- case 'function':
151
- return (
152
- <>
153
- {detail.parameters.length > 0 &&
154
- row('params', detail.parameters.map((p, i) => chip(p.type, undefined, i)))}
155
- {detail.returnType && row('return', <span>{detail.returnType}</span>)}
156
- {detail.callers.length > 0 &&
157
- row('callers', detail.callers.map((c, i) => chip(c.name, undefined, i)))}
158
- {detail.callees.length > 0 &&
159
- row('callees', detail.callees.map((c, i) => chip(c.name, undefined, i)))}
160
- </>
210
+ lines.push(...members);
211
+ if (hasBody) lines.push(line(pn('}'), 'end'));
212
+ if (cls && cls.instantiations.length > 0)
213
+ lines.push(commentListLine('// constructed by: ', cls.instantiations.map((c) => c.name), 'insts'));
214
+ if (cls && cls.references.length > 0)
215
+ lines.push(commentListLine('// referenced by: ', cls.references.map((r) => r.name), 'refs'));
216
+ break;
217
+ }
218
+ case 'function': {
219
+ const fn = detail?.kind === 'function' ? detail : undefined;
220
+ const params = fn?.parameters ?? [];
221
+ lines.push(
222
+ line(
223
+ <>
224
+ {kw('function')}
225
+ {' '}
226
+ {nm(displayName)}
227
+ {pn('(')}
228
+ {params.length > 0 &&
229
+ list(
230
+ params.map((param, i) => (
231
+ <Fragment key={i}>
232
+ {param.name ? (
233
+ <>
234
+ {mb(param.name)}
235
+ {pn(': ')}
236
+ </>
237
+ ) : null}
238
+ {ty(param.type)}
239
+ </Fragment>
240
+ )),
241
+ )}
242
+ {pn(')')}
243
+ {fn?.returnType ? (
244
+ <>
245
+ {pn(': ')}
246
+ {ty(fn.returnType)}
247
+ </>
248
+ ) : null}
249
+ {pn(';')}
250
+ </>,
251
+ 'decl',
252
+ ),
161
253
  );
162
- case 'type':
163
- return (
164
- <>
165
- {detail.properties.length > 0 &&
166
- row('props', detail.properties.map((p, i) => chip(p.name, p.type, i)))}
167
- {detail.usedBy.length > 0 &&
168
- row('used by', detail.usedBy.map((u, i) => chip(u.name, undefined, i)))}
169
- {detail.implementors.length > 0 &&
170
- row('impl', detail.implementors.map((e, i) => chip(e, undefined, i)))}
171
- </>
254
+ if (fn && fn.callers.length > 0)
255
+ lines.push(commentListLine('// called by: ', fn.callers.map((c) => c.name), 'callers'));
256
+ if (fn && fn.callees.length > 0)
257
+ lines.push(commentListLine('// calls: ', fn.callees.map((c) => c.name), 'callees'));
258
+ break;
259
+ }
260
+ case 'type': {
261
+ const tpe = detail?.kind === 'type' ? detail : undefined;
262
+ const hasBody = (tpe?.properties.length ?? 0) > 0;
263
+ lines.push(
264
+ line(
265
+ <>
266
+ {kw('interface')}
267
+ {' '}
268
+ {nm(displayName)}
269
+ {hasBody ? pn(' {') : null}
270
+ </>,
271
+ 'decl',
272
+ ),
172
273
  );
173
- case 'module':
174
- return (
175
- <>
176
- {detail.exports.length > 0 &&
177
- row('exports', detail.exports.map((e, i) => chip(e, undefined, i)))}
178
- {detail.imports.length > 0 &&
179
- row('imports', detail.imports.map((i, idx) => chip(i.name, undefined, idx)))}
180
- {detail.symbols.length > 0 &&
181
- row('symbols', detail.symbols.map((s, i) => chip(s, undefined, i)))}
182
- </>
274
+ if (tpe) {
275
+ tpe.properties.forEach((prop, i) =>
276
+ lines.push(
277
+ line(
278
+ <>
279
+ {mb(prop.name)}
280
+ {prop.type ? (
281
+ <>
282
+ {pn(': ')}
283
+ {ty(prop.type)}
284
+ </>
285
+ ) : null}
286
+ {pn(';')}
287
+ </>,
288
+ `p${i}`,
289
+ true,
290
+ ),
291
+ ),
292
+ );
293
+ }
294
+ if (hasBody) lines.push(line(pn('}'), 'end'));
295
+ if (tpe && tpe.implementors.length > 0)
296
+ lines.push(commentListLine('// implemented by: ', tpe.implementors, 'impl'));
297
+ if (tpe && tpe.usedBy.length > 0)
298
+ lines.push(commentListLine('// used by: ', tpe.usedBy.map((u) => u.name), 'usedBy'));
299
+ break;
300
+ }
301
+ case 'module': {
302
+ const mod = detail?.kind === 'module' ? detail : undefined;
303
+ if (mod) {
304
+ mod.imports.forEach((imp, i) =>
305
+ lines.push(
306
+ line(
307
+ <>
308
+ {kw('import')}
309
+ {' '}
310
+ {pn("'")}
311
+ {str(imp.name)}
312
+ {pn("';")}
313
+ </>,
314
+ `imp${i}`,
315
+ ),
316
+ ),
317
+ );
318
+ if (mod.exports.length > 0)
319
+ lines.push(
320
+ line(
321
+ <>
322
+ {kw('export')}
323
+ {' '}
324
+ {pn('{ ')}
325
+ {list(mod.exports.map((e, i) => <Fragment key={i}>{mb(e)}</Fragment>))}
326
+ {pn(' }')}
327
+ {pn(';')}
328
+ </>,
329
+ 'exports',
330
+ ),
331
+ );
332
+ if (mod.symbols.length > 0)
333
+ lines.push(commentLine(`// defines: ${mod.symbols.join(', ')}`, 'symbols'));
334
+ } else {
335
+ lines.push(
336
+ line(
337
+ <>
338
+ {kw('module')}
339
+ {' '}
340
+ {nm(displayName)}
341
+ </>,
342
+ 'decl',
343
+ ),
344
+ );
345
+ }
346
+ break;
347
+ }
348
+ case 'external': {
349
+ lines.push(
350
+ line(
351
+ <>
352
+ {kw('external')}
353
+ {' '}
354
+ {detail?.kind === 'external' ? (
355
+ <>
356
+ {pn("'")}
357
+ {str(detail.label)}
358
+ {pn("'")}
359
+ </>
360
+ ) : (
361
+ nm(displayName)
362
+ )}
363
+ </>,
364
+ 'decl',
365
+ ),
183
366
  );
184
- case 'external':
185
- return row('source', <span>{detail.label}</span>);
367
+ break;
368
+ }
186
369
  }
370
+
371
+ return (
372
+ <div
373
+ style={{
374
+ borderTop: `1px solid ${theme.colors.border}`,
375
+ background: theme.colors.backgroundSecondary,
376
+ padding: '10px 12px',
377
+ fontFamily: theme.fonts.monospace,
378
+ fontSize: theme.fontSizes[0],
379
+ lineHeight: 1.7,
380
+ wordBreak: 'break-word',
381
+ }}
382
+ >
383
+ {lines}
384
+ </div>
385
+ );
187
386
  }