@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
|
@@ -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
|
+
}
|
package/src/pierre/index.ts
CHANGED
|
@@ -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
|
|
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={{
|
|
@@ -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
|
|
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"
|