@djangocfg/ui-tools 2.1.156 → 2.1.158
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/README.md +149 -2
- package/dist/{Mermaid.client-AF4WOQZR.cjs → Mermaid.client-2TAFAXPW.cjs} +106 -136
- package/dist/Mermaid.client-2TAFAXPW.cjs.map +1 -0
- package/dist/{Mermaid.client-W4QXJX7Q.mjs → Mermaid.client-HG24D5KB.mjs} +107 -137
- package/dist/Mermaid.client-HG24D5KB.mjs.map +1 -0
- package/dist/index.cjs +4 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -17
- package/dist/index.d.ts +14 -17
- package/dist/index.mjs +4 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/tools/Mermaid/Mermaid.client.tsx +49 -45
- package/src/tools/Mermaid/Mermaid.story.tsx +195 -110
- package/src/tools/Mermaid/builders/FlowDiagram/FlowDiagram.ts +96 -0
- package/src/tools/Mermaid/builders/FlowDiagram/functions/getEdges.ts +50 -0
- package/src/tools/Mermaid/builders/FlowDiagram/functions/getNodes.ts +43 -0
- package/src/tools/Mermaid/builders/FlowDiagram/functions/getStyles.ts +90 -0
- package/src/tools/Mermaid/builders/FlowDiagram/functions/index.ts +8 -0
- package/src/tools/Mermaid/builders/FlowDiagram/index.ts +16 -0
- package/src/tools/Mermaid/builders/FlowDiagram/types.ts +130 -0
- package/src/tools/Mermaid/builders/JourneyDiagram/JourneyDiagram.ts +88 -0
- package/src/tools/Mermaid/builders/JourneyDiagram/index.ts +12 -0
- package/src/tools/Mermaid/builders/JourneyDiagram/types.ts +48 -0
- package/src/tools/Mermaid/builders/SequenceDiagram/SequenceDiagram.ts +123 -0
- package/src/tools/Mermaid/builders/SequenceDiagram/functions/getActivations.ts +30 -0
- package/src/tools/Mermaid/builders/SequenceDiagram/functions/getBlocks.ts +112 -0
- package/src/tools/Mermaid/builders/SequenceDiagram/functions/getMessages.ts +85 -0
- package/src/tools/Mermaid/builders/SequenceDiagram/functions/getNotes.ts +94 -0
- package/src/tools/Mermaid/builders/SequenceDiagram/functions/index.ts +16 -0
- package/src/tools/Mermaid/builders/SequenceDiagram/index.ts +17 -0
- package/src/tools/Mermaid/builders/SequenceDiagram/types.ts +147 -0
- package/src/tools/Mermaid/builders/core/DiagramStore.ts +138 -0
- package/src/tools/Mermaid/builders/core/index.ts +8 -0
- package/src/tools/Mermaid/builders/core/sanitize.ts +78 -0
- package/src/tools/Mermaid/builders/core/theme.ts +42 -0
- package/src/tools/Mermaid/builders/core/types.ts +183 -0
- package/src/tools/Mermaid/builders/index.ts +96 -0
- package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +78 -54
- package/src/tools/Mermaid/index.tsx +51 -10
- package/src/tools/Mermaid/lazy.tsx +2 -5
- package/dist/Mermaid.client-AF4WOQZR.cjs.map +0 -1
- package/dist/Mermaid.client-W4QXJX7Q.mjs.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Maximize2 } from 'lucide-react';
|
|
4
5
|
|
|
5
|
-
import {
|
|
6
|
+
import { Button } from '@djangocfg/ui-core/components';
|
|
6
7
|
import { useResolvedTheme } from '@djangocfg/ui-core/hooks';
|
|
7
8
|
import { MermaidFullscreenModal } from './components/MermaidFullscreenModal';
|
|
8
9
|
import { useMermaidFullscreen } from './hooks/useMermaidFullscreen';
|
|
@@ -12,18 +13,18 @@ interface MermaidProps {
|
|
|
12
13
|
chart: string;
|
|
13
14
|
className?: string;
|
|
14
15
|
isCompact?: boolean;
|
|
16
|
+
/** Enable click-to-fullscreen functionality (default: true) */
|
|
17
|
+
fullscreen?: boolean;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
const Mermaid: React.FC<MermaidProps> = ({
|
|
18
|
-
|
|
20
|
+
const Mermaid: React.FC<MermaidProps> = ({
|
|
21
|
+
chart,
|
|
22
|
+
className = '',
|
|
23
|
+
isCompact = false,
|
|
24
|
+
fullscreen = true,
|
|
25
|
+
}) => {
|
|
19
26
|
const theme = useResolvedTheme();
|
|
20
27
|
|
|
21
|
-
const labels = useMemo(() => ({
|
|
22
|
-
title: t('tools.diagram.title'),
|
|
23
|
-
clickToView: t('tools.diagram.clickToView'),
|
|
24
|
-
loading: t('ui.form.loading'),
|
|
25
|
-
}), [t]);
|
|
26
|
-
|
|
27
28
|
// Rendering logic
|
|
28
29
|
const { mermaidRef, svgContent, isVertical, isRendering } = useMermaidRenderer({
|
|
29
30
|
chart,
|
|
@@ -31,7 +32,7 @@ const Mermaid: React.FC<MermaidProps> = ({ chart, className = '', isCompact = fa
|
|
|
31
32
|
isCompact,
|
|
32
33
|
});
|
|
33
34
|
|
|
34
|
-
// Fullscreen modal logic
|
|
35
|
+
// Fullscreen modal logic (only used if fullscreen prop is true)
|
|
35
36
|
const {
|
|
36
37
|
isFullscreen,
|
|
37
38
|
fullscreenRef,
|
|
@@ -40,7 +41,8 @@ const Mermaid: React.FC<MermaidProps> = ({ chart, className = '', isCompact = fa
|
|
|
40
41
|
handleBackdropClick,
|
|
41
42
|
} = useMermaidFullscreen();
|
|
42
43
|
|
|
43
|
-
const
|
|
44
|
+
const handleOpenFullscreen = (e: React.MouseEvent) => {
|
|
45
|
+
e.stopPropagation();
|
|
44
46
|
if (svgContent) {
|
|
45
47
|
openFullscreen();
|
|
46
48
|
}
|
|
@@ -48,41 +50,43 @@ const Mermaid: React.FC<MermaidProps> = ({ chart, className = '', isCompact = fa
|
|
|
48
50
|
|
|
49
51
|
return (
|
|
50
52
|
<>
|
|
51
|
-
<div
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
53
|
+
<div className={`relative ${className}`}>
|
|
54
|
+
<div
|
|
55
|
+
ref={mermaidRef}
|
|
56
|
+
className="flex justify-center items-center"
|
|
57
|
+
style={{ isolation: 'isolate' }}
|
|
58
|
+
/>
|
|
59
|
+
{isRendering && (
|
|
60
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
61
|
+
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary" />
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{/* Fullscreen button in bottom-right corner */}
|
|
66
|
+
{fullscreen && svgContent && !isRendering && (
|
|
67
|
+
<Button
|
|
68
|
+
variant="secondary"
|
|
69
|
+
size="icon"
|
|
70
|
+
className="absolute bottom-2 right-2 h-8 w-8 opacity-60 hover:opacity-100 transition-opacity"
|
|
71
|
+
onClick={handleOpenFullscreen}
|
|
72
|
+
>
|
|
73
|
+
<Maximize2 className="h-4 w-4" />
|
|
74
|
+
</Button>
|
|
75
|
+
)}
|
|
74
76
|
</div>
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
{fullscreen && (
|
|
79
|
+
<MermaidFullscreenModal
|
|
80
|
+
isOpen={isFullscreen}
|
|
81
|
+
svgContent={svgContent}
|
|
82
|
+
isVertical={isVertical}
|
|
83
|
+
theme={theme}
|
|
84
|
+
chart={chart}
|
|
85
|
+
fullscreenRef={fullscreenRef}
|
|
86
|
+
onClose={closeFullscreen}
|
|
87
|
+
onBackdropClick={handleBackdropClick}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
86
90
|
</>
|
|
87
91
|
);
|
|
88
92
|
};
|
|
@@ -1,131 +1,216 @@
|
|
|
1
1
|
import { defineStory, useSelect, useBoolean } from '@djangocfg/playground';
|
|
2
|
-
import Mermaid
|
|
2
|
+
import Mermaid, {
|
|
3
|
+
FlowDiagram,
|
|
4
|
+
SequenceDiagram,
|
|
5
|
+
JourneyDiagram,
|
|
6
|
+
useStylePresets,
|
|
7
|
+
useBoxColors,
|
|
8
|
+
} from './index';
|
|
3
9
|
|
|
4
10
|
export default defineStory({
|
|
5
11
|
title: 'Tools/Mermaid',
|
|
6
12
|
component: Mermaid,
|
|
7
|
-
description: 'Mermaid diagram renderer
|
|
13
|
+
description: 'Mermaid diagram renderer with declarative type-safe builders.',
|
|
8
14
|
});
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Diagram Generators
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
function useFlowDiagram() {
|
|
21
|
+
type Nodes = 'start' | 'check' | 'success' | 'debug' | 'finish';
|
|
22
|
+
const flow = FlowDiagram<Nodes>({ direction: 'TB' });
|
|
23
|
+
const presets = useStylePresets();
|
|
24
|
+
|
|
25
|
+
flow.node('start').rect('Start');
|
|
26
|
+
flow.node('check').rhombus('Is it working?');
|
|
27
|
+
flow.node('success').rect('Great!');
|
|
28
|
+
flow.node('debug').rect('Debug');
|
|
29
|
+
flow.node('finish').stadium('End');
|
|
30
|
+
|
|
31
|
+
flow.edge('start').to('check').solid();
|
|
32
|
+
flow.edge('check').to('success').solid('Yes');
|
|
33
|
+
flow.edge('check').to('debug').solid('No');
|
|
34
|
+
flow.edge('debug').to('check').dotted();
|
|
35
|
+
flow.edge('success').to('finish').solid();
|
|
36
|
+
|
|
37
|
+
flow.style.define('success', presets.success);
|
|
38
|
+
flow.style.define('primary', presets.primary);
|
|
39
|
+
flow.style.apply('success', 'success', 'finish');
|
|
40
|
+
flow.style.apply('primary', 'start');
|
|
41
|
+
|
|
42
|
+
return flow.toString();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function useSequenceDiagram() {
|
|
46
|
+
const boxes = useBoxColors();
|
|
47
|
+
|
|
48
|
+
const { d, rect, alt, loop, blank, toString } = SequenceDiagram({
|
|
49
|
+
User: 'actor',
|
|
50
|
+
App: 'participant',
|
|
51
|
+
Auth: 'participant',
|
|
52
|
+
DB: 'participant',
|
|
53
|
+
}, { autoNumber: true });
|
|
54
|
+
|
|
55
|
+
rect(boxes.primary, () => {
|
|
56
|
+
d.User.sync.App.msg('Enter credentials');
|
|
57
|
+
d.App.sync.Auth.msg('Validate credentials');
|
|
58
|
+
|
|
59
|
+
alt('Valid credentials', () => {
|
|
60
|
+
d.Auth.sync.DB.msg('Get user profile');
|
|
61
|
+
d.DB.syncReply.Auth.msg('Profile data');
|
|
62
|
+
d.Auth.syncReply.App.msg('Auth token');
|
|
63
|
+
d.App.syncReply.User.msg('Welcome!');
|
|
64
|
+
}).else('Invalid credentials', () => {
|
|
65
|
+
d.Auth.syncReply.App.msg('Auth failed');
|
|
66
|
+
d.App.syncReply.User.msg('Error message');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
blank();
|
|
71
|
+
|
|
72
|
+
loop('Every 15 minutes', () => {
|
|
73
|
+
d.App.async.Auth.msg('Refresh token');
|
|
74
|
+
d.Auth.asyncReply.App.msg('New token');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return toString();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function useJourneyDiagram() {
|
|
81
|
+
const journey = JourneyDiagram({ title: 'User Onboarding' });
|
|
82
|
+
|
|
83
|
+
journey.section('Discovery')
|
|
84
|
+
.task('Visit landing page', 5, 'User')
|
|
85
|
+
.task('Read features', 4, 'User');
|
|
86
|
+
|
|
87
|
+
journey.section('Sign Up')
|
|
88
|
+
.task('Click Sign Up', 5, 'User')
|
|
89
|
+
.task('Fill form', 2, 'User')
|
|
90
|
+
.task('Verify email', 4, ['User', 'System']);
|
|
91
|
+
|
|
92
|
+
journey.section('Onboarding')
|
|
93
|
+
.task('Complete profile', 3, 'User')
|
|
94
|
+
.task('Create first project', 5, 'User');
|
|
95
|
+
|
|
96
|
+
return journey.toString();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function useArchitectureDiagram() {
|
|
100
|
+
type Nodes = 'client' | 'nginx' | 'api1' | 'api2' | 'db' | 'cache' | 'queue';
|
|
101
|
+
const flow = FlowDiagram<Nodes>({ direction: 'TB' });
|
|
102
|
+
const presets = useStylePresets();
|
|
103
|
+
|
|
104
|
+
flow.subgraph('Frontend', (sub) => {
|
|
105
|
+
sub.direction('LR');
|
|
106
|
+
sub.node('client').round('Browser');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
flow.subgraph('Load Balancer', (sub) => {
|
|
110
|
+
sub.node('nginx').hexagon('Nginx');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
flow.subgraph('API Servers', (sub) => {
|
|
114
|
+
sub.direction('LR');
|
|
115
|
+
sub.node('api1').rect('API Server 1');
|
|
116
|
+
sub.node('api2').rect('API Server 2');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
flow.subgraph('Data Layer', (sub) => {
|
|
120
|
+
sub.direction('LR');
|
|
121
|
+
sub.node('db').cylinder('PostgreSQL');
|
|
122
|
+
sub.node('cache').cylinder('Redis');
|
|
123
|
+
sub.node('queue').cylinder('RabbitMQ');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
flow.edge('client').to('nginx').solid();
|
|
127
|
+
flow.edge('nginx').to('api1').solid();
|
|
128
|
+
flow.edge('nginx').to('api2').solid();
|
|
129
|
+
flow.edge('api1').to('db').solid();
|
|
130
|
+
flow.edge('api2').to('db').solid();
|
|
131
|
+
flow.edge('api1').to('cache').dotted();
|
|
132
|
+
flow.edge('api2').to('cache').dotted();
|
|
133
|
+
flow.edge('api1').to('queue').dotted();
|
|
134
|
+
|
|
135
|
+
flow.style.define('frontend', presets.info);
|
|
136
|
+
flow.style.define('backend', presets.success);
|
|
137
|
+
flow.style.define('data', presets.warning);
|
|
138
|
+
flow.style.apply('frontend', 'client', 'nginx');
|
|
139
|
+
flow.style.apply('backend', 'api1', 'api2');
|
|
140
|
+
flow.style.apply('data', 'db', 'cache', 'queue');
|
|
141
|
+
|
|
142
|
+
return flow.toString();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// Stories
|
|
147
|
+
// ============================================================================
|
|
81
148
|
|
|
82
149
|
export const Interactive = () => {
|
|
83
150
|
const [diagramType] = useSelect('diagramType', {
|
|
84
|
-
options: ['
|
|
85
|
-
defaultValue: '
|
|
151
|
+
options: ['flow', 'sequence', 'journey', 'architecture'] as const,
|
|
152
|
+
defaultValue: 'flow',
|
|
86
153
|
label: 'Diagram Type',
|
|
87
|
-
description: 'Select Mermaid diagram type',
|
|
88
154
|
});
|
|
89
155
|
|
|
90
|
-
const [
|
|
156
|
+
const [fullscreen] = useBoolean('fullscreen', {
|
|
91
157
|
defaultValue: false,
|
|
92
|
-
label: '
|
|
93
|
-
description: 'Use smaller font size',
|
|
158
|
+
label: 'Enable Fullscreen',
|
|
94
159
|
});
|
|
95
160
|
|
|
161
|
+
const flowChart = useFlowDiagram();
|
|
162
|
+
const sequenceChart = useSequenceDiagram();
|
|
163
|
+
const journeyChart = useJourneyDiagram();
|
|
164
|
+
const architectureChart = useArchitectureDiagram();
|
|
165
|
+
|
|
166
|
+
const charts = {
|
|
167
|
+
flow: flowChart,
|
|
168
|
+
sequence: sequenceChart,
|
|
169
|
+
journey: journeyChart,
|
|
170
|
+
architecture: architectureChart,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const chart = charts[diagramType];
|
|
174
|
+
|
|
96
175
|
return (
|
|
97
|
-
<div className="
|
|
98
|
-
<Mermaid chart={
|
|
176
|
+
<div className="space-y-4">
|
|
177
|
+
<Mermaid chart={chart} fullscreen={fullscreen} />
|
|
178
|
+
<details className="text-xs">
|
|
179
|
+
<summary className="cursor-pointer text-muted-foreground">Generated Mermaid code</summary>
|
|
180
|
+
<pre className="mt-2 p-3 bg-muted rounded overflow-auto">{chart}</pre>
|
|
181
|
+
</details>
|
|
99
182
|
</div>
|
|
100
183
|
);
|
|
101
184
|
};
|
|
102
185
|
|
|
103
|
-
export const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
186
|
+
export const Flow = () => {
|
|
187
|
+
const chart = useFlowDiagram();
|
|
188
|
+
return <Mermaid chart={chart} />;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const Sequence = () => {
|
|
192
|
+
const chart = useSequenceDiagram();
|
|
193
|
+
return <Mermaid chart={chart} />;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const Journey = () => {
|
|
197
|
+
const chart = useJourneyDiagram();
|
|
198
|
+
return <Mermaid chart={chart} />;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const Architecture = () => {
|
|
202
|
+
const chart = useArchitectureDiagram();
|
|
203
|
+
return <Mermaid chart={chart} />;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const WithFullscreen = () => {
|
|
207
|
+
const chart = useFlowDiagram();
|
|
208
|
+
return (
|
|
209
|
+
<div className="space-y-2">
|
|
210
|
+
<p className="text-sm text-muted-foreground">
|
|
211
|
+
Click the expand button to open fullscreen
|
|
212
|
+
</p>
|
|
213
|
+
<Mermaid chart={chart} fullscreen />
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
};
|
|
@@ -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
|
+
}
|