@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.
Files changed (35) hide show
  1. package/dist/index.d.ts +4 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/pierre/PierreThroughlineCodeView.d.ts +19 -0
  6. package/dist/pierre/PierreThroughlineCodeView.d.ts.map +1 -0
  7. package/dist/pierre/PierreThroughlineCodeView.js +139 -0
  8. package/dist/pierre/PierreThroughlineCodeView.js.map +1 -0
  9. package/dist/pierre/index.d.ts +2 -0
  10. package/dist/pierre/index.d.ts.map +1 -1
  11. package/dist/pierre/index.js +1 -0
  12. package/dist/pierre/index.js.map +1 -1
  13. package/dist/subsystem/EdgeLegendModal.d.ts.map +1 -1
  14. package/dist/subsystem/EdgeLegendModal.js +3 -0
  15. package/dist/subsystem/EdgeLegendModal.js.map +1 -1
  16. package/dist/subsystem/FileDrawer.d.ts +8 -7
  17. package/dist/subsystem/FileDrawer.d.ts.map +1 -1
  18. package/dist/subsystem/FileDrawer.js +9 -9
  19. package/dist/subsystem/FileDrawer.js.map +1 -1
  20. package/dist/subsystem/SubsystemComponentGraph.d.ts +17 -5
  21. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  22. package/dist/subsystem/SubsystemComponentGraph.js +76 -21
  23. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  24. package/dist/subsystem/nodes.js +3 -3
  25. package/dist/subsystem/nodes.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/index.ts +5 -2
  28. package/src/pierre/PierreThroughlineCodeView.tsx +210 -0
  29. package/src/pierre/index.ts +2 -0
  30. package/src/stories/Pierre/CodeView.stories.tsx +196 -0
  31. package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +62 -4
  32. package/src/subsystem/EdgeLegendModal.tsx +3 -0
  33. package/src/subsystem/FileDrawer.tsx +11 -10
  34. package/src/subsystem/SubsystemComponentGraph.tsx +121 -34
  35. package/src/subsystem/nodes.tsx +3 -3
@@ -0,0 +1,210 @@
1
+ /**
2
+ * PierreThroughlineCodeView — multi-file step snippets via `@pierre/diffs` CodeView.
3
+ *
4
+ * Renders one sliced window per throughline step in a single virtualized
5
+ * scroll; when `stepIndex` changes, scrolls that step into view.
6
+ */
7
+
8
+ import { useEffect, useMemo, useRef, useState, type ReactElement } from 'react';
9
+ import {
10
+ CodeView,
11
+ type CodeViewHandle,
12
+ type CodeViewItem,
13
+ type CodeViewReactOptions,
14
+ } from '@pierre/diffs/react';
15
+ import { useTheme } from '@principal-ade/industry-theme';
16
+ import type { SubsystemThroughline } from '../subsystem/model';
17
+ import { buildPierreOptions, PIERRE_FILE_STYLE } from './pierreBackground';
18
+ import { resolvePierreSyntaxThemeName } from './pierreSyntaxTheme';
19
+ import { sliceSnippetWindow } from './sliceSnippet';
20
+
21
+ export interface PierreThroughlineCodeViewProps {
22
+ throughline: SubsystemThroughline;
23
+ /** Focused step; `null` shows all snippets without scrolling to a step. */
24
+ stepIndex: number | null;
25
+ readFile: (path: string) => Promise<string>;
26
+ /** Context lines above/below each step site; defaults to 8. */
27
+ contextLines?: number;
28
+ /** Override Pierre's container background. */
29
+ background?: string;
30
+ }
31
+
32
+ type FileLoadState =
33
+ | { status: 'loading' }
34
+ | { status: 'error'; message: string }
35
+ | { status: 'ready'; byPath: Map<string, string> };
36
+
37
+ function stepItemId(throughlineId: string, index: number): string {
38
+ return `${throughlineId}:${index}`;
39
+ }
40
+
41
+ export function PierreThroughlineCodeView({
42
+ throughline,
43
+ stepIndex,
44
+ readFile,
45
+ contextLines = 8,
46
+ background,
47
+ }: PierreThroughlineCodeViewProps) {
48
+ const { theme, mode } = useTheme();
49
+ const viewRef = useRef<CodeViewHandle<undefined>>(null);
50
+ const [load, setLoad] = useState<FileLoadState>({ status: 'loading' });
51
+
52
+ const pathsKey = useMemo(() => {
53
+ const paths = [...new Set(throughline.steps.map((s) => s.file))];
54
+ paths.sort();
55
+ return paths.join('\0');
56
+ }, [throughline.steps]);
57
+
58
+ useEffect(() => {
59
+ let cancelled = false;
60
+ setLoad({ status: 'loading' });
61
+ const paths = [...new Set(throughline.steps.map((s) => s.file))];
62
+ void Promise.all(
63
+ paths.map(async (path) => {
64
+ const contents = await readFile(path);
65
+ return [path, contents] as const;
66
+ }),
67
+ )
68
+ .then((entries) => {
69
+ if (cancelled) return;
70
+ setLoad({ status: 'ready', byPath: new Map(entries) });
71
+ })
72
+ .catch((err) => {
73
+ if (cancelled) return;
74
+ setLoad({
75
+ status: 'error',
76
+ message: err instanceof Error ? err.message : 'Failed to read files',
77
+ });
78
+ });
79
+ return () => {
80
+ cancelled = true;
81
+ };
82
+ }, [throughline.id, pathsKey, readFile]);
83
+
84
+ const items = useMemo((): CodeViewItem[] => {
85
+ if (load.status !== 'ready') return [];
86
+ return throughline.steps.map((step, index) => {
87
+ const contents = load.byPath.get(step.file) ?? '';
88
+ const slice = sliceSnippetWindow(
89
+ contents,
90
+ step.line,
91
+ step.line,
92
+ contextLines,
93
+ step.line,
94
+ );
95
+ return {
96
+ id: stepItemId(throughline.id, index),
97
+ type: 'file' as const,
98
+ version: 1,
99
+ file: {
100
+ // Real path → Shiki language detection from extension.
101
+ name: step.file,
102
+ contents: slice.contents,
103
+ cacheKey: `${throughline.id}:${index}:${step.file}:${step.line}:${slice.sliceStart}-${slice.sliceEnd}`,
104
+ },
105
+ annotations:
106
+ slice.focusOffset != null
107
+ ? [{ lineNumber: slice.focusOffset }]
108
+ : undefined,
109
+ };
110
+ });
111
+ }, [load, throughline.id, throughline.steps, contextLines]);
112
+
113
+ const renderHeaderPrefix = useMemo(() => {
114
+ return (item: CodeViewItem) => {
115
+ const index = Number.parseInt(item.id.split(':').pop() ?? '', 10);
116
+ const step = throughline.steps[index];
117
+ if (!step) return null;
118
+ const label =
119
+ step.symbol != null && step.symbol.length > 0
120
+ ? step.symbol
121
+ : `step ${index + 1}`;
122
+ return (
123
+ <span
124
+ style={{
125
+ fontFamily: theme.fonts.monospace,
126
+ fontSize: theme.fontSizes[0],
127
+ color: theme.colors.textSecondary,
128
+ marginRight: 8,
129
+ }}
130
+ >
131
+ {index + 1}. {label}
132
+ </span>
133
+ );
134
+ };
135
+ }, [throughline.steps, theme]);
136
+
137
+ const renderHeaderMetadata = useMemo(() => {
138
+ return (item: CodeViewItem) => {
139
+ const index = Number.parseInt(item.id.split(':').pop() ?? '', 10);
140
+ const step = throughline.steps[index];
141
+ if (!step) return null;
142
+ return (
143
+ <span
144
+ style={{
145
+ fontFamily: theme.fonts.monospace,
146
+ fontSize: theme.fontSizes[0],
147
+ color: theme.colors.textSecondary,
148
+ marginLeft: 8,
149
+ }}
150
+ >
151
+ L{step.line}
152
+ </span>
153
+ );
154
+ };
155
+ }, [throughline.steps, theme]);
156
+
157
+ const options = useMemo((): CodeViewReactOptions => {
158
+ return {
159
+ theme: {
160
+ dark: resolvePierreSyntaxThemeName('dark'),
161
+ light: resolvePierreSyntaxThemeName('light'),
162
+ },
163
+ stickyHeaders: true,
164
+ disableFileHeader: false,
165
+ layout: { paddingTop: 8, paddingBottom: 16, gap: 12 },
166
+ ...(background ? buildPierreOptions(background) : {}),
167
+ ...(mode === 'light' || mode === 'dark' ? { themeType: mode } : {}),
168
+ };
169
+ }, [background, mode]);
170
+
171
+ useEffect(() => {
172
+ if (load.status !== 'ready' || stepIndex == null) return;
173
+ if (stepIndex < 0 || stepIndex >= throughline.steps.length) return;
174
+ // Defer until CodeView has laid out items.
175
+ const id = stepItemId(throughline.id, stepIndex);
176
+ const t = window.setTimeout(() => {
177
+ viewRef.current?.scrollTo({ type: 'item', id, align: 'start' });
178
+ }, 50);
179
+ return () => window.clearTimeout(t);
180
+ }, [load.status, stepIndex, throughline.id, throughline.steps.length, items.length]);
181
+
182
+ if (load.status === 'error') {
183
+ return (
184
+ <div style={{ padding: 16, color: theme.colors.error ?? '#e5534b' }}>
185
+ {load.message}
186
+ </div>
187
+ );
188
+ }
189
+ if (load.status === 'loading' || items.length === 0) {
190
+ return (
191
+ <div style={{ padding: 16, color: theme.colors.textSecondary }}>
192
+ Loading…
193
+ </div>
194
+ );
195
+ }
196
+
197
+ // Pierre's CodeView prop / item unions blow past TS's complexity limit.
198
+ const CodeViewLoose = CodeView as unknown as (props: Record<string, unknown>) => ReactElement;
199
+
200
+ return (
201
+ <CodeViewLoose
202
+ ref={viewRef}
203
+ items={items}
204
+ options={options}
205
+ renderHeaderPrefix={renderHeaderPrefix}
206
+ renderHeaderMetadata={renderHeaderMetadata}
207
+ style={{ ...PIERRE_FILE_STYLE, height: '100%', overflow: 'auto' }}
208
+ />
209
+ );
210
+ }
@@ -2,6 +2,8 @@ export { PierreFileView } from './PierreFileView';
2
2
  export type { PierreFileViewProps } from './PierreFileView';
3
3
  export { PierreSnippetView } from './PierreSnippetView';
4
4
  export type { PierreSnippetViewProps } from './PierreSnippetView';
5
+ export { PierreThroughlineCodeView } from './PierreThroughlineCodeView';
6
+ export type { PierreThroughlineCodeViewProps } from './PierreThroughlineCodeView';
5
7
  export { sliceSnippetWindow } from './sliceSnippet';
6
8
  export type { SnippetSlice } from './sliceSnippet';
7
9
  export {
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Demo: Pierre CodeView with snippets from different files.
3
+ *
4
+ * Throughlines want many short windows across files in one scroll —
5
+ * CodeView is the Pierre primitive for that (virtualized list of file items).
6
+ */
7
+ import React, { useMemo, useRef } from 'react';
8
+ import type { Meta, StoryObj } from '@storybook/react';
9
+ import {
10
+ CodeView,
11
+ type CodeViewHandle,
12
+ type CodeViewItem,
13
+ } from '@pierre/diffs/react';
14
+ import { ThemeProvider, defaultEditorTheme, useTheme } from '@principal-ade/industry-theme';
15
+ import { sliceSnippetWindow } from '../../pierre/sliceSnippet';
16
+ import { resolvePierreSyntaxThemeName } from '../../pierre/pierreSyntaxTheme';
17
+ import componentDeclarationSource from '../../subsystem/ComponentDeclaration.tsx?raw';
18
+ import resolveSource from '../../graphify/resolve.ts?raw';
19
+ import modelSource from '../../subsystem/model.ts?raw';
20
+
21
+ type SnippetSpec = {
22
+ id: string;
23
+ path: string;
24
+ contents: string;
25
+ startLine: number;
26
+ endLine: number;
27
+ focusLine?: number;
28
+ label: string;
29
+ };
30
+
31
+ const SNIPPETS: SnippetSpec[] = [
32
+ {
33
+ id: 'step-1',
34
+ path: 'packages/react/src/subsystem/ComponentDeclaration.tsx',
35
+ contents: componentDeclarationSource,
36
+ startLine: 252,
37
+ endLine: 270,
38
+ focusLine: 252,
39
+ label: '1 · ComponentDeclaration export',
40
+ },
41
+ {
42
+ id: 'step-2',
43
+ path: 'packages/react/src/graphify/resolve.ts',
44
+ contents: resolveSource,
45
+ startLine: 1,
46
+ endLine: 40,
47
+ focusLine: 1,
48
+ label: '2 · graphify resolve',
49
+ },
50
+ {
51
+ id: 'step-3',
52
+ path: 'packages/react/src/subsystem/model.ts',
53
+ contents: modelSource,
54
+ startLine: 216,
55
+ endLine: 250,
56
+ focusLine: 216,
57
+ label: '3 · Throughline types',
58
+ },
59
+ ];
60
+
61
+ const meta = {
62
+ title: 'Pierre/CodeView',
63
+ parameters: {
64
+ layout: 'fullscreen',
65
+ },
66
+ tags: ['autodocs'],
67
+ decorators: [
68
+ (Story) => (
69
+ <ThemeProvider theme={defaultEditorTheme}>
70
+ <Story />
71
+ </ThemeProvider>
72
+ ),
73
+ ],
74
+ } satisfies Meta;
75
+
76
+ export default meta;
77
+ type Story = StoryObj;
78
+
79
+ function MultiFileSnippetsDemo() {
80
+ const { theme, mode } = useTheme();
81
+ const viewRef = useRef<CodeViewHandle>(null);
82
+
83
+ const items = useMemo((): CodeViewItem[] => {
84
+ return SNIPPETS.map((spec) => {
85
+ const slice = sliceSnippetWindow(
86
+ spec.contents,
87
+ spec.startLine,
88
+ spec.endLine,
89
+ 2,
90
+ spec.focusLine ?? spec.startLine,
91
+ );
92
+ return {
93
+ id: spec.id,
94
+ type: 'file' as const,
95
+ file: {
96
+ name: spec.path,
97
+ contents: slice.contents,
98
+ cacheKey: `${spec.id}:${slice.sliceStart}-${slice.sliceEnd}`,
99
+ },
100
+ annotations:
101
+ slice.focusOffset != null
102
+ ? [{ lineNumber: slice.focusOffset }]
103
+ : undefined,
104
+ };
105
+ });
106
+ }, []);
107
+
108
+ const options = useMemo(
109
+ () => ({
110
+ theme: {
111
+ dark: resolvePierreSyntaxThemeName('dark'),
112
+ light: resolvePierreSyntaxThemeName('light'),
113
+ } as const,
114
+ stickyHeaders: true,
115
+ layout: { paddingTop: 12, paddingBottom: 24, gap: 16 },
116
+ ...(mode === 'light' || mode === 'dark' ? { themeType: mode as 'light' | 'dark' } : {}),
117
+ }),
118
+ [mode],
119
+ );
120
+
121
+ return (
122
+ <div
123
+ style={{
124
+ display: 'flex',
125
+ height: '100vh',
126
+ background: theme.colors.background,
127
+ color: theme.colors.text,
128
+ fontFamily: theme.fonts.sans,
129
+ }}
130
+ >
131
+ <aside
132
+ style={{
133
+ width: 260,
134
+ flexShrink: 0,
135
+ borderRight: `1px solid ${theme.colors.border ?? '#333'}`,
136
+ padding: 16,
137
+ display: 'flex',
138
+ flexDirection: 'column',
139
+ gap: 8,
140
+ }}
141
+ >
142
+ <div style={{ fontSize: 13, fontWeight: 600, marginBottom: 4 }}>
143
+ Throughline steps
144
+ </div>
145
+ <div style={{ fontSize: 12, color: theme.colors.textSecondary, marginBottom: 8 }}>
146
+ Pierre <code>CodeView</code> — one scroll, snippets from different files.
147
+ </div>
148
+ {SNIPPETS.map((spec) => (
149
+ <button
150
+ key={spec.id}
151
+ type="button"
152
+ onClick={() => viewRef.current?.scrollTo({ type: 'item', id: spec.id })}
153
+ style={{
154
+ textAlign: 'left',
155
+ padding: '8px 10px',
156
+ borderRadius: 6,
157
+ border: `1px solid ${theme.colors.border ?? '#333'}`,
158
+ background: theme.colors.surface ?? 'transparent',
159
+ color: theme.colors.text,
160
+ cursor: 'pointer',
161
+ fontSize: 12,
162
+ }}
163
+ >
164
+ {spec.label}
165
+ <div
166
+ style={{
167
+ marginTop: 4,
168
+ fontFamily: theme.fonts.monospace,
169
+ fontSize: 10,
170
+ color: theme.colors.textSecondary,
171
+ overflow: 'hidden',
172
+ textOverflow: 'ellipsis',
173
+ whiteSpace: 'nowrap',
174
+ }}
175
+ >
176
+ {spec.path.split('/').pop()}:{spec.startLine}
177
+ </div>
178
+ </button>
179
+ ))}
180
+ </aside>
181
+ <div style={{ flex: 1, minWidth: 0, minHeight: 0 }}>
182
+ <CodeView
183
+ ref={viewRef}
184
+ items={items}
185
+ options={options}
186
+ style={{ height: '100%', overflow: 'auto' }}
187
+ />
188
+ </div>
189
+ </div>
190
+ );
191
+ }
192
+
193
+ export const MultiFileSnippets: Story = {
194
+ name: 'Multi-file snippets',
195
+ render: () => <MultiFileSnippetsDemo />,
196
+ };
@@ -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 and frames it on the canvas. Other opened flows stay
36
- // dimmed; the rest of the graph is hidden.
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** (the frame at that hop); clicking a step focuses that edge on the canvas."
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={{
@@ -27,6 +27,9 @@ export const MECHANISM_DESCRIPTIONS: [SubsystemEdgeMechanism, string, boolean][]
27
27
  ['contains', 'structural: contains / encapsulates', true],
28
28
  ['feeds', 'data flow: output feeds into input', false],
29
29
  ['produces', 'data flow: produces / outputs', false],
30
+ ['writes', 'state access: mutates retained state', true],
31
+ ['reads', 'state access: reads retained state', true],
32
+ ['watches', 'observes retained state without owning it', false],
30
33
  ['registers-into', 'registration pattern', false],
31
34
  ];
32
35
 
@@ -1,7 +1,8 @@
1
1
  /**
2
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.
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 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)
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
- file,
17
+ title,
18
18
  onClose,
19
19
  children,
20
20
  }: {
21
- file: string | null;
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 = file !== null;
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={file ?? undefined}
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
- {file}
80
+ {title}
80
81
  </span>
81
82
  <button
82
83
  type="button"