@djangocfg/widget-diagram 0.1.1
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/LICENSE +21 -0
- package/README.md +84 -0
- package/package.json +77 -0
- package/src/FloatingToolbar/FloatingToolbar.css +5 -0
- package/src/FloatingToolbar/actions/CopyAction.tsx +31 -0
- package/src/FloatingToolbar/actions/DownloadAction.tsx +51 -0
- package/src/FloatingToolbar/actions/ExpandAction.tsx +33 -0
- package/src/FloatingToolbar/actions/FullscreenAction.tsx +38 -0
- package/src/FloatingToolbar/actions/index.ts +4 -0
- package/src/FloatingToolbar/hooks/useScrollIsolation.ts +62 -0
- package/src/FloatingToolbar/index.tsx +184 -0
- package/src/Mermaid.client.tsx +97 -0
- package/src/builders/FlowDiagram/FlowDiagram.ts +96 -0
- package/src/builders/FlowDiagram/functions/getEdges.ts +50 -0
- package/src/builders/FlowDiagram/functions/getNodes.ts +43 -0
- package/src/builders/FlowDiagram/functions/getStyles.ts +90 -0
- package/src/builders/FlowDiagram/functions/index.ts +8 -0
- package/src/builders/FlowDiagram/index.ts +16 -0
- package/src/builders/FlowDiagram/types.ts +130 -0
- package/src/builders/JourneyDiagram/JourneyDiagram.ts +88 -0
- package/src/builders/JourneyDiagram/index.ts +12 -0
- package/src/builders/JourneyDiagram/types.ts +48 -0
- package/src/builders/SequenceDiagram/SequenceDiagram.ts +158 -0
- package/src/builders/SequenceDiagram/functions/getActivations.ts +30 -0
- package/src/builders/SequenceDiagram/functions/getBlocks.ts +112 -0
- package/src/builders/SequenceDiagram/functions/getMessages.ts +85 -0
- package/src/builders/SequenceDiagram/functions/getNotes.ts +94 -0
- package/src/builders/SequenceDiagram/functions/index.ts +16 -0
- package/src/builders/SequenceDiagram/index.ts +18 -0
- package/src/builders/SequenceDiagram/types.ts +192 -0
- package/src/builders/core/DiagramStore.ts +138 -0
- package/src/builders/core/index.ts +8 -0
- package/src/builders/core/sanitize.ts +83 -0
- package/src/builders/core/theme.ts +42 -0
- package/src/builders/core/types.ts +183 -0
- package/src/builders/index.ts +96 -0
- package/src/components/MermaidCodeViewer.tsx +95 -0
- package/src/components/MermaidErrorPanel.tsx +31 -0
- package/src/components/MermaidFullscreenModal.tsx +201 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useMermaidCleanup.ts +70 -0
- package/src/hooks/useMermaidFullscreen.ts +46 -0
- package/src/hooks/useMermaidRenderer.ts +329 -0
- package/src/hooks/useMermaidValidation.ts +97 -0
- package/src/index.tsx +79 -0
- package/src/lazy.tsx +40 -0
- package/src/mermaid.stories.tsx +217 -0
- package/src/types.ts +28 -0
- package/src/utils/mermaid-helpers.ts +157 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useResolvedTheme } from '@djangocfg/ui-core/hooks';
|
|
6
|
+
import { FloatingToolbar } from './FloatingToolbar';
|
|
7
|
+
import { CopyAction, FullscreenAction } from './FloatingToolbar/actions';
|
|
8
|
+
import { MermaidErrorPanel } from './components/MermaidErrorPanel';
|
|
9
|
+
import { MermaidFullscreenModal } from './components/MermaidFullscreenModal';
|
|
10
|
+
import { useMermaidFullscreen } from './hooks/useMermaidFullscreen';
|
|
11
|
+
import { useMermaidRenderer } from './hooks/useMermaidRenderer';
|
|
12
|
+
import type { MermaidProps } from './types';
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const Mermaid: React.FC<MermaidProps> = ({
|
|
16
|
+
chart,
|
|
17
|
+
className = '',
|
|
18
|
+
isCompact = false,
|
|
19
|
+
fullscreen = true,
|
|
20
|
+
scrollIsolation = false,
|
|
21
|
+
debounceMs = 300,
|
|
22
|
+
}) => {
|
|
23
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
const theme = useResolvedTheme();
|
|
25
|
+
|
|
26
|
+
// Rendering logic
|
|
27
|
+
const { mermaidRef, svgContent, isVertical, isRendering, error } = useMermaidRenderer({
|
|
28
|
+
chart,
|
|
29
|
+
theme,
|
|
30
|
+
isCompact,
|
|
31
|
+
debounceMs,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Fullscreen modal logic (only used if fullscreen prop is true)
|
|
35
|
+
const {
|
|
36
|
+
isFullscreen,
|
|
37
|
+
fullscreenRef,
|
|
38
|
+
openFullscreen,
|
|
39
|
+
closeFullscreen,
|
|
40
|
+
handleBackdropClick,
|
|
41
|
+
} = useMermaidFullscreen();
|
|
42
|
+
|
|
43
|
+
// Hoist derived UI state out of JSX (data-before-JSX).
|
|
44
|
+
const hasError = error !== null;
|
|
45
|
+
// The spinner needs a box to sit in while the first diagram renders
|
|
46
|
+
// (empty SVG host has no intrinsic height).
|
|
47
|
+
const showPlaceholderHeight = isRendering && !svgContent && !hasError;
|
|
48
|
+
const showToolbar = !!svgContent && !isRendering && !hasError;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<div ref={containerRef} className={`relative ${className}`}>
|
|
53
|
+
{hasError && <MermaidErrorPanel message={error} />}
|
|
54
|
+
|
|
55
|
+
<div
|
|
56
|
+
ref={mermaidRef}
|
|
57
|
+
className="flex justify-center items-center"
|
|
58
|
+
style={{
|
|
59
|
+
isolation: 'isolate',
|
|
60
|
+
minHeight: showPlaceholderHeight ? 100 : undefined,
|
|
61
|
+
display: hasError ? 'none' : undefined,
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
{isRendering && !hasError && (
|
|
66
|
+
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
67
|
+
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary" />
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{showToolbar && (
|
|
72
|
+
<FloatingToolbar containerRef={containerRef} scrollIsolation={scrollIsolation}>
|
|
73
|
+
<CopyAction value={chart} title="Copy source" />
|
|
74
|
+
{fullscreen && (
|
|
75
|
+
<FullscreenAction onToggle={openFullscreen} title="Fullscreen" />
|
|
76
|
+
)}
|
|
77
|
+
</FloatingToolbar>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{fullscreen && (
|
|
82
|
+
<MermaidFullscreenModal
|
|
83
|
+
isOpen={isFullscreen}
|
|
84
|
+
svgContent={svgContent}
|
|
85
|
+
isVertical={isVertical}
|
|
86
|
+
theme={theme}
|
|
87
|
+
chart={chart}
|
|
88
|
+
fullscreenRef={fullscreenRef}
|
|
89
|
+
onClose={closeFullscreen}
|
|
90
|
+
onBackdropClick={handleBackdropClick}
|
|
91
|
+
/>
|
|
92
|
+
)}
|
|
93
|
+
</>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default Mermaid;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlowDiagram Builder
|
|
3
|
+
* Declarative API for building Mermaid flowchart diagrams
|
|
4
|
+
* @module Mermaid/builders/FlowDiagram/FlowDiagram
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DiagramStore } from '../core/DiagramStore';
|
|
8
|
+
import type { FlowDirection } from '../core/types';
|
|
9
|
+
import { createNodeBuilder } from './functions/getNodes';
|
|
10
|
+
import { createEdgeBuilder } from './functions/getEdges';
|
|
11
|
+
import { createStyleBuilder } from './functions/getStyles';
|
|
12
|
+
import type {
|
|
13
|
+
FlowDiagramOptions,
|
|
14
|
+
FlowDiagramBuilder,
|
|
15
|
+
SubgraphBuilder,
|
|
16
|
+
NodeBuilder,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
const DEFAULT_OPTIONS: Required<FlowDiagramOptions> = {
|
|
20
|
+
direction: 'TB',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a FlowDiagram builder
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const flow = FlowDiagram<'A' | 'B' | 'C'>({ direction: 'LR' });
|
|
29
|
+
*
|
|
30
|
+
* flow.node('A').rect('Start');
|
|
31
|
+
* flow.node('B').circle('Process');
|
|
32
|
+
* flow.node('C').rect('End');
|
|
33
|
+
*
|
|
34
|
+
* flow.edge('A').to('B').solid();
|
|
35
|
+
* flow.edge('B').to('C').dotted('optional');
|
|
36
|
+
*
|
|
37
|
+
* flow.style.define('highlight', { fill: '#ff0', stroke: '#f00' });
|
|
38
|
+
* flow.style.apply('highlight', 'B');
|
|
39
|
+
*
|
|
40
|
+
* console.log(flow.toString());
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @param options - Diagram options
|
|
44
|
+
* @returns FlowDiagram builder instance
|
|
45
|
+
*/
|
|
46
|
+
export function FlowDiagram<Nodes extends string = string>(
|
|
47
|
+
options: FlowDiagramOptions = {},
|
|
48
|
+
): FlowDiagramBuilder<Nodes> {
|
|
49
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
50
|
+
const store = new DiagramStore(`graph ${opts.direction}`);
|
|
51
|
+
const styleBuilder = createStyleBuilder(store);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a subgraph builder that operates within the subgraph context
|
|
55
|
+
*/
|
|
56
|
+
const createSubgraphBuilder = (): SubgraphBuilder<Nodes> => ({
|
|
57
|
+
direction(dir: FlowDirection) {
|
|
58
|
+
store.add(`direction ${dir}`);
|
|
59
|
+
},
|
|
60
|
+
node(id: Nodes): NodeBuilder<Nodes> {
|
|
61
|
+
return createNodeBuilder<Nodes>(store, id);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
node(id: Nodes): NodeBuilder<Nodes> {
|
|
67
|
+
return createNodeBuilder<Nodes>(store, id);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
edge(from: Nodes) {
|
|
71
|
+
return createEdgeBuilder<Nodes>(store, from);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
subgraph(name: string, fn: (sub: SubgraphBuilder<Nodes>) => void) {
|
|
75
|
+
store.add(`subgraph ${name}`);
|
|
76
|
+
store.indent();
|
|
77
|
+
fn(createSubgraphBuilder());
|
|
78
|
+
store.dedent();
|
|
79
|
+
store.add('end');
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
style: styleBuilder,
|
|
83
|
+
|
|
84
|
+
comment(text: string) {
|
|
85
|
+
store.addComment(text);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
blank() {
|
|
89
|
+
store.addBlank();
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
toString() {
|
|
93
|
+
return store.toString();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge builder functions for FlowDiagram
|
|
3
|
+
* @module Mermaid/builders/FlowDiagram/functions/getEdges
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DiagramStore } from '../../core/DiagramStore';
|
|
7
|
+
import { SIMPLE_EDGE_SYNTAX } from '../../core/types';
|
|
8
|
+
import { toNodeId, sanitizeLabel } from '../../core/sanitize';
|
|
9
|
+
import type { EdgeBuilder, EdgeEndBuilder } from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create an edge builder for a specific source node
|
|
13
|
+
*/
|
|
14
|
+
export function createEdgeBuilder<Nodes extends string>(
|
|
15
|
+
store: DiagramStore,
|
|
16
|
+
fromId: Nodes,
|
|
17
|
+
): EdgeBuilder<Nodes> {
|
|
18
|
+
const safeFromId = toNodeId(fromId);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
to(toId: Nodes): EdgeEndBuilder {
|
|
22
|
+
const safeToId = toNodeId(toId);
|
|
23
|
+
|
|
24
|
+
const addEdge = (arrow: string, label?: string) => {
|
|
25
|
+
if (label) {
|
|
26
|
+
store.add(`${safeFromId} ${arrow}|${sanitizeLabel(label)}| ${safeToId}`);
|
|
27
|
+
} else {
|
|
28
|
+
store.add(`${safeFromId} ${arrow} ${safeToId}`);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
solid: (label) => addEdge(SIMPLE_EDGE_SYNTAX.solid, label),
|
|
34
|
+
dotted: (label) => addEdge(SIMPLE_EDGE_SYNTAX.dotted, label),
|
|
35
|
+
thick: (label) => addEdge(SIMPLE_EDGE_SYNTAX.thick, label),
|
|
36
|
+
line: (label) => addEdge(SIMPLE_EDGE_SYNTAX.solidOpen, label),
|
|
37
|
+
dottedLine: (label) => addEdge(SIMPLE_EDGE_SYNTAX.dottedOpen, label),
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create multiple edges at once (for convenience)
|
|
45
|
+
*/
|
|
46
|
+
export function createEdgesBuilder<Nodes extends string>(
|
|
47
|
+
store: DiagramStore,
|
|
48
|
+
): (from: Nodes) => EdgeBuilder<Nodes> {
|
|
49
|
+
return (from: Nodes) => createEdgeBuilder(store, from);
|
|
50
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node builder functions for FlowDiagram
|
|
3
|
+
* @module Mermaid/builders/FlowDiagram/functions/getNodes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DiagramStore } from '../../core/DiagramStore';
|
|
7
|
+
import { NODE_SHAPE_SYNTAX, type NodeShape } from '../../core/types';
|
|
8
|
+
import { formatLabel, toNodeId } from '../../core/sanitize';
|
|
9
|
+
import type { NodeBuilder } from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a node builder for a specific node ID
|
|
13
|
+
*/
|
|
14
|
+
export function createNodeBuilder<Nodes extends string>(
|
|
15
|
+
store: DiagramStore,
|
|
16
|
+
nodeId: Nodes,
|
|
17
|
+
): NodeBuilder<Nodes> {
|
|
18
|
+
const safeId = toNodeId(nodeId);
|
|
19
|
+
|
|
20
|
+
const addNode = (shape: NodeShape, label: string, subtitle?: string) => {
|
|
21
|
+
const [open, close] = NODE_SHAPE_SYNTAX[shape];
|
|
22
|
+
const formattedLabel = formatLabel(label, subtitle);
|
|
23
|
+
|
|
24
|
+
// Use quoted format for complex labels
|
|
25
|
+
if (subtitle || label.includes(' ') || label.includes(':')) {
|
|
26
|
+
store.add(`${safeId}${open}"${formattedLabel}"${close}`);
|
|
27
|
+
} else {
|
|
28
|
+
store.add(`${safeId}${open}${formattedLabel}${close}`);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
rect: (label, subtitle) => addNode('rect', label, subtitle),
|
|
34
|
+
round: (label, subtitle) => addNode('round', label, subtitle),
|
|
35
|
+
stadium: (label, subtitle) => addNode('stadium', label, subtitle),
|
|
36
|
+
circle: (label, subtitle) => addNode('circle', label, subtitle),
|
|
37
|
+
rhombus: (label, subtitle) => addNode('rhombus', label, subtitle),
|
|
38
|
+
hexagon: (label, subtitle) => addNode('hexagon', label, subtitle),
|
|
39
|
+
cylinder: (label, subtitle) => addNode('cylinder', label, subtitle),
|
|
40
|
+
subroutine: (label, subtitle) => addNode('subroutine', label, subtitle),
|
|
41
|
+
doubleCircle: (label, subtitle) => addNode('doubleCircle', label, subtitle),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style builder functions for FlowDiagram
|
|
3
|
+
* @module Mermaid/builders/FlowDiagram/functions/getStyles
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DiagramStore } from '../../core/DiagramStore';
|
|
7
|
+
import type { StyleProperties } from '../../core/types';
|
|
8
|
+
import { toNodeId } from '../../core/sanitize';
|
|
9
|
+
import type { StyleBuilder } from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert style properties to Mermaid classDef syntax
|
|
13
|
+
*/
|
|
14
|
+
function propertiesToString(properties: StyleProperties): string {
|
|
15
|
+
const parts: string[] = [];
|
|
16
|
+
|
|
17
|
+
if (properties.fill) parts.push(`fill:${properties.fill}`);
|
|
18
|
+
if (properties.stroke) parts.push(`stroke:${properties.stroke}`);
|
|
19
|
+
if (properties['stroke-width']) parts.push(`stroke-width:${properties['stroke-width']}`);
|
|
20
|
+
if (properties.color) parts.push(`color:${properties.color}`);
|
|
21
|
+
if (properties['font-weight']) parts.push(`font-weight:${properties['font-weight']}`);
|
|
22
|
+
if (properties['stroke-dasharray']) parts.push(`stroke-dasharray:${properties['stroke-dasharray']}`);
|
|
23
|
+
|
|
24
|
+
return parts.join(',');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a style builder
|
|
29
|
+
*/
|
|
30
|
+
export function createStyleBuilder(store: DiagramStore): StyleBuilder {
|
|
31
|
+
return {
|
|
32
|
+
define(name: string, properties: StyleProperties): void {
|
|
33
|
+
const propsStr = propertiesToString(properties);
|
|
34
|
+
store.add(`classDef ${name} ${propsStr}`);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
apply(className: string, ...nodeIds: string[]): void {
|
|
38
|
+
if (nodeIds.length === 0) return;
|
|
39
|
+
const safeIds = nodeIds.map((id) => toNodeId(id)).join(',');
|
|
40
|
+
store.add(`class ${safeIds} ${className}`);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Predefined Style Classes (Common patterns)
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Common style presets for quick use
|
|
51
|
+
*/
|
|
52
|
+
export const STYLE_PRESETS = {
|
|
53
|
+
/** Green success style */
|
|
54
|
+
success: {
|
|
55
|
+
fill: '#22c55e',
|
|
56
|
+
stroke: '#16a34a',
|
|
57
|
+
color: '#fff',
|
|
58
|
+
},
|
|
59
|
+
/** Blue primary style */
|
|
60
|
+
primary: {
|
|
61
|
+
fill: '#3b82f6',
|
|
62
|
+
stroke: '#1d4ed8',
|
|
63
|
+
color: '#fff',
|
|
64
|
+
},
|
|
65
|
+
/** Red error/danger style */
|
|
66
|
+
danger: {
|
|
67
|
+
fill: '#ef4444',
|
|
68
|
+
stroke: '#dc2626',
|
|
69
|
+
color: '#fff',
|
|
70
|
+
},
|
|
71
|
+
/** Yellow warning style */
|
|
72
|
+
warning: {
|
|
73
|
+
fill: '#f59e0b',
|
|
74
|
+
stroke: '#d97706',
|
|
75
|
+
color: '#fff',
|
|
76
|
+
},
|
|
77
|
+
/** Gray muted style */
|
|
78
|
+
muted: {
|
|
79
|
+
fill: '#6b7280',
|
|
80
|
+
stroke: '#4b5563',
|
|
81
|
+
color: '#fff',
|
|
82
|
+
},
|
|
83
|
+
/** Emerald/teal accent */
|
|
84
|
+
accent: {
|
|
85
|
+
fill: '#10b981',
|
|
86
|
+
stroke: '#047857',
|
|
87
|
+
color: '#fff',
|
|
88
|
+
'font-weight': 'bold' as const,
|
|
89
|
+
},
|
|
90
|
+
} satisfies Record<string, StyleProperties>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlowDiagram function exports
|
|
3
|
+
* @module Mermaid/builders/FlowDiagram/functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { createNodeBuilder } from './getNodes';
|
|
7
|
+
export { createEdgeBuilder, createEdgesBuilder } from './getEdges';
|
|
8
|
+
export { createStyleBuilder, STYLE_PRESETS } from './getStyles';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlowDiagram builder exports
|
|
3
|
+
* @module Mermaid/builders/FlowDiagram
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { FlowDiagram } from './FlowDiagram';
|
|
7
|
+
export { STYLE_PRESETS } from './functions/getStyles';
|
|
8
|
+
export type {
|
|
9
|
+
FlowDiagramOptions,
|
|
10
|
+
FlowDiagramBuilder,
|
|
11
|
+
NodeBuilder,
|
|
12
|
+
EdgeBuilder,
|
|
13
|
+
EdgeEndBuilder,
|
|
14
|
+
StyleBuilder,
|
|
15
|
+
SubgraphBuilder,
|
|
16
|
+
} from './types';
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for FlowDiagram builder
|
|
3
|
+
* @module Mermaid/builders/FlowDiagram/types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FlowDirection, NodeShape, StyleProperties } from '../core/types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Options for FlowDiagram builder
|
|
10
|
+
*/
|
|
11
|
+
export interface FlowDiagramOptions {
|
|
12
|
+
/** Diagram direction (default: 'TB') */
|
|
13
|
+
direction?: FlowDirection;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Node definition
|
|
18
|
+
*/
|
|
19
|
+
export interface NodeDefinition {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
shape: NodeShape;
|
|
23
|
+
subtitle?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Edge definition
|
|
28
|
+
*/
|
|
29
|
+
export interface EdgeDefinition {
|
|
30
|
+
from: string;
|
|
31
|
+
to: string;
|
|
32
|
+
label?: string;
|
|
33
|
+
style: 'solid' | 'dotted' | 'thick';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Style class definition for flow diagrams
|
|
38
|
+
*/
|
|
39
|
+
export interface FlowStyleClass {
|
|
40
|
+
name: string;
|
|
41
|
+
properties: StyleProperties;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Node builder interface - chainable API for creating nodes
|
|
46
|
+
*/
|
|
47
|
+
export interface NodeBuilder<Nodes extends string> {
|
|
48
|
+
/** Create a rectangular node [label] */
|
|
49
|
+
rect(label: string, subtitle?: string): void;
|
|
50
|
+
/** Create a rounded node (label) */
|
|
51
|
+
round(label: string, subtitle?: string): void;
|
|
52
|
+
/** Create a stadium/pill node ([label]) */
|
|
53
|
+
stadium(label: string, subtitle?: string): void;
|
|
54
|
+
/** Create a circular node ((label)) */
|
|
55
|
+
circle(label: string, subtitle?: string): void;
|
|
56
|
+
/** Create a rhombus/diamond node {label} */
|
|
57
|
+
rhombus(label: string, subtitle?: string): void;
|
|
58
|
+
/** Create a hexagon node {{label}} */
|
|
59
|
+
hexagon(label: string, subtitle?: string): void;
|
|
60
|
+
/** Create a cylinder/database node [(label)] */
|
|
61
|
+
cylinder(label: string, subtitle?: string): void;
|
|
62
|
+
/** Create a subroutine node [[label]] */
|
|
63
|
+
subroutine(label: string, subtitle?: string): void;
|
|
64
|
+
/** Create a double circle node (((label))) */
|
|
65
|
+
doubleCircle(label: string, subtitle?: string): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Edge builder interface - chainable API for creating edges
|
|
70
|
+
*/
|
|
71
|
+
export interface EdgeBuilder<Nodes extends string> {
|
|
72
|
+
/** Target node for the edge */
|
|
73
|
+
to(target: Nodes): EdgeEndBuilder;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Edge end builder - configures the edge after target is specified
|
|
78
|
+
*/
|
|
79
|
+
export interface EdgeEndBuilder {
|
|
80
|
+
/** Solid arrow --> */
|
|
81
|
+
solid(label?: string): void;
|
|
82
|
+
/** Dotted arrow -.-> */
|
|
83
|
+
dotted(label?: string): void;
|
|
84
|
+
/** Thick arrow ==> */
|
|
85
|
+
thick(label?: string): void;
|
|
86
|
+
/** Solid line without arrow --- */
|
|
87
|
+
line(label?: string): void;
|
|
88
|
+
/** Dotted line without arrow -.- */
|
|
89
|
+
dottedLine(label?: string): void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Style builder interface
|
|
94
|
+
*/
|
|
95
|
+
export interface StyleBuilder {
|
|
96
|
+
/** Define a style class */
|
|
97
|
+
define(name: string, properties: StyleProperties): void;
|
|
98
|
+
/** Apply a class to nodes */
|
|
99
|
+
apply(className: string, ...nodeIds: string[]): void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Subgraph builder interface
|
|
104
|
+
*/
|
|
105
|
+
export interface SubgraphBuilder<Nodes extends string> {
|
|
106
|
+
/** Set direction inside subgraph */
|
|
107
|
+
direction(dir: FlowDirection): void;
|
|
108
|
+
/** Create a node inside subgraph */
|
|
109
|
+
node(id: Nodes): NodeBuilder<Nodes>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Main FlowDiagram builder result
|
|
114
|
+
*/
|
|
115
|
+
export interface FlowDiagramBuilder<Nodes extends string> {
|
|
116
|
+
/** Create a node */
|
|
117
|
+
node: (id: Nodes) => NodeBuilder<Nodes>;
|
|
118
|
+
/** Create an edge from a node */
|
|
119
|
+
edge: (from: Nodes) => EdgeBuilder<Nodes>;
|
|
120
|
+
/** Create a subgraph */
|
|
121
|
+
subgraph: (name: string, fn: (sub: SubgraphBuilder<Nodes>) => void) => void;
|
|
122
|
+
/** Style utilities */
|
|
123
|
+
style: StyleBuilder;
|
|
124
|
+
/** Add a comment */
|
|
125
|
+
comment: (text: string) => void;
|
|
126
|
+
/** Add a blank line */
|
|
127
|
+
blank: () => void;
|
|
128
|
+
/** Get the Mermaid string */
|
|
129
|
+
toString: () => string;
|
|
130
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JourneyDiagram Builder
|
|
3
|
+
* Declarative API for building Mermaid user journey diagrams
|
|
4
|
+
* @module Mermaid/builders/JourneyDiagram/JourneyDiagram
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { DiagramStore } from '../core/DiagramStore';
|
|
8
|
+
import type { TaskScore } from '../core/types';
|
|
9
|
+
import { sanitizeLabel } from '../core/sanitize';
|
|
10
|
+
import type { JourneyDiagramOptions, JourneyDiagramBuilder, SectionBuilder } from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Format actors for journey task
|
|
14
|
+
*/
|
|
15
|
+
function formatActors(actors: string | string[]): string {
|
|
16
|
+
if (Array.isArray(actors)) {
|
|
17
|
+
return actors.map(sanitizeLabel).join(', ');
|
|
18
|
+
}
|
|
19
|
+
return sanitizeLabel(actors);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a JourneyDiagram builder
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const journey = JourneyDiagram({ title: 'User Registration Flow' });
|
|
28
|
+
*
|
|
29
|
+
* journey.section('Discovery')
|
|
30
|
+
* .task('Visit website', 5, 'User')
|
|
31
|
+
* .task('Read about product', 4, 'User');
|
|
32
|
+
*
|
|
33
|
+
* journey.section('Registration')
|
|
34
|
+
* .task('Click sign up', 5, 'User')
|
|
35
|
+
* .task('Fill form', 2, 'User')
|
|
36
|
+
* .task('Verify email', 3, ['User', 'System']);
|
|
37
|
+
*
|
|
38
|
+
* journey.section('Onboarding')
|
|
39
|
+
* .task('Complete profile', 4, 'User')
|
|
40
|
+
* .task('Start using app', 5, 'User');
|
|
41
|
+
*
|
|
42
|
+
* console.log(journey.toString());
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @param options - Diagram options (title is required)
|
|
46
|
+
* @returns JourneyDiagram builder instance
|
|
47
|
+
*/
|
|
48
|
+
export function JourneyDiagram(options: JourneyDiagramOptions): JourneyDiagramBuilder {
|
|
49
|
+
const store = new DiagramStore('journey');
|
|
50
|
+
store.add(`title ${sanitizeLabel(options.title)}`);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Add a task line
|
|
54
|
+
*/
|
|
55
|
+
const addTask = (name: string, score: TaskScore, actors: string | string[]) => {
|
|
56
|
+
const actorStr = formatActors(actors);
|
|
57
|
+
store.add(`${sanitizeLabel(name)}: ${score}: ${actorStr}`);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a section builder for chaining tasks
|
|
62
|
+
*/
|
|
63
|
+
const createSectionBuilder = (): SectionBuilder => ({
|
|
64
|
+
task(name: string, score: TaskScore, actors: string | string[]) {
|
|
65
|
+
addTask(name, score, actors);
|
|
66
|
+
return this;
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
section(name: string) {
|
|
72
|
+
store.add(`section ${sanitizeLabel(name)}`);
|
|
73
|
+
return createSectionBuilder();
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
task(name: string, score: TaskScore, actors: string | string[]) {
|
|
77
|
+
addTask(name, score, actors);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
comment(text: string) {
|
|
81
|
+
store.addComment(text);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
toString() {
|
|
85
|
+
return store.toString();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JourneyDiagram builder exports
|
|
3
|
+
* @module Mermaid/builders/JourneyDiagram
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { JourneyDiagram } from './JourneyDiagram';
|
|
7
|
+
export type {
|
|
8
|
+
JourneyDiagramOptions,
|
|
9
|
+
JourneyDiagramBuilder,
|
|
10
|
+
SectionBuilder,
|
|
11
|
+
TaskDefinition,
|
|
12
|
+
} from './types';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for JourneyDiagram builder
|
|
3
|
+
* @module Mermaid/builders/JourneyDiagram/types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TaskScore } from '../core/types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Options for JourneyDiagram builder
|
|
10
|
+
*/
|
|
11
|
+
export interface JourneyDiagramOptions {
|
|
12
|
+
/** Diagram title */
|
|
13
|
+
title: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Task definition
|
|
18
|
+
*/
|
|
19
|
+
export interface TaskDefinition {
|
|
20
|
+
/** Task name/description */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Score (1-5, where 5 is best) */
|
|
23
|
+
score: TaskScore;
|
|
24
|
+
/** Actor(s) involved */
|
|
25
|
+
actors: string | string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Section builder interface
|
|
30
|
+
*/
|
|
31
|
+
export interface SectionBuilder {
|
|
32
|
+
/** Add a task to the current section */
|
|
33
|
+
task(name: string, score: TaskScore, actors: string | string[]): SectionBuilder;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Main JourneyDiagram builder result
|
|
38
|
+
*/
|
|
39
|
+
export interface JourneyDiagramBuilder {
|
|
40
|
+
/** Add a new section */
|
|
41
|
+
section(name: string): SectionBuilder;
|
|
42
|
+
/** Add a task directly (without section - goes to last section or creates implicit one) */
|
|
43
|
+
task(name: string, score: TaskScore, actors: string | string[]): void;
|
|
44
|
+
/** Add a comment */
|
|
45
|
+
comment(text: string): void;
|
|
46
|
+
/** Get the Mermaid string */
|
|
47
|
+
toString(): string;
|
|
48
|
+
}
|