@principal-ai/principal-view-react 0.16.61 → 0.16.63
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 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreThroughlineCodeView.d.ts +19 -0
- package/dist/pierre/PierreThroughlineCodeView.d.ts.map +1 -0
- package/dist/pierre/PierreThroughlineCodeView.js +139 -0
- package/dist/pierre/PierreThroughlineCodeView.js.map +1 -0
- package/dist/pierre/index.d.ts +2 -0
- package/dist/pierre/index.d.ts.map +1 -1
- package/dist/pierre/index.js +1 -0
- package/dist/pierre/index.js.map +1 -1
- package/dist/subsystem/EdgeLegendModal.d.ts.map +1 -1
- package/dist/subsystem/EdgeLegendModal.js +3 -0
- package/dist/subsystem/EdgeLegendModal.js.map +1 -1
- package/dist/subsystem/FileDrawer.d.ts +8 -7
- package/dist/subsystem/FileDrawer.d.ts.map +1 -1
- package/dist/subsystem/FileDrawer.js +9 -9
- package/dist/subsystem/FileDrawer.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +17 -5
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +76 -21
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/nodes.js +3 -3
- package/dist/subsystem/nodes.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +5 -2
- package/src/pierre/PierreThroughlineCodeView.tsx +210 -0
- package/src/pierre/index.ts +2 -0
- package/src/stories/Pierre/CodeView.stories.tsx +196 -0
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +62 -4
- package/src/subsystem/EdgeLegendModal.tsx +3 -0
- package/src/subsystem/FileDrawer.tsx +11 -10
- package/src/subsystem/SubsystemComponentGraph.tsx +121 -34
- package/src/subsystem/nodes.tsx +3 -3
|
@@ -59,6 +59,17 @@ const EDGE_LABEL_MAX_EDGE_FRACTION = 0.55;
|
|
|
59
59
|
const EDGE_LABEL_CHAR_PX = 6.2;
|
|
60
60
|
const EDGE_LABEL_PAD_PX = 18;
|
|
61
61
|
|
|
62
|
+
/** Context passed to `renderThroughlineViewer` when a flow/step is focused. */
|
|
63
|
+
export interface ThroughlineViewerContext {
|
|
64
|
+
throughline: SubsystemThroughline;
|
|
65
|
+
/** Focused step index; `null` means the whole flow (no specific step). */
|
|
66
|
+
stepIndex: number | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type DrawerTarget =
|
|
70
|
+
| { kind: 'file'; file: string; startLine?: number }
|
|
71
|
+
| { kind: 'throughline'; throughlineId: string; stepIndex: number | null };
|
|
72
|
+
|
|
62
73
|
export interface SubsystemComponentGraphProps {
|
|
63
74
|
components: SubsystemComponent[];
|
|
64
75
|
edges: SubsystemComponentEdge[];
|
|
@@ -67,8 +78,9 @@ export interface SubsystemComponentGraphProps {
|
|
|
67
78
|
* flow. When present the sidebar's bottom half offers a Files/Flows toggle:
|
|
68
79
|
* the flows panel lists each throughline's steps (`symbol` or `file:line`);
|
|
69
80
|
* clicking a flow row toggles its steps; clicking a step focuses that
|
|
70
|
-
* step's edge
|
|
71
|
-
*
|
|
81
|
+
* step's edge and (when `renderThroughlineViewer` is set) opens the bottom
|
|
82
|
+
* drawer on that flow's snippets. Opened flows stay on the canvas
|
|
83
|
+
* (unselected ones dimmed); everything else is hidden.
|
|
72
84
|
*/
|
|
73
85
|
throughlines?: SubsystemThroughline[];
|
|
74
86
|
onSelect?: (componentId: string) => void;
|
|
@@ -105,11 +117,16 @@ export interface SubsystemComponentGraphProps {
|
|
|
105
117
|
sidebarAfterDescription?: ReactNode;
|
|
106
118
|
/**
|
|
107
119
|
* Host-injected reader/renderer for the bottom file drawer, keyed by
|
|
108
|
-
* repo-root-relative path. Opening happens on
|
|
109
|
-
*
|
|
110
|
-
* code-view dependencies.
|
|
120
|
+
* repo-root-relative path. Opening happens on declaration/file-tree clicks.
|
|
121
|
+
* Keeps this package free of fs and code-view dependencies.
|
|
111
122
|
*/
|
|
112
123
|
renderFileViewer?: (file: string, opts?: SubsystemOpenFileOptions) => ReactNode;
|
|
124
|
+
/**
|
|
125
|
+
* Host-injected multi-snippet viewer for a focused throughline. When set,
|
|
126
|
+
* clicking a flow step (or the whole flow) opens the bottom drawer with
|
|
127
|
+
* this content and updates it as the focused step changes.
|
|
128
|
+
*/
|
|
129
|
+
renderThroughlineViewer?: (ctx: ThroughlineViewerContext) => ReactNode;
|
|
113
130
|
/**
|
|
114
131
|
* Legacy component-keyed variant, kept for backward compatibility. When
|
|
115
132
|
* `renderFileViewer` is absent, drawer content resolves via the first
|
|
@@ -136,28 +153,39 @@ const edgeTypes: EdgeTypes = {
|
|
|
136
153
|
'subsystem-edge': SubsystemEdge,
|
|
137
154
|
};
|
|
138
155
|
|
|
139
|
-
// Memoized drawer body: only rebuilds children when the open
|
|
156
|
+
// Memoized drawer body: only rebuilds children when the open target changes.
|
|
140
157
|
// Inner re-renders on every viewport pan/zoom and hover; recreating host
|
|
141
158
|
// elements then would churn their readFile closures and flash the file
|
|
142
159
|
// viewer's loading state on each render.
|
|
143
|
-
const
|
|
160
|
+
const FileDrawerContent = memo(function FileDrawerContent({
|
|
144
161
|
render,
|
|
145
162
|
file,
|
|
146
163
|
startLine,
|
|
147
164
|
}: {
|
|
148
165
|
render: (file: string, opts?: SubsystemOpenFileOptions) => ReactNode;
|
|
149
|
-
file: string
|
|
166
|
+
file: string;
|
|
150
167
|
startLine?: number;
|
|
151
168
|
}) {
|
|
152
|
-
if (!file) return null;
|
|
153
169
|
return <>{render(file, startLine != null ? { startLine } : undefined)}</>;
|
|
154
170
|
});
|
|
155
171
|
|
|
172
|
+
const ThroughlineDrawerContent = memo(function ThroughlineDrawerContent({
|
|
173
|
+
render,
|
|
174
|
+
throughline,
|
|
175
|
+
stepIndex,
|
|
176
|
+
}: {
|
|
177
|
+
render: (ctx: ThroughlineViewerContext) => ReactNode;
|
|
178
|
+
throughline: SubsystemThroughline;
|
|
179
|
+
stepIndex: number | null;
|
|
180
|
+
}) {
|
|
181
|
+
return <>{render({ throughline, stepIndex })}</>;
|
|
182
|
+
});
|
|
183
|
+
|
|
156
184
|
interface InnerProps extends SubsystemComponentGraphProps {
|
|
157
185
|
measured: { w: number; h: number } | null;
|
|
158
186
|
}
|
|
159
187
|
|
|
160
|
-
function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showLegend, title, hideSidebar, graphTitle, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, onFileSelect, onVerifyComponent, componentVerification }: InnerProps) {
|
|
188
|
+
function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measured: _measured, maxNodeWidth, showEdgeLabels, showLegend, title, hideSidebar, graphTitle, description, canvasOverlay, sidebarExtra, sidebarAfterDescription, renderFileView, renderFileViewer, renderThroughlineViewer, onFileSelect, onVerifyComponent, componentVerification }: InnerProps) {
|
|
161
189
|
const { theme } = useTheme();
|
|
162
190
|
const { fitView } = useReactFlow();
|
|
163
191
|
const viewport = useViewport();
|
|
@@ -167,13 +195,8 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
167
195
|
});
|
|
168
196
|
const [layoutReady, setLayoutReady] = useState(false);
|
|
169
197
|
const [selected, setSelected] = useState<SubsystemComponent | null>(null);
|
|
170
|
-
/**
|
|
171
|
-
const [
|
|
172
|
-
file: string;
|
|
173
|
-
startLine?: number;
|
|
174
|
-
} | null>(null);
|
|
175
|
-
const openFile = openFileTarget?.file ?? null;
|
|
176
|
-
const openFileStartLine = openFileTarget?.startLine;
|
|
198
|
+
/** Bottom drawer: single file or throughline multi-snippet mode. */
|
|
199
|
+
const [drawerTarget, setDrawerTarget] = useState<DrawerTarget | null>(null);
|
|
177
200
|
// Edge-legend modal visibility (opened from the canvas's top-left button).
|
|
178
201
|
const [legendOpen, setLegendOpen] = useState(false);
|
|
179
202
|
// Component the pointer is over (null on leave) → transient tree highlight.
|
|
@@ -194,9 +217,31 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
194
217
|
// closure over the effect deps) can toggle without a stale value.
|
|
195
218
|
const selectedRef = useRef<SubsystemComponent | null>(null);
|
|
196
219
|
selectedRef.current = selected;
|
|
197
|
-
// Ref mirror of
|
|
220
|
+
// Ref mirror of the open file drawer target for tree-click toggle.
|
|
198
221
|
const openFileRef = useRef<{ file: string; startLine?: number } | null>(null);
|
|
199
|
-
|
|
222
|
+
const openFile =
|
|
223
|
+
drawerTarget?.kind === 'file' ? drawerTarget.file : null;
|
|
224
|
+
openFileRef.current =
|
|
225
|
+
drawerTarget?.kind === 'file'
|
|
226
|
+
? { file: drawerTarget.file, startLine: drawerTarget.startLine }
|
|
227
|
+
: null;
|
|
228
|
+
|
|
229
|
+
const focusedThroughline = useMemo(() => {
|
|
230
|
+
if (drawerTarget?.kind !== 'throughline' || !throughlines) return null;
|
|
231
|
+
return throughlines.find((t) => t.id === drawerTarget.throughlineId) ?? null;
|
|
232
|
+
}, [drawerTarget, throughlines]);
|
|
233
|
+
|
|
234
|
+
const drawerTitle = useMemo(() => {
|
|
235
|
+
if (!drawerTarget) return null;
|
|
236
|
+
if (drawerTarget.kind === 'file') return drawerTarget.file;
|
|
237
|
+
const tl = focusedThroughline;
|
|
238
|
+
if (!tl) return null;
|
|
239
|
+
if (drawerTarget.stepIndex == null) return tl.title;
|
|
240
|
+
const step = tl.steps[drawerTarget.stepIndex];
|
|
241
|
+
if (!step) return tl.title;
|
|
242
|
+
const site = `${step.file.split('/').pop() ?? step.file}:${step.line}`;
|
|
243
|
+
return `${tl.title} · ${site}`;
|
|
244
|
+
}, [drawerTarget, focusedThroughline]);
|
|
200
245
|
|
|
201
246
|
// Refresh selected component when the components list updates (e.g. verify
|
|
202
247
|
// writes back declarationRef).
|
|
@@ -594,10 +639,10 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
594
639
|
const onTreeSelectFile = useCallback(
|
|
595
640
|
(file: string) => {
|
|
596
641
|
if (openFileRef.current?.file === file && openFileRef.current.startLine == null) {
|
|
597
|
-
|
|
642
|
+
setDrawerTarget(null);
|
|
598
643
|
return;
|
|
599
644
|
}
|
|
600
|
-
|
|
645
|
+
setDrawerTarget({ kind: 'file', file });
|
|
601
646
|
onFileSelect?.(file);
|
|
602
647
|
},
|
|
603
648
|
[onFileSelect],
|
|
@@ -610,10 +655,10 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
610
655
|
openFileRef.current?.file === file &&
|
|
611
656
|
openFileRef.current.startLine === startLine
|
|
612
657
|
) {
|
|
613
|
-
|
|
658
|
+
setDrawerTarget(null);
|
|
614
659
|
return;
|
|
615
660
|
}
|
|
616
|
-
|
|
661
|
+
setDrawerTarget({ kind: 'file', file, startLine });
|
|
617
662
|
onFileSelect?.(file);
|
|
618
663
|
},
|
|
619
664
|
[onFileSelect],
|
|
@@ -643,7 +688,7 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
643
688
|
|
|
644
689
|
// Focus an entire flow: hide everything but the flow's nodes and edges, and
|
|
645
690
|
// frame the flow on the canvas. Selection state is cleared — the graph now
|
|
646
|
-
// reads as the narrative.
|
|
691
|
+
// reads as the narrative. Opens the throughline drawer when a viewer is set.
|
|
647
692
|
const focusThroughlineEdges = useCallback(
|
|
648
693
|
(tl: SubsystemThroughline) => {
|
|
649
694
|
setSelected(null);
|
|
@@ -651,17 +696,28 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
651
696
|
setFocusedStepIndex(null);
|
|
652
697
|
setFocusedThroughlineId(tl.id);
|
|
653
698
|
fitFocusBounds(new Set(tl.steps.map((s) => s.edgeId)));
|
|
699
|
+
if (renderThroughlineViewer) {
|
|
700
|
+
setDrawerTarget({ kind: 'throughline', throughlineId: tl.id, stepIndex: null });
|
|
701
|
+
} else if (tl.steps[0]) {
|
|
702
|
+
// Fallback: single-file snippet of the first step.
|
|
703
|
+
setDrawerTarget({
|
|
704
|
+
kind: 'file',
|
|
705
|
+
file: tl.steps[0].file,
|
|
706
|
+
startLine: tl.steps[0].line,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
654
709
|
},
|
|
655
|
-
[fitFocusBounds],
|
|
710
|
+
[fitFocusBounds, renderThroughlineViewer],
|
|
656
711
|
);
|
|
657
712
|
|
|
658
713
|
const clearThroughlineFocus = useCallback(() => {
|
|
659
714
|
setFocusedThroughlineId(null);
|
|
660
715
|
setFocusedStepIndex(null);
|
|
716
|
+
setDrawerTarget((prev) => (prev?.kind === 'throughline' ? null : prev));
|
|
661
717
|
}, []);
|
|
662
718
|
|
|
663
|
-
// Focus a single step's edge on the canvas
|
|
664
|
-
//
|
|
719
|
+
// Focus a single step's edge on the canvas and open/scroll the throughline
|
|
720
|
+
// drawer to that step's snippet.
|
|
665
721
|
const focusThroughlineStep = useCallback(
|
|
666
722
|
(tl: SubsystemThroughline, stepIndex: number) => {
|
|
667
723
|
const step = tl.steps[stepIndex];
|
|
@@ -671,8 +727,21 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
671
727
|
setFocusedStepIndex(stepIndex);
|
|
672
728
|
setFocusedThroughlineId(tl.id);
|
|
673
729
|
fitFocusBounds(new Set([step.edgeId]));
|
|
730
|
+
if (renderThroughlineViewer) {
|
|
731
|
+
setDrawerTarget({
|
|
732
|
+
kind: 'throughline',
|
|
733
|
+
throughlineId: tl.id,
|
|
734
|
+
stepIndex,
|
|
735
|
+
});
|
|
736
|
+
} else {
|
|
737
|
+
setDrawerTarget({
|
|
738
|
+
kind: 'file',
|
|
739
|
+
file: step.file,
|
|
740
|
+
startLine: step.line,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
674
743
|
},
|
|
675
|
-
[fitFocusBounds],
|
|
744
|
+
[fitFocusBounds, renderThroughlineViewer],
|
|
676
745
|
);
|
|
677
746
|
|
|
678
747
|
const toggleThroughlineCollapsed = useCallback((tlId: string) => {
|
|
@@ -781,6 +850,14 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
781
850
|
[],
|
|
782
851
|
);
|
|
783
852
|
|
|
853
|
+
const throughlineViewerRef = useRef(renderThroughlineViewer);
|
|
854
|
+
throughlineViewerRef.current = renderThroughlineViewer;
|
|
855
|
+
const renderThroughlineDrawerContent = useCallback(
|
|
856
|
+
(ctx: ThroughlineViewerContext) =>
|
|
857
|
+
throughlineViewerRef.current?.(ctx) ?? null,
|
|
858
|
+
[],
|
|
859
|
+
);
|
|
860
|
+
|
|
784
861
|
return (
|
|
785
862
|
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'row' }}>
|
|
786
863
|
{/* Sidebar: scrollable title/description on top, files or flows pinned to the bottom half */}
|
|
@@ -1157,12 +1234,22 @@ function Inner({ components, edges, throughlines, onSelect, onEdgeSelect, measur
|
|
|
1157
1234
|
mechanisms={usedMechanisms}
|
|
1158
1235
|
onClose={() => setLegendOpen(false)}
|
|
1159
1236
|
/>
|
|
1160
|
-
<FileDrawer
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1237
|
+
<FileDrawer title={drawerTitle} onClose={() => setDrawerTarget(null)}>
|
|
1238
|
+
{drawerTarget?.kind === 'throughline' &&
|
|
1239
|
+
focusedThroughline &&
|
|
1240
|
+
renderThroughlineViewer ? (
|
|
1241
|
+
<ThroughlineDrawerContent
|
|
1242
|
+
render={renderThroughlineDrawerContent}
|
|
1243
|
+
throughline={focusedThroughline}
|
|
1244
|
+
stepIndex={drawerTarget.stepIndex}
|
|
1245
|
+
/>
|
|
1246
|
+
) : drawerTarget?.kind === 'file' ? (
|
|
1247
|
+
<FileDrawerContent
|
|
1248
|
+
render={renderDrawerContent}
|
|
1249
|
+
file={drawerTarget.file}
|
|
1250
|
+
startLine={drawerTarget.startLine}
|
|
1251
|
+
/>
|
|
1252
|
+
) : null}
|
|
1166
1253
|
</FileDrawer>
|
|
1167
1254
|
{/* Startup cover — hides measurement, layout swap, and camera settle. */}
|
|
1168
1255
|
<GraphLayoutCover revealed={layoutReady} />
|
package/src/subsystem/nodes.tsx
CHANGED
|
@@ -322,10 +322,10 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
|
|
|
322
322
|
<div
|
|
323
323
|
style={{
|
|
324
324
|
position: 'absolute',
|
|
325
|
-
top: -
|
|
325
|
+
top: -19,
|
|
326
326
|
left: 12,
|
|
327
327
|
fontFamily: theme.fonts.monospace,
|
|
328
|
-
fontSize: theme.fontSizes[
|
|
328
|
+
fontSize: theme.fontSizes[3],
|
|
329
329
|
fontWeight: 700,
|
|
330
330
|
letterSpacing: 0.6,
|
|
331
331
|
color,
|
|
@@ -336,7 +336,7 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
|
|
|
336
336
|
whiteSpace: 'nowrap',
|
|
337
337
|
}}
|
|
338
338
|
>
|
|
339
|
-
{
|
|
339
|
+
{region.label}
|
|
340
340
|
</div>
|
|
341
341
|
</div>
|
|
342
342
|
);
|