termbeam 1.11.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/index.html DELETED
@@ -1,1521 +0,0 @@
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, viewport-fit=cover"
8
- />
9
- <meta name="apple-mobile-web-app-capable" content="yes" />
10
- <meta name="mobile-web-app-capable" content="yes" />
11
- <meta name="theme-color" content="#1e1e1e" />
12
- <meta
13
- name="description"
14
- content="TermBeam — beam your terminal to any device. Mobile-optimized web terminal with multi-session support, touch controls, and QR code connection. No SSH needed."
15
- />
16
- <link rel="manifest" href="/manifest.json" />
17
- <link rel="apple-touch-icon" href="/icons/icon-192.png" />
18
- <link rel="stylesheet" href="/css/themes.css" />
19
- <title>TermBeam — Beam Your Terminal to Any Device</title>
20
- <style>
21
- :root {
22
- --info: #b0b0b0;
23
- --shadow: rgba(0, 0, 0, 0.15);
24
- }
25
- [data-theme='light'] {
26
- --info: #616161;
27
- --shadow: rgba(0, 0, 0, 0.06);
28
- }
29
- [data-theme='monokai'] {
30
- --info: #a59f85;
31
- --shadow: rgba(0, 0, 0, 0.3);
32
- }
33
- [data-theme='solarized-dark'] {
34
- --info: #657b83;
35
- --shadow: rgba(0, 0, 0, 0.25);
36
- }
37
- [data-theme='solarized-light'] {
38
- --info: #93a1a1;
39
- --shadow: rgba(0, 0, 0, 0.08);
40
- }
41
- [data-theme='nord'] {
42
- --info: #b0bac9;
43
- --shadow: rgba(0, 0, 0, 0.2);
44
- }
45
- [data-theme='dracula'] {
46
- --info: #c1c4d2;
47
- --shadow: rgba(0, 0, 0, 0.25);
48
- }
49
- [data-theme='github-dark'] {
50
- --info: #8b949e;
51
- --shadow: rgba(0, 0, 0, 0.3);
52
- }
53
- [data-theme='one-dark'] {
54
- --info: #7f848e;
55
- --shadow: rgba(0, 0, 0, 0.25);
56
- }
57
- [data-theme='catppuccin'] {
58
- --info: #a6adc8;
59
- --shadow: rgba(0, 0, 0, 0.2);
60
- }
61
- [data-theme='gruvbox'] {
62
- --info: #d5c4a1;
63
- --shadow: rgba(0, 0, 0, 0.25);
64
- }
65
- [data-theme='night-owl'] {
66
- --info: #8badc1;
67
- --shadow: rgba(0, 0, 0, 0.3);
68
- }
69
- * {
70
- margin: 0;
71
- padding: 0;
72
- box-sizing: border-box;
73
- }
74
- html,
75
- body {
76
- height: 100%;
77
- width: 100%;
78
- background: var(--bg);
79
- color: var(--text);
80
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
81
- transition:
82
- background 0.3s,
83
- color 0.3s;
84
- }
85
-
86
- .header {
87
- padding: 20px 16px 12px;
88
- text-align: center;
89
- border-bottom: 1px solid var(--border);
90
- transition: border-color 0.3s;
91
- position: relative;
92
- }
93
- .header h1 {
94
- font-size: 22px;
95
- font-weight: 700;
96
- }
97
- .header h1 span {
98
- color: var(--accent);
99
- }
100
- .header p {
101
- font-size: 13px;
102
- color: var(--text-secondary);
103
- margin-top: 4px;
104
- }
105
- .header-btn {
106
- position: absolute;
107
- top: 16px;
108
- background: none;
109
- border: 1px solid var(--border);
110
- color: var(--text-dim);
111
- width: 32px;
112
- height: 32px;
113
- border-radius: 8px;
114
- cursor: pointer;
115
- display: flex;
116
- align-items: center;
117
- justify-content: center;
118
- font-size: 16px;
119
- transition:
120
- color 0.15s,
121
- border-color 0.15s,
122
- background 0.15s;
123
- -webkit-tap-highlight-color: transparent;
124
- }
125
- .header-btn:hover {
126
- color: var(--text);
127
- border-color: var(--border-subtle);
128
- background: var(--border);
129
- }
130
- .theme-wrap {
131
- position: absolute;
132
- top: 16px;
133
- right: 16px;
134
- }
135
- .theme-toggle {
136
- background: none;
137
- border: 1px solid var(--border);
138
- color: var(--text-dim);
139
- width: 32px;
140
- height: 32px;
141
- border-radius: 8px;
142
- cursor: pointer;
143
- display: flex;
144
- align-items: center;
145
- justify-content: center;
146
- font-size: 16px;
147
- transition:
148
- color 0.15s,
149
- border-color 0.15s,
150
- background 0.15s;
151
- -webkit-tap-highlight-color: transparent;
152
- }
153
- .theme-toggle:hover {
154
- color: var(--text);
155
- border-color: var(--border-subtle);
156
- background: var(--border);
157
- }
158
- .theme-picker {
159
- display: none;
160
- position: absolute;
161
- top: calc(100% + 4px);
162
- right: 0;
163
- background: var(--surface);
164
- border: 1px solid var(--border);
165
- border-radius: 8px;
166
- min-width: 160px;
167
- padding: 4px 0;
168
- z-index: 200;
169
- box-shadow: 0 4px 12px var(--shadow);
170
- }
171
- .theme-picker.open {
172
- display: block;
173
- }
174
- .theme-option {
175
- display: flex;
176
- align-items: center;
177
- gap: 8px;
178
- padding: 7px 12px;
179
- cursor: pointer;
180
- font-size: 13px;
181
- color: var(--text);
182
- transition: background 0.1s;
183
- white-space: nowrap;
184
- }
185
- .theme-option:hover {
186
- background: var(--border);
187
- }
188
- .theme-option.active {
189
- color: var(--accent);
190
- }
191
- .theme-swatch {
192
- width: 14px;
193
- height: 14px;
194
- border-radius: 50%;
195
- display: inline-block;
196
- flex-shrink: 0;
197
- border: 1px solid rgba(128, 128, 128, 0.3);
198
- }
199
-
200
- .update-banner {
201
- margin: 12px 16px 0;
202
- padding: 10px 14px;
203
- background: var(--surface);
204
- border: 1px solid var(--accent);
205
- border-radius: 10px;
206
- display: none;
207
- align-items: center;
208
- gap: 10px;
209
- font-size: 13px;
210
- color: var(--text);
211
- }
212
- .update-banner.visible {
213
- display: flex;
214
- }
215
- .update-banner-text {
216
- flex: 1;
217
- }
218
- .update-banner-text code {
219
- font-size: 12px;
220
- background: var(--border);
221
- padding: 2px 6px;
222
- border-radius: 4px;
223
- word-break: break-all;
224
- }
225
- .update-banner-dismiss {
226
- background: none;
227
- border: none;
228
- color: var(--text-dim);
229
- cursor: pointer;
230
- font-size: 18px;
231
- line-height: 1;
232
- padding: 0 2px;
233
- flex-shrink: 0;
234
- }
235
- .update-banner-dismiss:hover {
236
- color: var(--text);
237
- }
238
-
239
- .sessions-list {
240
- padding: 16px;
241
- padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
242
- display: flex;
243
- flex-direction: column;
244
- gap: 12px;
245
- }
246
-
247
- .session-card {
248
- background: var(--surface);
249
- border: 1px solid var(--border);
250
- border-radius: 12px;
251
- padding: 16px;
252
- display: flex;
253
- flex-direction: column;
254
- gap: 8px;
255
- text-decoration: none;
256
- color: inherit;
257
- transition:
258
- transform 0.2s ease,
259
- border-color 0.15s,
260
- background 0.3s;
261
- cursor: pointer;
262
- -webkit-tap-highlight-color: transparent;
263
- position: relative;
264
- z-index: 1;
265
- }
266
- .session-card:hover {
267
- border-color: var(--accent);
268
- }
269
-
270
- .swipe-wrap {
271
- position: relative;
272
- overflow: hidden;
273
- border-radius: 12px;
274
- }
275
- .swipe-delete {
276
- position: absolute;
277
- right: 0;
278
- top: 0;
279
- bottom: 0;
280
- width: 80px;
281
- background: var(--danger);
282
- display: flex;
283
- align-items: center;
284
- justify-content: center;
285
- border-radius: 0 12px 12px 0;
286
- z-index: 0;
287
- }
288
- .swipe-delete button {
289
- background: none;
290
- border: none;
291
- color: white;
292
- font-size: 24px;
293
- cursor: pointer;
294
- width: 100%;
295
- height: 100%;
296
- display: flex;
297
- align-items: center;
298
- justify-content: center;
299
- gap: 4px;
300
- flex-direction: column;
301
- }
302
- .swipe-delete button span {
303
- font-size: 11px;
304
- font-weight: 600;
305
- }
306
-
307
- .session-card .top {
308
- display: flex;
309
- align-items: center;
310
- justify-content: space-between;
311
- }
312
- .session-card .name {
313
- font-size: 17px;
314
- font-weight: 600;
315
- display: flex;
316
- align-items: center;
317
- gap: 8px;
318
- }
319
- .session-card .name .dot {
320
- width: 10px;
321
- height: 10px;
322
- border-radius: 50%;
323
- background: var(--success);
324
- flex-shrink: 0;
325
- }
326
- .session-card .pid {
327
- font-size: 12px;
328
- color: var(--text-secondary);
329
- background: var(--bg);
330
- padding: 2px 8px;
331
- border-radius: 4px;
332
- transition:
333
- background 0.3s,
334
- color 0.3s;
335
- }
336
- .session-card .details {
337
- display: flex;
338
- flex-wrap: wrap;
339
- gap: 6px 16px;
340
- font-size: 13px;
341
- color: var(--info);
342
- }
343
- .session-card .details span {
344
- display: flex;
345
- align-items: center;
346
- gap: 4px;
347
- }
348
- .session-card .connect-btn {
349
- align-self: flex-end;
350
- background: var(--accent);
351
- color: #ffffff;
352
- border: none;
353
- border-radius: 8px;
354
- padding: 8px 20px;
355
- font-size: 14px;
356
- font-weight: 600;
357
- cursor: pointer;
358
- transition:
359
- background 0.15s,
360
- transform 0.1s;
361
- }
362
- .session-card .connect-btn:hover {
363
- background: var(--accent-hover);
364
- }
365
- .session-card .connect-btn:active {
366
- background: var(--accent-active);
367
- transform: scale(0.95);
368
- }
369
-
370
- .session-card .git-info {
371
- display: flex;
372
- flex-wrap: wrap;
373
- gap: 4px 10px;
374
- font-size: 12px;
375
- color: var(--text-secondary);
376
- align-items: center;
377
- }
378
- .session-card .git-info .git-badge {
379
- display: inline-flex;
380
- align-items: center;
381
- gap: 4px;
382
- background: var(--bg);
383
- padding: 2px 8px;
384
- border-radius: 4px;
385
- transition:
386
- background 0.3s,
387
- color 0.3s;
388
- }
389
- .session-card .git-info .git-status-clean {
390
- color: var(--success);
391
- }
392
- .session-card .git-info .git-status-dirty {
393
- color: var(--warning, #fbbf24);
394
- }
395
-
396
- .new-session {
397
- position: fixed;
398
- bottom: calc(16px + env(safe-area-inset-bottom, 0px));
399
- left: calc(16px + env(safe-area-inset-left, 0px));
400
- right: calc(16px + env(safe-area-inset-right, 0px));
401
- padding: 14px;
402
- background: var(--accent);
403
- color: #ffffff;
404
- border: none;
405
- border-radius: 12px;
406
- font-size: 15px;
407
- font-weight: 600;
408
- cursor: pointer;
409
- text-align: center;
410
- z-index: 50;
411
- transition:
412
- background 0.15s,
413
- transform 0.1s,
414
- box-shadow 0.15s;
415
- box-shadow: 0 2px 8px rgba(0, 120, 212, 0.3);
416
- }
417
- .new-session:hover {
418
- background: var(--accent-hover);
419
- box-shadow: 0 4px 12px rgba(0, 120, 212, 0.4);
420
- }
421
- .new-session:active {
422
- background: var(--accent-active);
423
- transform: scale(0.98);
424
- box-shadow: 0 1px 4px rgba(0, 120, 212, 0.2);
425
- }
426
-
427
- .empty-state {
428
- text-align: center;
429
- padding: 60px 20px;
430
- color: var(--text-muted);
431
- font-size: 15px;
432
- }
433
-
434
- /* New session modal */
435
- .modal-overlay {
436
- display: none;
437
- position: fixed;
438
- top: 0;
439
- left: 0;
440
- right: 0;
441
- bottom: 0;
442
- background: var(--overlay-bg);
443
- z-index: 100;
444
- justify-content: center;
445
- align-items: center;
446
- }
447
- .modal-overlay.visible {
448
- display: flex;
449
- }
450
- .modal {
451
- background: var(--surface);
452
- border-radius: 16px;
453
- width: 90%;
454
- max-width: 500px;
455
- padding: 24px 20px;
456
- transition: background 0.3s;
457
- }
458
- .modal h2 {
459
- font-size: 18px;
460
- margin-bottom: 16px;
461
- }
462
- .modal label {
463
- display: block;
464
- font-size: 13px;
465
- color: var(--text-secondary);
466
- margin-bottom: 4px;
467
- margin-top: 12px;
468
- }
469
- .modal input,
470
- .modal select {
471
- width: 100%;
472
- padding: 10px 12px;
473
- background: var(--bg);
474
- border: 1px solid var(--border);
475
- border-radius: 8px;
476
- color: var(--text);
477
- font-size: 15px;
478
- outline: none;
479
- -webkit-appearance: none;
480
- appearance: none;
481
- transition:
482
- background 0.3s,
483
- border-color 0.15s,
484
- color 0.3s;
485
- }
486
- .modal select {
487
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
488
- background-repeat: no-repeat;
489
- background-position: right 12px center;
490
- padding-right: 32px;
491
- cursor: pointer;
492
- }
493
- .modal input:focus,
494
- .modal select:focus {
495
- border-color: var(--accent);
496
- }
497
- .modal-actions {
498
- display: flex;
499
- gap: 12px;
500
- margin-top: 20px;
501
- }
502
- .modal-actions button {
503
- flex: 1;
504
- padding: 12px;
505
- border: none;
506
- border-radius: 8px;
507
- font-size: 15px;
508
- font-weight: 600;
509
- cursor: pointer;
510
- transition:
511
- background 0.15s,
512
- transform 0.1s;
513
- }
514
- .modal-actions button:active {
515
- transform: scale(0.95);
516
- }
517
- .btn-cancel {
518
- background: var(--border);
519
- color: var(--text);
520
- }
521
- .btn-cancel:hover {
522
- background: var(--border-subtle);
523
- }
524
- .btn-create {
525
- background: var(--accent);
526
- color: #ffffff;
527
- }
528
- .btn-create:hover {
529
- background: var(--accent-hover);
530
- }
531
-
532
- /* Folder browser */
533
- .cwd-picker {
534
- display: flex;
535
- gap: 8px;
536
- }
537
- .cwd-picker input {
538
- flex: 1;
539
- }
540
- .cwd-browse-btn {
541
- background: var(--border);
542
- color: var(--text);
543
- border: 1px solid var(--border);
544
- border-radius: 8px;
545
- padding: 0 14px;
546
- font-size: 18px;
547
- cursor: pointer;
548
- flex-shrink: 0;
549
- display: flex;
550
- align-items: center;
551
- transition:
552
- background 0.15s,
553
- border-color 0.15s;
554
- }
555
- .cwd-browse-btn:hover {
556
- border-color: var(--accent);
557
- }
558
- .cwd-browse-btn:active {
559
- background: var(--accent);
560
- color: #ffffff;
561
- }
562
-
563
- .browser-overlay {
564
- display: none;
565
- position: fixed;
566
- top: 0;
567
- left: 0;
568
- right: 0;
569
- bottom: 0;
570
- background: var(--overlay-bg);
571
- z-index: 200;
572
- justify-content: center;
573
- align-items: flex-end;
574
- }
575
- .browser-overlay.visible {
576
- display: flex;
577
- }
578
- .browser-sheet {
579
- background: var(--surface);
580
- border-radius: 16px 16px 0 0;
581
- width: 100%;
582
- max-width: 500px;
583
- height: 80vh;
584
- display: flex;
585
- flex-direction: column;
586
- overflow: hidden;
587
- transition: background 0.3s;
588
- }
589
- .browser-header {
590
- padding: 16px 16px 12px;
591
- border-bottom: 1px solid var(--border);
592
- display: flex;
593
- align-items: center;
594
- justify-content: space-between;
595
- }
596
- .browser-header h3 {
597
- font-size: 17px;
598
- font-weight: 600;
599
- }
600
- .browser-close {
601
- background: none;
602
- border: none;
603
- color: var(--text-dim);
604
- font-size: 24px;
605
- cursor: pointer;
606
- padding: 0 4px;
607
- line-height: 1;
608
- transition: color 0.15s;
609
- }
610
- .browser-close:hover {
611
- color: var(--text);
612
- }
613
- .browser-close:active {
614
- color: var(--text);
615
- }
616
-
617
- .browser-breadcrumb {
618
- padding: 8px 16px;
619
- display: flex;
620
- align-items: center;
621
- gap: 2px;
622
- font-size: 13px;
623
- overflow-x: auto;
624
- white-space: nowrap;
625
- border-bottom: 1px solid var(--border);
626
- flex-shrink: 0;
627
- -webkit-overflow-scrolling: touch;
628
- }
629
- .crumb {
630
- background: none;
631
- border: none;
632
- color: var(--text-dim);
633
- font-size: 13px;
634
- cursor: pointer;
635
- padding: 4px 6px;
636
- border-radius: 4px;
637
- flex-shrink: 0;
638
- transition:
639
- background 0.15s,
640
- color 0.15s;
641
- }
642
- .crumb:active,
643
- .crumb:hover {
644
- background: var(--border);
645
- color: var(--text);
646
- }
647
- .crumb.current {
648
- color: var(--text);
649
- font-weight: 600;
650
- }
651
- .crumb-sep {
652
- color: var(--border-subtle);
653
- flex-shrink: 0;
654
- }
655
-
656
- .browser-list {
657
- flex: 1;
658
- overflow-y: auto;
659
- padding: 4px 0;
660
- -webkit-overflow-scrolling: touch;
661
- }
662
- .browser-empty {
663
- text-align: center;
664
- padding: 40px 20px;
665
- color: var(--text-muted);
666
- font-size: 14px;
667
- }
668
- .folder-item {
669
- display: flex;
670
- align-items: center;
671
- gap: 12px;
672
- padding: 12px 16px;
673
- cursor: pointer;
674
- border-bottom: 1px solid rgba(60, 60, 60, 0.5);
675
- transition: background 0.1s;
676
- -webkit-tap-highlight-color: transparent;
677
- }
678
- .folder-item:active {
679
- background: rgba(0, 120, 212, 0.2);
680
- }
681
- .folder-item:hover {
682
- background: rgba(0, 120, 212, 0.1);
683
- }
684
- .folder-icon {
685
- font-size: 22px;
686
- flex-shrink: 0;
687
- width: 28px;
688
- text-align: center;
689
- }
690
- .folder-name {
691
- font-size: 15px;
692
- color: var(--text);
693
- flex: 1;
694
- overflow: hidden;
695
- text-overflow: ellipsis;
696
- white-space: nowrap;
697
- }
698
- .folder-arrow {
699
- color: var(--border-subtle);
700
- font-size: 18px;
701
- flex-shrink: 0;
702
- }
703
-
704
- .browser-footer {
705
- padding: 12px 16px calc(env(safe-area-inset-bottom, 8px) + 8px);
706
- border-top: 1px solid var(--border);
707
- display: flex;
708
- flex-direction: column;
709
- gap: 8px;
710
- }
711
- .browser-current-path {
712
- font-size: 12px;
713
- color: var(--text-dim);
714
- overflow: hidden;
715
- text-overflow: ellipsis;
716
- white-space: nowrap;
717
- }
718
- .browser-select-btn {
719
- width: 100%;
720
- padding: 12px;
721
- background: var(--accent);
722
- color: #ffffff;
723
- border: none;
724
- border-radius: 10px;
725
- font-size: 16px;
726
- font-weight: 600;
727
- cursor: pointer;
728
- transition:
729
- background 0.15s,
730
- transform 0.1s;
731
- }
732
- .browser-select-btn:hover {
733
- background: var(--accent-hover);
734
- }
735
- .browser-select-btn:active {
736
- background: var(--accent-active);
737
- transform: scale(0.98);
738
- }
739
-
740
- .color-picker {
741
- display: flex;
742
- gap: 8px;
743
- padding: 6px 0;
744
- flex-wrap: wrap;
745
- }
746
- .color-swatch {
747
- width: 32px;
748
- height: 32px;
749
- border-radius: 50%;
750
- border: 3px solid transparent;
751
- cursor: pointer;
752
- transition:
753
- border-color 0.15s,
754
- transform 0.1s;
755
- -webkit-tap-highlight-color: transparent;
756
- padding: 0;
757
- outline: none;
758
- }
759
- .color-swatch:hover {
760
- transform: scale(1.1);
761
- }
762
- .color-swatch.selected {
763
- border-color: var(--text);
764
- transform: scale(1.15);
765
- }
766
- </style>
767
- </head>
768
- <body>
769
- <div class="header">
770
- <h1>📡 Term<span>Beam</span></h1>
771
- <p>
772
- Beam your terminal to any device ·
773
- <span id="version" style="color: var(--accent)"></span>
774
- </p>
775
- <button class="header-btn" id="share-btn" style="right: 96px; top: 16px" title="Share link">
776
- <svg
777
- width="16"
778
- height="16"
779
- viewBox="0 0 24 24"
780
- fill="none"
781
- stroke="currentColor"
782
- stroke-width="2"
783
- stroke-linecap="round"
784
- stroke-linejoin="round"
785
- >
786
- <circle cx="18" cy="5" r="3" />
787
- <circle cx="6" cy="12" r="3" />
788
- <circle cx="18" cy="19" r="3" />
789
- <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
790
- <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
791
- </svg>
792
- </button>
793
- <button
794
- class="header-btn"
795
- id="refresh-btn"
796
- style="right: 56px; top: 16px"
797
- title="Refresh app"
798
- >
799
- <svg
800
- width="16"
801
- height="16"
802
- viewBox="0 0 24 24"
803
- fill="none"
804
- stroke="currentColor"
805
- stroke-width="2"
806
- stroke-linecap="round"
807
- stroke-linejoin="round"
808
- >
809
- <polyline points="23 4 23 10 17 10" />
810
- <polyline points="1 20 1 14 7 14" />
811
- <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
812
- </svg>
813
- </button>
814
- <div class="theme-wrap" id="theme-wrap">
815
- <button class="theme-toggle" id="theme-toggle" title="Switch theme">
816
- <svg
817
- width="16"
818
- height="16"
819
- viewBox="0 0 24 24"
820
- fill="none"
821
- stroke="currentColor"
822
- stroke-width="2"
823
- stroke-linecap="round"
824
- stroke-linejoin="round"
825
- >
826
- <circle cx="13.5" cy="6.5" r=".5" fill="currentColor" />
827
- <circle cx="17.5" cy="10.5" r=".5" fill="currentColor" />
828
- <circle cx="8.5" cy="7.5" r=".5" fill="currentColor" />
829
- <circle cx="6.5" cy="12.5" r=".5" fill="currentColor" />
830
- <path
831
- d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"
832
- />
833
- </svg>
834
- </button>
835
- <div class="theme-picker" id="theme-picker">
836
- <div class="theme-option" data-theme-option="dark">
837
- <span class="theme-swatch" style="background: #1e1e1e"></span>Dark
838
- </div>
839
- <div class="theme-option" data-theme-option="light">
840
- <span class="theme-swatch" style="background: #ffffff"></span>Light
841
- </div>
842
- <div class="theme-option" data-theme-option="monokai">
843
- <span class="theme-swatch" style="background: #272822"></span>Monokai
844
- </div>
845
- <div class="theme-option" data-theme-option="solarized-dark">
846
- <span class="theme-swatch" style="background: #002b36"></span>Solarized Dark
847
- </div>
848
- <div class="theme-option" data-theme-option="solarized-light">
849
- <span class="theme-swatch" style="background: #fdf6e3"></span>Solarized Light
850
- </div>
851
- <div class="theme-option" data-theme-option="nord">
852
- <span class="theme-swatch" style="background: #2e3440"></span>Nord
853
- </div>
854
- <div class="theme-option" data-theme-option="dracula">
855
- <span class="theme-swatch" style="background: #282a36"></span>Dracula
856
- </div>
857
- <div class="theme-option" data-theme-option="github-dark">
858
- <span class="theme-swatch" style="background: #0d1117"></span>GitHub Dark
859
- </div>
860
- <div class="theme-option" data-theme-option="one-dark">
861
- <span class="theme-swatch" style="background: #282c34"></span>One Dark
862
- </div>
863
- <div class="theme-option" data-theme-option="catppuccin">
864
- <span class="theme-swatch" style="background: #1e1e2e"></span>Catppuccin
865
- </div>
866
- <div class="theme-option" data-theme-option="gruvbox">
867
- <span class="theme-swatch" style="background: #282828"></span>Gruvbox
868
- </div>
869
- <div class="theme-option" data-theme-option="night-owl">
870
- <span class="theme-swatch" style="background: #011627"></span>Night Owl
871
- </div>
872
- </div>
873
- </div>
874
- </div>
875
-
876
- <div class="update-banner" id="update-banner">
877
- <div class="update-banner-text">
878
- <strong>Update available:</strong> <span id="update-versions"></span><br />
879
- <span id="update-command-text"
880
- >Run: <code id="update-command">npm install -g termbeam@latest</code></span
881
- >
882
- </div>
883
- <button
884
- class="update-banner-dismiss"
885
- id="update-dismiss"
886
- title="Dismiss"
887
- aria-label="Dismiss update notification"
888
- >
889
- &times;
890
- </button>
891
- </div>
892
-
893
- <div class="sessions-list" id="sessions-list"></div>
894
- <button class="new-session" id="new-session-btn">+ New Session</button>
895
-
896
- <div class="modal-overlay" id="modal">
897
- <div class="modal">
898
- <h2>New Session</h2>
899
- <label for="sess-name">Name</label>
900
- <input type="text" id="sess-name" placeholder="My Session" />
901
- <label for="sess-shell">Shell</label>
902
- <select id="sess-shell">
903
- <option value="">Loading shells…</option>
904
- </select>
905
- <label for="sess-cmd"
906
- >Initial Command
907
- <span style="color: var(--text-muted); font-weight: normal">(optional)</span></label
908
- >
909
- <input type="text" id="sess-cmd" placeholder="e.g. copilot, htop, vim" />
910
- <label>Color</label>
911
- <div class="color-picker" id="color-picker">
912
- <button
913
- type="button"
914
- class="color-swatch selected"
915
- data-color="#4a9eff"
916
- style="background: #4a9eff"
917
- title="Blue"
918
- ></button>
919
- <button
920
- type="button"
921
- class="color-swatch"
922
- data-color="#4ade80"
923
- style="background: #4ade80"
924
- title="Green"
925
- ></button>
926
- <button
927
- type="button"
928
- class="color-swatch"
929
- data-color="#fbbf24"
930
- style="background: #fbbf24"
931
- title="Amber"
932
- ></button>
933
- <button
934
- type="button"
935
- class="color-swatch"
936
- data-color="#c084fc"
937
- style="background: #c084fc"
938
- title="Purple"
939
- ></button>
940
- <button
941
- type="button"
942
- class="color-swatch"
943
- data-color="#f87171"
944
- style="background: #f87171"
945
- title="Red"
946
- ></button>
947
- <button
948
- type="button"
949
- class="color-swatch"
950
- data-color="#22d3ee"
951
- style="background: #22d3ee"
952
- title="Cyan"
953
- ></button>
954
- <button
955
- type="button"
956
- class="color-swatch"
957
- data-color="#fb923c"
958
- style="background: #fb923c"
959
- title="Orange"
960
- ></button>
961
- <button
962
- type="button"
963
- class="color-swatch"
964
- data-color="#f472b6"
965
- style="background: #f472b6"
966
- title="Pink"
967
- ></button>
968
- </div>
969
- <label for="sess-cwd">Working Directory</label>
970
- <div class="cwd-picker">
971
- <input type="text" id="sess-cwd" placeholder="Uses server default" />
972
- <button type="button" class="cwd-browse-btn" id="browse-btn" title="Browse folders">
973
- <svg
974
- width="18"
975
- height="18"
976
- viewBox="0 0 24 24"
977
- fill="none"
978
- stroke="currentColor"
979
- stroke-width="2"
980
- stroke-linecap="round"
981
- stroke-linejoin="round"
982
- >
983
- <path
984
- d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
985
- />
986
- </svg>
987
- </button>
988
- </div>
989
- <div class="modal-actions">
990
- <button class="btn-cancel" id="modal-cancel">Cancel</button>
991
- <button class="btn-create" id="modal-create">Create</button>
992
- </div>
993
- </div>
994
- </div>
995
-
996
- <!-- Folder browser -->
997
- <div class="browser-overlay" id="browser-overlay">
998
- <div class="browser-sheet">
999
- <div class="browser-header">
1000
- <h3>
1001
- <svg
1002
- width="18"
1003
- height="18"
1004
- viewBox="0 0 24 24"
1005
- fill="none"
1006
- stroke="currentColor"
1007
- stroke-width="2"
1008
- stroke-linecap="round"
1009
- stroke-linejoin="round"
1010
- style="vertical-align: -3px; margin-right: 6px"
1011
- >
1012
- <path
1013
- d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
1014
- /></svg
1015
- >Choose Folder
1016
- </h3>
1017
- <button class="browser-close" id="browser-close">×</button>
1018
- </div>
1019
- <div class="browser-breadcrumb" id="browser-breadcrumb"></div>
1020
- <div class="browser-list" id="browser-list"></div>
1021
- <div class="browser-footer">
1022
- <div class="browser-current-path" id="browser-path">/</div>
1023
- <button class="browser-select-btn" id="browser-select">Select This Folder</button>
1024
- </div>
1025
- </div>
1026
- </div>
1027
-
1028
- <script src="/js/shared.js"></script>
1029
- <script src="/js/themes.js"></script>
1030
- <script>
1031
- // Close theme picker when option is selected (index-specific behavior)
1032
- document.querySelectorAll('.theme-option').forEach((el) => {
1033
- el.addEventListener('click', () => {
1034
- document.getElementById('theme-picker').classList.remove('open');
1035
- });
1036
- });
1037
-
1038
- const listEl = document.getElementById('sessions-list');
1039
- const modal = document.getElementById('modal');
1040
-
1041
- // Update notification
1042
- (async function checkUpdate() {
1043
- if (sessionStorage.getItem('update-dismissed')) return;
1044
- try {
1045
- const res = await fetch('/api/update-check');
1046
- if (!res.ok) return;
1047
- const info = await res.json();
1048
- if (info.updateAvailable && info.latest) {
1049
- const banner = document.getElementById('update-banner');
1050
- document.getElementById('update-versions').textContent =
1051
- 'v' + info.current + ' \u2192 v' + info.latest;
1052
- if (info.command) {
1053
- const cmdEl = document.getElementById('update-command');
1054
- cmdEl.textContent = info.command;
1055
- if (info.method === 'npx') {
1056
- document.getElementById('update-command-text').textContent = 'Next time, run: ';
1057
- document.getElementById('update-command-text').appendChild(cmdEl);
1058
- }
1059
- }
1060
- banner.classList.add('visible');
1061
- }
1062
- } catch {
1063
- // Silent — update check is non-critical
1064
- }
1065
- })();
1066
- document.getElementById('update-dismiss').addEventListener('click', () => {
1067
- document.getElementById('update-banner').classList.remove('visible');
1068
- sessionStorage.setItem('update-dismissed', '1');
1069
- });
1070
-
1071
- function getActivityLabel(ts) {
1072
- if (!ts) return '';
1073
- const diff = (Date.now() - ts) / 1000;
1074
- if (diff < 10) return 'Active now';
1075
- if (diff < 60) return Math.floor(diff) + 's ago';
1076
- if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
1077
- return Math.floor(diff / 3600) + 'h ago';
1078
- }
1079
-
1080
- async function loadSessions() {
1081
- try {
1082
- const res = await fetch('/api/sessions');
1083
- if (!res.ok) {
1084
- console.error(`Failed to load sessions: ${res.status}`);
1085
- return;
1086
- }
1087
- const sessions = await res.json();
1088
-
1089
- if (sessions.length === 0) {
1090
- listEl.innerHTML = '<div class="empty-state">No active sessions</div>';
1091
- return;
1092
- }
1093
-
1094
- listEl.innerHTML = sessions
1095
- .map(
1096
- (s) => `
1097
- <div class="swipe-wrap" data-session-id="${esc(s.id)}">
1098
- <div class="swipe-delete">
1099
- <button data-delete-id="${esc(s.id)}"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg><span>Delete</span></button>
1100
- </div>
1101
- <div class="session-card" data-nav-id="${esc(s.id)}">
1102
- <div class="top">
1103
- <div class="name"><span class="dot" data-color="${esc(s.color || '')}"></span>${esc(s.name)}</div>
1104
- <span class="pid">PID ${s.pid}</span>
1105
- </div>
1106
- <div class="details">
1107
- <span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> ${esc(s.cwd)}</span>
1108
- <span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg> ${esc(s.shell)}</span>
1109
- <span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg> ${s.clients} connected</span>
1110
- <span title="Last activity">${getActivityLabel(s.lastActivity)}</span>
1111
- </div>
1112
- ${
1113
- s.git
1114
- ? `<div class="git-info">
1115
- <span class="git-badge"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg> ${esc(s.git.branch || 'detached')}</span>
1116
- ${s.git.provider ? `<span class="git-badge">${esc(s.git.provider)}</span>` : ''}
1117
- ${s.git.repoName ? `<span class="git-badge">${esc(s.git.repoName)}</span>` : ''}
1118
- ${s.git.status ? `<span class="git-badge ${s.git.status.clean ? 'git-status-clean' : 'git-status-dirty'}">${s.git.status.clean ? '✓ clean' : esc(s.git.status.summary)}</span>` : ''}
1119
- </div>`
1120
- : ''
1121
- }
1122
- <button class="connect-btn">Connect →</button>
1123
- </div>
1124
- </div>
1125
- `,
1126
- )
1127
- .join('');
1128
-
1129
- // Attach swipe handlers and click handlers after rendering
1130
- listEl.querySelectorAll('.swipe-wrap').forEach(initSwipe);
1131
- listEl.querySelectorAll('[data-delete-id]').forEach((btn) => {
1132
- btn.addEventListener('click', (e) => deleteSession(btn.dataset.deleteId, e));
1133
- });
1134
- listEl.querySelectorAll('[data-nav-id]').forEach((card) => {
1135
- card.addEventListener('click', () => {
1136
- location.href = '/terminal?id=' + encodeURIComponent(card.dataset.navId);
1137
- });
1138
- });
1139
- listEl.querySelectorAll('.dot[data-color]').forEach((dot) => {
1140
- dot.style.background = dot.dataset.color || 'var(--success)';
1141
- });
1142
- } catch (err) {
1143
- console.error('Failed to load sessions:', err);
1144
- }
1145
- }
1146
-
1147
- document.getElementById('new-session-btn').addEventListener('click', () => {
1148
- loadShells();
1149
- modal.classList.add('visible');
1150
- });
1151
- document.getElementById('modal-cancel').addEventListener('click', () => {
1152
- modal.classList.remove('visible');
1153
- });
1154
- modal.addEventListener('click', (e) => {
1155
- if (e.target === modal) modal.classList.remove('visible');
1156
- });
1157
-
1158
- // Color picker
1159
- document.getElementById('color-picker').addEventListener('click', (e) => {
1160
- const swatch = e.target.closest('.color-swatch');
1161
- if (!swatch) return;
1162
- document
1163
- .querySelectorAll('#color-picker .color-swatch')
1164
- .forEach((s) => s.classList.remove('selected'));
1165
- swatch.classList.add('selected');
1166
- });
1167
-
1168
- document.getElementById('modal-create').addEventListener('click', async () => {
1169
- const name = document.getElementById('sess-name').value.trim();
1170
- const shell = document.getElementById('sess-shell').value.trim();
1171
- const cwd = document.getElementById('sess-cwd').value.trim();
1172
- const initialCommand = document.getElementById('sess-cmd').value.trim();
1173
-
1174
- const colorEl = document.querySelector('#color-picker .color-swatch.selected');
1175
- const color = colorEl ? colorEl.dataset.color : null;
1176
- const body = {};
1177
- if (name) body.name = name;
1178
- if (shell) body.shell = shell;
1179
- if (cwd) body.cwd = cwd;
1180
- if (initialCommand) body.initialCommand = initialCommand;
1181
- if (color) body.color = color;
1182
-
1183
- try {
1184
- const res = await fetch('/api/sessions', {
1185
- method: 'POST',
1186
- headers: { 'Content-Type': 'application/json' },
1187
- body: JSON.stringify(body),
1188
- });
1189
- if (!res.ok) {
1190
- console.error(`Failed to create session: ${res.status}`);
1191
- return;
1192
- }
1193
- const data = await res.json();
1194
- location.href = data.url;
1195
- } catch (err) {
1196
- console.error('Failed to create session:', err);
1197
- }
1198
- });
1199
-
1200
- // --- Shell detection ---
1201
- let shellsLoaded = false;
1202
- async function loadShells() {
1203
- if (shellsLoaded) return;
1204
- const shellSelect = document.getElementById('sess-shell');
1205
- try {
1206
- const res = await fetch('/api/shells');
1207
- if (!res.ok) throw new Error(`Failed to load shells: ${res.status}`);
1208
- const data = await res.json();
1209
- if (data.cwd) {
1210
- document.getElementById('sess-cwd').placeholder = data.cwd;
1211
- hubServerCwd = data.cwd;
1212
- }
1213
- shellSelect.innerHTML = '';
1214
- for (const s of data.shells) {
1215
- const opt = document.createElement('option');
1216
- opt.value = s.cmd;
1217
- opt.textContent = `${s.name} (${s.cmd})`;
1218
- if (s.cmd === data.default || s.path === data.default) {
1219
- opt.selected = true;
1220
- }
1221
- shellSelect.appendChild(opt);
1222
- }
1223
- shellsLoaded = true;
1224
- } catch {
1225
- shellSelect.innerHTML = '<option value="">Could not detect shells</option>';
1226
- }
1227
- }
1228
-
1229
- // --- Swipe to delete ---
1230
- async function deleteSession(id, e) {
1231
- e.stopPropagation();
1232
- const wrap = document.querySelector(`.swipe-wrap[data-session-id="${id}"]`);
1233
- try {
1234
- await fetch(`/api/sessions/${id}`, { method: 'DELETE' });
1235
- if (wrap) {
1236
- wrap.style.transition = 'opacity 0.25s, max-height 0.3s ease 0.1s';
1237
- wrap.style.opacity = '0';
1238
- wrap.style.maxHeight = wrap.offsetHeight + 'px';
1239
- requestAnimationFrame(() => {
1240
- wrap.style.maxHeight = '0';
1241
- wrap.style.overflow = 'hidden';
1242
- });
1243
- setTimeout(() => loadSessions(), 350);
1244
- } else {
1245
- loadSessions();
1246
- }
1247
- } catch {
1248
- loadSessions();
1249
- }
1250
- }
1251
-
1252
- function initSwipe(wrap) {
1253
- const card = wrap.querySelector('.session-card');
1254
- const THRESHOLD = 70;
1255
- let startX = 0,
1256
- currentX = 0,
1257
- swiping = false,
1258
- swiped = false;
1259
-
1260
- wrap.addEventListener(
1261
- 'touchstart',
1262
- (e) => {
1263
- startX = e.touches[0].clientX;
1264
- currentX = 0;
1265
- swiping = true;
1266
- card.style.transition = 'none';
1267
- },
1268
- { passive: true },
1269
- );
1270
-
1271
- wrap.addEventListener(
1272
- 'touchmove',
1273
- (e) => {
1274
- if (!swiping) return;
1275
- const dx = e.touches[0].clientX - startX;
1276
- currentX = Math.min(0, Math.max(-THRESHOLD, dx));
1277
- card.style.transform = `translateX(${currentX}px)`;
1278
- },
1279
- { passive: true },
1280
- );
1281
-
1282
- wrap.addEventListener('touchend', () => {
1283
- swiping = false;
1284
- card.style.transition = 'transform 0.2s ease';
1285
- if (currentX < -THRESHOLD / 2) {
1286
- card.style.transform = `translateX(-${THRESHOLD}px)`;
1287
- swiped = true;
1288
- } else {
1289
- card.style.transform = 'translateX(0)';
1290
- swiped = false;
1291
- }
1292
- });
1293
-
1294
- // Tap elsewhere to close
1295
- document.addEventListener(
1296
- 'touchstart',
1297
- (e) => {
1298
- if (swiped && !wrap.contains(e.target)) {
1299
- card.style.transition = 'transform 0.2s ease';
1300
- card.style.transform = 'translateX(0)';
1301
- swiped = false;
1302
- }
1303
- },
1304
- { passive: true },
1305
- );
1306
-
1307
- // Block card click when swiped open
1308
- card.addEventListener(
1309
- 'click',
1310
- (e) => {
1311
- if (swiped) {
1312
- e.preventDefault();
1313
- e.stopPropagation();
1314
- }
1315
- },
1316
- true,
1317
- );
1318
- }
1319
-
1320
- // --- Folder Browser ---
1321
- const cwdInput = document.getElementById('sess-cwd');
1322
- const browserOverlay = document.getElementById('browser-overlay');
1323
- const browserList = document.getElementById('browser-list');
1324
- const browserBreadcrumb = document.getElementById('browser-breadcrumb');
1325
- const browserPath = document.getElementById('browser-path');
1326
- let currentBrowsePath = '/';
1327
- let hubServerCwd = '/';
1328
-
1329
- document.getElementById('browse-btn').addEventListener('click', async () => {
1330
- if (hubServerCwd === '/') {
1331
- try {
1332
- const data = await fetch('/api/shells').then((r) => {
1333
- if (!r.ok) throw new Error(`${r.status}`);
1334
- return r.json();
1335
- });
1336
- if (data.cwd) hubServerCwd = data.cwd;
1337
- } catch {}
1338
- }
1339
- const initial = cwdInput.value.trim() || hubServerCwd;
1340
- navigateTo(initial);
1341
- browserOverlay.classList.add('visible');
1342
- });
1343
-
1344
- document.getElementById('browser-close').addEventListener('click', () => {
1345
- browserOverlay.classList.remove('visible');
1346
- });
1347
- browserOverlay.addEventListener('click', (e) => {
1348
- if (e.target === browserOverlay) browserOverlay.classList.remove('visible');
1349
- });
1350
-
1351
- document.getElementById('browser-select').addEventListener('click', () => {
1352
- cwdInput.value = currentBrowsePath;
1353
- browserOverlay.classList.remove('visible');
1354
- });
1355
-
1356
- async function navigateTo(dir) {
1357
- currentBrowsePath = dir;
1358
- browserPath.textContent = dir;
1359
- renderBreadcrumb(dir);
1360
- browserList.innerHTML = '<div class="browser-empty">Loading…</div>';
1361
-
1362
- try {
1363
- const res = await fetch(`/api/dirs?q=${encodeURIComponent(dir + '/')}`);
1364
- if (!res.ok) throw new Error(`Failed to load directories: ${res.status}`);
1365
- const data = await res.json();
1366
- let items = '';
1367
- // Add parent (..) entry unless at root
1368
- const parent =
1369
- dir.replace(/[/\\][^/\\]+$/, '') ||
1370
- (dir.includes('\\') ? dir.match(/^[A-Za-z]:\\/)?.[0] : '/');
1371
- if (parent && parent !== dir) {
1372
- items += `<div class="folder-item" data-path="${esc(parent)}">
1373
- <span class="folder-icon">📁</span>
1374
- <span class="folder-name">..</span>
1375
- <span class="folder-arrow">›</span>
1376
- </div>`;
1377
- }
1378
- items += data.dirs
1379
- .map((d) => {
1380
- const name = d.split(/[/\\]/).pop();
1381
- return `<div class="folder-item" data-path="${esc(d)}">
1382
- <span class="folder-icon">📁</span>
1383
- <span class="folder-name">${esc(name)}</span>
1384
- <span class="folder-arrow">›</span>
1385
- </div>`;
1386
- })
1387
- .join('');
1388
-
1389
- browserList.innerHTML = items || '<div class="browser-empty">No subfolders</div>';
1390
- browserList.querySelectorAll('.folder-item').forEach((el) => {
1391
- el.addEventListener('click', () => navigateTo(el.dataset.path));
1392
- });
1393
- browserList.scrollTop = 0;
1394
- } catch {
1395
- browserList.innerHTML = '<div class="browser-empty">Error loading folders</div>';
1396
- }
1397
- }
1398
-
1399
- function renderBreadcrumb(dir) {
1400
- const sep = dir.includes('\\') ? '\\' : '/';
1401
- const parts = dir.split(/[/\\]/).filter(Boolean);
1402
- const isWindows = /^[A-Za-z]:/.test(dir);
1403
- let html = isWindows ? '' : `<button class="crumb" data-path="/">/</button>`;
1404
- let accumulated = isWindows ? '' : '';
1405
- parts.forEach((part, i) => {
1406
- accumulated += (i === 0 && isWindows ? '' : sep) + part;
1407
- const isCurrent = i === parts.length - 1;
1408
- if (i > 0 || isWindows) html += `<span class="crumb-sep">›</span>`;
1409
- html += `<button class="crumb${isCurrent ? ' current' : ''}" data-path="${esc(accumulated)}">${esc(part)}</button>`;
1410
- });
1411
- browserBreadcrumb.innerHTML = html;
1412
- browserBreadcrumb.querySelectorAll('.crumb').forEach((el) => {
1413
- el.addEventListener('click', () => navigateTo(el.dataset.path));
1414
- });
1415
- // Scroll breadcrumb to end
1416
- browserBreadcrumb.scrollLeft = browserBreadcrumb.scrollWidth;
1417
- }
1418
-
1419
- // Fetch version
1420
- fetch('/api/version')
1421
- .then((r) => {
1422
- if (!r.ok) throw new Error(`${r.status}`);
1423
- return r.json();
1424
- })
1425
- .then((d) => {
1426
- document.getElementById('version').textContent = 'v' + d.version;
1427
- })
1428
- .catch(() => {});
1429
-
1430
- loadSessions();
1431
- loadShells();
1432
- setInterval(loadSessions, 3000);
1433
-
1434
- // Share button
1435
- function showShareToast(msg, duration) {
1436
- const toast = document.createElement('div');
1437
- toast.textContent = msg;
1438
- toast.style.cssText =
1439
- 'position:fixed;top:16px;left:50%;transform:translateX(-50%);background:var(--surface);color:var(--text);border:1px solid var(--border);padding:6px 16px;border-radius:8px;font-size:13px;font-weight:600;z-index:200;';
1440
- document.body.appendChild(toast);
1441
- setTimeout(() => toast.remove(), duration || 1500);
1442
- }
1443
-
1444
- function showShareUrlPrompt(url) {
1445
- const overlay = document.createElement('div');
1446
- overlay.style.cssText =
1447
- 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:300;display:flex;align-items:center;justify-content:center;';
1448
- const box = document.createElement('div');
1449
- box.style.cssText =
1450
- 'background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px;max-width:90vw;width:360px;text-align:center;';
1451
- box.innerHTML =
1452
- '<div style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:12px;">Copy this link</div>';
1453
- const input = document.createElement('input');
1454
- input.type = 'text';
1455
- input.readOnly = true;
1456
- input.value = url;
1457
- input.style.cssText =
1458
- 'width:100%;box-sizing:border-box;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:13px;margin-bottom:12px;';
1459
- box.appendChild(input);
1460
- const btn = document.createElement('button');
1461
- btn.textContent = 'Close';
1462
- btn.style.cssText =
1463
- 'padding:6px 20px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-size:13px;font-weight:600;cursor:pointer;';
1464
- btn.onclick = () => overlay.remove();
1465
- box.appendChild(btn);
1466
- overlay.appendChild(box);
1467
- overlay.addEventListener('click', (e) => {
1468
- if (e.target === overlay) overlay.remove();
1469
- });
1470
- document.body.appendChild(overlay);
1471
- input.focus();
1472
- input.select();
1473
- }
1474
-
1475
- document.getElementById('share-btn').addEventListener('click', async () => {
1476
- const urlPromise = fetch('/api/share-token')
1477
- .then((r) => (r.ok ? r.json() : null))
1478
- .then((data) => (data && data.url) || location.href)
1479
- .catch(() => location.href);
1480
- // ClipboardItem with a promise preserves user activation across the fetch
1481
- if (navigator.clipboard && typeof ClipboardItem !== 'undefined') {
1482
- try {
1483
- const blobPromise = urlPromise.then((u) => new Blob([u], { type: 'text/plain' }));
1484
- await navigator.clipboard.write([new ClipboardItem({ 'text/plain': blobPromise })]);
1485
- showShareToast('Link copied!');
1486
- return;
1487
- } catch {}
1488
- }
1489
- // Fallback: resolve URL first, then try legacy methods
1490
- const url = await urlPromise;
1491
- if (navigator.clipboard && navigator.clipboard.writeText) {
1492
- try {
1493
- await navigator.clipboard.writeText(url);
1494
- showShareToast('Link copied!');
1495
- return;
1496
- } catch {}
1497
- }
1498
- if (copyToClipboardFallback(url)) {
1499
- showShareToast('Link copied!');
1500
- } else {
1501
- showShareUrlPrompt(url);
1502
- }
1503
- });
1504
-
1505
- // Refresh button: clear SW cache and reload
1506
- document.getElementById('refresh-btn').addEventListener('click', async () => {
1507
- if ('caches' in window) {
1508
- const keys = await caches.keys();
1509
- await Promise.all(keys.map((k) => caches.delete(k)));
1510
- }
1511
- if (navigator.serviceWorker) {
1512
- const reg = await navigator.serviceWorker.getRegistration();
1513
- if (reg) await reg.update();
1514
- }
1515
- location.reload();
1516
- });
1517
-
1518
- registerServiceWorker();
1519
- </script>
1520
- </body>
1521
- </html>