@principal-ai/principal-view-react 0.15.6 → 0.15.8

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.
@@ -5,14 +5,13 @@
5
5
  * Uses namespaces to determine swimlanes and event order for vertical positioning.
6
6
  */
7
7
 
8
- import React, { useCallback, useMemo } from 'react';
8
+ import React, { useCallback, useMemo, useState } from 'react';
9
9
  import {
10
10
  ReactFlow,
11
11
  Background,
12
12
  BackgroundVariant,
13
13
  Controls,
14
14
  ReactFlowProvider,
15
- Panel,
16
15
  useViewport,
17
16
  useStore,
18
17
  Handle,
@@ -33,6 +32,7 @@ import {
33
32
  type SequenceEdge,
34
33
  type UseSequenceLayoutOptions,
35
34
  type Swimlane,
35
+ type ParentHeader,
36
36
  } from '../hooks/useSequenceLayout';
37
37
 
38
38
  /**
@@ -360,6 +360,42 @@ const defaultSequenceNodeTypes: NodeTypes = {
360
360
  sequenceMarker: SequenceMarkerNode,
361
361
  };
362
362
 
363
+ /**
364
+ * Shared transition for swimlane chrome — interpolates positions and sizes
365
+ * when lanes shift due to drill toggles.
366
+ */
367
+ const swimlaneTransition =
368
+ 'top 350ms ease-out, left 350ms ease-out, width 350ms ease-out, height 350ms ease-out';
369
+
370
+ /** Duration of the child slide-up exit animation. The close handler waits
371
+ * this long before applying the data change so the diagram body doesn't
372
+ * shift while the children are still on screen. */
373
+ const SWIMLANE_CLOSE_EXIT_MS = 280;
374
+
375
+ const swimlaneAnimationStyles = `
376
+ @keyframes swimlaneFadeIn {
377
+ from { opacity: 0; }
378
+ to { opacity: 1; }
379
+ }
380
+ @keyframes swimlaneFadeOut {
381
+ from { opacity: 1; }
382
+ to { opacity: 0; }
383
+ }
384
+ @keyframes swimlaneChildSlideDown {
385
+ from { transform: translateY(-100%); opacity: 0; }
386
+ to { transform: translateY(0); opacity: 1; }
387
+ }
388
+ @keyframes swimlaneChildSlideUp {
389
+ from { transform: translateY(0); opacity: 1; }
390
+ to { transform: translateY(-100%); opacity: 0; }
391
+ }
392
+ .swimlane-fade-in { animation: swimlaneFadeIn 250ms ease-out both; }
393
+ .swimlane-child-bg-in { animation: swimlaneFadeIn 300ms ease-out 250ms both; }
394
+ .swimlane-child-in { animation: swimlaneChildSlideDown 300ms ease-out 250ms both; }
395
+ .swimlane-child-out { animation: swimlaneChildSlideUp 280ms ease-in both; }
396
+ .swimlane-fade-out { animation: swimlaneFadeOut 250ms ease-in both; }
397
+ `;
398
+
363
399
  /**
364
400
  * Default edge types including sequence arrow and participant arrow
365
401
  */
@@ -373,10 +409,18 @@ const defaultSequenceEdgeTypes: EdgeTypes = {
373
409
  */
374
410
  interface SwimlaneLayerProps {
375
411
  swimlanes: Swimlane[];
412
+ parentHeaders: ParentHeader[];
413
+ headerRows: number;
376
414
  laneWidth: number;
415
+ /** Per-row height (each row in the header strip is this tall) */
377
416
  headerHeight: number;
378
417
  totalHeight: number;
379
- onToggleCollapse?: (namespace: string) => void;
418
+ /** Called when the user clicks a chevron or parent header to toggle its drilled state */
419
+ onToggleNamespace?: (namespace: string) => void;
420
+ /** Namespaces mid-close. Their children are still in the data but render
421
+ * with the exit animation so the body can stay put until the animation
422
+ * finishes and the data change applies. */
423
+ closingNamespaces?: Set<string>;
380
424
  stickyHeaders?: boolean;
381
425
  /** When true, render lane and header backgrounds as transparent. */
382
426
  transparent?: boolean;
@@ -387,11 +431,13 @@ interface SwimlaneLayerProps {
387
431
  */
388
432
  function SwimlaneLayer({
389
433
  swimlanes,
434
+ headerRows,
390
435
  laneWidth,
391
436
  headerHeight,
392
437
  totalHeight,
393
438
  transparent = false,
394
439
  }: SwimlaneLayerProps) {
440
+ const totalHeaderHeight = headerHeight * headerRows;
395
441
  const { x, y, zoom } = useViewport();
396
442
  const viewportHeight = useStore((s) => s.height);
397
443
  const { theme } = useTheme();
@@ -420,9 +466,13 @@ function SwimlaneLayer({
420
466
  : isEven
421
467
  ? theme.colors.muted
422
468
  : theme.colors.background;
469
+ const fadeClass = lane.isParentOpened
470
+ ? 'swimlane-child-bg-in'
471
+ : 'swimlane-fade-in';
423
472
  return (
424
473
  <div
425
474
  key={`bg-${lane.namespace}`}
475
+ className={fadeClass}
426
476
  style={{
427
477
  position: 'absolute',
428
478
  left: lane.x - laneWidth / 2,
@@ -431,6 +481,7 @@ function SwimlaneLayer({
431
481
  height: extendedHeight,
432
482
  backgroundColor: laneBackground,
433
483
  borderRight: `1px solid ${theme.colors.border}`,
484
+ transition: swimlaneTransition,
434
485
  }}
435
486
  />
436
487
  );
@@ -440,14 +491,18 @@ function SwimlaneLayer({
440
491
  {swimlanes.map((lane) => (
441
492
  <div
442
493
  key={`lifeline-${lane.namespace}`}
494
+ className={
495
+ lane.isParentOpened ? 'swimlane-child-bg-in' : 'swimlane-fade-in'
496
+ }
443
497
  style={{
444
498
  position: 'absolute',
445
499
  left: lane.x,
446
- top: headerHeight,
500
+ top: totalHeaderHeight,
447
501
  width: 2,
448
- height: extendedHeight - headerHeight,
502
+ height: extendedHeight - totalHeaderHeight,
449
503
  backgroundColor: 'rgba(255, 255, 255, 0.4)',
450
504
  transform: 'translateX(-1px)',
505
+ transition: swimlaneTransition,
451
506
  }}
452
507
  />
453
508
  ))}
@@ -455,23 +510,142 @@ function SwimlaneLayer({
455
510
  );
456
511
  }
457
512
 
513
+ /**
514
+ * Two offset rounded rectangles signaling that a header has nested lanes
515
+ * underneath it. When `opened`, the front layer is filled to indicate the
516
+ * stack is currently expanded.
517
+ */
518
+ function StackIcon({
519
+ opened = false,
520
+ hovered = false,
521
+ accentColor,
522
+ }: {
523
+ opened?: boolean;
524
+ hovered?: boolean;
525
+ accentColor?: string;
526
+ }) {
527
+ // Closed: rects compactly stacked, both outlined in text color.
528
+ // Hovered: strokes tint to the accent color (preview of the action).
529
+ // Opened: front rect drifts down/right and fades to filled in the accent.
530
+ const rectTransition =
531
+ 'transform 280ms cubic-bezier(0.2, 0, 0, 1), fill-opacity 280ms ease, stroke 200ms ease';
532
+ const useAccent = (opened || hovered) && !!accentColor;
533
+ const stroke = useAccent ? accentColor : 'currentColor';
534
+ const fill = accentColor || 'currentColor';
535
+ return (
536
+ <svg
537
+ width={22}
538
+ height={22}
539
+ viewBox="0 0 14 14"
540
+ fill="none"
541
+ strokeWidth={1.3}
542
+ strokeLinejoin="round"
543
+ aria-hidden="true"
544
+ style={{
545
+ flexShrink: 0,
546
+ opacity: opened ? 0.95 : hovered ? 0.9 : 0.75,
547
+ overflow: 'visible',
548
+ transition: 'opacity 200ms ease',
549
+ }}
550
+ >
551
+ <rect
552
+ x={2}
553
+ y={3}
554
+ width={7}
555
+ height={5}
556
+ rx={1}
557
+ stroke={stroke}
558
+ style={{
559
+ transition: rectTransition,
560
+ transform: opened
561
+ ? 'translate(-1.5px, -1.5px)'
562
+ : 'translate(0px, 0px)',
563
+ }}
564
+ />
565
+ <rect
566
+ x={5}
567
+ y={6}
568
+ width={7}
569
+ height={5}
570
+ rx={1}
571
+ stroke={stroke}
572
+ fill={fill}
573
+ style={{
574
+ transition: rectTransition,
575
+ transform: opened
576
+ ? 'translate(1.5px, 1.5px)'
577
+ : 'translate(0px, 0px)',
578
+ fillOpacity: opened ? 0.35 : 0,
579
+ }}
580
+ />
581
+ </svg>
582
+ );
583
+ }
584
+
458
585
  /**
459
586
  * Swimlane headers layer that renders on top of nodes for clickability
460
587
  */
461
588
  function SwimlaneHeadersLayer({
462
589
  swimlanes,
590
+ parentHeaders,
463
591
  laneWidth,
464
592
  headerHeight,
465
- onToggleCollapse,
593
+ onToggleNamespace,
594
+ closingNamespaces,
466
595
  stickyHeaders = true,
467
596
  transparent = false,
468
597
  }: SwimlaneLayerProps) {
469
598
  const { x, y, zoom } = useViewport();
470
599
  const { theme } = useTheme();
600
+ const [hoveredNamespace, setHoveredNamespace] = useState<string | null>(null);
601
+
602
+ // When sticky headers are enabled, drop vertical translation from the wrapper
603
+ // so headers stay locked to the top regardless of vertical pan. The inner
604
+ // cells then keep their natural `top` values (no per-scroll recompute), which
605
+ // avoids fighting the CSS transition on `top` used for drill-toggle animations.
606
+ const wrapperY = stickyHeaders ? 0 : y;
607
+
608
+ // Build a unified header list: each namespace currently in view (whether a
609
+ // leaf or an opened ancestor) gets ONE DOM element keyed by namespace, so
610
+ // clicking ▶ smoothly morphs the same cell from leaf-shape to parent-shape
611
+ // (wider, possibly across multiple lanes) instead of unmounting+remounting.
612
+ type HeaderCell = {
613
+ namespace: string;
614
+ label: string;
615
+ x: number;
616
+ width: number;
617
+ depth: number;
618
+ isOpened: boolean; // currently in `openedNamespaces`
619
+ isParentOpened: boolean;
620
+ canExpand: boolean; // only meaningful when !isOpened
621
+ };
622
+
623
+ const headers: HeaderCell[] = useMemo(
624
+ () => [
625
+ ...parentHeaders.map((h) => ({
626
+ namespace: h.namespace,
627
+ label: h.label,
628
+ x: h.x,
629
+ width: h.width,
630
+ depth: h.depth,
631
+ isOpened: true,
632
+ isParentOpened: h.depth > 1,
633
+ canExpand: false,
634
+ })),
635
+ ...swimlanes.map((lane) => ({
636
+ namespace: lane.namespace,
637
+ label: lane.label,
638
+ x: lane.x,
639
+ width: laneWidth,
640
+ depth: lane.namespace.split('.').length,
641
+ isOpened: false,
642
+ isParentOpened: lane.isParentOpened,
643
+ canExpand: lane.canExpand,
644
+ })),
645
+ ],
646
+ [parentHeaders, swimlanes, laneWidth]
647
+ );
471
648
 
472
- // When sticky headers are enabled, compensate for vertical viewport panning
473
- // to keep headers at the top of the screen
474
- const headerTop = stickyHeaders ? -y / zoom : 0;
475
649
 
476
650
  return (
477
651
  <div
@@ -480,59 +654,131 @@ function SwimlaneHeadersLayer({
480
654
  top: 0,
481
655
  left: 0,
482
656
  transformOrigin: '0 0',
483
- transform: `translate(${x}px, ${y}px) scale(${zoom})`,
657
+ transform: `translate(${x}px, ${wrapperY}px) scale(${zoom})`,
484
658
  pointerEvents: 'none',
485
659
  zIndex: 10,
486
660
  }}
487
661
  >
488
- {/* Lane headers */}
489
- {swimlanes.map((lane) => {
490
- const hasChildren = lane.children.length > 0;
662
+ {headers.map((header) => {
663
+ const rowTop = (header.depth - 1) * headerHeight;
664
+ // Child leaves (under an opened parent) get the differentiated, lighter
665
+ // styling. Top-level leaves AND opened parents share the original
666
+ // header look — so the cell you clicked doesn't change appearance, it
667
+ // just grows to span its children.
668
+ const isChild = header.isParentOpened && !header.isOpened;
669
+ const showOpen = header.canExpand && !header.isOpened;
670
+ const isClickable = header.isOpened || showOpen;
671
+ // If this child's parent is mid-close, play the exit animation
672
+ // instead of the entry. After SWIMLANE_CLOSE_EXIT_MS the data
673
+ // change applies and the child unmounts.
674
+ const parentNs =
675
+ header.depth > 1
676
+ ? header.namespace.split('.').slice(0, -1).join('.')
677
+ : undefined;
678
+ const isExiting =
679
+ isChild && !!parentNs && !!closingNamespaces?.has(parentNs);
680
+ const cellClassName = isExiting
681
+ ? 'swimlane-child-out'
682
+ : isChild
683
+ ? 'swimlane-child-in'
684
+ : 'swimlane-fade-in';
491
685
  return (
492
686
  <div
493
- key={`header-${lane.namespace}`}
687
+ key={header.namespace}
688
+ className={cellClassName}
689
+ role={isClickable ? 'button' : undefined}
690
+ aria-label={
691
+ header.isOpened
692
+ ? `Close ${header.namespace}`
693
+ : showOpen
694
+ ? `Open ${header.namespace}`
695
+ : undefined
696
+ }
697
+ title={header.namespace}
494
698
  style={{
495
699
  position: 'absolute',
496
- left: lane.x - laneWidth / 2,
497
- top: headerTop,
498
- width: laneWidth,
700
+ left: header.x - header.width / 2,
701
+ top: rowTop,
702
+ width: header.width,
499
703
  height: headerHeight,
500
704
  display: 'flex',
501
705
  alignItems: 'center',
502
706
  justifyContent: 'center',
503
707
  padding: '0 8px',
504
708
  boxSizing: 'border-box',
505
- backgroundColor: transparent ? 'transparent' : theme.colors.muted,
506
- borderBottom: `2px solid ${theme.colors.border}`,
507
- fontWeight: theme.fontWeights.semibold,
508
- fontSize: theme.fontSizes[2],
709
+ backgroundColor: transparent
710
+ ? 'transparent'
711
+ : isChild
712
+ ? theme.colors.background
713
+ : theme.colors.muted,
714
+ transition: swimlaneTransition,
715
+ borderBottom: isChild
716
+ ? `1px solid ${theme.colors.border}`
717
+ : `2px solid ${theme.colors.border}`,
718
+ borderLeft: isChild
719
+ ? `1px solid ${theme.colors.border}`
720
+ : 'none',
721
+ borderRight: isChild
722
+ ? `1px solid ${theme.colors.border}`
723
+ : 'none',
724
+ fontWeight: isChild
725
+ ? theme.fontWeights.medium
726
+ : theme.fontWeights.semibold,
727
+ fontSize: isChild ? theme.fontSizes[1] : theme.fontSizes[2],
509
728
  fontFamily: theme.fonts.heading,
510
- color: theme.colors.text,
729
+ color: isChild ? theme.colors.textSecondary : theme.colors.text,
511
730
  pointerEvents: 'auto',
512
- cursor: hasChildren ? 'pointer' : 'default',
513
731
  userSelect: 'none',
732
+ cursor: isClickable ? 'pointer' : 'default',
733
+ gap: 6,
734
+ // Parent (opened) cells sit above leaves so children slide out
735
+ // from behind them rather than over the top.
736
+ zIndex: header.isOpened ? 2 : 1,
514
737
  }}
515
- onClick={() => hasChildren && onToggleCollapse?.(lane.namespace)}
738
+ onClick={
739
+ isClickable
740
+ ? (e) => {
741
+ e.stopPropagation();
742
+ onToggleNamespace?.(header.namespace);
743
+ }
744
+ : undefined
745
+ }
746
+ onMouseEnter={
747
+ isClickable
748
+ ? () => setHoveredNamespace(header.namespace)
749
+ : undefined
750
+ }
751
+ onMouseLeave={
752
+ isClickable
753
+ ? () =>
754
+ setHoveredNamespace((current) =>
755
+ current === header.namespace ? null : current
756
+ )
757
+ : undefined
758
+ }
516
759
  >
517
- {hasChildren && (
518
- <span style={{ marginRight: 6, fontSize: 10 }}>
519
- {lane.isCollapsed ? '▼' : '▶'}
520
- </span>
521
- )}
522
760
  <span
523
761
  style={{
524
762
  overflowWrap: 'anywhere',
525
763
  wordBreak: 'break-word',
526
764
  lineHeight: 1.2,
527
765
  textAlign: 'center',
766
+ flex: 1,
528
767
  }}
529
- title={lane.label}
530
768
  >
531
- {lane.label}
769
+ {header.label}
532
770
  </span>
771
+ {(header.isOpened || showOpen) && (
772
+ <StackIcon
773
+ opened={header.isOpened}
774
+ hovered={hoveredNamespace === header.namespace}
775
+ accentColor={theme.colors.primary}
776
+ />
777
+ )}
533
778
  </div>
534
779
  );
535
780
  })}
781
+
536
782
  </div>
537
783
  );
538
784
  }
@@ -556,8 +802,12 @@ export interface SequenceDiagramRendererProps {
556
802
  /** Optional custom edge types */
557
803
  edgeTypes?: EdgeTypes;
558
804
 
559
- /** Callback when a namespace collapse state is toggled */
560
- onToggleCollapse?: (namespace: string) => void;
805
+ /**
806
+ * Called when the user toggles a lane's drill state via the header
807
+ * chevrons. Update `layoutOptions.openedNamespaces` in response to
808
+ * open/close the lane.
809
+ */
810
+ onToggleNamespace?: (namespace: string) => void;
561
811
 
562
812
  /** Callback when a node is clicked */
563
813
  onNodeClick?: (nodeId: string, event: React.MouseEvent) => void;
@@ -604,7 +854,7 @@ function SequenceDiagramInner({
604
854
  layoutOptions = {},
605
855
  nodeTypes: customNodeTypes,
606
856
  edgeTypes: customEdgeTypes,
607
- onToggleCollapse,
857
+ onToggleNamespace,
608
858
  onNodeClick,
609
859
  showControls = true,
610
860
  showBackground = false, // Default to false since swimlanes provide visual structure
@@ -618,6 +868,67 @@ function SequenceDiagramInner({
618
868
  // Extract layout params
619
869
  const { laneWidth = 250, headerHeight = 60 } = layoutOptions;
620
870
 
871
+ // openedNamespaces is controlled if provided in layoutOptions, otherwise
872
+ // we manage it internally so chevrons work out of the box.
873
+ const isOpenedControlled = layoutOptions.openedNamespaces !== undefined;
874
+ const [internalOpened, setInternalOpened] = useState<string[]>([]);
875
+ const effectiveOpened = isOpenedControlled
876
+ ? layoutOptions.openedNamespaces
877
+ : internalOpened;
878
+
879
+ // Namespaces currently mid-close. While one is here, the children still
880
+ // render in the data (lifelines, events, edges unchanged) but their header
881
+ // cells flip to the exit animation. After the exit completes we apply the
882
+ // real data change, so the diagram body shifts in sync with the parent
883
+ // header shrink instead of ahead of it.
884
+ const [closingNamespaces, setClosingNamespaces] = useState<Set<string>>(
885
+ () => new Set()
886
+ );
887
+
888
+ const handleToggleNamespace = useCallback(
889
+ (namespace: string) => {
890
+ const openedSet =
891
+ effectiveOpened instanceof Set
892
+ ? effectiveOpened
893
+ : new Set(effectiveOpened ?? []);
894
+ const isCurrentlyOpened = openedSet.has(namespace);
895
+
896
+ if (isCurrentlyOpened) {
897
+ // Stage the close: animate children out first, then apply data change.
898
+ setClosingNamespaces((prev) => {
899
+ if (prev.has(namespace)) return prev;
900
+ const next = new Set(prev);
901
+ next.add(namespace);
902
+ return next;
903
+ });
904
+ setTimeout(() => {
905
+ if (!isOpenedControlled) {
906
+ setInternalOpened((prev) => prev.filter((n) => n !== namespace));
907
+ }
908
+ onToggleNamespace?.(namespace);
909
+ setClosingNamespaces((prev) => {
910
+ if (!prev.has(namespace)) return prev;
911
+ const next = new Set(prev);
912
+ next.delete(namespace);
913
+ return next;
914
+ });
915
+ }, SWIMLANE_CLOSE_EXIT_MS);
916
+ } else {
917
+ // Open: data change is immediate; children animate in via CSS.
918
+ if (!isOpenedControlled) {
919
+ setInternalOpened((prev) => [...prev, namespace]);
920
+ }
921
+ onToggleNamespace?.(namespace);
922
+ }
923
+ },
924
+ [effectiveOpened, isOpenedControlled, onToggleNamespace]
925
+ );
926
+
927
+ const effectiveLayoutOptions = useMemo(
928
+ () => ({ ...layoutOptions, openedNamespaces: effectiveOpened }),
929
+ [layoutOptions, effectiveOpened]
930
+ );
931
+
621
932
  // Merge custom node/edge types with sequence defaults
622
933
  const nodeTypes = useMemo(
623
934
  () => ({ ...defaultSequenceNodeTypes, ...customNodeTypes }),
@@ -629,11 +940,15 @@ function SequenceDiagramInner({
629
940
  );
630
941
 
631
942
  // Compute layout
632
- const { nodes: layoutNodes, edges, swimlanes, totalWidth, totalHeight } = useSequenceLayout(
633
- events,
634
- sequenceEdges,
635
- layoutOptions
636
- );
943
+ const {
944
+ nodes: layoutNodes,
945
+ edges,
946
+ swimlanes,
947
+ parentHeaders,
948
+ headerRows,
949
+ totalWidth,
950
+ totalHeight,
951
+ } = useSequenceLayout(events, sequenceEdges, effectiveLayoutOptions);
637
952
 
638
953
  // Mark selected node and add showEventLabels to node data
639
954
  const nodes = useMemo(() => {
@@ -730,6 +1045,7 @@ function SequenceDiagramInner({
730
1045
  translateExtent={translateExtent}
731
1046
  style={{ background: transparent ? 'transparent' : theme.colors.background }}
732
1047
  >
1048
+ <style>{swimlaneAnimationStyles}</style>
733
1049
  {/* SVG defs for arrow markers */}
734
1050
  <svg style={{ position: 'absolute', width: 0, height: 0 }}>
735
1051
  <defs>
@@ -774,6 +1090,8 @@ function SequenceDiagramInner({
774
1090
  {/* Swimlane layer - renders behind nodes */}
775
1091
  <SwimlaneLayer
776
1092
  swimlanes={swimlanes}
1093
+ parentHeaders={parentHeaders}
1094
+ headerRows={headerRows}
777
1095
  laneWidth={laneWidth}
778
1096
  headerHeight={headerHeight}
779
1097
  totalHeight={totalHeight}
@@ -783,32 +1101,17 @@ function SequenceDiagramInner({
783
1101
  {/* Swimlane headers layer - renders on top for clickability */}
784
1102
  <SwimlaneHeadersLayer
785
1103
  swimlanes={swimlanes}
1104
+ parentHeaders={parentHeaders}
1105
+ headerRows={headerRows}
786
1106
  laneWidth={laneWidth}
787
1107
  headerHeight={headerHeight}
788
1108
  totalHeight={totalHeight}
789
- onToggleCollapse={onToggleCollapse}
1109
+ onToggleNamespace={handleToggleNamespace}
1110
+ closingNamespaces={closingNamespaces}
790
1111
  stickyHeaders={stickyHeaders}
791
1112
  transparent={transparent}
792
1113
  />
793
1114
 
794
- {/* Collapse toggle panel (for namespaces with children) */}
795
- {swimlanes.some((s) => s.children.length > 0) && (
796
- <Panel position="top-right">
797
- <div
798
- style={{
799
- background: theme.colors.background,
800
- border: `1px solid ${theme.colors.border}`,
801
- borderRadius: 4,
802
- padding: '6px 10px',
803
- fontSize: theme.fontSizes[0],
804
- fontFamily: theme.fonts.body,
805
- color: theme.colors.textSecondary,
806
- }}
807
- >
808
- Click lane headers to expand/collapse
809
- </div>
810
- </Panel>
811
- )}
812
1115
  </ReactFlow>
813
1116
  );
814
1117
  }
@@ -146,8 +146,8 @@ function convertWorkflowToSequence(
146
146
  const isMoveEvent = eventToMoveEventMap.get(eventName) ?? true;
147
147
 
148
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.
149
+ // participant's lane (the layout uses the first dotted segment as the
150
+ // default lane). Otherwise, use the event name as-is.
151
151
  const name = canvasScope ? `${canvasScope}.${eventName}` : eventName;
152
152
  const participant = canvasScope ?? eventName.split('.')[0] ?? eventName;
153
153
 
@@ -186,10 +186,7 @@ function convertWorkflowToSequence(
186
186
  * scenario={workflowScenario}
187
187
  * canvas={otelCanvas}
188
188
  * height={600}
189
- * layoutOptions={{
190
- * namespaceStrategy: 'first',
191
- * eventSpacing: 80,
192
- * }}
189
+ * layoutOptions={{ eventSpacing: 80 }}
193
190
  * />
194
191
  * ```
195
192
  */