@principal-ai/principal-view-react 0.15.3 → 0.15.5

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.
@@ -14,6 +14,7 @@ import {
14
14
  ReactFlowProvider,
15
15
  Panel,
16
16
  useViewport,
17
+ useStore,
17
18
  Handle,
18
19
  Position,
19
20
  BaseEdge,
@@ -361,6 +362,8 @@ interface SwimlaneLayerProps {
361
362
  totalHeight: number;
362
363
  onToggleCollapse?: (namespace: string) => void;
363
364
  stickyHeaders?: boolean;
365
+ /** When true, render lane and header backgrounds as transparent. */
366
+ transparent?: boolean;
364
367
  }
365
368
 
366
369
  /**
@@ -371,12 +374,15 @@ function SwimlaneLayer({
371
374
  laneWidth,
372
375
  headerHeight,
373
376
  totalHeight,
377
+ transparent = false,
374
378
  }: SwimlaneLayerProps) {
375
379
  const { x, y, zoom } = useViewport();
380
+ const viewportHeight = useStore((s) => s.height);
376
381
  const { theme } = useTheme();
377
382
 
378
- // Add a small buffer to ensure lanes extend slightly beyond the last event
379
- const extendedHeight = totalHeight + 20;
383
+ // Extend lanes to cover the viewport bottom even when content is short.
384
+ // In flow-coord space, the visible viewport is `viewportHeight / zoom` tall.
385
+ const extendedHeight = Math.max(totalHeight + 20, viewportHeight / zoom + 100);
380
386
 
381
387
  return (
382
388
  <div
@@ -393,6 +399,11 @@ function SwimlaneLayer({
393
399
  {/* Lane backgrounds */}
394
400
  {swimlanes.map((lane, index) => {
395
401
  const isEven = index % 2 === 0;
402
+ const laneBackground = transparent
403
+ ? 'transparent'
404
+ : isEven
405
+ ? theme.colors.muted
406
+ : theme.colors.background;
396
407
  return (
397
408
  <div
398
409
  key={`bg-${lane.namespace}`}
@@ -402,9 +413,7 @@ function SwimlaneLayer({
402
413
  top: 0,
403
414
  width: laneWidth,
404
415
  height: extendedHeight,
405
- backgroundColor: isEven
406
- ? theme.colors.muted
407
- : theme.colors.background,
416
+ backgroundColor: laneBackground,
408
417
  borderRight: `1px solid ${theme.colors.border}`,
409
418
  }}
410
419
  />
@@ -439,6 +448,7 @@ function SwimlaneHeadersLayer({
439
448
  headerHeight,
440
449
  onToggleCollapse,
441
450
  stickyHeaders = true,
451
+ transparent = false,
442
452
  }: SwimlaneLayerProps) {
443
453
  const { x, y, zoom } = useViewport();
444
454
  const { theme } = useTheme();
@@ -474,7 +484,9 @@ function SwimlaneHeadersLayer({
474
484
  display: 'flex',
475
485
  alignItems: 'center',
476
486
  justifyContent: 'center',
477
- backgroundColor: theme.colors.muted,
487
+ padding: '0 8px',
488
+ boxSizing: 'border-box',
489
+ backgroundColor: transparent ? 'transparent' : theme.colors.muted,
478
490
  borderBottom: `2px solid ${theme.colors.border}`,
479
491
  fontWeight: theme.fontWeights.semibold,
480
492
  fontSize: theme.fontSizes[2],
@@ -491,7 +503,17 @@ function SwimlaneHeadersLayer({
491
503
  {lane.isCollapsed ? '▼' : '▶'}
492
504
  </span>
493
505
  )}
494
- <span>{lane.label}</span>
506
+ <span
507
+ style={{
508
+ overflowWrap: 'anywhere',
509
+ wordBreak: 'break-word',
510
+ lineHeight: 1.2,
511
+ textAlign: 'center',
512
+ }}
513
+ title={lane.label}
514
+ >
515
+ {lane.label}
516
+ </span>
495
517
  </div>
496
518
  );
497
519
  })}
@@ -547,6 +569,14 @@ export interface SequenceDiagramRendererProps {
547
569
 
548
570
  /** Whether to show event labels on nodes (default: false, labels already shown on edges) */
549
571
  showEventLabels?: boolean;
572
+
573
+ /**
574
+ * When true, render the diagram chrome (canvas background, swimlane fills,
575
+ * header backgrounds) as transparent so the diagram can be composited over
576
+ * an arbitrary backdrop. Lifelines, borders, edges, and label pills keep
577
+ * their theme colors so the diagram stays legible. Defaults to `false`.
578
+ */
579
+ transparent?: boolean;
550
580
  }
551
581
 
552
582
  /**
@@ -565,11 +595,12 @@ function SequenceDiagramInner({
565
595
  stickyHeaders = true,
566
596
  selectedNodeId,
567
597
  showEventLabels = false, // Default to false - labels already shown on edges
598
+ transparent = false,
568
599
  }: SequenceDiagramRendererProps) {
569
600
  const { theme } = useTheme();
570
601
 
571
602
  // Extract layout params
572
- const { laneWidth = 200, headerHeight = 60 } = layoutOptions;
603
+ const { laneWidth = 250, headerHeight = 60 } = layoutOptions;
573
604
 
574
605
  // Merge custom node/edge types with sequence defaults
575
606
  const nodeTypes = useMemo(
@@ -582,7 +613,7 @@ function SequenceDiagramInner({
582
613
  );
583
614
 
584
615
  // Compute layout
585
- const { nodes: layoutNodes, edges, swimlanes, totalHeight } = useSequenceLayout(
616
+ const { nodes: layoutNodes, edges, swimlanes, totalWidth, totalHeight } = useSequenceLayout(
586
617
  events,
587
618
  sequenceEdges,
588
619
  layoutOptions
@@ -629,22 +660,19 @@ function SequenceDiagramInner({
629
660
  [onNodeClick]
630
661
  );
631
662
 
632
- // When sticky headers are enabled, limit upward panning to prevent headers from disconnecting
633
- // from swimlane backgrounds. Allow downward panning to see all content.
663
+ // Clamp horizontal and vertical panning to content bounds so swimlanes and
664
+ // headers stay in view. When sticky headers are disabled, allow a small
665
+ // negative top buffer for visual breathing room.
634
666
  const translateExtent = useMemo(() => {
635
- if (!stickyHeaders) {
636
- // No restrictions when sticky headers are disabled
637
- return undefined;
638
- }
639
-
640
- // Prevent panning up past the header area (y >= 0)
641
- // Allow panning down to see all content (y can be positive up to totalHeight)
642
- // Allow full horizontal panning
667
+ const xMin = 0;
668
+ const xMax = totalWidth;
669
+ const yMin = stickyHeaders ? 0 : -20;
670
+ const yMax = stickyHeaders ? totalHeight + 1000 : totalHeight + 20;
643
671
  return [
644
- [-Infinity, 0],
645
- [Infinity, totalHeight + 1000],
672
+ [xMin, yMin],
673
+ [xMax, yMax],
646
674
  ] as [[number, number], [number, number]];
647
- }, [stickyHeaders, totalHeight]);
675
+ }, [stickyHeaders, totalWidth, totalHeight]);
648
676
 
649
677
  // When sticky headers are enabled, use defaultViewport to ensure we start at y=0
650
678
  // This prevents the snap-to-position issue on initial load
@@ -684,7 +712,7 @@ function SequenceDiagramInner({
684
712
  panOnScroll
685
713
  zoomOnScroll
686
714
  translateExtent={translateExtent}
687
- style={{ background: theme.colors.background }}
715
+ style={{ background: transparent ? 'transparent' : theme.colors.background }}
688
716
  >
689
717
  {/* SVG defs for arrow markers */}
690
718
  <svg style={{ position: 'absolute', width: 0, height: 0 }}>
@@ -733,6 +761,7 @@ function SequenceDiagramInner({
733
761
  laneWidth={laneWidth}
734
762
  headerHeight={headerHeight}
735
763
  totalHeight={totalHeight}
764
+ transparent={transparent}
736
765
  />
737
766
 
738
767
  {/* Swimlane headers layer - renders on top for clickability */}
@@ -743,6 +772,7 @@ function SequenceDiagramInner({
743
772
  totalHeight={totalHeight}
744
773
  onToggleCollapse={onToggleCollapse}
745
774
  stickyHeaders={stickyHeaders}
775
+ transparent={transparent}
746
776
  />
747
777
 
748
778
  {/* Collapse toggle panel (for namespaces with children) */}
@@ -48,6 +48,13 @@ export interface WorkflowSequenceDiagramProps {
48
48
 
49
49
  /** Whether to show event labels on nodes (default: false, labels already shown on edges) */
50
50
  showEventLabels?: boolean;
51
+
52
+ /**
53
+ * When true, render the diagram chrome (canvas background, swimlane fills,
54
+ * header backgrounds) as transparent so the diagram can be composited over
55
+ * an arbitrary backdrop. Defaults to `false`.
56
+ */
57
+ transparent?: boolean;
51
58
  }
52
59
 
53
60
  /**
@@ -131,19 +138,25 @@ function convertWorkflowToSequence(
131
138
 
132
139
  // Convert to SequenceEvent format
133
140
  const events: SequenceEvent[] = eventNames.map((eventName, index) => {
134
- const scope = eventToScopeMap.get(eventName) || 'unknown';
141
+ const canvasScope = eventToScopeMap.get(eventName);
135
142
  const label = eventToLabelMap.get(eventName) || eventName.split('.').pop() || eventName;
136
143
 
137
144
  // Determine if this is a move event
138
145
  // Default to true unless explicitly marked as transform event in participant node
139
146
  const isMoveEvent = eventToMoveEventMap.get(eventName) ?? true;
140
147
 
148
+ // When the canvas provides a scope, prefix it so the event lands in that
149
+ // participant's lane. Otherwise, use the event name as-is so the layout's
150
+ // namespace strategy can derive the participant from the event name itself.
151
+ const name = canvasScope ? `${canvasScope}.${eventName}` : eventName;
152
+ const participant = canvasScope ?? eventName.split('.')[0] ?? eventName;
153
+
141
154
  return {
142
155
  id: `event-${index}`,
143
- name: `${scope}.${eventName}`,
156
+ name,
144
157
  label,
145
158
  moveEvent: isMoveEvent,
146
- participant: scope,
159
+ participant,
147
160
  };
148
161
  });
149
162
 
@@ -192,6 +205,7 @@ export function WorkflowSequenceDiagram({
192
205
  onEventIndexChange,
193
206
  selectedEventIndex,
194
207
  showEventLabels = false, // Default to false - labels already on edges
208
+ transparent = false,
195
209
  }: WorkflowSequenceDiagramProps) {
196
210
  // Convert workflow to sequence format
197
211
  const { events, edges } = useMemo(
@@ -253,6 +267,7 @@ export function WorkflowSequenceDiagram({
253
267
  onNodeClick={onEventIndexChange ? handleNodeClick : undefined}
254
268
  selectedNodeId={selectedNodeId}
255
269
  showEventLabels={showEventLabels}
270
+ transparent={transparent}
256
271
  />
257
272
  );
258
273
  }
@@ -23,6 +23,14 @@ export interface SequenceEvent {
23
23
  moveEvent?: boolean;
24
24
  /** Participant this event belongs to (for move events, this is the target) */
25
25
  participant?: string;
26
+ /**
27
+ * Optional source-file path this event originated from, expressed
28
+ * relative to the repository root (e.g.
29
+ * `auth-server/src/lib/auth-provider.ts`). Surfaced on the React Flow
30
+ * node `data` so downstream renderers (file-city overlays, jump-to-source,
31
+ * IDE bridges) can react to selection without reaching into `data`.
32
+ */
33
+ sourcePath?: string;
26
34
  /** Additional data to pass through to the node */
27
35
  data?: Record<string, unknown>;
28
36
  }
@@ -84,7 +92,7 @@ export interface UseSequenceLayoutOptions {
84
92
 
85
93
  /**
86
94
  * Width of each swimlane
87
- * @default 200
95
+ * @default 250
88
96
  */
89
97
  laneWidth?: number;
90
98
 
@@ -210,8 +218,8 @@ export function useSequenceLayout(
210
218
  ): UseSequenceLayoutResult {
211
219
  const {
212
220
  namespaceStrategy = 'all-but-last',
213
- laneWidth = 200,
214
- laneGap = 10,
221
+ laneWidth = 250,
222
+ laneGap = 0,
215
223
  eventSpacing = 80,
216
224
  headerHeight = 60,
217
225
  collapsedNamespaces = [],
@@ -334,6 +342,7 @@ export function useSequenceLayout(
334
342
  visibleNamespace,
335
343
  timeLayer: i,
336
344
  isMoveEvent: event.moveEvent === true,
345
+ sourcePath: event.sourcePath,
337
346
  ...event.data,
338
347
  },
339
348
  style: {
@@ -6,7 +6,7 @@ import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
6
6
  import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
7
7
 
8
8
  // Import the CLI events canvas
9
- import validationEventsCanvasUrl from '../../../../.principal-views/cli.events.canvas?url';
9
+ import validationEventsCanvasUrl from '../../../../.principal-views/principal-view-cli.events.canvas?url';
10
10
 
11
11
  // Helper to load canvas from URL
12
12
  async function loadCanvas(url: string): Promise<ExtendedCanvas> {
@@ -45,7 +45,7 @@ participant metadata from a canvas.
45
45
  decorators: [
46
46
  (Story) => (
47
47
  <ThemeProvider theme={defaultEditorTheme}>
48
- <div style={{ padding: 20, height: '100vh', boxSizing: 'border-box' }}>
48
+ <div style={{ height: '100vh' }}>
49
49
  <Story />
50
50
  </div>
51
51
  </ThemeProvider>
@@ -143,6 +143,27 @@ const errorHandlingScenario: WorkflowScenario = {
143
143
  },
144
144
  };
145
145
 
146
+ /**
147
+ * Scenario with long namespace titles for stress-testing swimlane header rendering
148
+ */
149
+ const longTitlesScenario: WorkflowScenario = {
150
+ id: 'long-titles',
151
+ name: 'Long Swimlane Titles',
152
+ description: 'Scenario designed to stress-test header rendering with long participant names',
153
+ template: {
154
+ events: {
155
+ 'authenticationAndAuthorizationService.request.received': {},
156
+ 'authenticationAndAuthorizationService.token.validated': {},
157
+ 'distributedTransactionCoordinator.transaction.started': {},
158
+ 'inventoryReservationAndAllocationManager.items.reserved': {},
159
+ 'paymentProcessingAndFraudDetectionGateway.charge.authorized': {},
160
+ 'paymentProcessingAndFraudDetectionGateway.charge.captured': {},
161
+ 'distributedTransactionCoordinator.transaction.committed': {},
162
+ 'notificationDispatchAndDeliveryService.confirmation.sent': {},
163
+ },
164
+ },
165
+ };
166
+
146
167
  /**
147
168
  * Empty scenario for testing empty state
148
169
  */
@@ -273,7 +294,7 @@ export const ClickDiagnostic: Story = {
273
294
  export const BasicScenario: Story = {
274
295
  args: {
275
296
  scenario: simpleAuthScenario,
276
- height: 500,
297
+ height: '100%',
277
298
  layoutOptions: {
278
299
  namespaceStrategy: 'first',
279
300
  eventSpacing: 80,
@@ -288,7 +309,7 @@ export const WithCanvasMetadata: Story = {
288
309
  args: {
289
310
  scenario: simpleAuthScenario,
290
311
  canvas: sampleCanvas,
291
- height: 500,
312
+ height: '100%',
292
313
  layoutOptions: {
293
314
  namespaceStrategy: 'first',
294
315
  eventSpacing: 80,
@@ -302,7 +323,7 @@ export const WithCanvasMetadata: Story = {
302
323
  export const ComplexWorkflow: Story = {
303
324
  args: {
304
325
  scenario: orderProcessingScenario,
305
- height: 700,
326
+ height: '100%',
306
327
  layoutOptions: {
307
328
  namespaceStrategy: 'first',
308
329
  laneWidth: 200,
@@ -317,7 +338,7 @@ export const ComplexWorkflow: Story = {
317
338
  export const MicroservicesPattern: Story = {
318
339
  args: {
319
340
  scenario: microservicesScenario,
320
- height: 600,
341
+ height: '100%',
321
342
  layoutOptions: {
322
343
  namespaceStrategy: 'first',
323
344
  laneWidth: 220,
@@ -332,7 +353,7 @@ export const MicroservicesPattern: Story = {
332
353
  export const ErrorHandling: Story = {
333
354
  args: {
334
355
  scenario: errorHandlingScenario,
335
- height: 500,
356
+ height: '100%',
336
357
  layoutOptions: {
337
358
  namespaceStrategy: 'first',
338
359
  eventSpacing: 80,
@@ -346,7 +367,23 @@ export const ErrorHandling: Story = {
346
367
  export const EmptyState: Story = {
347
368
  args: {
348
369
  scenario: emptyScenario,
349
- height: 300,
370
+ height: '100%',
371
+ },
372
+ };
373
+
374
+ /**
375
+ * Long swimlane titles - stress-tests header rendering with very long participant names.
376
+ * Use this to verify text wrapping, truncation, and overflow behavior in lane headers.
377
+ */
378
+ export const LongSwimlaneTitles: Story = {
379
+ args: {
380
+ scenario: longTitlesScenario,
381
+ height: '100%',
382
+ layoutOptions: {
383
+ namespaceStrategy: 'first',
384
+ eventSpacing: 80,
385
+ headerHeight: 100,
386
+ },
350
387
  },
351
388
  };
352
389
 
@@ -605,7 +642,7 @@ export const NamespaceStrategies: Story = {
605
642
  export const CompactLayout: Story = {
606
643
  args: {
607
644
  scenario: simpleAuthScenario,
608
- height: 400,
645
+ height: '100%',
609
646
  layoutOptions: {
610
647
  namespaceStrategy: 'first',
611
648
  laneWidth: 150,
@@ -621,7 +658,7 @@ export const CompactLayout: Story = {
621
658
  export const SpaciousLayout: Story = {
622
659
  args: {
623
660
  scenario: simpleAuthScenario,
624
- height: 600,
661
+ height: '100%',
625
662
  layoutOptions: {
626
663
  namespaceStrategy: 'first',
627
664
  laneWidth: 250,
@@ -637,7 +674,7 @@ export const SpaciousLayout: Story = {
637
674
  export const WithoutControls: Story = {
638
675
  args: {
639
676
  scenario: simpleAuthScenario,
640
- height: 500,
677
+ height: '100%',
641
678
  showControls: false,
642
679
  layoutOptions: {
643
680
  namespaceStrategy: 'first',
@@ -652,7 +689,7 @@ export const WithoutControls: Story = {
652
689
  export const WithBackground: Story = {
653
690
  args: {
654
691
  scenario: simpleAuthScenario,
655
- height: 500,
692
+ height: '100%',
656
693
  showBackground: true,
657
694
  layoutOptions: {
658
695
  namespaceStrategy: 'first',