termbeam 1.0.6 → 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 +10 -4
package/public/terminal.html
CHANGED
|
@@ -2,15 +2,24 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta
|
|
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
|
+
/>
|
|
6
9
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
7
10
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
8
11
|
<meta name="theme-color" content="#1e1e1e" />
|
|
9
|
-
<meta
|
|
12
|
+
<meta
|
|
13
|
+
name="description"
|
|
14
|
+
content="TermBeam terminal session — access your terminal remotely from any browser with a mobile-optimized touch interface."
|
|
15
|
+
/>
|
|
10
16
|
<link rel="manifest" href="/manifest.json" />
|
|
11
17
|
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
|
12
18
|
<title>TermBeam — Terminal</title>
|
|
13
|
-
<link
|
|
19
|
+
<link
|
|
20
|
+
rel="stylesheet"
|
|
21
|
+
href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css"
|
|
22
|
+
/>
|
|
14
23
|
<style>
|
|
15
24
|
:root {
|
|
16
25
|
--bg: #1e1e1e;
|
|
@@ -29,10 +38,10 @@
|
|
|
29
38
|
--success: #89d185;
|
|
30
39
|
--key-bg: #2d2d2d;
|
|
31
40
|
--key-border: #404040;
|
|
32
|
-
--key-shadow: rgba(0,0,0,0.4);
|
|
33
|
-
--overlay-bg: rgba(0,0,0,0.85);
|
|
41
|
+
--key-shadow: rgba(0, 0, 0, 0.4);
|
|
42
|
+
--overlay-bg: rgba(0, 0, 0, 0.85);
|
|
34
43
|
}
|
|
35
|
-
[data-theme=
|
|
44
|
+
[data-theme='light'] {
|
|
36
45
|
--bg: #ffffff;
|
|
37
46
|
--surface: #f3f3f3;
|
|
38
47
|
--border: #e0e0e0;
|
|
@@ -49,27 +58,47 @@
|
|
|
49
58
|
--success: #16825d;
|
|
50
59
|
--key-bg: #e8e8e8;
|
|
51
60
|
--key-border: #d0d0d0;
|
|
52
|
-
--key-shadow: rgba(0,0,0,0.08);
|
|
53
|
-
--overlay-bg: rgba(0,0,0,0.5);
|
|
61
|
+
--key-shadow: rgba(0, 0, 0, 0.08);
|
|
62
|
+
--overlay-bg: rgba(0, 0, 0, 0.5);
|
|
54
63
|
}
|
|
55
64
|
@font-face {
|
|
56
65
|
font-family: 'NerdFont';
|
|
57
|
-
src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')
|
|
58
|
-
|
|
66
|
+
src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')
|
|
67
|
+
format('truetype');
|
|
68
|
+
font-weight: normal;
|
|
69
|
+
font-style: normal;
|
|
70
|
+
font-display: swap;
|
|
59
71
|
}
|
|
60
72
|
@font-face {
|
|
61
73
|
font-family: 'NerdFont';
|
|
62
|
-
src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Bold/JetBrainsMonoNerdFont-Bold.ttf')
|
|
63
|
-
|
|
74
|
+
src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Bold/JetBrainsMonoNerdFont-Bold.ttf')
|
|
75
|
+
format('truetype');
|
|
76
|
+
font-weight: bold;
|
|
77
|
+
font-style: normal;
|
|
78
|
+
font-display: swap;
|
|
64
79
|
}
|
|
65
|
-
:root {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
:root {
|
|
81
|
+
--sab: env(safe-area-inset-bottom, 0px);
|
|
82
|
+
}
|
|
83
|
+
* {
|
|
84
|
+
margin: 0;
|
|
85
|
+
padding: 0;
|
|
86
|
+
box-sizing: border-box;
|
|
87
|
+
}
|
|
88
|
+
html,
|
|
89
|
+
body {
|
|
90
|
+
height: 100%;
|
|
91
|
+
width: 100%;
|
|
92
|
+
background: var(--bg);
|
|
93
|
+
color: var(--text);
|
|
69
94
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
70
|
-
overflow: hidden;
|
|
95
|
+
overflow: hidden;
|
|
96
|
+
touch-action: manipulation;
|
|
97
|
+
height: 100dvh;
|
|
71
98
|
overscroll-behavior: none;
|
|
72
|
-
transition:
|
|
99
|
+
transition:
|
|
100
|
+
background 0.3s,
|
|
101
|
+
color 0.3s;
|
|
73
102
|
}
|
|
74
103
|
|
|
75
104
|
/* ===== Top Bar (unified) ===== */
|
|
@@ -79,20 +108,51 @@
|
|
|
79
108
|
align-items: center;
|
|
80
109
|
background: var(--surface);
|
|
81
110
|
border-bottom: 1px solid var(--border);
|
|
82
|
-
padding: 0 calc(4px + env(safe-area-inset-right, 0px)) 0
|
|
111
|
+
padding: 0 calc(4px + env(safe-area-inset-right, 0px)) 0
|
|
112
|
+
calc(4px + env(safe-area-inset-left, 0px));
|
|
83
113
|
padding-top: env(safe-area-inset-top, 0px);
|
|
84
114
|
gap: 2px;
|
|
85
|
-
transition:
|
|
115
|
+
transition:
|
|
116
|
+
background 0.3s,
|
|
117
|
+
border-color 0.3s;
|
|
118
|
+
}
|
|
119
|
+
#top-bar .left {
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 4px;
|
|
123
|
+
flex: 1;
|
|
124
|
+
min-width: 0;
|
|
125
|
+
}
|
|
126
|
+
#top-bar .right {
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 2px;
|
|
130
|
+
flex-shrink: 0;
|
|
86
131
|
}
|
|
87
|
-
#top-bar .left { display: flex; align-items: center; gap: 4px; flex: 1; min-width: 0; }
|
|
88
|
-
#top-bar .right { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
|
|
89
132
|
#status-dot {
|
|
90
|
-
width: 8px;
|
|
91
|
-
|
|
133
|
+
width: 8px;
|
|
134
|
+
height: 8px;
|
|
135
|
+
border-radius: 50%;
|
|
136
|
+
background: var(--danger);
|
|
137
|
+
display: inline-block;
|
|
138
|
+
transition: background 0.3s;
|
|
139
|
+
}
|
|
140
|
+
#status-dot.connected {
|
|
141
|
+
background: var(--success);
|
|
142
|
+
}
|
|
143
|
+
#session-name {
|
|
144
|
+
font-weight: 600;
|
|
145
|
+
font-size: 13px;
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
text-overflow: ellipsis;
|
|
149
|
+
max-width: 120px;
|
|
150
|
+
}
|
|
151
|
+
#status-text {
|
|
152
|
+
color: var(--text-secondary);
|
|
153
|
+
font-size: 11px;
|
|
154
|
+
white-space: nowrap;
|
|
92
155
|
}
|
|
93
|
-
#status-dot.connected { background: var(--success); }
|
|
94
|
-
#session-name { font-weight: 600; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 120px; }
|
|
95
|
-
#status-text { color: var(--text-secondary); font-size: 11px; white-space: nowrap; }
|
|
96
156
|
#tab-list {
|
|
97
157
|
display: flex;
|
|
98
158
|
align-items: center;
|
|
@@ -105,7 +165,9 @@
|
|
|
105
165
|
height: 100%;
|
|
106
166
|
min-width: 0;
|
|
107
167
|
}
|
|
108
|
-
#tab-list::-webkit-scrollbar {
|
|
168
|
+
#tab-list::-webkit-scrollbar {
|
|
169
|
+
display: none;
|
|
170
|
+
}
|
|
109
171
|
.session-tab {
|
|
110
172
|
display: flex;
|
|
111
173
|
align-items: center;
|
|
@@ -122,40 +184,68 @@
|
|
|
122
184
|
white-space: nowrap;
|
|
123
185
|
flex-shrink: 0;
|
|
124
186
|
height: 30px;
|
|
125
|
-
transition:
|
|
187
|
+
transition:
|
|
188
|
+
background 0.15s,
|
|
189
|
+
color 0.15s;
|
|
126
190
|
-webkit-tap-highlight-color: transparent;
|
|
127
191
|
user-select: none;
|
|
128
192
|
}
|
|
129
|
-
.session-tab:hover {
|
|
193
|
+
.session-tab:hover {
|
|
194
|
+
background: var(--border);
|
|
195
|
+
color: var(--text);
|
|
196
|
+
}
|
|
130
197
|
.session-tab.active {
|
|
131
198
|
background: var(--bg);
|
|
132
199
|
color: var(--text);
|
|
133
200
|
font-weight: 600;
|
|
134
201
|
}
|
|
135
202
|
.session-tab.in-split {
|
|
136
|
-
background: rgba(0,120,212,0.1);
|
|
203
|
+
background: rgba(0, 120, 212, 0.1);
|
|
137
204
|
color: var(--text);
|
|
138
205
|
}
|
|
139
|
-
.session-tab.dragging {
|
|
206
|
+
.session-tab.dragging {
|
|
207
|
+
opacity: 0.4;
|
|
208
|
+
}
|
|
140
209
|
.tab-dot {
|
|
141
|
-
width: 8px;
|
|
210
|
+
width: 8px;
|
|
211
|
+
height: 8px;
|
|
212
|
+
border-radius: 50%;
|
|
213
|
+
flex-shrink: 0;
|
|
214
|
+
}
|
|
215
|
+
.tab-name {
|
|
216
|
+
max-width: 100px;
|
|
217
|
+
overflow: hidden;
|
|
218
|
+
text-overflow: ellipsis;
|
|
142
219
|
}
|
|
143
|
-
.tab-name { max-width: 100px; overflow: hidden; text-overflow: ellipsis; }
|
|
144
220
|
.tab-activity {
|
|
145
|
-
font-size: 10px;
|
|
221
|
+
font-size: 10px;
|
|
222
|
+
color: var(--text-muted);
|
|
223
|
+
flex-shrink: 0;
|
|
146
224
|
}
|
|
147
225
|
.tab-status {
|
|
148
|
-
width: 6px;
|
|
226
|
+
width: 6px;
|
|
227
|
+
height: 6px;
|
|
228
|
+
border-radius: 50%;
|
|
229
|
+
flex-shrink: 0;
|
|
149
230
|
}
|
|
150
231
|
.tab-close {
|
|
151
232
|
display: none;
|
|
152
|
-
background: none;
|
|
153
|
-
|
|
233
|
+
background: none;
|
|
234
|
+
border: none;
|
|
235
|
+
color: var(--text-muted);
|
|
236
|
+
font-size: 14px;
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
padding: 0 2px;
|
|
239
|
+
line-height: 1;
|
|
154
240
|
transition: color 0.15s;
|
|
155
241
|
}
|
|
156
242
|
.session-tab:hover .tab-close,
|
|
157
|
-
.session-tab.active .tab-close {
|
|
158
|
-
|
|
243
|
+
.session-tab.active .tab-close {
|
|
244
|
+
display: block;
|
|
245
|
+
}
|
|
246
|
+
.tab-close:hover {
|
|
247
|
+
color: var(--danger);
|
|
248
|
+
}
|
|
159
249
|
/* ===== Tab Preview ===== */
|
|
160
250
|
#tab-preview {
|
|
161
251
|
display: none;
|
|
@@ -165,18 +255,28 @@
|
|
|
165
255
|
background: var(--bg);
|
|
166
256
|
border: 1px solid var(--border);
|
|
167
257
|
border-radius: 10px;
|
|
168
|
-
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
|
|
258
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
|
169
259
|
overflow: hidden;
|
|
170
260
|
pointer-events: none;
|
|
171
261
|
}
|
|
172
|
-
#tab-preview.visible {
|
|
262
|
+
#tab-preview.visible {
|
|
263
|
+
display: block;
|
|
264
|
+
}
|
|
173
265
|
.preview-header {
|
|
174
|
-
display: flex;
|
|
266
|
+
display: flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
gap: 6px;
|
|
175
269
|
padding: 6px 10px;
|
|
176
270
|
border-bottom: 1px solid var(--border);
|
|
177
|
-
font-size: 12px;
|
|
271
|
+
font-size: 12px;
|
|
272
|
+
font-weight: 600;
|
|
273
|
+
}
|
|
274
|
+
.preview-dot {
|
|
275
|
+
width: 8px;
|
|
276
|
+
height: 8px;
|
|
277
|
+
border-radius: 50%;
|
|
278
|
+
flex-shrink: 0;
|
|
178
279
|
}
|
|
179
|
-
.preview-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
180
280
|
.preview-body {
|
|
181
281
|
padding: 6px 8px;
|
|
182
282
|
font-family: 'NerdFont', 'JetBrains Mono', monospace;
|
|
@@ -197,46 +297,118 @@
|
|
|
197
297
|
}
|
|
198
298
|
|
|
199
299
|
.tab-bar-btn {
|
|
200
|
-
background: none;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
300
|
+
background: none;
|
|
301
|
+
border: none;
|
|
302
|
+
color: var(--text-dim);
|
|
303
|
+
width: 30px;
|
|
304
|
+
height: 30px;
|
|
305
|
+
border-radius: 8px;
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
display: flex;
|
|
308
|
+
align-items: center;
|
|
309
|
+
justify-content: center;
|
|
310
|
+
flex-shrink: 0;
|
|
311
|
+
font-size: 18px;
|
|
312
|
+
font-weight: 600;
|
|
313
|
+
transition:
|
|
314
|
+
background 0.15s,
|
|
315
|
+
color 0.15s;
|
|
205
316
|
-webkit-tap-highlight-color: transparent;
|
|
206
317
|
}
|
|
207
|
-
.tab-bar-btn:hover {
|
|
208
|
-
|
|
209
|
-
|
|
318
|
+
.tab-bar-btn:hover {
|
|
319
|
+
background: var(--border);
|
|
320
|
+
color: var(--text);
|
|
321
|
+
}
|
|
322
|
+
.tab-bar-btn:active {
|
|
323
|
+
background: var(--accent);
|
|
324
|
+
color: #fff;
|
|
325
|
+
}
|
|
326
|
+
.tab-bar-btn.active {
|
|
327
|
+
color: var(--accent);
|
|
328
|
+
}
|
|
210
329
|
#tab-new-btn {
|
|
211
|
-
gap: 3px;
|
|
330
|
+
gap: 3px;
|
|
331
|
+
width: auto;
|
|
332
|
+
padding: 0 10px;
|
|
333
|
+
font-size: 12px;
|
|
334
|
+
}
|
|
335
|
+
.new-btn-label {
|
|
336
|
+
font-size: 11px;
|
|
337
|
+
font-weight: 600;
|
|
212
338
|
}
|
|
213
|
-
.new-btn-label { font-size: 11px; font-weight: 600; }
|
|
214
339
|
|
|
215
340
|
.bar-btn {
|
|
216
|
-
background: none;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
341
|
+
background: none;
|
|
342
|
+
border: none;
|
|
343
|
+
color: var(--text-dim);
|
|
344
|
+
width: 30px;
|
|
345
|
+
height: 30px;
|
|
346
|
+
border-radius: 8px;
|
|
347
|
+
cursor: pointer;
|
|
348
|
+
display: flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
justify-content: center;
|
|
351
|
+
transition:
|
|
352
|
+
background 0.15s,
|
|
353
|
+
color 0.15s;
|
|
220
354
|
-webkit-tap-highlight-color: transparent;
|
|
221
355
|
}
|
|
222
|
-
.bar-btn:hover {
|
|
223
|
-
|
|
224
|
-
|
|
356
|
+
.bar-btn:hover {
|
|
357
|
+
background: var(--border);
|
|
358
|
+
color: var(--text);
|
|
359
|
+
}
|
|
360
|
+
.bar-btn:active {
|
|
361
|
+
background: var(--accent);
|
|
362
|
+
color: #fff;
|
|
363
|
+
}
|
|
364
|
+
.bar-btn svg {
|
|
365
|
+
display: block;
|
|
366
|
+
}
|
|
225
367
|
.bar-group {
|
|
226
|
-
display: flex;
|
|
227
|
-
|
|
368
|
+
display: flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
background: var(--bg);
|
|
371
|
+
border-radius: 8px;
|
|
372
|
+
padding: 2px;
|
|
373
|
+
gap: 1px;
|
|
374
|
+
transition: background 0.3s;
|
|
375
|
+
}
|
|
376
|
+
.bar-group .bar-btn {
|
|
377
|
+
width: 26px;
|
|
378
|
+
height: 26px;
|
|
379
|
+
border-radius: 6px;
|
|
380
|
+
font-size: 14px;
|
|
381
|
+
font-weight: 600;
|
|
228
382
|
}
|
|
229
|
-
.bar-group .bar-btn { width: 26px; height: 26px; border-radius: 6px; font-size: 14px; font-weight: 600; }
|
|
230
383
|
#stop-btn {
|
|
231
|
-
background: none;
|
|
232
|
-
border
|
|
233
|
-
|
|
384
|
+
background: none;
|
|
385
|
+
border: none;
|
|
386
|
+
color: var(--danger);
|
|
387
|
+
height: 30px;
|
|
388
|
+
border-radius: 8px;
|
|
389
|
+
cursor: pointer;
|
|
390
|
+
display: flex;
|
|
391
|
+
align-items: center;
|
|
392
|
+
justify-content: center;
|
|
393
|
+
gap: 4px;
|
|
394
|
+
padding: 0 8px;
|
|
395
|
+
font-size: 11px;
|
|
234
396
|
font-weight: 600;
|
|
235
|
-
transition:
|
|
397
|
+
transition:
|
|
398
|
+
background 0.15s,
|
|
399
|
+
color 0.15s,
|
|
400
|
+
transform 0.1s;
|
|
236
401
|
-webkit-tap-highlight-color: transparent;
|
|
237
402
|
}
|
|
238
|
-
#stop-btn:hover {
|
|
239
|
-
|
|
403
|
+
#stop-btn:hover {
|
|
404
|
+
background: var(--danger);
|
|
405
|
+
color: #fff;
|
|
406
|
+
}
|
|
407
|
+
#stop-btn:active {
|
|
408
|
+
background: var(--danger-hover);
|
|
409
|
+
color: #fff;
|
|
410
|
+
transform: scale(0.9);
|
|
411
|
+
}
|
|
240
412
|
|
|
241
413
|
/* ===== Terminals Wrapper ===== */
|
|
242
414
|
#terminals-wrapper {
|
|
@@ -248,13 +420,22 @@
|
|
|
248
420
|
display: flex;
|
|
249
421
|
overflow: hidden;
|
|
250
422
|
}
|
|
251
|
-
#terminals-wrapper.split-h {
|
|
252
|
-
|
|
423
|
+
#terminals-wrapper.split-h {
|
|
424
|
+
flex-direction: row;
|
|
425
|
+
}
|
|
426
|
+
#terminals-wrapper.split-v {
|
|
427
|
+
flex-direction: column;
|
|
428
|
+
}
|
|
253
429
|
.terminal-pane {
|
|
254
|
-
flex: 1;
|
|
430
|
+
flex: 1;
|
|
431
|
+
padding: 2px;
|
|
432
|
+
overflow: hidden;
|
|
433
|
+
position: relative;
|
|
255
434
|
display: none;
|
|
256
435
|
}
|
|
257
|
-
.terminal-pane.visible {
|
|
436
|
+
.terminal-pane.visible {
|
|
437
|
+
display: block;
|
|
438
|
+
}
|
|
258
439
|
#terminals-wrapper.split-h .terminal-pane.visible + .terminal-pane.visible {
|
|
259
440
|
border-left: 2px solid var(--accent);
|
|
260
441
|
}
|
|
@@ -264,112 +445,284 @@
|
|
|
264
445
|
|
|
265
446
|
/* ===== Key Bar ===== */
|
|
266
447
|
#key-bar {
|
|
267
|
-
position: fixed;
|
|
448
|
+
position: fixed;
|
|
449
|
+
bottom: 0;
|
|
450
|
+
left: 0;
|
|
451
|
+
right: 0;
|
|
268
452
|
height: calc(52px + env(safe-area-inset-bottom, 0px));
|
|
269
|
-
display: flex;
|
|
453
|
+
display: flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
background: var(--surface);
|
|
270
456
|
border-top: 1px solid var(--border);
|
|
271
|
-
padding: 0 calc(4px + env(safe-area-inset-right, 0px)) env(safe-area-inset-bottom, 0px)
|
|
272
|
-
|
|
273
|
-
|
|
457
|
+
padding: 0 calc(4px + env(safe-area-inset-right, 0px)) env(safe-area-inset-bottom, 0px)
|
|
458
|
+
calc(4px + env(safe-area-inset-left, 0px));
|
|
459
|
+
gap: 4px;
|
|
460
|
+
z-index: 50;
|
|
461
|
+
transition:
|
|
462
|
+
background 0.3s,
|
|
463
|
+
border-color 0.3s;
|
|
274
464
|
}
|
|
275
465
|
.key-btn {
|
|
276
|
-
min-width: 0;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
466
|
+
min-width: 0;
|
|
467
|
+
height: 40px;
|
|
468
|
+
background: var(--key-bg);
|
|
469
|
+
color: var(--text);
|
|
470
|
+
border: 1px solid var(--key-border);
|
|
471
|
+
border-radius: 8px;
|
|
472
|
+
font-size: 12px;
|
|
473
|
+
font-weight: 600;
|
|
474
|
+
cursor: pointer;
|
|
475
|
+
display: flex;
|
|
476
|
+
flex-direction: column;
|
|
477
|
+
align-items: center;
|
|
478
|
+
justify-content: center;
|
|
479
|
+
-webkit-tap-highlight-color: transparent;
|
|
480
|
+
user-select: none;
|
|
481
|
+
white-space: nowrap;
|
|
482
|
+
padding: 2px 8px;
|
|
483
|
+
flex: 1 1 0;
|
|
484
|
+
gap: 0;
|
|
485
|
+
line-height: 1;
|
|
486
|
+
transition:
|
|
487
|
+
background 0.15s,
|
|
488
|
+
color 0.15s,
|
|
489
|
+
border-color 0.15s,
|
|
490
|
+
transform 0.1s,
|
|
491
|
+
box-shadow 0.15s;
|
|
492
|
+
box-shadow:
|
|
493
|
+
0 1px 3px var(--key-shadow),
|
|
494
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
|
495
|
+
}
|
|
496
|
+
.key-btn .hint {
|
|
497
|
+
font-size: 8px;
|
|
498
|
+
font-weight: 400;
|
|
499
|
+
opacity: 0.5;
|
|
500
|
+
margin-top: 1px;
|
|
501
|
+
letter-spacing: 0.02em;
|
|
502
|
+
}
|
|
503
|
+
.key-btn:hover {
|
|
504
|
+
background: var(--border);
|
|
505
|
+
border-color: var(--accent);
|
|
506
|
+
box-shadow: 0 2px 6px var(--key-shadow);
|
|
507
|
+
}
|
|
508
|
+
.key-btn:active {
|
|
509
|
+
background: var(--accent);
|
|
510
|
+
color: #fff;
|
|
511
|
+
border-color: var(--accent);
|
|
512
|
+
transform: scale(0.93);
|
|
513
|
+
box-shadow: none;
|
|
514
|
+
}
|
|
515
|
+
.key-btn.wide {
|
|
516
|
+
flex: 1.4 1 0;
|
|
517
|
+
}
|
|
518
|
+
.key-sep {
|
|
519
|
+
width: 1px;
|
|
520
|
+
height: 20px;
|
|
521
|
+
background: var(--border);
|
|
522
|
+
flex-shrink: 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.xterm {
|
|
526
|
+
height: 100% !important;
|
|
527
|
+
}
|
|
528
|
+
.xterm-viewport {
|
|
529
|
+
overflow-y: auto !important;
|
|
530
|
+
scrollbar-width: none;
|
|
531
|
+
overscroll-behavior: contain;
|
|
532
|
+
}
|
|
533
|
+
.xterm-viewport::-webkit-scrollbar {
|
|
534
|
+
display: none;
|
|
535
|
+
}
|
|
294
536
|
.terminal-pane,
|
|
295
537
|
.terminal-pane .xterm,
|
|
296
538
|
.terminal-pane .xterm-screen,
|
|
297
539
|
.terminal-pane .xterm-viewport,
|
|
298
|
-
.terminal-pane .xterm-helper-textarea {
|
|
540
|
+
.terminal-pane .xterm-helper-textarea {
|
|
541
|
+
touch-action: none;
|
|
542
|
+
}
|
|
299
543
|
|
|
300
544
|
/* ===== Toasts & Overlays ===== */
|
|
301
545
|
#copy-toast {
|
|
302
|
-
position: fixed;
|
|
546
|
+
position: fixed;
|
|
547
|
+
top: 48px;
|
|
548
|
+
left: 50%;
|
|
303
549
|
transform: translateX(-50%) translateY(-8px);
|
|
304
|
-
background: var(--surface);
|
|
305
|
-
|
|
306
|
-
|
|
550
|
+
background: var(--surface);
|
|
551
|
+
color: var(--text);
|
|
552
|
+
border: 1px solid var(--border);
|
|
553
|
+
padding: 6px 16px;
|
|
554
|
+
border-radius: 8px;
|
|
555
|
+
font-size: 13px;
|
|
556
|
+
font-weight: 600;
|
|
557
|
+
opacity: 0;
|
|
558
|
+
pointer-events: none;
|
|
559
|
+
transition:
|
|
560
|
+
opacity 0.2s,
|
|
561
|
+
transform 0.2s;
|
|
562
|
+
z-index: 200;
|
|
563
|
+
}
|
|
564
|
+
#copy-toast.visible {
|
|
565
|
+
opacity: 1;
|
|
566
|
+
transform: translateX(-50%) translateY(0);
|
|
307
567
|
}
|
|
308
|
-
#copy-toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
309
568
|
|
|
310
569
|
#paste-overlay {
|
|
311
|
-
display: none;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
570
|
+
display: none;
|
|
571
|
+
position: fixed;
|
|
572
|
+
top: 0;
|
|
573
|
+
left: 0;
|
|
574
|
+
right: 0;
|
|
575
|
+
bottom: 0;
|
|
576
|
+
background: var(--overlay-bg);
|
|
577
|
+
z-index: 150;
|
|
578
|
+
flex-direction: column;
|
|
579
|
+
align-items: center;
|
|
580
|
+
justify-content: flex-start;
|
|
581
|
+
padding-top: calc(80px + env(safe-area-inset-top, 0px));
|
|
582
|
+
gap: 12px;
|
|
583
|
+
}
|
|
584
|
+
#paste-overlay.visible {
|
|
585
|
+
display: flex;
|
|
586
|
+
}
|
|
587
|
+
#paste-overlay label {
|
|
588
|
+
font-size: 15px;
|
|
589
|
+
color: #fff;
|
|
590
|
+
font-weight: 600;
|
|
315
591
|
}
|
|
316
|
-
#paste-overlay.visible { display: flex; }
|
|
317
|
-
#paste-overlay label { font-size: 15px; color: #fff; font-weight: 600; }
|
|
318
592
|
#paste-input {
|
|
319
|
-
width: 80%;
|
|
320
|
-
|
|
321
|
-
|
|
593
|
+
width: 80%;
|
|
594
|
+
max-width: 400px;
|
|
595
|
+
min-height: 80px;
|
|
596
|
+
background: var(--surface);
|
|
597
|
+
color: var(--text);
|
|
598
|
+
border: 1px solid var(--border);
|
|
599
|
+
border-radius: 8px;
|
|
600
|
+
padding: 10px;
|
|
601
|
+
font-size: 14px;
|
|
602
|
+
font-family: 'NerdFont', 'JetBrains Mono', monospace;
|
|
603
|
+
resize: vertical;
|
|
322
604
|
}
|
|
323
605
|
|
|
324
606
|
#select-overlay {
|
|
325
|
-
display: none;
|
|
326
|
-
|
|
607
|
+
display: none;
|
|
608
|
+
position: fixed;
|
|
609
|
+
top: 0;
|
|
610
|
+
left: 0;
|
|
611
|
+
right: 0;
|
|
612
|
+
bottom: 0;
|
|
613
|
+
background: var(--bg);
|
|
614
|
+
z-index: 160;
|
|
327
615
|
flex-direction: column;
|
|
328
616
|
}
|
|
329
|
-
#select-overlay.visible {
|
|
617
|
+
#select-overlay.visible {
|
|
618
|
+
display: flex;
|
|
619
|
+
}
|
|
330
620
|
.select-overlay-header {
|
|
331
|
-
display: flex;
|
|
332
|
-
|
|
333
|
-
|
|
621
|
+
display: flex;
|
|
622
|
+
align-items: center;
|
|
623
|
+
justify-content: space-between;
|
|
624
|
+
padding: 12px 12px;
|
|
625
|
+
border-bottom: 1px solid var(--border);
|
|
626
|
+
font-size: 15px;
|
|
627
|
+
font-weight: 600;
|
|
628
|
+
color: var(--text);
|
|
334
629
|
padding-top: calc(12px + env(safe-area-inset-top, 0px));
|
|
335
|
-
min-height: 48px;
|
|
630
|
+
min-height: 48px;
|
|
631
|
+
box-sizing: border-box;
|
|
336
632
|
}
|
|
337
633
|
.select-overlay-header button {
|
|
338
|
-
padding: 6px 12px;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
634
|
+
padding: 6px 12px;
|
|
635
|
+
border: none;
|
|
636
|
+
border-radius: 8px;
|
|
637
|
+
font-size: 13px;
|
|
638
|
+
font-weight: 600;
|
|
639
|
+
cursor: pointer;
|
|
640
|
+
white-space: nowrap;
|
|
641
|
+
flex-shrink: 0;
|
|
642
|
+
transition:
|
|
643
|
+
background 0.15s,
|
|
644
|
+
transform 0.1s;
|
|
645
|
+
}
|
|
646
|
+
.select-overlay-header button:active {
|
|
647
|
+
transform: scale(0.95);
|
|
648
|
+
}
|
|
649
|
+
#select-copy {
|
|
650
|
+
background: var(--accent);
|
|
651
|
+
color: #fff;
|
|
652
|
+
}
|
|
653
|
+
#select-close {
|
|
654
|
+
background: var(--border);
|
|
655
|
+
color: var(--text);
|
|
656
|
+
}
|
|
346
657
|
#select-content {
|
|
347
|
-
flex: 1;
|
|
658
|
+
flex: 1;
|
|
659
|
+
overflow: auto;
|
|
660
|
+
padding: 12px 16px;
|
|
348
661
|
font-family: 'NerdFont', 'JetBrains Mono', monospace;
|
|
349
|
-
font-size: 13px;
|
|
350
|
-
|
|
351
|
-
|
|
662
|
+
font-size: 13px;
|
|
663
|
+
line-height: 1.4;
|
|
664
|
+
color: var(--text);
|
|
665
|
+
white-space: pre;
|
|
666
|
+
word-break: normal;
|
|
667
|
+
-webkit-user-select: text;
|
|
668
|
+
user-select: text;
|
|
352
669
|
margin: 0;
|
|
353
670
|
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
|
|
354
671
|
}
|
|
355
672
|
|
|
356
673
|
#reconnect-overlay {
|
|
357
|
-
display: none;
|
|
358
|
-
|
|
359
|
-
|
|
674
|
+
display: none;
|
|
675
|
+
position: fixed;
|
|
676
|
+
top: 0;
|
|
677
|
+
left: 0;
|
|
678
|
+
right: 0;
|
|
679
|
+
bottom: 0;
|
|
680
|
+
background: var(--overlay-bg);
|
|
681
|
+
z-index: 100;
|
|
682
|
+
flex-direction: column;
|
|
683
|
+
align-items: center;
|
|
684
|
+
justify-content: center;
|
|
685
|
+
gap: 16px;
|
|
686
|
+
}
|
|
687
|
+
#reconnect-overlay.visible {
|
|
688
|
+
display: flex;
|
|
689
|
+
}
|
|
690
|
+
#reconnect-overlay .msg {
|
|
691
|
+
font-size: 17px;
|
|
692
|
+
color: #fff;
|
|
693
|
+
}
|
|
694
|
+
.overlay-actions {
|
|
695
|
+
display: flex;
|
|
696
|
+
gap: 12px;
|
|
360
697
|
}
|
|
361
|
-
#reconnect-overlay.visible { display: flex; }
|
|
362
|
-
#reconnect-overlay .msg { font-size: 17px; color: #fff; }
|
|
363
|
-
.overlay-actions { display: flex; gap: 12px; }
|
|
364
698
|
.overlay-actions button {
|
|
365
|
-
padding: 10px 24px;
|
|
366
|
-
|
|
699
|
+
padding: 10px 24px;
|
|
700
|
+
border: none;
|
|
701
|
+
border-radius: 8px;
|
|
702
|
+
font-size: 15px;
|
|
703
|
+
font-weight: 600;
|
|
704
|
+
cursor: pointer;
|
|
705
|
+
transition:
|
|
706
|
+
background 0.15s,
|
|
707
|
+
transform 0.1s;
|
|
708
|
+
}
|
|
709
|
+
.overlay-actions button:active {
|
|
710
|
+
transform: scale(0.95);
|
|
711
|
+
}
|
|
712
|
+
#reconnect-btn {
|
|
713
|
+
background: var(--accent);
|
|
714
|
+
color: #fff;
|
|
715
|
+
}
|
|
716
|
+
#reconnect-btn:hover {
|
|
717
|
+
background: var(--accent-hover);
|
|
718
|
+
}
|
|
719
|
+
#back-to-sessions {
|
|
720
|
+
background: rgba(255, 255, 255, 0.15);
|
|
721
|
+
color: #fff;
|
|
722
|
+
}
|
|
723
|
+
#back-to-sessions:hover {
|
|
724
|
+
background: rgba(255, 255, 255, 0.25);
|
|
367
725
|
}
|
|
368
|
-
.overlay-actions button:active { transform: scale(0.95); }
|
|
369
|
-
#reconnect-btn { background: var(--accent); color: #fff; }
|
|
370
|
-
#reconnect-btn:hover { background: var(--accent-hover); }
|
|
371
|
-
#back-to-sessions { background: rgba(255,255,255,0.15); color: #fff; }
|
|
372
|
-
#back-to-sessions:hover { background: rgba(255,255,255,0.25); }
|
|
373
726
|
|
|
374
727
|
/* ===== Folder Browser ===== */
|
|
375
728
|
.cwd-picker {
|
|
@@ -390,7 +743,9 @@
|
|
|
390
743
|
flex-shrink: 0;
|
|
391
744
|
display: flex;
|
|
392
745
|
align-items: center;
|
|
393
|
-
transition:
|
|
746
|
+
transition:
|
|
747
|
+
background 0.15s,
|
|
748
|
+
border-color 0.15s;
|
|
394
749
|
}
|
|
395
750
|
.cwd-browse-btn:hover {
|
|
396
751
|
border-color: var(--accent);
|
|
@@ -402,7 +757,10 @@
|
|
|
402
757
|
.browser-overlay {
|
|
403
758
|
display: none;
|
|
404
759
|
position: fixed;
|
|
405
|
-
top: 0;
|
|
760
|
+
top: 0;
|
|
761
|
+
left: 0;
|
|
762
|
+
right: 0;
|
|
763
|
+
bottom: 0;
|
|
406
764
|
background: var(--overlay-bg);
|
|
407
765
|
z-index: 300;
|
|
408
766
|
justify-content: center;
|
|
@@ -443,8 +801,12 @@
|
|
|
443
801
|
line-height: 1;
|
|
444
802
|
transition: color 0.15s;
|
|
445
803
|
}
|
|
446
|
-
.browser-close:hover {
|
|
447
|
-
|
|
804
|
+
.browser-close:hover {
|
|
805
|
+
color: var(--text);
|
|
806
|
+
}
|
|
807
|
+
.browser-close:active {
|
|
808
|
+
color: var(--text);
|
|
809
|
+
}
|
|
448
810
|
.browser-breadcrumb {
|
|
449
811
|
padding: 8px 16px;
|
|
450
812
|
display: flex;
|
|
@@ -458,195 +820,418 @@
|
|
|
458
820
|
-webkit-overflow-scrolling: touch;
|
|
459
821
|
}
|
|
460
822
|
.crumb {
|
|
461
|
-
background: none;
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
823
|
+
background: none;
|
|
824
|
+
border: none;
|
|
825
|
+
color: var(--text-dim);
|
|
826
|
+
font-size: 13px;
|
|
827
|
+
cursor: pointer;
|
|
828
|
+
padding: 4px 6px;
|
|
829
|
+
border-radius: 4px;
|
|
830
|
+
flex-shrink: 0;
|
|
831
|
+
transition:
|
|
832
|
+
background 0.15s,
|
|
833
|
+
color 0.15s;
|
|
834
|
+
}
|
|
835
|
+
.crumb:active,
|
|
836
|
+
.crumb:hover {
|
|
837
|
+
background: var(--border);
|
|
838
|
+
color: var(--text);
|
|
839
|
+
}
|
|
840
|
+
.crumb.current {
|
|
841
|
+
color: var(--text);
|
|
842
|
+
font-weight: 600;
|
|
843
|
+
}
|
|
844
|
+
.crumb-sep {
|
|
845
|
+
color: var(--border-subtle);
|
|
846
|
+
flex-shrink: 0;
|
|
847
|
+
}
|
|
469
848
|
.browser-list {
|
|
470
|
-
flex: 1;
|
|
849
|
+
flex: 1;
|
|
850
|
+
overflow-y: auto;
|
|
851
|
+
padding: 4px 0;
|
|
471
852
|
-webkit-overflow-scrolling: touch;
|
|
472
853
|
}
|
|
473
854
|
.browser-empty {
|
|
474
|
-
text-align: center;
|
|
475
|
-
|
|
855
|
+
text-align: center;
|
|
856
|
+
padding: 40px 20px;
|
|
857
|
+
color: var(--text-muted);
|
|
858
|
+
font-size: 14px;
|
|
476
859
|
}
|
|
477
860
|
.folder-item {
|
|
478
|
-
display: flex;
|
|
479
|
-
|
|
480
|
-
|
|
861
|
+
display: flex;
|
|
862
|
+
align-items: center;
|
|
863
|
+
gap: 12px;
|
|
864
|
+
padding: 12px 16px;
|
|
865
|
+
cursor: pointer;
|
|
866
|
+
border-bottom: 1px solid rgba(60, 60, 60, 0.5);
|
|
481
867
|
transition: background 0.1s;
|
|
482
868
|
-webkit-tap-highlight-color: transparent;
|
|
483
869
|
}
|
|
484
|
-
.folder-item:active {
|
|
485
|
-
|
|
486
|
-
|
|
870
|
+
.folder-item:active {
|
|
871
|
+
background: rgba(0, 120, 212, 0.2);
|
|
872
|
+
}
|
|
873
|
+
.folder-item:hover {
|
|
874
|
+
background: rgba(0, 120, 212, 0.1);
|
|
875
|
+
}
|
|
876
|
+
.folder-icon {
|
|
877
|
+
font-size: 22px;
|
|
878
|
+
flex-shrink: 0;
|
|
879
|
+
width: 28px;
|
|
880
|
+
text-align: center;
|
|
881
|
+
}
|
|
487
882
|
.folder-name {
|
|
488
|
-
font-size: 15px;
|
|
489
|
-
|
|
883
|
+
font-size: 15px;
|
|
884
|
+
color: var(--text);
|
|
885
|
+
flex: 1;
|
|
886
|
+
overflow: hidden;
|
|
887
|
+
text-overflow: ellipsis;
|
|
888
|
+
white-space: nowrap;
|
|
889
|
+
}
|
|
890
|
+
.folder-arrow {
|
|
891
|
+
color: var(--border-subtle);
|
|
892
|
+
font-size: 18px;
|
|
893
|
+
flex-shrink: 0;
|
|
490
894
|
}
|
|
491
|
-
.folder-arrow { color: var(--border-subtle); font-size: 18px; flex-shrink: 0; }
|
|
492
895
|
.browser-footer {
|
|
493
896
|
padding: 12px 16px calc(env(safe-area-inset-bottom, 8px) + 8px);
|
|
494
897
|
border-top: 1px solid var(--border);
|
|
495
|
-
display: flex;
|
|
898
|
+
display: flex;
|
|
899
|
+
flex-direction: column;
|
|
900
|
+
gap: 8px;
|
|
496
901
|
}
|
|
497
902
|
.browser-current-path {
|
|
498
|
-
font-size: 12px;
|
|
499
|
-
|
|
903
|
+
font-size: 12px;
|
|
904
|
+
color: var(--text-dim);
|
|
905
|
+
overflow: hidden;
|
|
906
|
+
text-overflow: ellipsis;
|
|
907
|
+
white-space: nowrap;
|
|
500
908
|
}
|
|
501
909
|
.browser-select-btn {
|
|
502
|
-
width: 100%;
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
910
|
+
width: 100%;
|
|
911
|
+
padding: 12px;
|
|
912
|
+
background: var(--accent);
|
|
913
|
+
color: #ffffff;
|
|
914
|
+
border: none;
|
|
915
|
+
border-radius: 10px;
|
|
916
|
+
font-size: 16px;
|
|
917
|
+
font-weight: 600;
|
|
918
|
+
cursor: pointer;
|
|
919
|
+
transition:
|
|
920
|
+
background 0.15s,
|
|
921
|
+
transform 0.1s;
|
|
922
|
+
}
|
|
923
|
+
.browser-select-btn:hover {
|
|
924
|
+
background: var(--accent-hover);
|
|
925
|
+
}
|
|
926
|
+
.browser-select-btn:active {
|
|
927
|
+
background: var(--accent-active);
|
|
928
|
+
transform: scale(0.98);
|
|
506
929
|
}
|
|
507
|
-
.browser-select-btn:hover { background: var(--accent-hover); }
|
|
508
|
-
.browser-select-btn:active { background: var(--accent-active); transform: scale(0.98); }
|
|
509
930
|
|
|
510
931
|
/* ===== New Session Modal ===== */
|
|
511
932
|
.modal-overlay {
|
|
512
|
-
display: none;
|
|
513
|
-
|
|
514
|
-
|
|
933
|
+
display: none;
|
|
934
|
+
position: fixed;
|
|
935
|
+
top: 0;
|
|
936
|
+
left: 0;
|
|
937
|
+
right: 0;
|
|
938
|
+
bottom: 0;
|
|
939
|
+
background: var(--overlay-bg);
|
|
940
|
+
z-index: 200;
|
|
941
|
+
justify-content: center;
|
|
942
|
+
align-items: center;
|
|
943
|
+
}
|
|
944
|
+
.modal-overlay.visible {
|
|
945
|
+
display: flex;
|
|
515
946
|
}
|
|
516
|
-
.modal-overlay.visible { display: flex; }
|
|
517
947
|
.modal {
|
|
518
|
-
background: var(--surface);
|
|
519
|
-
|
|
948
|
+
background: var(--surface);
|
|
949
|
+
border-radius: 16px;
|
|
950
|
+
width: 90%;
|
|
951
|
+
max-width: 500px;
|
|
952
|
+
padding: 24px 20px;
|
|
953
|
+
transition: background 0.3s;
|
|
954
|
+
max-height: 90vh;
|
|
955
|
+
overflow-y: auto;
|
|
956
|
+
}
|
|
957
|
+
.modal h2 {
|
|
958
|
+
font-size: 18px;
|
|
959
|
+
margin-bottom: 16px;
|
|
520
960
|
}
|
|
521
|
-
.modal h2 { font-size: 18px; margin-bottom: 16px; }
|
|
522
961
|
.modal label {
|
|
523
|
-
display: block;
|
|
524
|
-
|
|
962
|
+
display: block;
|
|
963
|
+
font-size: 13px;
|
|
964
|
+
color: var(--text-secondary);
|
|
965
|
+
margin-bottom: 4px;
|
|
966
|
+
margin-top: 12px;
|
|
525
967
|
}
|
|
526
|
-
.modal input,
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
968
|
+
.modal input,
|
|
969
|
+
.modal select {
|
|
970
|
+
width: 100%;
|
|
971
|
+
padding: 10px 12px;
|
|
972
|
+
background: var(--bg);
|
|
973
|
+
border: 1px solid var(--border);
|
|
974
|
+
border-radius: 8px;
|
|
975
|
+
color: var(--text);
|
|
976
|
+
font-size: 15px;
|
|
977
|
+
outline: none;
|
|
978
|
+
-webkit-appearance: none;
|
|
979
|
+
appearance: none;
|
|
980
|
+
transition:
|
|
981
|
+
background 0.3s,
|
|
982
|
+
border-color 0.15s,
|
|
983
|
+
color 0.3s;
|
|
531
984
|
}
|
|
532
985
|
.modal select {
|
|
533
986
|
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");
|
|
534
|
-
background-repeat: no-repeat;
|
|
535
|
-
|
|
987
|
+
background-repeat: no-repeat;
|
|
988
|
+
background-position: right 12px center;
|
|
989
|
+
padding-right: 32px;
|
|
990
|
+
cursor: pointer;
|
|
991
|
+
}
|
|
992
|
+
.modal input:focus,
|
|
993
|
+
.modal select:focus {
|
|
994
|
+
border-color: var(--accent);
|
|
995
|
+
}
|
|
996
|
+
.modal-actions {
|
|
997
|
+
display: flex;
|
|
998
|
+
gap: 12px;
|
|
999
|
+
margin-top: 20px;
|
|
536
1000
|
}
|
|
537
|
-
.modal input:focus, .modal select:focus { border-color: var(--accent); }
|
|
538
|
-
.modal-actions { display: flex; gap: 12px; margin-top: 20px; }
|
|
539
1001
|
.modal-actions button {
|
|
540
|
-
flex: 1;
|
|
541
|
-
|
|
542
|
-
|
|
1002
|
+
flex: 1;
|
|
1003
|
+
padding: 12px;
|
|
1004
|
+
border: none;
|
|
1005
|
+
border-radius: 8px;
|
|
1006
|
+
font-size: 15px;
|
|
1007
|
+
font-weight: 600;
|
|
1008
|
+
cursor: pointer;
|
|
1009
|
+
transition:
|
|
1010
|
+
background 0.15s,
|
|
1011
|
+
transform 0.1s;
|
|
1012
|
+
}
|
|
1013
|
+
.modal-actions button:active {
|
|
1014
|
+
transform: scale(0.95);
|
|
1015
|
+
}
|
|
1016
|
+
.btn-cancel {
|
|
1017
|
+
background: var(--border);
|
|
1018
|
+
color: var(--text);
|
|
1019
|
+
}
|
|
1020
|
+
.btn-cancel:hover {
|
|
1021
|
+
background: var(--border-subtle);
|
|
1022
|
+
}
|
|
1023
|
+
.btn-create {
|
|
1024
|
+
background: var(--accent);
|
|
1025
|
+
color: #fff;
|
|
1026
|
+
}
|
|
1027
|
+
.btn-create:hover {
|
|
1028
|
+
background: var(--accent-hover);
|
|
543
1029
|
}
|
|
544
|
-
.modal-actions button:active { transform: scale(0.95); }
|
|
545
|
-
.btn-cancel { background: var(--border); color: var(--text); }
|
|
546
|
-
.btn-cancel:hover { background: var(--border-subtle); }
|
|
547
|
-
.btn-create { background: var(--accent); color: #fff; }
|
|
548
|
-
.btn-create:hover { background: var(--accent-hover); }
|
|
549
1030
|
|
|
550
|
-
.color-picker {
|
|
1031
|
+
.color-picker {
|
|
1032
|
+
display: flex;
|
|
1033
|
+
gap: 8px;
|
|
1034
|
+
padding: 6px 0;
|
|
1035
|
+
flex-wrap: wrap;
|
|
1036
|
+
}
|
|
551
1037
|
.color-swatch {
|
|
552
|
-
width: 32px;
|
|
553
|
-
|
|
554
|
-
|
|
1038
|
+
width: 32px;
|
|
1039
|
+
height: 32px;
|
|
1040
|
+
border-radius: 50%;
|
|
1041
|
+
border: 3px solid transparent;
|
|
1042
|
+
cursor: pointer;
|
|
1043
|
+
padding: 0;
|
|
1044
|
+
outline: none;
|
|
1045
|
+
transition:
|
|
1046
|
+
border-color 0.15s,
|
|
1047
|
+
transform 0.1s;
|
|
555
1048
|
-webkit-tap-highlight-color: transparent;
|
|
556
1049
|
}
|
|
557
|
-
.color-swatch:hover {
|
|
558
|
-
|
|
1050
|
+
.color-swatch:hover {
|
|
1051
|
+
transform: scale(1.1);
|
|
1052
|
+
}
|
|
1053
|
+
.color-swatch.selected {
|
|
1054
|
+
border-color: var(--text);
|
|
1055
|
+
transform: scale(1.15);
|
|
1056
|
+
}
|
|
559
1057
|
|
|
560
1058
|
/* ===== Sessions Side Panel (mobile) ===== */
|
|
561
1059
|
#panel-toggle {
|
|
562
1060
|
display: none;
|
|
563
|
-
background: none;
|
|
564
|
-
|
|
565
|
-
|
|
1061
|
+
background: none;
|
|
1062
|
+
border: none;
|
|
1063
|
+
color: var(--text-dim);
|
|
1064
|
+
width: 30px;
|
|
1065
|
+
height: 30px;
|
|
1066
|
+
border-radius: 8px;
|
|
1067
|
+
cursor: pointer;
|
|
1068
|
+
align-items: center;
|
|
1069
|
+
justify-content: center;
|
|
566
1070
|
flex-shrink: 0;
|
|
567
|
-
transition:
|
|
1071
|
+
transition:
|
|
1072
|
+
background 0.15s,
|
|
1073
|
+
color 0.15s;
|
|
568
1074
|
-webkit-tap-highlight-color: transparent;
|
|
569
1075
|
}
|
|
570
|
-
#panel-toggle:hover {
|
|
1076
|
+
#panel-toggle:hover {
|
|
1077
|
+
background: var(--border);
|
|
1078
|
+
color: var(--text);
|
|
1079
|
+
}
|
|
571
1080
|
|
|
572
1081
|
#side-panel-backdrop {
|
|
573
|
-
display: none;
|
|
574
|
-
|
|
575
|
-
|
|
1082
|
+
display: none;
|
|
1083
|
+
position: fixed;
|
|
1084
|
+
top: 0;
|
|
1085
|
+
left: 0;
|
|
1086
|
+
right: 0;
|
|
1087
|
+
bottom: 0;
|
|
1088
|
+
background: rgba(0, 0, 0, 0.5);
|
|
1089
|
+
z-index: 400;
|
|
1090
|
+
opacity: 0;
|
|
1091
|
+
transition: opacity 0.25s;
|
|
1092
|
+
}
|
|
1093
|
+
#side-panel-backdrop.visible {
|
|
1094
|
+
display: block;
|
|
1095
|
+
opacity: 1;
|
|
576
1096
|
}
|
|
577
|
-
#side-panel-backdrop.visible { display: block; opacity: 1; }
|
|
578
1097
|
|
|
579
1098
|
#side-panel {
|
|
580
|
-
position: fixed;
|
|
1099
|
+
position: fixed;
|
|
1100
|
+
top: 0;
|
|
1101
|
+
left: 0;
|
|
1102
|
+
bottom: 0;
|
|
581
1103
|
width: min(85vw, 380px);
|
|
582
1104
|
background: var(--surface);
|
|
583
1105
|
border-right: 1px solid var(--border);
|
|
584
1106
|
z-index: 410;
|
|
585
1107
|
transform: translateX(-100%);
|
|
586
|
-
transition:
|
|
587
|
-
|
|
1108
|
+
transition:
|
|
1109
|
+
transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
|
1110
|
+
background 0.3s;
|
|
1111
|
+
display: flex;
|
|
1112
|
+
flex-direction: column;
|
|
588
1113
|
overflow: hidden;
|
|
589
1114
|
padding-top: env(safe-area-inset-top, 0px);
|
|
590
1115
|
padding-left: env(safe-area-inset-left, 0px);
|
|
591
1116
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
592
1117
|
}
|
|
593
|
-
#side-panel.open {
|
|
1118
|
+
#side-panel.open {
|
|
1119
|
+
transform: translateX(0);
|
|
1120
|
+
}
|
|
594
1121
|
|
|
595
1122
|
.side-panel-header {
|
|
596
|
-
display: flex;
|
|
597
|
-
|
|
598
|
-
|
|
1123
|
+
display: flex;
|
|
1124
|
+
align-items: center;
|
|
1125
|
+
justify-content: space-between;
|
|
1126
|
+
padding: 12px 14px;
|
|
1127
|
+
border-bottom: 1px solid var(--border);
|
|
1128
|
+
font-size: 15px;
|
|
1129
|
+
font-weight: 700;
|
|
599
1130
|
}
|
|
600
1131
|
.side-panel-close {
|
|
601
|
-
background: none;
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1132
|
+
background: none;
|
|
1133
|
+
border: none;
|
|
1134
|
+
color: var(--text-dim);
|
|
1135
|
+
width: 30px;
|
|
1136
|
+
height: 30px;
|
|
1137
|
+
border-radius: 8px;
|
|
1138
|
+
cursor: pointer;
|
|
1139
|
+
display: flex;
|
|
1140
|
+
align-items: center;
|
|
1141
|
+
justify-content: center;
|
|
1142
|
+
font-size: 20px;
|
|
1143
|
+
transition:
|
|
1144
|
+
background 0.15s,
|
|
1145
|
+
color 0.15s;
|
|
605
1146
|
-webkit-tap-highlight-color: transparent;
|
|
606
1147
|
}
|
|
607
|
-
.side-panel-close:hover {
|
|
1148
|
+
.side-panel-close:hover {
|
|
1149
|
+
background: var(--border);
|
|
1150
|
+
color: var(--text);
|
|
1151
|
+
}
|
|
608
1152
|
|
|
609
1153
|
.side-panel-list {
|
|
610
|
-
flex: 1;
|
|
1154
|
+
flex: 1;
|
|
1155
|
+
overflow-y: auto;
|
|
1156
|
+
padding: 8px;
|
|
611
1157
|
-webkit-overflow-scrolling: touch;
|
|
612
|
-
display: flex;
|
|
1158
|
+
display: flex;
|
|
1159
|
+
flex-direction: column;
|
|
1160
|
+
gap: 6px;
|
|
613
1161
|
}
|
|
614
1162
|
|
|
615
1163
|
.side-panel-card {
|
|
616
|
-
background: var(--bg);
|
|
617
|
-
border
|
|
618
|
-
|
|
1164
|
+
background: var(--bg);
|
|
1165
|
+
border: 1px solid var(--border);
|
|
1166
|
+
border-radius: 10px;
|
|
1167
|
+
cursor: pointer;
|
|
1168
|
+
overflow: hidden;
|
|
1169
|
+
transition:
|
|
1170
|
+
background 0.15s,
|
|
1171
|
+
border-color 0.15s;
|
|
619
1172
|
-webkit-tap-highlight-color: transparent;
|
|
620
1173
|
}
|
|
621
|
-
.side-panel-card:hover {
|
|
622
|
-
|
|
1174
|
+
.side-panel-card:hover {
|
|
1175
|
+
border-color: var(--accent);
|
|
1176
|
+
}
|
|
1177
|
+
.side-panel-card.active {
|
|
1178
|
+
border-color: var(--accent);
|
|
1179
|
+
border-width: 2px;
|
|
1180
|
+
}
|
|
623
1181
|
|
|
624
1182
|
.side-panel-card-header {
|
|
625
|
-
display: flex;
|
|
1183
|
+
display: flex;
|
|
1184
|
+
align-items: center;
|
|
1185
|
+
gap: 8px;
|
|
626
1186
|
padding: 10px 12px 6px;
|
|
627
1187
|
}
|
|
628
1188
|
.side-panel-card-dot {
|
|
629
|
-
width: 10px;
|
|
1189
|
+
width: 10px;
|
|
1190
|
+
height: 10px;
|
|
1191
|
+
border-radius: 50%;
|
|
1192
|
+
flex-shrink: 0;
|
|
630
1193
|
}
|
|
631
1194
|
.side-panel-card-name {
|
|
632
|
-
font-size: 13px;
|
|
633
|
-
|
|
1195
|
+
font-size: 13px;
|
|
1196
|
+
font-weight: 600;
|
|
1197
|
+
flex: 1;
|
|
1198
|
+
overflow: hidden;
|
|
1199
|
+
text-overflow: ellipsis;
|
|
1200
|
+
white-space: nowrap;
|
|
634
1201
|
}
|
|
635
1202
|
.side-panel-card-status {
|
|
636
|
-
width: 7px;
|
|
1203
|
+
width: 7px;
|
|
1204
|
+
height: 7px;
|
|
1205
|
+
border-radius: 50%;
|
|
1206
|
+
flex-shrink: 0;
|
|
637
1207
|
}
|
|
638
1208
|
.side-panel-card-close {
|
|
639
|
-
background: none;
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
1209
|
+
background: none;
|
|
1210
|
+
border: none;
|
|
1211
|
+
color: var(--text-muted);
|
|
1212
|
+
width: 26px;
|
|
1213
|
+
height: 26px;
|
|
1214
|
+
border-radius: 6px;
|
|
1215
|
+
cursor: pointer;
|
|
1216
|
+
display: flex;
|
|
1217
|
+
align-items: center;
|
|
1218
|
+
justify-content: center;
|
|
1219
|
+
font-size: 16px;
|
|
1220
|
+
flex-shrink: 0;
|
|
1221
|
+
padding: 0;
|
|
1222
|
+
transition:
|
|
1223
|
+
background 0.15s,
|
|
1224
|
+
color 0.15s;
|
|
644
1225
|
-webkit-tap-highlight-color: transparent;
|
|
645
1226
|
}
|
|
646
|
-
.side-panel-card-close:hover {
|
|
1227
|
+
.side-panel-card-close:hover {
|
|
1228
|
+
background: var(--danger);
|
|
1229
|
+
color: #fff;
|
|
1230
|
+
}
|
|
647
1231
|
.side-panel-card-meta {
|
|
648
1232
|
padding: 0 12px 4px;
|
|
649
|
-
font-size: 10px;
|
|
1233
|
+
font-size: 10px;
|
|
1234
|
+
color: var(--text-muted);
|
|
650
1235
|
}
|
|
651
1236
|
|
|
652
1237
|
.side-panel-card-preview {
|
|
@@ -664,22 +1249,47 @@
|
|
|
664
1249
|
-webkit-text-size-adjust: none;
|
|
665
1250
|
}
|
|
666
1251
|
.side-panel-card-preview.empty {
|
|
667
|
-
font-style: italic;
|
|
668
|
-
|
|
1252
|
+
font-style: italic;
|
|
1253
|
+
color: var(--text-muted);
|
|
1254
|
+
text-align: center;
|
|
1255
|
+
font-family: inherit;
|
|
1256
|
+
font-size: 11px;
|
|
669
1257
|
white-space: normal;
|
|
670
1258
|
}
|
|
671
1259
|
|
|
672
1260
|
@media (max-width: 640px) {
|
|
673
|
-
#panel-toggle {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
#
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
#
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
#
|
|
1261
|
+
#panel-toggle {
|
|
1262
|
+
display: flex;
|
|
1263
|
+
}
|
|
1264
|
+
#tab-list {
|
|
1265
|
+
display: none;
|
|
1266
|
+
}
|
|
1267
|
+
#split-toggle {
|
|
1268
|
+
display: none;
|
|
1269
|
+
}
|
|
1270
|
+
#version-text {
|
|
1271
|
+
display: none;
|
|
1272
|
+
}
|
|
1273
|
+
#back-btn {
|
|
1274
|
+
display: none;
|
|
1275
|
+
}
|
|
1276
|
+
#theme-toggle {
|
|
1277
|
+
display: flex;
|
|
1278
|
+
}
|
|
1279
|
+
#stop-btn {
|
|
1280
|
+
padding: 0 8px;
|
|
1281
|
+
}
|
|
1282
|
+
#session-name {
|
|
1283
|
+
max-width: 30vw;
|
|
1284
|
+
}
|
|
1285
|
+
#status-text {
|
|
1286
|
+
display: none;
|
|
1287
|
+
}
|
|
1288
|
+
#tab-new-btn {
|
|
1289
|
+
font-size: 13px;
|
|
1290
|
+
padding: 0 8px;
|
|
1291
|
+
width: auto;
|
|
1292
|
+
}
|
|
683
1293
|
}
|
|
684
1294
|
</style>
|
|
685
1295
|
</head>
|
|
@@ -692,8 +1302,28 @@
|
|
|
692
1302
|
<button class="side-panel-close" id="side-panel-close" title="Close">×</button>
|
|
693
1303
|
</div>
|
|
694
1304
|
<div class="side-panel-list" id="side-panel-list"></div>
|
|
695
|
-
<div style="padding:8px;border-top:1px solid var(--border)">
|
|
696
|
-
<button
|
|
1305
|
+
<div style="padding: 8px; border-top: 1px solid var(--border)">
|
|
1306
|
+
<button
|
|
1307
|
+
id="side-panel-new-btn"
|
|
1308
|
+
style="
|
|
1309
|
+
width: 100%;
|
|
1310
|
+
padding: 10px;
|
|
1311
|
+
border: 1px dashed var(--border);
|
|
1312
|
+
border-radius: 8px;
|
|
1313
|
+
background: none;
|
|
1314
|
+
color: var(--accent);
|
|
1315
|
+
font-size: 14px;
|
|
1316
|
+
font-weight: 600;
|
|
1317
|
+
cursor: pointer;
|
|
1318
|
+
display: flex;
|
|
1319
|
+
align-items: center;
|
|
1320
|
+
justify-content: center;
|
|
1321
|
+
gap: 6px;
|
|
1322
|
+
transition: background 0.15s;
|
|
1323
|
+
"
|
|
1324
|
+
>
|
|
1325
|
+
+ New Session
|
|
1326
|
+
</button>
|
|
697
1327
|
</div>
|
|
698
1328
|
</div>
|
|
699
1329
|
|
|
@@ -701,28 +1331,141 @@
|
|
|
701
1331
|
<div id="top-bar">
|
|
702
1332
|
<div class="left">
|
|
703
1333
|
<button id="panel-toggle" title="Sessions">
|
|
704
|
-
<svg
|
|
1334
|
+
<svg
|
|
1335
|
+
width="18"
|
|
1336
|
+
height="18"
|
|
1337
|
+
viewBox="0 0 24 24"
|
|
1338
|
+
fill="none"
|
|
1339
|
+
stroke="currentColor"
|
|
1340
|
+
stroke-width="2"
|
|
1341
|
+
stroke-linecap="round"
|
|
1342
|
+
stroke-linejoin="round"
|
|
1343
|
+
>
|
|
1344
|
+
<line x1="3" y1="6" x2="21" y2="6" />
|
|
1345
|
+
<line x1="3" y1="12" x2="21" y2="12" />
|
|
1346
|
+
<line x1="3" y1="18" x2="21" y2="18" />
|
|
1347
|
+
</svg>
|
|
1348
|
+
</button>
|
|
1349
|
+
<button
|
|
1350
|
+
class="bar-btn"
|
|
1351
|
+
id="back-btn"
|
|
1352
|
+
onclick="location.href = '/'"
|
|
1353
|
+
title="Back to sessions"
|
|
1354
|
+
>
|
|
1355
|
+
<svg
|
|
1356
|
+
width="18"
|
|
1357
|
+
height="18"
|
|
1358
|
+
viewBox="0 0 24 24"
|
|
1359
|
+
fill="none"
|
|
1360
|
+
stroke="currentColor"
|
|
1361
|
+
stroke-width="2"
|
|
1362
|
+
stroke-linecap="round"
|
|
1363
|
+
stroke-linejoin="round"
|
|
1364
|
+
>
|
|
1365
|
+
<polyline points="15 18 9 12 15 6" />
|
|
1366
|
+
</svg>
|
|
705
1367
|
</button>
|
|
706
|
-
<button class="bar-btn" id="back-btn" onclick="location.href='/'" title="Back to sessions"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg></button>
|
|
707
1368
|
<span id="status-dot"></span>
|
|
708
1369
|
<span id="session-name">…</span>
|
|
709
1370
|
<span id="status-text">Connecting…</span>
|
|
710
1371
|
</div>
|
|
711
1372
|
<div id="tab-list"></div>
|
|
712
1373
|
<div class="right">
|
|
713
|
-
<span id="version-text" style="font-size:11px;color:var(--text-muted)"></span>
|
|
714
|
-
<button class="tab-bar-btn" id="tab-new-btn" title="New session"
|
|
1374
|
+
<span id="version-text" style="font-size: 11px; color: var(--text-muted)"></span>
|
|
1375
|
+
<button class="tab-bar-btn" id="tab-new-btn" title="New session">
|
|
1376
|
+
<svg
|
|
1377
|
+
width="14"
|
|
1378
|
+
height="14"
|
|
1379
|
+
viewBox="0 0 24 24"
|
|
1380
|
+
fill="none"
|
|
1381
|
+
stroke="currentColor"
|
|
1382
|
+
stroke-width="2.5"
|
|
1383
|
+
stroke-linecap="round"
|
|
1384
|
+
>
|
|
1385
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
1386
|
+
<line x1="5" y1="12" x2="19" y2="12" /></svg
|
|
1387
|
+
><span class="new-btn-label">New</span>
|
|
1388
|
+
</button>
|
|
715
1389
|
<button class="tab-bar-btn" id="split-toggle" title="Split view">
|
|
716
|
-
<svg
|
|
1390
|
+
<svg
|
|
1391
|
+
width="16"
|
|
1392
|
+
height="16"
|
|
1393
|
+
viewBox="0 0 24 24"
|
|
1394
|
+
fill="none"
|
|
1395
|
+
stroke="currentColor"
|
|
1396
|
+
stroke-width="2"
|
|
1397
|
+
stroke-linecap="round"
|
|
1398
|
+
stroke-linejoin="round"
|
|
1399
|
+
>
|
|
1400
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
1401
|
+
<line x1="12" y1="3" x2="12" y2="21" />
|
|
1402
|
+
</svg>
|
|
717
1403
|
</button>
|
|
718
1404
|
<div class="bar-group">
|
|
719
1405
|
<button class="bar-btn" id="zoom-out" title="Decrease font size">−</button>
|
|
720
1406
|
<button class="bar-btn" id="zoom-in" title="Increase font size">+</button>
|
|
721
1407
|
</div>
|
|
722
|
-
<button class="bar-btn" id="share-btn" title="Share link"
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1408
|
+
<button class="bar-btn" id="share-btn" title="Share link">
|
|
1409
|
+
<svg
|
|
1410
|
+
width="16"
|
|
1411
|
+
height="16"
|
|
1412
|
+
viewBox="0 0 24 24"
|
|
1413
|
+
fill="none"
|
|
1414
|
+
stroke="currentColor"
|
|
1415
|
+
stroke-width="2"
|
|
1416
|
+
stroke-linecap="round"
|
|
1417
|
+
stroke-linejoin="round"
|
|
1418
|
+
>
|
|
1419
|
+
<circle cx="18" cy="5" r="3" />
|
|
1420
|
+
<circle cx="6" cy="12" r="3" />
|
|
1421
|
+
<circle cx="18" cy="19" r="3" />
|
|
1422
|
+
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
|
|
1423
|
+
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
|
|
1424
|
+
</svg>
|
|
1425
|
+
</button>
|
|
1426
|
+
<button class="bar-btn" id="refresh-btn" title="Refresh app">
|
|
1427
|
+
<svg
|
|
1428
|
+
width="16"
|
|
1429
|
+
height="16"
|
|
1430
|
+
viewBox="0 0 24 24"
|
|
1431
|
+
fill="none"
|
|
1432
|
+
stroke="currentColor"
|
|
1433
|
+
stroke-width="2"
|
|
1434
|
+
stroke-linecap="round"
|
|
1435
|
+
stroke-linejoin="round"
|
|
1436
|
+
>
|
|
1437
|
+
<polyline points="23 4 23 10 17 10" />
|
|
1438
|
+
<polyline points="1 20 1 14 7 14" />
|
|
1439
|
+
<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" />
|
|
1440
|
+
</svg>
|
|
1441
|
+
</button>
|
|
1442
|
+
<button class="bar-btn" id="theme-toggle" title="Toggle theme">
|
|
1443
|
+
<svg
|
|
1444
|
+
width="16"
|
|
1445
|
+
height="16"
|
|
1446
|
+
viewBox="0 0 24 24"
|
|
1447
|
+
fill="none"
|
|
1448
|
+
stroke="currentColor"
|
|
1449
|
+
stroke-width="2"
|
|
1450
|
+
stroke-linecap="round"
|
|
1451
|
+
stroke-linejoin="round"
|
|
1452
|
+
>
|
|
1453
|
+
<circle cx="12" cy="12" r="5" />
|
|
1454
|
+
<line x1="12" y1="1" x2="12" y2="3" />
|
|
1455
|
+
<line x1="12" y1="21" x2="12" y2="23" />
|
|
1456
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
1457
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
1458
|
+
<line x1="1" y1="12" x2="3" y2="12" />
|
|
1459
|
+
<line x1="21" y1="12" x2="23" y2="12" />
|
|
1460
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
1461
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
1462
|
+
</svg>
|
|
1463
|
+
</button>
|
|
1464
|
+
<button id="stop-btn" title="Stop session">
|
|
1465
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
|
1466
|
+
<rect x="6" y="6" width="12" height="12" rx="2" /></svg
|
|
1467
|
+
>Stop
|
|
1468
|
+
</button>
|
|
726
1469
|
</div>
|
|
727
1470
|
</div>
|
|
728
1471
|
|
|
@@ -741,8 +1484,12 @@
|
|
|
741
1484
|
<div id="copy-toast">Copied!</div>
|
|
742
1485
|
|
|
743
1486
|
<div id="key-bar">
|
|
744
|
-
<button class="key-btn" data-key="[A" title="Previous command"
|
|
745
|
-
|
|
1487
|
+
<button class="key-btn" data-key="[A" title="Previous command">
|
|
1488
|
+
↑<span class="hint">prev</span>
|
|
1489
|
+
</button>
|
|
1490
|
+
<button class="key-btn" data-key="[B" title="Next command">
|
|
1491
|
+
↓<span class="hint">next</span>
|
|
1492
|
+
</button>
|
|
746
1493
|
<button class="key-btn" data-key="[D" title="Left">←</button>
|
|
747
1494
|
<button class="key-btn" data-key="[C" title="Right">→</button>
|
|
748
1495
|
<button class="key-btn" data-key="[H" title="Home">Home</button>
|
|
@@ -753,15 +1500,19 @@
|
|
|
753
1500
|
<div class="key-sep"></div>
|
|
754
1501
|
<button class="key-btn wide" data-key="	" title="Autocomplete">Tab</button>
|
|
755
1502
|
<div class="key-sep"></div>
|
|
756
|
-
<button class="key-btn" data-key="" title="Interrupt process"
|
|
1503
|
+
<button class="key-btn" data-key="" title="Interrupt process">
|
|
1504
|
+
^C<span class="hint">stop</span>
|
|
1505
|
+
</button>
|
|
757
1506
|
<div class="key-sep"></div>
|
|
758
|
-
<button class="key-btn" data-key="enter" title="Enter / Return"
|
|
1507
|
+
<button class="key-btn" data-key="enter" title="Enter / Return">
|
|
1508
|
+
↵<span class="hint">enter</span>
|
|
1509
|
+
</button>
|
|
759
1510
|
</div>
|
|
760
1511
|
|
|
761
1512
|
<div id="reconnect-overlay">
|
|
762
1513
|
<div class="msg">Session disconnected</div>
|
|
763
1514
|
<div class="overlay-actions">
|
|
764
|
-
<button id="back-to-sessions" onclick="location.href='/'">Sessions</button>
|
|
1515
|
+
<button id="back-to-sessions" onclick="location.href = '/'">Sessions</button>
|
|
765
1516
|
<button id="reconnect-btn">Reconnect</button>
|
|
766
1517
|
</div>
|
|
767
1518
|
</div>
|
|
@@ -770,20 +1521,37 @@
|
|
|
770
1521
|
<label for="paste-input">Paste your text below</label>
|
|
771
1522
|
<textarea id="paste-input" placeholder="Long-press here and paste…"></textarea>
|
|
772
1523
|
<div class="overlay-actions">
|
|
773
|
-
<button id="paste-cancel" style="background:rgba(255,255,255,0.15);color
|
|
774
|
-
|
|
1524
|
+
<button id="paste-cancel" style="background: rgba(255, 255, 255, 0.15); color: #fff">
|
|
1525
|
+
Cancel
|
|
1526
|
+
</button>
|
|
1527
|
+
<button id="paste-send" style="background: var(--accent); color: #fff">Send</button>
|
|
775
1528
|
</div>
|
|
776
1529
|
</div>
|
|
777
1530
|
|
|
778
1531
|
<div id="select-overlay">
|
|
779
1532
|
<div class="select-overlay-header">
|
|
780
1533
|
<span id="select-title">Copy Text</span>
|
|
781
|
-
<div style="display:flex;gap:8px">
|
|
1534
|
+
<div style="display: flex; gap: 8px">
|
|
782
1535
|
<button id="select-copy">Copy</button>
|
|
783
1536
|
<button id="select-close">Done</button>
|
|
784
1537
|
</div>
|
|
785
1538
|
</div>
|
|
786
|
-
<button
|
|
1539
|
+
<button
|
|
1540
|
+
id="select-load-more"
|
|
1541
|
+
style="
|
|
1542
|
+
display: none;
|
|
1543
|
+
width: 100%;
|
|
1544
|
+
padding: 8px;
|
|
1545
|
+
background: var(--surface);
|
|
1546
|
+
color: var(--accent);
|
|
1547
|
+
border: 1px solid var(--border);
|
|
1548
|
+
border-radius: 6px;
|
|
1549
|
+
font-size: 13px;
|
|
1550
|
+
cursor: pointer;
|
|
1551
|
+
"
|
|
1552
|
+
>
|
|
1553
|
+
▲ Load more
|
|
1554
|
+
</button>
|
|
787
1555
|
<pre id="select-content"></pre>
|
|
788
1556
|
</div>
|
|
789
1557
|
|
|
@@ -794,25 +1562,94 @@
|
|
|
794
1562
|
<label for="ns-name">Name</label>
|
|
795
1563
|
<input type="text" id="ns-name" placeholder="My Session" />
|
|
796
1564
|
<label for="ns-shell">Shell</label>
|
|
797
|
-
<select id="ns-shell"
|
|
798
|
-
|
|
1565
|
+
<select id="ns-shell">
|
|
1566
|
+
<option value="">Loading shells…</option>
|
|
1567
|
+
</select>
|
|
1568
|
+
<label for="ns-cmd"
|
|
1569
|
+
>Initial Command
|
|
1570
|
+
<span style="color: var(--text-muted); font-weight: normal">(optional)</span></label
|
|
1571
|
+
>
|
|
799
1572
|
<input type="text" id="ns-cmd" placeholder="e.g. htop, vim" />
|
|
800
1573
|
<label>Color</label>
|
|
801
1574
|
<div class="color-picker" id="ns-color-picker">
|
|
802
|
-
<button
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
<button
|
|
1575
|
+
<button
|
|
1576
|
+
type="button"
|
|
1577
|
+
class="color-swatch selected"
|
|
1578
|
+
data-color="#4a9eff"
|
|
1579
|
+
style="background: #4a9eff"
|
|
1580
|
+
title="Blue"
|
|
1581
|
+
></button>
|
|
1582
|
+
<button
|
|
1583
|
+
type="button"
|
|
1584
|
+
class="color-swatch"
|
|
1585
|
+
data-color="#4ade80"
|
|
1586
|
+
style="background: #4ade80"
|
|
1587
|
+
title="Green"
|
|
1588
|
+
></button>
|
|
1589
|
+
<button
|
|
1590
|
+
type="button"
|
|
1591
|
+
class="color-swatch"
|
|
1592
|
+
data-color="#fbbf24"
|
|
1593
|
+
style="background: #fbbf24"
|
|
1594
|
+
title="Amber"
|
|
1595
|
+
></button>
|
|
1596
|
+
<button
|
|
1597
|
+
type="button"
|
|
1598
|
+
class="color-swatch"
|
|
1599
|
+
data-color="#c084fc"
|
|
1600
|
+
style="background: #c084fc"
|
|
1601
|
+
title="Purple"
|
|
1602
|
+
></button>
|
|
1603
|
+
<button
|
|
1604
|
+
type="button"
|
|
1605
|
+
class="color-swatch"
|
|
1606
|
+
data-color="#f87171"
|
|
1607
|
+
style="background: #f87171"
|
|
1608
|
+
title="Red"
|
|
1609
|
+
></button>
|
|
1610
|
+
<button
|
|
1611
|
+
type="button"
|
|
1612
|
+
class="color-swatch"
|
|
1613
|
+
data-color="#22d3ee"
|
|
1614
|
+
style="background: #22d3ee"
|
|
1615
|
+
title="Cyan"
|
|
1616
|
+
></button>
|
|
1617
|
+
<button
|
|
1618
|
+
type="button"
|
|
1619
|
+
class="color-swatch"
|
|
1620
|
+
data-color="#fb923c"
|
|
1621
|
+
style="background: #fb923c"
|
|
1622
|
+
title="Orange"
|
|
1623
|
+
></button>
|
|
1624
|
+
<button
|
|
1625
|
+
type="button"
|
|
1626
|
+
class="color-swatch"
|
|
1627
|
+
data-color="#f472b6"
|
|
1628
|
+
style="background: #f472b6"
|
|
1629
|
+
title="Pink"
|
|
1630
|
+
></button>
|
|
810
1631
|
</div>
|
|
811
|
-
<label for="ns-cwd"
|
|
1632
|
+
<label for="ns-cwd"
|
|
1633
|
+
>Working Directory
|
|
1634
|
+
<span style="color: var(--text-muted); font-weight: normal">(optional)</span></label
|
|
1635
|
+
>
|
|
812
1636
|
<div class="cwd-picker">
|
|
813
1637
|
<input type="text" id="ns-cwd" placeholder="Uses server default" />
|
|
814
1638
|
<button type="button" class="cwd-browse-btn" id="ns-browse-btn" title="Browse folders">
|
|
815
|
-
<svg
|
|
1639
|
+
<svg
|
|
1640
|
+
width="18"
|
|
1641
|
+
height="18"
|
|
1642
|
+
viewBox="0 0 24 24"
|
|
1643
|
+
fill="none"
|
|
1644
|
+
stroke="currentColor"
|
|
1645
|
+
stroke-width="2"
|
|
1646
|
+
stroke-linecap="round"
|
|
1647
|
+
stroke-linejoin="round"
|
|
1648
|
+
>
|
|
1649
|
+
<path
|
|
1650
|
+
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"
|
|
1651
|
+
/>
|
|
1652
|
+
</svg>
|
|
816
1653
|
</button>
|
|
817
1654
|
</div>
|
|
818
1655
|
<div class="modal-actions">
|
|
@@ -827,7 +1664,21 @@
|
|
|
827
1664
|
<div class="browser-sheet">
|
|
828
1665
|
<div class="browser-header">
|
|
829
1666
|
<h3>
|
|
830
|
-
<svg
|
|
1667
|
+
<svg
|
|
1668
|
+
width="18"
|
|
1669
|
+
height="18"
|
|
1670
|
+
viewBox="0 0 24 24"
|
|
1671
|
+
fill="none"
|
|
1672
|
+
stroke="currentColor"
|
|
1673
|
+
stroke-width="2"
|
|
1674
|
+
stroke-linecap="round"
|
|
1675
|
+
stroke-linejoin="round"
|
|
1676
|
+
style="vertical-align: -3px; margin-right: 6px"
|
|
1677
|
+
>
|
|
1678
|
+
<path
|
|
1679
|
+
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"
|
|
1680
|
+
/></svg
|
|
1681
|
+
>Choose Folder
|
|
831
1682
|
</h3>
|
|
832
1683
|
<button class="browser-close" id="ns-browser-close">×</button>
|
|
833
1684
|
</div>
|
|
@@ -845,7 +1696,16 @@
|
|
|
845
1696
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
|
|
846
1697
|
<script>
|
|
847
1698
|
// ===== Constants =====
|
|
848
|
-
const SESSION_COLORS = [
|
|
1699
|
+
const SESSION_COLORS = [
|
|
1700
|
+
'#4a9eff',
|
|
1701
|
+
'#4ade80',
|
|
1702
|
+
'#fbbf24',
|
|
1703
|
+
'#c084fc',
|
|
1704
|
+
'#f87171',
|
|
1705
|
+
'#22d3ee',
|
|
1706
|
+
'#fb923c',
|
|
1707
|
+
'#f472b6',
|
|
1708
|
+
];
|
|
849
1709
|
|
|
850
1710
|
// ===== State =====
|
|
851
1711
|
const managed = new Map(); // sessionId -> ManagedSession
|
|
@@ -861,7 +1721,10 @@
|
|
|
861
1721
|
ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px';
|
|
862
1722
|
document.body.appendChild(ta);
|
|
863
1723
|
ta.select();
|
|
864
|
-
try {
|
|
1724
|
+
try {
|
|
1725
|
+
document.execCommand('copy');
|
|
1726
|
+
showToast('Copied!');
|
|
1727
|
+
} catch {}
|
|
865
1728
|
document.body.removeChild(ta);
|
|
866
1729
|
}
|
|
867
1730
|
|
|
@@ -875,33 +1738,66 @@
|
|
|
875
1738
|
|
|
876
1739
|
// ===== Terminal Themes =====
|
|
877
1740
|
const darkTermTheme = {
|
|
878
|
-
background: '#1e1e1e',
|
|
1741
|
+
background: '#1e1e1e',
|
|
1742
|
+
foreground: '#d4d4d4',
|
|
1743
|
+
cursor: '#aeafad',
|
|
1744
|
+
cursorAccent: '#1e1e1e',
|
|
879
1745
|
selectionBackground: 'rgba(38, 79, 120, 0.5)',
|
|
880
|
-
black: '#000000',
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1746
|
+
black: '#000000',
|
|
1747
|
+
red: '#cd3131',
|
|
1748
|
+
green: '#0dbc79',
|
|
1749
|
+
yellow: '#e5e510',
|
|
1750
|
+
blue: '#2472c8',
|
|
1751
|
+
magenta: '#bc3fbc',
|
|
1752
|
+
cyan: '#11a8cd',
|
|
1753
|
+
white: '#e5e5e5',
|
|
1754
|
+
brightBlack: '#666666',
|
|
1755
|
+
brightRed: '#f14c4c',
|
|
1756
|
+
brightGreen: '#23d18b',
|
|
1757
|
+
brightYellow: '#f5f543',
|
|
1758
|
+
brightBlue: '#3b8eea',
|
|
1759
|
+
brightMagenta: '#d670d6',
|
|
1760
|
+
brightCyan: '#29b8db',
|
|
1761
|
+
brightWhite: '#e5e5e5',
|
|
885
1762
|
};
|
|
886
1763
|
const lightTermTheme = {
|
|
887
|
-
background: '#ffffff',
|
|
1764
|
+
background: '#ffffff',
|
|
1765
|
+
foreground: '#1e1e1e',
|
|
1766
|
+
cursor: '#000000',
|
|
1767
|
+
cursorAccent: '#ffffff',
|
|
888
1768
|
selectionBackground: 'rgba(0, 120, 215, 0.3)',
|
|
889
|
-
black: '#000000',
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1769
|
+
black: '#000000',
|
|
1770
|
+
red: '#cd3131',
|
|
1771
|
+
green: '#00bc7c',
|
|
1772
|
+
yellow: '#949800',
|
|
1773
|
+
blue: '#0451a5',
|
|
1774
|
+
magenta: '#bc05bc',
|
|
1775
|
+
cyan: '#0598bc',
|
|
1776
|
+
white: '#555555',
|
|
1777
|
+
brightBlack: '#666666',
|
|
1778
|
+
brightRed: '#cd3131',
|
|
1779
|
+
brightGreen: '#14ce14',
|
|
1780
|
+
brightYellow: '#b5ba00',
|
|
1781
|
+
brightBlue: '#0451a5',
|
|
1782
|
+
brightMagenta: '#bc05bc',
|
|
1783
|
+
brightCyan: '#0598bc',
|
|
1784
|
+
brightWhite: '#a5a5a5',
|
|
894
1785
|
};
|
|
895
1786
|
|
|
896
1787
|
// ===== Theme =====
|
|
897
|
-
function getTheme() {
|
|
1788
|
+
function getTheme() {
|
|
1789
|
+
return localStorage.getItem('termbeam-theme') || 'dark';
|
|
1790
|
+
}
|
|
898
1791
|
function applyTheme(theme) {
|
|
899
1792
|
document.documentElement.setAttribute('data-theme', theme);
|
|
900
|
-
document.querySelector('meta[name="theme-color"]').content =
|
|
1793
|
+
document.querySelector('meta[name="theme-color"]').content =
|
|
1794
|
+
theme === 'light' ? '#f3f3f3' : '#1e1e1e';
|
|
901
1795
|
const btn = document.getElementById('theme-toggle');
|
|
902
|
-
if (btn)
|
|
903
|
-
|
|
904
|
-
|
|
1796
|
+
if (btn)
|
|
1797
|
+
btn.innerHTML =
|
|
1798
|
+
theme === 'light'
|
|
1799
|
+
? '<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>'
|
|
1800
|
+
: '<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>';
|
|
905
1801
|
localStorage.setItem('termbeam-theme', theme);
|
|
906
1802
|
for (const [, ms] of managed) {
|
|
907
1803
|
ms.term.options.theme = theme === 'light' ? lightTermTheme : darkTermTheme;
|
|
@@ -913,17 +1809,39 @@
|
|
|
913
1809
|
});
|
|
914
1810
|
|
|
915
1811
|
// ===== Font Loading (non-blocking) =====
|
|
916
|
-
const nerdFont = new FontFace(
|
|
917
|
-
|
|
918
|
-
|
|
1812
|
+
const nerdFont = new FontFace(
|
|
1813
|
+
'NerdFont',
|
|
1814
|
+
"url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')",
|
|
1815
|
+
);
|
|
1816
|
+
nerdFont
|
|
1817
|
+
.load()
|
|
1818
|
+
.then((font) => {
|
|
1819
|
+
document.fonts.add(font);
|
|
1820
|
+
})
|
|
1821
|
+
.catch(() => {
|
|
1822
|
+
console.warn('Nerd Font failed to load, using fallback');
|
|
1823
|
+
});
|
|
919
1824
|
|
|
920
1825
|
// Start immediately — don't wait for font
|
|
921
1826
|
init();
|
|
922
1827
|
|
|
923
1828
|
// ===== Helpers =====
|
|
924
|
-
function esc(str) {
|
|
925
|
-
|
|
926
|
-
|
|
1829
|
+
function esc(str) {
|
|
1830
|
+
const d = document.createElement('div');
|
|
1831
|
+
d.textContent = str;
|
|
1832
|
+
return d.innerHTML;
|
|
1833
|
+
}
|
|
1834
|
+
function escAttr(str) {
|
|
1835
|
+
return String(str)
|
|
1836
|
+
.replace(/&/g, '&')
|
|
1837
|
+
.replace(/"/g, '"')
|
|
1838
|
+
.replace(/'/g, ''')
|
|
1839
|
+
.replace(/</g, '<')
|
|
1840
|
+
.replace(/>/g, '>');
|
|
1841
|
+
}
|
|
1842
|
+
function safeColor(c) {
|
|
1843
|
+
return /^(#[0-9a-fA-F]{3,8}|var\(--[a-z-]+\)|[a-z]+)$/.test(c) ? c : 'var(--text-muted)';
|
|
1844
|
+
}
|
|
927
1845
|
|
|
928
1846
|
function showToast(msg) {
|
|
929
1847
|
const toast = document.getElementById('copy-toast');
|
|
@@ -943,7 +1861,8 @@
|
|
|
943
1861
|
}
|
|
944
1862
|
|
|
945
1863
|
// ===== Zoom =====
|
|
946
|
-
const MIN_FONT = 2,
|
|
1864
|
+
const MIN_FONT = 2,
|
|
1865
|
+
MAX_FONT = 28;
|
|
947
1866
|
let fontSize = parseInt(localStorage.getItem('termbeam-fontsize') || '8', 10);
|
|
948
1867
|
|
|
949
1868
|
function applyZoom(size) {
|
|
@@ -973,25 +1892,36 @@
|
|
|
973
1892
|
function getTabOrder() {
|
|
974
1893
|
let order = JSON.parse(localStorage.getItem('termbeam-tab-order') || '[]');
|
|
975
1894
|
const allIds = [...managed.keys()];
|
|
976
|
-
for (const id of allIds) {
|
|
977
|
-
|
|
1895
|
+
for (const id of allIds) {
|
|
1896
|
+
if (!order.includes(id)) order.push(id);
|
|
1897
|
+
}
|
|
1898
|
+
order = order.filter((id) => managed.has(id));
|
|
978
1899
|
return order;
|
|
979
1900
|
}
|
|
980
|
-
function saveTabOrder(order) {
|
|
1901
|
+
function saveTabOrder(order) {
|
|
1902
|
+
localStorage.setItem('termbeam-tab-order', JSON.stringify(order));
|
|
1903
|
+
}
|
|
981
1904
|
|
|
982
1905
|
// ===== Init =====
|
|
983
1906
|
async function init() {
|
|
984
|
-
const sessionList = await fetch('/api/sessions').then(r => r.json());
|
|
1907
|
+
const sessionList = await fetch('/api/sessions').then((r) => r.json());
|
|
985
1908
|
const initialId = new URLSearchParams(location.search).get('id');
|
|
986
1909
|
|
|
987
1910
|
for (const s of sessionList) addSession(s);
|
|
988
1911
|
|
|
989
|
-
const startId =
|
|
990
|
-
|
|
991
|
-
|
|
1912
|
+
const startId =
|
|
1913
|
+
initialId && managed.has(initialId)
|
|
1914
|
+
? initialId
|
|
1915
|
+
: sessionList.length > 0
|
|
1916
|
+
? sessionList[0].id
|
|
1917
|
+
: null;
|
|
992
1918
|
|
|
993
1919
|
if (startId) activateSession(startId);
|
|
994
|
-
else {
|
|
1920
|
+
else {
|
|
1921
|
+
sessionNameEl.textContent = 'No sessions';
|
|
1922
|
+
statusText.textContent = '';
|
|
1923
|
+
document.getElementById('stop-btn').style.display = 'none';
|
|
1924
|
+
}
|
|
995
1925
|
|
|
996
1926
|
renderTabs();
|
|
997
1927
|
setupKeyBar();
|
|
@@ -1004,12 +1934,17 @@
|
|
|
1004
1934
|
|
|
1005
1935
|
// Zoom
|
|
1006
1936
|
document.getElementById('zoom-in').addEventListener('click', () => applyZoom(fontSize + 2));
|
|
1007
|
-
document
|
|
1937
|
+
document
|
|
1938
|
+
.getElementById('zoom-out')
|
|
1939
|
+
.addEventListener('click', () => applyZoom(fontSize - 2));
|
|
1008
1940
|
|
|
1009
1941
|
// Resize
|
|
1010
1942
|
function doResize() {
|
|
1011
1943
|
for (const [, ms] of managed) {
|
|
1012
|
-
if (ms.container.classList.contains('visible')) {
|
|
1944
|
+
if (ms.container.classList.contains('visible')) {
|
|
1945
|
+
ms.fitAddon.fit();
|
|
1946
|
+
sendResize(ms);
|
|
1947
|
+
}
|
|
1013
1948
|
}
|
|
1014
1949
|
}
|
|
1015
1950
|
window.addEventListener('resize', doResize);
|
|
@@ -1032,7 +1967,7 @@
|
|
|
1032
1967
|
keyBar.style.bottom = keyboardHeight + 'px';
|
|
1033
1968
|
keyBar.style.height = '52px';
|
|
1034
1969
|
keyBar.style.paddingBottom = '0px';
|
|
1035
|
-
terminalsWrapper.style.bottom =
|
|
1970
|
+
terminalsWrapper.style.bottom = 52 + keyboardHeight + 'px';
|
|
1036
1971
|
} else {
|
|
1037
1972
|
keyBar.style.bottom = '0px';
|
|
1038
1973
|
keyBar.style.height = '';
|
|
@@ -1053,9 +1988,13 @@
|
|
|
1053
1988
|
window.visualViewport.addEventListener('scroll', onViewportScroll);
|
|
1054
1989
|
// Page should never scroll — catch any browser-initiated scroll
|
|
1055
1990
|
// (e.g. iOS scrolling to show focused xterm textarea behind keyboard)
|
|
1056
|
-
window.addEventListener(
|
|
1057
|
-
|
|
1058
|
-
|
|
1991
|
+
window.addEventListener(
|
|
1992
|
+
'scroll',
|
|
1993
|
+
() => {
|
|
1994
|
+
if (window.scrollY !== 0) resetScroll();
|
|
1995
|
+
},
|
|
1996
|
+
{ passive: true },
|
|
1997
|
+
);
|
|
1059
1998
|
}
|
|
1060
1999
|
|
|
1061
2000
|
// Split toggle
|
|
@@ -1085,7 +2024,10 @@
|
|
|
1085
2024
|
document.getElementById('reconnect-btn').addEventListener('click', () => {
|
|
1086
2025
|
const ms = managed.get(activeId);
|
|
1087
2026
|
if (ms) {
|
|
1088
|
-
if (ms.reconnectTimer) {
|
|
2027
|
+
if (ms.reconnectTimer) {
|
|
2028
|
+
clearTimeout(ms.reconnectTimer);
|
|
2029
|
+
ms.reconnectTimer = null;
|
|
2030
|
+
}
|
|
1089
2031
|
ms.exited = false;
|
|
1090
2032
|
ms.reconnectDelay = 3000;
|
|
1091
2033
|
ms.term.clear();
|
|
@@ -1101,9 +2043,12 @@
|
|
|
1101
2043
|
});
|
|
1102
2044
|
|
|
1103
2045
|
// Version
|
|
1104
|
-
fetch('/api/version')
|
|
1105
|
-
|
|
1106
|
-
|
|
2046
|
+
fetch('/api/version')
|
|
2047
|
+
.then((r) => r.json())
|
|
2048
|
+
.then((d) => {
|
|
2049
|
+
document.getElementById('version-text').textContent = 'v' + d.version;
|
|
2050
|
+
})
|
|
2051
|
+
.catch(() => {});
|
|
1107
2052
|
}
|
|
1108
2053
|
|
|
1109
2054
|
// ===== Session Management =====
|
|
@@ -1111,11 +2056,17 @@
|
|
|
1111
2056
|
if (managed.has(data.id)) return;
|
|
1112
2057
|
|
|
1113
2058
|
const term = new window.Terminal({
|
|
1114
|
-
cursorBlink: true,
|
|
1115
|
-
|
|
1116
|
-
|
|
2059
|
+
cursorBlink: true,
|
|
2060
|
+
fontSize: fontSize,
|
|
2061
|
+
fontFamily:
|
|
2062
|
+
"'NerdFont', 'JetBrains Mono', 'MesloLGS NF', 'Hack Nerd Font', 'Fira Code', Menlo, monospace",
|
|
2063
|
+
fontWeight: 'normal',
|
|
2064
|
+
fontWeightBold: 'bold',
|
|
2065
|
+
letterSpacing: 0,
|
|
2066
|
+
lineHeight: 1.1,
|
|
1117
2067
|
theme: getTheme() === 'light' ? lightTermTheme : darkTermTheme,
|
|
1118
|
-
allowProposedApi: true,
|
|
2068
|
+
allowProposedApi: true,
|
|
2069
|
+
scrollback: 10000,
|
|
1119
2070
|
});
|
|
1120
2071
|
|
|
1121
2072
|
const fitAddon = new window.FitAddon.FitAddon();
|
|
@@ -1130,46 +2081,64 @@
|
|
|
1130
2081
|
|
|
1131
2082
|
// Pointer-event scroll handler — uses setPointerCapture so scrolling
|
|
1132
2083
|
// survives xterm DOM re-renders under the finger (touch on a letter).
|
|
1133
|
-
(function() {
|
|
1134
|
-
let startY = null,
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
startY = e.clientY;
|
|
1139
|
-
scrolling = false;
|
|
1140
|
-
accum = 0;
|
|
1141
|
-
ptrId = e.pointerId;
|
|
1142
|
-
}, { capture: true });
|
|
2084
|
+
(function () {
|
|
2085
|
+
let startY = null,
|
|
2086
|
+
scrolling = false,
|
|
2087
|
+
accum = 0,
|
|
2088
|
+
ptrId = null;
|
|
1143
2089
|
|
|
1144
|
-
container.addEventListener(
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
2090
|
+
container.addEventListener(
|
|
2091
|
+
'pointerdown',
|
|
2092
|
+
(e) => {
|
|
2093
|
+
if (e.pointerType !== 'touch' || ptrId !== null) return;
|
|
2094
|
+
startY = e.clientY;
|
|
2095
|
+
scrolling = false;
|
|
2096
|
+
accum = 0;
|
|
2097
|
+
ptrId = e.pointerId;
|
|
2098
|
+
},
|
|
2099
|
+
{ capture: true },
|
|
2100
|
+
);
|
|
2101
|
+
|
|
2102
|
+
container.addEventListener(
|
|
2103
|
+
'pointermove',
|
|
2104
|
+
(e) => {
|
|
2105
|
+
if (e.pointerId !== ptrId) return;
|
|
2106
|
+
const y = e.clientY;
|
|
2107
|
+
const delta = startY - y;
|
|
2108
|
+
if (!scrolling) {
|
|
2109
|
+
if (Math.abs(delta) > 10) {
|
|
2110
|
+
scrolling = true;
|
|
2111
|
+
term.clearSelection();
|
|
2112
|
+
// Lock all future pointer events to this element —
|
|
2113
|
+
// immune to DOM mutations, xterm re-renders, etc.
|
|
2114
|
+
try {
|
|
2115
|
+
container.setPointerCapture(ptrId);
|
|
2116
|
+
} catch (_) {}
|
|
2117
|
+
} else {
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
1157
2120
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
2121
|
+
e.preventDefault();
|
|
2122
|
+
e.stopPropagation();
|
|
2123
|
+
startY = y;
|
|
2124
|
+
const lineH = term.options.fontSize * (term.options.lineHeight || 1);
|
|
2125
|
+
accum += delta;
|
|
2126
|
+
const lines = Math.trunc(accum / lineH);
|
|
2127
|
+
if (lines !== 0) {
|
|
2128
|
+
term.scrollLines(lines);
|
|
2129
|
+
accum -= lines * lineH;
|
|
2130
|
+
}
|
|
2131
|
+
},
|
|
2132
|
+
{ capture: true, passive: false },
|
|
2133
|
+
);
|
|
1167
2134
|
|
|
1168
2135
|
function endScroll(e) {
|
|
1169
2136
|
if (e.pointerId !== ptrId) return;
|
|
1170
2137
|
if (scrolling) {
|
|
1171
2138
|
e.stopPropagation();
|
|
1172
|
-
try {
|
|
2139
|
+
try {
|
|
2140
|
+
container.releasePointerCapture(ptrId);
|
|
2141
|
+
} catch (_) {}
|
|
1173
2142
|
}
|
|
1174
2143
|
startY = null;
|
|
1175
2144
|
scrolling = false;
|
|
@@ -1184,10 +2153,13 @@
|
|
|
1184
2153
|
const sel = term.getSelection();
|
|
1185
2154
|
if (!sel) return;
|
|
1186
2155
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1187
|
-
navigator.clipboard
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
2156
|
+
navigator.clipboard
|
|
2157
|
+
.writeText(sel)
|
|
2158
|
+
.then(() => showToast('Copied!'))
|
|
2159
|
+
.catch(() => {
|
|
2160
|
+
// Fallback for non-secure contexts (HTTP over LAN)
|
|
2161
|
+
copyFallback(sel);
|
|
2162
|
+
});
|
|
1191
2163
|
} else {
|
|
1192
2164
|
copyFallback(sel);
|
|
1193
2165
|
}
|
|
@@ -1207,19 +2179,26 @@
|
|
|
1207
2179
|
});
|
|
1208
2180
|
|
|
1209
2181
|
const ms = {
|
|
1210
|
-
id: data.id,
|
|
2182
|
+
id: data.id,
|
|
2183
|
+
name: data.name,
|
|
1211
2184
|
color: data.color || SESSION_COLORS[managed.size % SESSION_COLORS.length],
|
|
1212
|
-
shell: data.shell,
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
2185
|
+
shell: data.shell,
|
|
2186
|
+
cwd: data.cwd,
|
|
2187
|
+
pid: data.pid,
|
|
2188
|
+
term,
|
|
2189
|
+
fitAddon,
|
|
2190
|
+
container,
|
|
2191
|
+
ws: null,
|
|
2192
|
+
exited: false,
|
|
2193
|
+
reconnectTimer: null,
|
|
2194
|
+
reconnectDelay: 3000,
|
|
1216
2195
|
lastActivity: data.lastActivity || Date.now(),
|
|
1217
2196
|
};
|
|
1218
2197
|
|
|
1219
2198
|
managed.set(data.id, ms);
|
|
1220
2199
|
|
|
1221
2200
|
// Terminal input → WebSocket
|
|
1222
|
-
term.onData(input => {
|
|
2201
|
+
term.onData((input) => {
|
|
1223
2202
|
if (ms.ws && ms.ws.readyState === 1) {
|
|
1224
2203
|
ms.ws.send(JSON.stringify({ type: 'input', data: input }));
|
|
1225
2204
|
}
|
|
@@ -1230,8 +2209,15 @@
|
|
|
1230
2209
|
}
|
|
1231
2210
|
|
|
1232
2211
|
function connectSession(ms) {
|
|
1233
|
-
if (ms.reconnectTimer) {
|
|
1234
|
-
|
|
2212
|
+
if (ms.reconnectTimer) {
|
|
2213
|
+
clearTimeout(ms.reconnectTimer);
|
|
2214
|
+
ms.reconnectTimer = null;
|
|
2215
|
+
}
|
|
2216
|
+
if (ms.ws) {
|
|
2217
|
+
try {
|
|
2218
|
+
ms.ws.close();
|
|
2219
|
+
} catch {}
|
|
2220
|
+
}
|
|
1235
2221
|
|
|
1236
2222
|
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
1237
2223
|
const ws = new WebSocket(`${proto}://${location.host}/ws`);
|
|
@@ -1260,20 +2246,27 @@
|
|
|
1260
2246
|
if (ms.container.classList.contains('visible')) {
|
|
1261
2247
|
sendResize(ms);
|
|
1262
2248
|
}
|
|
1263
|
-
if (ms.id === activeId) {
|
|
2249
|
+
if (ms.id === activeId) {
|
|
2250
|
+
statusDot.className = 'connected';
|
|
2251
|
+
statusText.textContent = '';
|
|
2252
|
+
}
|
|
1264
2253
|
} else if (msg.type === 'exit') {
|
|
1265
2254
|
ms.exited = true;
|
|
1266
2255
|
renderTabs();
|
|
1267
2256
|
if (ms.id === activeId) {
|
|
1268
2257
|
statusText.textContent = 'Exited (code ' + msg.code + ')';
|
|
1269
2258
|
statusDot.className = '';
|
|
1270
|
-
reconnectOverlay.querySelector('.msg').textContent =
|
|
2259
|
+
reconnectOverlay.querySelector('.msg').textContent =
|
|
2260
|
+
'Session exited (code ' + msg.code + ')';
|
|
1271
2261
|
reconnectOverlay.classList.add('visible');
|
|
1272
2262
|
}
|
|
1273
2263
|
} else if (msg.type === 'error') {
|
|
1274
2264
|
if (msg.message === 'Session not found') {
|
|
1275
2265
|
ms.exited = true;
|
|
1276
|
-
if (ms.reconnectTimer) {
|
|
2266
|
+
if (ms.reconnectTimer) {
|
|
2267
|
+
clearTimeout(ms.reconnectTimer);
|
|
2268
|
+
ms.reconnectTimer = null;
|
|
2269
|
+
}
|
|
1277
2270
|
renderTabs();
|
|
1278
2271
|
}
|
|
1279
2272
|
if (ms.id === activeId) {
|
|
@@ -1289,7 +2282,10 @@
|
|
|
1289
2282
|
|
|
1290
2283
|
ws.onclose = () => {
|
|
1291
2284
|
if (ms.ws !== ws) return;
|
|
1292
|
-
if (ms.id === activeId) {
|
|
2285
|
+
if (ms.id === activeId) {
|
|
2286
|
+
statusDot.className = '';
|
|
2287
|
+
statusText.textContent = 'Disconnected';
|
|
2288
|
+
}
|
|
1293
2289
|
if (!ms.exited) {
|
|
1294
2290
|
ms.reconnectTimer = setTimeout(() => {
|
|
1295
2291
|
ms.reconnectDelay = Math.min(ms.reconnectDelay * 1.5, 30000);
|
|
@@ -1345,19 +2341,28 @@
|
|
|
1345
2341
|
}
|
|
1346
2342
|
stopBtn.style.display = '';
|
|
1347
2343
|
sessionNameEl.textContent = ms.name;
|
|
1348
|
-
statusDot.className =
|
|
1349
|
-
statusText.textContent =
|
|
2344
|
+
statusDot.className = ms.ws && ms.ws.readyState === 1 ? 'connected' : '';
|
|
2345
|
+
statusText.textContent =
|
|
2346
|
+
ms.ws && ms.ws.readyState === 1 ? '' : ms.exited ? 'Exited' : 'Disconnected';
|
|
1350
2347
|
}
|
|
1351
2348
|
|
|
1352
2349
|
async function removeSession(id) {
|
|
1353
2350
|
const ms = managed.get(id);
|
|
1354
2351
|
if (ms) {
|
|
1355
2352
|
ms.exited = true;
|
|
1356
|
-
if (ms.reconnectTimer) {
|
|
1357
|
-
|
|
2353
|
+
if (ms.reconnectTimer) {
|
|
2354
|
+
clearTimeout(ms.reconnectTimer);
|
|
2355
|
+
ms.reconnectTimer = null;
|
|
2356
|
+
}
|
|
2357
|
+
if (ms.ws)
|
|
2358
|
+
try {
|
|
2359
|
+
ms.ws.close();
|
|
2360
|
+
} catch {}
|
|
1358
2361
|
}
|
|
1359
2362
|
|
|
1360
|
-
try {
|
|
2363
|
+
try {
|
|
2364
|
+
await fetch('/api/sessions/' + encodeURIComponent(id), { method: 'DELETE' });
|
|
2365
|
+
} catch {}
|
|
1361
2366
|
|
|
1362
2367
|
if (ms) {
|
|
1363
2368
|
ms.term.dispose();
|
|
@@ -1384,25 +2389,44 @@
|
|
|
1384
2389
|
function renderTabs() {
|
|
1385
2390
|
const order = getTabOrder();
|
|
1386
2391
|
|
|
1387
|
-
tabListEl.innerHTML = order
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
2392
|
+
tabListEl.innerHTML = order
|
|
2393
|
+
.map((id) => {
|
|
2394
|
+
const ms = managed.get(id);
|
|
2395
|
+
if (!ms) return '';
|
|
2396
|
+
const isActive = id === activeId;
|
|
2397
|
+
const isSplit = splitMode && id === splitSecondId;
|
|
2398
|
+
const statusColor = ms.exited
|
|
2399
|
+
? 'var(--danger)'
|
|
2400
|
+
: ms.ws && ms.ws.readyState === 1
|
|
2401
|
+
? 'var(--success)'
|
|
2402
|
+
: 'var(--text-muted)';
|
|
2403
|
+
const activity = getActivityLabel(ms.lastActivity);
|
|
2404
|
+
let cls = 'session-tab';
|
|
2405
|
+
if (isActive) cls += ' active';
|
|
2406
|
+
if (isSplit) cls += ' in-split';
|
|
2407
|
+
return (
|
|
2408
|
+
'<button class="' +
|
|
2409
|
+
cls +
|
|
2410
|
+
'" data-id="' +
|
|
2411
|
+
escAttr(id) +
|
|
2412
|
+
'" draggable="true">' +
|
|
2413
|
+
'<span class="tab-dot" style="background:' +
|
|
2414
|
+
safeColor(ms.color) +
|
|
2415
|
+
'"></span>' +
|
|
2416
|
+
'<span class="tab-name">' +
|
|
2417
|
+
esc(ms.name) +
|
|
2418
|
+
'</span>' +
|
|
2419
|
+
(activity ? '<span class="tab-activity">' + activity + '</span>' : '') +
|
|
2420
|
+
'<span class="tab-status" style="background:' +
|
|
2421
|
+
safeColor(statusColor) +
|
|
2422
|
+
'"></span>' +
|
|
2423
|
+
'<span class="tab-close" data-close="' +
|
|
2424
|
+
escAttr(id) +
|
|
2425
|
+
'">×</span>' +
|
|
2426
|
+
'</button>'
|
|
2427
|
+
);
|
|
2428
|
+
})
|
|
2429
|
+
.join('');
|
|
1406
2430
|
|
|
1407
2431
|
attachTabHandlers();
|
|
1408
2432
|
initTabDrag();
|
|
@@ -1423,7 +2447,10 @@
|
|
|
1423
2447
|
let lastNonEmpty = -1;
|
|
1424
2448
|
for (let i = totalRows - 1; i >= 0; i--) {
|
|
1425
2449
|
const line = buf.getLine(i);
|
|
1426
|
-
if (line && line.translateToString(true).trim() !== '') {
|
|
2450
|
+
if (line && line.translateToString(true).trim() !== '') {
|
|
2451
|
+
lastNonEmpty = i;
|
|
2452
|
+
break;
|
|
2453
|
+
}
|
|
1427
2454
|
}
|
|
1428
2455
|
if (lastNonEmpty < 0) return [];
|
|
1429
2456
|
const start = Math.max(0, lastNonEmpty - count + 1);
|
|
@@ -1465,12 +2492,15 @@
|
|
|
1465
2492
|
function hidePreview() {
|
|
1466
2493
|
previewEl.classList.remove('visible');
|
|
1467
2494
|
previewVisible = false;
|
|
1468
|
-
if (previewTimer) {
|
|
2495
|
+
if (previewTimer) {
|
|
2496
|
+
clearTimeout(previewTimer);
|
|
2497
|
+
previewTimer = null;
|
|
2498
|
+
}
|
|
1469
2499
|
}
|
|
1470
2500
|
|
|
1471
2501
|
function attachTabHandlers() {
|
|
1472
|
-
tabListEl.querySelectorAll('.session-tab').forEach(tab => {
|
|
1473
|
-
tab.addEventListener('click', e => {
|
|
2502
|
+
tabListEl.querySelectorAll('.session-tab').forEach((tab) => {
|
|
2503
|
+
tab.addEventListener('click', (e) => {
|
|
1474
2504
|
if (e.target.dataset.close) return;
|
|
1475
2505
|
hidePreview();
|
|
1476
2506
|
activateSession(tab.dataset.id);
|
|
@@ -1483,8 +2513,8 @@
|
|
|
1483
2513
|
});
|
|
1484
2514
|
tab.addEventListener('mouseleave', () => hidePreview());
|
|
1485
2515
|
});
|
|
1486
|
-
tabListEl.querySelectorAll('.tab-close').forEach(btn => {
|
|
1487
|
-
btn.addEventListener('click', e => {
|
|
2516
|
+
tabListEl.querySelectorAll('.tab-close').forEach((btn) => {
|
|
2517
|
+
btn.addEventListener('click', (e) => {
|
|
1488
2518
|
e.stopPropagation();
|
|
1489
2519
|
if (confirm('Close this session?')) removeSession(btn.dataset.close);
|
|
1490
2520
|
});
|
|
@@ -1509,30 +2539,50 @@
|
|
|
1509
2539
|
|
|
1510
2540
|
function renderSidePanel() {
|
|
1511
2541
|
const order = getTabOrder();
|
|
1512
|
-
sidePanelList.innerHTML = order
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
2542
|
+
sidePanelList.innerHTML = order
|
|
2543
|
+
.map((id) => {
|
|
2544
|
+
const ms = managed.get(id);
|
|
2545
|
+
if (!ms) return '';
|
|
2546
|
+
const isActive = id === activeId;
|
|
2547
|
+
const statusColor = ms.exited
|
|
2548
|
+
? 'var(--danger)'
|
|
2549
|
+
: ms.ws && ms.ws.readyState === 1
|
|
2550
|
+
? 'var(--success)'
|
|
2551
|
+
: 'var(--text-muted)';
|
|
2552
|
+
const activity = getActivityLabel(ms.lastActivity);
|
|
2553
|
+
const lines = getTerminalLines(ms, 6);
|
|
2554
|
+
const previewContent =
|
|
2555
|
+
lines.length > 0
|
|
2556
|
+
? '<div class="side-panel-card-preview">' + esc(lines.join('\n')) + '</div>'
|
|
2557
|
+
: '<div class="side-panel-card-preview empty">No output yet</div>';
|
|
2558
|
+
return (
|
|
2559
|
+
'<div class="side-panel-card' +
|
|
2560
|
+
(isActive ? ' active' : '') +
|
|
2561
|
+
'" data-id="' +
|
|
2562
|
+
escAttr(id) +
|
|
2563
|
+
'">' +
|
|
2564
|
+
'<div class="side-panel-card-header">' +
|
|
2565
|
+
'<span class="side-panel-card-dot" style="background:' +
|
|
2566
|
+
safeColor(ms.color) +
|
|
2567
|
+
'"></span>' +
|
|
2568
|
+
'<span class="side-panel-card-name">' +
|
|
2569
|
+
esc(ms.name) +
|
|
2570
|
+
'</span>' +
|
|
2571
|
+
'<span class="side-panel-card-status" style="background:' +
|
|
2572
|
+
safeColor(statusColor) +
|
|
2573
|
+
'"></span>' +
|
|
2574
|
+
'<button class="side-panel-card-close" data-close-id="' +
|
|
2575
|
+
escAttr(id) +
|
|
2576
|
+
'" title="Close session">×</button>' +
|
|
2577
|
+
'</div>' +
|
|
2578
|
+
(activity ? '<div class="side-panel-card-meta">' + activity + ' ago</div>' : '') +
|
|
2579
|
+
previewContent +
|
|
2580
|
+
'</div>'
|
|
2581
|
+
);
|
|
2582
|
+
})
|
|
2583
|
+
.join('');
|
|
2584
|
+
|
|
2585
|
+
sidePanelList.querySelectorAll('.side-panel-card-close').forEach((btn) => {
|
|
1536
2586
|
btn.addEventListener('click', async (e) => {
|
|
1537
2587
|
e.stopPropagation();
|
|
1538
2588
|
const id = btn.dataset.closeId;
|
|
@@ -1543,7 +2593,7 @@
|
|
|
1543
2593
|
});
|
|
1544
2594
|
});
|
|
1545
2595
|
|
|
1546
|
-
sidePanelList.querySelectorAll('.side-panel-card').forEach(card => {
|
|
2596
|
+
sidePanelList.querySelectorAll('.side-panel-card').forEach((card) => {
|
|
1547
2597
|
card.addEventListener('click', () => {
|
|
1548
2598
|
activateSession(card.dataset.id);
|
|
1549
2599
|
closeSidePanel();
|
|
@@ -1560,40 +2610,45 @@
|
|
|
1560
2610
|
let dragId = null;
|
|
1561
2611
|
let dragEl = null;
|
|
1562
2612
|
|
|
1563
|
-
tabListEl.querySelectorAll('.session-tab').forEach(tab => {
|
|
2613
|
+
tabListEl.querySelectorAll('.session-tab').forEach((tab) => {
|
|
1564
2614
|
// --- Touch drag (long press) + preview ---
|
|
1565
2615
|
let longPressTimer = null;
|
|
1566
2616
|
let previewHoldTimer = null;
|
|
1567
|
-
let startX = 0,
|
|
2617
|
+
let startX = 0,
|
|
2618
|
+
startY = 0;
|
|
1568
2619
|
let isDragging = false;
|
|
1569
2620
|
let didPreview = false;
|
|
1570
2621
|
|
|
1571
|
-
tab.addEventListener(
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
2622
|
+
tab.addEventListener(
|
|
2623
|
+
'touchstart',
|
|
2624
|
+
(e) => {
|
|
2625
|
+
startX = e.touches[0].clientX;
|
|
2626
|
+
startY = e.touches[0].clientY;
|
|
2627
|
+
didPreview = false;
|
|
2628
|
+
|
|
2629
|
+
// 200ms: show preview (if not active tab)
|
|
2630
|
+
if (tab.dataset.id !== activeId) {
|
|
2631
|
+
previewHoldTimer = setTimeout(() => {
|
|
2632
|
+
didPreview = true;
|
|
2633
|
+
showPreview(tab, tab.dataset.id);
|
|
2634
|
+
if (navigator.vibrate) navigator.vibrate(30);
|
|
2635
|
+
}, 200);
|
|
2636
|
+
}
|
|
1584
2637
|
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
2638
|
+
// 600ms: enter drag mode (dismiss preview)
|
|
2639
|
+
longPressTimer = setTimeout(() => {
|
|
2640
|
+
hidePreview();
|
|
2641
|
+
isDragging = true;
|
|
2642
|
+
dragId = tab.dataset.id;
|
|
2643
|
+
dragEl = tab;
|
|
2644
|
+
tab.classList.add('dragging');
|
|
2645
|
+
if (navigator.vibrate) navigator.vibrate(50);
|
|
2646
|
+
}, 600);
|
|
2647
|
+
},
|
|
2648
|
+
{ passive: true },
|
|
2649
|
+
);
|
|
2650
|
+
|
|
2651
|
+
tab.addEventListener('touchmove', (e) => {
|
|
1597
2652
|
const dx = Math.abs(e.touches[0].clientX - startX);
|
|
1598
2653
|
const dy = Math.abs(e.touches[0].clientY - startY);
|
|
1599
2654
|
if (!isDragging && (dx > 10 || dy > 10)) {
|
|
@@ -1626,7 +2681,9 @@
|
|
|
1626
2681
|
isDragging = false;
|
|
1627
2682
|
dragEl.classList.remove('dragging');
|
|
1628
2683
|
// Save new order from DOM
|
|
1629
|
-
const newOrder = [...tabListEl.querySelectorAll('.session-tab')].map(
|
|
2684
|
+
const newOrder = [...tabListEl.querySelectorAll('.session-tab')].map(
|
|
2685
|
+
(t) => t.dataset.id,
|
|
2686
|
+
);
|
|
1630
2687
|
saveTabOrder(newOrder);
|
|
1631
2688
|
dragId = null;
|
|
1632
2689
|
dragEl = null;
|
|
@@ -1634,19 +2691,19 @@
|
|
|
1634
2691
|
});
|
|
1635
2692
|
|
|
1636
2693
|
// --- Desktop drag ---
|
|
1637
|
-
tab.addEventListener('dragstart', e => {
|
|
2694
|
+
tab.addEventListener('dragstart', (e) => {
|
|
1638
2695
|
dragId = tab.dataset.id;
|
|
1639
2696
|
dragEl = tab;
|
|
1640
2697
|
tab.classList.add('dragging');
|
|
1641
2698
|
e.dataTransfer.effectAllowed = 'move';
|
|
1642
2699
|
});
|
|
1643
2700
|
|
|
1644
|
-
tab.addEventListener('dragover', e => {
|
|
2701
|
+
tab.addEventListener('dragover', (e) => {
|
|
1645
2702
|
e.preventDefault();
|
|
1646
2703
|
e.dataTransfer.dropEffect = 'move';
|
|
1647
2704
|
});
|
|
1648
2705
|
|
|
1649
|
-
tab.addEventListener('drop', e => {
|
|
2706
|
+
tab.addEventListener('drop', (e) => {
|
|
1650
2707
|
e.preventDefault();
|
|
1651
2708
|
if (dragId && tab.dataset.id !== dragId) {
|
|
1652
2709
|
const rect = tab.getBoundingClientRect();
|
|
@@ -1655,7 +2712,9 @@
|
|
|
1655
2712
|
} else {
|
|
1656
2713
|
tab.parentNode.insertBefore(dragEl, tab.nextSibling);
|
|
1657
2714
|
}
|
|
1658
|
-
const newOrder = [...tabListEl.querySelectorAll('.session-tab')].map(
|
|
2715
|
+
const newOrder = [...tabListEl.querySelectorAll('.session-tab')].map(
|
|
2716
|
+
(t) => t.dataset.id,
|
|
2717
|
+
);
|
|
1659
2718
|
saveTabOrder(newOrder);
|
|
1660
2719
|
}
|
|
1661
2720
|
});
|
|
@@ -1676,7 +2735,7 @@
|
|
|
1676
2735
|
if (splitMode) {
|
|
1677
2736
|
// Find a second session to show
|
|
1678
2737
|
const order = getTabOrder();
|
|
1679
|
-
const others = order.filter(id => id !== activeId);
|
|
2738
|
+
const others = order.filter((id) => id !== activeId);
|
|
1680
2739
|
splitSecondId = others.length > 0 ? others[0] : null;
|
|
1681
2740
|
|
|
1682
2741
|
if (splitSecondId) {
|
|
@@ -1695,7 +2754,10 @@
|
|
|
1695
2754
|
|
|
1696
2755
|
requestAnimationFrame(() => {
|
|
1697
2756
|
for (const [, ms] of managed) {
|
|
1698
|
-
if (ms.container.classList.contains('visible')) {
|
|
2757
|
+
if (ms.container.classList.contains('visible')) {
|
|
2758
|
+
ms.fitAddon.fit();
|
|
2759
|
+
sendResize(ms);
|
|
2760
|
+
}
|
|
1699
2761
|
}
|
|
1700
2762
|
});
|
|
1701
2763
|
|
|
@@ -1733,19 +2795,32 @@
|
|
|
1733
2795
|
}
|
|
1734
2796
|
|
|
1735
2797
|
let keyBarTouched = false;
|
|
1736
|
-
keyBar.addEventListener('mousedown', e => {
|
|
1737
|
-
if (keyBarTouched) {
|
|
2798
|
+
keyBar.addEventListener('mousedown', (e) => {
|
|
2799
|
+
if (keyBarTouched) {
|
|
2800
|
+
keyBarTouched = false;
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
1738
2803
|
const btn = e.target.closest('.key-btn');
|
|
1739
|
-
if (btn && btn.dataset.key) {
|
|
2804
|
+
if (btn && btn.dataset.key) {
|
|
2805
|
+
e.preventDefault();
|
|
2806
|
+
startRepeat(btn);
|
|
2807
|
+
}
|
|
1740
2808
|
});
|
|
1741
2809
|
keyBar.addEventListener('mouseup', stopRepeat);
|
|
1742
2810
|
keyBar.addEventListener('mouseleave', stopRepeat);
|
|
1743
2811
|
|
|
1744
|
-
keyBar.addEventListener(
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
2812
|
+
keyBar.addEventListener(
|
|
2813
|
+
'touchstart',
|
|
2814
|
+
(e) => {
|
|
2815
|
+
keyBarTouched = true;
|
|
2816
|
+
const btn = e.target.closest('.key-btn');
|
|
2817
|
+
if (btn && btn.dataset.key) {
|
|
2818
|
+
e.preventDefault();
|
|
2819
|
+
startRepeat(btn);
|
|
2820
|
+
}
|
|
2821
|
+
},
|
|
2822
|
+
{ passive: false },
|
|
2823
|
+
);
|
|
1749
2824
|
keyBar.addEventListener('touchend', (e) => {
|
|
1750
2825
|
const btn = e.target.closest('.key-btn');
|
|
1751
2826
|
if (btn && btn.dataset.key) e.preventDefault();
|
|
@@ -1753,9 +2828,12 @@
|
|
|
1753
2828
|
});
|
|
1754
2829
|
keyBar.addEventListener('touchcancel', stopRepeat);
|
|
1755
2830
|
|
|
1756
|
-
keyBar.addEventListener('click', e => {
|
|
2831
|
+
keyBar.addEventListener('click', (e) => {
|
|
1757
2832
|
const btn = e.target.closest('.key-btn');
|
|
1758
|
-
if (btn) {
|
|
2833
|
+
if (btn) {
|
|
2834
|
+
const ms = managed.get(activeId);
|
|
2835
|
+
if (ms) ms.term.focus();
|
|
2836
|
+
}
|
|
1759
2837
|
});
|
|
1760
2838
|
}
|
|
1761
2839
|
|
|
@@ -1793,7 +2871,7 @@
|
|
|
1793
2871
|
try {
|
|
1794
2872
|
const items = await navigator.clipboard.read();
|
|
1795
2873
|
for (const item of items) {
|
|
1796
|
-
const imageType = item.types.find(t => t.startsWith('image/'));
|
|
2874
|
+
const imageType = item.types.find((t) => t.startsWith('image/'));
|
|
1797
2875
|
if (imageType) {
|
|
1798
2876
|
const blob = await item.getType(imageType);
|
|
1799
2877
|
const res = await fetch('/api/upload', {
|
|
@@ -1826,9 +2904,12 @@
|
|
|
1826
2904
|
pasteTouched = true;
|
|
1827
2905
|
handlePaste();
|
|
1828
2906
|
});
|
|
1829
|
-
pasteBtn.addEventListener('mousedown', e => e.preventDefault());
|
|
2907
|
+
pasteBtn.addEventListener('mousedown', (e) => e.preventDefault());
|
|
1830
2908
|
pasteBtn.addEventListener('click', () => {
|
|
1831
|
-
if (pasteTouched) {
|
|
2909
|
+
if (pasteTouched) {
|
|
2910
|
+
pasteTouched = false;
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
1832
2913
|
handlePaste();
|
|
1833
2914
|
});
|
|
1834
2915
|
|
|
@@ -1885,9 +2966,10 @@
|
|
|
1885
2966
|
// Show line count in title
|
|
1886
2967
|
const title = document.getElementById('select-title');
|
|
1887
2968
|
const shown = allLines.length - loadedFrom;
|
|
1888
|
-
title.textContent =
|
|
1889
|
-
|
|
1890
|
-
|
|
2969
|
+
title.textContent =
|
|
2970
|
+
allLines.length <= PAGE_SIZE
|
|
2971
|
+
? `Copy Text (${allLines.length} lines)`
|
|
2972
|
+
: `Copy Text (${shown}/${allLines.length} lines)`;
|
|
1891
2973
|
|
|
1892
2974
|
selectBtn.style.display = 'none';
|
|
1893
2975
|
selectOverlay.classList.add('visible');
|
|
@@ -1911,18 +2993,21 @@
|
|
|
1911
2993
|
title.textContent = `Copy Text (${shown}/${allLines.length} lines)`;
|
|
1912
2994
|
});
|
|
1913
2995
|
|
|
1914
|
-
selectBtn.addEventListener('mousedown', e => e.preventDefault());
|
|
2996
|
+
selectBtn.addEventListener('mousedown', (e) => e.preventDefault());
|
|
1915
2997
|
selectBtn.addEventListener('click', openSelectOverlay);
|
|
1916
2998
|
|
|
1917
2999
|
document.getElementById('select-copy').addEventListener('click', () => {
|
|
1918
3000
|
// Copy finger selection if any, otherwise copy all loaded text
|
|
1919
3001
|
const sel = window.getSelection();
|
|
1920
|
-
const text =
|
|
3002
|
+
const text = sel && sel.toString() ? sel.toString() : selectContent.textContent;
|
|
1921
3003
|
if (!text) return;
|
|
1922
3004
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1923
|
-
navigator.clipboard
|
|
1924
|
-
|
|
1925
|
-
|
|
3005
|
+
navigator.clipboard
|
|
3006
|
+
.writeText(text)
|
|
3007
|
+
.then(() => showToast('Copied!'))
|
|
3008
|
+
.catch(() => {
|
|
3009
|
+
copyFallback(text);
|
|
3010
|
+
});
|
|
1926
3011
|
} else {
|
|
1927
3012
|
copyFallback(text);
|
|
1928
3013
|
}
|
|
@@ -1989,15 +3074,18 @@
|
|
|
1989
3074
|
document.getElementById('ns-cancel').addEventListener('click', () => {
|
|
1990
3075
|
document.getElementById('new-session-modal').classList.remove('visible');
|
|
1991
3076
|
});
|
|
1992
|
-
document.getElementById('new-session-modal').addEventListener('click', e => {
|
|
1993
|
-
if (e.target.id === 'new-session-modal')
|
|
3077
|
+
document.getElementById('new-session-modal').addEventListener('click', (e) => {
|
|
3078
|
+
if (e.target.id === 'new-session-modal')
|
|
3079
|
+
document.getElementById('new-session-modal').classList.remove('visible');
|
|
1994
3080
|
});
|
|
1995
3081
|
|
|
1996
3082
|
// Color picker
|
|
1997
|
-
document.getElementById('ns-color-picker').addEventListener('click', e => {
|
|
3083
|
+
document.getElementById('ns-color-picker').addEventListener('click', (e) => {
|
|
1998
3084
|
const swatch = e.target.closest('.color-swatch');
|
|
1999
3085
|
if (!swatch) return;
|
|
2000
|
-
document
|
|
3086
|
+
document
|
|
3087
|
+
.querySelectorAll('#ns-color-picker .color-swatch')
|
|
3088
|
+
.forEach((s) => s.classList.remove('selected'));
|
|
2001
3089
|
swatch.classList.add('selected');
|
|
2002
3090
|
});
|
|
2003
3091
|
|
|
@@ -2015,7 +3103,7 @@
|
|
|
2015
3103
|
document.getElementById('ns-browse-btn').addEventListener('click', async () => {
|
|
2016
3104
|
if (serverCwd === '/') {
|
|
2017
3105
|
try {
|
|
2018
|
-
const data = await fetch('/api/shells').then(r => r.json());
|
|
3106
|
+
const data = await fetch('/api/shells').then((r) => r.json());
|
|
2019
3107
|
if (data.cwd) serverCwd = data.cwd;
|
|
2020
3108
|
} catch {}
|
|
2021
3109
|
}
|
|
@@ -2046,7 +3134,9 @@
|
|
|
2046
3134
|
const data = await res.json();
|
|
2047
3135
|
let items = '';
|
|
2048
3136
|
// Add parent (..) entry unless at root
|
|
2049
|
-
const parent =
|
|
3137
|
+
const parent =
|
|
3138
|
+
dir.replace(/[/\\][^/\\]+$/, '') ||
|
|
3139
|
+
(dir.includes('\\') ? dir.match(/^[A-Za-z]:\\/)?.[0] : '/');
|
|
2050
3140
|
if (parent && parent !== dir) {
|
|
2051
3141
|
items += `<div class="folder-item" data-path="${escAttr(parent)}">
|
|
2052
3142
|
<span class="folder-icon">📁</span>
|
|
@@ -2054,16 +3144,18 @@
|
|
|
2054
3144
|
<span class="folder-arrow">›</span>
|
|
2055
3145
|
</div>`;
|
|
2056
3146
|
}
|
|
2057
|
-
items += data.dirs
|
|
2058
|
-
|
|
2059
|
-
|
|
3147
|
+
items += data.dirs
|
|
3148
|
+
.map((d) => {
|
|
3149
|
+
const name = d.split(/[/\\]/).pop();
|
|
3150
|
+
return `<div class="folder-item" data-path="${escAttr(d)}">
|
|
2060
3151
|
<span class="folder-icon">📁</span>
|
|
2061
3152
|
<span class="folder-name">${esc(name)}</span>
|
|
2062
3153
|
<span class="folder-arrow">›</span>
|
|
2063
3154
|
</div>`;
|
|
2064
|
-
|
|
3155
|
+
})
|
|
3156
|
+
.join('');
|
|
2065
3157
|
nsBrowserList.innerHTML = items || '<div class="browser-empty">No subfolders</div>';
|
|
2066
|
-
nsBrowserList.querySelectorAll('.folder-item').forEach(el => {
|
|
3158
|
+
nsBrowserList.querySelectorAll('.folder-item').forEach((el) => {
|
|
2067
3159
|
el.addEventListener('click', () => nsBrowseNavigate(el.dataset.path));
|
|
2068
3160
|
});
|
|
2069
3161
|
nsBrowserList.scrollTop = 0;
|
|
@@ -2085,7 +3177,7 @@
|
|
|
2085
3177
|
html += `<button class="crumb${isCurrent ? ' current' : ''}" data-path="${escAttr(accumulated)}">${esc(part)}</button>`;
|
|
2086
3178
|
});
|
|
2087
3179
|
nsBrowserBreadcrumb.innerHTML = html;
|
|
2088
|
-
nsBrowserBreadcrumb.querySelectorAll('.crumb').forEach(el => {
|
|
3180
|
+
nsBrowserBreadcrumb.querySelectorAll('.crumb').forEach((el) => {
|
|
2089
3181
|
el.addEventListener('click', () => nsBrowseNavigate(el.dataset.path));
|
|
2090
3182
|
});
|
|
2091
3183
|
nsBrowserBreadcrumb.scrollLeft = nsBrowserBreadcrumb.scrollWidth;
|
|
@@ -2096,17 +3188,29 @@
|
|
|
2096
3188
|
if (shellsLoaded) return;
|
|
2097
3189
|
const sel = document.getElementById('ns-shell');
|
|
2098
3190
|
try {
|
|
2099
|
-
const data = await fetch('/api/shells').then(r => r.json());
|
|
3191
|
+
const data = await fetch('/api/shells').then((r) => r.json());
|
|
2100
3192
|
if (data.cwd) {
|
|
2101
3193
|
serverCwd = data.cwd;
|
|
2102
3194
|
document.getElementById('ns-cwd').placeholder = data.cwd;
|
|
2103
3195
|
}
|
|
2104
|
-
sel.innerHTML = data.shells
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
3196
|
+
sel.innerHTML = data.shells
|
|
3197
|
+
.map(
|
|
3198
|
+
(s) =>
|
|
3199
|
+
'<option value="' +
|
|
3200
|
+
escAttr(s.cmd) +
|
|
3201
|
+
'"' +
|
|
3202
|
+
(s.cmd === data.default ? ' selected' : '') +
|
|
3203
|
+
'>' +
|
|
3204
|
+
esc(s.name) +
|
|
3205
|
+
' (' +
|
|
3206
|
+
esc(s.cmd) +
|
|
3207
|
+
')</option>',
|
|
3208
|
+
)
|
|
3209
|
+
.join('');
|
|
2108
3210
|
shellsLoaded = true;
|
|
2109
|
-
} catch {
|
|
3211
|
+
} catch {
|
|
3212
|
+
sel.innerHTML = '<option value="">Could not detect shells</option>';
|
|
3213
|
+
}
|
|
2110
3214
|
}
|
|
2111
3215
|
|
|
2112
3216
|
async function createNewSession() {
|
|
@@ -2133,8 +3237,8 @@
|
|
|
2133
3237
|
const data = await res.json();
|
|
2134
3238
|
|
|
2135
3239
|
// Fetch full session list to get the new session data
|
|
2136
|
-
const list = await fetch('/api/sessions').then(r => r.json());
|
|
2137
|
-
const newSession = list.find(s => s.id === data.id);
|
|
3240
|
+
const list = await fetch('/api/sessions').then((r) => r.json());
|
|
3241
|
+
const newSession = list.find((s) => s.id === data.id);
|
|
2138
3242
|
if (newSession) {
|
|
2139
3243
|
addSession(newSession);
|
|
2140
3244
|
activateSession(data.id);
|
|
@@ -2153,8 +3257,8 @@
|
|
|
2153
3257
|
function startPolling() {
|
|
2154
3258
|
setInterval(async () => {
|
|
2155
3259
|
try {
|
|
2156
|
-
const list = await fetch('/api/sessions').then(r => r.json());
|
|
2157
|
-
const serverIds = new Set(list.map(s => s.id));
|
|
3260
|
+
const list = await fetch('/api/sessions').then((r) => r.json());
|
|
3261
|
+
const serverIds = new Set(list.map((s) => s.id));
|
|
2158
3262
|
|
|
2159
3263
|
// Add new sessions created elsewhere
|
|
2160
3264
|
for (const s of list) {
|
|
@@ -2174,8 +3278,14 @@
|
|
|
2174
3278
|
if (!serverIds.has(id)) {
|
|
2175
3279
|
const ms = managed.get(id);
|
|
2176
3280
|
ms.exited = true;
|
|
2177
|
-
if (ms.reconnectTimer) {
|
|
2178
|
-
|
|
3281
|
+
if (ms.reconnectTimer) {
|
|
3282
|
+
clearTimeout(ms.reconnectTimer);
|
|
3283
|
+
ms.reconnectTimer = null;
|
|
3284
|
+
}
|
|
3285
|
+
if (ms.ws)
|
|
3286
|
+
try {
|
|
3287
|
+
ms.ws.close();
|
|
3288
|
+
} catch {}
|
|
2179
3289
|
ms.term.dispose();
|
|
2180
3290
|
ms.container.remove();
|
|
2181
3291
|
managed.delete(id);
|
|
@@ -2206,25 +3316,82 @@
|
|
|
2206
3316
|
ta.value = text;
|
|
2207
3317
|
ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px';
|
|
2208
3318
|
document.body.appendChild(ta);
|
|
3319
|
+
ta.focus();
|
|
2209
3320
|
ta.select();
|
|
2210
|
-
|
|
3321
|
+
let ok = false;
|
|
3322
|
+
try {
|
|
3323
|
+
ok = document.execCommand('copy');
|
|
3324
|
+
} catch {}
|
|
2211
3325
|
document.body.removeChild(ta);
|
|
3326
|
+
return ok;
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
function showShareUrlPrompt(url) {
|
|
3330
|
+
const overlay = document.createElement('div');
|
|
3331
|
+
overlay.style.cssText =
|
|
3332
|
+
'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:300;display:flex;align-items:center;justify-content:center;';
|
|
3333
|
+
const box = document.createElement('div');
|
|
3334
|
+
box.style.cssText =
|
|
3335
|
+
'background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px;max-width:90vw;width:360px;text-align:center;';
|
|
3336
|
+
box.innerHTML =
|
|
3337
|
+
'<div style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:12px;">Copy this link</div>';
|
|
3338
|
+
const input = document.createElement('input');
|
|
3339
|
+
input.type = 'text';
|
|
3340
|
+
input.readOnly = true;
|
|
3341
|
+
input.value = url;
|
|
3342
|
+
input.style.cssText =
|
|
3343
|
+
'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;';
|
|
3344
|
+
box.appendChild(input);
|
|
3345
|
+
const btn = document.createElement('button');
|
|
3346
|
+
btn.textContent = 'Close';
|
|
3347
|
+
btn.style.cssText =
|
|
3348
|
+
'padding:6px 20px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-size:13px;font-weight:600;cursor:pointer;';
|
|
3349
|
+
btn.onclick = () => overlay.remove();
|
|
3350
|
+
box.appendChild(btn);
|
|
3351
|
+
overlay.appendChild(box);
|
|
3352
|
+
overlay.addEventListener('click', (e) => {
|
|
3353
|
+
if (e.target === overlay) overlay.remove();
|
|
3354
|
+
});
|
|
3355
|
+
document.body.appendChild(overlay);
|
|
3356
|
+
input.focus();
|
|
3357
|
+
input.select();
|
|
2212
3358
|
}
|
|
2213
3359
|
|
|
2214
3360
|
document.getElementById('share-btn').addEventListener('click', async () => {
|
|
2215
|
-
const
|
|
3361
|
+
const urlPromise = fetch('/api/share-token')
|
|
3362
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
3363
|
+
.then((data) => (data && data.url) || location.href)
|
|
3364
|
+
.catch(() => location.href);
|
|
3365
|
+
// ClipboardItem with a promise preserves user activation across the fetch
|
|
3366
|
+
if (navigator.clipboard && typeof ClipboardItem !== 'undefined') {
|
|
3367
|
+
try {
|
|
3368
|
+
const blobPromise = urlPromise.then((u) => new Blob([u], { type: 'text/plain' }));
|
|
3369
|
+
await navigator.clipboard.write([new ClipboardItem({ 'text/plain': blobPromise })]);
|
|
3370
|
+
showToast('Link copied!');
|
|
3371
|
+
return;
|
|
3372
|
+
} catch {}
|
|
3373
|
+
}
|
|
3374
|
+
// Fallback: resolve URL first, then try legacy methods
|
|
3375
|
+
const url = await urlPromise;
|
|
2216
3376
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
2217
|
-
try {
|
|
3377
|
+
try {
|
|
3378
|
+
await navigator.clipboard.writeText(url);
|
|
3379
|
+
showToast('Link copied!');
|
|
3380
|
+
return;
|
|
3381
|
+
} catch {}
|
|
3382
|
+
}
|
|
3383
|
+
if (copyToClipboardFallback(url)) {
|
|
3384
|
+
showToast('Link copied!');
|
|
3385
|
+
} else {
|
|
3386
|
+
showShareUrlPrompt(url);
|
|
2218
3387
|
}
|
|
2219
|
-
copyToClipboardFallback(url);
|
|
2220
|
-
showToast('Link copied!');
|
|
2221
3388
|
});
|
|
2222
3389
|
|
|
2223
3390
|
// ===== Refresh Button =====
|
|
2224
3391
|
document.getElementById('refresh-btn').addEventListener('click', async () => {
|
|
2225
3392
|
if ('caches' in window) {
|
|
2226
3393
|
const keys = await caches.keys();
|
|
2227
|
-
await Promise.all(keys.map(k => caches.delete(k)));
|
|
3394
|
+
await Promise.all(keys.map((k) => caches.delete(k)));
|
|
2228
3395
|
}
|
|
2229
3396
|
if (navigator.serviceWorker) {
|
|
2230
3397
|
const reg = await navigator.serviceWorker.getRegistration();
|