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