klaude-code 1.2.27__py3-none-any.whl → 1.2.29__py3-none-any.whl
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.
- klaude_code/cli/config_cmd.py +13 -6
- klaude_code/cli/debug.py +9 -1
- klaude_code/cli/list_model.py +1 -1
- klaude_code/cli/main.py +39 -14
- klaude_code/cli/runtime.py +11 -5
- klaude_code/command/__init__.py +3 -0
- klaude_code/command/export_online_cmd.py +15 -12
- klaude_code/command/fork_session_cmd.py +42 -0
- klaude_code/config/__init__.py +11 -1
- klaude_code/config/config.py +21 -17
- klaude_code/config/select_model.py +1 -0
- klaude_code/core/executor.py +2 -1
- klaude_code/core/reminders.py +52 -16
- klaude_code/core/tool/web/mermaid_tool.md +17 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -2
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/model.py +2 -0
- klaude_code/session/export.py +61 -17
- klaude_code/session/session.py +23 -1
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/trace/log.py +7 -1
- klaude_code/ui/modes/repl/__init__.py +3 -44
- klaude_code/ui/modes/repl/completers.py +35 -3
- klaude_code/ui/modes/repl/event_handler.py +9 -5
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +32 -65
- klaude_code/ui/modes/repl/renderer.py +1 -6
- klaude_code/ui/renderers/assistant.py +4 -2
- klaude_code/ui/renderers/common.py +11 -4
- klaude_code/ui/renderers/developer.py +26 -7
- klaude_code/ui/renderers/errors.py +10 -5
- klaude_code/ui/renderers/mermaid_viewer.py +58 -0
- klaude_code/ui/renderers/tools.py +46 -18
- klaude_code/ui/rich/markdown.py +4 -4
- klaude_code/ui/rich/theme.py +12 -2
- {klaude_code-1.2.27.dist-info → klaude_code-1.2.29.dist-info}/METADATA +1 -1
- {klaude_code-1.2.27.dist-info → klaude_code-1.2.29.dist-info}/RECORD +39 -36
- {klaude_code-1.2.27.dist-info → klaude_code-1.2.29.dist-info}/entry_points.txt +1 -0
- {klaude_code-1.2.27.dist-info → klaude_code-1.2.29.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
|
8
|
+
/>
|
|
9
|
+
<title>Klaude Code - Mermaid</title>
|
|
10
|
+
<link
|
|
11
|
+
rel="icon"
|
|
12
|
+
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%233b82f6%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22><polyline points=%2216 18 22 12 16 6%22></polyline><polyline points=%228 6 2 12 8 18%22></polyline></svg>"
|
|
13
|
+
/>
|
|
14
|
+
<link
|
|
15
|
+
href="https://cdn.jsdelivr.net/npm/@fontsource/geist/400.css"
|
|
16
|
+
rel="stylesheet"
|
|
17
|
+
/>
|
|
18
|
+
<link
|
|
19
|
+
href="https://cdn.jsdelivr.net/npm/@fontsource/geist/600.css"
|
|
20
|
+
rel="stylesheet"
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
|
|
25
|
+
:root {
|
|
26
|
+
--font-sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
27
|
+
Roboto, sans-serif;
|
|
28
|
+
--font-mono: "SF Mono", Menlo, ui-monospace, SFMono-Regular, monospace;
|
|
29
|
+
--bg-color: #f8fafc;
|
|
30
|
+
--panel-bg: rgba(255, 255, 255, 0.95);
|
|
31
|
+
--panel-border: rgba(226, 232, 240, 1);
|
|
32
|
+
--text-primary: #1e293b;
|
|
33
|
+
--text-secondary: #64748b;
|
|
34
|
+
--accent: #3b82f6;
|
|
35
|
+
--accent-hover: #2563eb;
|
|
36
|
+
--error: #ef4444;
|
|
37
|
+
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
38
|
+
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1),
|
|
39
|
+
0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
40
|
+
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1),
|
|
41
|
+
0 4px 6px -4px rgb(0 0 0 / 0.05);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
* {
|
|
45
|
+
box-sizing: border-box;
|
|
46
|
+
margin: 0;
|
|
47
|
+
padding: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
body {
|
|
51
|
+
margin: 0;
|
|
52
|
+
height: 100vh;
|
|
53
|
+
width: 100vw;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
background-color: var(--bg-color);
|
|
56
|
+
font-family: var(--font-sans);
|
|
57
|
+
color: var(--text-primary);
|
|
58
|
+
-webkit-font-smoothing: antialiased;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.app {
|
|
62
|
+
position: relative;
|
|
63
|
+
width: 100%;
|
|
64
|
+
height: 100%;
|
|
65
|
+
display: flex;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Preview / Canvas Area */
|
|
69
|
+
.preview-container {
|
|
70
|
+
position: absolute;
|
|
71
|
+
inset: 0;
|
|
72
|
+
z-index: 0;
|
|
73
|
+
overflow: hidden;
|
|
74
|
+
cursor: grab;
|
|
75
|
+
background-color: #f8fafc;
|
|
76
|
+
background-image: radial-gradient(#cbd5e1 1px, transparent 1px);
|
|
77
|
+
background-size: 24px 24px;
|
|
78
|
+
/* Hardware acceleration can cause blurriness on scale if not careful.
|
|
79
|
+
We removed will-change: transform to prefer crisp rasterization on each frame/zoom stop. */
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.preview-container:active,
|
|
83
|
+
.preview-container.grabbing {
|
|
84
|
+
cursor: grabbing;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.canvas-wrapper {
|
|
88
|
+
width: 100%;
|
|
89
|
+
height: 100%;
|
|
90
|
+
transform-origin: 0 0;
|
|
91
|
+
/* Removed will-change: transform to fix blurriness on zoom */
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
pointer-events: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#canvas {
|
|
99
|
+
pointer-events: auto;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#canvas svg {
|
|
103
|
+
overflow: visible;
|
|
104
|
+
/* Ensure specific dimensions from mermaid are respected but allow override */
|
|
105
|
+
max-width: none !important;
|
|
106
|
+
height: auto;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Mermaid edge/arrow thickness */
|
|
110
|
+
#canvas svg .flowchart-link,
|
|
111
|
+
#canvas svg .edgePath path,
|
|
112
|
+
#canvas svg .edgePath path.path {
|
|
113
|
+
stroke-width: 1px !important;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#canvas svg marker path {
|
|
117
|
+
stroke-width: 1px !important;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Floating Editor Panel (Sidebar) */
|
|
121
|
+
.editor-panel {
|
|
122
|
+
position: absolute;
|
|
123
|
+
top: 24px;
|
|
124
|
+
left: 24px;
|
|
125
|
+
bottom: 24px;
|
|
126
|
+
width: 360px;
|
|
127
|
+
max-width: 90vw;
|
|
128
|
+
background: var(--panel-bg);
|
|
129
|
+
border: 1px solid var(--panel-border);
|
|
130
|
+
border-radius: 12px;
|
|
131
|
+
box-shadow: var(--shadow-lg);
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
z-index: 10;
|
|
135
|
+
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.editor-panel.collapsed {
|
|
139
|
+
transform: translateX(calc(-100% - 40px));
|
|
140
|
+
opacity: 0;
|
|
141
|
+
pointer-events: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.expand-trigger {
|
|
145
|
+
position: absolute;
|
|
146
|
+
top: 24px;
|
|
147
|
+
left: 24px;
|
|
148
|
+
z-index: 15;
|
|
149
|
+
background: var(--panel-bg);
|
|
150
|
+
border: 1px solid var(--panel-border);
|
|
151
|
+
box-shadow: var(--shadow-md);
|
|
152
|
+
border-radius: 8px;
|
|
153
|
+
height: 40px;
|
|
154
|
+
padding: 0 14px;
|
|
155
|
+
font-family: var(--font-sans);
|
|
156
|
+
font-size: 13px;
|
|
157
|
+
font-weight: 600;
|
|
158
|
+
color: var(--text-primary);
|
|
159
|
+
cursor: pointer;
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 8px;
|
|
163
|
+
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
|
164
|
+
opacity: 0;
|
|
165
|
+
transform: translateX(-20px) scale(0.95);
|
|
166
|
+
pointer-events: none;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.expand-trigger:hover {
|
|
170
|
+
background: #fff;
|
|
171
|
+
transform: translateX(0) scale(1.02);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.expand-trigger.visible {
|
|
175
|
+
opacity: 1;
|
|
176
|
+
transform: translateX(0) scale(1);
|
|
177
|
+
pointer-events: auto;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.panel-header {
|
|
181
|
+
padding: 14px 18px;
|
|
182
|
+
border-bottom: 1px solid var(--panel-border);
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
justify-content: space-between;
|
|
186
|
+
background: rgba(248, 250, 252, 0.5);
|
|
187
|
+
border-radius: 12px 12px 0 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.brand {
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: column;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.brand h1 {
|
|
196
|
+
font-size: 14px;
|
|
197
|
+
font-weight: 600;
|
|
198
|
+
color: var(--text-primary);
|
|
199
|
+
letter-spacing: -0.01em;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.brand span {
|
|
203
|
+
font-size: 11px;
|
|
204
|
+
color: var(--text-secondary);
|
|
205
|
+
margin-top: 2px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.header-actions {
|
|
209
|
+
display: flex;
|
|
210
|
+
gap: 4px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.icon-btn {
|
|
214
|
+
background: transparent;
|
|
215
|
+
border: 1px solid transparent;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
padding: 6px;
|
|
218
|
+
border-radius: 6px;
|
|
219
|
+
color: var(--text-secondary);
|
|
220
|
+
transition: all 0.2s;
|
|
221
|
+
display: flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
justify-content: center;
|
|
224
|
+
text-decoration: none;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.icon-btn:hover {
|
|
228
|
+
background: rgba(0, 0, 0, 0.05);
|
|
229
|
+
color: var(--text-primary);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.icon-btn svg {
|
|
233
|
+
width: 16px;
|
|
234
|
+
height: 16px;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.editor-content {
|
|
238
|
+
flex: 1;
|
|
239
|
+
position: relative;
|
|
240
|
+
overflow: hidden;
|
|
241
|
+
display: flex;
|
|
242
|
+
flex-direction: column;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
textarea {
|
|
246
|
+
flex: 1;
|
|
247
|
+
width: 100%;
|
|
248
|
+
border: none;
|
|
249
|
+
background: transparent;
|
|
250
|
+
padding: 16px;
|
|
251
|
+
font-family: var(--font-mono);
|
|
252
|
+
font-size: 12px;
|
|
253
|
+
line-height: 1.5;
|
|
254
|
+
color: var(--text-primary);
|
|
255
|
+
resize: none;
|
|
256
|
+
outline: none;
|
|
257
|
+
white-space: pre;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
textarea::-webkit-scrollbar {
|
|
261
|
+
width: 8px;
|
|
262
|
+
height: 8px;
|
|
263
|
+
}
|
|
264
|
+
textarea::-webkit-scrollbar-thumb {
|
|
265
|
+
background: rgba(0, 0, 0, 0.1);
|
|
266
|
+
border-radius: 4px;
|
|
267
|
+
}
|
|
268
|
+
textarea::-webkit-scrollbar-track {
|
|
269
|
+
background: transparent;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.error-overlay {
|
|
273
|
+
border-top: 1px solid #fee2e2;
|
|
274
|
+
background: #fef2f2;
|
|
275
|
+
color: #b91c1c;
|
|
276
|
+
padding: 12px 16px;
|
|
277
|
+
font-size: 12px;
|
|
278
|
+
font-family: var(--font-mono);
|
|
279
|
+
line-height: 1.4;
|
|
280
|
+
max-height: 120px;
|
|
281
|
+
overflow-y: auto;
|
|
282
|
+
display: none;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.error-overlay.visible {
|
|
286
|
+
display: block;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Floating Toolbar */
|
|
290
|
+
.toolbar {
|
|
291
|
+
position: absolute;
|
|
292
|
+
bottom: 32px;
|
|
293
|
+
left: 50%;
|
|
294
|
+
transform: translateX(-50%);
|
|
295
|
+
background: var(--panel-bg);
|
|
296
|
+
border: 1px solid var(--panel-border);
|
|
297
|
+
padding: 4px;
|
|
298
|
+
border-radius: 99px;
|
|
299
|
+
display: flex;
|
|
300
|
+
align-items: center;
|
|
301
|
+
gap: 2px;
|
|
302
|
+
box-shadow: var(--shadow-lg);
|
|
303
|
+
z-index: 20;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.tool-btn {
|
|
307
|
+
background: transparent;
|
|
308
|
+
border: none;
|
|
309
|
+
border-radius: 99px;
|
|
310
|
+
height: 32px;
|
|
311
|
+
min-width: 32px;
|
|
312
|
+
padding: 0 10px;
|
|
313
|
+
display: flex;
|
|
314
|
+
align-items: center;
|
|
315
|
+
justify-content: center;
|
|
316
|
+
gap: 6px;
|
|
317
|
+
font-size: 13px;
|
|
318
|
+
font-weight: 500;
|
|
319
|
+
color: var(--text-secondary);
|
|
320
|
+
cursor: pointer;
|
|
321
|
+
transition: all 0.2s;
|
|
322
|
+
font-family: var(--font-sans);
|
|
323
|
+
white-space: nowrap;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.tool-btn:hover {
|
|
327
|
+
background: rgba(0, 0, 0, 0.04);
|
|
328
|
+
color: var(--text-primary);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.tool-btn:active {
|
|
332
|
+
background: rgba(0, 0, 0, 0.08);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.divider {
|
|
336
|
+
width: 1px;
|
|
337
|
+
height: 16px;
|
|
338
|
+
background: var(--panel-border);
|
|
339
|
+
margin: 0 4px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
#zoom-label {
|
|
343
|
+
font-feature-settings: "tnum";
|
|
344
|
+
min-width: 4ch;
|
|
345
|
+
text-align: center;
|
|
346
|
+
font-size: 12px;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
@media (max-width: 768px) {
|
|
350
|
+
.editor-panel {
|
|
351
|
+
top: auto;
|
|
352
|
+
bottom: 0;
|
|
353
|
+
left: 0;
|
|
354
|
+
right: 0;
|
|
355
|
+
width: 100%;
|
|
356
|
+
height: 40vh;
|
|
357
|
+
max-width: none;
|
|
358
|
+
border-radius: 16px 16px 0 0;
|
|
359
|
+
border-left: none;
|
|
360
|
+
border-right: none;
|
|
361
|
+
border-bottom: none;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.editor-panel.collapsed {
|
|
365
|
+
transform: translateY(110%);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.expand-trigger {
|
|
369
|
+
top: auto;
|
|
370
|
+
bottom: 90px;
|
|
371
|
+
left: 50%;
|
|
372
|
+
transform: translateX(-50%) translateY(20px) scale(0.95);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.expand-trigger:hover {
|
|
376
|
+
transform: translateX(-50%) translateY(0) scale(1.02);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.expand-trigger.visible {
|
|
380
|
+
transform: translateX(-50%) translateY(0) scale(1);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.toolbar {
|
|
384
|
+
bottom: calc(40vh + 16px);
|
|
385
|
+
scale: 0.95;
|
|
386
|
+
transition: bottom 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.editor-panel.collapsed ~ .toolbar {
|
|
390
|
+
bottom: 32px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.tool-btn span {
|
|
394
|
+
display: none;
|
|
395
|
+
}
|
|
396
|
+
#zoom-label {
|
|
397
|
+
display: block;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
</style>
|
|
401
|
+
</head>
|
|
402
|
+
<body>
|
|
403
|
+
<div class="app">
|
|
404
|
+
<button id="btn-expand" class="expand-trigger visible" title="Show Editor">
|
|
405
|
+
<svg
|
|
406
|
+
width="18"
|
|
407
|
+
height="18"
|
|
408
|
+
viewBox="0 0 24 24"
|
|
409
|
+
fill="none"
|
|
410
|
+
stroke="currentColor"
|
|
411
|
+
stroke-width="2"
|
|
412
|
+
>
|
|
413
|
+
<path
|
|
414
|
+
d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"
|
|
415
|
+
></path>
|
|
416
|
+
<polyline points="17 21 17 13 7 13 7 21"></polyline>
|
|
417
|
+
<polyline points="7 3 7 8 15 8"></polyline>
|
|
418
|
+
</svg>
|
|
419
|
+
<span>Editor</span>
|
|
420
|
+
</button>
|
|
421
|
+
|
|
422
|
+
<!-- Canvas Area -->
|
|
423
|
+
<div class="preview-container" id="preview-container">
|
|
424
|
+
<div class="canvas-wrapper" id="canvas-wrapper">
|
|
425
|
+
<div id="canvas"></div>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<!-- Editor Panel -->
|
|
430
|
+
<div class="editor-panel collapsed">
|
|
431
|
+
<div class="panel-header">
|
|
432
|
+
<div class="brand">
|
|
433
|
+
<h1>Mermaid Viewer</h1>
|
|
434
|
+
<span>Live Editor</span>
|
|
435
|
+
</div>
|
|
436
|
+
<div class="header-actions">
|
|
437
|
+
<button id="btn-collapse" class="icon-btn" title="Hide Editor">
|
|
438
|
+
<svg
|
|
439
|
+
viewBox="0 0 24 24"
|
|
440
|
+
fill="none"
|
|
441
|
+
stroke="currentColor"
|
|
442
|
+
stroke-width="2"
|
|
443
|
+
>
|
|
444
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
445
|
+
<line x1="9" y1="3" x2="9" y2="21"></line>
|
|
446
|
+
<polyline points="16 15 13 12 16 9"></polyline>
|
|
447
|
+
</svg>
|
|
448
|
+
</button>
|
|
449
|
+
<div class="divider" style="height: 12px; margin: 0 4px"></div>
|
|
450
|
+
<a
|
|
451
|
+
href="__KLAUDE_VIEW_LINK__"
|
|
452
|
+
target="_blank"
|
|
453
|
+
class="icon-btn"
|
|
454
|
+
title="Open in Cloud Viewer"
|
|
455
|
+
>
|
|
456
|
+
<svg
|
|
457
|
+
viewBox="0 0 24 24"
|
|
458
|
+
fill="none"
|
|
459
|
+
stroke="currentColor"
|
|
460
|
+
stroke-width="2"
|
|
461
|
+
>
|
|
462
|
+
<path
|
|
463
|
+
d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"
|
|
464
|
+
></path>
|
|
465
|
+
<polyline points="15 3 21 3 21 9"></polyline>
|
|
466
|
+
<line x1="10" y1="14" x2="21" y2="3"></line>
|
|
467
|
+
</svg>
|
|
468
|
+
</a>
|
|
469
|
+
<button id="btn-copy-code" class="icon-btn" title="Copy Code">
|
|
470
|
+
<svg
|
|
471
|
+
viewBox="0 0 24 24"
|
|
472
|
+
fill="none"
|
|
473
|
+
stroke="currentColor"
|
|
474
|
+
stroke-width="2"
|
|
475
|
+
>
|
|
476
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
477
|
+
<path
|
|
478
|
+
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
|
|
479
|
+
></path>
|
|
480
|
+
</svg>
|
|
481
|
+
</button>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
<div class="editor-content">
|
|
485
|
+
<textarea
|
|
486
|
+
id="source-code"
|
|
487
|
+
spellcheck="false"
|
|
488
|
+
placeholder="Enter mermaid diagram code..."
|
|
489
|
+
>
|
|
490
|
+
__KLAUDE_CODE__</textarea
|
|
491
|
+
>
|
|
492
|
+
<div id="error-overlay" class="error-overlay"></div>
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<!-- Controls -->
|
|
497
|
+
<div class="toolbar">
|
|
498
|
+
<button class="tool-btn" id="btn-zoom-out" title="Zoom Out">
|
|
499
|
+
<svg
|
|
500
|
+
width="16"
|
|
501
|
+
height="16"
|
|
502
|
+
viewBox="0 0 24 24"
|
|
503
|
+
fill="none"
|
|
504
|
+
stroke="currentColor"
|
|
505
|
+
stroke-width="2"
|
|
506
|
+
>
|
|
507
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
508
|
+
</svg>
|
|
509
|
+
</button>
|
|
510
|
+
<button class="tool-btn" id="btn-reset" title="Fit to View">
|
|
511
|
+
<span id="zoom-label">100%</span>
|
|
512
|
+
</button>
|
|
513
|
+
<button class="tool-btn" id="btn-zoom-in" title="Zoom In">
|
|
514
|
+
<svg
|
|
515
|
+
width="16"
|
|
516
|
+
height="16"
|
|
517
|
+
viewBox="0 0 24 24"
|
|
518
|
+
fill="none"
|
|
519
|
+
stroke="currentColor"
|
|
520
|
+
stroke-width="2"
|
|
521
|
+
>
|
|
522
|
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
523
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
524
|
+
</svg>
|
|
525
|
+
</button>
|
|
526
|
+
<div class="divider"></div>
|
|
527
|
+
<button class="tool-btn" id="btn-download" title="Download SVG">
|
|
528
|
+
<svg
|
|
529
|
+
width="16"
|
|
530
|
+
height="16"
|
|
531
|
+
viewBox="0 0 24 24"
|
|
532
|
+
fill="none"
|
|
533
|
+
stroke="currentColor"
|
|
534
|
+
stroke-width="2"
|
|
535
|
+
>
|
|
536
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
537
|
+
<polyline points="7 10 12 15 17 10"></polyline>
|
|
538
|
+
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
539
|
+
</svg>
|
|
540
|
+
<span>SVG</span>
|
|
541
|
+
</button>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
<script type="module">
|
|
546
|
+
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
|
|
547
|
+
|
|
548
|
+
// --- State ---
|
|
549
|
+
const state = {
|
|
550
|
+
scale: 1,
|
|
551
|
+
x: 0,
|
|
552
|
+
y: 0,
|
|
553
|
+
isDragging: false,
|
|
554
|
+
startX: 0,
|
|
555
|
+
startY: 0,
|
|
556
|
+
initialX: 0,
|
|
557
|
+
initialY: 0,
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// --- Elements ---
|
|
561
|
+
const els = {
|
|
562
|
+
preview: document.getElementById("preview-container"),
|
|
563
|
+
wrapper: document.getElementById("canvas-wrapper"),
|
|
564
|
+
canvas: document.getElementById("canvas"),
|
|
565
|
+
textarea: document.getElementById("source-code"),
|
|
566
|
+
error: document.getElementById("error-overlay"),
|
|
567
|
+
zoomLabel: document.getElementById("zoom-label"),
|
|
568
|
+
btns: {
|
|
569
|
+
zoomIn: document.getElementById("btn-zoom-in"),
|
|
570
|
+
zoomOut: document.getElementById("btn-zoom-out"),
|
|
571
|
+
reset: document.getElementById("btn-reset"),
|
|
572
|
+
download: document.getElementById("btn-download"),
|
|
573
|
+
copy: document.getElementById("btn-copy-code"),
|
|
574
|
+
collapse: document.getElementById("btn-collapse"),
|
|
575
|
+
expand: document.getElementById("btn-expand"),
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// --- Config ---
|
|
580
|
+
const sansFont = getComputedStyle(
|
|
581
|
+
document.documentElement
|
|
582
|
+
).getPropertyValue("--font-sans");
|
|
583
|
+
|
|
584
|
+
mermaid.initialize({
|
|
585
|
+
startOnLoad: false,
|
|
586
|
+
theme: "neutral",
|
|
587
|
+
themeVariables: {
|
|
588
|
+
fontFamily: sansFont,
|
|
589
|
+
fontSize: "14px",
|
|
590
|
+
lineColor: "#5c6c7f", // Match slate-500 roughly for better contrast
|
|
591
|
+
},
|
|
592
|
+
flowchart: {
|
|
593
|
+
useMaxWidth: false,
|
|
594
|
+
htmlLabels: true,
|
|
595
|
+
curve: "basis",
|
|
596
|
+
nodeSpacing: 30, // Tighter horizontal
|
|
597
|
+
rankSpacing: 40,
|
|
598
|
+
ranker: "tight-tree", // Tighter vertical layout algorithm
|
|
599
|
+
diagramPadding: 10,
|
|
600
|
+
wrappingWidth: 250, // Allow slightly wider text to reduce height
|
|
601
|
+
},
|
|
602
|
+
sequence: { useMaxWidth: false },
|
|
603
|
+
gantt: { useMaxWidth: false },
|
|
604
|
+
journey: { useMaxWidth: false },
|
|
605
|
+
class: { useMaxWidth: false },
|
|
606
|
+
state: { useMaxWidth: false },
|
|
607
|
+
er: { useMaxWidth: false },
|
|
608
|
+
pie: { useMaxWidth: false },
|
|
609
|
+
securityLevel: "loose",
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// --- Render ---
|
|
613
|
+
let renderedOnce = false;
|
|
614
|
+
|
|
615
|
+
const render = async () => {
|
|
616
|
+
const code = els.textarea.value.trim();
|
|
617
|
+
if (!code) {
|
|
618
|
+
els.canvas.innerHTML = "";
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
const id = "mermaid-" + Date.now();
|
|
624
|
+
// Generate SVG
|
|
625
|
+
const { svg } = await mermaid.render(id, code);
|
|
626
|
+
els.canvas.innerHTML = svg;
|
|
627
|
+
|
|
628
|
+
els.error.textContent = "";
|
|
629
|
+
els.error.classList.remove("visible");
|
|
630
|
+
|
|
631
|
+
if (!renderedOnce) {
|
|
632
|
+
renderedOnce = true;
|
|
633
|
+
// Short delay to ensure layout is done
|
|
634
|
+
setTimeout(fitToViewInitial, 50);
|
|
635
|
+
}
|
|
636
|
+
} catch (e) {
|
|
637
|
+
console.error(e);
|
|
638
|
+
const msg = e.message || String(e);
|
|
639
|
+
// Show simplified error
|
|
640
|
+
const cleanMsg = msg.split("\n")[0];
|
|
641
|
+
els.error.textContent = cleanMsg;
|
|
642
|
+
els.error.classList.add("visible");
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const debounce = (fn, ms) => {
|
|
647
|
+
let t;
|
|
648
|
+
return (...args) => {
|
|
649
|
+
clearTimeout(t);
|
|
650
|
+
t = setTimeout(() => fn(...args), ms);
|
|
651
|
+
};
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
els.textarea.addEventListener("input", debounce(render, 400));
|
|
655
|
+
els.textarea.addEventListener("keydown", (e) => {
|
|
656
|
+
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
|
657
|
+
e.preventDefault();
|
|
658
|
+
render();
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
// --- Zoom / Pan ---
|
|
663
|
+
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
|
|
664
|
+
|
|
665
|
+
const updateTransform = () => {
|
|
666
|
+
els.wrapper.style.transform = `translate(${state.x}px, ${state.y}px) scale(${state.scale})`;
|
|
667
|
+
els.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// Attempt to fit content to view, but don't shrink too much initially
|
|
671
|
+
const fitToViewInitial = () => {
|
|
672
|
+
fitToView(true);
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const fitToView = (isInitial = false) => {
|
|
676
|
+
const svg = els.canvas.querySelector("svg");
|
|
677
|
+
if (!svg) return;
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
const bbox = svg.getBBox();
|
|
681
|
+
if (bbox.width < 1 || bbox.height < 1) return;
|
|
682
|
+
|
|
683
|
+
const padding = 60;
|
|
684
|
+
const containerW = els.preview.clientWidth;
|
|
685
|
+
const containerH = els.preview.clientHeight;
|
|
686
|
+
|
|
687
|
+
// Calculate scale to fit
|
|
688
|
+
const scaleW = (containerW - padding * 2) / bbox.width;
|
|
689
|
+
const scaleH = (containerH - padding * 2) / bbox.height;
|
|
690
|
+
const fitScale = Math.min(scaleW, scaleH);
|
|
691
|
+
|
|
692
|
+
// Logic:
|
|
693
|
+
// If user clicks "Fit" (isInitial=false), force fit (clamp 0.1 - 2)
|
|
694
|
+
// If initial load (isInitial=true):
|
|
695
|
+
// - If diagram is small, fit it (up to 1.5x)
|
|
696
|
+
// - If diagram is huge (fitScale very small), don't shrink below 0.75x so text stays readable.
|
|
697
|
+
|
|
698
|
+
let finalScale = fitScale;
|
|
699
|
+
|
|
700
|
+
if (isInitial) {
|
|
701
|
+
// Prevent starting too small, unless the graph is absolutely massive in a way that navigation is impossible?
|
|
702
|
+
// Usually 0.8 is a good compromise for "Overview" vs "Readability"
|
|
703
|
+
finalScale = Math.max(fitScale, 0.85);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
state.scale = clamp(finalScale, 0.1, 3);
|
|
707
|
+
|
|
708
|
+
// Center
|
|
709
|
+
// We moved the svg to center using flexbox in wrapper
|
|
710
|
+
// But we translate the wrapper.
|
|
711
|
+
// If wrapper is centered, x=0 y=0 is center.
|
|
712
|
+
state.x = 0;
|
|
713
|
+
state.y = 0;
|
|
714
|
+
|
|
715
|
+
updateTransform();
|
|
716
|
+
} catch (e) {
|
|
717
|
+
console.error("Fit failed", e);
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const zoomAt = (factor, cx, cy) => {
|
|
722
|
+
const oldScale = state.scale;
|
|
723
|
+
const newScale = clamp(oldScale * factor, 0.1, 5);
|
|
724
|
+
|
|
725
|
+
// Adjust position to keep client point relatively stable
|
|
726
|
+
// Formula: NewPos = MousePos - (MousePos - OldPos) * (NewScale/OldScale)
|
|
727
|
+
// Note: state.x/y are translations from center? No, they are translate() values.
|
|
728
|
+
// And origin is 0,0 (top left of wrapper). Wrapper is size of screen.
|
|
729
|
+
// cx, cy are client coordinates.
|
|
730
|
+
// We need wrapper-relative coords.
|
|
731
|
+
|
|
732
|
+
const rect = els.wrapper.getBoundingClientRect();
|
|
733
|
+
// wrapper's top-left in client space:
|
|
734
|
+
const wx = rect.left;
|
|
735
|
+
const wy = rect.top;
|
|
736
|
+
|
|
737
|
+
// The point on the scaled content under the mouse:
|
|
738
|
+
// (cx - wx) is distance from current top-left of transformed element
|
|
739
|
+
// But (cx - wx) includes the current scale.
|
|
740
|
+
|
|
741
|
+
// This math is tricky. Simplified approach:
|
|
742
|
+
// x -= (cx - wrapperLeft) * (factor - 1) ... ?
|
|
743
|
+
// Let's use the delta approach which is robust.
|
|
744
|
+
|
|
745
|
+
// Relative mouse pos from wrapper center (since we pan the wrapper):
|
|
746
|
+
// Actually, let's treat it simply:
|
|
747
|
+
|
|
748
|
+
// P_world = (P_screen - Translate) / Scale
|
|
749
|
+
// We want P_world to stay at P_screen after scale change.
|
|
750
|
+
// P_screen = P_world * NewScale + NewTranslate
|
|
751
|
+
// NewTranslate = P_screen - P_world * NewScale
|
|
752
|
+
// = P_screen - ((P_screen - Translate)/Scale) * NewScale
|
|
753
|
+
|
|
754
|
+
// Wait, element is w=100% h=100%, top=0 left=0.
|
|
755
|
+
// Translate is applied to it.
|
|
756
|
+
// Origin is top-left (default).
|
|
757
|
+
// So:
|
|
758
|
+
|
|
759
|
+
state.x = cx - ((cx - state.x) / oldScale) * newScale;
|
|
760
|
+
state.y = cy - ((cy - state.y) / oldScale) * newScale;
|
|
761
|
+
state.scale = newScale;
|
|
762
|
+
|
|
763
|
+
updateTransform();
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// --- event listeners ---
|
|
767
|
+
|
|
768
|
+
els.preview.addEventListener(
|
|
769
|
+
"wheel",
|
|
770
|
+
(e) => {
|
|
771
|
+
e.preventDefault();
|
|
772
|
+
// User requested mouse wheel to zoom (Maps style)
|
|
773
|
+
// Drag is used for panning
|
|
774
|
+
const factor = Math.exp(-e.deltaY * 0.002);
|
|
775
|
+
zoomAt(factor, e.clientX, e.clientY);
|
|
776
|
+
},
|
|
777
|
+
{ passive: false }
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
// Pointer events for panning
|
|
781
|
+
els.preview.addEventListener("pointerdown", (e) => {
|
|
782
|
+
if (e.pointerType === "touch" || e.button !== 0) return;
|
|
783
|
+
state.isDragging = true;
|
|
784
|
+
state.startX = e.clientX;
|
|
785
|
+
state.startY = e.clientY;
|
|
786
|
+
state.initialX = state.x;
|
|
787
|
+
state.initialY = state.y;
|
|
788
|
+
els.preview.classList.add("grabbing");
|
|
789
|
+
els.preview.setPointerCapture(e.pointerId);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
els.preview.addEventListener("pointermove", (e) => {
|
|
793
|
+
if (!state.isDragging) return;
|
|
794
|
+
const dx = e.clientX - state.startX;
|
|
795
|
+
const dy = e.clientY - state.startY;
|
|
796
|
+
state.x = state.initialX + dx;
|
|
797
|
+
state.y = state.initialY + dy;
|
|
798
|
+
updateTransform();
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
els.preview.addEventListener("pointerup", () => {
|
|
802
|
+
state.isDragging = false;
|
|
803
|
+
els.preview.classList.remove("grabbing");
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// Touch handling
|
|
807
|
+
let lastPinchDist = -1;
|
|
808
|
+
|
|
809
|
+
els.preview.addEventListener(
|
|
810
|
+
"touchstart",
|
|
811
|
+
(e) => {
|
|
812
|
+
if (e.touches.length === 2) {
|
|
813
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
814
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
815
|
+
lastPinchDist = Math.hypot(dx, dy);
|
|
816
|
+
} else if (e.touches.length === 1) {
|
|
817
|
+
state.isDragging = true;
|
|
818
|
+
state.startX = e.touches[0].clientX;
|
|
819
|
+
state.startY = e.touches[0].clientY;
|
|
820
|
+
state.initialX = state.x;
|
|
821
|
+
state.initialY = state.y;
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
{ passive: false }
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
els.preview.addEventListener(
|
|
828
|
+
"touchmove",
|
|
829
|
+
(e) => {
|
|
830
|
+
e.preventDefault();
|
|
831
|
+
if (e.touches.length === 2 && lastPinchDist > 0) {
|
|
832
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
833
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
834
|
+
const dist = Math.hypot(dx, dy);
|
|
835
|
+
const cx = (e.touches[0].clientX + e.touches[1].clientX) / 2;
|
|
836
|
+
const cy = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
|
837
|
+
|
|
838
|
+
const factor = dist / lastPinchDist; // relative change
|
|
839
|
+
// We need to apply this factor to the current scale
|
|
840
|
+
// But zoomAt takes a multiplier for state.scale.
|
|
841
|
+
// So factor is correct.
|
|
842
|
+
|
|
843
|
+
const oldScale = state.scale;
|
|
844
|
+
const newScale = clamp(oldScale * factor, 0.1, 5);
|
|
845
|
+
|
|
846
|
+
// Same math as zoomAt
|
|
847
|
+
state.x = cx - ((cx - state.x) / oldScale) * newScale;
|
|
848
|
+
state.y = cy - ((cy - state.y) / oldScale) * newScale;
|
|
849
|
+
state.scale = newScale;
|
|
850
|
+
|
|
851
|
+
updateTransform();
|
|
852
|
+
lastPinchDist = dist;
|
|
853
|
+
} else if (e.touches.length === 1 && state.isDragging) {
|
|
854
|
+
const dx = e.touches[0].clientX - state.startX;
|
|
855
|
+
const dy = e.touches[0].clientY - state.startY;
|
|
856
|
+
state.x = state.initialX + dx;
|
|
857
|
+
state.y = state.initialY + dy;
|
|
858
|
+
updateTransform();
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
{ passive: false }
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
els.preview.addEventListener("touchend", (e) => {
|
|
865
|
+
if (e.touches.length < 2) lastPinchDist = -1;
|
|
866
|
+
if (e.touches.length === 0) state.isDragging = false;
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
// Buttons
|
|
870
|
+
els.btns.zoomIn.onclick = () =>
|
|
871
|
+
zoomAt(1.2, els.preview.clientWidth / 2, els.preview.clientHeight / 2);
|
|
872
|
+
els.btns.zoomOut.onclick = () =>
|
|
873
|
+
zoomAt(
|
|
874
|
+
1 / 1.2,
|
|
875
|
+
els.preview.clientWidth / 2,
|
|
876
|
+
els.preview.clientHeight / 2
|
|
877
|
+
);
|
|
878
|
+
els.btns.reset.onclick = () => fitToView(false); // Force fit
|
|
879
|
+
|
|
880
|
+
els.btns.download.onclick = () => {
|
|
881
|
+
const svg = els.canvas.querySelector("svg");
|
|
882
|
+
if (!svg) return;
|
|
883
|
+
// Add white background for download
|
|
884
|
+
const clone = svg.cloneNode(true);
|
|
885
|
+
clone.style.backgroundColor = "white";
|
|
886
|
+
const blob = new Blob([clone.outerHTML], { type: "image/svg+xml" });
|
|
887
|
+
const url = URL.createObjectURL(blob);
|
|
888
|
+
const a = document.createElement("a");
|
|
889
|
+
a.href = url;
|
|
890
|
+
a.download = "diagram.svg";
|
|
891
|
+
document.body.appendChild(a);
|
|
892
|
+
a.click();
|
|
893
|
+
a.remove();
|
|
894
|
+
URL.revokeObjectURL(url);
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
els.btns.copy.onclick = async () => {
|
|
898
|
+
try {
|
|
899
|
+
await navigator.clipboard.writeText(els.textarea.value);
|
|
900
|
+
els.btns.copy.style.color = "var(--accent)";
|
|
901
|
+
setTimeout(() => (els.btns.copy.style.color = ""), 1000);
|
|
902
|
+
} catch {}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
const toggleEditor = (show) => {
|
|
906
|
+
const panel = document.querySelector(".editor-panel");
|
|
907
|
+
const trigger = document.getElementById("btn-expand");
|
|
908
|
+
|
|
909
|
+
if (!show) {
|
|
910
|
+
panel.classList.add("collapsed");
|
|
911
|
+
trigger.classList.add("visible");
|
|
912
|
+
} else {
|
|
913
|
+
panel.classList.remove("collapsed");
|
|
914
|
+
trigger.classList.remove("visible");
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
if (els.btns.collapse)
|
|
919
|
+
els.btns.collapse.onclick = () => toggleEditor(false);
|
|
920
|
+
if (els.btns.expand) els.btns.expand.onclick = () => toggleEditor(true);
|
|
921
|
+
|
|
922
|
+
// Start
|
|
923
|
+
requestAnimationFrame(render);
|
|
924
|
+
</script>
|
|
925
|
+
</body>
|
|
926
|
+
</html>
|