termbeam 0.0.5 β†’ 0.0.7

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.7",
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": {
Binary file
Binary file
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
2
+ <rect width="512" height="512" rx="80" fill="#1e1e1e"/>
3
+ <rect x="32" y="32" width="448" height="448" rx="56" fill="#252526" stroke="#3c3c3c" stroke-width="4"/>
4
+ <polyline points="140,200 220,260 140,320" fill="none" stroke="#0078d4" stroke-width="32" stroke-linecap="round" stroke-linejoin="round"/>
5
+ <line x1="260" y1="320" x2="380" y2="320" stroke="#d4d4d4" stroke-width="28" stroke-linecap="round"/>
6
+ </svg>
package/public/index.html CHANGED
@@ -8,9 +8,49 @@
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
+ <link rel="manifest" href="/manifest.json" />
13
+ <link rel="apple-touch-icon" href="/icons/icon-192.png" />
12
14
  <title>TermBeam</title>
13
15
  <style>
16
+ :root {
17
+ --bg: #1e1e1e;
18
+ --surface: #252526;
19
+ --border: #3c3c3c;
20
+ --border-subtle: #474747;
21
+ --text: #d4d4d4;
22
+ --text-secondary: #858585;
23
+ --text-dim: #6e6e6e;
24
+ --text-muted: #5a5a5a;
25
+ --accent: #0078d4;
26
+ --accent-hover: #1a8ae8;
27
+ --accent-active: #005a9e;
28
+ --danger: #f14c4c;
29
+ --danger-hover: #d73a3a;
30
+ --success: #89d185;
31
+ --info: #b0b0b0;
32
+ --shadow: rgba(0,0,0,0.15);
33
+ --overlay-bg: rgba(0,0,0,0.7);
34
+ }
35
+ [data-theme="light"] {
36
+ --bg: #ffffff;
37
+ --surface: #f3f3f3;
38
+ --border: #e0e0e0;
39
+ --border-subtle: #d0d0d0;
40
+ --text: #1e1e1e;
41
+ --text-secondary: #616161;
42
+ --text-dim: #767676;
43
+ --text-muted: #a0a0a0;
44
+ --accent: #0078d4;
45
+ --accent-hover: #106ebe;
46
+ --accent-active: #005a9e;
47
+ --danger: #e51400;
48
+ --danger-hover: #c20000;
49
+ --success: #16825d;
50
+ --info: #616161;
51
+ --shadow: rgba(0,0,0,0.06);
52
+ --overlay-bg: rgba(0,0,0,0.4);
53
+ }
14
54
  * {
15
55
  margin: 0;
16
56
  padding: 0;
@@ -20,39 +60,66 @@
20
60
  body {
21
61
  height: 100%;
22
62
  width: 100%;
23
- background: #1a1a2e;
24
- color: #e0e0e0;
63
+ background: var(--bg);
64
+ color: var(--text);
25
65
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
66
+ transition: background 0.3s, color 0.3s;
26
67
  }
27
68
 
28
69
  .header {
29
70
  padding: 20px 16px 12px;
30
71
  text-align: center;
31
- border-bottom: 1px solid #0f3460;
72
+ border-bottom: 1px solid var(--border);
73
+ transition: border-color 0.3s;
74
+ position: relative;
32
75
  }
33
76
  .header h1 {
34
77
  font-size: 22px;
35
78
  font-weight: 700;
36
79
  }
37
80
  .header h1 span {
38
- color: #533483;
81
+ color: var(--accent);
39
82
  }
40
83
  .header p {
41
84
  font-size: 13px;
42
- color: #888;
85
+ color: var(--text-secondary);
43
86
  margin-top: 4px;
44
87
  }
88
+ .theme-toggle {
89
+ position: absolute;
90
+ top: 16px;
91
+ right: 16px;
92
+ background: none;
93
+ border: 1px solid var(--border);
94
+ color: var(--text-dim);
95
+ width: 32px;
96
+ height: 32px;
97
+ border-radius: 8px;
98
+ cursor: pointer;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ font-size: 16px;
103
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
104
+ -webkit-tap-highlight-color: transparent;
105
+ }
106
+ .theme-toggle:hover {
107
+ color: var(--text);
108
+ border-color: var(--border-subtle);
109
+ background: var(--border);
110
+ }
45
111
 
46
112
  .sessions-list {
47
113
  padding: 16px;
114
+ padding-bottom: 80px;
48
115
  display: flex;
49
116
  flex-direction: column;
50
117
  gap: 12px;
51
118
  }
52
119
 
53
120
  .session-card {
54
- background: #16213e;
55
- border: 1px solid #0f3460;
121
+ background: var(--surface);
122
+ border: 1px solid var(--border);
56
123
  border-radius: 12px;
57
124
  padding: 16px;
58
125
  display: flex;
@@ -60,14 +127,14 @@
60
127
  gap: 8px;
61
128
  text-decoration: none;
62
129
  color: inherit;
63
- transition: transform 0.2s ease;
130
+ transition: transform 0.2s ease, border-color 0.15s, background 0.3s;
64
131
  cursor: pointer;
65
132
  -webkit-tap-highlight-color: transparent;
66
133
  position: relative;
67
134
  z-index: 1;
68
135
  }
69
136
  .session-card:hover {
70
- border-color: #533483;
137
+ border-color: var(--accent);
71
138
  }
72
139
 
73
140
  .swipe-wrap {
@@ -81,7 +148,7 @@
81
148
  top: 0;
82
149
  bottom: 0;
83
150
  width: 80px;
84
- background: #e74c3c;
151
+ background: var(--danger);
85
152
  display: flex;
86
153
  align-items: center;
87
154
  justify-content: center;
@@ -123,22 +190,23 @@
123
190
  width: 10px;
124
191
  height: 10px;
125
192
  border-radius: 50%;
126
- background: #2ecc71;
193
+ background: var(--success);
127
194
  flex-shrink: 0;
128
195
  }
129
196
  .session-card .pid {
130
197
  font-size: 12px;
131
- color: #888;
132
- background: #1a1a2e;
198
+ color: var(--text-secondary);
199
+ background: var(--bg);
133
200
  padding: 2px 8px;
134
201
  border-radius: 4px;
202
+ transition: background 0.3s, color 0.3s;
135
203
  }
136
204
  .session-card .details {
137
205
  display: flex;
138
206
  flex-wrap: wrap;
139
207
  gap: 6px 16px;
140
208
  font-size: 13px;
141
- color: #aaa;
209
+ color: var(--info);
142
210
  }
143
211
  .session-card .details span {
144
212
  display: flex;
@@ -147,43 +215,57 @@
147
215
  }
148
216
  .session-card .connect-btn {
149
217
  align-self: flex-end;
150
- background: #533483;
151
- color: white;
218
+ background: var(--accent);
219
+ color: #ffffff;
152
220
  border: none;
153
221
  border-radius: 8px;
154
222
  padding: 8px 20px;
155
223
  font-size: 14px;
156
224
  font-weight: 600;
157
225
  cursor: pointer;
226
+ transition: background 0.15s, transform 0.1s;
227
+ }
228
+ .session-card .connect-btn:hover {
229
+ background: var(--accent-hover);
158
230
  }
159
231
  .session-card .connect-btn:active {
160
- background: #6a42a8;
232
+ background: var(--accent-active);
233
+ transform: scale(0.95);
161
234
  }
162
235
 
163
236
  .new-session {
164
- margin: 0 16px;
237
+ position: fixed;
238
+ bottom: 16px;
239
+ left: 16px;
240
+ right: 16px;
165
241
  padding: 14px;
166
- background: transparent;
167
- border: 2px dashed #0f3460;
242
+ background: var(--accent);
243
+ color: #ffffff;
244
+ border: none;
168
245
  border-radius: 12px;
169
- color: #888;
170
246
  font-size: 15px;
171
247
  font-weight: 600;
172
248
  cursor: pointer;
173
249
  text-align: center;
174
- transition:
175
- border-color 0.15s,
176
- color 0.15s;
250
+ z-index: 50;
251
+ transition: background 0.15s, transform 0.1s, box-shadow 0.15s;
252
+ box-shadow: 0 2px 8px rgba(0, 120, 212, 0.3);
253
+ padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
254
+ }
255
+ .new-session:hover {
256
+ background: var(--accent-hover);
257
+ box-shadow: 0 4px 12px rgba(0, 120, 212, 0.4);
177
258
  }
178
259
  .new-session:active {
179
- border-color: #533483;
180
- color: #e0e0e0;
260
+ background: var(--accent-active);
261
+ transform: scale(0.98);
262
+ box-shadow: 0 1px 4px rgba(0, 120, 212, 0.2);
181
263
  }
182
264
 
183
265
  .empty-state {
184
266
  text-align: center;
185
267
  padding: 60px 20px;
186
- color: #666;
268
+ color: var(--text-muted);
187
269
  font-size: 15px;
188
270
  }
189
271
 
@@ -195,7 +277,7 @@
195
277
  left: 0;
196
278
  right: 0;
197
279
  bottom: 0;
198
- background: rgba(0, 0, 0, 0.7);
280
+ background: var(--overlay-bg);
199
281
  z-index: 100;
200
282
  justify-content: center;
201
283
  align-items: center;
@@ -204,11 +286,12 @@
204
286
  display: flex;
205
287
  }
206
288
  .modal {
207
- background: #16213e;
289
+ background: var(--surface);
208
290
  border-radius: 16px;
209
291
  width: 90%;
210
292
  max-width: 500px;
211
293
  padding: 24px 20px;
294
+ transition: background 0.3s;
212
295
  }
213
296
  .modal h2 {
214
297
  font-size: 18px;
@@ -217,7 +300,7 @@
217
300
  .modal label {
218
301
  display: block;
219
302
  font-size: 13px;
220
- color: #aaa;
303
+ color: var(--text-secondary);
221
304
  margin-bottom: 4px;
222
305
  margin-top: 12px;
223
306
  }
@@ -225,14 +308,15 @@
225
308
  .modal select {
226
309
  width: 100%;
227
310
  padding: 10px 12px;
228
- background: #1a1a2e;
229
- border: 1px solid #0f3460;
311
+ background: var(--bg);
312
+ border: 1px solid var(--border);
230
313
  border-radius: 8px;
231
- color: #e0e0e0;
314
+ color: var(--text);
232
315
  font-size: 15px;
233
316
  outline: none;
234
317
  -webkit-appearance: none;
235
318
  appearance: none;
319
+ transition: background 0.3s, border-color 0.15s, color 0.3s;
236
320
  }
237
321
  .modal select {
238
322
  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 +327,7 @@
243
327
  }
244
328
  .modal input:focus,
245
329
  .modal select:focus {
246
- border-color: #533483;
330
+ border-color: var(--accent);
247
331
  }
248
332
  .modal-actions {
249
333
  display: flex;
@@ -258,14 +342,24 @@
258
342
  font-size: 15px;
259
343
  font-weight: 600;
260
344
  cursor: pointer;
345
+ transition: background 0.15s, transform 0.1s;
346
+ }
347
+ .modal-actions button:active {
348
+ transform: scale(0.95);
261
349
  }
262
350
  .btn-cancel {
263
- background: #0f3460;
264
- color: #e0e0e0;
351
+ background: var(--border);
352
+ color: var(--text);
353
+ }
354
+ .btn-cancel:hover {
355
+ background: var(--border-subtle);
265
356
  }
266
357
  .btn-create {
267
- background: #533483;
268
- color: white;
358
+ background: var(--accent);
359
+ color: #ffffff;
360
+ }
361
+ .btn-create:hover {
362
+ background: var(--accent-hover);
269
363
  }
270
364
 
271
365
  /* Folder browser */
@@ -277,9 +371,9 @@
277
371
  flex: 1;
278
372
  }
279
373
  .cwd-browse-btn {
280
- background: #0f3460;
281
- color: #e0e0e0;
282
- border: 1px solid #0f3460;
374
+ background: var(--border);
375
+ color: var(--text);
376
+ border: 1px solid var(--border);
283
377
  border-radius: 8px;
284
378
  padding: 0 14px;
285
379
  font-size: 18px;
@@ -287,9 +381,14 @@
287
381
  flex-shrink: 0;
288
382
  display: flex;
289
383
  align-items: center;
384
+ transition: background 0.15s, border-color 0.15s;
385
+ }
386
+ .cwd-browse-btn:hover {
387
+ border-color: var(--accent);
290
388
  }
291
389
  .cwd-browse-btn:active {
292
- background: #533483;
390
+ background: var(--accent);
391
+ color: #ffffff;
293
392
  }
294
393
 
295
394
  .browser-overlay {
@@ -299,7 +398,7 @@
299
398
  left: 0;
300
399
  right: 0;
301
400
  bottom: 0;
302
- background: rgba(0, 0, 0, 0.8);
401
+ background: var(--overlay-bg);
303
402
  z-index: 200;
304
403
  justify-content: center;
305
404
  align-items: flex-end;
@@ -308,7 +407,7 @@
308
407
  display: flex;
309
408
  }
310
409
  .browser-sheet {
311
- background: #16213e;
410
+ background: var(--surface);
312
411
  border-radius: 16px 16px 0 0;
313
412
  width: 100%;
314
413
  max-width: 500px;
@@ -316,10 +415,11 @@
316
415
  display: flex;
317
416
  flex-direction: column;
318
417
  overflow: hidden;
418
+ transition: background 0.3s;
319
419
  }
320
420
  .browser-header {
321
421
  padding: 16px 16px 12px;
322
- border-bottom: 1px solid #0f3460;
422
+ border-bottom: 1px solid var(--border);
323
423
  display: flex;
324
424
  align-items: center;
325
425
  justify-content: space-between;
@@ -331,14 +431,18 @@
331
431
  .browser-close {
332
432
  background: none;
333
433
  border: none;
334
- color: #888;
434
+ color: var(--text-dim);
335
435
  font-size: 24px;
336
436
  cursor: pointer;
337
437
  padding: 0 4px;
338
438
  line-height: 1;
439
+ transition: color 0.15s;
440
+ }
441
+ .browser-close:hover {
442
+ color: var(--text);
339
443
  }
340
444
  .browser-close:active {
341
- color: #e0e0e0;
445
+ color: var(--text);
342
446
  }
343
447
 
344
448
  .browser-breadcrumb {
@@ -349,31 +453,32 @@
349
453
  font-size: 13px;
350
454
  overflow-x: auto;
351
455
  white-space: nowrap;
352
- border-bottom: 1px solid #0f3460;
456
+ border-bottom: 1px solid var(--border);
353
457
  flex-shrink: 0;
354
458
  -webkit-overflow-scrolling: touch;
355
459
  }
356
460
  .crumb {
357
461
  background: none;
358
462
  border: none;
359
- color: #888;
463
+ color: var(--text-dim);
360
464
  font-size: 13px;
361
465
  cursor: pointer;
362
466
  padding: 4px 6px;
363
467
  border-radius: 4px;
364
468
  flex-shrink: 0;
469
+ transition: background 0.15s, color 0.15s;
365
470
  }
366
471
  .crumb:active,
367
472
  .crumb:hover {
368
- background: #0f3460;
369
- color: #e0e0e0;
473
+ background: var(--border);
474
+ color: var(--text);
370
475
  }
371
476
  .crumb.current {
372
- color: #e0e0e0;
477
+ color: var(--text);
373
478
  font-weight: 600;
374
479
  }
375
480
  .crumb-sep {
376
- color: #444;
481
+ color: var(--border-subtle);
377
482
  flex-shrink: 0;
378
483
  }
379
484
 
@@ -386,7 +491,7 @@
386
491
  .browser-empty {
387
492
  text-align: center;
388
493
  padding: 40px 20px;
389
- color: #666;
494
+ color: var(--text-muted);
390
495
  font-size: 14px;
391
496
  }
392
497
  .folder-item {
@@ -395,15 +500,15 @@
395
500
  gap: 12px;
396
501
  padding: 12px 16px;
397
502
  cursor: pointer;
398
- border-bottom: 1px solid rgba(15, 52, 96, 0.5);
503
+ border-bottom: 1px solid rgba(60, 60, 60, 0.5);
399
504
  transition: background 0.1s;
400
505
  -webkit-tap-highlight-color: transparent;
401
506
  }
402
507
  .folder-item:active {
403
- background: rgba(83, 52, 131, 0.3);
508
+ background: rgba(0, 120, 212, 0.2);
404
509
  }
405
510
  .folder-item:hover {
406
- background: rgba(83, 52, 131, 0.15);
511
+ background: rgba(0, 120, 212, 0.1);
407
512
  }
408
513
  .folder-icon {
409
514
  font-size: 22px;
@@ -413,28 +518,28 @@
413
518
  }
414
519
  .folder-name {
415
520
  font-size: 15px;
416
- color: #e0e0e0;
521
+ color: var(--text);
417
522
  flex: 1;
418
523
  overflow: hidden;
419
524
  text-overflow: ellipsis;
420
525
  white-space: nowrap;
421
526
  }
422
527
  .folder-arrow {
423
- color: #444;
528
+ color: var(--border-subtle);
424
529
  font-size: 18px;
425
530
  flex-shrink: 0;
426
531
  }
427
532
 
428
533
  .browser-footer {
429
534
  padding: 12px 16px calc(env(safe-area-inset-bottom, 8px) + 8px);
430
- border-top: 1px solid #0f3460;
535
+ border-top: 1px solid var(--border);
431
536
  display: flex;
432
537
  flex-direction: column;
433
538
  gap: 8px;
434
539
  }
435
540
  .browser-current-path {
436
541
  font-size: 12px;
437
- color: #888;
542
+ color: var(--text-dim);
438
543
  overflow: hidden;
439
544
  text-overflow: ellipsis;
440
545
  white-space: nowrap;
@@ -442,23 +547,29 @@
442
547
  .browser-select-btn {
443
548
  width: 100%;
444
549
  padding: 12px;
445
- background: #533483;
446
- color: white;
550
+ background: var(--accent);
551
+ color: #ffffff;
447
552
  border: none;
448
553
  border-radius: 10px;
449
554
  font-size: 16px;
450
555
  font-weight: 600;
451
556
  cursor: pointer;
557
+ transition: background 0.15s, transform 0.1s;
558
+ }
559
+ .browser-select-btn:hover {
560
+ background: var(--accent-hover);
452
561
  }
453
562
  .browser-select-btn:active {
454
- background: #6a42a8;
563
+ background: var(--accent-active);
564
+ transform: scale(0.98);
455
565
  }
456
566
  </style>
457
567
  </head>
458
568
  <body>
459
569
  <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>
570
+ <h1>πŸ“‘ Term<span>Beam</span></h1>
571
+ <p>Beam your terminal to any device Β· <span id="version" style="color: var(--accent)"></span></p>
572
+ <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
573
  </div>
463
574
 
464
575
  <div class="sessions-list" id="sessions-list"></div>
@@ -473,7 +584,7 @@
473
584
  <select id="sess-shell">
474
585
  <option value="">Loading shells…</option>
475
586
  </select>
476
- <label for="sess-cmd">Initial Command <span style="color:#666;font-weight:normal">(optional)</span></label>
587
+ <label for="sess-cmd">Initial Command <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
477
588
  <input type="text" id="sess-cmd" placeholder="e.g. copilot, htop, vim" />
478
589
  <label for="sess-cwd">Working Directory</label>
479
590
  <div class="cwd-picker">
@@ -535,6 +646,23 @@
535
646
  </div>
536
647
 
537
648
  <script>
649
+ // Theme
650
+ function getTheme() { return localStorage.getItem('termbeam-theme') || 'dark'; }
651
+ function applyTheme(theme) {
652
+ document.documentElement.setAttribute('data-theme', theme);
653
+ document.querySelector('meta[name="theme-color"]').content =
654
+ theme === 'light' ? '#f3f3f3' : '#1e1e1e';
655
+ const btn = document.getElementById('theme-toggle');
656
+ if (btn) btn.innerHTML = theme === 'light'
657
+ ? '<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>'
658
+ : '<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>';
659
+ localStorage.setItem('termbeam-theme', theme);
660
+ }
661
+ applyTheme(getTheme());
662
+ document.getElementById('theme-toggle').addEventListener('click', () => {
663
+ applyTheme(getTheme() === 'light' ? 'dark' : 'light');
664
+ });
665
+
538
666
  const listEl = document.getElementById('sessions-list');
539
667
  const modal = document.getElementById('modal');
540
668
 
@@ -815,6 +943,10 @@
815
943
 
816
944
  loadSessions();
817
945
  setInterval(loadSessions, 3000);
946
+
947
+ if ('serviceWorker' in navigator) {
948
+ navigator.serviceWorker.register('/sw.js').catch(() => {});
949
+ }
818
950
  </script>
819
951
  </body>
820
952
  </html>
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "TermBeam",
3
+ "short_name": "TermBeam",
4
+ "description": "Beam your terminal to any device",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#1e1e1e",
8
+ "theme_color": "#1e1e1e",
9
+ "icons": [
10
+ { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
11
+ { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
12
+ { "src": "/icons/icon.svg", "sizes": "any", "type": "image/svg+xml" }
13
+ ]
14
+ }
package/public/sw.js ADDED
@@ -0,0 +1,54 @@
1
+ const CACHE_NAME = 'termbeam-v1';
2
+ const SHELL_URLS = ['/', '/terminal'];
3
+
4
+ self.addEventListener('install', (event) => {
5
+ event.waitUntil(
6
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_URLS))
7
+ );
8
+ self.skipWaiting();
9
+ });
10
+
11
+ self.addEventListener('activate', (event) => {
12
+ event.waitUntil(
13
+ caches.keys().then((keys) =>
14
+ Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
15
+ )
16
+ );
17
+ self.clients.claim();
18
+ });
19
+
20
+ self.addEventListener('fetch', (event) => {
21
+ const url = new URL(event.request.url);
22
+
23
+ // Don't cache WebSocket upgrades or external resources
24
+ if (
25
+ event.request.mode === 'websocket' ||
26
+ url.protocol === 'ws:' ||
27
+ url.protocol === 'wss:' ||
28
+ url.origin !== self.location.origin
29
+ ) {
30
+ return;
31
+ }
32
+
33
+ // Network-first for API calls
34
+ if (url.pathname.startsWith('/api/')) {
35
+ event.respondWith(
36
+ fetch(event.request).catch(() => caches.match(event.request))
37
+ );
38
+ return;
39
+ }
40
+
41
+ // Cache-first for static assets
42
+ event.respondWith(
43
+ caches.match(event.request).then((cached) => {
44
+ if (cached) return cached;
45
+ return fetch(event.request).then((response) => {
46
+ if (response.ok) {
47
+ const clone = response.clone();
48
+ caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
49
+ }
50
+ return response;
51
+ });
52
+ })
53
+ );
54
+ });