@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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -0
  3. package/package.json +77 -0
  4. package/src/FloatingToolbar/FloatingToolbar.css +5 -0
  5. package/src/FloatingToolbar/actions/CopyAction.tsx +31 -0
  6. package/src/FloatingToolbar/actions/DownloadAction.tsx +51 -0
  7. package/src/FloatingToolbar/actions/ExpandAction.tsx +33 -0
  8. package/src/FloatingToolbar/actions/FullscreenAction.tsx +38 -0
  9. package/src/FloatingToolbar/actions/index.ts +4 -0
  10. package/src/FloatingToolbar/hooks/useScrollIsolation.ts +62 -0
  11. package/src/FloatingToolbar/index.tsx +184 -0
  12. package/src/Mermaid.client.tsx +97 -0
  13. package/src/builders/FlowDiagram/FlowDiagram.ts +96 -0
  14. package/src/builders/FlowDiagram/functions/getEdges.ts +50 -0
  15. package/src/builders/FlowDiagram/functions/getNodes.ts +43 -0
  16. package/src/builders/FlowDiagram/functions/getStyles.ts +90 -0
  17. package/src/builders/FlowDiagram/functions/index.ts +8 -0
  18. package/src/builders/FlowDiagram/index.ts +16 -0
  19. package/src/builders/FlowDiagram/types.ts +130 -0
  20. package/src/builders/JourneyDiagram/JourneyDiagram.ts +88 -0
  21. package/src/builders/JourneyDiagram/index.ts +12 -0
  22. package/src/builders/JourneyDiagram/types.ts +48 -0
  23. package/src/builders/SequenceDiagram/SequenceDiagram.ts +158 -0
  24. package/src/builders/SequenceDiagram/functions/getActivations.ts +30 -0
  25. package/src/builders/SequenceDiagram/functions/getBlocks.ts +112 -0
  26. package/src/builders/SequenceDiagram/functions/getMessages.ts +85 -0
  27. package/src/builders/SequenceDiagram/functions/getNotes.ts +94 -0
  28. package/src/builders/SequenceDiagram/functions/index.ts +16 -0
  29. package/src/builders/SequenceDiagram/index.ts +18 -0
  30. package/src/builders/SequenceDiagram/types.ts +192 -0
  31. package/src/builders/core/DiagramStore.ts +138 -0
  32. package/src/builders/core/index.ts +8 -0
  33. package/src/builders/core/sanitize.ts +83 -0
  34. package/src/builders/core/theme.ts +42 -0
  35. package/src/builders/core/types.ts +183 -0
  36. package/src/builders/index.ts +96 -0
  37. package/src/components/MermaidCodeViewer.tsx +95 -0
  38. package/src/components/MermaidErrorPanel.tsx +31 -0
  39. package/src/components/MermaidFullscreenModal.tsx +201 -0
  40. package/src/hooks/index.ts +4 -0
  41. package/src/hooks/useMermaidCleanup.ts +70 -0
  42. package/src/hooks/useMermaidFullscreen.ts +46 -0
  43. package/src/hooks/useMermaidRenderer.ts +329 -0
  44. package/src/hooks/useMermaidValidation.ts +97 -0
  45. package/src/index.tsx +79 -0
  46. package/src/lazy.tsx +40 -0
  47. package/src/mermaid.stories.tsx +217 -0
  48. package/src/types.ts +28 -0
  49. package/src/utils/mermaid-helpers.ts +157 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * DiagramStore - Base class for building Mermaid diagrams
3
+ * Accumulates diagram lines with proper indentation
4
+ * @module Mermaid/builders/core/DiagramStore
5
+ */
6
+
7
+ export interface DiagramStoreOptions {
8
+ /** Indentation string (default: ' ' - two spaces) */
9
+ indent?: string;
10
+ }
11
+
12
+ const DEFAULT_OPTIONS: Required<DiagramStoreOptions> = {
13
+ indent: ' ',
14
+ };
15
+
16
+ /**
17
+ * Base store for accumulating Mermaid diagram code
18
+ * Handles indentation and line management
19
+ */
20
+ export class DiagramStore {
21
+ protected lines: string[] = [];
22
+ protected indentLevel = 0;
23
+ protected options: Required<DiagramStoreOptions>;
24
+
25
+ constructor(header: string, options: DiagramStoreOptions = {}) {
26
+ this.options = { ...DEFAULT_OPTIONS, ...options };
27
+ this.lines.push(header);
28
+ }
29
+
30
+ /**
31
+ * Add a line to the diagram with current indentation
32
+ */
33
+ add(line: string): this {
34
+ const indent = this.options.indent.repeat(this.indentLevel);
35
+ this.lines.push(indent + line);
36
+ return this;
37
+ }
38
+
39
+ /**
40
+ * Add a raw line without indentation
41
+ */
42
+ addRaw(line: string): this {
43
+ this.lines.push(line);
44
+ return this;
45
+ }
46
+
47
+ /**
48
+ * Add an empty line
49
+ */
50
+ addBlank(): this {
51
+ this.lines.push('');
52
+ return this;
53
+ }
54
+
55
+ /**
56
+ * Add a comment line
57
+ */
58
+ addComment(comment: string): this {
59
+ return this.add(`%% ${comment}`);
60
+ }
61
+
62
+ /**
63
+ * Increase indentation level
64
+ */
65
+ indent(): this {
66
+ this.indentLevel++;
67
+ return this;
68
+ }
69
+
70
+ /**
71
+ * Decrease indentation level
72
+ */
73
+ dedent(): this {
74
+ if (this.indentLevel > 0) {
75
+ this.indentLevel--;
76
+ }
77
+ return this;
78
+ }
79
+
80
+ /**
81
+ * Execute a callback within an indented block
82
+ * @param header - Block header line
83
+ * @param fn - Callback to execute inside the block
84
+ * @param footer - Block footer line (default: 'end')
85
+ */
86
+ block(header: string, fn: () => void, footer = 'end'): this {
87
+ this.add(header);
88
+ this.indent();
89
+ fn();
90
+ this.dedent();
91
+ this.add(footer);
92
+ return this;
93
+ }
94
+
95
+ /**
96
+ * Execute a callback within a subgraph block
97
+ * @param name - Subgraph name/title
98
+ * @param fn - Callback to execute inside the subgraph
99
+ */
100
+ subgraph(name: string, fn: () => void): this {
101
+ return this.block(`subgraph ${name}`, fn);
102
+ }
103
+
104
+ /**
105
+ * Add direction directive (for subgraphs)
106
+ */
107
+ direction(dir: 'TB' | 'BT' | 'LR' | 'RL'): this {
108
+ return this.add(`direction ${dir}`);
109
+ }
110
+
111
+ /**
112
+ * Get the current indentation string
113
+ */
114
+ getIndent(): string {
115
+ return this.options.indent.repeat(this.indentLevel);
116
+ }
117
+
118
+ /**
119
+ * Get the current indentation level
120
+ */
121
+ getIndentLevel(): number {
122
+ return this.indentLevel;
123
+ }
124
+
125
+ /**
126
+ * Convert the diagram to a Mermaid string
127
+ */
128
+ toString(): string {
129
+ return this.lines.join('\n');
130
+ }
131
+
132
+ /**
133
+ * Get all lines (for debugging)
134
+ */
135
+ getLines(): readonly string[] {
136
+ return this.lines;
137
+ }
138
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Core exports for Mermaid builders
3
+ * @module Mermaid/builders/core
4
+ */
5
+
6
+ export { DiagramStore, type DiagramStoreOptions } from './DiagramStore';
7
+ export * from './types';
8
+ export * from './theme';
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Sanitization utilities for Mermaid diagrams
3
+ * @module Mermaid/builders/core/sanitize
4
+ */
5
+
6
+ /**
7
+ * Characters that need to be escaped or removed in Mermaid labels
8
+ */
9
+ const UNSAFE_CHARS = /["\n\r\\<>{}|]/g;
10
+
11
+ /**
12
+ * Mermaid reserved keywords that cannot be used as node IDs.
13
+ *
14
+ * Stored LOWERCASE because the lookup lowercases its input. Held as `classDef`
15
+ * and `linkStyle`, those two never matched — a node called `classDef` emitted a
16
+ * style directive where a node was meant, and what followed was read as its
17
+ * arguments.
18
+ */
19
+ const RESERVED_WORDS = new Set([
20
+ 'end',
21
+ 'graph',
22
+ 'subgraph',
23
+ 'direction',
24
+ 'click',
25
+ 'style',
26
+ 'class',
27
+ 'classdef',
28
+ 'linkstyle',
29
+ 'callback',
30
+ ]);
31
+
32
+ /**
33
+ * Sanitize a label for use in Mermaid diagrams
34
+ * Removes or escapes characters that could break the diagram syntax
35
+ */
36
+ export function sanitizeLabel(label: string): string {
37
+ return label
38
+ .replace(UNSAFE_CHARS, '')
39
+ .replace(/&/g, '&amp;')
40
+ .replace(/'/g, "'")
41
+ .trim();
42
+ }
43
+
44
+ /**
45
+ * Convert an ID to a valid Mermaid node ID
46
+ * - Removes special characters
47
+ * - Adds optional prefix to ensure uniqueness
48
+ * - Ensures ID starts with a letter
49
+ * - Escapes reserved Mermaid keywords
50
+ */
51
+ export function toNodeId(id: string, prefix = ''): string {
52
+ // Remove special characters and spaces
53
+ const cleanId = id.replace(/[^a-zA-Z0-9_]/g, '_');
54
+
55
+ // Ensure starts with letter
56
+ let safeId = /^[a-zA-Z]/.test(cleanId) ? cleanId : `n${cleanId}`;
57
+
58
+ // Escape reserved words by adding underscore suffix
59
+ if (RESERVED_WORDS.has(safeId.toLowerCase())) {
60
+ safeId = `${safeId}_`;
61
+ }
62
+
63
+ return prefix ? `${prefix}_${safeId}` : safeId;
64
+ }
65
+
66
+ /**
67
+ * Escape a string for use in quoted Mermaid labels
68
+ */
69
+ export function escapeQuoted(str: string): string {
70
+ return str
71
+ .replace(/\\/g, '\\\\')
72
+ .replace(/"/g, '\\"')
73
+ .replace(/\n/g, '<br/>');
74
+ }
75
+
76
+ /**
77
+ * Format a label with optional subtitle using HTML-like syntax
78
+ */
79
+ export function formatLabel(main: string, subtitle?: string): string {
80
+ const sanitized = sanitizeLabel(main);
81
+ if (!subtitle) return sanitized;
82
+ return `${sanitized}<br/><small>${sanitizeLabel(subtitle)}</small>`;
83
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Theme utilities for Mermaid builders
3
+ *
4
+ * Re-exports palette hooks from @djangocfg/ui-core for convenience.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * import { useStylePresets, useBoxColors } from './theme';
9
+ *
10
+ * function MyDiagram() {
11
+ * const presets = useStylePresets();
12
+ * const boxes = useBoxColors();
13
+ *
14
+ * const flow = FlowDiagram();
15
+ * flow.style.define('success', presets.success);
16
+ *
17
+ * const { rect } = SequenceDiagram({ ... });
18
+ * rect(boxes.primary, () => { ... });
19
+ * }
20
+ * ```
21
+ *
22
+ * @module Mermaid/builders/core/theme
23
+ */
24
+
25
+ // Re-export from ui-core palette
26
+ export {
27
+ // Types
28
+ type ThemePalette,
29
+ type StyleColors,
30
+ type StylePresets,
31
+ type BoxColors,
32
+
33
+ // Hooks
34
+ useThemePalette,
35
+ useStylePresets,
36
+ useBoxColors,
37
+
38
+ // Utils
39
+ hslToHex,
40
+ hslToRgbString,
41
+ hslToRgba,
42
+ } from '@djangocfg/ui-core/styles/palette';
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Shared types for Mermaid diagram builders
3
+ * @module Mermaid/builders/core/types
4
+ */
5
+
6
+ // ============================================================================
7
+ // Node Types
8
+ // ============================================================================
9
+
10
+ export type NodeId = string;
11
+
12
+ /**
13
+ * Node shapes supported by Mermaid flowcharts
14
+ * @see https://mermaid.js.org/syntax/flowchart.html#node-shapes
15
+ */
16
+ export type NodeShape =
17
+ | 'rect' // [text]
18
+ | 'round' // (text)
19
+ | 'stadium' // ([text])
20
+ | 'subroutine' // [[text]]
21
+ | 'cylinder' // [(text)]
22
+ | 'circle' // ((text))
23
+ | 'asymmetric' // >text]
24
+ | 'rhombus' // {text}
25
+ | 'hexagon' // {{text}}
26
+ | 'parallelogram' // [/text/]
27
+ | 'parallelogramAlt' // [\text\]
28
+ | 'trapezoid' // [/text\]
29
+ | 'trapezoidAlt' // [\text/]
30
+ | 'doubleCircle'; // (((text)))
31
+
32
+ /**
33
+ * Shape syntax lookup for Mermaid
34
+ */
35
+ export const NODE_SHAPE_SYNTAX: Record<NodeShape, [string, string]> = {
36
+ rect: ['[', ']'],
37
+ round: ['(', ')'],
38
+ stadium: ['([', '])'],
39
+ subroutine: ['[[', ']]'],
40
+ cylinder: ['[(', ')]'],
41
+ circle: ['((', '))'],
42
+ asymmetric: ['>', ']'],
43
+ rhombus: ['{', '}'],
44
+ hexagon: ['{{', '}}'],
45
+ parallelogram: ['[/', '/]'],
46
+ parallelogramAlt: ['[\\', '\\]'],
47
+ trapezoid: ['[/', '\\]'],
48
+ trapezoidAlt: ['[\\', '/]'],
49
+ doubleCircle: ['(((', ')))'],
50
+ };
51
+
52
+ // ============================================================================
53
+ // Edge Types
54
+ // ============================================================================
55
+
56
+ /**
57
+ * Edge line styles
58
+ */
59
+ export type EdgeLineStyle = 'solid' | 'dotted' | 'thick';
60
+
61
+ /**
62
+ * Arrow head types
63
+ */
64
+ export type ArrowHead = 'arrow' | 'open' | 'circle' | 'cross' | 'none';
65
+
66
+ /**
67
+ * Edge syntax lookup
68
+ */
69
+ export const EDGE_SYNTAX: Record<EdgeLineStyle, Record<ArrowHead, string>> = {
70
+ solid: {
71
+ arrow: '-->',
72
+ open: '---',
73
+ circle: '--o',
74
+ cross: '--x',
75
+ none: '---',
76
+ },
77
+ dotted: {
78
+ arrow: '-..->',
79
+ open: '-.-',
80
+ circle: '-.-o',
81
+ cross: '-.-x',
82
+ none: '-.-',
83
+ },
84
+ thick: {
85
+ arrow: '==>',
86
+ open: '===',
87
+ circle: '==o',
88
+ cross: '==x',
89
+ none: '===',
90
+ },
91
+ };
92
+
93
+ /**
94
+ * Simplified edge syntax for common cases
95
+ */
96
+ export const SIMPLE_EDGE_SYNTAX = {
97
+ solid: '-->',
98
+ dotted: '-.->',
99
+ thick: '==>',
100
+ solidOpen: '---',
101
+ dottedOpen: '-.-',
102
+ } as const;
103
+
104
+ // ============================================================================
105
+ // Style Types
106
+ // ============================================================================
107
+
108
+ /**
109
+ * CSS-like style properties for nodes
110
+ */
111
+ export interface StyleProperties {
112
+ fill?: string;
113
+ stroke?: string;
114
+ 'stroke-width'?: string;
115
+ color?: string;
116
+ 'font-weight'?: 'bold' | 'normal';
117
+ 'stroke-dasharray'?: string;
118
+ }
119
+
120
+ /**
121
+ * Style class definition
122
+ */
123
+ export interface StyleClass {
124
+ name: string;
125
+ properties: StyleProperties;
126
+ }
127
+
128
+ // ============================================================================
129
+ // Flow Diagram Types
130
+ // ============================================================================
131
+
132
+ /**
133
+ * Flow diagram directions
134
+ */
135
+ export type FlowDirection = 'TB' | 'BT' | 'LR' | 'RL';
136
+
137
+ // ============================================================================
138
+ // Sequence Diagram Types
139
+ // ============================================================================
140
+
141
+ /**
142
+ * Participant types in sequence diagrams
143
+ */
144
+ export type ParticipantType = 'participant' | 'actor';
145
+
146
+ /**
147
+ * Message arrow types in sequence diagrams
148
+ */
149
+ export const MESSAGE_ARROWS = {
150
+ sync: '->>', // Synchronous call
151
+ syncReply: '-->>', // Synchronous reply
152
+ async: '-)', // Async call
153
+ asyncReply: '--)', // Async reply
154
+ solid: '->', // Solid line
155
+ dotted: '-->', // Dotted line
156
+ cross: '-x', // Cross (failure)
157
+ crossDotted: '--x', // Dotted cross
158
+ } as const;
159
+
160
+ export type MessageArrowType = keyof typeof MESSAGE_ARROWS;
161
+
162
+ // ============================================================================
163
+ // Journey Diagram Types
164
+ // ============================================================================
165
+
166
+ /**
167
+ * Task score (1-5, where 5 is best)
168
+ */
169
+ export type TaskScore = 1 | 2 | 3 | 4 | 5;
170
+
171
+ // ============================================================================
172
+ // Utility Types
173
+ // ============================================================================
174
+
175
+ /**
176
+ * Makes all properties in T writable (removes readonly)
177
+ */
178
+ export type Writable<T> = { -readonly [P in keyof T]: T[P] };
179
+
180
+ /**
181
+ * Extracts keys from a const object
182
+ */
183
+ export type KeysOf<T> = keyof T & string;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Mermaid Diagram Builders
3
+ *
4
+ * Declarative, type-safe builders for creating Mermaid diagrams.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * import {
9
+ * FlowDiagram,
10
+ * SequenceDiagram,
11
+ * JourneyDiagram,
12
+ * useStylePresets,
13
+ * useBoxColors
14
+ * } from '@djangocfg/widget-diagram';
15
+ *
16
+ * function MyDiagram() {
17
+ * const presets = useStylePresets();
18
+ * const boxes = useBoxColors();
19
+ *
20
+ * // Flow diagram
21
+ * const flow = FlowDiagram<'A' | 'B'>({ direction: 'LR' });
22
+ * flow.node('A').rect('Start');
23
+ * flow.edge('A').to('B').solid();
24
+ * flow.style.define('highlight', presets.success);
25
+ *
26
+ * // Sequence diagram
27
+ * const { d, rect } = SequenceDiagram({
28
+ * Alice: 'participant',
29
+ * Bob: 'actor',
30
+ * });
31
+ * rect(boxes.primary, () => {
32
+ * d.Alice.sync.Bob.msg('Hello!');
33
+ * });
34
+ *
35
+ * // Journey diagram
36
+ * const journey = JourneyDiagram({ title: 'User Flow' });
37
+ * journey.section('Start').task('Open app', 5, 'User');
38
+ * }
39
+ * ```
40
+ *
41
+ * @module Mermaid/builders
42
+ */
43
+
44
+ // Core
45
+ export { DiagramStore, type DiagramStoreOptions } from './core/DiagramStore';
46
+ export { sanitizeLabel, toNodeId, escapeQuoted, formatLabel } from './core/sanitize';
47
+ export * from './core/types';
48
+
49
+ // Theme hooks and utilities (re-exported from @djangocfg/ui-core/styles/palette)
50
+ export {
51
+ useThemePalette,
52
+ useStylePresets,
53
+ useBoxColors,
54
+ hslToHex,
55
+ hslToRgbString,
56
+ hslToRgba,
57
+ type ThemePalette,
58
+ type StyleColors,
59
+ type StylePresets,
60
+ type BoxColors,
61
+ } from './core/theme';
62
+
63
+ // FlowDiagram
64
+ export { FlowDiagram, STYLE_PRESETS } from './FlowDiagram';
65
+ export type {
66
+ FlowDiagramOptions,
67
+ FlowDiagramBuilder,
68
+ NodeBuilder,
69
+ EdgeBuilder,
70
+ EdgeEndBuilder,
71
+ StyleBuilder,
72
+ SubgraphBuilder,
73
+ } from './FlowDiagram';
74
+
75
+ // SequenceDiagram
76
+ export { SequenceDiagram } from './SequenceDiagram';
77
+ export type {
78
+ ParticipantsObject,
79
+ SequenceDiagramOptions,
80
+ SequenceDiagramBuilder,
81
+ MessageBuilder,
82
+ MessageArrows,
83
+ MessageTarget,
84
+ MessageAction,
85
+ NoteBuilder,
86
+ ActivationBuilder,
87
+ } from './SequenceDiagram';
88
+
89
+ // JourneyDiagram
90
+ export { JourneyDiagram } from './JourneyDiagram';
91
+ export type {
92
+ JourneyDiagramOptions,
93
+ JourneyDiagramBuilder,
94
+ SectionBuilder,
95
+ TaskDefinition,
96
+ } from './JourneyDiagram';
@@ -0,0 +1,95 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+
5
+ interface MermaidCodeViewerProps {
6
+ chart: string;
7
+ renderPreview: () => React.ReactNode;
8
+ }
9
+
10
+ export const MermaidCodeViewer: React.FC<MermaidCodeViewerProps> = ({
11
+ chart,
12
+ renderPreview,
13
+ }) => {
14
+ const [activeTab, setActiveTab] = useState<'preview' | 'code'>('preview');
15
+ const [copied, setCopied] = useState(false);
16
+
17
+ const handleCopy = async () => {
18
+ await navigator.clipboard.writeText(chart);
19
+ setCopied(true);
20
+ setTimeout(() => setCopied(false), 2000);
21
+ };
22
+
23
+ return (
24
+ <div className="flex flex-col h-full">
25
+ {/* Tabs */}
26
+ <div className="flex items-center justify-between border-b border-border px-4">
27
+ <div className="flex">
28
+ <button
29
+ onClick={() => setActiveTab('preview')}
30
+ className={`px-4 py-3 text-sm font-medium transition-colors relative ${
31
+ activeTab === 'preview'
32
+ ? 'text-foreground'
33
+ : 'text-muted-foreground hover:text-foreground'
34
+ }`}
35
+ >
36
+ Preview
37
+ {activeTab === 'preview' && (
38
+ <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
39
+ )}
40
+ </button>
41
+ <button
42
+ onClick={() => setActiveTab('code')}
43
+ className={`px-4 py-3 text-sm font-medium transition-colors relative ${
44
+ activeTab === 'code'
45
+ ? 'text-foreground'
46
+ : 'text-muted-foreground hover:text-foreground'
47
+ }`}
48
+ >
49
+ Code
50
+ {activeTab === 'code' && (
51
+ <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
52
+ )}
53
+ </button>
54
+ </div>
55
+
56
+ {/* Copy button - show only on Code tab */}
57
+ {activeTab === 'code' && (
58
+ <button
59
+ onClick={handleCopy}
60
+ className="flex items-center gap-2 px-3 py-1.5 text-xs font-medium bg-primary/10 hover:bg-primary/20 text-primary rounded transition-colors"
61
+ >
62
+ {copied ? (
63
+ <>
64
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
65
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
66
+ </svg>
67
+ Copied!
68
+ </>
69
+ ) : (
70
+ <>
71
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
72
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
73
+ </svg>
74
+ Copy
75
+ </>
76
+ )}
77
+ </button>
78
+ )}
79
+ </div>
80
+
81
+ {/* Content */}
82
+ <div className="flex-1 overflow-auto">
83
+ {activeTab === 'preview' ? (
84
+ <div className="p-6 flex items-center justify-center min-h-full">
85
+ {renderPreview()}
86
+ </div>
87
+ ) : (
88
+ <pre className="p-6 text-sm font-mono text-foreground bg-muted/30 h-full overflow-auto">
89
+ <code>{chart}</code>
90
+ </pre>
91
+ )}
92
+ </div>
93
+ </div>
94
+ );
95
+ };
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { AlertTriangle } from 'lucide-react';
5
+
6
+ interface MermaidErrorPanelProps {
7
+ /** Human-readable error message from the Mermaid parser. */
8
+ message: string;
9
+ }
10
+
11
+ /**
12
+ * Inline error panel shown when a Mermaid diagram fails to parse.
13
+ *
14
+ * Replaces the previous `innerHTML`-injected markup — keeps rendering in
15
+ * React, uses semantic `destructive` tokens, and stays accessible
16
+ * (`role="alert"`).
17
+ */
18
+ export const MermaidErrorPanel: React.FC<MermaidErrorPanelProps> = ({ message }) => {
19
+ return (
20
+ <div
21
+ role="alert"
22
+ className="flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/10 p-4 text-destructive"
23
+ >
24
+ <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
25
+ <div className="min-w-0">
26
+ <p className="text-sm font-semibold">Diagram syntax error</p>
27
+ <p className="mt-0.5 break-words text-sm text-destructive/90">{message}</p>
28
+ </div>
29
+ </div>
30
+ );
31
+ };