@principal-ai/subsystems-react 0.22.0 → 0.23.1
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/pierre/PierreFileView.d.ts +3 -1
- package/dist/pierre/PierreFileView.d.ts.map +1 -1
- package/dist/pierre/PierreFileView.js +13 -3
- package/dist/pierre/PierreFileView.js.map +1 -1
- package/dist/pierre/PierreSnippetView.d.ts.map +1 -1
- package/dist/pierre/PierreSnippetView.js +1 -7
- package/dist/pierre/PierreSnippetView.js.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts +4 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.js +46 -6
- package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -1
- package/dist/pierre/scrollAnchor.d.ts +2 -0
- package/dist/pierre/scrollAnchor.d.ts.map +1 -1
- package/dist/pierre/scrollAnchor.js +7 -0
- package/dist/pierre/scrollAnchor.js.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js +0 -1
- package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
- package/dist/subsystem/FileDrawer.d.ts +7 -2
- package/dist/subsystem/FileDrawer.d.ts.map +1 -1
- package/dist/subsystem/FileDrawer.js +13 -9
- package/dist/subsystem/FileDrawer.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +2 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +101 -10
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/declarationRef.d.ts +6 -1
- package/dist/subsystem/declarationRef.d.ts.map +1 -1
- package/dist/subsystem/model.d.ts +1 -1
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +0 -3
- package/dist/subsystem/model.js.map +1 -1
- package/package.json +3 -3
- package/src/pierre/PierreFileView.tsx +25 -2
- package/src/pierre/PierreSnippetView.tsx +1 -13
- package/src/pierre/PierreWalkthroughCodeView.tsx +79 -3
- package/src/pierre/scrollAnchor.ts +13 -0
- package/src/stories/Subsystem/ComponentGraph/DetailPanel.stories.tsx +2 -1
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +3 -1
- package/src/stories/Subsystem/ComponentGraph/fixtures.ts +0 -1
- package/src/subsystem/FileDrawer.tsx +19 -7
- package/src/subsystem/SubsystemComponentGraph.tsx +170 -8
- package/src/subsystem/declarationRef.ts +6 -1
- package/src/subsystem/model.ts +1 -5
|
@@ -69,6 +69,8 @@ export interface WalkthroughViewerContext {
|
|
|
69
69
|
walkthrough: SubsystemWalkthrough;
|
|
70
70
|
/** Focused step index; `null` means the whole flow (no specific step). */
|
|
71
71
|
stepIndex: number | null;
|
|
72
|
+
/** Open a step's full source file over the walkthrough drawer (keeps snippets mounted). */
|
|
73
|
+
onOpenFile: (path: string, opts?: SubsystemOpenFileOptions) => void;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
type DrawerTarget =
|
|
@@ -190,24 +192,68 @@ const FileDrawerContent = memo(function FileDrawerContent({
|
|
|
190
192
|
render,
|
|
191
193
|
file,
|
|
192
194
|
startLine,
|
|
195
|
+
fullFile,
|
|
193
196
|
}: {
|
|
194
197
|
render: (file: string, opts?: SubsystemOpenFileOptions) => ReactNode;
|
|
195
198
|
file: string;
|
|
196
199
|
startLine?: number;
|
|
200
|
+
fullFile?: boolean;
|
|
197
201
|
}) {
|
|
198
|
-
return
|
|
202
|
+
return (
|
|
203
|
+
<>
|
|
204
|
+
{render(
|
|
205
|
+
file,
|
|
206
|
+
startLine != null
|
|
207
|
+
? { startLine, ...(fullFile ? { fullFile: true } : {}) }
|
|
208
|
+
: undefined,
|
|
209
|
+
)}
|
|
210
|
+
</>
|
|
211
|
+
);
|
|
199
212
|
});
|
|
200
213
|
|
|
214
|
+
function FileOverlayCloseButton({ onClose }: { onClose: () => void }) {
|
|
215
|
+
const { theme } = useTheme();
|
|
216
|
+
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
217
|
+
const [hover, setHover] = useState(false);
|
|
218
|
+
return (
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
onClick={onClose}
|
|
222
|
+
onMouseEnter={() => setHover(true)}
|
|
223
|
+
onMouseLeave={() => setHover(false)}
|
|
224
|
+
aria-label="Close file"
|
|
225
|
+
style={{
|
|
226
|
+
display: 'inline-flex',
|
|
227
|
+
alignItems: 'center',
|
|
228
|
+
justifyContent: 'center',
|
|
229
|
+
width: 22,
|
|
230
|
+
height: 22,
|
|
231
|
+
padding: 0,
|
|
232
|
+
border: 'none',
|
|
233
|
+
borderRadius: 4,
|
|
234
|
+
background: hover ? theme.colors.border : 'transparent',
|
|
235
|
+
color: hover ? theme.colors.text : muted,
|
|
236
|
+
cursor: 'pointer',
|
|
237
|
+
transition: 'background 120ms ease, color 120ms ease',
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
<X size={14} />
|
|
241
|
+
</button>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
201
245
|
const WalkthroughDrawerContent = memo(function WalkthroughDrawerContent({
|
|
202
246
|
render,
|
|
203
247
|
walkthrough,
|
|
204
248
|
stepIndex,
|
|
249
|
+
onOpenFile,
|
|
205
250
|
}: {
|
|
206
251
|
render: (ctx: WalkthroughViewerContext) => ReactNode;
|
|
207
252
|
walkthrough: SubsystemWalkthrough;
|
|
208
253
|
stepIndex: number | null;
|
|
254
|
+
onOpenFile: (path: string, opts?: SubsystemOpenFileOptions) => void;
|
|
209
255
|
}) {
|
|
210
|
-
return <>{render({ walkthrough, stepIndex })}</>;
|
|
256
|
+
return <>{render({ walkthrough, stepIndex, onOpenFile })}</>;
|
|
211
257
|
});
|
|
212
258
|
|
|
213
259
|
interface InnerProps extends SubsystemComponentGraphProps {
|
|
@@ -230,6 +276,11 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
230
276
|
const [selected, setSelected] = useState<SubsystemComponent | null>(null);
|
|
231
277
|
/** Bottom drawer: single file or walkthrough multi-snippet mode. */
|
|
232
278
|
const [drawerTarget, setDrawerTarget] = useState<DrawerTarget | null>(null);
|
|
279
|
+
/** Full-file layer over an open walkthrough drawer — walkthrough stays mounted. */
|
|
280
|
+
const [fileOverlay, setFileOverlay] = useState<{
|
|
281
|
+
file: string;
|
|
282
|
+
startLine?: number;
|
|
283
|
+
} | null>(null);
|
|
233
284
|
// Mirror so step-focus can decide whether to wait for the drawer open
|
|
234
285
|
// transition before fitView (avoids framing against full-height canvas).
|
|
235
286
|
const drawerOpenRef = useRef(false);
|
|
@@ -316,9 +367,11 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
316
367
|
// Ref mirror of the open file drawer target for tree-click toggle.
|
|
317
368
|
const openFileRef = useRef<{ file: string; startLine?: number } | null>(null);
|
|
318
369
|
const openFile =
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
370
|
+
fileOverlay?.file ??
|
|
371
|
+
(drawerTarget?.kind === 'file' ? drawerTarget.file : null);
|
|
372
|
+
openFileRef.current = fileOverlay
|
|
373
|
+
? { file: fileOverlay.file, startLine: fileOverlay.startLine }
|
|
374
|
+
: drawerTarget?.kind === 'file'
|
|
322
375
|
? { file: drawerTarget.file, startLine: drawerTarget.startLine }
|
|
323
376
|
: null;
|
|
324
377
|
|
|
@@ -864,6 +917,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
864
917
|
);
|
|
865
918
|
const onTreeSelectFile = useCallback(
|
|
866
919
|
(file: string) => {
|
|
920
|
+
setFileOverlay(null);
|
|
867
921
|
if (openFileRef.current?.file === file && openFileRef.current.startLine == null) {
|
|
868
922
|
setDrawerTarget(null);
|
|
869
923
|
return;
|
|
@@ -876,6 +930,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
876
930
|
|
|
877
931
|
const onOpenDeclarationFile = useCallback(
|
|
878
932
|
(file: string, opts?: SubsystemOpenFileOptions) => {
|
|
933
|
+
setFileOverlay(null);
|
|
879
934
|
const startLine = opts?.startLine;
|
|
880
935
|
if (
|
|
881
936
|
openFileRef.current?.file === file &&
|
|
@@ -890,6 +945,47 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
890
945
|
[onFileSelect],
|
|
891
946
|
);
|
|
892
947
|
|
|
948
|
+
const onOpenFileFromWalkthrough = useCallback(
|
|
949
|
+
(file: string, opts?: SubsystemOpenFileOptions) => {
|
|
950
|
+
setFileOverlay({ file, startLine: opts?.startLine });
|
|
951
|
+
onFileSelect?.(file);
|
|
952
|
+
},
|
|
953
|
+
[onFileSelect],
|
|
954
|
+
);
|
|
955
|
+
|
|
956
|
+
const closeFileOverlay = useCallback(() => {
|
|
957
|
+
setFileOverlay(null);
|
|
958
|
+
}, []);
|
|
959
|
+
|
|
960
|
+
const closeDrawer = useCallback(() => {
|
|
961
|
+
setFileOverlay(null);
|
|
962
|
+
setDrawerTarget(null);
|
|
963
|
+
}, []);
|
|
964
|
+
|
|
965
|
+
const drawerFillHeight =
|
|
966
|
+
drawerTarget?.kind === 'file' && drawerTarget.startLine == null;
|
|
967
|
+
const fileOverlayOpen = fileOverlay != null;
|
|
968
|
+
const collapseCanvas = drawerFillHeight || fileOverlayOpen;
|
|
969
|
+
|
|
970
|
+
// Drop the full-file overlay when the underlying walkthrough drawer changes
|
|
971
|
+
// or closes — open-file keeps the same drawerTarget so the snippets stay mounted.
|
|
972
|
+
const walkthroughDrawerKey =
|
|
973
|
+
drawerTarget?.kind === 'walkthrough'
|
|
974
|
+
? `${drawerTarget.walkthroughId}:${drawerTarget.stepIndex}`
|
|
975
|
+
: null;
|
|
976
|
+
useEffect(() => {
|
|
977
|
+
setFileOverlay(null);
|
|
978
|
+
}, [walkthroughDrawerKey]);
|
|
979
|
+
|
|
980
|
+
useEffect(() => {
|
|
981
|
+
if (!fileOverlayOpen) return;
|
|
982
|
+
const onKey = (e: KeyboardEvent) => {
|
|
983
|
+
if (e.key === 'Escape') closeFileOverlay();
|
|
984
|
+
};
|
|
985
|
+
window.addEventListener('keydown', onKey);
|
|
986
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
987
|
+
}, [fileOverlayOpen, closeFileOverlay]);
|
|
988
|
+
|
|
893
989
|
// Camera helper shared by the walkthrough interactions: frames the focused
|
|
894
990
|
// edges' endpoint nodes via `fitView({ nodes })`, which uses the store's
|
|
895
991
|
// live positions + measured dims — so the frame always includes BOTH
|
|
@@ -1532,8 +1628,19 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1532
1628
|
{/* Graph area */}
|
|
1533
1629
|
<div style={{ flex: 1, position: 'relative', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
|
1534
1630
|
{/* Canvas box — the edge-label overlay and legend anchor here, so
|
|
1535
|
-
overlays shift with the canvas, not the labels.
|
|
1536
|
-
|
|
1631
|
+
overlays shift with the canvas, not the labels. Collapses when the
|
|
1632
|
+
file drawer fills the column (whole-file reading). */}
|
|
1633
|
+
<div
|
|
1634
|
+
style={{
|
|
1635
|
+
position: 'relative',
|
|
1636
|
+
flex: collapseCanvas ? 0 : 1,
|
|
1637
|
+
minHeight: 0,
|
|
1638
|
+
height: collapseCanvas ? 0 : undefined,
|
|
1639
|
+
overflow: 'hidden',
|
|
1640
|
+
display: 'flex',
|
|
1641
|
+
flexDirection: 'column',
|
|
1642
|
+
}}
|
|
1643
|
+
>
|
|
1537
1644
|
{/* Edge-label overlay — sits above the ReactFlow pane so clicks land. */}
|
|
1538
1645
|
{showEdgeLabels !== false && (
|
|
1539
1646
|
<div
|
|
@@ -1796,7 +1903,12 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1796
1903
|
</div>
|
|
1797
1904
|
)}
|
|
1798
1905
|
</div>
|
|
1799
|
-
<FileDrawer
|
|
1906
|
+
<FileDrawer
|
|
1907
|
+
title={drawerTitle}
|
|
1908
|
+
onClose={closeDrawer}
|
|
1909
|
+
fillHeight={drawerFillHeight}
|
|
1910
|
+
suppressEscape={fileOverlayOpen}
|
|
1911
|
+
>
|
|
1800
1912
|
{drawerTarget?.kind === 'walkthrough' &&
|
|
1801
1913
|
focusedWalkthrough &&
|
|
1802
1914
|
renderWalkthroughViewer ? (
|
|
@@ -1804,6 +1916,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1804
1916
|
render={renderWalkthroughDrawerContent}
|
|
1805
1917
|
walkthrough={focusedWalkthrough}
|
|
1806
1918
|
stepIndex={drawerTarget.stepIndex}
|
|
1919
|
+
onOpenFile={onOpenFileFromWalkthrough}
|
|
1807
1920
|
/>
|
|
1808
1921
|
) : drawerTarget?.kind === 'file' ? (
|
|
1809
1922
|
<FileDrawerContent
|
|
@@ -1813,6 +1926,55 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
|
|
|
1813
1926
|
/>
|
|
1814
1927
|
) : null}
|
|
1815
1928
|
</FileDrawer>
|
|
1929
|
+
{fileOverlay && (
|
|
1930
|
+
<div
|
|
1931
|
+
style={{
|
|
1932
|
+
position: 'absolute',
|
|
1933
|
+
inset: 0,
|
|
1934
|
+
zIndex: 8,
|
|
1935
|
+
display: 'flex',
|
|
1936
|
+
flexDirection: 'column',
|
|
1937
|
+
background: theme.colors.background,
|
|
1938
|
+
borderLeft: `1px solid ${theme.colors.border}`,
|
|
1939
|
+
}}
|
|
1940
|
+
>
|
|
1941
|
+
<div
|
|
1942
|
+
style={{
|
|
1943
|
+
display: 'flex',
|
|
1944
|
+
alignItems: 'center',
|
|
1945
|
+
gap: 8,
|
|
1946
|
+
padding: '6px 10px',
|
|
1947
|
+
borderBottom: `1px solid ${theme.colors.border}`,
|
|
1948
|
+
flexShrink: 0,
|
|
1949
|
+
}}
|
|
1950
|
+
>
|
|
1951
|
+
<span
|
|
1952
|
+
title={fileOverlay.file}
|
|
1953
|
+
style={{
|
|
1954
|
+
flex: 1,
|
|
1955
|
+
minWidth: 0,
|
|
1956
|
+
overflow: 'hidden',
|
|
1957
|
+
textOverflow: 'ellipsis',
|
|
1958
|
+
whiteSpace: 'nowrap',
|
|
1959
|
+
fontFamily: theme.fonts.monospace,
|
|
1960
|
+
fontSize: theme.fontSizes[0],
|
|
1961
|
+
color: theme.colors.textMuted ?? theme.colors.textSecondary,
|
|
1962
|
+
}}
|
|
1963
|
+
>
|
|
1964
|
+
{fileOverlay.file}
|
|
1965
|
+
</span>
|
|
1966
|
+
<FileOverlayCloseButton onClose={closeFileOverlay} />
|
|
1967
|
+
</div>
|
|
1968
|
+
<div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
|
|
1969
|
+
<FileDrawerContent
|
|
1970
|
+
render={renderDrawerContent}
|
|
1971
|
+
file={fileOverlay.file}
|
|
1972
|
+
startLine={fileOverlay.startLine}
|
|
1973
|
+
fullFile
|
|
1974
|
+
/>
|
|
1975
|
+
</div>
|
|
1976
|
+
</div>
|
|
1977
|
+
)}
|
|
1816
1978
|
{/* Startup cover — hides measurement, layout swap, and camera settle. */}
|
|
1817
1979
|
<GraphLayoutCover revealed={layoutReady} />
|
|
1818
1980
|
{canvasOverlay}
|
|
@@ -58,6 +58,11 @@ export function extractDeclarationLine(content: string, lineNumber: number): str
|
|
|
58
58
|
|
|
59
59
|
/** Options when opening a file in the subsystem graph drawer. */
|
|
60
60
|
export interface SubsystemOpenFileOptions {
|
|
61
|
-
/** Scroll/highlight this 1-based line (declaration start). */
|
|
61
|
+
/** Scroll/highlight this 1-based line (declaration start or walkthrough site). */
|
|
62
62
|
startLine?: number;
|
|
63
|
+
/**
|
|
64
|
+
* When true with `startLine`, show the whole file scrolled to that line
|
|
65
|
+
* instead of a focused snippet window.
|
|
66
|
+
*/
|
|
67
|
+
fullFile?: boolean;
|
|
63
68
|
}
|
package/src/subsystem/model.ts
CHANGED
|
@@ -96,8 +96,7 @@ export type SubsystemRelationType =
|
|
|
96
96
|
| 'implements'
|
|
97
97
|
| 'mixes_in'
|
|
98
98
|
| 'method'
|
|
99
|
-
| 'references'
|
|
100
|
-
| 'contains';
|
|
99
|
+
| 'references';
|
|
101
100
|
|
|
102
101
|
/**
|
|
103
102
|
* Walkthrough hop mechanism — runtime seams with a `file:line` site.
|
|
@@ -619,7 +618,6 @@ export const MECHANISM_COLOR: Record<SubsystemEdgeMechanism, string> = {
|
|
|
619
618
|
uses: '#e3b341', // gold
|
|
620
619
|
method: '#c586c0', // magenta
|
|
621
620
|
references: '#e07a5f', // terracotta
|
|
622
|
-
contains: '#6c5ce7', // indigo
|
|
623
621
|
feeds: '#22c55e', // green — data-flow into a processor
|
|
624
622
|
produces: '#e07a5f', // terracotta — emits an output type
|
|
625
623
|
writes: '#2f9e44', // deep green — mutates retained state
|
|
@@ -638,7 +636,6 @@ export const MECHANISM_STYLE: Record<SubsystemEdgeMechanism, 'solid' | 'dashed'
|
|
|
638
636
|
uses: 'solid',
|
|
639
637
|
method: 'solid',
|
|
640
638
|
references: 'dotted',
|
|
641
|
-
contains: 'solid',
|
|
642
639
|
feeds: 'solid',
|
|
643
640
|
produces: 'solid',
|
|
644
641
|
writes: 'solid',
|
|
@@ -659,7 +656,6 @@ export const MECHANISM_DESCRIPTIONS: [SubsystemEdgeMechanism, string, boolean][]
|
|
|
659
656
|
['uses', 'general dependency (import, call, or reference)', false],
|
|
660
657
|
['method', 'structural: has method / member', true],
|
|
661
658
|
['references', 'type / symbol reference (not a call)', true],
|
|
662
|
-
['contains', 'UI composition: shell/view owns a child surface', false],
|
|
663
659
|
['feeds', 'data flow: output feeds into input', false],
|
|
664
660
|
['produces', 'data flow: produces / outputs', false],
|
|
665
661
|
['writes', 'state access: mutates retained state', true],
|