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.
Files changed (39) hide show
  1. klaude_code/cli/config_cmd.py +13 -6
  2. klaude_code/cli/debug.py +9 -1
  3. klaude_code/cli/list_model.py +1 -1
  4. klaude_code/cli/main.py +39 -14
  5. klaude_code/cli/runtime.py +11 -5
  6. klaude_code/command/__init__.py +3 -0
  7. klaude_code/command/export_online_cmd.py +15 -12
  8. klaude_code/command/fork_session_cmd.py +42 -0
  9. klaude_code/config/__init__.py +11 -1
  10. klaude_code/config/config.py +21 -17
  11. klaude_code/config/select_model.py +1 -0
  12. klaude_code/core/executor.py +2 -1
  13. klaude_code/core/reminders.py +52 -16
  14. klaude_code/core/tool/web/mermaid_tool.md +17 -0
  15. klaude_code/core/tool/web/mermaid_tool.py +2 -2
  16. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  17. klaude_code/protocol/commands.py +1 -0
  18. klaude_code/protocol/model.py +2 -0
  19. klaude_code/session/export.py +61 -17
  20. klaude_code/session/session.py +23 -1
  21. klaude_code/session/templates/mermaid_viewer.html +926 -0
  22. klaude_code/trace/log.py +7 -1
  23. klaude_code/ui/modes/repl/__init__.py +3 -44
  24. klaude_code/ui/modes/repl/completers.py +35 -3
  25. klaude_code/ui/modes/repl/event_handler.py +9 -5
  26. klaude_code/ui/modes/repl/input_prompt_toolkit.py +32 -65
  27. klaude_code/ui/modes/repl/renderer.py +1 -6
  28. klaude_code/ui/renderers/assistant.py +4 -2
  29. klaude_code/ui/renderers/common.py +11 -4
  30. klaude_code/ui/renderers/developer.py +26 -7
  31. klaude_code/ui/renderers/errors.py +10 -5
  32. klaude_code/ui/renderers/mermaid_viewer.py +58 -0
  33. klaude_code/ui/renderers/tools.py +46 -18
  34. klaude_code/ui/rich/markdown.py +4 -4
  35. klaude_code/ui/rich/theme.py +12 -2
  36. {klaude_code-1.2.27.dist-info → klaude_code-1.2.29.dist-info}/METADATA +1 -1
  37. {klaude_code-1.2.27.dist-info → klaude_code-1.2.29.dist-info}/RECORD +39 -36
  38. {klaude_code-1.2.27.dist-info → klaude_code-1.2.29.dist-info}/entry_points.txt +1 -0
  39. {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>