@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
@@ -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. Opened flows stay on the canvas (unselected ones dimmed);
71
- * everything else is hidden.
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 node click (component with a
109
- * `file`) and sidebar file-tree click. Keeps this package free of fs and
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 file changes.
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 DrawerContent = memo(function DrawerContent({
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 | null;
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
- /** File shown in the bottom drawer + optional declaration scroll target. */
171
- const [openFileTarget, setOpenFileTarget] = useState<{
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 `openFile` for the tree-click toggle.
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
- openFileRef.current = openFileTarget;
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
- setOpenFileTarget(null);
642
+ setDrawerTarget(null);
598
643
  return;
599
644
  }
600
- setOpenFileTarget({ file });
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
- setOpenFileTarget(null);
658
+ setDrawerTarget(null);
614
659
  return;
615
660
  }
616
- setOpenFileTarget({ file, startLine });
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. The step's file:line is listed
664
- // in the row; we don't open the drawer from here.
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 file={openFile} onClose={() => setOpenFileTarget(null)}>
1161
- <DrawerContent
1162
- render={renderDrawerContent}
1163
- file={openFile}
1164
- startLine={openFileStartLine}
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} />
@@ -322,10 +322,10 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
322
322
  <div
323
323
  style={{
324
324
  position: 'absolute',
325
- top: -13,
325
+ top: -19,
326
326
  left: 12,
327
327
  fontFamily: theme.fonts.monospace,
328
- fontSize: theme.fontSizes[0],
328
+ fontSize: theme.fontSizes[3],
329
329
  fontWeight: 700,
330
330
  letterSpacing: 0.6,
331
331
  color,
@@ -336,7 +336,7 @@ export function SubsystemGroupNode(props: NodeProps<Node<SubsystemGroupNodeData,
336
336
  whiteSpace: 'nowrap',
337
337
  }}
338
338
  >
339
- {`process: ${region.label}`}
339
+ {region.label}
340
340
  </div>
341
341
  </div>
342
342
  );