@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.
- package/dist/components/SequenceDiagramRenderer.d.ts +6 -2
- package/dist/components/SequenceDiagramRenderer.d.ts.map +1 -1
- package/dist/components/SequenceDiagramRenderer.js +225 -37
- package/dist/components/SequenceDiagramRenderer.js.map +1 -1
- package/dist/components/WorkflowSequenceDiagram.d.ts +1 -4
- package/dist/components/WorkflowSequenceDiagram.d.ts.map +1 -1
- package/dist/components/WorkflowSequenceDiagram.js +3 -6
- package/dist/components/WorkflowSequenceDiagram.js.map +1 -1
- package/dist/hooks/useSequenceLayout.d.ts +51 -27
- package/dist/hooks/useSequenceLayout.d.ts.map +1 -1
- package/dist/hooks/useSequenceLayout.js +140 -118
- package/dist/hooks/useSequenceLayout.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/SequenceDiagramRenderer.tsx +361 -58
- package/src/components/WorkflowSequenceDiagram.tsx +3 -6
- package/src/hooks/useSequenceLayout.ts +192 -163
- package/src/index.ts +0 -1
- package/src/stories/FileCitySequence.stories.tsx +22 -50
- package/src/stories/SequenceDiagram.stories.tsx +12 -29
- package/src/stories/WorkflowSequenceDiagram.stories.tsx +14 -26
|
@@ -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
|
-
|
|
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:
|
|
500
|
+
top: totalHeaderHeight,
|
|
447
501
|
width: 2,
|
|
448
|
-
height: extendedHeight -
|
|
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
|
-
|
|
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, ${
|
|
657
|
+
transform: `translate(${x}px, ${wrapperY}px) scale(${zoom})`,
|
|
484
658
|
pointerEvents: 'none',
|
|
485
659
|
zIndex: 10,
|
|
486
660
|
}}
|
|
487
661
|
>
|
|
488
|
-
{
|
|
489
|
-
|
|
490
|
-
|
|
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={
|
|
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:
|
|
497
|
-
top:
|
|
498
|
-
width:
|
|
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
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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={
|
|
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
|
-
{
|
|
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
|
-
/**
|
|
560
|
-
|
|
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
|
-
|
|
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 {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
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
|
|
150
|
-
//
|
|
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
|
*/
|