@principal-ai/principal-view-react 0.16.34 → 0.16.36

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 (52) 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 +3 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/subsystem/ComponentDetail.d.ts +21 -0
  16. package/dist/subsystem/ComponentDetail.d.ts.map +1 -0
  17. package/dist/subsystem/ComponentDetail.js +137 -0
  18. package/dist/subsystem/ComponentDetail.js.map +1 -0
  19. package/dist/subsystem/EdgeLegendModal.d.ts +18 -0
  20. package/dist/subsystem/EdgeLegendModal.d.ts.map +1 -0
  21. package/dist/subsystem/EdgeLegendModal.js +98 -0
  22. package/dist/subsystem/EdgeLegendModal.js.map +1 -0
  23. package/dist/subsystem/FileDrawer.d.ts +16 -0
  24. package/dist/subsystem/FileDrawer.d.ts.map +1 -0
  25. package/dist/subsystem/FileDrawer.js +72 -0
  26. package/dist/subsystem/FileDrawer.js.map +1 -0
  27. package/dist/subsystem/SubsystemComponentGraph.d.ts +0 -3
  28. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  29. package/dist/subsystem/SubsystemComponentGraph.js +57 -191
  30. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  31. package/dist/subsystem/model.d.ts +14 -3
  32. package/dist/subsystem/model.d.ts.map +1 -1
  33. package/dist/subsystem/model.js +26 -1
  34. package/dist/subsystem/model.js.map +1 -1
  35. package/dist/subsystem/nodes.d.ts +2 -2
  36. package/dist/subsystem/nodes.d.ts.map +1 -1
  37. package/dist/subsystem/nodes.js +3 -21
  38. package/dist/subsystem/nodes.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/graphify/consolidated.ts +17 -3
  41. package/src/graphify/index.ts +9 -0
  42. package/src/graphify/resolve.test.ts +89 -0
  43. package/src/graphify/resolve.ts +115 -0
  44. package/src/index.ts +10 -0
  45. package/src/stories/SubsystemComponentGraph.stories.tsx +333 -13
  46. package/src/subsystem/ComponentDetail.tsx +386 -0
  47. package/src/subsystem/EdgeLegendModal.tsx +146 -0
  48. package/src/subsystem/FileDrawer.tsx +105 -0
  49. package/src/subsystem/SubsystemComponentGraph.tsx +93 -328
  50. package/src/subsystem/model.test.ts +11 -0
  51. package/src/subsystem/model.ts +25 -4
  52. package/src/subsystem/nodes.tsx +2 -29
@@ -0,0 +1,386 @@
1
+ /**
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.
7
+ */
8
+
9
+ import { Fragment, type ReactNode } from 'react';
10
+ import { useTheme } from '@principal-ade/industry-theme';
11
+ import {
12
+ KIND_COLOR,
13
+ deriveNameFromSymbol,
14
+ formatPurl,
15
+ type SubsystemComponent,
16
+ } from './model';
17
+
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;
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
+ return (
30
+ <span
31
+ onClick={(e) => {
32
+ e.stopPropagation();
33
+ onClick();
34
+ }}
35
+ style={{
36
+ cursor: 'pointer',
37
+ textDecorationLine: 'underline',
38
+ textDecorationStyle: 'dotted',
39
+ textUnderlineOffset: 2,
40
+ }}
41
+ >
42
+ {children}
43
+ </span>
44
+ );
45
+ }
46
+
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) {
60
+ const { theme } = useTheme();
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);
64
+
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>
74
+ );
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}
81
+ </div>
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
+ ));
117
+
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
+ ),
209
+ );
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
+ ),
253
+ );
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
+ ),
273
+ );
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
+ ),
366
+ );
367
+ break;
368
+ }
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
+ );
386
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * EdgeLegendModal — modal over the graph area showing the edge-mechanism
3
+ * legend (moved out of the sidebar), plus the canonical mechanism
4
+ * descriptions table shared with the canvas's edge-label overlay.
5
+ */
6
+
7
+ import { useEffect } from 'react';
8
+ import { useTheme } from '@principal-ade/industry-theme';
9
+ import { X } from 'lucide-react';
10
+ import { MECHANISM_COLOR, type SubsystemEdgeMechanism } from './model';
11
+
12
+ /** Mechanism → [description, verifiable-with-graphify]. Drives the legend
13
+ * modal and the "not directly verifiable" styling of edge labels. */
14
+ export const MECHANISM_DESCRIPTIONS: [SubsystemEdgeMechanism, string, boolean][] = [
15
+ ['imports', 'import statement (code-level dependency)', true],
16
+ ['imports_from', 'imported by (reverse dependency)', true],
17
+ ['re_exports', 're-exports symbols from', true],
18
+ ['defines', 'defines / declares symbol', true],
19
+ ['calls', 'function/method call (call graph edge)', true],
20
+ ['extends', 'class inheritance', true],
21
+ ['inherits', 'class inheritance', true],
22
+ ['implements', 'implements interface / protocol', true],
23
+ ['mixes_in', 'applies mixin', true],
24
+ ['uses', 'general dependency (import, call, or reference)', false],
25
+ ['method', 'structural: has method / member', true],
26
+ ['references', 'type / symbol reference (not a call)', true],
27
+ ['contains', 'structural: contains / encapsulates', true],
28
+ ['feeds', 'data flow: output feeds into input', false],
29
+ ['produces', 'data flow: produces / outputs', false],
30
+ ['registers-into', 'registration pattern', false],
31
+ ];
32
+
33
+ /** Modal over the graph area showing the edge-mechanism legend (moved out of
34
+ * the sidebar). Closes on backdrop click or Escape; renders nothing when
35
+ * closed. */
36
+ export function EdgeLegendModal({
37
+ open,
38
+ mechanisms,
39
+ onClose,
40
+ }: {
41
+ open: boolean;
42
+ mechanisms: Set<string>;
43
+ onClose: () => void;
44
+ }) {
45
+ const { theme } = useTheme();
46
+ const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
47
+
48
+ useEffect(() => {
49
+ if (!open) return;
50
+ const onKey = (e: KeyboardEvent) => {
51
+ if (e.key === 'Escape') onClose();
52
+ };
53
+ window.addEventListener('keydown', onKey);
54
+ return () => window.removeEventListener('keydown', onKey);
55
+ }, [open, onClose]);
56
+
57
+ if (!open) return null;
58
+
59
+ return (
60
+ <div
61
+ onClick={onClose}
62
+ style={{
63
+ position: 'absolute',
64
+ inset: 0,
65
+ zIndex: 20,
66
+ display: 'flex',
67
+ alignItems: 'center',
68
+ justifyContent: 'center',
69
+ background: 'rgba(0,0,0,0.5)',
70
+ }}
71
+ >
72
+ <div
73
+ onClick={(e) => e.stopPropagation()}
74
+ style={{
75
+ background: theme.colors.background,
76
+ border: `1px solid ${theme.colors.border}`,
77
+ borderRadius: 8,
78
+ boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
79
+ padding: '16px',
80
+ width: 420,
81
+ maxWidth: '90%',
82
+ maxHeight: '80%',
83
+ overflowY: 'auto',
84
+ fontFamily: theme.fonts.body,
85
+ }}
86
+ >
87
+ <div
88
+ style={{
89
+ display: 'flex',
90
+ alignItems: 'center',
91
+ justifyContent: 'space-between',
92
+ marginBottom: 12,
93
+ }}
94
+ >
95
+ <span
96
+ style={{
97
+ fontSize: theme.fontSizes[1],
98
+ fontWeight: 600,
99
+ color: theme.colors.text,
100
+ }}
101
+ >
102
+ Edge Legend
103
+ </span>
104
+ <button
105
+ type="button"
106
+ onClick={onClose}
107
+ aria-label="Close legend"
108
+ style={{
109
+ display: 'inline-flex',
110
+ alignItems: 'center',
111
+ justifyContent: 'center',
112
+ width: 22,
113
+ height: 22,
114
+ padding: 0,
115
+ border: 'none',
116
+ borderRadius: 4,
117
+ background: 'transparent',
118
+ color: theme.colors.text,
119
+ cursor: 'pointer',
120
+ }}
121
+ >
122
+ <X size={14} />
123
+ </button>
124
+ </div>
125
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
126
+ {MECHANISM_DESCRIPTIONS.filter(([m]) => mechanisms.has(m)).map(([mechanism, desc, verifiable]) => (
127
+ <div key={mechanism} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: theme.fontSizes[0] }}>
128
+ <span
129
+ style={{
130
+ width: 10,
131
+ height: 10,
132
+ borderRadius: verifiable ? '50%' : '30% 70% 70% 30% / 30% 30% 70% 70%',
133
+ background: MECHANISM_COLOR[mechanism],
134
+ border: verifiable ? 'none' : `1px dashed ${MECHANISM_COLOR[mechanism]}`,
135
+ flexShrink: 0,
136
+ }}
137
+ />
138
+ <span style={{ fontFamily: theme.fonts.monospace, color: MECHANISM_COLOR[mechanism], fontWeight: 500 }}>{mechanism}</span>
139
+ <span style={{ color: muted }}>{desc}</span>
140
+ </div>
141
+ ))}
142
+ </div>
143
+ </div>
144
+ </div>
145
+ );
146
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * FileDrawer — bottom panel that slides up from the bottom of the graph area
3
+ * to show a file's contents. Opened by sidebar file-tree clicks (and any host
4
+ * wiring); content is injected as children by the graph component.
5
+ */
6
+
7
+ import { useEffect } from 'react';
8
+ import type { ReactNode } from 'react';
9
+ import { useTheme } from '@principal-ade/industry-theme';
10
+ import { X } from 'lucide-react';
11
+
12
+ /** Bottom panel that slides up from the bottom of the graph area to show a
13
+ * file's contents — opened by node clicks and sidebar file-tree clicks
14
+ * alike. Sits in normal flow (canvas shrinks while open, nothing covered)
15
+ * and animates via height; stays mounted so open/close animates. */
16
+ export function FileDrawer({
17
+ file,
18
+ onClose,
19
+ children,
20
+ }: {
21
+ file: string | null;
22
+ onClose: () => void;
23
+ children?: ReactNode;
24
+ }) {
25
+ const { theme } = useTheme();
26
+ const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
27
+ const open = file !== null;
28
+
29
+ useEffect(() => {
30
+ if (!open) return;
31
+ const onKey = (e: KeyboardEvent) => {
32
+ if (e.key === 'Escape') onClose();
33
+ };
34
+ window.addEventListener('keydown', onKey);
35
+ return () => window.removeEventListener('keydown', onKey);
36
+ }, [open, onClose]);
37
+
38
+ return (
39
+ <div
40
+ style={{
41
+ position: 'relative',
42
+ // Above the absolute edge-label overlay (zIndex 5), which spans the
43
+ // whole graph-area container including this panel's slice.
44
+ zIndex: 6,
45
+ flexShrink: 0,
46
+ height: open ? '45%' : 0,
47
+ minHeight: 0,
48
+ overflow: 'hidden',
49
+ display: 'flex',
50
+ flexDirection: 'column',
51
+ background: theme.colors.background,
52
+ borderTop: open ? `1px solid ${theme.colors.border}` : 'none',
53
+ transition: 'height 200ms ease',
54
+ }}
55
+ >
56
+ <div
57
+ style={{
58
+ display: 'flex',
59
+ alignItems: 'center',
60
+ gap: 8,
61
+ padding: '6px 10px',
62
+ borderBottom: `1px solid ${theme.colors.border}`,
63
+ flexShrink: 0,
64
+ }}
65
+ >
66
+ <span
67
+ title={file ?? undefined}
68
+ style={{
69
+ flex: 1,
70
+ minWidth: 0,
71
+ overflow: 'hidden',
72
+ textOverflow: 'ellipsis',
73
+ whiteSpace: 'nowrap',
74
+ fontFamily: theme.fonts.monospace,
75
+ fontSize: theme.fontSizes[0],
76
+ color: muted,
77
+ }}
78
+ >
79
+ {file}
80
+ </span>
81
+ <button
82
+ type="button"
83
+ onClick={onClose}
84
+ aria-label="Close file"
85
+ style={{
86
+ display: 'inline-flex',
87
+ alignItems: 'center',
88
+ justifyContent: 'center',
89
+ width: 22,
90
+ height: 22,
91
+ padding: 0,
92
+ border: 'none',
93
+ borderRadius: 4,
94
+ background: 'transparent',
95
+ color: theme.colors.text,
96
+ cursor: 'pointer',
97
+ }}
98
+ >
99
+ <X size={14} />
100
+ </button>
101
+ </div>
102
+ <div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>{children}</div>
103
+ </div>
104
+ );
105
+ }