@principal-ai/subsystems-react 0.22.0 → 0.23.0

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 (43) hide show
  1. package/dist/pierre/PierreFileView.d.ts +3 -1
  2. package/dist/pierre/PierreFileView.d.ts.map +1 -1
  3. package/dist/pierre/PierreFileView.js +13 -3
  4. package/dist/pierre/PierreFileView.js.map +1 -1
  5. package/dist/pierre/PierreSnippetView.d.ts.map +1 -1
  6. package/dist/pierre/PierreSnippetView.js +1 -7
  7. package/dist/pierre/PierreSnippetView.js.map +1 -1
  8. package/dist/pierre/PierreWalkthroughCodeView.d.ts +3 -1
  9. package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -1
  10. package/dist/pierre/PierreWalkthroughCodeView.js +46 -6
  11. package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -1
  12. package/dist/pierre/scrollAnchor.d.ts +2 -0
  13. package/dist/pierre/scrollAnchor.d.ts.map +1 -1
  14. package/dist/pierre/scrollAnchor.js +7 -0
  15. package/dist/pierre/scrollAnchor.js.map +1 -1
  16. package/dist/stories/Subsystem/ComponentGraph/fixtures.d.ts.map +1 -1
  17. package/dist/stories/Subsystem/ComponentGraph/fixtures.js +0 -1
  18. package/dist/stories/Subsystem/ComponentGraph/fixtures.js.map +1 -1
  19. package/dist/subsystem/FileDrawer.d.ts +7 -2
  20. package/dist/subsystem/FileDrawer.d.ts.map +1 -1
  21. package/dist/subsystem/FileDrawer.js +13 -9
  22. package/dist/subsystem/FileDrawer.js.map +1 -1
  23. package/dist/subsystem/SubsystemComponentGraph.d.ts +2 -0
  24. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  25. package/dist/subsystem/SubsystemComponentGraph.js +97 -8
  26. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  27. package/dist/subsystem/declarationRef.d.ts +6 -1
  28. package/dist/subsystem/declarationRef.d.ts.map +1 -1
  29. package/dist/subsystem/model.d.ts +1 -1
  30. package/dist/subsystem/model.d.ts.map +1 -1
  31. package/dist/subsystem/model.js +0 -3
  32. package/dist/subsystem/model.js.map +1 -1
  33. package/package.json +3 -3
  34. package/src/pierre/PierreFileView.tsx +25 -2
  35. package/src/pierre/PierreSnippetView.tsx +1 -13
  36. package/src/pierre/PierreWalkthroughCodeView.tsx +72 -3
  37. package/src/pierre/scrollAnchor.ts +13 -0
  38. package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +2 -1
  39. package/src/stories/Subsystem/ComponentGraph/fixtures.ts +0 -1
  40. package/src/subsystem/FileDrawer.tsx +19 -7
  41. package/src/subsystem/SubsystemComponentGraph.tsx +153 -7
  42. package/src/subsystem/declarationRef.ts +6 -1
  43. 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) => void;
72
74
  }
73
75
 
74
76
  type DrawerTarget =
@@ -198,16 +200,49 @@ const FileDrawerContent = memo(function FileDrawerContent({
198
200
  return <>{render(file, startLine != null ? { startLine } : undefined)}</>;
199
201
  });
200
202
 
203
+ function FileOverlayCloseButton({ onClose }: { onClose: () => void }) {
204
+ const { theme } = useTheme();
205
+ const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
206
+ const [hover, setHover] = useState(false);
207
+ return (
208
+ <button
209
+ type="button"
210
+ onClick={onClose}
211
+ onMouseEnter={() => setHover(true)}
212
+ onMouseLeave={() => setHover(false)}
213
+ aria-label="Close file"
214
+ style={{
215
+ display: 'inline-flex',
216
+ alignItems: 'center',
217
+ justifyContent: 'center',
218
+ width: 22,
219
+ height: 22,
220
+ padding: 0,
221
+ border: 'none',
222
+ borderRadius: 4,
223
+ background: hover ? theme.colors.border : 'transparent',
224
+ color: hover ? theme.colors.text : muted,
225
+ cursor: 'pointer',
226
+ transition: 'background 120ms ease, color 120ms ease',
227
+ }}
228
+ >
229
+ <X size={14} />
230
+ </button>
231
+ );
232
+ }
233
+
201
234
  const WalkthroughDrawerContent = memo(function WalkthroughDrawerContent({
202
235
  render,
203
236
  walkthrough,
204
237
  stepIndex,
238
+ onOpenFile,
205
239
  }: {
206
240
  render: (ctx: WalkthroughViewerContext) => ReactNode;
207
241
  walkthrough: SubsystemWalkthrough;
208
242
  stepIndex: number | null;
243
+ onOpenFile: (path: string) => void;
209
244
  }) {
210
- return <>{render({ walkthrough, stepIndex })}</>;
245
+ return <>{render({ walkthrough, stepIndex, onOpenFile })}</>;
211
246
  });
212
247
 
213
248
  interface InnerProps extends SubsystemComponentGraphProps {
@@ -230,6 +265,8 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
230
265
  const [selected, setSelected] = useState<SubsystemComponent | null>(null);
231
266
  /** Bottom drawer: single file or walkthrough multi-snippet mode. */
232
267
  const [drawerTarget, setDrawerTarget] = useState<DrawerTarget | null>(null);
268
+ /** Full-file layer over an open walkthrough drawer — walkthrough stays mounted. */
269
+ const [fileOverlay, setFileOverlay] = useState<{ file: string } | null>(null);
233
270
  // Mirror so step-focus can decide whether to wait for the drawer open
234
271
  // transition before fitView (avoids framing against full-height canvas).
235
272
  const drawerOpenRef = useRef(false);
@@ -316,9 +353,11 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
316
353
  // Ref mirror of the open file drawer target for tree-click toggle.
317
354
  const openFileRef = useRef<{ file: string; startLine?: number } | null>(null);
318
355
  const openFile =
319
- drawerTarget?.kind === 'file' ? drawerTarget.file : null;
320
- openFileRef.current =
321
- drawerTarget?.kind === 'file'
356
+ fileOverlay?.file ??
357
+ (drawerTarget?.kind === 'file' ? drawerTarget.file : null);
358
+ openFileRef.current = fileOverlay
359
+ ? { file: fileOverlay.file }
360
+ : drawerTarget?.kind === 'file'
322
361
  ? { file: drawerTarget.file, startLine: drawerTarget.startLine }
323
362
  : null;
324
363
 
@@ -864,6 +903,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
864
903
  );
865
904
  const onTreeSelectFile = useCallback(
866
905
  (file: string) => {
906
+ setFileOverlay(null);
867
907
  if (openFileRef.current?.file === file && openFileRef.current.startLine == null) {
868
908
  setDrawerTarget(null);
869
909
  return;
@@ -876,6 +916,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
876
916
 
877
917
  const onOpenDeclarationFile = useCallback(
878
918
  (file: string, opts?: SubsystemOpenFileOptions) => {
919
+ setFileOverlay(null);
879
920
  const startLine = opts?.startLine;
880
921
  if (
881
922
  openFileRef.current?.file === file &&
@@ -890,6 +931,47 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
890
931
  [onFileSelect],
891
932
  );
892
933
 
934
+ const onOpenFileFromWalkthrough = useCallback(
935
+ (file: string) => {
936
+ setFileOverlay({ file });
937
+ onFileSelect?.(file);
938
+ },
939
+ [onFileSelect],
940
+ );
941
+
942
+ const closeFileOverlay = useCallback(() => {
943
+ setFileOverlay(null);
944
+ }, []);
945
+
946
+ const closeDrawer = useCallback(() => {
947
+ setFileOverlay(null);
948
+ setDrawerTarget(null);
949
+ }, []);
950
+
951
+ const drawerFillHeight =
952
+ drawerTarget?.kind === 'file' && drawerTarget.startLine == null;
953
+ const fileOverlayOpen = fileOverlay != null;
954
+ const collapseCanvas = drawerFillHeight || fileOverlayOpen;
955
+
956
+ // Drop the full-file overlay when the underlying walkthrough drawer changes
957
+ // or closes — open-file keeps the same drawerTarget so the snippets stay mounted.
958
+ const walkthroughDrawerKey =
959
+ drawerTarget?.kind === 'walkthrough'
960
+ ? `${drawerTarget.walkthroughId}:${drawerTarget.stepIndex}`
961
+ : null;
962
+ useEffect(() => {
963
+ setFileOverlay(null);
964
+ }, [walkthroughDrawerKey]);
965
+
966
+ useEffect(() => {
967
+ if (!fileOverlayOpen) return;
968
+ const onKey = (e: KeyboardEvent) => {
969
+ if (e.key === 'Escape') closeFileOverlay();
970
+ };
971
+ window.addEventListener('keydown', onKey);
972
+ return () => window.removeEventListener('keydown', onKey);
973
+ }, [fileOverlayOpen, closeFileOverlay]);
974
+
893
975
  // Camera helper shared by the walkthrough interactions: frames the focused
894
976
  // edges' endpoint nodes via `fitView({ nodes })`, which uses the store's
895
977
  // live positions + measured dims — so the frame always includes BOTH
@@ -1532,8 +1614,19 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
1532
1614
  {/* Graph area */}
1533
1615
  <div style={{ flex: 1, position: 'relative', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
1534
1616
  {/* Canvas box — the edge-label overlay and legend anchor here, so
1535
- overlays shift with the canvas, not the labels. */}
1536
- <div style={{ position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
1617
+ overlays shift with the canvas, not the labels. Collapses when the
1618
+ file drawer fills the column (whole-file reading). */}
1619
+ <div
1620
+ style={{
1621
+ position: 'relative',
1622
+ flex: collapseCanvas ? 0 : 1,
1623
+ minHeight: 0,
1624
+ height: collapseCanvas ? 0 : undefined,
1625
+ overflow: 'hidden',
1626
+ display: 'flex',
1627
+ flexDirection: 'column',
1628
+ }}
1629
+ >
1537
1630
  {/* Edge-label overlay — sits above the ReactFlow pane so clicks land. */}
1538
1631
  {showEdgeLabels !== false && (
1539
1632
  <div
@@ -1796,7 +1889,12 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
1796
1889
  </div>
1797
1890
  )}
1798
1891
  </div>
1799
- <FileDrawer title={drawerTitle} onClose={() => setDrawerTarget(null)}>
1892
+ <FileDrawer
1893
+ title={drawerTitle}
1894
+ onClose={closeDrawer}
1895
+ fillHeight={drawerFillHeight}
1896
+ suppressEscape={fileOverlayOpen}
1897
+ >
1800
1898
  {drawerTarget?.kind === 'walkthrough' &&
1801
1899
  focusedWalkthrough &&
1802
1900
  renderWalkthroughViewer ? (
@@ -1804,6 +1902,7 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
1804
1902
  render={renderWalkthroughDrawerContent}
1805
1903
  walkthrough={focusedWalkthrough}
1806
1904
  stepIndex={drawerTarget.stepIndex}
1905
+ onOpenFile={onOpenFileFromWalkthrough}
1807
1906
  />
1808
1907
  ) : drawerTarget?.kind === 'file' ? (
1809
1908
  <FileDrawerContent
@@ -1813,6 +1912,53 @@ function Inner({ components, relations, walkthroughs, onSelect, onEdgeSelect, me
1813
1912
  />
1814
1913
  ) : null}
1815
1914
  </FileDrawer>
1915
+ {fileOverlay && (
1916
+ <div
1917
+ style={{
1918
+ position: 'absolute',
1919
+ inset: 0,
1920
+ zIndex: 8,
1921
+ display: 'flex',
1922
+ flexDirection: 'column',
1923
+ background: theme.colors.background,
1924
+ borderLeft: `1px solid ${theme.colors.border}`,
1925
+ }}
1926
+ >
1927
+ <div
1928
+ style={{
1929
+ display: 'flex',
1930
+ alignItems: 'center',
1931
+ gap: 8,
1932
+ padding: '6px 10px',
1933
+ borderBottom: `1px solid ${theme.colors.border}`,
1934
+ flexShrink: 0,
1935
+ }}
1936
+ >
1937
+ <span
1938
+ title={fileOverlay.file}
1939
+ style={{
1940
+ flex: 1,
1941
+ minWidth: 0,
1942
+ overflow: 'hidden',
1943
+ textOverflow: 'ellipsis',
1944
+ whiteSpace: 'nowrap',
1945
+ fontFamily: theme.fonts.monospace,
1946
+ fontSize: theme.fontSizes[0],
1947
+ color: theme.colors.textMuted ?? theme.colors.textSecondary,
1948
+ }}
1949
+ >
1950
+ {fileOverlay.file}
1951
+ </span>
1952
+ <FileOverlayCloseButton onClose={closeFileOverlay} />
1953
+ </div>
1954
+ <div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
1955
+ <FileDrawerContent
1956
+ render={renderDrawerContent}
1957
+ file={fileOverlay.file}
1958
+ />
1959
+ </div>
1960
+ </div>
1961
+ )}
1816
1962
  {/* Startup cover — hides measurement, layout swap, and camera settle. */}
1817
1963
  <GraphLayoutCover revealed={layoutReady} />
1818
1964
  {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
  }
@@ -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],