@principal-ai/principal-view-react 0.16.34 → 0.16.35
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/ComponentDetail.d.ts +12 -0
- package/dist/subsystem/ComponentDetail.d.ts.map +1 -0
- package/dist/subsystem/ComponentDetail.js +95 -0
- package/dist/subsystem/ComponentDetail.js.map +1 -0
- package/dist/subsystem/EdgeLegendModal.d.ts +18 -0
- package/dist/subsystem/EdgeLegendModal.d.ts.map +1 -0
- package/dist/subsystem/EdgeLegendModal.js +98 -0
- package/dist/subsystem/EdgeLegendModal.js.map +1 -0
- package/dist/subsystem/FileDrawer.d.ts +16 -0
- package/dist/subsystem/FileDrawer.d.ts.map +1 -0
- package/dist/subsystem/FileDrawer.js +72 -0
- package/dist/subsystem/FileDrawer.js.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts +0 -3
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +28 -190
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/model.d.ts +0 -3
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +2 -2
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +3 -21
- package/dist/subsystem/nodes.js.map +1 -1
- package/package.json +1 -1
- package/src/stories/SubsystemComponentGraph.stories.tsx +10 -10
- package/src/subsystem/ComponentDetail.tsx +187 -0
- package/src/subsystem/EdgeLegendModal.tsx +146 -0
- package/src/subsystem/FileDrawer.tsx +105 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +42 -327
- package/src/subsystem/model.ts +0 -3
- package/src/subsystem/nodes.tsx +2 -29
|
@@ -77,7 +77,7 @@ function TwoNodeDemo({ showEdgeLabels = true }: { showEdgeLabels?: boolean }) {
|
|
|
77
77
|
onSelect={(id) => setSelected(id)}
|
|
78
78
|
onEdgeSelect={(e) => setSelectedEdge(e)}
|
|
79
79
|
showEdgeLabels={showEdgeLabels}
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
/>
|
|
82
82
|
<div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
|
|
83
83
|
{selectedEdge
|
|
@@ -136,7 +136,7 @@ function PlaygroundDemo({ leftCount, rightCount, showEdgeLabels }: { leftCount:
|
|
|
136
136
|
|
|
137
137
|
return (
|
|
138
138
|
<div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
139
|
-
<SubsystemComponentGraph components={comps} edges={edgeList} showEdgeLabels={showEdgeLabels}
|
|
139
|
+
<SubsystemComponentGraph components={comps} edges={edgeList} showEdgeLabels={showEdgeLabels} />
|
|
140
140
|
</div>
|
|
141
141
|
);
|
|
142
142
|
}
|
|
@@ -201,7 +201,7 @@ function V2ReaderDemo() {
|
|
|
201
201
|
edges={v2ReaderEdges}
|
|
202
202
|
onSelect={(id) => setSelected(id)}
|
|
203
203
|
onEdgeSelect={(e) => setSelectedEdge(e)}
|
|
204
|
-
|
|
204
|
+
|
|
205
205
|
/>
|
|
206
206
|
<div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
|
|
207
207
|
{selectedEdge
|
|
@@ -312,7 +312,7 @@ const investigateOnlyEdges = edges([
|
|
|
312
312
|
export const InvestigateOnly: Story = {
|
|
313
313
|
render: () => (
|
|
314
314
|
<div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
315
|
-
<SubsystemComponentGraph components={investigateOnlyComponents} edges={investigateOnlyEdges}
|
|
315
|
+
<SubsystemComponentGraph components={investigateOnlyComponents} edges={investigateOnlyEdges} />
|
|
316
316
|
</div>
|
|
317
317
|
),
|
|
318
318
|
};
|
|
@@ -325,7 +325,7 @@ export const NarrowMaxWidth: Story = {
|
|
|
325
325
|
components={investigateOnlyComponents}
|
|
326
326
|
edges={investigateOnlyEdges}
|
|
327
327
|
maxNodeWidth={140}
|
|
328
|
-
|
|
328
|
+
|
|
329
329
|
/>
|
|
330
330
|
</div>
|
|
331
331
|
),
|
|
@@ -394,7 +394,7 @@ export const NamingConventions: Story = {
|
|
|
394
394
|
components={namingConventionComponents}
|
|
395
395
|
edges={[]}
|
|
396
396
|
maxNodeWidth={180}
|
|
397
|
-
|
|
397
|
+
|
|
398
398
|
/>
|
|
399
399
|
</div>
|
|
400
400
|
),
|
|
@@ -509,7 +509,7 @@ function DetailKindsDemo() {
|
|
|
509
509
|
components={detailKindComponents}
|
|
510
510
|
edges={[]}
|
|
511
511
|
onSelect={(id) => setSelected(id)}
|
|
512
|
-
|
|
512
|
+
|
|
513
513
|
/>
|
|
514
514
|
<div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
|
|
515
515
|
{selected ? `selected: ${selected}` : 'click a component to see its GraphifyComponentDetail'}
|
|
@@ -585,7 +585,7 @@ function MinimalVsResolvedDemo({ resolved }: { resolved: boolean }) {
|
|
|
585
585
|
components={resolved ? resolvedComponents : minimalComponents}
|
|
586
586
|
edges={sharedEdges}
|
|
587
587
|
onSelect={(id) => setSelected(id)}
|
|
588
|
-
|
|
588
|
+
|
|
589
589
|
/>
|
|
590
590
|
<div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
|
|
591
591
|
{resolved
|
|
@@ -662,7 +662,7 @@ function InvestigationDemo() {
|
|
|
662
662
|
components={investigationComponents}
|
|
663
663
|
edges={investigationEdges}
|
|
664
664
|
onSelect={(id) => setSelected(id)}
|
|
665
|
-
|
|
665
|
+
|
|
666
666
|
/>
|
|
667
667
|
<div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
|
|
668
668
|
read-only investigation snapshot (grok 019fd2a9) — components analyzed, not edited
|
|
@@ -754,7 +754,7 @@ function MermaidDemo() {
|
|
|
754
754
|
components={mermaidComponents}
|
|
755
755
|
edges={mermaidEdges}
|
|
756
756
|
onSelect={(id) => setSelected(id)}
|
|
757
|
-
|
|
757
|
+
|
|
758
758
|
/>
|
|
759
759
|
<div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
|
|
760
760
|
mermaid diagram rendering pipeline — lazy → render → zoom → modal
|
|
@@ -0,0 +1,187 @@
|
|
|
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.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ReactNode } from 'react';
|
|
8
|
+
import { useTheme } from '@principal-ade/industry-theme';
|
|
9
|
+
import {
|
|
10
|
+
KIND_COLOR,
|
|
11
|
+
deriveNameFromSymbol,
|
|
12
|
+
type SubsystemComponent,
|
|
13
|
+
} from './model';
|
|
14
|
+
import type { GraphifyComponentDetail } from '../graphify';
|
|
15
|
+
|
|
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]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
style={{
|
|
32
|
+
borderTop: `1px solid ${theme.colors.border}`,
|
|
33
|
+
background: theme.colors.background,
|
|
34
|
+
padding: '10px 12px',
|
|
35
|
+
fontFamily: theme.fonts.body,
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
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>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Renders the graphify drill-down payload for a component. */
|
|
88
|
+
function GraphifyDetailSections({ detail }: { detail: GraphifyComponentDetail }) {
|
|
89
|
+
const { theme } = useTheme();
|
|
90
|
+
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
91
|
+
|
|
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>
|
|
105
|
+
);
|
|
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>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
|
|
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
|
+
</>
|
|
149
|
+
);
|
|
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
|
+
</>
|
|
161
|
+
);
|
|
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
|
+
</>
|
|
172
|
+
);
|
|
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
|
+
</>
|
|
183
|
+
);
|
|
184
|
+
case 'external':
|
|
185
|
+
return row('source', <span>{detail.label}</span>);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -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
|
+
}
|