termbeam 1.2.0 → 1.2.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/terminal.html +309 -90
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
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": {
@@ -36,9 +36,10 @@
36
36
  --danger: #f14c4c;
37
37
  --danger-hover: #d73a3a;
38
38
  --success: #89d185;
39
- --key-bg: #2d2d2d;
40
- --key-border: #404040;
41
- --key-shadow: rgba(0, 0, 0, 0.4);
39
+ --key-bg: #4a4a4c;
40
+ --key-border: #5a5a5c;
41
+ --key-shadow: rgba(0, 0, 0, 0.5);
42
+ --key-special-bg: #333335;
42
43
  --overlay-bg: rgba(0, 0, 0, 0.85);
43
44
  }
44
45
  [data-theme='light'] {
@@ -56,9 +57,10 @@
56
57
  --danger: #e51400;
57
58
  --danger-hover: #c20000;
58
59
  --success: #16825d;
59
- --key-bg: #e8e8e8;
60
- --key-border: #d0d0d0;
61
- --key-shadow: rgba(0, 0, 0, 0.08);
60
+ --key-bg: #ffffff;
61
+ --key-border: #b5b5b5;
62
+ --key-shadow: rgba(0, 0, 0, 0.12);
63
+ --key-special-bg: #adb5bd;
62
64
  --overlay-bg: rgba(0, 0, 0, 0.5);
63
65
  }
64
66
  @font-face {
@@ -416,7 +418,7 @@
416
418
  top: calc(41px + env(safe-area-inset-top, 0px));
417
419
  left: env(safe-area-inset-left, 0px);
418
420
  right: env(safe-area-inset-right, 0px);
419
- bottom: calc(52px + env(safe-area-inset-bottom, 0px));
421
+ bottom: calc(80px + env(safe-area-inset-bottom, 0px));
420
422
  display: flex;
421
423
  overflow: hidden;
422
424
  }
@@ -449,76 +451,124 @@
449
451
  bottom: 0;
450
452
  left: 0;
451
453
  right: 0;
452
- height: calc(52px + env(safe-area-inset-bottom, 0px));
454
+ height: calc(80px + env(safe-area-inset-bottom, 0px));
453
455
  display: flex;
454
- align-items: center;
455
- background: var(--surface);
456
+ flex-direction: column;
457
+ background: #1c1c1e;
456
458
  border-top: 1px solid var(--border);
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;
459
+ padding: 4px calc(3px + env(safe-area-inset-right, 0px)) env(safe-area-inset-bottom, 0px)
460
+ calc(3px + env(safe-area-inset-left, 0px));
461
+ gap: 6px;
460
462
  z-index: 50;
461
463
  transition:
462
464
  background 0.3s,
463
465
  border-color 0.3s;
464
466
  }
467
+ [data-theme='light'] #key-bar {
468
+ background: #d1d3d9;
469
+ }
470
+ .key-row {
471
+ display: flex;
472
+ align-items: center;
473
+ gap: 4px;
474
+ flex: 1;
475
+ }
465
476
  .key-btn {
466
477
  min-width: 0;
467
- height: 40px;
478
+ height: 34px;
468
479
  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;
480
+ color: #fff;
481
+ border: none;
482
+ border-radius: 6px;
483
+ font-size: 13px;
484
+ font-weight: 500;
474
485
  cursor: pointer;
475
486
  display: flex;
476
- flex-direction: column;
477
487
  align-items: center;
478
488
  justify-content: center;
479
489
  -webkit-tap-highlight-color: transparent;
480
490
  user-select: none;
481
491
  white-space: nowrap;
482
- padding: 2px 8px;
492
+ padding: 0 6px;
483
493
  flex: 1 1 0;
484
- gap: 0;
485
494
  line-height: 1;
486
495
  transition:
487
- background 0.15s,
488
- color 0.15s,
489
- border-color 0.15s,
490
- transform 0.1s,
491
- box-shadow 0.15s;
496
+ background 0.1s,
497
+ transform 0.08s,
498
+ box-shadow 0.1s;
499
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
500
+ }
501
+ [data-theme='light'] .key-btn {
502
+ color: #000;
503
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
504
+ }
505
+ .key-btn:active {
506
+ background: #6e6e72;
507
+ transform: scale(0.95);
508
+ box-shadow: none;
509
+ }
510
+ [data-theme='light'] .key-btn:active {
511
+ background: #c8c8cc;
512
+ }
513
+ .key-btn.flash {
514
+ background: #fff !important;
515
+ color: #000 !important;
516
+ transition: none;
517
+ }
518
+ [data-theme='light'] .key-btn.flash {
519
+ background: #333 !important;
520
+ color: #fff !important;
521
+ }
522
+ .key-btn.modifier,
523
+ .key-btn.special {
524
+ background: var(--key-special-bg);
525
+ font-size: 12px;
526
+ font-weight: 600;
527
+ }
528
+ [data-theme='light'] .key-btn.modifier,
529
+ [data-theme='light'] .key-btn.special {
530
+ background: var(--key-special-bg);
531
+ color: #000;
532
+ }
533
+ .key-btn.modifier.active {
534
+ background: var(--accent);
535
+ color: #fff;
492
536
  box-shadow:
493
- 0 1px 3px var(--key-shadow),
494
- inset 0 1px 0 rgba(255, 255, 255, 0.05);
537
+ 0 1px 0 rgba(0, 0, 0, 0.2),
538
+ 0 0 10px rgba(0, 120, 212, 0.5);
495
539
  }
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;
540
+ [data-theme='light'] .key-btn.modifier.active {
541
+ background: var(--accent);
542
+ color: #fff;
543
+ box-shadow:
544
+ 0 1px 0 rgba(0, 0, 0, 0.2),
545
+ 0 0 10px rgba(0, 120, 212, 0.3);
502
546
  }
503
- .key-btn:hover {
504
- background: var(--border);
505
- border-color: var(--accent);
506
- box-shadow: 0 2px 6px var(--key-shadow);
547
+ .key-btn.icon-btn {
548
+ font-size: 18px;
507
549
  }
508
- .key-btn:active {
550
+ .key-btn.key-enter {
509
551
  background: var(--accent);
510
552
  color: #fff;
511
- border-color: var(--accent);
512
- transform: scale(0.93);
513
- box-shadow: none;
553
+ font-size: 20px;
514
554
  }
515
- .key-btn.wide {
516
- flex: 1.4 1 0;
555
+ .key-btn.key-enter:active {
556
+ background: var(--accent-active);
557
+ }
558
+ .key-btn.key-danger {
559
+ background: #5c2222;
560
+ color: #f87171;
561
+ }
562
+ [data-theme='light'] .key-btn.key-danger {
563
+ background: #fee2e2;
564
+ color: #dc2626;
565
+ }
566
+ .key-btn.key-danger:active {
567
+ background: var(--danger);
568
+ color: #fff;
517
569
  }
518
570
  .key-sep {
519
- width: 1px;
520
- height: 20px;
521
- background: var(--border);
571
+ width: 0;
522
572
  flex-shrink: 0;
523
573
  }
524
574
 
@@ -1122,14 +1172,41 @@
1122
1172
 
1123
1173
  .side-panel-header {
1124
1174
  display: flex;
1125
- align-items: center;
1126
- justify-content: space-between;
1127
- padding: 12px 14px;
1175
+ flex-direction: column;
1176
+ padding: 16px 14px 12px;
1128
1177
  border-bottom: 1px solid var(--border);
1129
- font-size: 15px;
1178
+ position: relative;
1179
+ }
1180
+ .side-panel-brand {
1181
+ display: flex;
1182
+ align-items: center;
1183
+ gap: 8px;
1184
+ font-size: 18px;
1130
1185
  font-weight: 700;
1186
+ letter-spacing: -0.02em;
1187
+ }
1188
+ .side-panel-brand svg {
1189
+ flex-shrink: 0;
1190
+ }
1191
+ .side-panel-version {
1192
+ font-size: 11px;
1193
+ color: var(--text-muted);
1194
+ font-weight: 400;
1195
+ margin-top: 2px;
1196
+ padding-left: 28px;
1197
+ }
1198
+ .side-panel-section-title {
1199
+ font-size: 11px;
1200
+ font-weight: 600;
1201
+ text-transform: uppercase;
1202
+ letter-spacing: 0.06em;
1203
+ color: var(--text-dim);
1204
+ padding: 10px 14px 4px;
1131
1205
  }
1132
1206
  .side-panel-close {
1207
+ position: absolute;
1208
+ top: 14px;
1209
+ right: 10px;
1133
1210
  background: none;
1134
1211
  border: none;
1135
1212
  color: var(--text-dim);
@@ -1152,7 +1229,8 @@
1152
1229
  }
1153
1230
 
1154
1231
  .side-panel-list {
1155
- flex: 1;
1232
+ flex: 1 1 0;
1233
+ min-height: 0;
1156
1234
  overflow-y: auto;
1157
1235
  padding: 8px;
1158
1236
  -webkit-overflow-scrolling: touch;
@@ -1299,9 +1377,26 @@
1299
1377
  <div id="side-panel-backdrop"></div>
1300
1378
  <div id="side-panel">
1301
1379
  <div class="side-panel-header">
1302
- <span>Sessions</span>
1380
+ <div class="side-panel-brand">
1381
+ <svg
1382
+ width="20"
1383
+ height="20"
1384
+ viewBox="0 0 24 24"
1385
+ fill="none"
1386
+ stroke="currentColor"
1387
+ stroke-width="2"
1388
+ stroke-linecap="round"
1389
+ stroke-linejoin="round"
1390
+ >
1391
+ <polyline points="4 17 10 11 4 5"></polyline>
1392
+ <line x1="12" y1="19" x2="20" y2="19"></line>
1393
+ </svg>
1394
+ TermBeam
1395
+ </div>
1396
+ <div class="side-panel-version" id="side-panel-version"></div>
1303
1397
  <button class="side-panel-close" id="side-panel-close" title="Close">×</button>
1304
1398
  </div>
1399
+ <div class="side-panel-section-title">Sessions</div>
1305
1400
  <div class="side-panel-list" id="side-panel-list"></div>
1306
1401
  <div style="padding: 8px; border-top: 1px solid var(--border)">
1307
1402
  <button
@@ -1493,29 +1588,28 @@
1493
1588
  <div id="copy-toast">Copied!</div>
1494
1589
 
1495
1590
  <div id="key-bar">
1496
- <button class="key-btn" data-key="&#x1b;[A" title="Previous command">
1497
- ↑<span class="hint">prev</span>
1498
- </button>
1499
- <button class="key-btn" data-key="&#x1b;[B" title="Next command">
1500
- ↓<span class="hint">next</span>
1501
- </button>
1502
- <button class="key-btn" data-key="&#x1b;[D" title="Left">←</button>
1503
- <button class="key-btn" data-key="&#x1b;[C" title="Right">→</button>
1504
- <button class="key-btn" data-key="&#x1b;[H" title="Home">Home</button>
1505
- <button class="key-btn" data-key="&#x1b;[F" title="End">End</button>
1506
- <div class="key-sep"></div>
1507
- <button class="key-btn" id="select-btn" title="Copy text">Copy</button>
1508
- <button class="key-btn" id="paste-btn" title="Paste from clipboard">Paste</button>
1509
- <div class="key-sep"></div>
1510
- <button class="key-btn wide" data-key="&#x09;" title="Autocomplete">Tab</button>
1511
- <div class="key-sep"></div>
1512
- <button class="key-btn" data-key="&#x03;" title="Interrupt process">
1513
- ^C<span class="hint">stop</span>
1514
- </button>
1515
- <div class="key-sep"></div>
1516
- <button class="key-btn" data-key="enter" title="Enter / Return">
1517
- ↵<span class="hint">enter</span>
1518
- </button>
1591
+ <div class="key-row">
1592
+ <button class="key-btn special" data-key="&#x1b;" title="Escape">Esc</button>
1593
+ <button class="key-btn special" id="select-btn" title="Copy text">Copy</button>
1594
+ <button class="key-btn special" id="paste-btn" title="Paste from clipboard">Paste</button>
1595
+ <button class="key-btn special" data-key="&#x1b;OH" title="Home">Home</button>
1596
+ <button class="key-btn special" data-key="&#x1b;OF" title="End">End</button>
1597
+ <button class="key-btn icon-btn" data-key="&#x1b;[A" title="Up">↑</button>
1598
+ <button class="key-btn icon-btn key-enter" data-key="enter" title="Enter / Return">
1599
+
1600
+ </button>
1601
+ </div>
1602
+ <div class="key-row">
1603
+ <button class="key-btn modifier" id="ctrl-btn" title="Toggle Ctrl modifier">Ctrl</button>
1604
+ <button class="key-btn modifier" id="shift-btn" title="Toggle Shift modifier">Shift</button>
1605
+ <button class="key-btn special" data-key="&#x09;" title="Autocomplete">Tab</button>
1606
+ <button class="key-btn special key-danger" data-key="&#x03;" title="Interrupt process">
1607
+ ^C
1608
+ </button>
1609
+ <button class="key-btn icon-btn" data-key="&#x1b;[D" title="Left">←</button>
1610
+ <button class="key-btn icon-btn" data-key="&#x1b;[B" title="Down">↓</button>
1611
+ <button class="key-btn icon-btn" data-key="&#x1b;[C" title="Right">→</button>
1612
+ </div>
1519
1613
  </div>
1520
1614
 
1521
1615
  <div id="reconnect-overlay">
@@ -2030,9 +2124,9 @@
2030
2124
  const keyboardOpen = keyboardHeight > 50;
2031
2125
  if (keyboardOpen) {
2032
2126
  keyBar.style.bottom = keyboardHeight + 'px';
2033
- keyBar.style.height = '52px';
2127
+ keyBar.style.height = '80px';
2034
2128
  keyBar.style.paddingBottom = '0px';
2035
- terminalsWrapper.style.bottom = 52 + keyboardHeight + 'px';
2129
+ terminalsWrapper.style.bottom = 80 + keyboardHeight + 'px';
2036
2130
  } else {
2037
2131
  keyBar.style.bottom = '0px';
2038
2132
  keyBar.style.height = '';
@@ -2112,6 +2206,7 @@
2112
2206
  .then((r) => r.json())
2113
2207
  .then((d) => {
2114
2208
  document.getElementById('version-text').textContent = 'v' + d.version;
2209
+ document.getElementById('side-panel-version').textContent = 'v' + d.version;
2115
2210
  })
2116
2211
  .catch(() => {});
2117
2212
  }
@@ -2265,7 +2360,18 @@
2265
2360
  // Terminal input → WebSocket
2266
2361
  term.onData((input) => {
2267
2362
  if (ms.ws && ms.ws.readyState === 1) {
2268
- ms.ws.send(JSON.stringify({ type: 'input', data: input }));
2363
+ let data = input;
2364
+ if (ctrlActive && input.length === 1) {
2365
+ const code = input.toLowerCase().charCodeAt(0);
2366
+ // a-z → Ctrl+letter (0x01-0x1a)
2367
+ if (code >= 97 && code <= 122) {
2368
+ data = String.fromCharCode(code - 96);
2369
+ }
2370
+ clearModifiers();
2371
+ } else if (shiftActive && !ctrlActive) {
2372
+ clearModifiers();
2373
+ }
2374
+ ms.ws.send(JSON.stringify({ type: 'input', data }));
2269
2375
  }
2270
2376
  });
2271
2377
 
@@ -2830,18 +2936,65 @@
2830
2936
  }
2831
2937
 
2832
2938
  // ===== Key Bar =====
2939
+ // Modifier state (shared with terminal onData)
2940
+ let ctrlActive = false;
2941
+ let shiftActive = false;
2942
+ function clearModifiers() {
2943
+ ctrlActive = false;
2944
+ shiftActive = false;
2945
+ const ctrlBtn = document.getElementById('ctrl-btn');
2946
+ const shiftBtn = document.getElementById('shift-btn');
2947
+ if (ctrlBtn) ctrlBtn.classList.remove('active');
2948
+ if (shiftBtn) shiftBtn.classList.remove('active');
2949
+ }
2950
+
2833
2951
  function setupKeyBar() {
2834
2952
  const keyBar = document.getElementById('key-bar');
2953
+ const ctrlBtn = document.getElementById('ctrl-btn');
2954
+ const shiftBtn = document.getElementById('shift-btn');
2835
2955
  let repeatTimer = null;
2836
2956
  let repeatInterval = null;
2837
2957
 
2958
+ function toggleModifier(which) {
2959
+ if (which === 'ctrl') {
2960
+ ctrlActive = !ctrlActive;
2961
+ ctrlBtn.classList.toggle('active', ctrlActive);
2962
+ } else {
2963
+ shiftActive = !shiftActive;
2964
+ shiftBtn.classList.toggle('active', shiftActive);
2965
+ }
2966
+ }
2967
+
2968
+ function applyModifiers(key) {
2969
+ if (!ctrlActive && !shiftActive) return key;
2970
+ // Modifier param: Shift=2, Ctrl=5, Ctrl+Shift=6
2971
+ const mod = ctrlActive && shiftActive ? 6 : ctrlActive ? 5 : 2;
2972
+ // Arrow keys: \x1b[X → \x1b[1;{mod}X
2973
+ const csiMatch = key.match(/^\x1b\[([ABCD])$/);
2974
+ if (csiMatch) return '\x1b[1;' + mod + csiMatch[1];
2975
+ // Home/End: \x1bOH/\x1bOF → \x1b[1;{mod}H/F
2976
+ const ssMatch = key.match(/^\x1bO([HF])$/);
2977
+ if (ssMatch) return '\x1b[1;' + mod + ssMatch[1];
2978
+ // Tab with Shift → reverse tab
2979
+ if (key === '\x09' && shiftActive && !ctrlActive) return '\x1b[Z';
2980
+ return key;
2981
+ }
2982
+
2983
+ function flashBtn(btn) {
2984
+ btn.classList.add('flash');
2985
+ setTimeout(() => btn.classList.remove('flash'), 120);
2986
+ }
2987
+
2838
2988
  function sendKey(btn) {
2839
2989
  if (!btn || !btn.dataset.key) return;
2840
- const data = btn.dataset.key === 'enter' ? '\r' : btn.dataset.key;
2990
+ flashBtn(btn);
2991
+ let data = btn.dataset.key === 'enter' ? '\r' : btn.dataset.key;
2992
+ data = applyModifiers(data);
2841
2993
  const ms = managed.get(activeId);
2842
2994
  if (ms && ms.ws && ms.ws.readyState === 1) {
2843
2995
  ms.ws.send(JSON.stringify({ type: 'input', data }));
2844
2996
  }
2997
+ clearModifiers();
2845
2998
  }
2846
2999
 
2847
3000
  function stopRepeat() {
@@ -2859,6 +3012,37 @@
2859
3012
  }, 400);
2860
3013
  }
2861
3014
 
3015
+ ctrlBtn.addEventListener('click', (e) => {
3016
+ e.preventDefault();
3017
+ e.stopPropagation();
3018
+ toggleModifier('ctrl');
3019
+ });
3020
+ shiftBtn.addEventListener('click', (e) => {
3021
+ e.preventDefault();
3022
+ e.stopPropagation();
3023
+ toggleModifier('shift');
3024
+ });
3025
+ ctrlBtn.addEventListener('mousedown', (e) => e.preventDefault());
3026
+ shiftBtn.addEventListener('mousedown', (e) => e.preventDefault());
3027
+ ctrlBtn.addEventListener(
3028
+ 'touchstart',
3029
+ (e) => {
3030
+ e.preventDefault();
3031
+ keyBarTouched = true;
3032
+ toggleModifier('ctrl');
3033
+ },
3034
+ { passive: false },
3035
+ );
3036
+ shiftBtn.addEventListener(
3037
+ 'touchstart',
3038
+ (e) => {
3039
+ e.preventDefault();
3040
+ keyBarTouched = true;
3041
+ toggleModifier('shift');
3042
+ },
3043
+ { passive: false },
3044
+ );
3045
+
2862
3046
  let keyBarTouched = false;
2863
3047
  keyBar.addEventListener('mousedown', (e) => {
2864
3048
  if (keyBarTouched) {
@@ -2874,24 +3058,45 @@
2874
3058
  keyBar.addEventListener('mouseup', stopRepeat);
2875
3059
  keyBar.addEventListener('mouseleave', stopRepeat);
2876
3060
 
3061
+ // Touch handling: allow native scroll when swiping, fire key only on tap
3062
+ const SWIPE_THRESHOLD = 10;
3063
+ let touchStartX = 0;
3064
+ let touchBtn = null;
3065
+ let touchMoved = false;
3066
+
2877
3067
  keyBar.addEventListener(
2878
3068
  'touchstart',
2879
3069
  (e) => {
2880
3070
  keyBarTouched = true;
2881
- const btn = e.target.closest('.key-btn');
2882
- if (btn && btn.dataset.key) {
2883
- e.preventDefault();
2884
- startRepeat(btn);
3071
+ touchBtn = e.target.closest('.key-btn');
3072
+ touchMoved = false;
3073
+ touchStartX = e.touches[0].clientX;
3074
+ // Don't preventDefault — let the browser handle scroll
3075
+ },
3076
+ { passive: true },
3077
+ );
3078
+ keyBar.addEventListener(
3079
+ 'touchmove',
3080
+ (e) => {
3081
+ if (Math.abs(e.touches[0].clientX - touchStartX) > SWIPE_THRESHOLD) {
3082
+ touchMoved = true;
3083
+ stopRepeat();
2885
3084
  }
2886
3085
  },
2887
- { passive: false },
3086
+ { passive: true },
2888
3087
  );
2889
3088
  keyBar.addEventListener('touchend', (e) => {
2890
- const btn = e.target.closest('.key-btn');
2891
- if (btn && btn.dataset.key) e.preventDefault();
3089
+ if (!touchMoved && touchBtn && touchBtn.dataset.key) {
3090
+ e.preventDefault();
3091
+ sendKey(touchBtn);
3092
+ }
2892
3093
  stopRepeat();
3094
+ touchBtn = null;
3095
+ });
3096
+ keyBar.addEventListener('touchcancel', () => {
3097
+ stopRepeat();
3098
+ touchBtn = null;
2893
3099
  });
2894
- keyBar.addEventListener('touchcancel', stopRepeat);
2895
3100
 
2896
3101
  keyBar.addEventListener('click', (e) => {
2897
3102
  const btn = e.target.closest('.key-btn');
@@ -3059,7 +3264,21 @@
3059
3264
  });
3060
3265
 
3061
3266
  selectBtn.addEventListener('mousedown', (e) => e.preventDefault());
3062
- selectBtn.addEventListener('click', openSelectOverlay);
3267
+ selectBtn.addEventListener(
3268
+ 'touchend',
3269
+ (e) => {
3270
+ e.preventDefault();
3271
+ const ms = managed.get(activeId);
3272
+ if (ms) ms.term.blur();
3273
+ openSelectOverlay();
3274
+ },
3275
+ { passive: false },
3276
+ );
3277
+ selectBtn.addEventListener('click', () => {
3278
+ const ms = managed.get(activeId);
3279
+ if (ms) ms.term.blur();
3280
+ openSelectOverlay();
3281
+ });
3063
3282
 
3064
3283
  document.getElementById('select-copy').addEventListener('click', () => {
3065
3284
  // Copy finger selection if any, otherwise copy all loaded text