@eventcatalog/core 3.12.7 → 3.12.9-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-RZOSO7BH.js → chunk-2UUS3AQ2.js} +1 -1
- package/dist/{chunk-UFF5Q7GJ.js → chunk-EDX2LGRV.js} +1 -1
- package/dist/{chunk-2EFYBMLH.js → chunk-RJOB6XEC.js} +1 -1
- package/dist/{chunk-YO6IQXB3.js → chunk-V7YMKA4P.js} +1 -1
- package/dist/{chunk-PLQAZDJI.js → chunk-WTCJKTEF.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/MDX/Design/Design.astro +2 -2
- package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +2 -2
- package/eventcatalog/src/components/MDX/Flow/Flow.astro +2 -2
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +104 -0
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +2 -2
- package/eventcatalog/src/components/MDX/NodeGraph/README.md +85 -0
- package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -2
- package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +66 -17
- package/eventcatalog/src/utils/node-graphs/data-products-node-graph.ts +14 -5
- package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +1 -1
- package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +133 -18
- package/eventcatalog/src/utils/node-graphs/services-node-graph.ts +36 -14
- package/eventcatalog/src/utils/node-graphs/utils/utils.ts +115 -4
- package/package.json +4 -4
- package/eventcatalog/src/components/MDX/NodeGraph/DownloadButton.tsx +0 -62
- package/eventcatalog/src/components/MDX/NodeGraph/Edges/AnimatedMessageEdge.tsx +0 -110
- package/eventcatalog/src/components/MDX/NodeGraph/Edges/FlowEdge.tsx +0 -96
- package/eventcatalog/src/components/MDX/NodeGraph/Edges/MultilineEdgeLabel.tsx +0 -52
- package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeContent.tsx +0 -294
- package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeNodeActions.tsx +0 -92
- package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModePlaceholder.tsx +0 -26
- package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/utils.ts +0 -163
- package/eventcatalog/src/components/MDX/NodeGraph/FocusModeModal.tsx +0 -99
- package/eventcatalog/src/components/MDX/NodeGraph/MermaidView.tsx +0 -242
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +0 -1181
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Actor.tsx +0 -46
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Channel.tsx +0 -55
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Command.tsx +0 -27
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Custom.tsx +0 -159
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Data.tsx +0 -63
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/DataProduct.tsx +0 -132
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Domain.tsx +0 -155
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Entity.tsx +0 -154
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Event.tsx +0 -29
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem.tsx +0 -79
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem2.tsx +0 -24
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Flow.tsx +0 -107
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/MessageContextMenu.tsx +0 -63
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Query.tsx +0 -28
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Service.tsx +0 -127
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Step.tsx +0 -64
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/User.tsx +0 -76
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/View.tsx +0 -24
- package/eventcatalog/src/components/MDX/NodeGraph/StepWalkthrough.tsx +0 -296
- package/eventcatalog/src/components/MDX/NodeGraph/StudioModal.tsx +0 -129
- package/eventcatalog/src/components/MDX/NodeGraph/VisualiserSearch.tsx +0 -258
- package/eventcatalog/src/components/MDX/NodeGraph/VisualizerDropdownContent.tsx +0 -313
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import type { Node, Edge } from '@xyflow/react';
|
|
2
|
-
|
|
3
|
-
export interface NodeDisplayInfo {
|
|
4
|
-
id: string;
|
|
5
|
-
name: string;
|
|
6
|
-
type: string;
|
|
7
|
-
version?: string;
|
|
8
|
-
description?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ConnectedNodes {
|
|
12
|
-
leftNodes: Node[];
|
|
13
|
-
rightNodes: Node[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const NODE_COLOR_CLASSES: Record<string, string> = {
|
|
17
|
-
events: 'bg-orange-600',
|
|
18
|
-
services: 'bg-pink-600',
|
|
19
|
-
flows: 'bg-teal-600',
|
|
20
|
-
commands: 'bg-blue-600',
|
|
21
|
-
queries: 'bg-green-600',
|
|
22
|
-
channels: 'bg-gray-600',
|
|
23
|
-
externalSystem: 'bg-pink-600',
|
|
24
|
-
actor: 'bg-yellow-500',
|
|
25
|
-
step: 'bg-gray-700',
|
|
26
|
-
data: 'bg-blue-600',
|
|
27
|
-
'data-products': 'bg-indigo-600',
|
|
28
|
-
domains: 'bg-yellow-600',
|
|
29
|
-
entities: 'bg-purple-600',
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export const NODE_TYPE_LABELS: Record<string, string> = {
|
|
33
|
-
events: 'Event',
|
|
34
|
-
services: 'Service',
|
|
35
|
-
flows: 'Flow',
|
|
36
|
-
commands: 'Command',
|
|
37
|
-
queries: 'Query',
|
|
38
|
-
channels: 'Channel',
|
|
39
|
-
externalSystem: 'External System',
|
|
40
|
-
actor: 'Actor',
|
|
41
|
-
step: 'Step',
|
|
42
|
-
data: 'Data',
|
|
43
|
-
'data-products': 'Data Product',
|
|
44
|
-
domains: 'Domain',
|
|
45
|
-
entities: 'Entity',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Get connected nodes for a given center node
|
|
50
|
-
* Left nodes: incoming edges (they send TO this node)
|
|
51
|
-
* Right nodes: outgoing edges (this node sends TO them)
|
|
52
|
-
*/
|
|
53
|
-
export function getConnectedNodes(centerNodeId: string, nodes: Node[], edges: Edge[]): ConnectedNodes {
|
|
54
|
-
const leftIds = new Set<string>();
|
|
55
|
-
const rightIds = new Set<string>();
|
|
56
|
-
|
|
57
|
-
edges.forEach((edge) => {
|
|
58
|
-
if (edge.target === centerNodeId) {
|
|
59
|
-
leftIds.add(edge.source);
|
|
60
|
-
}
|
|
61
|
-
if (edge.source === centerNodeId) {
|
|
62
|
-
rightIds.add(edge.target);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
leftNodes: nodes.filter((n) => leftIds.has(n.id)),
|
|
68
|
-
rightNodes: nodes.filter((n) => rightIds.has(n.id)),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Entity keys that follow the standard data structure pattern
|
|
73
|
-
const ENTITY_KEYS = ['service', 'message', 'flow', 'channel', 'domain', 'entity', 'dataProduct'] as const;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Extract display information from a ReactFlow node
|
|
77
|
-
*/
|
|
78
|
-
export function getNodeDisplayInfo(node: Node): NodeDisplayInfo {
|
|
79
|
-
const nodeType = node.type || 'unknown';
|
|
80
|
-
const data = node.data as any;
|
|
81
|
-
|
|
82
|
-
// Find the entity in data using standard keys
|
|
83
|
-
const entityKey = ENTITY_KEYS.find((key) => data[key]);
|
|
84
|
-
const entity = entityKey ? data[entityKey] : null;
|
|
85
|
-
|
|
86
|
-
const name = entity?.data?.name || entity?.id || data.label || data.name || node.id;
|
|
87
|
-
const version = entity?.data?.version || entity?.version || data.version || '';
|
|
88
|
-
const description = entity?.data?.summary || entity?.data?.description || data.summary || data.description || '';
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
id: node.id,
|
|
92
|
-
name,
|
|
93
|
-
type: nodeType,
|
|
94
|
-
version,
|
|
95
|
-
description: description ? truncateDescription(description, 100) : undefined,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Truncate description to a max length
|
|
101
|
-
*/
|
|
102
|
-
function truncateDescription(text: string, maxLength: number): string {
|
|
103
|
-
if (text.length <= maxLength) return text;
|
|
104
|
-
return text.slice(0, maxLength).trim() + '...';
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Get the color class for a node type
|
|
109
|
-
*/
|
|
110
|
-
export function getNodeColorClass(nodeType: string): string {
|
|
111
|
-
return NODE_COLOR_CLASSES[nodeType] || 'bg-gray-500';
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Get the display label for a node type
|
|
116
|
-
*/
|
|
117
|
-
export function getNodeTypeLabel(nodeType: string): string {
|
|
118
|
-
return NODE_TYPE_LABELS[nodeType] || nodeType;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Mapping from entity key to doc path
|
|
122
|
-
const DOC_PATH_MAP: Record<string, string> = {
|
|
123
|
-
service: 'services',
|
|
124
|
-
flow: 'flows',
|
|
125
|
-
channel: 'channels',
|
|
126
|
-
domain: 'domains',
|
|
127
|
-
entity: 'entities',
|
|
128
|
-
dataProduct: 'data-products',
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Get the documentation URL for a node
|
|
133
|
-
*/
|
|
134
|
-
export function getNodeDocUrl(node: Node): string | null {
|
|
135
|
-
const nodeType = node.type || 'unknown';
|
|
136
|
-
const data = node.data as any;
|
|
137
|
-
|
|
138
|
-
// Handle message type separately due to type mapping
|
|
139
|
-
if (data.message) {
|
|
140
|
-
const id = data.message.data?.id || data.message.id || '';
|
|
141
|
-
const version = data.message.data?.version || data.message.version || '';
|
|
142
|
-
const collectionType = nodeType === 'events' ? 'events' : nodeType === 'commands' ? 'commands' : 'queries';
|
|
143
|
-
return id && version ? `/docs/${collectionType}/${id}/${version}` : null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Handle data/container nodes with nested data.data structure
|
|
147
|
-
if (data.data && nodeType === 'data') {
|
|
148
|
-
const id = data.data.id || '';
|
|
149
|
-
const version = data.data.version || '';
|
|
150
|
-
return id && version ? `/docs/containers/${id}/${version}` : null;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Handle standard entity types
|
|
154
|
-
for (const [key, path] of Object.entries(DOC_PATH_MAP)) {
|
|
155
|
-
if (data[key]) {
|
|
156
|
-
const id = data[key].data?.id || data[key].id || '';
|
|
157
|
-
const version = data[key].data?.version || data[key].version || '';
|
|
158
|
-
return id && version ? `/docs/${path}/${id}/${version}` : null;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import React, { useState, useCallback, useEffect } from 'react';
|
|
2
|
-
import * as Dialog from '@radix-ui/react-dialog';
|
|
3
|
-
import { XIcon, FocusIcon } from 'lucide-react';
|
|
4
|
-
import { ReactFlowProvider, type Node, type Edge, type NodeTypes, type EdgeTypes } from '@xyflow/react';
|
|
5
|
-
import FocusModeContent from './FocusMode/FocusModeContent';
|
|
6
|
-
import { getNodeDisplayInfo } from './FocusMode/utils';
|
|
7
|
-
|
|
8
|
-
interface FocusModeModalProps {
|
|
9
|
-
isOpen: boolean;
|
|
10
|
-
onClose: () => void;
|
|
11
|
-
initialNodeId: string | null;
|
|
12
|
-
nodes: Node[];
|
|
13
|
-
edges: Edge[];
|
|
14
|
-
nodeTypes: NodeTypes;
|
|
15
|
-
edgeTypes: EdgeTypes;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const FocusModeModal: React.FC<FocusModeModalProps> = ({
|
|
19
|
-
isOpen,
|
|
20
|
-
onClose,
|
|
21
|
-
initialNodeId,
|
|
22
|
-
nodes,
|
|
23
|
-
edges,
|
|
24
|
-
nodeTypes,
|
|
25
|
-
edgeTypes,
|
|
26
|
-
}) => {
|
|
27
|
-
const [centerNodeId, setCenterNodeId] = useState<string | null>(initialNodeId);
|
|
28
|
-
|
|
29
|
-
// Reset center node when modal opens with new initial node
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (isOpen && initialNodeId) {
|
|
32
|
-
setCenterNodeId(initialNodeId);
|
|
33
|
-
}
|
|
34
|
-
}, [isOpen, initialNodeId]);
|
|
35
|
-
|
|
36
|
-
const handleSwitchCenter = useCallback((newCenterNodeId: string, _direction: 'left' | 'right') => {
|
|
37
|
-
setCenterNodeId(newCenterNodeId);
|
|
38
|
-
}, []);
|
|
39
|
-
|
|
40
|
-
// Get center node info for title
|
|
41
|
-
const centerNode = centerNodeId ? nodes.find((n) => n.id === centerNodeId) : null;
|
|
42
|
-
const centerNodeInfo = centerNode ? getNodeDisplayInfo(centerNode) : null;
|
|
43
|
-
|
|
44
|
-
if (!centerNodeId) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<Dialog.Root open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
|
50
|
-
<Dialog.Portal container={typeof document !== 'undefined' ? document.body : undefined}>
|
|
51
|
-
<div className="fixed inset-0 z-[99999]" style={{ isolation: 'isolate' }}>
|
|
52
|
-
<Dialog.Overlay className="fixed inset-0 bg-black/70 data-[state=open]:animate-overlayShow" />
|
|
53
|
-
<Dialog.Content className="fixed inset-4 md:inset-8 lg:inset-12 rounded-lg bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] shadow-xl focus:outline-none data-[state=open]:animate-contentShow flex flex-col overflow-hidden">
|
|
54
|
-
{/* Header */}
|
|
55
|
-
<div className="flex items-center justify-between px-6 py-4 border-b border-[rgb(var(--ec-page-border))] flex-shrink-0">
|
|
56
|
-
<div className="flex items-center gap-3">
|
|
57
|
-
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-[rgb(var(--ec-accent-subtle))]">
|
|
58
|
-
<FocusIcon className="w-5 h-5 text-[rgb(var(--ec-accent))]" />
|
|
59
|
-
</div>
|
|
60
|
-
<div>
|
|
61
|
-
<Dialog.Title className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Focus Mode</Dialog.Title>
|
|
62
|
-
<Dialog.Description className="text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
63
|
-
{centerNodeInfo
|
|
64
|
-
? `Exploring: ${centerNodeInfo.name} - Click on connected nodes to navigate`
|
|
65
|
-
: 'Explore node connections'}
|
|
66
|
-
</Dialog.Description>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
<Dialog.Close asChild>
|
|
70
|
-
<button
|
|
71
|
-
className="flex items-center justify-center w-10 h-10 rounded-lg text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-content-hover,var(--ec-page-border)/0.5))] transition-colors"
|
|
72
|
-
aria-label="Close"
|
|
73
|
-
>
|
|
74
|
-
<XIcon className="w-5 h-5" />
|
|
75
|
-
</button>
|
|
76
|
-
</Dialog.Close>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
{/* Content */}
|
|
80
|
-
<div className="flex-1 overflow-hidden">
|
|
81
|
-
<ReactFlowProvider>
|
|
82
|
-
<FocusModeContent
|
|
83
|
-
centerNodeId={centerNodeId}
|
|
84
|
-
nodes={nodes}
|
|
85
|
-
edges={edges}
|
|
86
|
-
nodeTypes={nodeTypes}
|
|
87
|
-
edgeTypes={edgeTypes}
|
|
88
|
-
onSwitchCenter={handleSwitchCenter}
|
|
89
|
-
/>
|
|
90
|
-
</ReactFlowProvider>
|
|
91
|
-
</div>
|
|
92
|
-
</Dialog.Content>
|
|
93
|
-
</div>
|
|
94
|
-
</Dialog.Portal>
|
|
95
|
-
</Dialog.Root>
|
|
96
|
-
);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export default FocusModeModal;
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
-
import { CheckIcon, ClipboardIcon } from 'lucide-react';
|
|
3
|
-
import type { Node, Edge } from '@xyflow/react';
|
|
4
|
-
import { convertToMermaid } from '@utils/node-graphs/export-mermaid';
|
|
5
|
-
import { copyToClipboard } from '@utils/clipboard';
|
|
6
|
-
|
|
7
|
-
interface MermaidViewProps {
|
|
8
|
-
nodes: Node[];
|
|
9
|
-
edges: Edge[];
|
|
10
|
-
maxTextSize?: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const MermaidView = ({ nodes, edges, maxTextSize = 100000 }: MermaidViewProps) => {
|
|
14
|
-
const [copySuccess, setCopySuccess] = useState(false);
|
|
15
|
-
const [mermaidCode, setMermaidCode] = useState('');
|
|
16
|
-
const [previewSvg, setPreviewSvg] = useState<string | null>(null);
|
|
17
|
-
const [previewError, setPreviewError] = useState<string | null>(null);
|
|
18
|
-
const [isRendering, setIsRendering] = useState(true);
|
|
19
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
20
|
-
const svgContainerRef = useRef<HTMLDivElement>(null);
|
|
21
|
-
const panZoomInstanceRef = useRef<any>(null);
|
|
22
|
-
|
|
23
|
-
// Generate mermaid code
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
const code = convertToMermaid(nodes, edges, { includeStyles: true, direction: 'LR' });
|
|
26
|
-
setMermaidCode(code);
|
|
27
|
-
}, [nodes, edges]);
|
|
28
|
-
|
|
29
|
-
// Render mermaid preview
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (!mermaidCode) return;
|
|
32
|
-
|
|
33
|
-
let cancelled = false;
|
|
34
|
-
setIsRendering(true);
|
|
35
|
-
setPreviewError(null);
|
|
36
|
-
|
|
37
|
-
const renderMermaid = async () => {
|
|
38
|
-
try {
|
|
39
|
-
const { default: mermaid } = await import('mermaid');
|
|
40
|
-
|
|
41
|
-
// Detect current theme
|
|
42
|
-
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
43
|
-
const currentTheme = isDarkMode ? 'dark' : 'default';
|
|
44
|
-
|
|
45
|
-
mermaid.initialize({
|
|
46
|
-
maxTextSize: maxTextSize,
|
|
47
|
-
startOnLoad: false,
|
|
48
|
-
theme: currentTheme,
|
|
49
|
-
flowchart: {
|
|
50
|
-
curve: 'basis',
|
|
51
|
-
padding: 20,
|
|
52
|
-
},
|
|
53
|
-
securityLevel: 'loose',
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const id = 'mermaid-view-' + Math.random().toString(36).substring(2, 9);
|
|
57
|
-
const { svg } = await mermaid.render(id, mermaidCode);
|
|
58
|
-
|
|
59
|
-
if (!cancelled) {
|
|
60
|
-
setPreviewSvg(svg);
|
|
61
|
-
setPreviewError(null);
|
|
62
|
-
}
|
|
63
|
-
} catch (error) {
|
|
64
|
-
if (!cancelled) {
|
|
65
|
-
console.error('Mermaid render error:', error);
|
|
66
|
-
setPreviewError(error instanceof Error ? error.message : 'Failed to render diagram');
|
|
67
|
-
setPreviewSvg(null);
|
|
68
|
-
}
|
|
69
|
-
} finally {
|
|
70
|
-
if (!cancelled) {
|
|
71
|
-
setIsRendering(false);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
renderMermaid();
|
|
77
|
-
|
|
78
|
-
return () => {
|
|
79
|
-
cancelled = true;
|
|
80
|
-
};
|
|
81
|
-
}, [mermaidCode]);
|
|
82
|
-
|
|
83
|
-
// Initialize pan/zoom after SVG is rendered
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
if (!previewSvg || !svgContainerRef.current) return;
|
|
86
|
-
|
|
87
|
-
const initZoom = async () => {
|
|
88
|
-
const svgElement = svgContainerRef.current?.querySelector('svg');
|
|
89
|
-
if (!svgElement) return;
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
const { default: svgPanZoom } = await import('svg-pan-zoom');
|
|
93
|
-
|
|
94
|
-
// Set SVG to fill container
|
|
95
|
-
svgElement.style.width = '100%';
|
|
96
|
-
svgElement.style.height = '100%';
|
|
97
|
-
svgElement.removeAttribute('height');
|
|
98
|
-
svgElement.removeAttribute('width');
|
|
99
|
-
|
|
100
|
-
const instance = svgPanZoom(svgElement, {
|
|
101
|
-
zoomEnabled: true,
|
|
102
|
-
controlIconsEnabled: false,
|
|
103
|
-
fit: true,
|
|
104
|
-
center: true,
|
|
105
|
-
minZoom: 0.1,
|
|
106
|
-
maxZoom: 10,
|
|
107
|
-
zoomScaleSensitivity: 0.15,
|
|
108
|
-
dblClickZoomEnabled: true,
|
|
109
|
-
mouseWheelZoomEnabled: true,
|
|
110
|
-
panEnabled: true,
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
panZoomInstanceRef.current = instance;
|
|
114
|
-
} catch (e) {
|
|
115
|
-
console.warn('Failed to initialize zoom:', e);
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
initZoom();
|
|
120
|
-
|
|
121
|
-
return () => {
|
|
122
|
-
if (panZoomInstanceRef.current) {
|
|
123
|
-
try {
|
|
124
|
-
panZoomInstanceRef.current.destroy();
|
|
125
|
-
} catch (e) {
|
|
126
|
-
// Ignore
|
|
127
|
-
}
|
|
128
|
-
panZoomInstanceRef.current = null;
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
}, [previewSvg]);
|
|
132
|
-
|
|
133
|
-
const handleCopyToClipboard = useCallback(async () => {
|
|
134
|
-
await copyToClipboard(mermaidCode);
|
|
135
|
-
setCopySuccess(true);
|
|
136
|
-
setTimeout(() => setCopySuccess(false), 2000);
|
|
137
|
-
}, [mermaidCode]);
|
|
138
|
-
|
|
139
|
-
return (
|
|
140
|
-
<div
|
|
141
|
-
ref={containerRef}
|
|
142
|
-
className="w-full h-full bg-[rgb(var(--ec-page-bg))] relative flex flex-col"
|
|
143
|
-
style={{ animation: 'fadeIn 200ms ease-out' }}
|
|
144
|
-
>
|
|
145
|
-
<style>{`
|
|
146
|
-
@keyframes fadeIn {
|
|
147
|
-
from { opacity: 0; }
|
|
148
|
-
to { opacity: 1; }
|
|
149
|
-
}
|
|
150
|
-
`}</style>
|
|
151
|
-
|
|
152
|
-
{/* Copy button - top right */}
|
|
153
|
-
<div className="absolute top-[10px] right-4 z-20">
|
|
154
|
-
<div className="relative group">
|
|
155
|
-
<button
|
|
156
|
-
onClick={handleCopyToClipboard}
|
|
157
|
-
className={`p-2.5 rounded-md shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgb(var(--ec-accent))] transition-all duration-150 ${
|
|
158
|
-
copySuccess
|
|
159
|
-
? 'bg-green-500 text-white scale-110'
|
|
160
|
-
: 'bg-[rgb(var(--ec-card-bg))] hover:bg-[rgb(var(--ec-page-border))/0.5] text-[rgb(var(--ec-icon-color))] hover:scale-105'
|
|
161
|
-
}`}
|
|
162
|
-
aria-label={copySuccess ? 'Copied!' : 'Copy Mermaid code'}
|
|
163
|
-
>
|
|
164
|
-
{copySuccess ? <CheckIcon className="h-5 w-5" /> : <ClipboardIcon className="h-5 w-5" />}
|
|
165
|
-
</button>
|
|
166
|
-
<div className="absolute top-full right-0 mt-2 px-2 py-1 bg-[rgb(var(--ec-page-text))] text-[rgb(var(--ec-page-bg))] text-xs rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50">
|
|
167
|
-
{copySuccess ? 'Copied!' : 'Copy Mermaid code'}
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
|
|
172
|
-
{/* Mermaid diagram container */}
|
|
173
|
-
<div className="flex-1 overflow-hidden">
|
|
174
|
-
{isRendering && (
|
|
175
|
-
<div className="w-full h-full flex items-center justify-center">
|
|
176
|
-
<div className="relative">
|
|
177
|
-
<div className="flex items-center gap-4 opacity-40">
|
|
178
|
-
<div className="w-24 h-10 bg-[rgb(var(--ec-page-border))] rounded animate-pulse" />
|
|
179
|
-
<div className="w-12 h-0.5 bg-[rgb(var(--ec-page-border))] animate-pulse" />
|
|
180
|
-
<div
|
|
181
|
-
className="w-20 h-10 bg-[rgb(var(--ec-page-border))] rounded-full animate-pulse"
|
|
182
|
-
style={{ animationDelay: '75ms' }}
|
|
183
|
-
/>
|
|
184
|
-
<div className="w-12 h-0.5 bg-[rgb(var(--ec-page-border))] animate-pulse" style={{ animationDelay: '150ms' }} />
|
|
185
|
-
<div
|
|
186
|
-
className="w-24 h-10 bg-[rgb(var(--ec-page-border))] rounded animate-pulse"
|
|
187
|
-
style={{ animationDelay: '225ms' }}
|
|
188
|
-
/>
|
|
189
|
-
</div>
|
|
190
|
-
<p className="text-center text-sm text-[rgb(var(--ec-page-text-muted))] mt-4">Rendering diagram...</p>
|
|
191
|
-
</div>
|
|
192
|
-
</div>
|
|
193
|
-
)}
|
|
194
|
-
|
|
195
|
-
{previewError && !isRendering && (
|
|
196
|
-
<div className="w-full h-full flex flex-col items-center justify-center p-4">
|
|
197
|
-
<div className="text-red-500 text-sm mb-2">Failed to render diagram</div>
|
|
198
|
-
<div className="text-[rgb(var(--ec-page-text-muted))] text-xs font-mono bg-[rgb(var(--ec-code-bg))] p-2 rounded max-w-lg overflow-auto">
|
|
199
|
-
{previewError}
|
|
200
|
-
</div>
|
|
201
|
-
<div className="mt-4 text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
202
|
-
<p>You can still copy the Mermaid code and paste it into</p>
|
|
203
|
-
<a
|
|
204
|
-
href="https://mermaid.live"
|
|
205
|
-
target="_blank"
|
|
206
|
-
rel="noopener noreferrer"
|
|
207
|
-
className="text-[rgb(var(--ec-accent))] hover:underline"
|
|
208
|
-
>
|
|
209
|
-
mermaid.live
|
|
210
|
-
</a>
|
|
211
|
-
</div>
|
|
212
|
-
</div>
|
|
213
|
-
)}
|
|
214
|
-
|
|
215
|
-
{previewSvg && !isRendering && !previewError && (
|
|
216
|
-
<div
|
|
217
|
-
ref={svgContainerRef}
|
|
218
|
-
className="w-full h-full cursor-grab active:cursor-grabbing [&_svg]:w-full [&_svg]:h-full"
|
|
219
|
-
dangerouslySetInnerHTML={{ __html: previewSvg }}
|
|
220
|
-
/>
|
|
221
|
-
)}
|
|
222
|
-
</div>
|
|
223
|
-
|
|
224
|
-
{/* Footer hint */}
|
|
225
|
-
<div className="absolute bottom-4 left-4 z-20">
|
|
226
|
-
<div className="flex items-center gap-2 bg-[rgb(var(--ec-card-bg))]/90 backdrop-blur-sm px-3 py-1.5 rounded-md shadow-sm border border-[rgb(var(--ec-page-border))]">
|
|
227
|
-
<svg className="w-3.5 h-3.5 text-[rgb(var(--ec-icon-color))]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
228
|
-
<path
|
|
229
|
-
strokeLinecap="round"
|
|
230
|
-
strokeLinejoin="round"
|
|
231
|
-
strokeWidth={2}
|
|
232
|
-
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
|
|
233
|
-
/>
|
|
234
|
-
</svg>
|
|
235
|
-
<span className="text-xs text-[rgb(var(--ec-page-text-muted))]">Scroll to zoom · Drag to pan</span>
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
239
|
-
);
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
export default MermaidView;
|