termbeam 0.0.5 → 0.0.6

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 CHANGED
@@ -164,3 +164,7 @@ Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for:
164
164
  ## 📄 License
165
165
 
166
166
  [MIT](LICENSE) — made with ❤️ by [@dorlugasigal](https://github.com/dorlugasigal)
167
+
168
+ ## 🙏 Acknowledgments
169
+
170
+ Special thanks to [@tamirdresher](https://github.com/tamirdresher) for the [blog post](https://www.tamirdresher.com/blog/2026/02/26/squad-remote-control) that inspired the solution idea for this project, and for his [cli-tunnel](https://github.com/tamirdresher/cli-tunnel) implementation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Beam your terminal to any device — mobile-optimized web terminal with multi-session support",
5
5
  "main": "src/server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -8,9 +8,47 @@
8
8
  />
9
9
  <meta name="apple-mobile-web-app-capable" content="yes" />
10
10
  <meta name="mobile-web-app-capable" content="yes" />
11
- <meta name="theme-color" content="#1a1a2e" />
11
+ <meta name="theme-color" content="#1e1e1e" />
12
12
  <title>TermBeam</title>
13
13
  <style>
14
+ :root {
15
+ --bg: #1e1e1e;
16
+ --surface: #252526;
17
+ --border: #3c3c3c;
18
+ --border-subtle: #474747;
19
+ --text: #d4d4d4;
20
+ --text-secondary: #858585;
21
+ --text-dim: #6e6e6e;
22
+ --text-muted: #5a5a5a;
23
+ --accent: #0078d4;
24
+ --accent-hover: #1a8ae8;
25
+ --accent-active: #005a9e;
26
+ --danger: #f14c4c;
27
+ --danger-hover: #d73a3a;
28
+ --success: #89d185;
29
+ --info: #b0b0b0;
30
+ --shadow: rgba(0,0,0,0.15);
31
+ --overlay-bg: rgba(0,0,0,0.7);
32
+ }
33
+ [data-theme="light"] {
34
+ --bg: #ffffff;
35
+ --surface: #f3f3f3;
36
+ --border: #e0e0e0;
37
+ --border-subtle: #d0d0d0;
38
+ --text: #1e1e1e;
39
+ --text-secondary: #616161;
40
+ --text-dim: #767676;
41
+ --text-muted: #a0a0a0;
42
+ --accent: #0078d4;
43
+ --accent-hover: #106ebe;
44
+ --accent-active: #005a9e;
45
+ --danger: #e51400;
46
+ --danger-hover: #c20000;
47
+ --success: #16825d;
48
+ --info: #616161;
49
+ --shadow: rgba(0,0,0,0.06);
50
+ --overlay-bg: rgba(0,0,0,0.4);
51
+ }
14
52
  * {
15
53
  margin: 0;
16
54
  padding: 0;
@@ -20,39 +58,66 @@
20
58
  body {
21
59
  height: 100%;
22
60
  width: 100%;
23
- background: #1a1a2e;
24
- color: #e0e0e0;
61
+ background: var(--bg);
62
+ color: var(--text);
25
63
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
64
+ transition: background 0.3s, color 0.3s;
26
65
  }
27
66
 
28
67
  .header {
29
68
  padding: 20px 16px 12px;
30
69
  text-align: center;
31
- border-bottom: 1px solid #0f3460;
70
+ border-bottom: 1px solid var(--border);
71
+ transition: border-color 0.3s;
72
+ position: relative;
32
73
  }
33
74
  .header h1 {
34
75
  font-size: 22px;
35
76
  font-weight: 700;
36
77
  }
37
78
  .header h1 span {
38
- color: #533483;
79
+ color: var(--accent);
39
80
  }
40
81
  .header p {
41
82
  font-size: 13px;
42
- color: #888;
83
+ color: var(--text-secondary);
43
84
  margin-top: 4px;
44
85
  }
86
+ .theme-toggle {
87
+ position: absolute;
88
+ top: 16px;
89
+ right: 16px;
90
+ background: none;
91
+ border: 1px solid var(--border);
92
+ color: var(--text-dim);
93
+ width: 32px;
94
+ height: 32px;
95
+ border-radius: 8px;
96
+ cursor: pointer;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ font-size: 16px;
101
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
102
+ -webkit-tap-highlight-color: transparent;
103
+ }
104
+ .theme-toggle:hover {
105
+ color: var(--text);
106
+ border-color: var(--border-subtle);
107
+ background: var(--border);
108
+ }
45
109
 
46
110
  .sessions-list {
47
111
  padding: 16px;
112
+ padding-bottom: 80px;
48
113
  display: flex;
49
114
  flex-direction: column;
50
115
  gap: 12px;
51
116
  }
52
117
 
53
118
  .session-card {
54
- background: #16213e;
55
- border: 1px solid #0f3460;
119
+ background: var(--surface);
120
+ border: 1px solid var(--border);
56
121
  border-radius: 12px;
57
122
  padding: 16px;
58
123
  display: flex;
@@ -60,14 +125,14 @@
60
125
  gap: 8px;
61
126
  text-decoration: none;
62
127
  color: inherit;
63
- transition: transform 0.2s ease;
128
+ transition: transform 0.2s ease, border-color 0.15s, background 0.3s;
64
129
  cursor: pointer;
65
130
  -webkit-tap-highlight-color: transparent;
66
131
  position: relative;
67
132
  z-index: 1;
68
133
  }
69
134
  .session-card:hover {
70
- border-color: #533483;
135
+ border-color: var(--accent);
71
136
  }
72
137
 
73
138
  .swipe-wrap {
@@ -81,7 +146,7 @@
81
146
  top: 0;
82
147
  bottom: 0;
83
148
  width: 80px;
84
- background: #e74c3c;
149
+ background: var(--danger);
85
150
  display: flex;
86
151
  align-items: center;
87
152
  justify-content: center;
@@ -123,22 +188,23 @@
123
188
  width: 10px;
124
189
  height: 10px;
125
190
  border-radius: 50%;
126
- background: #2ecc71;
191
+ background: var(--success);
127
192
  flex-shrink: 0;
128
193
  }
129
194
  .session-card .pid {
130
195
  font-size: 12px;
131
- color: #888;
132
- background: #1a1a2e;
196
+ color: var(--text-secondary);
197
+ background: var(--bg);
133
198
  padding: 2px 8px;
134
199
  border-radius: 4px;
200
+ transition: background 0.3s, color 0.3s;
135
201
  }
136
202
  .session-card .details {
137
203
  display: flex;
138
204
  flex-wrap: wrap;
139
205
  gap: 6px 16px;
140
206
  font-size: 13px;
141
- color: #aaa;
207
+ color: var(--info);
142
208
  }
143
209
  .session-card .details span {
144
210
  display: flex;
@@ -147,43 +213,57 @@
147
213
  }
148
214
  .session-card .connect-btn {
149
215
  align-self: flex-end;
150
- background: #533483;
151
- color: white;
216
+ background: var(--accent);
217
+ color: #ffffff;
152
218
  border: none;
153
219
  border-radius: 8px;
154
220
  padding: 8px 20px;
155
221
  font-size: 14px;
156
222
  font-weight: 600;
157
223
  cursor: pointer;
224
+ transition: background 0.15s, transform 0.1s;
225
+ }
226
+ .session-card .connect-btn:hover {
227
+ background: var(--accent-hover);
158
228
  }
159
229
  .session-card .connect-btn:active {
160
- background: #6a42a8;
230
+ background: var(--accent-active);
231
+ transform: scale(0.95);
161
232
  }
162
233
 
163
234
  .new-session {
164
- margin: 0 16px;
235
+ position: fixed;
236
+ bottom: 16px;
237
+ left: 16px;
238
+ right: 16px;
165
239
  padding: 14px;
166
- background: transparent;
167
- border: 2px dashed #0f3460;
240
+ background: var(--accent);
241
+ color: #ffffff;
242
+ border: none;
168
243
  border-radius: 12px;
169
- color: #888;
170
244
  font-size: 15px;
171
245
  font-weight: 600;
172
246
  cursor: pointer;
173
247
  text-align: center;
174
- transition:
175
- border-color 0.15s,
176
- color 0.15s;
248
+ z-index: 50;
249
+ transition: background 0.15s, transform 0.1s, box-shadow 0.15s;
250
+ box-shadow: 0 2px 8px rgba(0, 120, 212, 0.3);
251
+ padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
252
+ }
253
+ .new-session:hover {
254
+ background: var(--accent-hover);
255
+ box-shadow: 0 4px 12px rgba(0, 120, 212, 0.4);
177
256
  }
178
257
  .new-session:active {
179
- border-color: #533483;
180
- color: #e0e0e0;
258
+ background: var(--accent-active);
259
+ transform: scale(0.98);
260
+ box-shadow: 0 1px 4px rgba(0, 120, 212, 0.2);
181
261
  }
182
262
 
183
263
  .empty-state {
184
264
  text-align: center;
185
265
  padding: 60px 20px;
186
- color: #666;
266
+ color: var(--text-muted);
187
267
  font-size: 15px;
188
268
  }
189
269
 
@@ -195,7 +275,7 @@
195
275
  left: 0;
196
276
  right: 0;
197
277
  bottom: 0;
198
- background: rgba(0, 0, 0, 0.7);
278
+ background: var(--overlay-bg);
199
279
  z-index: 100;
200
280
  justify-content: center;
201
281
  align-items: center;
@@ -204,11 +284,12 @@
204
284
  display: flex;
205
285
  }
206
286
  .modal {
207
- background: #16213e;
287
+ background: var(--surface);
208
288
  border-radius: 16px;
209
289
  width: 90%;
210
290
  max-width: 500px;
211
291
  padding: 24px 20px;
292
+ transition: background 0.3s;
212
293
  }
213
294
  .modal h2 {
214
295
  font-size: 18px;
@@ -217,7 +298,7 @@
217
298
  .modal label {
218
299
  display: block;
219
300
  font-size: 13px;
220
- color: #aaa;
301
+ color: var(--text-secondary);
221
302
  margin-bottom: 4px;
222
303
  margin-top: 12px;
223
304
  }
@@ -225,14 +306,15 @@
225
306
  .modal select {
226
307
  width: 100%;
227
308
  padding: 10px 12px;
228
- background: #1a1a2e;
229
- border: 1px solid #0f3460;
309
+ background: var(--bg);
310
+ border: 1px solid var(--border);
230
311
  border-radius: 8px;
231
- color: #e0e0e0;
312
+ color: var(--text);
232
313
  font-size: 15px;
233
314
  outline: none;
234
315
  -webkit-appearance: none;
235
316
  appearance: none;
317
+ transition: background 0.3s, border-color 0.15s, color 0.3s;
236
318
  }
237
319
  .modal select {
238
320
  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");
@@ -243,7 +325,7 @@
243
325
  }
244
326
  .modal input:focus,
245
327
  .modal select:focus {
246
- border-color: #533483;
328
+ border-color: var(--accent);
247
329
  }
248
330
  .modal-actions {
249
331
  display: flex;
@@ -258,14 +340,24 @@
258
340
  font-size: 15px;
259
341
  font-weight: 600;
260
342
  cursor: pointer;
343
+ transition: background 0.15s, transform 0.1s;
344
+ }
345
+ .modal-actions button:active {
346
+ transform: scale(0.95);
261
347
  }
262
348
  .btn-cancel {
263
- background: #0f3460;
264
- color: #e0e0e0;
349
+ background: var(--border);
350
+ color: var(--text);
351
+ }
352
+ .btn-cancel:hover {
353
+ background: var(--border-subtle);
265
354
  }
266
355
  .btn-create {
267
- background: #533483;
268
- color: white;
356
+ background: var(--accent);
357
+ color: #ffffff;
358
+ }
359
+ .btn-create:hover {
360
+ background: var(--accent-hover);
269
361
  }
270
362
 
271
363
  /* Folder browser */
@@ -277,9 +369,9 @@
277
369
  flex: 1;
278
370
  }
279
371
  .cwd-browse-btn {
280
- background: #0f3460;
281
- color: #e0e0e0;
282
- border: 1px solid #0f3460;
372
+ background: var(--border);
373
+ color: var(--text);
374
+ border: 1px solid var(--border);
283
375
  border-radius: 8px;
284
376
  padding: 0 14px;
285
377
  font-size: 18px;
@@ -287,9 +379,14 @@
287
379
  flex-shrink: 0;
288
380
  display: flex;
289
381
  align-items: center;
382
+ transition: background 0.15s, border-color 0.15s;
383
+ }
384
+ .cwd-browse-btn:hover {
385
+ border-color: var(--accent);
290
386
  }
291
387
  .cwd-browse-btn:active {
292
- background: #533483;
388
+ background: var(--accent);
389
+ color: #ffffff;
293
390
  }
294
391
 
295
392
  .browser-overlay {
@@ -299,7 +396,7 @@
299
396
  left: 0;
300
397
  right: 0;
301
398
  bottom: 0;
302
- background: rgba(0, 0, 0, 0.8);
399
+ background: var(--overlay-bg);
303
400
  z-index: 200;
304
401
  justify-content: center;
305
402
  align-items: flex-end;
@@ -308,7 +405,7 @@
308
405
  display: flex;
309
406
  }
310
407
  .browser-sheet {
311
- background: #16213e;
408
+ background: var(--surface);
312
409
  border-radius: 16px 16px 0 0;
313
410
  width: 100%;
314
411
  max-width: 500px;
@@ -316,10 +413,11 @@
316
413
  display: flex;
317
414
  flex-direction: column;
318
415
  overflow: hidden;
416
+ transition: background 0.3s;
319
417
  }
320
418
  .browser-header {
321
419
  padding: 16px 16px 12px;
322
- border-bottom: 1px solid #0f3460;
420
+ border-bottom: 1px solid var(--border);
323
421
  display: flex;
324
422
  align-items: center;
325
423
  justify-content: space-between;
@@ -331,14 +429,18 @@
331
429
  .browser-close {
332
430
  background: none;
333
431
  border: none;
334
- color: #888;
432
+ color: var(--text-dim);
335
433
  font-size: 24px;
336
434
  cursor: pointer;
337
435
  padding: 0 4px;
338
436
  line-height: 1;
437
+ transition: color 0.15s;
438
+ }
439
+ .browser-close:hover {
440
+ color: var(--text);
339
441
  }
340
442
  .browser-close:active {
341
- color: #e0e0e0;
443
+ color: var(--text);
342
444
  }
343
445
 
344
446
  .browser-breadcrumb {
@@ -349,31 +451,32 @@
349
451
  font-size: 13px;
350
452
  overflow-x: auto;
351
453
  white-space: nowrap;
352
- border-bottom: 1px solid #0f3460;
454
+ border-bottom: 1px solid var(--border);
353
455
  flex-shrink: 0;
354
456
  -webkit-overflow-scrolling: touch;
355
457
  }
356
458
  .crumb {
357
459
  background: none;
358
460
  border: none;
359
- color: #888;
461
+ color: var(--text-dim);
360
462
  font-size: 13px;
361
463
  cursor: pointer;
362
464
  padding: 4px 6px;
363
465
  border-radius: 4px;
364
466
  flex-shrink: 0;
467
+ transition: background 0.15s, color 0.15s;
365
468
  }
366
469
  .crumb:active,
367
470
  .crumb:hover {
368
- background: #0f3460;
369
- color: #e0e0e0;
471
+ background: var(--border);
472
+ color: var(--text);
370
473
  }
371
474
  .crumb.current {
372
- color: #e0e0e0;
475
+ color: var(--text);
373
476
  font-weight: 600;
374
477
  }
375
478
  .crumb-sep {
376
- color: #444;
479
+ color: var(--border-subtle);
377
480
  flex-shrink: 0;
378
481
  }
379
482
 
@@ -386,7 +489,7 @@
386
489
  .browser-empty {
387
490
  text-align: center;
388
491
  padding: 40px 20px;
389
- color: #666;
492
+ color: var(--text-muted);
390
493
  font-size: 14px;
391
494
  }
392
495
  .folder-item {
@@ -395,15 +498,15 @@
395
498
  gap: 12px;
396
499
  padding: 12px 16px;
397
500
  cursor: pointer;
398
- border-bottom: 1px solid rgba(15, 52, 96, 0.5);
501
+ border-bottom: 1px solid rgba(60, 60, 60, 0.5);
399
502
  transition: background 0.1s;
400
503
  -webkit-tap-highlight-color: transparent;
401
504
  }
402
505
  .folder-item:active {
403
- background: rgba(83, 52, 131, 0.3);
506
+ background: rgba(0, 120, 212, 0.2);
404
507
  }
405
508
  .folder-item:hover {
406
- background: rgba(83, 52, 131, 0.15);
509
+ background: rgba(0, 120, 212, 0.1);
407
510
  }
408
511
  .folder-icon {
409
512
  font-size: 22px;
@@ -413,28 +516,28 @@
413
516
  }
414
517
  .folder-name {
415
518
  font-size: 15px;
416
- color: #e0e0e0;
519
+ color: var(--text);
417
520
  flex: 1;
418
521
  overflow: hidden;
419
522
  text-overflow: ellipsis;
420
523
  white-space: nowrap;
421
524
  }
422
525
  .folder-arrow {
423
- color: #444;
526
+ color: var(--border-subtle);
424
527
  font-size: 18px;
425
528
  flex-shrink: 0;
426
529
  }
427
530
 
428
531
  .browser-footer {
429
532
  padding: 12px 16px calc(env(safe-area-inset-bottom, 8px) + 8px);
430
- border-top: 1px solid #0f3460;
533
+ border-top: 1px solid var(--border);
431
534
  display: flex;
432
535
  flex-direction: column;
433
536
  gap: 8px;
434
537
  }
435
538
  .browser-current-path {
436
539
  font-size: 12px;
437
- color: #888;
540
+ color: var(--text-dim);
438
541
  overflow: hidden;
439
542
  text-overflow: ellipsis;
440
543
  white-space: nowrap;
@@ -442,23 +545,29 @@
442
545
  .browser-select-btn {
443
546
  width: 100%;
444
547
  padding: 12px;
445
- background: #533483;
446
- color: white;
548
+ background: var(--accent);
549
+ color: #ffffff;
447
550
  border: none;
448
551
  border-radius: 10px;
449
552
  font-size: 16px;
450
553
  font-weight: 600;
451
554
  cursor: pointer;
555
+ transition: background 0.15s, transform 0.1s;
556
+ }
557
+ .browser-select-btn:hover {
558
+ background: var(--accent-hover);
452
559
  }
453
560
  .browser-select-btn:active {
454
- background: #6a42a8;
561
+ background: var(--accent-active);
562
+ transform: scale(0.98);
455
563
  }
456
564
  </style>
457
565
  </head>
458
566
  <body>
459
567
  <div class="header">
460
- <h1>📡 Term<span>Cast</span></h1>
461
- <p>Beam your terminal to any device · <span id="version" style="color: #533483"></span></p>
568
+ <h1>📡 Term<span>Beam</span></h1>
569
+ <p>Beam your terminal to any device · <span id="version" style="color: var(--accent)"></span></p>
570
+ <button class="theme-toggle" id="theme-toggle" title="Toggle theme"><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></button>
462
571
  </div>
463
572
 
464
573
  <div class="sessions-list" id="sessions-list"></div>
@@ -473,7 +582,7 @@
473
582
  <select id="sess-shell">
474
583
  <option value="">Loading shells…</option>
475
584
  </select>
476
- <label for="sess-cmd">Initial Command <span style="color:#666;font-weight:normal">(optional)</span></label>
585
+ <label for="sess-cmd">Initial Command <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
477
586
  <input type="text" id="sess-cmd" placeholder="e.g. copilot, htop, vim" />
478
587
  <label for="sess-cwd">Working Directory</label>
479
588
  <div class="cwd-picker">
@@ -535,6 +644,23 @@
535
644
  </div>
536
645
 
537
646
  <script>
647
+ // Theme
648
+ function getTheme() { return localStorage.getItem('termbeam-theme') || 'dark'; }
649
+ function applyTheme(theme) {
650
+ document.documentElement.setAttribute('data-theme', theme);
651
+ document.querySelector('meta[name="theme-color"]').content =
652
+ theme === 'light' ? '#f3f3f3' : '#1e1e1e';
653
+ const btn = document.getElementById('theme-toggle');
654
+ if (btn) btn.innerHTML = theme === 'light'
655
+ ? '<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>'
656
+ : '<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>';
657
+ localStorage.setItem('termbeam-theme', theme);
658
+ }
659
+ applyTheme(getTheme());
660
+ document.getElementById('theme-toggle').addEventListener('click', () => {
661
+ applyTheme(getTheme() === 'light' ? 'dark' : 'light');
662
+ });
663
+
538
664
  const listEl = document.getElementById('sessions-list');
539
665
  const modal = document.getElementById('modal');
540
666
 
@@ -8,13 +8,53 @@
8
8
  />
9
9
  <meta name="apple-mobile-web-app-capable" content="yes" />
10
10
  <meta name="mobile-web-app-capable" content="yes" />
11
- <meta name="theme-color" content="#1a1a2e" />
11
+ <meta name="theme-color" content="#1e1e1e" />
12
12
  <title>TermBeam — Terminal</title>
13
13
  <link
14
14
  rel="stylesheet"
15
15
  href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css"
16
16
  />
17
17
  <style>
18
+ :root {
19
+ --bg: #1e1e1e;
20
+ --surface: #252526;
21
+ --border: #3c3c3c;
22
+ --border-subtle: #474747;
23
+ --text: #d4d4d4;
24
+ --text-secondary: #858585;
25
+ --text-dim: #6e6e6e;
26
+ --text-muted: #555555;
27
+ --accent: #0078d4;
28
+ --accent-hover: #1a8ae8;
29
+ --accent-active: #005a9e;
30
+ --danger: #f14c4c;
31
+ --danger-hover: #d73a3a;
32
+ --success: #89d185;
33
+ --key-bg: #2d2d2d;
34
+ --key-border: #404040;
35
+ --key-shadow: rgba(0,0,0,0.4);
36
+ --overlay-bg: rgba(0,0,0,0.85);
37
+ }
38
+ [data-theme="light"] {
39
+ --bg: #ffffff;
40
+ --surface: #f3f3f3;
41
+ --border: #e0e0e0;
42
+ --border-subtle: #d0d0d0;
43
+ --text: #1e1e1e;
44
+ --text-secondary: #616161;
45
+ --text-dim: #767676;
46
+ --text-muted: #a0a0a0;
47
+ --accent: #0078d4;
48
+ --accent-hover: #106ebe;
49
+ --accent-active: #005a9e;
50
+ --danger: #e51400;
51
+ --danger-hover: #c20000;
52
+ --success: #16825d;
53
+ --key-bg: #e8e8e8;
54
+ --key-border: #d0d0d0;
55
+ --key-shadow: rgba(0,0,0,0.08);
56
+ --overlay-bg: rgba(0,0,0,0.5);
57
+ }
18
58
  @font-face {
19
59
  font-family: 'NerdFont';
20
60
  src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')
@@ -41,74 +81,117 @@
41
81
  body {
42
82
  height: 100%;
43
83
  width: 100%;
44
- background: #1a1a2e;
45
- color: #e0e0e0;
84
+ background: var(--bg);
85
+ color: var(--text);
46
86
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
47
87
  overflow: hidden;
48
88
  touch-action: manipulation;
49
- /* Use dvh to account for mobile browser chrome + keyboard */
50
89
  height: 100dvh;
90
+ transition: background 0.3s, color 0.3s;
51
91
  }
52
92
 
53
93
  #status-bar {
54
- height: 36px;
94
+ height: 40px;
55
95
  display: flex;
56
96
  align-items: center;
57
97
  justify-content: space-between;
58
- padding: 0 12px;
59
- background: #16213e;
60
- border-bottom: 1px solid #0f3460;
98
+ padding: 0 8px;
99
+ background: var(--surface);
100
+ border-bottom: 1px solid var(--border);
61
101
  font-size: 13px;
102
+ transition: background 0.3s, border-color 0.3s;
62
103
  }
63
104
  #status-bar .left {
64
105
  display: flex;
65
106
  align-items: center;
66
- gap: 8px;
107
+ gap: 6px;
67
108
  }
68
109
  #status-bar .right {
69
110
  display: flex;
70
111
  align-items: center;
71
- gap: 8px;
112
+ gap: 4px;
72
113
  }
73
- #back-btn {
114
+ .bar-btn {
74
115
  background: none;
75
116
  border: none;
76
- color: #888;
77
- font-size: 18px;
117
+ color: var(--text-dim);
118
+ width: 30px;
119
+ height: 30px;
120
+ border-radius: 8px;
78
121
  cursor: pointer;
79
- padding: 0 4px;
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ transition: background 0.15s, color 0.15s;
126
+ -webkit-tap-highlight-color: transparent;
127
+ }
128
+ .bar-btn:hover {
129
+ background: var(--border);
130
+ color: var(--text);
131
+ }
132
+ .bar-btn:active {
133
+ background: var(--accent);
134
+ color: #ffffff;
80
135
  }
81
- #back-btn:active {
82
- color: #e0e0e0;
136
+ .bar-btn svg {
137
+ display: block;
138
+ }
139
+ .bar-group {
140
+ display: flex;
141
+ align-items: center;
142
+ background: var(--bg);
143
+ border-radius: 8px;
144
+ padding: 2px;
145
+ gap: 1px;
146
+ transition: background 0.3s;
147
+ }
148
+ .bar-group .bar-btn {
149
+ width: 26px;
150
+ height: 26px;
151
+ border-radius: 6px;
152
+ font-size: 14px;
153
+ font-weight: 600;
83
154
  }
84
155
  #stop-btn {
85
- background: #e74c3c;
156
+ background: none;
86
157
  border: none;
87
- color: white;
88
- font-size: 11px;
89
- font-weight: 600;
158
+ color: var(--danger);
159
+ height: 30px;
160
+ border-radius: 8px;
90
161
  cursor: pointer;
91
- padding: 4px 10px;
92
- border-radius: 6px;
93
162
  display: flex;
94
163
  align-items: center;
164
+ justify-content: center;
95
165
  gap: 4px;
166
+ padding: 0 10px;
167
+ font-size: 11px;
168
+ font-weight: 600;
169
+ margin-left: 2px;
170
+ transition: background 0.15s, color 0.15s, transform 0.1s;
171
+ -webkit-tap-highlight-color: transparent;
172
+ }
173
+ #stop-btn:hover {
174
+ background: var(--danger);
175
+ color: #ffffff;
96
176
  }
97
177
  #stop-btn:active {
98
- background: #c0392b;
178
+ background: var(--danger-hover);
179
+ color: #ffffff;
180
+ transform: scale(0.9);
99
181
  }
100
182
  #status-dot {
101
183
  width: 8px;
102
184
  height: 8px;
103
185
  border-radius: 50%;
104
- background: #e74c3c;
186
+ background: var(--danger);
105
187
  display: inline-block;
188
+ transition: background 0.3s;
106
189
  }
107
190
  #status-dot.connected {
108
- background: #2ecc71;
191
+ background: var(--success);
109
192
  }
110
193
  #status-text {
111
- color: #aaa;
194
+ color: var(--text-secondary);
112
195
  }
113
196
  #session-name {
114
197
  font-weight: 600;
@@ -116,10 +199,10 @@
116
199
 
117
200
  #terminal-container {
118
201
  position: absolute;
119
- top: 36px;
202
+ top: 40px;
120
203
  left: 0;
121
204
  right: 0;
122
- bottom: 44px;
205
+ bottom: 52px;
123
206
  padding: 2px;
124
207
  overflow: hidden;
125
208
  }
@@ -129,46 +212,67 @@
129
212
  bottom: 0;
130
213
  left: 0;
131
214
  right: 0;
132
- height: 44px;
215
+ height: 52px;
133
216
  display: flex;
134
217
  align-items: center;
135
- background: #16213e;
136
- border-top: 1px solid #0f3460;
137
- padding: 0 3px;
218
+ background: var(--surface);
219
+ border-top: 1px solid var(--border);
220
+ padding: 0 4px;
138
221
  padding-bottom: env(safe-area-inset-bottom, 0);
139
- gap: 3px;
140
- overflow-x: auto;
222
+ gap: 4px;
141
223
  z-index: 50;
224
+ transition: background 0.3s, border-color 0.3s;
142
225
  }
143
226
  .key-btn {
144
- min-width: 40px;
145
- height: 32px;
146
- background: #0f3460;
147
- color: #e0e0e0;
148
- border: 1px solid #1a1a5e;
149
- border-radius: 6px;
150
- font-size: 11px;
227
+ min-width: 0;
228
+ height: 40px;
229
+ background: var(--key-bg);
230
+ color: var(--text);
231
+ border: 1px solid var(--key-border);
232
+ border-radius: 8px;
233
+ font-size: 12px;
151
234
  font-weight: 600;
152
235
  cursor: pointer;
153
236
  display: flex;
237
+ flex-direction: column;
154
238
  align-items: center;
155
239
  justify-content: center;
156
240
  -webkit-tap-highlight-color: transparent;
157
241
  user-select: none;
158
242
  white-space: nowrap;
159
- padding: 0 6px;
160
- flex-shrink: 0;
243
+ padding: 2px 8px;
244
+ flex: 1 1 0;
245
+ gap: 0;
246
+ line-height: 1;
247
+ transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s, box-shadow 0.15s;
248
+ box-shadow: 0 1px 3px var(--key-shadow), inset 0 1px 0 rgba(255,255,255,0.05);
249
+ }
250
+ .key-btn .hint {
251
+ font-size: 8px;
252
+ font-weight: 400;
253
+ opacity: 0.5;
254
+ margin-top: 1px;
255
+ letter-spacing: 0.02em;
256
+ }
257
+ .key-btn:hover {
258
+ background: var(--border);
259
+ border-color: var(--accent);
260
+ box-shadow: 0 2px 6px var(--key-shadow);
161
261
  }
162
262
  .key-btn:active {
163
- background: #533483;
263
+ background: var(--accent);
264
+ color: #ffffff;
265
+ border-color: var(--accent);
266
+ transform: scale(0.93);
267
+ box-shadow: none;
164
268
  }
165
269
  .key-btn.wide {
166
- min-width: 52px;
270
+ flex: 1.4 1 0;
167
271
  }
168
272
  .key-sep {
169
273
  width: 1px;
170
274
  height: 20px;
171
- background: #0f3460;
275
+ background: var(--border);
172
276
  flex-shrink: 0;
173
277
  }
174
278
 
@@ -186,7 +290,7 @@
186
290
  left: 0;
187
291
  right: 0;
188
292
  bottom: 0;
189
- background: rgba(0, 0, 0, 0.85);
293
+ background: var(--overlay-bg);
190
294
  z-index: 100;
191
295
  flex-direction: column;
192
296
  align-items: center;
@@ -198,6 +302,7 @@
198
302
  }
199
303
  #reconnect-overlay .msg {
200
304
  font-size: 17px;
305
+ color: #ffffff;
201
306
  }
202
307
  .overlay-actions {
203
308
  display: flex;
@@ -210,50 +315,60 @@
210
315
  font-size: 15px;
211
316
  font-weight: 600;
212
317
  cursor: pointer;
318
+ transition: background 0.15s, transform 0.1s;
319
+ }
320
+ .overlay-actions button:active {
321
+ transform: scale(0.95);
213
322
  }
214
323
  #reconnect-btn {
215
- background: #533483;
216
- color: white;
324
+ background: var(--accent);
325
+ color: #ffffff;
326
+ }
327
+ #reconnect-btn:hover {
328
+ background: var(--accent-hover);
217
329
  }
218
330
  #back-to-sessions {
219
- background: #0f3460;
220
- color: #e0e0e0;
331
+ background: rgba(255,255,255,0.15);
332
+ color: #ffffff;
221
333
  }
334
+ #back-to-sessions:hover {
335
+ background: rgba(255,255,255,0.25);
336
+ }
337
+
222
338
  </style>
223
339
  </head>
224
340
  <body>
225
341
  <div id="status-bar">
226
342
  <div class="left">
227
- <button id="back-btn" onclick="location.href = '/'">‹</button>
343
+ <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>
228
344
  <span id="status-dot"></span>
229
345
  <span id="session-name">…</span>
230
346
  </div>
231
347
  <div class="right">
232
348
  <span id="status-text">Connecting…</span>
233
- <span id="version-text" style="font-size: 11px; color: #555"></span>
234
- <button id="stop-btn" title="Stop session">■ Stop</button>
349
+ <span id="version-text" style="font-size: 11px; color: var(--text-muted)"></span>
350
+ <div class="bar-group">
351
+ <button class="bar-btn" id="zoom-out" title="Decrease font size">−</button>
352
+ <button class="bar-btn" id="zoom-in" title="Increase font size">+</button>
353
+ </div>
354
+ <button class="bar-btn" id="theme-toggle" title="Toggle theme"><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></button>
355
+ <button id="stop-btn" title="Stop session"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>Stop</button>
235
356
  </div>
236
357
  </div>
237
358
 
238
359
  <div id="terminal-container"></div>
239
360
 
240
361
  <div id="key-bar">
241
- <button class="key-btn" data-key="&#x1b;[A">↑</button>
242
- <button class="key-btn" data-key="&#x1b;[B">↓</button>
243
- <button class="key-btn" data-key="&#x1b;[D">←</button>
244
- <button class="key-btn" data-key="&#x1b;[C">→</button>
245
- <div class="key-sep"></div>
246
- <button class="key-btn wide" data-key="&#x09;">Tab</button>
247
- <button class="key-btn wide" data-key="&#x0d;">Enter</button>
248
- <button class="key-btn" data-key="&#x1b;">Esc</button>
362
+ <button class="key-btn" data-key="&#x1b;[A" title="Previous command">↑<span class="hint">prev</span></button>
363
+ <button class="key-btn" data-key="&#x1b;[B" title="Next command">↓<span class="hint">next</span></button>
364
+ <button class="key-btn" data-key="&#x1b;[D" title="Left">←</button>
365
+ <button class="key-btn" data-key="&#x1b;[C" title="Right">→</button>
366
+ <button class="key-btn" data-key="&#x1b;[H" title="Home">Home</button>
367
+ <button class="key-btn" data-key="&#x1b;[F" title="End">End</button>
249
368
  <div class="key-sep"></div>
250
- <button class="key-btn" data-key="&#x03;">^C</button>
251
- <button class="key-btn" data-key="&#x04;">^D</button>
252
- <button class="key-btn" data-key="&#x1a;">^Z</button>
253
- <button class="key-btn" data-key="&#x0c;">^L</button>
369
+ <button class="key-btn wide" data-key="&#x09;" title="Autocomplete">Tab</button>
254
370
  <div class="key-sep"></div>
255
- <button class="key-btn" id="zoom-out">A-</button>
256
- <button class="key-btn" id="zoom-in">A+</button>
371
+ <button class="key-btn" data-key="&#x03;" title="Interrupt process">^C<span class="hint">stop</span></button>
257
372
  </div>
258
373
 
259
374
  <div id="reconnect-overlay">
@@ -277,6 +392,50 @@
277
392
  const statusText = document.getElementById('status-text');
278
393
  const sessionName = document.getElementById('session-name');
279
394
  const reconnectOverlay = document.getElementById('reconnect-overlay');
395
+ let sessionExited = false;
396
+ let reconnectTimer = null;
397
+ let reconnectDelay = 3000;
398
+ const MAX_RECONNECT_DELAY = 30000;
399
+
400
+ // Terminal themes
401
+ const darkTermTheme = {
402
+ background: '#1e1e1e', foreground: '#d4d4d4', cursor: '#aeafad', cursorAccent: '#1e1e1e',
403
+ selectionBackground: 'rgba(38, 79, 120, 0.5)',
404
+ black: '#000000', red: '#cd3131', green: '#0dbc79', yellow: '#e5e510',
405
+ blue: '#2472c8', magenta: '#bc3fbc', cyan: '#11a8cd', white: '#e5e5e5',
406
+ brightBlack: '#666666', brightRed: '#f14c4c', brightGreen: '#23d18b',
407
+ brightYellow: '#f5f543', brightBlue: '#3b8eea', brightMagenta: '#d670d6',
408
+ brightCyan: '#29b8db', brightWhite: '#e5e5e5',
409
+ };
410
+ const lightTermTheme = {
411
+ background: '#ffffff', foreground: '#1e1e1e', cursor: '#000000', cursorAccent: '#ffffff',
412
+ selectionBackground: 'rgba(0, 120, 215, 0.3)',
413
+ black: '#000000', red: '#cd3131', green: '#00bc7c', yellow: '#949800',
414
+ blue: '#0451a5', magenta: '#bc05bc', cyan: '#0598bc', white: '#555555',
415
+ brightBlack: '#666666', brightRed: '#cd3131', brightGreen: '#14ce14',
416
+ brightYellow: '#b5ba00', brightBlue: '#0451a5', brightMagenta: '#bc05bc',
417
+ brightCyan: '#0598bc', brightWhite: '#a5a5a5',
418
+ };
419
+
420
+ function getTheme() { return localStorage.getItem('termbeam-theme') || 'dark'; }
421
+ function applyTheme(theme) {
422
+ document.documentElement.setAttribute('data-theme', theme);
423
+ document.querySelector('meta[name="theme-color"]').content =
424
+ theme === 'light' ? '#f3f3f3' : '#1e1e1e';
425
+ const btn = document.getElementById('theme-toggle');
426
+ if (btn) btn.innerHTML = theme === 'light'
427
+ ? '<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>'
428
+ : '<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>';
429
+ localStorage.setItem('termbeam-theme', theme);
430
+ if (window._term) {
431
+ window._term.options.theme = theme === 'light' ? lightTermTheme : darkTermTheme;
432
+ }
433
+ }
434
+ applyTheme(getTheme());
435
+
436
+ document.getElementById('theme-toggle').addEventListener('click', () => {
437
+ applyTheme(getTheme() === 'light' ? 'dark' : 'light');
438
+ });
280
439
 
281
440
  // Load Nerd Font, then init terminal
282
441
  const nerdFont = new FontFace(
@@ -307,32 +466,11 @@
307
466
  fontWeightBold: 'bold',
308
467
  letterSpacing: 0,
309
468
  lineHeight: 1.1,
310
- theme: {
311
- background: '#1a1a2e',
312
- foreground: '#e0e0e0',
313
- cursor: '#533483',
314
- cursorAccent: '#1a1a2e',
315
- selectionBackground: 'rgba(83, 52, 131, 0.4)',
316
- black: '#1a1a2e',
317
- red: '#e74c3c',
318
- green: '#2ecc71',
319
- yellow: '#f1c40f',
320
- blue: '#3498db',
321
- magenta: '#9b59b6',
322
- cyan: '#1abc9c',
323
- white: '#ecf0f1',
324
- brightBlack: '#636e72',
325
- brightRed: '#ff6b6b',
326
- brightGreen: '#55efc4',
327
- brightYellow: '#ffeaa7',
328
- brightBlue: '#74b9ff',
329
- brightMagenta: '#a29bfe',
330
- brightCyan: '#81ecec',
331
- brightWhite: '#ffffff',
332
- },
469
+ theme: getTheme() === 'light' ? lightTermTheme : darkTermTheme,
333
470
  allowProposedApi: true,
334
471
  scrollback: 10000,
335
472
  });
473
+ window._term = term;
336
474
 
337
475
  const fitAddon = new window.FitAddon.FitAddon();
338
476
  const webLinksAddon = new window.WebLinksAddon.WebLinksAddon();
@@ -351,8 +489,9 @@
351
489
 
352
490
  ws.onopen = () => {
353
491
  statusDot.className = 'connected';
354
- statusText.textContent = 'Connected';
492
+ statusText.textContent = '';
355
493
  reconnectOverlay.classList.remove('visible');
494
+ reconnectDelay = 3000;
356
495
  // Attach to session
357
496
  ws.send(JSON.stringify({ type: 'attach', sessionId }));
358
497
  };
@@ -369,6 +508,8 @@
369
508
  ws.send(JSON.stringify({ type: 'resize', cols: dims.cols, rows: dims.rows }));
370
509
  }
371
510
  } else if (msg.type === 'exit') {
511
+ sessionExited = true;
512
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
372
513
  statusText.textContent = `Exited (code ${msg.code})`;
373
514
  statusDot.className = '';
374
515
  reconnectOverlay.querySelector('.msg').textContent =
@@ -388,6 +529,9 @@
388
529
  statusDot.className = '';
389
530
  statusText.textContent = 'Disconnected';
390
531
  reconnectOverlay.classList.add('visible');
532
+ if (!sessionExited) {
533
+ scheduleReconnect();
534
+ }
391
535
  };
392
536
 
393
537
  ws.onerror = () => {
@@ -415,7 +559,7 @@
415
559
  window.addEventListener('resize', doResize);
416
560
  screen.orientation?.addEventListener('change', () => setTimeout(doResize, 150));
417
561
 
418
- // Key bar (skip zoom buttons — handled separately)
562
+ // Key bar
419
563
  // Use mousedown + preventDefault to stop buttons from stealing focus/opening keyboard
420
564
  document.getElementById('key-bar').addEventListener('mousedown', (e) => {
421
565
  // Only prevent default on buttons, not the scrollable bar itself
@@ -429,34 +573,43 @@
429
573
  .addEventListener('touchstart', () => {}, { passive: true });
430
574
  document.getElementById('key-bar').addEventListener('click', (e) => {
431
575
  const btn = e.target.closest('.key-btn');
432
- if (!btn || btn.id === 'zoom-in' || btn.id === 'zoom-out') return;
576
+ if (!btn) return;
433
577
  if (ws && ws.readyState === 1) {
434
578
  ws.send(JSON.stringify({ type: 'input', data: btn.dataset.key }));
435
579
  }
436
580
  // Don't call term.focus() here — it opens the soft keyboard
437
581
  });
438
582
 
439
- // Zoom
440
- const MIN_FONT = 2,
441
- MAX_FONT = 28;
442
- let fontSize = parseInt(localStorage.getItem('termbeam-fontsize') || '8', 10);
443
-
583
+ // Zoom (status bar)
584
+ const MIN_FONT = 2, MAX_FONT = 28;
585
+ let fontSize = savedFontSize;
444
586
  function applyZoom(size) {
445
587
  fontSize = Math.max(MIN_FONT, Math.min(MAX_FONT, size));
446
588
  term.options.fontSize = fontSize;
447
589
  localStorage.setItem('termbeam-fontsize', fontSize);
448
590
  doResize();
449
591
  }
450
-
451
- document.getElementById('zoom-in').addEventListener('click', () => {
452
- applyZoom(fontSize + 2);
453
- });
454
- document.getElementById('zoom-out').addEventListener('click', () => {
455
- applyZoom(fontSize - 2);
456
- });
592
+ document.getElementById('zoom-in').addEventListener('click', () => applyZoom(fontSize + 2));
593
+ document.getElementById('zoom-out').addEventListener('click', () => applyZoom(fontSize - 2));
594
+
595
+ function scheduleReconnect() {
596
+ if (reconnectTimer) clearTimeout(reconnectTimer);
597
+ const seconds = Math.round(reconnectDelay / 1000);
598
+ reconnectOverlay.querySelector('.msg').textContent =
599
+ `Disconnected — reconnecting in ${seconds}s…`;
600
+ reconnectTimer = setTimeout(() => {
601
+ reconnectTimer = null;
602
+ reconnectOverlay.querySelector('.msg').textContent = 'Reconnecting…';
603
+ reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY);
604
+ connect();
605
+ }, reconnectDelay);
606
+ }
457
607
 
458
608
  // Reconnect
459
609
  document.getElementById('reconnect-btn').addEventListener('click', () => {
610
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
611
+ sessionExited = false;
612
+ reconnectDelay = 3000;
460
613
  term.clear();
461
614
  connect();
462
615
  });
@@ -486,11 +639,11 @@
486
639
  if (keyboardHeight > 50) {
487
640
  // Keyboard is open — move key bar above it
488
641
  keyBar.style.bottom = keyboardHeight + 'px';
489
- container.style.bottom = 44 + keyboardHeight + 'px';
642
+ container.style.bottom = 52 + keyboardHeight + 'px';
490
643
  } else {
491
644
  // Keyboard closed
492
645
  keyBar.style.bottom = '0px';
493
- container.style.bottom = '44px';
646
+ container.style.bottom = '52px';
494
647
  }
495
648
  // Refit terminal to new available space
496
649
  setTimeout(() => doResize(), 50);