@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.
Files changed (43) hide show
  1. package/README.md +149 -2
  2. package/dist/{Mermaid.client-AF4WOQZR.cjs → Mermaid.client-2TAFAXPW.cjs} +106 -136
  3. package/dist/Mermaid.client-2TAFAXPW.cjs.map +1 -0
  4. package/dist/{Mermaid.client-W4QXJX7Q.mjs → Mermaid.client-HG24D5KB.mjs} +107 -137
  5. package/dist/Mermaid.client-HG24D5KB.mjs.map +1 -0
  6. package/dist/index.cjs +4 -9
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +14 -17
  9. package/dist/index.d.ts +14 -17
  10. package/dist/index.mjs +4 -9
  11. package/dist/index.mjs.map +1 -1
  12. package/package.json +6 -6
  13. package/src/tools/Mermaid/Mermaid.client.tsx +49 -45
  14. package/src/tools/Mermaid/Mermaid.story.tsx +195 -110
  15. package/src/tools/Mermaid/builders/FlowDiagram/FlowDiagram.ts +96 -0
  16. package/src/tools/Mermaid/builders/FlowDiagram/functions/getEdges.ts +50 -0
  17. package/src/tools/Mermaid/builders/FlowDiagram/functions/getNodes.ts +43 -0
  18. package/src/tools/Mermaid/builders/FlowDiagram/functions/getStyles.ts +90 -0
  19. package/src/tools/Mermaid/builders/FlowDiagram/functions/index.ts +8 -0
  20. package/src/tools/Mermaid/builders/FlowDiagram/index.ts +16 -0
  21. package/src/tools/Mermaid/builders/FlowDiagram/types.ts +130 -0
  22. package/src/tools/Mermaid/builders/JourneyDiagram/JourneyDiagram.ts +88 -0
  23. package/src/tools/Mermaid/builders/JourneyDiagram/index.ts +12 -0
  24. package/src/tools/Mermaid/builders/JourneyDiagram/types.ts +48 -0
  25. package/src/tools/Mermaid/builders/SequenceDiagram/SequenceDiagram.ts +123 -0
  26. package/src/tools/Mermaid/builders/SequenceDiagram/functions/getActivations.ts +30 -0
  27. package/src/tools/Mermaid/builders/SequenceDiagram/functions/getBlocks.ts +112 -0
  28. package/src/tools/Mermaid/builders/SequenceDiagram/functions/getMessages.ts +85 -0
  29. package/src/tools/Mermaid/builders/SequenceDiagram/functions/getNotes.ts +94 -0
  30. package/src/tools/Mermaid/builders/SequenceDiagram/functions/index.ts +16 -0
  31. package/src/tools/Mermaid/builders/SequenceDiagram/index.ts +17 -0
  32. package/src/tools/Mermaid/builders/SequenceDiagram/types.ts +147 -0
  33. package/src/tools/Mermaid/builders/core/DiagramStore.ts +138 -0
  34. package/src/tools/Mermaid/builders/core/index.ts +8 -0
  35. package/src/tools/Mermaid/builders/core/sanitize.ts +78 -0
  36. package/src/tools/Mermaid/builders/core/theme.ts +42 -0
  37. package/src/tools/Mermaid/builders/core/types.ts +183 -0
  38. package/src/tools/Mermaid/builders/index.ts +96 -0
  39. package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +78 -54
  40. package/src/tools/Mermaid/index.tsx +51 -10
  41. package/src/tools/Mermaid/lazy.tsx +2 -5
  42. package/dist/Mermaid.client-AF4WOQZR.cjs.map +0 -1
  43. package/dist/Mermaid.client-W4QXJX7Q.mjs.map +0 -1
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
- import React, { useMemo } from 'react';
3
+ import React from 'react';
4
+ import { Maximize2 } from 'lucide-react';
4
5
 
5
- import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
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> = ({ chart, className = '', isCompact = false }) => {
18
- const t = useTypedT<I18nTranslations>();
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 handleClick = () => {
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
- className={`relative bg-card rounded-sm border border-border overflow-hidden cursor-pointer hover:shadow-sm transition-shadow ${className}`}
53
- onClick={handleClick}
54
- >
55
- <div className="p-4 border-b border-border bg-muted/50">
56
- <h6 className="text-sm font-semibold text-foreground">{labels.title}</h6>
57
- <p className="text-xs text-muted-foreground mt-1">{labels.clickToView}</p>
58
- </div>
59
- <div className="relative p-4 overflow-hidden">
60
- <div
61
- ref={mermaidRef}
62
- className="flex justify-center items-center min-h-[200px]"
63
- style={{ isolation: 'isolate' }}
64
- />
65
- {isRendering && (
66
- <div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm">
67
- <div className="flex flex-col items-center gap-2">
68
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
69
- <p className="text-xs text-muted-foreground">{labels.loading}</p>
70
- </div>
71
- </div>
72
- )}
73
- </div>
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
- <MermaidFullscreenModal
77
- isOpen={isFullscreen}
78
- svgContent={svgContent}
79
- isVertical={isVertical}
80
- theme={theme}
81
- chart={chart}
82
- fullscreenRef={fullscreenRef}
83
- onClose={closeFullscreen}
84
- onBackdropClick={handleBackdropClick}
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 from './index';
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 for flowcharts, sequences, and more.',
13
+ description: 'Mermaid diagram renderer with declarative type-safe builders.',
8
14
  });
9
15
 
10
- const DIAGRAMS = {
11
- flowchart: `flowchart TD
12
- A[Start] --> B{Is it working?}
13
- B -->|Yes| C[Great!]
14
- B -->|No| D[Debug]
15
- D --> B
16
- C --> E[End]`,
17
- sequence: `sequenceDiagram
18
- participant U as User
19
- participant F as Frontend
20
- participant A as API
21
- participant D as Database
22
-
23
- U->>F: Click login
24
- F->>A: POST /auth/login
25
- A->>D: Query user
26
- D-->>A: User data
27
- A-->>F: JWT token
28
- F-->>U: Redirect to dashboard`,
29
- class: `classDiagram
30
- class Vehicle {
31
- +String id
32
- +String make
33
- +String model
34
- +int year
35
- +float price
36
- +getFullName()
37
- }
38
- class Car {
39
- +int doors
40
- +String fuelType
41
- }
42
- class Motorcycle {
43
- +String engineType
44
- }
45
- Vehicle <|-- Car
46
- Vehicle <|-- Motorcycle`,
47
- er: `erDiagram
48
- USER ||--o{ ORDER : places
49
- ORDER ||--|{ LINE_ITEM : contains
50
- PRODUCT ||--o{ LINE_ITEM : "ordered in"
51
- USER {
52
- string id PK
53
- string email
54
- string name
55
- }
56
- ORDER {
57
- string id PK
58
- date created_at
59
- string status
60
- }
61
- PRODUCT {
62
- string id PK
63
- string name
64
- float price
65
- }`,
66
- gantt: `gantt
67
- title Project Timeline
68
- dateFormat YYYY-MM-DD
69
- section Design
70
- Research :a1, 2024-01-01, 7d
71
- Wireframes :a2, after a1, 5d
72
- UI Design :a3, after a2, 10d
73
- section Development
74
- Frontend :b1, after a3, 14d
75
- Backend :b2, after a3, 14d
76
- Integration :b3, after b1, 7d
77
- section Testing
78
- QA Testing :c1, after b3, 7d
79
- Bug Fixes :c2, after c1, 5d`,
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: ['flowchart', 'sequence', 'class', 'er', 'gantt'] as const,
85
- defaultValue: 'flowchart',
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 [isCompact] = useBoolean('isCompact', {
156
+ const [fullscreen] = useBoolean('fullscreen', {
91
157
  defaultValue: false,
92
- label: 'Compact Mode',
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="max-w-4xl">
98
- <Mermaid chart={DIAGRAMS[diagramType]} isCompact={isCompact} />
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 Flowchart = () => (
104
- <div className="max-w-2xl">
105
- <Mermaid chart={DIAGRAMS.flowchart} />
106
- </div>
107
- );
108
-
109
- export const SequenceDiagram = () => (
110
- <div className="max-w-3xl">
111
- <Mermaid chart={DIAGRAMS.sequence} />
112
- </div>
113
- );
114
-
115
- export const ClassDiagram = () => (
116
- <div className="max-w-2xl">
117
- <Mermaid chart={DIAGRAMS.class} />
118
- </div>
119
- );
120
-
121
- export const ERDiagram = () => (
122
- <div className="max-w-3xl">
123
- <Mermaid chart={DIAGRAMS.er} />
124
- </div>
125
- );
126
-
127
- export const GanttChart = () => (
128
- <div className="max-w-4xl">
129
- <Mermaid chart={DIAGRAMS.gantt} />
130
- </div>
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
+ }