claude-code-monitor 1.0.3 → 1.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +83 -104
  3. package/dist/bin/ccm.js +51 -18
  4. package/dist/components/Dashboard.d.ts +6 -1
  5. package/dist/components/Dashboard.d.ts.map +1 -1
  6. package/dist/components/Dashboard.js +39 -5
  7. package/dist/components/SessionCard.d.ts.map +1 -1
  8. package/dist/components/SessionCard.js +2 -4
  9. package/dist/components/Spinner.js +1 -1
  10. package/dist/constants.d.ts +5 -2
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +5 -2
  13. package/dist/hook/handler.d.ts.map +1 -1
  14. package/dist/hook/handler.js +16 -15
  15. package/dist/hooks/useServer.d.ts +10 -0
  16. package/dist/hooks/useServer.d.ts.map +1 -0
  17. package/dist/hooks/useServer.js +39 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/server/index.d.ts +12 -0
  22. package/dist/server/index.d.ts.map +1 -0
  23. package/dist/server/index.js +309 -0
  24. package/dist/store/file-store.d.ts +5 -0
  25. package/dist/store/file-store.d.ts.map +1 -1
  26. package/dist/store/file-store.js +40 -7
  27. package/dist/types/index.d.ts +2 -0
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/dist/utils/applescript.d.ts +7 -0
  30. package/dist/utils/applescript.d.ts.map +1 -0
  31. package/dist/utils/applescript.js +18 -0
  32. package/dist/utils/focus.d.ts +0 -1
  33. package/dist/utils/focus.d.ts.map +1 -1
  34. package/dist/utils/focus.js +16 -22
  35. package/dist/utils/send-text.d.ts +40 -0
  36. package/dist/utils/send-text.d.ts.map +1 -0
  37. package/dist/utils/send-text.js +324 -0
  38. package/dist/utils/status.js +2 -2
  39. package/dist/utils/stdin.d.ts +6 -0
  40. package/dist/utils/stdin.d.ts.map +1 -0
  41. package/dist/utils/stdin.js +12 -0
  42. package/dist/utils/terminal-strategy.d.ts +18 -0
  43. package/dist/utils/terminal-strategy.d.ts.map +1 -0
  44. package/dist/utils/terminal-strategy.js +15 -0
  45. package/dist/utils/time.d.ts.map +1 -1
  46. package/dist/utils/time.js +1 -3
  47. package/dist/utils/transcript.d.ts +10 -0
  48. package/dist/utils/transcript.d.ts.map +1 -0
  49. package/dist/utils/transcript.js +45 -0
  50. package/dist/utils/tty-cache.d.ts +0 -5
  51. package/dist/utils/tty-cache.d.ts.map +1 -1
  52. package/dist/utils/tty-cache.js +0 -7
  53. package/package.json +6 -2
  54. package/public/index.html +1219 -0
@@ -0,0 +1,1219 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
+ <meta name="apple-mobile-web-app-capable" content="yes">
7
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
8
+ <title>Claude Code Monitor</title>
9
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
11
+ <style>
12
+ * {
13
+ box-sizing: border-box;
14
+ margin: 0;
15
+ padding: 0;
16
+ }
17
+
18
+ :root {
19
+ --bg-primary: #0a0a0a;
20
+ --bg-card: #141414;
21
+ --bg-card-hover: #1a1a1a;
22
+ --bg-input: #1e1e1e;
23
+ --border-subtle: #2a2a2a;
24
+ --border-default: #3a3a3a;
25
+ --text-primary: #f5f5f5;
26
+ --text-secondary: #a8a8a8;
27
+ --text-muted: #666;
28
+ --status-running: #22c55e;
29
+ --status-running-bg: rgba(34, 197, 94, 0.15);
30
+ --status-running-border: rgba(34, 197, 94, 0.4);
31
+ --status-waiting: #f59e0b;
32
+ --status-waiting-bg: rgba(245, 158, 11, 0.15);
33
+ --status-waiting-border: rgba(245, 158, 11, 0.4);
34
+ --status-stopped: #71717a;
35
+ --status-stopped-bg: rgba(113, 113, 122, 0.1);
36
+ --status-stopped-border: rgba(113, 113, 122, 0.3);
37
+ --accent: #6366f1;
38
+ --accent-hover: #818cf8;
39
+ --accent-glow: rgba(99, 102, 241, 0.3);
40
+ --danger: #ef4444;
41
+ --radius-sm: 10px;
42
+ --radius-md: 14px;
43
+ --radius-lg: 20px;
44
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
45
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
46
+ --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.5);
47
+ }
48
+
49
+ /* Light mode */
50
+ :root.light {
51
+ --bg-primary: #f5f5f7;
52
+ --bg-card: #ffffff;
53
+ --bg-card-hover: #f0f0f2;
54
+ --bg-input: #f0f0f2;
55
+ --border-subtle: #c7c7cc;
56
+ --border-default: #aeaeb2;
57
+ --text-primary: #1c1c1e;
58
+ --text-secondary: #48484a;
59
+ --text-muted: #8e8e93;
60
+ --status-running: #28a745;
61
+ --status-running-bg: rgba(40, 167, 69, 0.12);
62
+ --status-running-border: rgba(40, 167, 69, 0.6);
63
+ --status-waiting: #e67700;
64
+ --status-waiting-bg: rgba(230, 119, 0, 0.12);
65
+ --status-waiting-border: rgba(230, 119, 0, 0.6);
66
+ --status-stopped: #6c757d;
67
+ --status-stopped-bg: rgba(108, 117, 125, 0.08);
68
+ --status-stopped-border: rgba(108, 117, 125, 0.5);
69
+ --accent: #5856d6;
70
+ --accent-hover: #4f46e5;
71
+ --accent-glow: rgba(88, 86, 214, 0.3);
72
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
73
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.12);
74
+ --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.18);
75
+ }
76
+
77
+ body {
78
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
79
+ background: var(--bg-primary);
80
+ color: var(--text-primary);
81
+ min-height: 100vh;
82
+ min-height: 100dvh;
83
+ padding: env(safe-area-inset-top, 0) 0 env(safe-area-inset-bottom, 0) 0;
84
+ -webkit-font-smoothing: antialiased;
85
+ line-height: 1.5;
86
+ }
87
+
88
+ .app {
89
+ max-width: 480px;
90
+ margin: 0 auto;
91
+ padding: 16px;
92
+ }
93
+
94
+ /* Header */
95
+ .header {
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: space-between;
99
+ margin-bottom: 24px;
100
+ padding-bottom: 16px;
101
+ border-bottom: 1px solid var(--border-subtle);
102
+ }
103
+
104
+ .header-left {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 12px;
108
+ }
109
+
110
+ .header-title {
111
+ font-size: 18px;
112
+ font-weight: 700;
113
+ letter-spacing: -0.4px;
114
+ }
115
+
116
+ .header-actions {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 10px;
120
+ }
121
+
122
+ .icon-btn {
123
+ width: 44px;
124
+ height: 44px;
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ background: var(--bg-card);
129
+ border: 1px solid var(--border-default);
130
+ border-radius: 50%;
131
+ font-size: 18px;
132
+ color: var(--text-secondary);
133
+ cursor: pointer;
134
+ -webkit-tap-highlight-color: transparent;
135
+ transition: all 0.15s ease;
136
+ }
137
+
138
+ .icon-btn:active {
139
+ background: var(--bg-card-hover);
140
+ transform: scale(0.92);
141
+ }
142
+
143
+ .icon-btn svg {
144
+ width: 20px;
145
+ height: 20px;
146
+ fill: currentColor;
147
+ }
148
+
149
+ .conn-indicator {
150
+ width: 10px;
151
+ height: 10px;
152
+ border-radius: 50%;
153
+ background: var(--danger);
154
+ box-shadow: 0 0 0 2px var(--bg-primary), 0 0 0 4px rgba(239, 68, 68, 0.3);
155
+ transition: all 0.3s ease;
156
+ flex-shrink: 0;
157
+ }
158
+
159
+ .conn-indicator.online {
160
+ background: var(--status-running);
161
+ box-shadow: 0 0 0 2px var(--bg-primary), 0 0 0 4px var(--status-running-bg), 0 0 12px var(--status-running);
162
+ animation: pulse 2s ease-in-out infinite;
163
+ }
164
+
165
+ @keyframes pulse {
166
+ 0%, 100% { opacity: 1; transform: scale(1); }
167
+ 50% { opacity: 0.6; transform: scale(0.9); }
168
+ }
169
+
170
+ /* Session List */
171
+ .sessions {
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 16px;
175
+ }
176
+
177
+ /* Session Card - Tappable */
178
+ .card {
179
+ background: var(--bg-card);
180
+ border-radius: var(--radius-md);
181
+ border: 2px solid var(--border-default);
182
+ overflow: hidden;
183
+ box-shadow: var(--shadow-md);
184
+ cursor: pointer;
185
+ -webkit-tap-highlight-color: transparent;
186
+ transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
187
+ }
188
+
189
+ .card:active {
190
+ transform: scale(0.98);
191
+ }
192
+
193
+ .card.running {
194
+ border-color: var(--status-running);
195
+ box-shadow: var(--shadow-md), 0 0 0 1px var(--status-running-border), 0 0 24px var(--status-running-bg);
196
+ }
197
+
198
+ .card.waiting_input {
199
+ border-color: var(--status-waiting);
200
+ box-shadow: var(--shadow-md), 0 0 0 1px var(--status-waiting-border), 0 0 24px var(--status-waiting-bg);
201
+ }
202
+
203
+ .card.stopped {
204
+ border-color: var(--status-stopped-border);
205
+ opacity: 0.75;
206
+ }
207
+
208
+ /* Status Header */
209
+ .card-status {
210
+ display: flex;
211
+ align-items: center;
212
+ gap: 10px;
213
+ padding: 14px 16px;
214
+ font-size: 12px;
215
+ font-weight: 700;
216
+ text-transform: uppercase;
217
+ letter-spacing: 0.8px;
218
+ }
219
+
220
+ .card-status-chevron {
221
+ margin-left: auto;
222
+ font-size: 18px;
223
+ opacity: 0.7;
224
+ }
225
+
226
+ .card.running .card-status {
227
+ background: var(--status-running-bg);
228
+ color: var(--status-running);
229
+ }
230
+
231
+ .card.waiting_input .card-status {
232
+ background: var(--status-waiting-bg);
233
+ color: var(--status-waiting);
234
+ }
235
+
236
+ .card.stopped .card-status {
237
+ background: var(--status-stopped-bg);
238
+ color: var(--status-stopped);
239
+ }
240
+
241
+ .card-status-dot {
242
+ width: 10px;
243
+ height: 10px;
244
+ border-radius: 50%;
245
+ background: currentColor;
246
+ }
247
+
248
+ .card.running .card-status-dot {
249
+ animation: pulse 1.5s ease-in-out infinite;
250
+ }
251
+
252
+ .card.waiting_input .card-status-dot {
253
+ animation: pulse 1s ease-in-out infinite;
254
+ }
255
+
256
+ /* Card Body */
257
+ .card-body {
258
+ padding: 16px;
259
+ }
260
+
261
+ .card-name {
262
+ font-size: 20px;
263
+ font-weight: 700;
264
+ color: var(--text-primary);
265
+ margin-bottom: 4px;
266
+ letter-spacing: -0.3px;
267
+ }
268
+
269
+ .card-path {
270
+ font-size: 13px;
271
+ color: var(--text-muted);
272
+ margin-bottom: 14px;
273
+ word-break: break-all;
274
+ }
275
+
276
+ /* Message Preview */
277
+ .card-message {
278
+ background: var(--bg-input);
279
+ border: 1px solid var(--border-default);
280
+ border-radius: var(--radius-sm);
281
+ padding: 12px 14px;
282
+ }
283
+
284
+ .card-message-text {
285
+ font-size: 14px;
286
+ color: var(--text-primary);
287
+ line-height: 1.5;
288
+ display: -webkit-box;
289
+ -webkit-line-clamp: 2;
290
+ -webkit-box-orient: vertical;
291
+ overflow: hidden;
292
+ }
293
+
294
+ .card-message-empty {
295
+ font-size: 13px;
296
+ color: var(--text-muted);
297
+ font-style: italic;
298
+ }
299
+
300
+ /* Modal Waiting Notice */
301
+ .modal-waiting-notice {
302
+ flex-shrink: 0;
303
+ padding: 16px 20px;
304
+ border-top: 1px solid var(--border-subtle);
305
+ background: var(--status-waiting-bg);
306
+ text-align: center;
307
+ }
308
+
309
+ .modal-waiting-notice-text {
310
+ font-size: 14px;
311
+ font-weight: 600;
312
+ color: var(--status-waiting);
313
+ }
314
+
315
+ /* Empty State */
316
+ .empty {
317
+ text-align: center;
318
+ padding: 60px 24px;
319
+ background: var(--bg-card);
320
+ border: 2px solid var(--border-default);
321
+ border-radius: var(--radius-md);
322
+ box-shadow: var(--shadow-sm);
323
+ }
324
+
325
+ .empty-icon {
326
+ font-size: 48px;
327
+ margin-bottom: 16px;
328
+ opacity: 0.5;
329
+ }
330
+
331
+ .empty-title {
332
+ font-size: 18px;
333
+ font-weight: 700;
334
+ margin-bottom: 8px;
335
+ }
336
+
337
+ .empty-text {
338
+ font-size: 14px;
339
+ color: var(--text-secondary);
340
+ }
341
+
342
+ /* Loading */
343
+ .loading {
344
+ display: flex;
345
+ flex-direction: column;
346
+ align-items: center;
347
+ gap: 16px;
348
+ padding: 60px 24px;
349
+ background: var(--bg-card);
350
+ border: 2px solid var(--border-default);
351
+ border-radius: var(--radius-md);
352
+ box-shadow: var(--shadow-sm);
353
+ }
354
+
355
+ .loading-spinner {
356
+ width: 36px;
357
+ height: 36px;
358
+ border: 3px solid var(--border-default);
359
+ border-top-color: var(--accent);
360
+ border-radius: 50%;
361
+ animation: spin 0.8s linear infinite;
362
+ }
363
+
364
+ @keyframes spin {
365
+ to { transform: rotate(360deg); }
366
+ }
367
+
368
+ .loading-text {
369
+ font-size: 14px;
370
+ color: var(--text-secondary);
371
+ }
372
+
373
+ /* Toast */
374
+ .toast {
375
+ position: fixed;
376
+ top: calc(env(safe-area-inset-top, 0) + 16px);
377
+ left: 50%;
378
+ transform: translateX(-50%) translateY(-100px);
379
+ background: rgba(20, 20, 20, 0.95);
380
+ backdrop-filter: blur(12px);
381
+ -webkit-backdrop-filter: blur(12px);
382
+ color: #ffffff;
383
+ padding: 14px 24px;
384
+ border-radius: var(--radius-lg);
385
+ font-size: 14px;
386
+ font-weight: 600;
387
+ box-shadow: var(--shadow-lg), 0 0 0 1px rgba(255, 255, 255, 0.1);
388
+ opacity: 0;
389
+ transition: all 0.3s cubic-bezier(0.32, 0.72, 0, 1);
390
+ z-index: 1100;
391
+ }
392
+
393
+ .toast.show {
394
+ opacity: 1;
395
+ transform: translateX(-50%) translateY(0);
396
+ }
397
+
398
+ .toast.ok {
399
+ background: rgba(34, 197, 94, 0.98);
400
+ color: white;
401
+ box-shadow: var(--shadow-lg), 0 0 20px rgba(34, 197, 94, 0.4);
402
+ }
403
+
404
+ .toast.err {
405
+ background: rgba(239, 68, 68, 0.98);
406
+ color: white;
407
+ box-shadow: var(--shadow-lg), 0 0 20px rgba(239, 68, 68, 0.4);
408
+ }
409
+
410
+ /* Modal - 80% height */
411
+ .modal-bg {
412
+ position: fixed;
413
+ inset: 0;
414
+ background: rgba(0, 0, 0, 0.8);
415
+ backdrop-filter: blur(8px);
416
+ -webkit-backdrop-filter: blur(8px);
417
+ display: flex;
418
+ align-items: flex-end;
419
+ justify-content: center;
420
+ z-index: 1000;
421
+ opacity: 0;
422
+ visibility: hidden;
423
+ transition: opacity 0.3s, visibility 0.3s;
424
+ }
425
+
426
+ .modal-bg.show {
427
+ opacity: 1;
428
+ visibility: visible;
429
+ }
430
+
431
+ .modal {
432
+ background: var(--bg-card);
433
+ border: 1px solid var(--border-subtle);
434
+ border-bottom: none;
435
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
436
+ width: 100%;
437
+ max-width: 480px;
438
+ height: 80vh;
439
+ height: 80dvh;
440
+ transform: translateY(100%);
441
+ transition: transform 0.4s cubic-bezier(0.32, 0.72, 0, 1);
442
+ box-shadow: 0 -4px 30px rgba(0, 0, 0, 0.5);
443
+ display: flex;
444
+ flex-direction: column;
445
+ }
446
+
447
+ .modal-bg.show .modal {
448
+ transform: translateY(0);
449
+ }
450
+
451
+ .modal-handle {
452
+ width: 40px;
453
+ height: 5px;
454
+ background: var(--border-default);
455
+ border-radius: 3px;
456
+ margin: 12px auto;
457
+ flex-shrink: 0;
458
+ }
459
+
460
+ .modal-header {
461
+ display: flex;
462
+ align-items: center;
463
+ padding: 8px 20px 16px;
464
+ flex-shrink: 0;
465
+ border-bottom: 1px solid var(--border-subtle);
466
+ gap: 12px;
467
+ }
468
+
469
+ .modal-header-info {
470
+ flex: 1;
471
+ min-width: 0;
472
+ }
473
+
474
+ .modal-header h2 {
475
+ font-size: 18px;
476
+ font-weight: 700;
477
+ margin-bottom: 2px;
478
+ }
479
+
480
+ .modal-header-path {
481
+ font-size: 13px;
482
+ color: var(--text-muted);
483
+ white-space: nowrap;
484
+ overflow: hidden;
485
+ text-overflow: ellipsis;
486
+ }
487
+
488
+ .modal-header-actions {
489
+ display: flex;
490
+ align-items: center;
491
+ gap: 8px;
492
+ flex-shrink: 0;
493
+ }
494
+
495
+ .modal-focus-btn {
496
+ display: flex;
497
+ align-items: center;
498
+ justify-content: center;
499
+ gap: 6px;
500
+ padding: 12px 18px;
501
+ min-height: 44px;
502
+ background: linear-gradient(135deg, var(--accent) 0%, #4f46e5 100%);
503
+ border: none;
504
+ border-radius: 22px;
505
+ color: white;
506
+ font-family: inherit;
507
+ font-size: 14px;
508
+ font-weight: 700;
509
+ cursor: pointer;
510
+ box-shadow: 0 4px 12px var(--accent-glow);
511
+ transition: all 0.15s ease;
512
+ }
513
+
514
+ .modal-focus-btn:active {
515
+ transform: scale(0.95);
516
+ }
517
+
518
+ .modal-close {
519
+ width: 44px;
520
+ height: 44px;
521
+ display: flex;
522
+ align-items: center;
523
+ justify-content: center;
524
+ background: var(--bg-input);
525
+ border: 1px solid var(--border-subtle);
526
+ border-radius: 50%;
527
+ color: var(--text-secondary);
528
+ font-size: 22px;
529
+ cursor: pointer;
530
+ }
531
+
532
+ .modal-close:active {
533
+ background: var(--bg-card-hover);
534
+ transform: scale(0.95);
535
+ }
536
+
537
+ /* Message Section */
538
+ .modal-message {
539
+ flex: 1;
540
+ display: flex;
541
+ flex-direction: column;
542
+ min-height: 0;
543
+ padding: 16px 20px;
544
+ overflow: hidden;
545
+ }
546
+
547
+ .modal-message-label {
548
+ font-size: 12px;
549
+ font-weight: 600;
550
+ color: var(--text-muted);
551
+ text-transform: uppercase;
552
+ letter-spacing: 0.5px;
553
+ margin-bottom: 12px;
554
+ flex-shrink: 0;
555
+ }
556
+
557
+ .modal-message-content {
558
+ flex: 0 1 auto;
559
+ max-height: 100%;
560
+ background: var(--bg-input);
561
+ border: 1px solid var(--border-subtle);
562
+ border-radius: var(--radius-sm);
563
+ padding: 16px;
564
+ overflow-y: auto;
565
+ overflow-x: auto;
566
+ font-size: 15px;
567
+ color: var(--text-secondary);
568
+ line-height: 1.7;
569
+ }
570
+
571
+ .modal-message-content::-webkit-scrollbar {
572
+ width: 6px;
573
+ }
574
+
575
+ .modal-message-content::-webkit-scrollbar-thumb {
576
+ background: var(--border-default);
577
+ border-radius: 3px;
578
+ }
579
+
580
+ .modal-message-content p { margin: 0 0 14px; }
581
+ .modal-message-content p:last-child { margin-bottom: 0; }
582
+ .modal-message-content code {
583
+ background: var(--bg-primary);
584
+ padding: 2px 6px;
585
+ border-radius: 4px;
586
+ font-size: 13px;
587
+ font-family: 'SF Mono', Menlo, monospace;
588
+ }
589
+ .modal-message-content pre {
590
+ background: var(--bg-primary);
591
+ padding: 14px;
592
+ border-radius: var(--radius-sm);
593
+ overflow-x: auto;
594
+ margin: 14px 0;
595
+ font-size: 13px;
596
+ font-family: 'SF Mono', Menlo, monospace;
597
+ line-height: 1.5;
598
+ }
599
+ .modal-message-content pre code {
600
+ background: none;
601
+ padding: 0;
602
+ }
603
+ .modal-message-content ul, .modal-message-content ol {
604
+ margin: 14px 0;
605
+ padding-left: 24px;
606
+ list-style-position: inside;
607
+ }
608
+ .modal-message-content li {
609
+ margin: 8px 0;
610
+ text-indent: -4px;
611
+ }
612
+ .modal-message-content h1, .modal-message-content h2, .modal-message-content h3 {
613
+ margin: 18px 0 12px;
614
+ font-weight: 600;
615
+ color: var(--text-primary);
616
+ }
617
+ .modal-message-content h1 { font-size: 20px; }
618
+ .modal-message-content h2 { font-size: 18px; }
619
+ .modal-message-content h3 { font-size: 16px; }
620
+ .modal-message-content blockquote {
621
+ margin: 14px 0;
622
+ padding: 12px 16px;
623
+ border-left: 3px solid var(--accent);
624
+ background: var(--bg-primary);
625
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
626
+ }
627
+ .modal-message-content a {
628
+ color: var(--accent-hover);
629
+ text-decoration: none;
630
+ }
631
+ .modal-message-content table {
632
+ width: max-content;
633
+ min-width: 100%;
634
+ border-collapse: collapse;
635
+ margin: 14px 0;
636
+ font-size: 14px;
637
+ }
638
+ .modal-message-content th, .modal-message-content td {
639
+ padding: 10px 12px;
640
+ border: 1px solid var(--border-default);
641
+ text-align: left;
642
+ white-space: nowrap;
643
+ }
644
+ .modal-message-content th {
645
+ background: var(--bg-primary);
646
+ font-weight: 600;
647
+ }
648
+
649
+ .modal-message-empty {
650
+ color: var(--text-muted);
651
+ font-style: italic;
652
+ }
653
+
654
+ /* Input Section */
655
+ .modal-input {
656
+ flex-shrink: 0;
657
+ padding: 16px 20px;
658
+ padding-bottom: calc(env(safe-area-inset-bottom, 0) + 16px);
659
+ border-top: 1px solid var(--border-subtle);
660
+ background: var(--bg-card);
661
+ }
662
+
663
+ .modal-input-row {
664
+ display: flex;
665
+ gap: 12px;
666
+ align-items: flex-start;
667
+ }
668
+
669
+ .modal-input-field {
670
+ flex: 1;
671
+ }
672
+
673
+ .modal-input textarea {
674
+ width: 100%;
675
+ height: 50px;
676
+ max-height: 120px;
677
+ background: var(--bg-input);
678
+ border: 1px solid var(--border-default);
679
+ border-radius: var(--radius-sm);
680
+ color: var(--text-primary);
681
+ font-family: inherit;
682
+ font-size: 16px;
683
+ padding: 13px 14px;
684
+ resize: none;
685
+ transition: all 0.2s ease;
686
+ }
687
+
688
+ .modal-input textarea::placeholder {
689
+ color: var(--text-muted);
690
+ }
691
+
692
+ .modal-input textarea:focus {
693
+ outline: none;
694
+ border-color: var(--accent);
695
+ box-shadow: 0 0 0 3px var(--accent-glow);
696
+ }
697
+
698
+ .modal-warn {
699
+ margin-top: 10px;
700
+ padding: 10px 12px;
701
+ background: rgba(239, 68, 68, 0.15);
702
+ border: 1px solid rgba(239, 68, 68, 0.3);
703
+ border-radius: var(--radius-sm);
704
+ font-size: 12px;
705
+ font-weight: 600;
706
+ color: var(--danger);
707
+ display: none;
708
+ }
709
+
710
+ .modal-warn.show {
711
+ display: flex;
712
+ align-items: center;
713
+ gap: 8px;
714
+ }
715
+
716
+ .modal-send-btn {
717
+ width: 50px;
718
+ height: 50px;
719
+ display: flex;
720
+ align-items: center;
721
+ justify-content: center;
722
+ background: linear-gradient(135deg, var(--accent) 0%, #4f46e5 100%);
723
+ border: none;
724
+ border-radius: var(--radius-sm);
725
+ color: white;
726
+ font-size: 22px;
727
+ cursor: pointer;
728
+ flex-shrink: 0;
729
+ box-shadow: 0 4px 12px var(--accent-glow);
730
+ transition: all 0.2s ease;
731
+ }
732
+
733
+ .modal-send-btn:active {
734
+ transform: scale(0.95);
735
+ }
736
+
737
+ .modal-send-btn:disabled {
738
+ background: var(--bg-input);
739
+ color: var(--text-muted);
740
+ box-shadow: none;
741
+ }
742
+
743
+ /* Desktop Warning */
744
+ .desktop-warning {
745
+ display: none;
746
+ flex-direction: column;
747
+ align-items: center;
748
+ justify-content: center;
749
+ min-height: 100vh;
750
+ min-height: 100dvh;
751
+ padding: 40px;
752
+ text-align: center;
753
+ }
754
+
755
+ .desktop-warning-icon {
756
+ font-size: 64px;
757
+ margin-bottom: 24px;
758
+ }
759
+
760
+ .desktop-warning-title {
761
+ font-size: 24px;
762
+ font-weight: 700;
763
+ margin-bottom: 12px;
764
+ }
765
+
766
+ .desktop-warning-text {
767
+ font-size: 15px;
768
+ color: var(--text-secondary);
769
+ line-height: 1.6;
770
+ }
771
+
772
+ .desktop-qr {
773
+ margin-top: 32px;
774
+ border-radius: var(--radius-md);
775
+ box-shadow: var(--shadow-lg);
776
+ }
777
+
778
+ @media (min-width: 1025px) and (pointer: fine) {
779
+ .app, .toast, .modal-bg {
780
+ display: none !important;
781
+ }
782
+ .desktop-warning {
783
+ display: flex;
784
+ }
785
+ }
786
+ </style>
787
+ </head>
788
+ <body>
789
+ <div class="desktop-warning">
790
+ <div class="desktop-warning-icon">📱</div>
791
+ <h1 class="desktop-warning-title">Mobile Only</h1>
792
+ <p class="desktop-warning-text">
793
+ This interface is designed for mobile devices.<br>
794
+ Scan the QR code to access from your phone.
795
+ </p>
796
+ <img id="qrcode" class="desktop-qr" alt="QR Code">
797
+ </div>
798
+
799
+ <div class="app">
800
+ <header class="header">
801
+ <div class="header-left">
802
+ <h1 class="header-title">Claude Code Monitor</h1>
803
+ <div class="conn-indicator" id="connStatus"></div>
804
+ </div>
805
+ <div class="header-actions">
806
+ <button class="icon-btn" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme">
807
+ <svg id="themeIcon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
808
+ <path d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 0 1-4.4 2.26 5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z"/>
809
+ </svg>
810
+ </button>
811
+ <button class="icon-btn" onclick="clearAllSessions()" title="Clear sessions" aria-label="Clear sessions">
812
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
813
+ <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
814
+ </svg>
815
+ </button>
816
+ </div>
817
+ </header>
818
+
819
+ <div class="sessions" id="sessions">
820
+ <div class="loading">
821
+ <div class="loading-spinner"></div>
822
+ <span class="loading-text">Connecting...</span>
823
+ </div>
824
+ </div>
825
+ </div>
826
+
827
+ <div class="toast" id="toast"></div>
828
+
829
+ <div class="modal-bg" id="modal">
830
+ <div class="modal">
831
+ <div class="modal-handle"></div>
832
+ <div class="modal-header">
833
+ <div class="modal-header-info">
834
+ <h2 id="modalTitle">Project Name</h2>
835
+ <div class="modal-header-path" id="modalPath">~/path/to/project</div>
836
+ </div>
837
+ <div class="modal-header-actions">
838
+ <button class="modal-focus-btn" id="modalFocusBtn" onclick="focusFromModal()">
839
+ 💻 Focus
840
+ </button>
841
+ <button class="modal-close" onclick="closeModal()">&times;</button>
842
+ </div>
843
+ </div>
844
+ <div class="modal-message">
845
+ <div class="modal-message-label" id="modalMessageLabel">Message</div>
846
+ <div class="modal-message-content" id="modalMessage">
847
+ <span class="modal-message-empty">No messages yet</span>
848
+ </div>
849
+ </div>
850
+ <div class="modal-waiting-notice" id="modalWaitingNotice" style="display: none;">
851
+ <div class="modal-waiting-notice-text">Permission required. Please respond on PC.</div>
852
+ </div>
853
+ <div class="modal-input">
854
+ <div class="modal-input-row">
855
+ <div class="modal-input-field">
856
+ <textarea id="msgInput" placeholder="Send a message..." rows="1"></textarea>
857
+ <div class="modal-warn" id="warn">
858
+ <span>!</span>
859
+ <span>Dangerous command detected</span>
860
+ </div>
861
+ </div>
862
+ <button class="modal-send-btn" id="sendBtn" onclick="sendMsg()" disabled>↑</button>
863
+ </div>
864
+ </div>
865
+ </div>
866
+ </div>
867
+
868
+ <script>
869
+ const $sessions = document.getElementById('sessions');
870
+ const $connStatus = document.getElementById('connStatus');
871
+ const $toast = document.getElementById('toast');
872
+ const $themeIcon = document.getElementById('themeIcon');
873
+ const $modal = document.getElementById('modal');
874
+ const $modalTitle = document.getElementById('modalTitle');
875
+ const $modalPath = document.getElementById('modalPath');
876
+ const $modalMessageLabel = document.getElementById('modalMessageLabel');
877
+ const $modalMessage = document.getElementById('modalMessage');
878
+ const $modalWaitingNotice = document.getElementById('modalWaitingNotice');
879
+ const $msgInput = document.getElementById('msgInput');
880
+ const $sendBtn = document.getElementById('sendBtn');
881
+ const $warn = document.getElementById('warn');
882
+
883
+ const TOAST_DURATION_MS = 2500;
884
+ const RECONNECT_DELAY_MS = 2000;
885
+ const SEND_TIMEOUT_MS = 5000;
886
+ const SWIPE_THRESHOLD_PX = 100;
887
+
888
+ let ws = null;
889
+ let targetSession = null;
890
+ let pendingSend = null;
891
+ let sessionsData = [];
892
+
893
+ const STATUS = {
894
+ running: 'Running',
895
+ waiting_input: 'Waiting for Input',
896
+ stopped: 'Stopped'
897
+ };
898
+
899
+ const DANGER = [
900
+ /rm\s+(-rf?|--recursive)/i,
901
+ /sudo\s+rm/i,
902
+ /mkfs/i,
903
+ /dd\s+if=/i,
904
+ />\s*\/dev\//i,
905
+ /chmod\s+777/i,
906
+ /curl.*\|\s*(ba)?sh/i,
907
+ ];
908
+
909
+ // Theme management
910
+ const THEME_KEY = 'ccm-theme';
911
+ const SUN_PATH = 'M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 0 0 0-1.41l-1.06-1.06zm1.06-10.96a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z';
912
+ const MOON_PATH = 'M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 0 1-4.4 2.26 5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z';
913
+
914
+ function initTheme() {
915
+ const saved = localStorage.getItem(THEME_KEY);
916
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
917
+ const isDark = saved ? saved === 'dark' : prefersDark;
918
+ document.documentElement.classList.toggle('light', !isDark);
919
+ updateThemeIcon(!isDark);
920
+ }
921
+
922
+ function updateThemeIcon(isLight) {
923
+ $themeIcon.querySelector('path').setAttribute('d', isLight ? MOON_PATH : SUN_PATH);
924
+ }
925
+
926
+ function toggleTheme() {
927
+ const isCurrentlyLight = document.documentElement.classList.contains('light');
928
+ document.documentElement.classList.toggle('light');
929
+ localStorage.setItem(THEME_KEY, isCurrentlyLight ? 'dark' : 'light');
930
+ updateThemeIcon(!isCurrentlyLight);
931
+ }
932
+
933
+ initTheme();
934
+
935
+ function toast(msg, type = '') {
936
+ $toast.textContent = msg;
937
+ $toast.className = `toast show ${type}`;
938
+ setTimeout(() => $toast.className = 'toast', TOAST_DURATION_MS);
939
+ }
940
+
941
+ function setConn(ok) {
942
+ $connStatus.className = ok ? 'conn-indicator online' : 'conn-indicator';
943
+ }
944
+
945
+ function getDirName(path) {
946
+ if (!path) return 'Unknown';
947
+ const parts = path.split('/').filter(p => p);
948
+ return parts[parts.length - 1] || path;
949
+ }
950
+
951
+ function formatPath(path) {
952
+ if (!path) return '';
953
+ return path.replace(/^\/Users\/[^/]+/, '~');
954
+ }
955
+
956
+ function truncateMessage(text, maxLen = 100) {
957
+ if (!text) return '';
958
+ const clean = text.replace(/[#*`_~\[\]]/g, '').replace(/\s+/g, ' ').trim();
959
+ if (clean.length <= maxLen) return clean;
960
+ return clean.slice(0, maxLen) + '...';
961
+ }
962
+
963
+ function renderMarkdown(markdownText) {
964
+ if (!markdownText) return '<span class="modal-message-empty">No messages yet</span>';
965
+ if (typeof marked !== 'undefined') {
966
+ marked.setOptions({ breaks: true, gfm: true });
967
+ const html = marked.parse(markdownText);
968
+ if (typeof DOMPurify !== 'undefined') {
969
+ return DOMPurify.sanitize(html);
970
+ }
971
+ return html;
972
+ }
973
+ const container = document.createElement('div');
974
+ container.textContent = markdownText;
975
+ return container.innerHTML;
976
+ }
977
+
978
+ function getSessionById(id) {
979
+ return sessionsData.find(s => s.session_id === id);
980
+ }
981
+
982
+ function openModal(id) {
983
+ const session = getSessionById(id);
984
+ if (!session) return;
985
+
986
+ const isWaiting = session.status === 'waiting_input';
987
+
988
+ targetSession = id;
989
+ $modalTitle.textContent = getDirName(session.cwd);
990
+ $modalPath.textContent = formatPath(session.cwd);
991
+ $modalMessageLabel.textContent = 'Message';
992
+ $modalMessage.innerHTML = renderMarkdown(session.lastMessage);
993
+ $msgInput.value = '';
994
+ $warn.classList.remove('show');
995
+ $sendBtn.disabled = true;
996
+
997
+ // Show notice and disable input if waiting for permission
998
+ $modalWaitingNotice.style.display = isWaiting ? 'block' : 'none';
999
+ $msgInput.disabled = isWaiting;
1000
+ $msgInput.placeholder = isWaiting ? 'Waiting for permission...' : 'Send a message...';
1001
+
1002
+ $modal.classList.add('show');
1003
+ document.body.style.overflow = 'hidden';
1004
+ if (!isWaiting) {
1005
+ setTimeout(() => $msgInput.focus(), 150);
1006
+ }
1007
+ }
1008
+
1009
+ function closeModal() {
1010
+ $modal.classList.remove('show');
1011
+ document.body.style.overflow = '';
1012
+ targetSession = null;
1013
+ }
1014
+
1015
+ function sendMsg() {
1016
+ const text = $msgInput.value.trim();
1017
+ if (!text || !targetSession) return;
1018
+ if (!ws || ws.readyState !== 1) {
1019
+ toast('Not connected', 'err');
1020
+ return;
1021
+ }
1022
+ if (pendingSend) return;
1023
+
1024
+ pendingSend = targetSession;
1025
+ $sendBtn.disabled = true;
1026
+
1027
+ ws.send(JSON.stringify({ type: 'sendText', sessionId: targetSession, text }));
1028
+
1029
+ setTimeout(() => {
1030
+ if (pendingSend) {
1031
+ pendingSend = null;
1032
+ $sendBtn.disabled = !$msgInput.value.trim();
1033
+ toast('Send timeout', 'err');
1034
+ }
1035
+ }, SEND_TIMEOUT_MS);
1036
+ }
1037
+
1038
+ function focusFromModal() {
1039
+ if (!targetSession) return;
1040
+ if (!ws || ws.readyState !== 1) return toast('Not connected', 'err');
1041
+ ws.send(JSON.stringify({ type: 'focus', sessionId: targetSession }));
1042
+ }
1043
+
1044
+ function clearAllSessions() {
1045
+ if (!ws || ws.readyState !== 1) return toast('Not connected', 'err');
1046
+ ws.send(JSON.stringify({ type: 'clearSessions' }));
1047
+ }
1048
+
1049
+ $msgInput.addEventListener('input', () => {
1050
+ const text = $msgInput.value.trim();
1051
+ $sendBtn.disabled = !text;
1052
+ $warn.classList.toggle('show', DANGER.some(p => p.test(text)));
1053
+ $msgInput.style.height = '50px';
1054
+ $msgInput.style.height = Math.min($msgInput.scrollHeight, 120) + 'px';
1055
+ });
1056
+
1057
+ $msgInput.addEventListener('keydown', (e) => {
1058
+ if (e.key === 'Escape') closeModal();
1059
+ });
1060
+
1061
+ $modal.addEventListener('click', (e) => {
1062
+ if (e.target === $modal) closeModal();
1063
+ });
1064
+
1065
+ // Swipe to close
1066
+ let touchStartY = 0, currentTouchY = 0, isDragging = false;
1067
+ const $modalContent = $modal.querySelector('.modal');
1068
+ const $modalHandle = $modal.querySelector('.modal-handle');
1069
+ const $modalHeader = $modal.querySelector('.modal-header');
1070
+
1071
+ function handleSwipeStart(e) {
1072
+ touchStartY = currentTouchY = e.touches[0].clientY;
1073
+ isDragging = true;
1074
+ $modalContent.style.transition = 'none';
1075
+ }
1076
+
1077
+ function handleSwipeMove(e) {
1078
+ if (!isDragging) return;
1079
+ currentTouchY = e.touches[0].clientY;
1080
+ const d = currentTouchY - touchStartY;
1081
+ if (d > 0) {
1082
+ $modalContent.style.transform = `translateY(${d}px)`;
1083
+ }
1084
+ }
1085
+
1086
+ function handleSwipeEnd() {
1087
+ if (!isDragging) return;
1088
+ isDragging = false;
1089
+ $modalContent.style.transition = 'transform 0.3s cubic-bezier(0.32, 0.72, 0, 1)';
1090
+ const swipeDistance = currentTouchY - touchStartY;
1091
+ if (swipeDistance > SWIPE_THRESHOLD_PX) {
1092
+ closeModal();
1093
+ setTimeout(() => { $modalContent.style.transform = ''; }, 300);
1094
+ } else {
1095
+ $modalContent.style.transform = '';
1096
+ }
1097
+ }
1098
+
1099
+ // Enable swipe on handle and header area
1100
+ $modalHandle.addEventListener('touchstart', handleSwipeStart, { passive: true });
1101
+ $modalHandle.addEventListener('touchmove', handleSwipeMove, { passive: true });
1102
+ $modalHandle.addEventListener('touchend', handleSwipeEnd);
1103
+ $modalHeader.addEventListener('touchstart', handleSwipeStart, { passive: true });
1104
+ $modalHeader.addEventListener('touchmove', handleSwipeMove, { passive: true });
1105
+ $modalHeader.addEventListener('touchend', handleSwipeEnd);
1106
+
1107
+ function render(list) {
1108
+ sessionsData = list || [];
1109
+
1110
+ if (!list || !list.length) {
1111
+ $sessions.innerHTML = `
1112
+ <div class="empty">
1113
+ <div class="empty-icon">📋</div>
1114
+ <div class="empty-title">No Active Sessions</div>
1115
+ <div class="empty-text">Start a Claude Code session to monitor it here</div>
1116
+ </div>`;
1117
+ return;
1118
+ }
1119
+
1120
+ $sessions.innerHTML = list.map(session => {
1121
+ const dirName = getDirName(session.cwd);
1122
+ const shortPath = formatPath(session.cwd);
1123
+ const statusLabel = STATUS[session.status];
1124
+ const msgPreview = truncateMessage(session.lastMessage);
1125
+
1126
+ return `
1127
+ <div class="card ${session.status}" onclick="openModal('${session.session_id}')">
1128
+ <div class="card-status">
1129
+ <span class="card-status-dot"></span>
1130
+ <span>${statusLabel}</span>
1131
+ <span class="card-status-chevron">›</span>
1132
+ </div>
1133
+ <div class="card-body">
1134
+ <div class="card-name">${dirName}</div>
1135
+ <div class="card-path">${shortPath}</div>
1136
+ <div class="card-message">
1137
+ ${msgPreview
1138
+ ? `<div class="card-message-text">${msgPreview}</div>`
1139
+ : `<div class="card-message-empty">No messages yet</div>`
1140
+ }
1141
+ </div>
1142
+ </div>
1143
+ </div>`;
1144
+ }).join('');
1145
+ }
1146
+
1147
+ function connect() {
1148
+ const params = new URLSearchParams(window.location.search);
1149
+ const token = params.get('token');
1150
+
1151
+ if (!token) {
1152
+ $sessions.innerHTML = `
1153
+ <div class="empty">
1154
+ <div class="empty-icon">🔒</div>
1155
+ <div class="empty-title">Unauthorized</div>
1156
+ <div class="empty-text">Please scan the QR code from the terminal</div>
1157
+ </div>`;
1158
+ setConn(false);
1159
+ return;
1160
+ }
1161
+
1162
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1163
+ ws = new WebSocket(`${proto}//${location.host}?token=${token}`);
1164
+
1165
+ ws.onopen = () => {
1166
+ setConn(true);
1167
+ pendingSend = null; // Reset pending state on reconnect
1168
+ };
1169
+ ws.onclose = () => {
1170
+ setConn(false);
1171
+ pendingSend = null; // Reset pending state on disconnect
1172
+ setTimeout(connect, RECONNECT_DELAY_MS);
1173
+ };
1174
+ ws.onerror = () => {
1175
+ setConn(false);
1176
+ pendingSend = null;
1177
+ };
1178
+ ws.onmessage = (event) => {
1179
+ try {
1180
+ const msg = JSON.parse(event.data);
1181
+ if (msg.type === 'sessions') {
1182
+ render(msg.data);
1183
+ } else if (msg.type === 'focusResult') {
1184
+ if (!msg.success) toast('Failed', 'err');
1185
+ } else if (msg.type === 'sendTextResult') {
1186
+ pendingSend = null;
1187
+ $sendBtn.disabled = !$msgInput.value.trim();
1188
+ if (!msg.success) {
1189
+ toast(msg.error || 'Failed', 'err');
1190
+ } else {
1191
+ $msgInput.value = '';
1192
+ $msgInput.style.height = '50px';
1193
+ $sendBtn.disabled = true;
1194
+ }
1195
+ } else if (msg.type === 'clearSessionsResult') {
1196
+ if (!msg.success) toast(msg.error || 'Failed', 'err');
1197
+ }
1198
+ } catch {}
1199
+ };
1200
+ }
1201
+
1202
+ connect();
1203
+
1204
+ // QR code for desktop
1205
+ (function() {
1206
+ const qrImg = document.getElementById('qrcode');
1207
+ if (qrImg) {
1208
+ const url = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(window.location.href)}&bgcolor=1a1a1a&color=ffffff`;
1209
+ qrImg.src = url;
1210
+ }
1211
+ })();
1212
+
1213
+ // Prevent zoom
1214
+ document.body.addEventListener('touchmove', (e) => {
1215
+ if (e.touches.length > 1) e.preventDefault();
1216
+ }, { passive: false });
1217
+ </script>
1218
+ </body>
1219
+ </html>