@principal-ai/principal-view-react 0.16.61 → 0.16.62
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/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/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/FileDrawer.tsx +11 -10
- package/src/subsystem/SubsystemComponentGraph.tsx +121 -34
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
2
|
import '@xyflow/react/dist/style.css';
|
|
3
3
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
4
|
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
SubsystemComponentEdge,
|
|
9
9
|
SubsystemThroughline,
|
|
10
10
|
} from '../../../subsystem/model';
|
|
11
|
+
import { PierreThroughlineCodeView } from '../../../pierre';
|
|
12
|
+
import type { ThroughlineViewerContext } from '../../../subsystem/SubsystemComponentGraph';
|
|
11
13
|
|
|
12
14
|
const meta = {
|
|
13
15
|
title: 'Subsystem/ComponentGraph/Flows',
|
|
@@ -32,10 +34,53 @@ type Story = StoryObj<typeof meta>;
|
|
|
32
34
|
// Mirror of the retrofitted electron-app drawing graph: the sidebar's Files
|
|
33
35
|
// panel swaps to a Flows panel listing three throughlines (open / save /
|
|
34
36
|
// delete). Clicking a flow row toggles its steps; clicking a step focuses
|
|
35
|
-
// that step's edge
|
|
36
|
-
// dimmed; the rest of the
|
|
37
|
+
// that step's edge, frames it on the canvas, and scrolls the bottom CodeView
|
|
38
|
+
// to that step's snippet. Other opened flows stay dimmed; the rest of the
|
|
39
|
+
// graph is hidden.
|
|
37
40
|
// ---------------------------------------------------------------------------
|
|
38
41
|
|
|
42
|
+
/** Build enough placeholder lines so step `line` values land inside the file. */
|
|
43
|
+
function fakeSource(path: string, markedLines: number[]): string {
|
|
44
|
+
const maxLine = Math.max(80, ...markedLines);
|
|
45
|
+
const marks = new Set(markedLines);
|
|
46
|
+
const lines: string[] = [`// ${path}`, ''];
|
|
47
|
+
for (let i = 3; i <= maxLine; i++) {
|
|
48
|
+
if (marks.has(i)) {
|
|
49
|
+
lines.push(`export function stepAtLine${i}() {`);
|
|
50
|
+
lines.push(` return ${i};`);
|
|
51
|
+
lines.push(`}`);
|
|
52
|
+
lines.push('');
|
|
53
|
+
} else {
|
|
54
|
+
lines.push(`// context line ${i}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const storyFiles: Record<string, string> = {
|
|
61
|
+
'src/panels/DrawingsLeftPanel.tsx': fakeSource('src/panels/DrawingsLeftPanel.tsx', [
|
|
62
|
+
56, 85,
|
|
63
|
+
]),
|
|
64
|
+
'src/hooks/useDrawingsHost.ts': fakeSource('src/hooks/useDrawingsHost.ts', [
|
|
65
|
+
45, 64, 66,
|
|
66
|
+
]),
|
|
67
|
+
'src/workspace/WorkspaceShell.tsx': fakeSource('src/workspace/WorkspaceShell.tsx', [
|
|
68
|
+
369, 372,
|
|
69
|
+
]),
|
|
70
|
+
'src/storage/drawingsStorage.ts': fakeSource('src/storage/drawingsStorage.ts', [90]),
|
|
71
|
+
'src/components/DrawingTabContent.tsx': fakeSource('src/components/DrawingTabContent.tsx', [
|
|
72
|
+
83, 112, 121,
|
|
73
|
+
]),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function readStoryFile(path: string): Promise<string> {
|
|
77
|
+
const content = storyFiles[path];
|
|
78
|
+
if (content == null) {
|
|
79
|
+
return Promise.reject(new Error(`No story fixture for ${path}`));
|
|
80
|
+
}
|
|
81
|
+
return Promise.resolve(content);
|
|
82
|
+
}
|
|
83
|
+
|
|
39
84
|
const drawingComponents: SubsystemComponent[] = [
|
|
40
85
|
{
|
|
41
86
|
id: 'panel',
|
|
@@ -148,6 +193,18 @@ const drawingThroughlines: SubsystemThroughline[] = [
|
|
|
148
193
|
];
|
|
149
194
|
|
|
150
195
|
function FlowsDemo() {
|
|
196
|
+
const renderThroughlineViewer = useCallback(
|
|
197
|
+
({ throughline, stepIndex }: ThroughlineViewerContext) => (
|
|
198
|
+
<PierreThroughlineCodeView
|
|
199
|
+
throughline={throughline}
|
|
200
|
+
stepIndex={stepIndex}
|
|
201
|
+
readFile={readStoryFile}
|
|
202
|
+
contextLines={4}
|
|
203
|
+
/>
|
|
204
|
+
),
|
|
205
|
+
[],
|
|
206
|
+
);
|
|
207
|
+
|
|
151
208
|
return (
|
|
152
209
|
<div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
153
210
|
<SubsystemComponentGraph
|
|
@@ -155,7 +212,8 @@ function FlowsDemo() {
|
|
|
155
212
|
edges={drawingEdges}
|
|
156
213
|
throughlines={drawingThroughlines}
|
|
157
214
|
title="drawing-files flow"
|
|
158
|
-
description="Three throughlines over one graph — opening, saving, and deleting a drawing. The sidebar's **Flows** panel lists each step by **symbol
|
|
215
|
+
description="Three throughlines over one graph — opening, saving, and deleting a drawing. The sidebar's **Flows** panel lists each step by **symbol**; clicking a step focuses that edge and scrolls the bottom CodeView to that snippet."
|
|
216
|
+
renderThroughlineViewer={renderThroughlineViewer}
|
|
159
217
|
renderFileViewer={(file, opts) => (
|
|
160
218
|
<div
|
|
161
219
|
style={{
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FileDrawer — bottom panel that slides up from the bottom of the graph area
|
|
3
|
-
* to show
|
|
4
|
-
*
|
|
3
|
+
* to show file / throughline code. Opened by sidebar file-tree clicks,
|
|
4
|
+
* declaration links, and throughline step focus; content is injected as
|
|
5
|
+
* children by the graph component.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { useEffect } from 'react';
|
|
@@ -9,22 +10,22 @@ import type { ReactNode } from 'react';
|
|
|
9
10
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
10
11
|
import { X } from 'lucide-react';
|
|
11
12
|
|
|
12
|
-
/** Bottom panel that slides up from the bottom of the graph area
|
|
13
|
-
*
|
|
14
|
-
* alike. Sits in normal flow (canvas shrinks while open, nothing covered)
|
|
13
|
+
/** Bottom panel that slides up from the bottom of the graph area.
|
|
14
|
+
* Sits in normal flow (canvas shrinks while open, nothing covered)
|
|
15
15
|
* and animates via height; stays mounted so open/close animates. */
|
|
16
16
|
export function FileDrawer({
|
|
17
|
-
|
|
17
|
+
title,
|
|
18
18
|
onClose,
|
|
19
19
|
children,
|
|
20
20
|
}: {
|
|
21
|
-
|
|
21
|
+
/** Drawer chrome title; `null` closes the drawer. */
|
|
22
|
+
title: string | null;
|
|
22
23
|
onClose: () => void;
|
|
23
24
|
children?: ReactNode;
|
|
24
25
|
}) {
|
|
25
26
|
const { theme } = useTheme();
|
|
26
27
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
27
|
-
const open =
|
|
28
|
+
const open = title !== null;
|
|
28
29
|
|
|
29
30
|
useEffect(() => {
|
|
30
31
|
if (!open) return;
|
|
@@ -64,7 +65,7 @@ export function FileDrawer({
|
|
|
64
65
|
}}
|
|
65
66
|
>
|
|
66
67
|
<span
|
|
67
|
-
title={
|
|
68
|
+
title={title ?? undefined}
|
|
68
69
|
style={{
|
|
69
70
|
flex: 1,
|
|
70
71
|
minWidth: 0,
|
|
@@ -76,7 +77,7 @@ export function FileDrawer({
|
|
|
76
77
|
color: muted,
|
|
77
78
|
}}
|
|
78
79
|
>
|
|
79
|
-
{
|
|
80
|
+
{title}
|
|
80
81
|
</span>
|
|
81
82
|
<button
|
|
82
83
|
type="button"
|
|
@@ -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} />
|