termbeam 1.0.7 → 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.
- package/README.md +13 -12
- package/package.json +1 -1
- package/public/index.html +265 -52
- package/public/terminal.html +1699 -532
- package/src/auth.js +45 -9
- package/src/routes.js +37 -3
- package/src/server.js +9 -3
package/README.md
CHANGED
|
@@ -54,7 +54,8 @@ termbeam --no-password # disable password protection
|
|
|
54
54
|
- **Side panel** (mobile) — slide-out session list with output previews for quick switching
|
|
55
55
|
- **Create sessions anywhere** — new session modal available from both the hub page and the terminal page
|
|
56
56
|
- **Touch scrolling** — swipe to scroll through terminal history
|
|
57
|
-
- **Share button** — share the TermBeam URL via Web Share API, clipboard, or legacy copy fallback (works over HTTP)
|
|
57
|
+
- **Share button** — share the TermBeam URL via Web Share API, clipboard, or legacy copy fallback (works over HTTP); each share gets a fresh auto-login link with a 5-minute share token
|
|
58
|
+
- **QR code auto-login** — scan the QR code to log in automatically without typing the password (share token, 5-minute expiry)
|
|
58
59
|
- **Refresh button** — clear PWA/service worker cache and reload to get the latest version
|
|
59
60
|
- **iPhone PWA safe area** — full support for `viewport-fit=cover` and safe area insets on notched devices
|
|
60
61
|
- **Password auth** with token-based cookies and rate-limited login
|
|
@@ -95,17 +96,17 @@ termbeam --port 8080 # custom port (default: 3456)
|
|
|
95
96
|
termbeam --host 127.0.0.1 # restrict to localhost (default: 0.0.0.0)
|
|
96
97
|
```
|
|
97
98
|
|
|
98
|
-
| Flag | Description
|
|
99
|
-
| --------------------- |
|
|
99
|
+
| Flag | Description | Default |
|
|
100
|
+
| --------------------- | ---------------------------------------------------- | -------------- |
|
|
100
101
|
| `--password <pw>` | Set access password (also accepts `--password=<pw>`) | Auto-generated |
|
|
101
|
-
| `--no-password` | Disable password
|
|
102
|
-
| `--generate-password` | Auto-generate a secure password
|
|
103
|
-
| `--tunnel` | Create an ephemeral devtunnel URL
|
|
104
|
-
| `--no-tunnel` | Disable tunnel (LAN-only)
|
|
105
|
-
| `--persisted-tunnel` | Create a reusable devtunnel URL
|
|
106
|
-
| `--port <port>` | Server port
|
|
107
|
-
| `--host <addr>` | Bind address
|
|
108
|
-
| `--log-level <level>` | Log verbosity (error/warn/info/debug)
|
|
102
|
+
| `--no-password` | Disable password | — |
|
|
103
|
+
| `--generate-password` | Auto-generate a secure password | On |
|
|
104
|
+
| `--tunnel` | Create an ephemeral devtunnel URL | On |
|
|
105
|
+
| `--no-tunnel` | Disable tunnel (LAN-only) | — |
|
|
106
|
+
| `--persisted-tunnel` | Create a reusable devtunnel URL | Off |
|
|
107
|
+
| `--port <port>` | Server port | `3456` |
|
|
108
|
+
| `--host <addr>` | Bind address | `0.0.0.0` |
|
|
109
|
+
| `--log-level <level>` | Log verbosity (error/warn/info/debug) | `info` |
|
|
109
110
|
|
|
110
111
|
Environment variables: `PORT`, `TERMBEAM_PASSWORD`, `TERMBEAM_CWD`, `TERMBEAM_LOG_LEVEL`, `SHELL` (Unix fallback), `COMSPEC` (Windows fallback). See [Configuration docs](https://dorlugasigal.github.io/TermBeam/configuration/).
|
|
111
112
|
|
|
@@ -113,7 +114,7 @@ Environment variables: `PORT`, `TERMBEAM_PASSWORD`, `TERMBEAM_CWD`, `TERMBEAM_LO
|
|
|
113
114
|
|
|
114
115
|
TermBeam auto-generates a password and creates a tunnel by default, so your terminal is protected out of the box. Be aware that the tunnel exposes your terminal to the internet — use `--no-tunnel` for LAN-only access, or `--host 127.0.0.1` to restrict to your machine only.
|
|
115
116
|
|
|
116
|
-
Auth uses secure httpOnly cookies with 24-hour expiry, login is rate-limited to 5 attempts per minute, and security headers (X-Frame-Options, X-Content-Type-Options, etc.) are set on all responses. API clients that can't use cookies can authenticate with an `Authorization: Bearer <password>` header. See the [Security Guide](https://dorlugasigal.github.io/TermBeam/security/) for more.
|
|
117
|
+
Auth uses secure httpOnly cookies with 24-hour expiry, login is rate-limited to 5 attempts per minute, and security headers (X-Frame-Options, X-Content-Type-Options, etc.) are set on all responses. The QR code on startup embeds a share token for password-free login — the token is reusable within its 5-minute validity window, which handles tunnel proxy retries and link preview services. API clients that can't use cookies can authenticate with an `Authorization: Bearer <password>` header. See the [Security Guide](https://dorlugasigal.github.io/TermBeam/security/) for more.
|
|
117
118
|
|
|
118
119
|
## Contributing
|
|
119
120
|
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
10
10
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
11
11
|
<meta name="theme-color" content="#1e1e1e" />
|
|
12
|
-
<meta
|
|
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
|
+
/>
|
|
13
16
|
<link rel="manifest" href="/manifest.json" />
|
|
14
17
|
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
|
15
18
|
<title>TermBeam — Beam Your Terminal to Any Device</title>
|
|
@@ -30,10 +33,10 @@
|
|
|
30
33
|
--danger-hover: #d73a3a;
|
|
31
34
|
--success: #89d185;
|
|
32
35
|
--info: #b0b0b0;
|
|
33
|
-
--shadow: rgba(0,0,0,0.15);
|
|
34
|
-
--overlay-bg: rgba(0,0,0,0.7);
|
|
36
|
+
--shadow: rgba(0, 0, 0, 0.15);
|
|
37
|
+
--overlay-bg: rgba(0, 0, 0, 0.7);
|
|
35
38
|
}
|
|
36
|
-
[data-theme=
|
|
39
|
+
[data-theme='light'] {
|
|
37
40
|
--bg: #ffffff;
|
|
38
41
|
--surface: #f3f3f3;
|
|
39
42
|
--border: #e0e0e0;
|
|
@@ -49,8 +52,8 @@
|
|
|
49
52
|
--danger-hover: #c20000;
|
|
50
53
|
--success: #16825d;
|
|
51
54
|
--info: #616161;
|
|
52
|
-
--shadow: rgba(0,0,0,0.06);
|
|
53
|
-
--overlay-bg: rgba(0,0,0,0.4);
|
|
55
|
+
--shadow: rgba(0, 0, 0, 0.06);
|
|
56
|
+
--overlay-bg: rgba(0, 0, 0, 0.4);
|
|
54
57
|
}
|
|
55
58
|
* {
|
|
56
59
|
margin: 0;
|
|
@@ -64,7 +67,9 @@
|
|
|
64
67
|
background: var(--bg);
|
|
65
68
|
color: var(--text);
|
|
66
69
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
67
|
-
transition:
|
|
70
|
+
transition:
|
|
71
|
+
background 0.3s,
|
|
72
|
+
color 0.3s;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
.header {
|
|
@@ -100,7 +105,10 @@
|
|
|
100
105
|
align-items: center;
|
|
101
106
|
justify-content: center;
|
|
102
107
|
font-size: 16px;
|
|
103
|
-
transition:
|
|
108
|
+
transition:
|
|
109
|
+
color 0.15s,
|
|
110
|
+
border-color 0.15s,
|
|
111
|
+
background 0.15s;
|
|
104
112
|
-webkit-tap-highlight-color: transparent;
|
|
105
113
|
}
|
|
106
114
|
.header-btn:hover {
|
|
@@ -123,7 +131,10 @@
|
|
|
123
131
|
align-items: center;
|
|
124
132
|
justify-content: center;
|
|
125
133
|
font-size: 16px;
|
|
126
|
-
transition:
|
|
134
|
+
transition:
|
|
135
|
+
color 0.15s,
|
|
136
|
+
border-color 0.15s,
|
|
137
|
+
background 0.15s;
|
|
127
138
|
-webkit-tap-highlight-color: transparent;
|
|
128
139
|
}
|
|
129
140
|
.theme-toggle:hover {
|
|
@@ -150,7 +161,10 @@
|
|
|
150
161
|
gap: 8px;
|
|
151
162
|
text-decoration: none;
|
|
152
163
|
color: inherit;
|
|
153
|
-
transition:
|
|
164
|
+
transition:
|
|
165
|
+
transform 0.2s ease,
|
|
166
|
+
border-color 0.15s,
|
|
167
|
+
background 0.3s;
|
|
154
168
|
cursor: pointer;
|
|
155
169
|
-webkit-tap-highlight-color: transparent;
|
|
156
170
|
position: relative;
|
|
@@ -222,7 +236,9 @@
|
|
|
222
236
|
background: var(--bg);
|
|
223
237
|
padding: 2px 8px;
|
|
224
238
|
border-radius: 4px;
|
|
225
|
-
transition:
|
|
239
|
+
transition:
|
|
240
|
+
background 0.3s,
|
|
241
|
+
color 0.3s;
|
|
226
242
|
}
|
|
227
243
|
.session-card .details {
|
|
228
244
|
display: flex;
|
|
@@ -246,7 +262,9 @@
|
|
|
246
262
|
font-size: 14px;
|
|
247
263
|
font-weight: 600;
|
|
248
264
|
cursor: pointer;
|
|
249
|
-
transition:
|
|
265
|
+
transition:
|
|
266
|
+
background 0.15s,
|
|
267
|
+
transform 0.1s;
|
|
250
268
|
}
|
|
251
269
|
.session-card .connect-btn:hover {
|
|
252
270
|
background: var(--accent-hover);
|
|
@@ -271,7 +289,10 @@
|
|
|
271
289
|
cursor: pointer;
|
|
272
290
|
text-align: center;
|
|
273
291
|
z-index: 50;
|
|
274
|
-
transition:
|
|
292
|
+
transition:
|
|
293
|
+
background 0.15s,
|
|
294
|
+
transform 0.1s,
|
|
295
|
+
box-shadow 0.15s;
|
|
275
296
|
box-shadow: 0 2px 8px rgba(0, 120, 212, 0.3);
|
|
276
297
|
}
|
|
277
298
|
.new-session:hover {
|
|
@@ -338,7 +359,10 @@
|
|
|
338
359
|
outline: none;
|
|
339
360
|
-webkit-appearance: none;
|
|
340
361
|
appearance: none;
|
|
341
|
-
transition:
|
|
362
|
+
transition:
|
|
363
|
+
background 0.3s,
|
|
364
|
+
border-color 0.15s,
|
|
365
|
+
color 0.3s;
|
|
342
366
|
}
|
|
343
367
|
.modal select {
|
|
344
368
|
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");
|
|
@@ -364,7 +388,9 @@
|
|
|
364
388
|
font-size: 15px;
|
|
365
389
|
font-weight: 600;
|
|
366
390
|
cursor: pointer;
|
|
367
|
-
transition:
|
|
391
|
+
transition:
|
|
392
|
+
background 0.15s,
|
|
393
|
+
transform 0.1s;
|
|
368
394
|
}
|
|
369
395
|
.modal-actions button:active {
|
|
370
396
|
transform: scale(0.95);
|
|
@@ -403,7 +429,9 @@
|
|
|
403
429
|
flex-shrink: 0;
|
|
404
430
|
display: flex;
|
|
405
431
|
align-items: center;
|
|
406
|
-
transition:
|
|
432
|
+
transition:
|
|
433
|
+
background 0.15s,
|
|
434
|
+
border-color 0.15s;
|
|
407
435
|
}
|
|
408
436
|
.cwd-browse-btn:hover {
|
|
409
437
|
border-color: var(--accent);
|
|
@@ -488,7 +516,9 @@
|
|
|
488
516
|
padding: 4px 6px;
|
|
489
517
|
border-radius: 4px;
|
|
490
518
|
flex-shrink: 0;
|
|
491
|
-
transition:
|
|
519
|
+
transition:
|
|
520
|
+
background 0.15s,
|
|
521
|
+
color 0.15s;
|
|
492
522
|
}
|
|
493
523
|
.crumb:active,
|
|
494
524
|
.crumb:hover {
|
|
@@ -576,7 +606,9 @@
|
|
|
576
606
|
font-size: 16px;
|
|
577
607
|
font-weight: 600;
|
|
578
608
|
cursor: pointer;
|
|
579
|
-
transition:
|
|
609
|
+
transition:
|
|
610
|
+
background 0.15s,
|
|
611
|
+
transform 0.1s;
|
|
580
612
|
}
|
|
581
613
|
.browser-select-btn:hover {
|
|
582
614
|
background: var(--accent-hover);
|
|
@@ -598,7 +630,9 @@
|
|
|
598
630
|
border-radius: 50%;
|
|
599
631
|
border: 3px solid transparent;
|
|
600
632
|
cursor: pointer;
|
|
601
|
-
transition:
|
|
633
|
+
transition:
|
|
634
|
+
border-color 0.15s,
|
|
635
|
+
transform 0.1s;
|
|
602
636
|
-webkit-tap-highlight-color: transparent;
|
|
603
637
|
padding: 0;
|
|
604
638
|
outline: none;
|
|
@@ -615,10 +649,70 @@
|
|
|
615
649
|
<body>
|
|
616
650
|
<div class="header">
|
|
617
651
|
<h1>📡 Term<span>Beam</span></h1>
|
|
618
|
-
<p>
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
<button class="
|
|
652
|
+
<p>
|
|
653
|
+
Beam your terminal to any device · <span id="version" style="color: var(--accent)"></span>
|
|
654
|
+
</p>
|
|
655
|
+
<button class="header-btn" id="share-btn" style="right: 96px; top: 16px" title="Share link">
|
|
656
|
+
<svg
|
|
657
|
+
width="16"
|
|
658
|
+
height="16"
|
|
659
|
+
viewBox="0 0 24 24"
|
|
660
|
+
fill="none"
|
|
661
|
+
stroke="currentColor"
|
|
662
|
+
stroke-width="2"
|
|
663
|
+
stroke-linecap="round"
|
|
664
|
+
stroke-linejoin="round"
|
|
665
|
+
>
|
|
666
|
+
<circle cx="18" cy="5" r="3" />
|
|
667
|
+
<circle cx="6" cy="12" r="3" />
|
|
668
|
+
<circle cx="18" cy="19" r="3" />
|
|
669
|
+
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
|
|
670
|
+
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
|
|
671
|
+
</svg>
|
|
672
|
+
</button>
|
|
673
|
+
<button
|
|
674
|
+
class="header-btn"
|
|
675
|
+
id="refresh-btn"
|
|
676
|
+
style="right: 56px; top: 16px"
|
|
677
|
+
title="Refresh app"
|
|
678
|
+
>
|
|
679
|
+
<svg
|
|
680
|
+
width="16"
|
|
681
|
+
height="16"
|
|
682
|
+
viewBox="0 0 24 24"
|
|
683
|
+
fill="none"
|
|
684
|
+
stroke="currentColor"
|
|
685
|
+
stroke-width="2"
|
|
686
|
+
stroke-linecap="round"
|
|
687
|
+
stroke-linejoin="round"
|
|
688
|
+
>
|
|
689
|
+
<polyline points="23 4 23 10 17 10" />
|
|
690
|
+
<polyline points="1 20 1 14 7 14" />
|
|
691
|
+
<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" />
|
|
692
|
+
</svg>
|
|
693
|
+
</button>
|
|
694
|
+
<button class="theme-toggle" id="theme-toggle" title="Toggle theme">
|
|
695
|
+
<svg
|
|
696
|
+
width="16"
|
|
697
|
+
height="16"
|
|
698
|
+
viewBox="0 0 24 24"
|
|
699
|
+
fill="none"
|
|
700
|
+
stroke="currentColor"
|
|
701
|
+
stroke-width="2"
|
|
702
|
+
stroke-linecap="round"
|
|
703
|
+
stroke-linejoin="round"
|
|
704
|
+
>
|
|
705
|
+
<circle cx="12" cy="12" r="5" />
|
|
706
|
+
<line x1="12" y1="1" x2="12" y2="3" />
|
|
707
|
+
<line x1="12" y1="21" x2="12" y2="23" />
|
|
708
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
709
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
710
|
+
<line x1="1" y1="12" x2="3" y2="12" />
|
|
711
|
+
<line x1="21" y1="12" x2="23" y2="12" />
|
|
712
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
713
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
714
|
+
</svg>
|
|
715
|
+
</button>
|
|
622
716
|
</div>
|
|
623
717
|
|
|
624
718
|
<div class="sessions-list" id="sessions-list"></div>
|
|
@@ -633,18 +727,69 @@
|
|
|
633
727
|
<select id="sess-shell">
|
|
634
728
|
<option value="">Loading shells…</option>
|
|
635
729
|
</select>
|
|
636
|
-
<label for="sess-cmd"
|
|
730
|
+
<label for="sess-cmd"
|
|
731
|
+
>Initial Command
|
|
732
|
+
<span style="color: var(--text-muted); font-weight: normal">(optional)</span></label
|
|
733
|
+
>
|
|
637
734
|
<input type="text" id="sess-cmd" placeholder="e.g. copilot, htop, vim" />
|
|
638
735
|
<label>Color</label>
|
|
639
736
|
<div class="color-picker" id="color-picker">
|
|
640
|
-
<button
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
<button
|
|
737
|
+
<button
|
|
738
|
+
type="button"
|
|
739
|
+
class="color-swatch selected"
|
|
740
|
+
data-color="#4a9eff"
|
|
741
|
+
style="background: #4a9eff"
|
|
742
|
+
title="Blue"
|
|
743
|
+
></button>
|
|
744
|
+
<button
|
|
745
|
+
type="button"
|
|
746
|
+
class="color-swatch"
|
|
747
|
+
data-color="#4ade80"
|
|
748
|
+
style="background: #4ade80"
|
|
749
|
+
title="Green"
|
|
750
|
+
></button>
|
|
751
|
+
<button
|
|
752
|
+
type="button"
|
|
753
|
+
class="color-swatch"
|
|
754
|
+
data-color="#fbbf24"
|
|
755
|
+
style="background: #fbbf24"
|
|
756
|
+
title="Amber"
|
|
757
|
+
></button>
|
|
758
|
+
<button
|
|
759
|
+
type="button"
|
|
760
|
+
class="color-swatch"
|
|
761
|
+
data-color="#c084fc"
|
|
762
|
+
style="background: #c084fc"
|
|
763
|
+
title="Purple"
|
|
764
|
+
></button>
|
|
765
|
+
<button
|
|
766
|
+
type="button"
|
|
767
|
+
class="color-swatch"
|
|
768
|
+
data-color="#f87171"
|
|
769
|
+
style="background: #f87171"
|
|
770
|
+
title="Red"
|
|
771
|
+
></button>
|
|
772
|
+
<button
|
|
773
|
+
type="button"
|
|
774
|
+
class="color-swatch"
|
|
775
|
+
data-color="#22d3ee"
|
|
776
|
+
style="background: #22d3ee"
|
|
777
|
+
title="Cyan"
|
|
778
|
+
></button>
|
|
779
|
+
<button
|
|
780
|
+
type="button"
|
|
781
|
+
class="color-swatch"
|
|
782
|
+
data-color="#fb923c"
|
|
783
|
+
style="background: #fb923c"
|
|
784
|
+
title="Orange"
|
|
785
|
+
></button>
|
|
786
|
+
<button
|
|
787
|
+
type="button"
|
|
788
|
+
class="color-swatch"
|
|
789
|
+
data-color="#f472b6"
|
|
790
|
+
style="background: #f472b6"
|
|
791
|
+
title="Pink"
|
|
792
|
+
></button>
|
|
648
793
|
</div>
|
|
649
794
|
<label for="sess-cwd">Working Directory</label>
|
|
650
795
|
<div class="cwd-picker">
|
|
@@ -707,15 +852,19 @@
|
|
|
707
852
|
|
|
708
853
|
<script>
|
|
709
854
|
// Theme
|
|
710
|
-
function getTheme() {
|
|
855
|
+
function getTheme() {
|
|
856
|
+
return localStorage.getItem('termbeam-theme') || 'dark';
|
|
857
|
+
}
|
|
711
858
|
function applyTheme(theme) {
|
|
712
859
|
document.documentElement.setAttribute('data-theme', theme);
|
|
713
860
|
document.querySelector('meta[name="theme-color"]').content =
|
|
714
861
|
theme === 'light' ? '#f3f3f3' : '#1e1e1e';
|
|
715
862
|
const btn = document.getElementById('theme-toggle');
|
|
716
|
-
if (btn)
|
|
717
|
-
|
|
718
|
-
|
|
863
|
+
if (btn)
|
|
864
|
+
btn.innerHTML =
|
|
865
|
+
theme === 'light'
|
|
866
|
+
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>'
|
|
867
|
+
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
|
|
719
868
|
localStorage.setItem('termbeam-theme', theme);
|
|
720
869
|
}
|
|
721
870
|
applyTheme(getTheme());
|
|
@@ -771,13 +920,15 @@
|
|
|
771
920
|
|
|
772
921
|
// Attach swipe handlers and click handlers after rendering
|
|
773
922
|
listEl.querySelectorAll('.swipe-wrap').forEach(initSwipe);
|
|
774
|
-
listEl.querySelectorAll('[data-delete-id]').forEach(btn => {
|
|
923
|
+
listEl.querySelectorAll('[data-delete-id]').forEach((btn) => {
|
|
775
924
|
btn.addEventListener('click', (e) => deleteSession(btn.dataset.deleteId, e));
|
|
776
925
|
});
|
|
777
|
-
listEl.querySelectorAll('[data-nav-id]').forEach(card => {
|
|
778
|
-
card.addEventListener('click', () => {
|
|
926
|
+
listEl.querySelectorAll('[data-nav-id]').forEach((card) => {
|
|
927
|
+
card.addEventListener('click', () => {
|
|
928
|
+
location.href = '/terminal?id=' + encodeURIComponent(card.dataset.navId);
|
|
929
|
+
});
|
|
779
930
|
});
|
|
780
|
-
listEl.querySelectorAll('.dot[data-color]').forEach(dot => {
|
|
931
|
+
listEl.querySelectorAll('.dot[data-color]').forEach((dot) => {
|
|
781
932
|
dot.style.background = dot.dataset.color || 'var(--success)';
|
|
782
933
|
});
|
|
783
934
|
}
|
|
@@ -803,7 +954,9 @@
|
|
|
803
954
|
document.getElementById('color-picker').addEventListener('click', (e) => {
|
|
804
955
|
const swatch = e.target.closest('.color-swatch');
|
|
805
956
|
if (!swatch) return;
|
|
806
|
-
document
|
|
957
|
+
document
|
|
958
|
+
.querySelectorAll('#color-picker .color-swatch')
|
|
959
|
+
.forEach((s) => s.classList.remove('selected'));
|
|
807
960
|
swatch.classList.add('selected');
|
|
808
961
|
});
|
|
809
962
|
|
|
@@ -962,7 +1115,7 @@
|
|
|
962
1115
|
document.getElementById('browse-btn').addEventListener('click', async () => {
|
|
963
1116
|
if (hubServerCwd === '/') {
|
|
964
1117
|
try {
|
|
965
|
-
const data = await fetch('/api/shells').then(r => r.json());
|
|
1118
|
+
const data = await fetch('/api/shells').then((r) => r.json());
|
|
966
1119
|
if (data.cwd) hubServerCwd = data.cwd;
|
|
967
1120
|
} catch {}
|
|
968
1121
|
}
|
|
@@ -994,7 +1147,9 @@
|
|
|
994
1147
|
const data = await res.json();
|
|
995
1148
|
let items = '';
|
|
996
1149
|
// Add parent (..) entry unless at root
|
|
997
|
-
const parent =
|
|
1150
|
+
const parent =
|
|
1151
|
+
dir.replace(/[/\\][^/\\]+$/, '') ||
|
|
1152
|
+
(dir.includes('\\') ? dir.match(/^[A-Za-z]:\\/)?.[0] : '/');
|
|
998
1153
|
if (parent && parent !== dir) {
|
|
999
1154
|
items += `<div class="folder-item" data-path="${esc(parent)}">
|
|
1000
1155
|
<span class="folder-icon">📁</span>
|
|
@@ -1061,33 +1216,91 @@
|
|
|
1061
1216
|
ta.value = text;
|
|
1062
1217
|
ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px';
|
|
1063
1218
|
document.body.appendChild(ta);
|
|
1219
|
+
ta.focus();
|
|
1064
1220
|
ta.select();
|
|
1065
|
-
|
|
1221
|
+
let ok = false;
|
|
1222
|
+
try {
|
|
1223
|
+
ok = document.execCommand('copy');
|
|
1224
|
+
} catch {}
|
|
1066
1225
|
document.body.removeChild(ta);
|
|
1226
|
+
return ok;
|
|
1067
1227
|
}
|
|
1068
1228
|
|
|
1069
|
-
function showShareToast(msg) {
|
|
1229
|
+
function showShareToast(msg, duration) {
|
|
1070
1230
|
const toast = document.createElement('div');
|
|
1071
1231
|
toast.textContent = msg;
|
|
1072
|
-
toast.style.cssText =
|
|
1232
|
+
toast.style.cssText =
|
|
1233
|
+
'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;';
|
|
1073
1234
|
document.body.appendChild(toast);
|
|
1074
|
-
setTimeout(() => toast.remove(), 1500);
|
|
1235
|
+
setTimeout(() => toast.remove(), duration || 1500);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function showShareUrlPrompt(url) {
|
|
1239
|
+
const overlay = document.createElement('div');
|
|
1240
|
+
overlay.style.cssText =
|
|
1241
|
+
'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:300;display:flex;align-items:center;justify-content:center;';
|
|
1242
|
+
const box = document.createElement('div');
|
|
1243
|
+
box.style.cssText =
|
|
1244
|
+
'background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px;max-width:90vw;width:360px;text-align:center;';
|
|
1245
|
+
box.innerHTML =
|
|
1246
|
+
'<div style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:12px;">Copy this link</div>';
|
|
1247
|
+
const input = document.createElement('input');
|
|
1248
|
+
input.type = 'text';
|
|
1249
|
+
input.readOnly = true;
|
|
1250
|
+
input.value = url;
|
|
1251
|
+
input.style.cssText =
|
|
1252
|
+
'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;';
|
|
1253
|
+
box.appendChild(input);
|
|
1254
|
+
const btn = document.createElement('button');
|
|
1255
|
+
btn.textContent = 'Close';
|
|
1256
|
+
btn.style.cssText =
|
|
1257
|
+
'padding:6px 20px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-size:13px;font-weight:600;cursor:pointer;';
|
|
1258
|
+
btn.onclick = () => overlay.remove();
|
|
1259
|
+
box.appendChild(btn);
|
|
1260
|
+
overlay.appendChild(box);
|
|
1261
|
+
overlay.addEventListener('click', (e) => {
|
|
1262
|
+
if (e.target === overlay) overlay.remove();
|
|
1263
|
+
});
|
|
1264
|
+
document.body.appendChild(overlay);
|
|
1265
|
+
input.focus();
|
|
1266
|
+
input.select();
|
|
1075
1267
|
}
|
|
1076
1268
|
|
|
1077
1269
|
document.getElementById('share-btn').addEventListener('click', async () => {
|
|
1078
|
-
const
|
|
1270
|
+
const urlPromise = fetch('/api/share-token')
|
|
1271
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
1272
|
+
.then((data) => (data && data.url) || location.href)
|
|
1273
|
+
.catch(() => location.href);
|
|
1274
|
+
// ClipboardItem with a promise preserves user activation across the fetch
|
|
1275
|
+
if (navigator.clipboard && typeof ClipboardItem !== 'undefined') {
|
|
1276
|
+
try {
|
|
1277
|
+
const blobPromise = urlPromise.then((u) => new Blob([u], { type: 'text/plain' }));
|
|
1278
|
+
await navigator.clipboard.write([new ClipboardItem({ 'text/plain': blobPromise })]);
|
|
1279
|
+
showShareToast('Link copied!');
|
|
1280
|
+
return;
|
|
1281
|
+
} catch {}
|
|
1282
|
+
}
|
|
1283
|
+
// Fallback: resolve URL first, then try legacy methods
|
|
1284
|
+
const url = await urlPromise;
|
|
1079
1285
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1080
|
-
try {
|
|
1286
|
+
try {
|
|
1287
|
+
await navigator.clipboard.writeText(url);
|
|
1288
|
+
showShareToast('Link copied!');
|
|
1289
|
+
return;
|
|
1290
|
+
} catch {}
|
|
1291
|
+
}
|
|
1292
|
+
if (copyToClipboardFallback(url)) {
|
|
1293
|
+
showShareToast('Link copied!');
|
|
1294
|
+
} else {
|
|
1295
|
+
showShareUrlPrompt(url);
|
|
1081
1296
|
}
|
|
1082
|
-
copyToClipboardFallback(url);
|
|
1083
|
-
showShareToast('Link copied!');
|
|
1084
1297
|
});
|
|
1085
1298
|
|
|
1086
1299
|
// Refresh button: clear SW cache and reload
|
|
1087
1300
|
document.getElementById('refresh-btn').addEventListener('click', async () => {
|
|
1088
1301
|
if ('caches' in window) {
|
|
1089
1302
|
const keys = await caches.keys();
|
|
1090
|
-
await Promise.all(keys.map(k => caches.delete(k)));
|
|
1303
|
+
await Promise.all(keys.map((k) => caches.delete(k)));
|
|
1091
1304
|
}
|
|
1092
1305
|
if (navigator.serviceWorker) {
|
|
1093
1306
|
const reg = await navigator.serviceWorker.getRegistration();
|