@principal-ai/principal-view-react 0.16.32 → 0.16.34
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/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/subsystem/GraphLayoutCover.d.ts +19 -0
- package/dist/subsystem/GraphLayoutCover.d.ts.map +1 -0
- package/dist/subsystem/GraphLayoutCover.js +42 -0
- package/dist/subsystem/GraphLayoutCover.js.map +1 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts +18 -3
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +185 -34
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SubsystemFileTree.d.ts +20 -0
- package/dist/subsystem/SubsystemFileTree.d.ts.map +1 -0
- package/dist/subsystem/SubsystemFileTree.js +124 -0
- package/dist/subsystem/SubsystemFileTree.js.map +1 -0
- package/dist/subsystem/model.d.ts +7 -0
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/subsystem/nodes.d.ts +2 -0
- package/dist/subsystem/nodes.d.ts.map +1 -1
- package/dist/subsystem/nodes.js +21 -9
- package/dist/subsystem/nodes.js.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +4 -0
- package/src/stories/SubsystemComponentGraph.stories.tsx +22 -14
- package/src/subsystem/GraphLayoutCover.tsx +69 -0
- package/src/subsystem/SubsystemComponentGraph.tsx +246 -28
- package/src/subsystem/SubsystemFileTree.tsx +158 -0
- package/src/subsystem/model.ts +7 -0
- package/src/subsystem/nodes.tsx +26 -11
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubsystemFileTree — sidebar file tree for a subsystem's components, built
|
|
3
|
+
* on @pierre/trees following the panel repo's SessionFileTreePanel pattern:
|
|
4
|
+
* init-once options + latest-value refs, and a suppress flag around
|
|
5
|
+
* programmatic selection so `onSelectionChange` echoes don't re-open files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
9
|
+
import type { CSSProperties } from 'react';
|
|
10
|
+
import { FileTree, useFileTree } from '@pierre/trees/react';
|
|
11
|
+
import { useTheme } from '@principal-ade/industry-theme';
|
|
12
|
+
|
|
13
|
+
export interface SubsystemFileTreeProps {
|
|
14
|
+
/** Repo-root-relative file paths; duplicates are deduped. */
|
|
15
|
+
files: string[];
|
|
16
|
+
/** File to select/highlight (the drawer-open file). */
|
|
17
|
+
selectedFile?: string | null;
|
|
18
|
+
/** File to transiently highlight while its node is hovered on the graph;
|
|
19
|
+
* falls back to `selectedFile` when null. */
|
|
20
|
+
hoveredFile?: string | null;
|
|
21
|
+
/** Called when a file row is clicked; upstream toggles the drawer. */
|
|
22
|
+
onSelectFile?: (file: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Fills its parent height by design — pin it with a sized flex container. */
|
|
26
|
+
export function SubsystemFileTree({ files, selectedFile, hoveredFile, onSelectFile }: SubsystemFileTreeProps) {
|
|
27
|
+
const { theme } = useTheme();
|
|
28
|
+
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
29
|
+
const paths = useMemo(() => Array.from(new Set(files)).sort(), [files]);
|
|
30
|
+
|
|
31
|
+
// Latest-value refs so the stable `onSelectionChange` closure never goes
|
|
32
|
+
// stale — `useFileTree` reads its options once on init.
|
|
33
|
+
const pathSet = useMemo(() => new Set(paths), [paths]);
|
|
34
|
+
const pathSetRef = useRef(pathSet);
|
|
35
|
+
pathSetRef.current = pathSet;
|
|
36
|
+
const onSelectFileRef = useRef(onSelectFile);
|
|
37
|
+
onSelectFileRef.current = onSelectFile;
|
|
38
|
+
const selectedFileRef = useRef<string | null>(selectedFile ?? null);
|
|
39
|
+
selectedFileRef.current = selectedFile ?? null;
|
|
40
|
+
// Suppresses onSelectFile while selection is driven programmatically from
|
|
41
|
+
// the drawer state (`selectedFile`) — no synthetic open/close events.
|
|
42
|
+
const suppressSelectRef = useRef(false);
|
|
43
|
+
const initialPaths = useRef(paths);
|
|
44
|
+
|
|
45
|
+
const { model } = useFileTree({
|
|
46
|
+
paths: initialPaths.current,
|
|
47
|
+
initialExpansion: 'open',
|
|
48
|
+
onSelectionChange: (selected) => {
|
|
49
|
+
if (suppressSelectRef.current) return;
|
|
50
|
+
const raw = selected[selected.length - 1] ?? selected[0];
|
|
51
|
+
if (!raw) return;
|
|
52
|
+
// Directory rows carry a trailing slash; only emit for real files.
|
|
53
|
+
const path = raw.endsWith('/') ? raw.slice(0, -1) : raw;
|
|
54
|
+
if (!pathSetRef.current.has(path)) return;
|
|
55
|
+
// Includes re-clicks on the open row — upstream toggles it closed.
|
|
56
|
+
onSelectFileRef.current?.(path);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Reconcile the tree's native selection with the desired highlight: the
|
|
61
|
+
// hovered node's file while hovering, else the drawer-open file. Deselects
|
|
62
|
+
// the previous target and scrolls the new one into view.
|
|
63
|
+
const lastSyncedRef = useRef<string | null>(null);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const target = hoveredFile ?? selectedFile ?? null;
|
|
66
|
+
if (lastSyncedRef.current === target) return;
|
|
67
|
+
const prev = lastSyncedRef.current;
|
|
68
|
+
lastSyncedRef.current = target;
|
|
69
|
+
suppressSelectRef.current = true;
|
|
70
|
+
try {
|
|
71
|
+
if (prev) model.getItem(prev)?.deselect();
|
|
72
|
+
if (target) {
|
|
73
|
+
model.getItem(target)?.select();
|
|
74
|
+
model.scrollToPath(target);
|
|
75
|
+
}
|
|
76
|
+
} finally {
|
|
77
|
+
suppressSelectRef.current = false;
|
|
78
|
+
}
|
|
79
|
+
}, [model, selectedFile, hoveredFile]);
|
|
80
|
+
|
|
81
|
+
// Re-scope the tree in place when the subsystem's file set changes.
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
model.resetPaths(paths);
|
|
84
|
+
}, [model, paths]);
|
|
85
|
+
|
|
86
|
+
// Pierre emits no selection change when the clicked row is already
|
|
87
|
+
// selected — detect re-clicks on the open file's row via focus and let
|
|
88
|
+
// upstream toggle the drawer closed.
|
|
89
|
+
const handleContainerClick = () => {
|
|
90
|
+
const focused = model.getFocusedPath();
|
|
91
|
+
if (!focused) return;
|
|
92
|
+
const path = focused.endsWith('/') ? focused.slice(0, -1) : focused;
|
|
93
|
+
if (path !== selectedFileRef.current) return;
|
|
94
|
+
suppressSelectRef.current = true;
|
|
95
|
+
try {
|
|
96
|
+
onSelectFileRef.current?.(path);
|
|
97
|
+
} finally {
|
|
98
|
+
suppressSelectRef.current = false;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
onClickCapture={handleContainerClick}
|
|
105
|
+
style={{
|
|
106
|
+
height: '50%',
|
|
107
|
+
minHeight: 120,
|
|
108
|
+
flexShrink: 0,
|
|
109
|
+
borderTop: `1px solid ${theme.colors.border}`,
|
|
110
|
+
display: 'flex',
|
|
111
|
+
flexDirection: 'column',
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<div
|
|
115
|
+
style={{
|
|
116
|
+
padding: '10px 16px 4px',
|
|
117
|
+
display: 'flex',
|
|
118
|
+
alignItems: 'baseline',
|
|
119
|
+
gap: 6,
|
|
120
|
+
flexShrink: 0,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
<span
|
|
124
|
+
style={{
|
|
125
|
+
fontSize: theme.fontSizes[0] * 0.8,
|
|
126
|
+
fontFamily: theme.fonts.monospace,
|
|
127
|
+
textTransform: 'uppercase',
|
|
128
|
+
color: muted,
|
|
129
|
+
fontWeight: 600,
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
Files
|
|
133
|
+
</span>
|
|
134
|
+
<span style={{ fontSize: theme.fontSizes[0] * 0.8, fontFamily: theme.fonts.monospace, color: muted }}>
|
|
135
|
+
{paths.length}
|
|
136
|
+
</span>
|
|
137
|
+
</div>
|
|
138
|
+
<FileTree
|
|
139
|
+
model={model}
|
|
140
|
+
style={
|
|
141
|
+
{
|
|
142
|
+
flex: 1,
|
|
143
|
+
minHeight: 0,
|
|
144
|
+
'--trees-padding-inline-override': '8px',
|
|
145
|
+
'--trees-bg-override': 'transparent',
|
|
146
|
+
'--trees-fg-override': theme.colors.text,
|
|
147
|
+
'--trees-fg-muted-override': muted,
|
|
148
|
+
'--trees-accent-override': theme.colors.primary,
|
|
149
|
+
'--trees-font-family-override': theme.fonts.monospace,
|
|
150
|
+
'--trees-font-size-override': `${theme.fontSizes[0]}px`,
|
|
151
|
+
'--trees-theme-list-hover-bg': theme.colors.border,
|
|
152
|
+
'--trees-theme-list-active-selection-bg': theme.colors.border,
|
|
153
|
+
} as CSSProperties
|
|
154
|
+
}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
package/src/subsystem/model.ts
CHANGED
|
@@ -132,6 +132,13 @@ export type SubsystemGraphNodeType = 'subsystem-component' | 'subsystem-group';
|
|
|
132
132
|
|
|
133
133
|
export interface SubsystemGraphNodeData extends Record<string, unknown> {
|
|
134
134
|
component: SubsystemComponent;
|
|
135
|
+
/** Set while a file is open in the drawer: true if this node's component
|
|
136
|
+
* lives in that file (spotlighted), false otherwise (dimmed). Absent when
|
|
137
|
+
* no file is open — render neutrally. */
|
|
138
|
+
fileMatch?: boolean;
|
|
139
|
+
/** False hides the click-to-open file chip under the component name.
|
|
140
|
+
* Absent/true renders it (default). */
|
|
141
|
+
showFileBadges?: boolean;
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
export type SubsystemGraphNode = Node<SubsystemGraphNodeData, SubsystemGraphNodeType>;
|
package/src/subsystem/nodes.tsx
CHANGED
|
@@ -50,6 +50,8 @@ export interface SubsystemGraphCallbacks {
|
|
|
50
50
|
onSelect?: (componentId: string) => void;
|
|
51
51
|
/** Click an edge (or its label) — select the relationship. */
|
|
52
52
|
onEdgeSelect?: (edgeId: string) => void;
|
|
53
|
+
/** Hover a component (null on leave) — associates it with the file tree. */
|
|
54
|
+
onHover?: (componentId: string | null) => void;
|
|
53
55
|
/** Upper bound for node width; nodes grow with content up to this, then wrap. */
|
|
54
56
|
maxNodeWidth?: number;
|
|
55
57
|
}
|
|
@@ -67,11 +69,21 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
|
67
69
|
const maxWidth = configuredMax ?? 300;
|
|
68
70
|
// `symbol` is the source of truth; `name` is derived from it consistently.
|
|
69
71
|
const displayName = deriveNameFromSymbol(c.symbol, c.kind, c.name, c.file);
|
|
72
|
+
// Set while a file is open in the drawer: true → spotlight, false → dim,
|
|
73
|
+
// absent (no file open) → neutral.
|
|
74
|
+
const fileMatch = data.fileMatch as boolean | undefined;
|
|
75
|
+
const showFileBadges = data.showFileBadges !== false;
|
|
70
76
|
|
|
71
77
|
return (
|
|
72
78
|
<div
|
|
73
|
-
onMouseEnter={() =>
|
|
74
|
-
|
|
79
|
+
onMouseEnter={() => {
|
|
80
|
+
setHover(true);
|
|
81
|
+
SUBSYSTEM_CALLBACKS.onHover?.(c.id);
|
|
82
|
+
}}
|
|
83
|
+
onMouseLeave={() => {
|
|
84
|
+
setHover(false);
|
|
85
|
+
SUBSYSTEM_CALLBACKS.onHover?.(null);
|
|
86
|
+
}}
|
|
75
87
|
onClick={(e) => {
|
|
76
88
|
e.stopPropagation();
|
|
77
89
|
SUBSYSTEM_CALLBACKS.onSelect?.(c.id);
|
|
@@ -90,8 +102,12 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
|
90
102
|
padding: '6px 10px',
|
|
91
103
|
borderRadius: 8,
|
|
92
104
|
background: theme.colors.backgroundSecondary ?? theme.colors.background,
|
|
93
|
-
border: `2px solid ${selected ? theme.colors.primary : color}`,
|
|
94
|
-
boxShadow:
|
|
105
|
+
border: `2px solid ${selected || fileMatch ? theme.colors.primary : color}`,
|
|
106
|
+
boxShadow: fileMatch
|
|
107
|
+
? `0 1px 4px rgba(0,0,0,0.25), 0 0 12px ${theme.colors.primary}55`
|
|
108
|
+
: '0 1px 4px rgba(0,0,0,0.25)',
|
|
109
|
+
opacity: fileMatch === false ? 0.18 : 1,
|
|
110
|
+
transition: 'opacity 150ms ease',
|
|
95
111
|
cursor: 'pointer',
|
|
96
112
|
fontFamily: theme.fonts.body,
|
|
97
113
|
}}
|
|
@@ -110,7 +126,6 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
|
110
126
|
whiteSpace: 'nowrap',
|
|
111
127
|
fontSize: theme.fontSizes[0] * 0.8,
|
|
112
128
|
fontFamily: theme.fonts.monospace,
|
|
113
|
-
textTransform: 'uppercase',
|
|
114
129
|
letterSpacing: 0.5,
|
|
115
130
|
color,
|
|
116
131
|
background: theme.colors.background,
|
|
@@ -119,8 +134,10 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
|
119
134
|
padding: '1px 6px',
|
|
120
135
|
}}
|
|
121
136
|
>
|
|
122
|
-
{
|
|
123
|
-
|
|
137
|
+
<span style={{ textTransform: 'uppercase' }}>
|
|
138
|
+
{KIND_LABEL[c.kind] ?? c.kind}
|
|
139
|
+
</span>
|
|
140
|
+
{c.file ? ` · ${c.file.split('/').pop()}` : ''}
|
|
124
141
|
{c.capture && c.capture !== 'edited' ? ` · ${c.capture}` : ''}
|
|
125
142
|
</div>
|
|
126
143
|
)}
|
|
@@ -162,9 +179,7 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
|
162
179
|
whiteSpace: 'normal',
|
|
163
180
|
overflowWrap: 'anywhere',
|
|
164
181
|
maxWidth: '100%',
|
|
165
|
-
|
|
166
|
-
fontFamily:
|
|
167
|
-
c.kind === 'type' ? 'Georgia, "Times New Roman", serif' : theme.fonts.body,
|
|
182
|
+
fontFamily: theme.fonts.body,
|
|
168
183
|
}}
|
|
169
184
|
>
|
|
170
185
|
{breakWords(displayName)}
|
|
@@ -189,7 +204,7 @@ export function SubsystemComponentNode(props: NodeProps<SubsystemGraphNode>) {
|
|
|
189
204
|
</div>
|
|
190
205
|
)}
|
|
191
206
|
|
|
192
|
-
{c.file && (
|
|
207
|
+
{c.file && showFileBadges && (
|
|
193
208
|
<div
|
|
194
209
|
onClick={(e) => {
|
|
195
210
|
e.stopPropagation();
|