mermaid-live-mcp 0.1.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/README.md +98 -0
- package/dist/index.js +1164 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
8
|
+
import { dirname, resolve } from "path";
|
|
9
|
+
|
|
10
|
+
// ../preview/dist/server.js
|
|
11
|
+
import { createServer } from "http";
|
|
12
|
+
import { WebSocketServer } from "ws";
|
|
13
|
+
import { exec } from "child_process";
|
|
14
|
+
var HTML_PAGE = `<!DOCTYPE html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="UTF-8">
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
<title>Sketchdraw Preview</title>
|
|
20
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
21
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
22
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
23
|
+
<style>
|
|
24
|
+
:root {
|
|
25
|
+
--bg-base: #0f0f1a;
|
|
26
|
+
--bg-surface: #161625;
|
|
27
|
+
--bg-elevated: #1e1e32;
|
|
28
|
+
--accent: #6c5ce7;
|
|
29
|
+
--accent-hover: #7c6ef7;
|
|
30
|
+
--accent-glow: rgba(108,92,231,0.35);
|
|
31
|
+
--text-primary: #eaeaff;
|
|
32
|
+
--text-secondary: #a0a0c0;
|
|
33
|
+
--text-tertiary: #6a6a8a;
|
|
34
|
+
--border: #2a2a44;
|
|
35
|
+
--border-light: #33335a;
|
|
36
|
+
--green: #4ade80;
|
|
37
|
+
--red: #f87171;
|
|
38
|
+
--radius-sm: 6px;
|
|
39
|
+
--radius-md: 10px;
|
|
40
|
+
--radius-lg: 14px;
|
|
41
|
+
--shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
|
|
42
|
+
--shadow-md: 0 4px 16px rgba(0,0,0,0.4);
|
|
43
|
+
--shadow-lg: 0 8px 32px rgba(0,0,0,0.5), 0 2px 8px rgba(0,0,0,0.3);
|
|
44
|
+
--transition-fast: 0.15s ease;
|
|
45
|
+
--transition-normal: 0.25s ease;
|
|
46
|
+
}
|
|
47
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
48
|
+
body {
|
|
49
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
50
|
+
background: var(--bg-base);
|
|
51
|
+
color: var(--text-primary);
|
|
52
|
+
height: 100vh;
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-direction: column;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* \u2500\u2500 Header \u2500\u2500 */
|
|
59
|
+
header {
|
|
60
|
+
padding: 10px 20px;
|
|
61
|
+
background: rgba(22,22,37,0.82);
|
|
62
|
+
backdrop-filter: blur(12px);
|
|
63
|
+
-webkit-backdrop-filter: blur(12px);
|
|
64
|
+
border-bottom: 1px solid var(--border);
|
|
65
|
+
display: flex;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: 14px;
|
|
68
|
+
z-index: 10;
|
|
69
|
+
}
|
|
70
|
+
.logo-icon {
|
|
71
|
+
flex-shrink: 0;
|
|
72
|
+
}
|
|
73
|
+
.logo-icon svg { display: block; }
|
|
74
|
+
.header-title { display: flex; flex-direction: column; gap: 1px; }
|
|
75
|
+
.header-title h1 {
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
color: var(--text-primary);
|
|
79
|
+
line-height: 1.2;
|
|
80
|
+
}
|
|
81
|
+
.header-subtitle {
|
|
82
|
+
font-size: 11px;
|
|
83
|
+
color: var(--text-tertiary);
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: 6px;
|
|
87
|
+
line-height: 1.2;
|
|
88
|
+
}
|
|
89
|
+
.status-dot {
|
|
90
|
+
width: 6px; height: 6px;
|
|
91
|
+
border-radius: 50%;
|
|
92
|
+
background: var(--red);
|
|
93
|
+
flex-shrink: 0;
|
|
94
|
+
}
|
|
95
|
+
.status-dot.connected {
|
|
96
|
+
background: var(--green);
|
|
97
|
+
animation: pulse-green 2s ease-in-out infinite;
|
|
98
|
+
}
|
|
99
|
+
@keyframes pulse-green {
|
|
100
|
+
0%,100% { box-shadow: 0 0 0 0 rgba(74,222,128,0.5); }
|
|
101
|
+
50% { box-shadow: 0 0 0 4px rgba(74,222,128,0); }
|
|
102
|
+
}
|
|
103
|
+
.status-dot.disconnected {
|
|
104
|
+
background: var(--red);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* \u2500\u2500 Toolbar \u2500\u2500 */
|
|
108
|
+
.toolbar {
|
|
109
|
+
display: none;
|
|
110
|
+
align-items: center;
|
|
111
|
+
gap: 4px;
|
|
112
|
+
margin-left: auto;
|
|
113
|
+
}
|
|
114
|
+
.toolbar.visible { display: flex; }
|
|
115
|
+
.toolbar .divider {
|
|
116
|
+
width: 1px; height: 20px;
|
|
117
|
+
background: var(--border);
|
|
118
|
+
margin: 0 6px;
|
|
119
|
+
}
|
|
120
|
+
.btn {
|
|
121
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
122
|
+
padding: 5px 10px;
|
|
123
|
+
border: 1px solid var(--border);
|
|
124
|
+
border-radius: var(--radius-sm);
|
|
125
|
+
background: var(--bg-elevated);
|
|
126
|
+
color: var(--text-secondary);
|
|
127
|
+
font-family: inherit;
|
|
128
|
+
font-size: 12px;
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
transition: background var(--transition-fast), transform var(--transition-fast), color var(--transition-fast), box-shadow var(--transition-fast);
|
|
131
|
+
white-space: nowrap;
|
|
132
|
+
line-height: 1;
|
|
133
|
+
}
|
|
134
|
+
.btn svg { flex-shrink: 0; }
|
|
135
|
+
.btn:hover {
|
|
136
|
+
background: var(--border);
|
|
137
|
+
color: var(--text-primary);
|
|
138
|
+
transform: translateY(-1px);
|
|
139
|
+
}
|
|
140
|
+
.btn:active { transform: translateY(0); }
|
|
141
|
+
.btn:focus-visible {
|
|
142
|
+
outline: 2px solid var(--accent);
|
|
143
|
+
outline-offset: 2px;
|
|
144
|
+
}
|
|
145
|
+
.btn-primary {
|
|
146
|
+
background: var(--accent);
|
|
147
|
+
border-color: var(--accent);
|
|
148
|
+
color: #fff;
|
|
149
|
+
}
|
|
150
|
+
.btn-primary:hover {
|
|
151
|
+
background: var(--accent-hover);
|
|
152
|
+
border-color: var(--accent-hover);
|
|
153
|
+
box-shadow: 0 0 16px var(--accent-glow);
|
|
154
|
+
color: #fff;
|
|
155
|
+
}
|
|
156
|
+
.zoom-display {
|
|
157
|
+
font-size: 11px;
|
|
158
|
+
color: var(--text-tertiary);
|
|
159
|
+
min-width: 36px;
|
|
160
|
+
text-align: center;
|
|
161
|
+
font-variant-numeric: tabular-nums;
|
|
162
|
+
user-select: none;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* \u2500\u2500 Main \u2500\u2500 */
|
|
166
|
+
main {
|
|
167
|
+
flex: 1;
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-direction: column;
|
|
170
|
+
padding: 20px;
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
min-height: 0;
|
|
173
|
+
position: relative;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* \u2500\u2500 Diagram container \u2500\u2500 */
|
|
177
|
+
#diagram-container {
|
|
178
|
+
background: #ffffff;
|
|
179
|
+
border-radius: var(--radius-lg);
|
|
180
|
+
box-shadow: var(--shadow-lg);
|
|
181
|
+
width: 100%;
|
|
182
|
+
flex: 1;
|
|
183
|
+
position: relative;
|
|
184
|
+
overflow: hidden;
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
min-height: 0;
|
|
188
|
+
transition: background var(--transition-normal);
|
|
189
|
+
}
|
|
190
|
+
#diagram-container.dark-canvas {
|
|
191
|
+
background: #1a1a2e;
|
|
192
|
+
}
|
|
193
|
+
#diagram-container.dark-canvas #diagram-viewport svg {
|
|
194
|
+
filter: invert(1) hue-rotate(180deg);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* \u2500\u2500 Viewport (zoom/pan) \u2500\u2500 */
|
|
198
|
+
#diagram-viewport {
|
|
199
|
+
flex: 1;
|
|
200
|
+
overflow: hidden;
|
|
201
|
+
cursor: grab;
|
|
202
|
+
display: flex;
|
|
203
|
+
min-height: 0;
|
|
204
|
+
}
|
|
205
|
+
#diagram-viewport.grabbing { cursor: grabbing; }
|
|
206
|
+
.diagram-transform {
|
|
207
|
+
transform-origin: 0 0;
|
|
208
|
+
transition: none;
|
|
209
|
+
}
|
|
210
|
+
.diagram-transform svg {
|
|
211
|
+
display: block;
|
|
212
|
+
}
|
|
213
|
+
.diagram-entrance {
|
|
214
|
+
animation: diagramIn 0.35s ease forwards;
|
|
215
|
+
}
|
|
216
|
+
@keyframes diagramIn {
|
|
217
|
+
from { opacity: 0; }
|
|
218
|
+
to { opacity: 1; }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* \u2500\u2500 Empty state \u2500\u2500 */
|
|
222
|
+
.empty-state {
|
|
223
|
+
display: flex;
|
|
224
|
+
flex-direction: column;
|
|
225
|
+
align-items: center;
|
|
226
|
+
justify-content: center;
|
|
227
|
+
flex: 1;
|
|
228
|
+
gap: 12px;
|
|
229
|
+
padding: 32px;
|
|
230
|
+
}
|
|
231
|
+
.empty-icon {
|
|
232
|
+
animation: breathe 3s ease-in-out infinite;
|
|
233
|
+
color: var(--text-tertiary);
|
|
234
|
+
}
|
|
235
|
+
@keyframes breathe {
|
|
236
|
+
0%,100% { transform: translateY(0); }
|
|
237
|
+
50% { transform: translateY(-8px); }
|
|
238
|
+
}
|
|
239
|
+
.empty-title {
|
|
240
|
+
font-size: 15px;
|
|
241
|
+
font-weight: 500;
|
|
242
|
+
color: var(--text-secondary);
|
|
243
|
+
}
|
|
244
|
+
.empty-subtitle {
|
|
245
|
+
font-size: 12px;
|
|
246
|
+
color: var(--text-tertiary);
|
|
247
|
+
}
|
|
248
|
+
.dots-loading { display: flex; gap: 4px; margin-top: 4px; }
|
|
249
|
+
.dots-loading span {
|
|
250
|
+
width: 5px; height: 5px;
|
|
251
|
+
border-radius: 50%;
|
|
252
|
+
background: var(--text-tertiary);
|
|
253
|
+
animation: dotBounce 1.4s ease-in-out infinite;
|
|
254
|
+
}
|
|
255
|
+
.dots-loading span:nth-child(2) { animation-delay: 0.16s; }
|
|
256
|
+
.dots-loading span:nth-child(3) { animation-delay: 0.32s; }
|
|
257
|
+
@keyframes dotBounce {
|
|
258
|
+
0%,80%,100% { transform: translateY(0); opacity: 0.4; }
|
|
259
|
+
40% { transform: translateY(-6px); opacity: 1; }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* \u2500\u2500 Loading state \u2500\u2500 */
|
|
263
|
+
.loading-overlay {
|
|
264
|
+
position: absolute;
|
|
265
|
+
inset: 0;
|
|
266
|
+
background: rgba(255,255,255,0.85);
|
|
267
|
+
display: flex;
|
|
268
|
+
flex-direction: column;
|
|
269
|
+
align-items: center;
|
|
270
|
+
justify-content: center;
|
|
271
|
+
gap: 12px;
|
|
272
|
+
z-index: 5;
|
|
273
|
+
opacity: 1;
|
|
274
|
+
transition: opacity 0.3s ease;
|
|
275
|
+
border-radius: var(--radius-lg);
|
|
276
|
+
}
|
|
277
|
+
.loading-overlay.hidden { opacity: 0; pointer-events: none; }
|
|
278
|
+
#diagram-container.dark-canvas .loading-overlay {
|
|
279
|
+
background: rgba(26,26,46,0.85);
|
|
280
|
+
}
|
|
281
|
+
.spinner {
|
|
282
|
+
width: 28px; height: 28px;
|
|
283
|
+
border: 3px solid var(--border);
|
|
284
|
+
border-top-color: var(--accent);
|
|
285
|
+
border-radius: 50%;
|
|
286
|
+
animation: spin 0.7s linear infinite;
|
|
287
|
+
}
|
|
288
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
289
|
+
.loading-text {
|
|
290
|
+
font-size: 12px;
|
|
291
|
+
color: var(--text-tertiary);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* \u2500\u2500 Error panel \u2500\u2500 */
|
|
295
|
+
.error-panel {
|
|
296
|
+
display: none;
|
|
297
|
+
flex-direction: column;
|
|
298
|
+
gap: 10px;
|
|
299
|
+
padding: 20px;
|
|
300
|
+
flex: 1;
|
|
301
|
+
}
|
|
302
|
+
.error-panel.visible { display: flex; }
|
|
303
|
+
.error-header {
|
|
304
|
+
display: flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
gap: 8px;
|
|
307
|
+
color: var(--red);
|
|
308
|
+
font-size: 14px;
|
|
309
|
+
font-weight: 600;
|
|
310
|
+
}
|
|
311
|
+
.error-message {
|
|
312
|
+
font-family: 'SF Mono', Menlo, Consolas, monospace;
|
|
313
|
+
font-size: 12px;
|
|
314
|
+
color: var(--text-secondary);
|
|
315
|
+
background: var(--bg-base);
|
|
316
|
+
border: 1px solid var(--border);
|
|
317
|
+
border-radius: var(--radius-sm);
|
|
318
|
+
padding: 12px;
|
|
319
|
+
overflow: auto;
|
|
320
|
+
max-height: 200px;
|
|
321
|
+
white-space: pre-wrap;
|
|
322
|
+
word-break: break-word;
|
|
323
|
+
}
|
|
324
|
+
.error-dismiss {
|
|
325
|
+
align-self: flex-start;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* \u2500\u2500 Info bar \u2500\u2500 */
|
|
329
|
+
.info-bar {
|
|
330
|
+
display: none;
|
|
331
|
+
padding: 6px 14px;
|
|
332
|
+
font-family: 'SF Mono', Menlo, Consolas, monospace;
|
|
333
|
+
font-size: 11px;
|
|
334
|
+
color: var(--text-tertiary);
|
|
335
|
+
border-top: 1px solid var(--border);
|
|
336
|
+
gap: 16px;
|
|
337
|
+
background: rgba(0,0,0,0.1);
|
|
338
|
+
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
|
339
|
+
}
|
|
340
|
+
.info-bar.visible { display: flex; }
|
|
341
|
+
|
|
342
|
+
/* \u2500\u2500 Shortcuts overlay \u2500\u2500 */
|
|
343
|
+
.shortcuts-overlay {
|
|
344
|
+
display: none;
|
|
345
|
+
position: fixed;
|
|
346
|
+
inset: 0;
|
|
347
|
+
z-index: 100;
|
|
348
|
+
background: rgba(0,0,0,0.6);
|
|
349
|
+
align-items: center;
|
|
350
|
+
justify-content: center;
|
|
351
|
+
backdrop-filter: blur(4px);
|
|
352
|
+
}
|
|
353
|
+
.shortcuts-overlay.visible { display: flex; }
|
|
354
|
+
.shortcuts-panel {
|
|
355
|
+
background: var(--bg-surface);
|
|
356
|
+
border: 1px solid var(--border);
|
|
357
|
+
border-radius: var(--radius-lg);
|
|
358
|
+
padding: 24px;
|
|
359
|
+
max-width: 360px;
|
|
360
|
+
width: 90%;
|
|
361
|
+
box-shadow: var(--shadow-lg);
|
|
362
|
+
}
|
|
363
|
+
.shortcuts-panel h3 {
|
|
364
|
+
font-size: 14px;
|
|
365
|
+
font-weight: 600;
|
|
366
|
+
margin-bottom: 14px;
|
|
367
|
+
color: var(--text-primary);
|
|
368
|
+
}
|
|
369
|
+
.shortcut-row {
|
|
370
|
+
display: flex;
|
|
371
|
+
justify-content: space-between;
|
|
372
|
+
align-items: center;
|
|
373
|
+
padding: 5px 0;
|
|
374
|
+
font-size: 12px;
|
|
375
|
+
}
|
|
376
|
+
.shortcut-row span:first-child { color: var(--text-secondary); }
|
|
377
|
+
.shortcut-keys {
|
|
378
|
+
display: flex; gap: 3px;
|
|
379
|
+
}
|
|
380
|
+
kbd {
|
|
381
|
+
background: var(--bg-elevated);
|
|
382
|
+
border: 1px solid var(--border);
|
|
383
|
+
border-radius: 4px;
|
|
384
|
+
padding: 2px 6px;
|
|
385
|
+
font-family: 'SF Mono', Menlo, Consolas, monospace;
|
|
386
|
+
font-size: 11px;
|
|
387
|
+
color: var(--text-primary);
|
|
388
|
+
}
|
|
389
|
+
.shortcuts-close {
|
|
390
|
+
margin-top: 16px;
|
|
391
|
+
width: 100%;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* \u2500\u2500 Responsive \u2500\u2500 */
|
|
395
|
+
@media (max-width: 640px) {
|
|
396
|
+
header { flex-wrap: wrap; padding: 8px 12px; gap: 8px; }
|
|
397
|
+
.toolbar { gap: 3px; }
|
|
398
|
+
.btn .btn-label { display: none; }
|
|
399
|
+
.btn { padding: 5px 7px; }
|
|
400
|
+
.divider { display: none; }
|
|
401
|
+
main { padding: 12px; }
|
|
402
|
+
}
|
|
403
|
+
@media (max-width: 400px) {
|
|
404
|
+
.zoom-display { display: none; }
|
|
405
|
+
}
|
|
406
|
+
</style>
|
|
407
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
|
408
|
+
</head>
|
|
409
|
+
<body>
|
|
410
|
+
<header>
|
|
411
|
+
<div class="logo-icon">
|
|
412
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#6c5ce7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
413
|
+
<path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
|
|
414
|
+
<path d="m15 5 4 4"/>
|
|
415
|
+
</svg>
|
|
416
|
+
</div>
|
|
417
|
+
<div class="header-title">
|
|
418
|
+
<h1 id="page-title">Sketchdraw Preview</h1>
|
|
419
|
+
<div class="header-subtitle">
|
|
420
|
+
<span class="status-dot disconnected" id="status-dot"></span>
|
|
421
|
+
<span id="status-text">Connecting</span>
|
|
422
|
+
<span id="diagram-id-label"></span>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
<div class="toolbar" id="toolbar">
|
|
426
|
+
<button class="btn" id="zoom-out" title="Zoom out (-)">
|
|
427
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
428
|
+
</button>
|
|
429
|
+
<span class="zoom-display" id="zoom-display">100%</span>
|
|
430
|
+
<button class="btn" id="zoom-in" title="Zoom in (+)">
|
|
431
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
432
|
+
</button>
|
|
433
|
+
<button class="btn" id="zoom-fit" title="Reset zoom (0)">
|
|
434
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="m21 3-7 7"/><path d="m3 21 7-7"/></svg>
|
|
435
|
+
</button>
|
|
436
|
+
<div class="divider"></div>
|
|
437
|
+
<button class="btn" id="theme-toggle" title="Toggle canvas theme (T)">
|
|
438
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="theme-icon-sun"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
|
439
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="theme-icon-moon" style="display:none"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
440
|
+
</button>
|
|
441
|
+
<div class="divider"></div>
|
|
442
|
+
<button class="btn" id="download-svg" title="Download SVG (Cmd+S)">
|
|
443
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
444
|
+
<span class="btn-label">SVG</span>
|
|
445
|
+
</button>
|
|
446
|
+
<button class="btn btn-primary" id="download-png" title="Download PNG (Cmd+Shift+S)">
|
|
447
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
448
|
+
<span class="btn-label">PNG</span>
|
|
449
|
+
</button>
|
|
450
|
+
</div>
|
|
451
|
+
</header>
|
|
452
|
+
|
|
453
|
+
<main>
|
|
454
|
+
<div id="diagram-container">
|
|
455
|
+
<!-- Empty state -->
|
|
456
|
+
<div class="empty-state" id="empty-state">
|
|
457
|
+
<div class="empty-icon">
|
|
458
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
459
|
+
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
|
460
|
+
<path d="M3 9h18"/><path d="M9 21V9"/>
|
|
461
|
+
</svg>
|
|
462
|
+
</div>
|
|
463
|
+
<div class="empty-title">Waiting for a diagram...</div>
|
|
464
|
+
<div class="empty-subtitle">Use generate_diagram to send a diagram here</div>
|
|
465
|
+
<div class="dots-loading"><span></span><span></span><span></span></div>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<!-- Diagram viewport -->
|
|
469
|
+
<div id="diagram-viewport" style="display:none;">
|
|
470
|
+
<div class="diagram-transform" id="diagram-transform"></div>
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
<!-- Loading overlay -->
|
|
474
|
+
<div class="loading-overlay hidden" id="loading-overlay">
|
|
475
|
+
<div class="spinner"></div>
|
|
476
|
+
<div class="loading-text">Rendering diagram...</div>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<!-- Error panel -->
|
|
480
|
+
<div class="error-panel" id="error-panel">
|
|
481
|
+
<div class="error-header">
|
|
482
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
|
483
|
+
Render Error
|
|
484
|
+
</div>
|
|
485
|
+
<div class="error-message" id="error-message"></div>
|
|
486
|
+
<button class="btn error-dismiss" id="error-dismiss">Dismiss</button>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<!-- Info bar -->
|
|
490
|
+
<div class="info-bar" id="info-bar">
|
|
491
|
+
<span id="info-id"></span>
|
|
492
|
+
<span id="info-type"></span>
|
|
493
|
+
<span id="info-time"></span>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
</main>
|
|
497
|
+
|
|
498
|
+
<!-- Shortcuts overlay -->
|
|
499
|
+
<div class="shortcuts-overlay" id="shortcuts-overlay">
|
|
500
|
+
<div class="shortcuts-panel">
|
|
501
|
+
<h3>Keyboard Shortcuts</h3>
|
|
502
|
+
<div class="shortcut-row"><span>Download SVG</span><span class="shortcut-keys"><kbd>Cmd</kbd><kbd>S</kbd></span></div>
|
|
503
|
+
<div class="shortcut-row"><span>Download PNG</span><span class="shortcut-keys"><kbd>Cmd</kbd><kbd>Shift</kbd><kbd>S</kbd></span></div>
|
|
504
|
+
<div class="shortcut-row"><span>Zoom in</span><span class="shortcut-keys"><kbd>+</kbd></span></div>
|
|
505
|
+
<div class="shortcut-row"><span>Zoom out</span><span class="shortcut-keys"><kbd>-</kbd></span></div>
|
|
506
|
+
<div class="shortcut-row"><span>Reset zoom</span><span class="shortcut-keys"><kbd>0</kbd></span></div>
|
|
507
|
+
<div class="shortcut-row"><span>Toggle theme</span><span class="shortcut-keys"><kbd>T</kbd></span></div>
|
|
508
|
+
<div class="shortcut-row"><span>Show shortcuts</span><span class="shortcut-keys"><kbd>?</kbd></span></div>
|
|
509
|
+
<button class="btn shortcuts-close" id="shortcuts-close">Close</button>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
<script>
|
|
514
|
+
mermaid.initialize({ startOnLoad: false, theme: 'default' });
|
|
515
|
+
|
|
516
|
+
/* \u2500\u2500 DOM refs \u2500\u2500 */
|
|
517
|
+
const diagramContainer = document.getElementById('diagram-container');
|
|
518
|
+
const viewport = document.getElementById('diagram-viewport');
|
|
519
|
+
const transformEl = document.getElementById('diagram-transform');
|
|
520
|
+
const emptyState = document.getElementById('empty-state');
|
|
521
|
+
const loadingOverlay = document.getElementById('loading-overlay');
|
|
522
|
+
const errorPanel = document.getElementById('error-panel');
|
|
523
|
+
const errorMessage = document.getElementById('error-message');
|
|
524
|
+
const toolbar = document.getElementById('toolbar');
|
|
525
|
+
const statusDot = document.getElementById('status-dot');
|
|
526
|
+
const statusText = document.getElementById('status-text');
|
|
527
|
+
const diagramIdLabel = document.getElementById('diagram-id-label');
|
|
528
|
+
const zoomDisplay = document.getElementById('zoom-display');
|
|
529
|
+
const infoBar = document.getElementById('info-bar');
|
|
530
|
+
const infoId = document.getElementById('info-id');
|
|
531
|
+
const infoType = document.getElementById('info-type');
|
|
532
|
+
const infoTime = document.getElementById('info-time');
|
|
533
|
+
const shortcutsOverlay = document.getElementById('shortcuts-overlay');
|
|
534
|
+
const themeIconSun = document.getElementById('theme-icon-sun');
|
|
535
|
+
const themeIconMoon = document.getElementById('theme-icon-moon');
|
|
536
|
+
|
|
537
|
+
/* \u2500\u2500 State \u2500\u2500 */
|
|
538
|
+
let ws;
|
|
539
|
+
let currentDiagramId = null;
|
|
540
|
+
let zoom = 1;
|
|
541
|
+
let panX = 0, panY = 0;
|
|
542
|
+
let svgBaseWidth = 0, svgBaseHeight = 0;
|
|
543
|
+
let isPanning = false;
|
|
544
|
+
let panStartX = 0, panStartY = 0;
|
|
545
|
+
let darkCanvas = localStorage.getItem('sketchdraw-dark-canvas') === 'true';
|
|
546
|
+
|
|
547
|
+
const ZOOM_MIN = 0.1;
|
|
548
|
+
const ZOOM_MAX = 5;
|
|
549
|
+
const ZOOM_STEP = 0.15;
|
|
550
|
+
|
|
551
|
+
/* \u2500\u2500 Init canvas theme \u2500\u2500 */
|
|
552
|
+
if (darkCanvas) {
|
|
553
|
+
diagramContainer.classList.add('dark-canvas');
|
|
554
|
+
themeIconSun.style.display = 'none';
|
|
555
|
+
themeIconMoon.style.display = '';
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/* \u2500\u2500 Helpers \u2500\u2500 */
|
|
559
|
+
function applyTransform() {
|
|
560
|
+
var svg = transformEl.querySelector('svg');
|
|
561
|
+
if (svg) {
|
|
562
|
+
svg.setAttribute('width', svgBaseWidth * zoom);
|
|
563
|
+
svg.setAttribute('height', svgBaseHeight * zoom);
|
|
564
|
+
}
|
|
565
|
+
transformEl.style.transform = 'translate(' + panX + 'px,' + panY + 'px)';
|
|
566
|
+
zoomDisplay.textContent = Math.round(zoom * 100) + '%';
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function fitToViewport() {
|
|
570
|
+
if (!svgBaseWidth || !svgBaseHeight) return;
|
|
571
|
+
var rect = viewport.getBoundingClientRect();
|
|
572
|
+
var pad = 32;
|
|
573
|
+
var availW = rect.width - pad * 2;
|
|
574
|
+
var availH = rect.height - pad * 2;
|
|
575
|
+
if (availW <= 0 || availH <= 0) return;
|
|
576
|
+
var scaleX = availW / svgBaseWidth;
|
|
577
|
+
var scaleY = availH / svgBaseHeight;
|
|
578
|
+
zoom = Math.min(scaleX, scaleY);
|
|
579
|
+
var renderedW = svgBaseWidth * zoom;
|
|
580
|
+
var renderedH = svgBaseHeight * zoom;
|
|
581
|
+
panX = (rect.width - renderedW) / 2;
|
|
582
|
+
panY = (rect.height - renderedH) / 2;
|
|
583
|
+
applyTransform();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
transformEl.addEventListener('animationend', function() {
|
|
587
|
+
transformEl.classList.remove('diagram-entrance');
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
function setZoom(newZoom, centerX, centerY) {
|
|
591
|
+
const clamped = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, newZoom));
|
|
592
|
+
if (centerX !== undefined && centerY !== undefined) {
|
|
593
|
+
const ratio = clamped / zoom;
|
|
594
|
+
panX = centerX - ratio * (centerX - panX);
|
|
595
|
+
panY = centerY - ratio * (centerY - panY);
|
|
596
|
+
}
|
|
597
|
+
zoom = clamped;
|
|
598
|
+
applyTransform();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function resetZoom() {
|
|
602
|
+
fitToViewport();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function showState(state) {
|
|
606
|
+
emptyState.style.display = state === 'empty' ? 'flex' : 'none';
|
|
607
|
+
viewport.style.display = state === 'diagram' ? 'flex' : 'none';
|
|
608
|
+
errorPanel.classList.toggle('visible', state === 'error');
|
|
609
|
+
if (state === 'diagram') {
|
|
610
|
+
loadingOverlay.classList.remove('hidden');
|
|
611
|
+
}
|
|
612
|
+
if (state !== 'diagram') {
|
|
613
|
+
infoBar.classList.remove('visible');
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function showToolbar() {
|
|
618
|
+
toolbar.classList.add('visible');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function detectDiagramType(syntax) {
|
|
622
|
+
const first = syntax.trim().split('\\n')[0].trim().toLowerCase();
|
|
623
|
+
const types = ['graph','flowchart','sequenceDiagram','classDiagram','stateDiagram','erDiagram','journey','gantt','pie','quadrantChart','requirementDiagram','gitGraph','mindmap','timeline','sankey','xychart'];
|
|
624
|
+
for (const t of types) {
|
|
625
|
+
if (first.startsWith(t.toLowerCase())) return t;
|
|
626
|
+
}
|
|
627
|
+
return 'diagram';
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function formatTime(d) {
|
|
631
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/* \u2500\u2500 Canvas theme toggle \u2500\u2500 */
|
|
635
|
+
function toggleCanvasTheme() {
|
|
636
|
+
darkCanvas = !darkCanvas;
|
|
637
|
+
diagramContainer.classList.toggle('dark-canvas', darkCanvas);
|
|
638
|
+
themeIconSun.style.display = darkCanvas ? 'none' : '';
|
|
639
|
+
themeIconMoon.style.display = darkCanvas ? '' : 'none';
|
|
640
|
+
localStorage.setItem('sketchdraw-dark-canvas', darkCanvas);
|
|
641
|
+
}
|
|
642
|
+
document.getElementById('theme-toggle').addEventListener('click', toggleCanvasTheme);
|
|
643
|
+
|
|
644
|
+
/* \u2500\u2500 Zoom buttons \u2500\u2500 */
|
|
645
|
+
document.getElementById('zoom-in').addEventListener('click', function() { setZoom(zoom + ZOOM_STEP); });
|
|
646
|
+
document.getElementById('zoom-out').addEventListener('click', function() { setZoom(zoom - ZOOM_STEP); });
|
|
647
|
+
document.getElementById('zoom-fit').addEventListener('click', resetZoom);
|
|
648
|
+
|
|
649
|
+
/* \u2500\u2500 Mouse wheel zoom \u2500\u2500 */
|
|
650
|
+
viewport.addEventListener('wheel', function(e) {
|
|
651
|
+
e.preventDefault();
|
|
652
|
+
var rect = viewport.getBoundingClientRect();
|
|
653
|
+
var cx = e.clientX - rect.left;
|
|
654
|
+
var cy = e.clientY - rect.top;
|
|
655
|
+
var delta = -e.deltaY * 0.002;
|
|
656
|
+
setZoom(zoom + delta, cx, cy);
|
|
657
|
+
}, { passive: false });
|
|
658
|
+
|
|
659
|
+
/* \u2500\u2500 Pan with mouse \u2500\u2500 */
|
|
660
|
+
viewport.addEventListener('mousedown', function(e) {
|
|
661
|
+
if (e.button !== 0) return;
|
|
662
|
+
isPanning = true;
|
|
663
|
+
panStartX = e.clientX - panX;
|
|
664
|
+
panStartY = e.clientY - panY;
|
|
665
|
+
viewport.classList.add('grabbing');
|
|
666
|
+
e.preventDefault();
|
|
667
|
+
});
|
|
668
|
+
window.addEventListener('mousemove', function(e) {
|
|
669
|
+
if (!isPanning) return;
|
|
670
|
+
panX = e.clientX - panStartX;
|
|
671
|
+
panY = e.clientY - panStartY;
|
|
672
|
+
applyTransform();
|
|
673
|
+
});
|
|
674
|
+
window.addEventListener('mouseup', function() {
|
|
675
|
+
if (!isPanning) return;
|
|
676
|
+
isPanning = false;
|
|
677
|
+
viewport.classList.remove('grabbing');
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
/* \u2500\u2500 Touch pan \u2500\u2500 */
|
|
681
|
+
var touchId = null;
|
|
682
|
+
viewport.addEventListener('touchstart', function(e) {
|
|
683
|
+
if (e.touches.length === 1) {
|
|
684
|
+
var t = e.touches[0];
|
|
685
|
+
touchId = t.identifier;
|
|
686
|
+
panStartX = t.clientX - panX;
|
|
687
|
+
panStartY = t.clientY - panY;
|
|
688
|
+
viewport.classList.add('grabbing');
|
|
689
|
+
}
|
|
690
|
+
}, { passive: true });
|
|
691
|
+
viewport.addEventListener('touchmove', function(e) {
|
|
692
|
+
for (var i = 0; i < e.changedTouches.length; i++) {
|
|
693
|
+
var t = e.changedTouches[i];
|
|
694
|
+
if (t.identifier === touchId) {
|
|
695
|
+
panX = t.clientX - panStartX;
|
|
696
|
+
panY = t.clientY - panStartY;
|
|
697
|
+
applyTransform();
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}, { passive: true });
|
|
702
|
+
viewport.addEventListener('touchend', function(e) {
|
|
703
|
+
for (var i = 0; i < e.changedTouches.length; i++) {
|
|
704
|
+
if (e.changedTouches[i].identifier === touchId) {
|
|
705
|
+
touchId = null;
|
|
706
|
+
viewport.classList.remove('grabbing');
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}, { passive: true });
|
|
711
|
+
|
|
712
|
+
/* \u2500\u2500 Error dismiss \u2500\u2500 */
|
|
713
|
+
document.getElementById('error-dismiss').addEventListener('click', function() {
|
|
714
|
+
showState('empty');
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
/* \u2500\u2500 Shortcuts overlay \u2500\u2500 */
|
|
718
|
+
document.getElementById('shortcuts-close').addEventListener('click', function() {
|
|
719
|
+
shortcutsOverlay.classList.remove('visible');
|
|
720
|
+
});
|
|
721
|
+
shortcutsOverlay.addEventListener('click', function(e) {
|
|
722
|
+
if (e.target === shortcutsOverlay) shortcutsOverlay.classList.remove('visible');
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
/* \u2500\u2500 Download SVG \u2500\u2500 */
|
|
726
|
+
function downloadSvg() {
|
|
727
|
+
var svg = transformEl.querySelector('svg');
|
|
728
|
+
if (!svg) return;
|
|
729
|
+
var clone = svg.cloneNode(true);
|
|
730
|
+
clone.setAttribute('width', svgBaseWidth);
|
|
731
|
+
clone.setAttribute('height', svgBaseHeight);
|
|
732
|
+
var blob = new Blob([clone.outerHTML], { type: 'image/svg+xml' });
|
|
733
|
+
var url = URL.createObjectURL(blob);
|
|
734
|
+
var a = document.createElement('a');
|
|
735
|
+
a.href = url;
|
|
736
|
+
a.download = (currentDiagramId || 'diagram') + '.svg';
|
|
737
|
+
a.click();
|
|
738
|
+
URL.revokeObjectURL(url);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/* \u2500\u2500 Download PNG \u2500\u2500 */
|
|
742
|
+
function downloadPng() {
|
|
743
|
+
var svg = transformEl.querySelector('svg');
|
|
744
|
+
if (!svg) return;
|
|
745
|
+
var clone = svg.cloneNode(true);
|
|
746
|
+
var w = svgBaseWidth;
|
|
747
|
+
var h = svgBaseHeight;
|
|
748
|
+
clone.setAttribute('width', w);
|
|
749
|
+
clone.setAttribute('height', h);
|
|
750
|
+
if (diagramContainer.classList.contains('dark-canvas')) {
|
|
751
|
+
clone.style.filter = 'invert(1) hue-rotate(180deg)';
|
|
752
|
+
}
|
|
753
|
+
var svgData = new XMLSerializer().serializeToString(clone);
|
|
754
|
+
var dataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData);
|
|
755
|
+
var img = new Image();
|
|
756
|
+
img.onload = function() {
|
|
757
|
+
var scale = 2;
|
|
758
|
+
var canvas = document.createElement('canvas');
|
|
759
|
+
canvas.width = w * scale;
|
|
760
|
+
canvas.height = h * scale;
|
|
761
|
+
var ctx = canvas.getContext('2d');
|
|
762
|
+
ctx.scale(scale, scale);
|
|
763
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
764
|
+
canvas.toBlob(function(blob) {
|
|
765
|
+
if (!blob) return;
|
|
766
|
+
var pngUrl = URL.createObjectURL(blob);
|
|
767
|
+
var a = document.createElement('a');
|
|
768
|
+
a.href = pngUrl;
|
|
769
|
+
a.download = (currentDiagramId || 'diagram') + '.png';
|
|
770
|
+
a.click();
|
|
771
|
+
URL.revokeObjectURL(pngUrl);
|
|
772
|
+
}, 'image/png');
|
|
773
|
+
};
|
|
774
|
+
img.src = dataUrl;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
document.getElementById('download-svg').addEventListener('click', downloadSvg);
|
|
778
|
+
document.getElementById('download-png').addEventListener('click', downloadPng);
|
|
779
|
+
|
|
780
|
+
/* \u2500\u2500 Keyboard shortcuts \u2500\u2500 */
|
|
781
|
+
document.addEventListener('keydown', function(e) {
|
|
782
|
+
var mod = e.metaKey || e.ctrlKey;
|
|
783
|
+
|
|
784
|
+
if (mod && e.shiftKey && e.key.toLowerCase() === 's') {
|
|
785
|
+
e.preventDefault();
|
|
786
|
+
downloadPng();
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (mod && e.key.toLowerCase() === 's') {
|
|
790
|
+
e.preventDefault();
|
|
791
|
+
downloadSvg();
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
796
|
+
|
|
797
|
+
if (e.key === '+' || e.key === '=') { setZoom(zoom + ZOOM_STEP); e.preventDefault(); }
|
|
798
|
+
else if (e.key === '-') { setZoom(zoom - ZOOM_STEP); e.preventDefault(); }
|
|
799
|
+
else if (e.key === '0') { resetZoom(); e.preventDefault(); }
|
|
800
|
+
else if (e.key.toLowerCase() === 't' && !mod) { toggleCanvasTheme(); e.preventDefault(); }
|
|
801
|
+
else if (e.key === '?' || (e.shiftKey && e.key === '/')) {
|
|
802
|
+
shortcutsOverlay.classList.toggle('visible');
|
|
803
|
+
e.preventDefault();
|
|
804
|
+
}
|
|
805
|
+
else if (e.key === 'Escape') {
|
|
806
|
+
shortcutsOverlay.classList.remove('visible');
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
/* \u2500\u2500 WebSocket connection \u2500\u2500 */
|
|
811
|
+
function connect() {
|
|
812
|
+
ws = new WebSocket('ws://' + location.host + '/ws');
|
|
813
|
+
|
|
814
|
+
ws.onopen = function() {
|
|
815
|
+
statusDot.className = 'status-dot connected';
|
|
816
|
+
statusText.textContent = 'Connected';
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
ws.onmessage = async function(event) {
|
|
820
|
+
var data = JSON.parse(event.data);
|
|
821
|
+
if (data.type === 'title') {
|
|
822
|
+
document.title = data.title;
|
|
823
|
+
document.getElementById('page-title').textContent = data.title;
|
|
824
|
+
} else if (data.type === 'mermaid') {
|
|
825
|
+
currentDiagramId = data.id;
|
|
826
|
+
diagramIdLabel.textContent = data.id ? ' \xB7 ' + data.id : '';
|
|
827
|
+
|
|
828
|
+
showState('diagram');
|
|
829
|
+
|
|
830
|
+
try {
|
|
831
|
+
var result = await mermaid.render('mermaid-output', data.syntax);
|
|
832
|
+
transformEl.innerHTML = result.svg;
|
|
833
|
+
|
|
834
|
+
var svg = transformEl.querySelector('svg');
|
|
835
|
+
if (svg) {
|
|
836
|
+
var vb = svg.viewBox.baseVal;
|
|
837
|
+
if (vb && vb.width) {
|
|
838
|
+
svgBaseWidth = vb.width;
|
|
839
|
+
svgBaseHeight = vb.height;
|
|
840
|
+
} else {
|
|
841
|
+
svgBaseWidth = svg.width.baseVal.value || svg.getBoundingClientRect().width;
|
|
842
|
+
svgBaseHeight = svg.height.baseVal.value || svg.getBoundingClientRect().height;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
fitToViewport();
|
|
847
|
+
transformEl.classList.remove('diagram-entrance');
|
|
848
|
+
void transformEl.offsetWidth;
|
|
849
|
+
transformEl.classList.add('diagram-entrance');
|
|
850
|
+
loadingOverlay.classList.add('hidden');
|
|
851
|
+
showToolbar();
|
|
852
|
+
|
|
853
|
+
/* Info bar */
|
|
854
|
+
infoId.textContent = 'ID: ' + (data.id || '\u2013');
|
|
855
|
+
infoType.textContent = 'Type: ' + detectDiagramType(data.syntax);
|
|
856
|
+
infoTime.textContent = 'Updated: ' + formatTime(new Date());
|
|
857
|
+
infoBar.classList.add('visible');
|
|
858
|
+
|
|
859
|
+
if (ws.readyState === 1) {
|
|
860
|
+
ws.send(JSON.stringify({ type: 'svg-result', id: data.id, svg: result.svg }));
|
|
861
|
+
}
|
|
862
|
+
} catch (err) {
|
|
863
|
+
showState('error');
|
|
864
|
+
errorMessage.textContent = err.message || String(err);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
ws.onclose = function() {
|
|
870
|
+
statusDot.className = 'status-dot disconnected';
|
|
871
|
+
statusText.textContent = 'Disconnected';
|
|
872
|
+
setTimeout(connect, 2000);
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
ws.onerror = function() { ws.close(); };
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
connect();
|
|
879
|
+
</script>
|
|
880
|
+
</body>
|
|
881
|
+
</html>`;
|
|
882
|
+
var PreviewServer = class {
|
|
883
|
+
httpServer;
|
|
884
|
+
wss;
|
|
885
|
+
clients = /* @__PURE__ */ new Set();
|
|
886
|
+
currentContent = null;
|
|
887
|
+
port;
|
|
888
|
+
title = "Sketchdraw Preview";
|
|
889
|
+
onSvgRendered = null;
|
|
890
|
+
constructor(port = 0) {
|
|
891
|
+
this.port = port;
|
|
892
|
+
this.httpServer = createServer((req, res) => {
|
|
893
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
894
|
+
res.end(HTML_PAGE);
|
|
895
|
+
});
|
|
896
|
+
this.wss = new WebSocketServer({ server: this.httpServer, path: "/ws" });
|
|
897
|
+
this.wss.on("connection", (ws) => {
|
|
898
|
+
this.clients.add(ws);
|
|
899
|
+
ws.send(JSON.stringify({ type: "title", title: this.title }));
|
|
900
|
+
if (this.currentContent) {
|
|
901
|
+
ws.send(JSON.stringify(this.currentContent));
|
|
902
|
+
}
|
|
903
|
+
ws.on("message", (raw) => {
|
|
904
|
+
try {
|
|
905
|
+
const msg = JSON.parse(typeof raw === "string" ? raw : raw.toString());
|
|
906
|
+
if (msg.type === "svg-result" && msg.id && msg.svg) {
|
|
907
|
+
this.onSvgRendered?.(msg.id, msg.svg);
|
|
908
|
+
}
|
|
909
|
+
} catch {
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
ws.on("close", () => {
|
|
913
|
+
this.clients.delete(ws);
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
async start() {
|
|
918
|
+
return new Promise((resolve2, reject) => {
|
|
919
|
+
this.httpServer.listen(this.port, () => {
|
|
920
|
+
const addr = this.httpServer.address();
|
|
921
|
+
if (addr && typeof addr === "object") {
|
|
922
|
+
this.port = addr.port;
|
|
923
|
+
}
|
|
924
|
+
const url = `http://localhost:${this.port}`;
|
|
925
|
+
resolve2(url);
|
|
926
|
+
});
|
|
927
|
+
this.httpServer.on("error", reject);
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
broadcast(message) {
|
|
931
|
+
const data = JSON.stringify(message);
|
|
932
|
+
for (const client of this.clients) {
|
|
933
|
+
if (client.readyState === 1) {
|
|
934
|
+
client.send(data);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
setTitle(title) {
|
|
939
|
+
this.title = title;
|
|
940
|
+
this.broadcast({ type: "title", title });
|
|
941
|
+
}
|
|
942
|
+
updateMermaid(id, syntax, title) {
|
|
943
|
+
if (title)
|
|
944
|
+
this.setTitle(title);
|
|
945
|
+
this.currentContent = { type: "mermaid", syntax, id };
|
|
946
|
+
this.broadcast(this.currentContent);
|
|
947
|
+
}
|
|
948
|
+
openBrowser() {
|
|
949
|
+
const url = `http://localhost:${this.port}`;
|
|
950
|
+
const platform = process.platform;
|
|
951
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
952
|
+
exec(`${cmd} ${url}`);
|
|
953
|
+
}
|
|
954
|
+
async stop() {
|
|
955
|
+
for (const client of this.clients) {
|
|
956
|
+
client.close();
|
|
957
|
+
}
|
|
958
|
+
this.wss.close();
|
|
959
|
+
return new Promise((resolve2) => {
|
|
960
|
+
this.httpServer.close(() => resolve2());
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// src/index.ts
|
|
966
|
+
var diagrams = /* @__PURE__ */ new Map();
|
|
967
|
+
var nextId = 1;
|
|
968
|
+
function generateId() {
|
|
969
|
+
return `diagram_${nextId++}`;
|
|
970
|
+
}
|
|
971
|
+
var previewServers = /* @__PURE__ */ new Map();
|
|
972
|
+
async function ensurePreviewForDiagram(id) {
|
|
973
|
+
const existing = previewServers.get(id);
|
|
974
|
+
if (existing) return existing;
|
|
975
|
+
const preview = new PreviewServer();
|
|
976
|
+
preview.onSvgRendered = (diagId, svg) => {
|
|
977
|
+
const stored = diagrams.get(diagId);
|
|
978
|
+
if (stored) stored.svg = svg;
|
|
979
|
+
};
|
|
980
|
+
try {
|
|
981
|
+
const url = await preview.start();
|
|
982
|
+
const entry = { server: preview, url };
|
|
983
|
+
previewServers.set(id, entry);
|
|
984
|
+
preview.openBrowser();
|
|
985
|
+
return entry;
|
|
986
|
+
} catch {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async function pushMermaidToPreview(id, syntax, title) {
|
|
991
|
+
const entry = await ensurePreviewForDiagram(id);
|
|
992
|
+
if (!entry) return null;
|
|
993
|
+
entry.server.updateMermaid(id, syntax, title);
|
|
994
|
+
return entry.url;
|
|
995
|
+
}
|
|
996
|
+
var server = new McpServer({
|
|
997
|
+
name: "mermaid-live-mcp",
|
|
998
|
+
version: "0.1.0"
|
|
999
|
+
});
|
|
1000
|
+
server.tool(
|
|
1001
|
+
"generate_mermaid",
|
|
1002
|
+
"Generate a diagram from Mermaid.js syntax. Renders in live preview browser with SVG/PNG download options. Supports flowcharts, sequence diagrams, class diagrams, state diagrams, ER diagrams, Gantt charts, and more.",
|
|
1003
|
+
{
|
|
1004
|
+
syntax: z.string().describe("Mermaid diagram syntax (e.g. 'graph TD; A-->B;')"),
|
|
1005
|
+
title: z.string().optional().describe("Optional title for the browser preview tab")
|
|
1006
|
+
},
|
|
1007
|
+
async ({ syntax, title }) => {
|
|
1008
|
+
const id = generateId();
|
|
1009
|
+
diagrams.set(id, {
|
|
1010
|
+
id,
|
|
1011
|
+
syntax,
|
|
1012
|
+
svg: "",
|
|
1013
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1014
|
+
});
|
|
1015
|
+
const preview = await pushMermaidToPreview(id, syntax, title);
|
|
1016
|
+
const storedDiagram = diagrams.get(id);
|
|
1017
|
+
if (storedDiagram && preview) storedDiagram.previewUrl = preview;
|
|
1018
|
+
const responseLines = [`Mermaid diagram sent to preview.`, `ID: ${id}`];
|
|
1019
|
+
if (preview) {
|
|
1020
|
+
responseLines.push(`Preview: ${preview}`);
|
|
1021
|
+
responseLines.push(
|
|
1022
|
+
`Use the download buttons in the browser to export as SVG or PNG.`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
return {
|
|
1026
|
+
content: [{ type: "text", text: responseLines.join("\n") }]
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
);
|
|
1030
|
+
server.tool(
|
|
1031
|
+
"update_diagram",
|
|
1032
|
+
"Replace a diagram's Mermaid syntax and re-render it in the preview.",
|
|
1033
|
+
{
|
|
1034
|
+
diagram_id: z.string().describe("The ID of the diagram to update"),
|
|
1035
|
+
syntax: z.string().describe("New Mermaid diagram syntax to replace the existing one"),
|
|
1036
|
+
title: z.string().optional().describe("Optional title for the browser preview tab")
|
|
1037
|
+
},
|
|
1038
|
+
async ({ diagram_id, syntax, title }) => {
|
|
1039
|
+
const stored = diagrams.get(diagram_id);
|
|
1040
|
+
if (!stored) {
|
|
1041
|
+
return {
|
|
1042
|
+
content: [
|
|
1043
|
+
{
|
|
1044
|
+
type: "text",
|
|
1045
|
+
text: `Diagram not found: ${diagram_id}. Use list_diagrams to see available diagrams.`
|
|
1046
|
+
}
|
|
1047
|
+
],
|
|
1048
|
+
isError: true
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
stored.syntax = syntax;
|
|
1052
|
+
stored.svg = "";
|
|
1053
|
+
const preview = await pushMermaidToPreview(diagram_id, syntax, title);
|
|
1054
|
+
const responseLines = [`Diagram ${diagram_id} updated successfully.`];
|
|
1055
|
+
if (preview) {
|
|
1056
|
+
responseLines.push(`Preview: ${preview}`);
|
|
1057
|
+
}
|
|
1058
|
+
return {
|
|
1059
|
+
content: [{ type: "text", text: responseLines.join("\n") }]
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
);
|
|
1063
|
+
server.tool(
|
|
1064
|
+
"list_diagrams",
|
|
1065
|
+
"List all diagrams generated in this session.",
|
|
1066
|
+
{},
|
|
1067
|
+
async () => {
|
|
1068
|
+
if (diagrams.size === 0) {
|
|
1069
|
+
return {
|
|
1070
|
+
content: [
|
|
1071
|
+
{ type: "text", text: "No diagrams generated yet." }
|
|
1072
|
+
]
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
const lines = Array.from(diagrams.values()).map((d) => {
|
|
1076
|
+
const firstLine = d.syntax.split("\n")[0].trim();
|
|
1077
|
+
const parts = [
|
|
1078
|
+
`ID: ${d.id}`,
|
|
1079
|
+
`Created: ${d.createdAt.toISOString()}`,
|
|
1080
|
+
`SVG available: ${d.svg ? "yes" : "no"}`,
|
|
1081
|
+
`Syntax: ${firstLine}`
|
|
1082
|
+
];
|
|
1083
|
+
if (d.previewUrl) {
|
|
1084
|
+
parts.push(`Preview: ${d.previewUrl}`);
|
|
1085
|
+
}
|
|
1086
|
+
if (d.filePath) {
|
|
1087
|
+
parts.push(`File: ${d.filePath}`);
|
|
1088
|
+
}
|
|
1089
|
+
return parts.join(" | ");
|
|
1090
|
+
});
|
|
1091
|
+
return {
|
|
1092
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
);
|
|
1096
|
+
server.tool(
|
|
1097
|
+
"export_diagram",
|
|
1098
|
+
"Write a diagram's SVG to disk. PNG export is available via the browser download buttons.",
|
|
1099
|
+
{
|
|
1100
|
+
diagram_id: z.string().describe("The ID of the diagram to export"),
|
|
1101
|
+
path: z.string().describe("File path to save the SVG file")
|
|
1102
|
+
},
|
|
1103
|
+
async ({ diagram_id, path: outputPath }) => {
|
|
1104
|
+
const stored = diagrams.get(diagram_id);
|
|
1105
|
+
if (!stored) {
|
|
1106
|
+
return {
|
|
1107
|
+
content: [
|
|
1108
|
+
{
|
|
1109
|
+
type: "text",
|
|
1110
|
+
text: `Diagram not found: ${diagram_id}. Use list_diagrams to see available diagrams.`
|
|
1111
|
+
}
|
|
1112
|
+
],
|
|
1113
|
+
isError: true
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
if (!stored.svg) {
|
|
1117
|
+
return {
|
|
1118
|
+
content: [
|
|
1119
|
+
{
|
|
1120
|
+
type: "text",
|
|
1121
|
+
text: `SVG not yet available for ${diagram_id}. The browser may still be rendering \u2014 try again in a moment.`
|
|
1122
|
+
}
|
|
1123
|
+
],
|
|
1124
|
+
isError: true
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
const outPath = resolve(outputPath);
|
|
1128
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
1129
|
+
writeFileSync(outPath, stored.svg, "utf-8");
|
|
1130
|
+
stored.filePath = outPath;
|
|
1131
|
+
return {
|
|
1132
|
+
content: [
|
|
1133
|
+
{
|
|
1134
|
+
type: "text",
|
|
1135
|
+
text: `Exported SVG to: ${outPath}`
|
|
1136
|
+
}
|
|
1137
|
+
]
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
);
|
|
1141
|
+
async function main() {
|
|
1142
|
+
const transport = new StdioServerTransport();
|
|
1143
|
+
await server.connect(transport);
|
|
1144
|
+
}
|
|
1145
|
+
main().catch((err) => {
|
|
1146
|
+
console.error("Failed to start MCP server:", err);
|
|
1147
|
+
process.exit(1);
|
|
1148
|
+
});
|
|
1149
|
+
async function shutdownAllPreviews() {
|
|
1150
|
+
const stops = Array.from(previewServers.values()).map(
|
|
1151
|
+
({ server: server2 }) => server2.stop()
|
|
1152
|
+
);
|
|
1153
|
+
await Promise.allSettled(stops);
|
|
1154
|
+
previewServers.clear();
|
|
1155
|
+
}
|
|
1156
|
+
process.on("SIGINT", async () => {
|
|
1157
|
+
await shutdownAllPreviews();
|
|
1158
|
+
process.exit(0);
|
|
1159
|
+
});
|
|
1160
|
+
process.on("SIGTERM", async () => {
|
|
1161
|
+
await shutdownAllPreviews();
|
|
1162
|
+
process.exit(0);
|
|
1163
|
+
});
|
|
1164
|
+
//# sourceMappingURL=index.js.map
|