termbeam 1.2.0 → 1.2.1

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 +307 -89
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
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);
@@ -1299,9 +1376,26 @@
1299
1376
  <div id="side-panel-backdrop"></div>
1300
1377
  <div id="side-panel">
1301
1378
  <div class="side-panel-header">
1302
- <span>Sessions</span>
1379
+ <div class="side-panel-brand">
1380
+ <svg
1381
+ width="20"
1382
+ height="20"
1383
+ viewBox="0 0 24 24"
1384
+ fill="none"
1385
+ stroke="currentColor"
1386
+ stroke-width="2"
1387
+ stroke-linecap="round"
1388
+ stroke-linejoin="round"
1389
+ >
1390
+ <polyline points="4 17 10 11 4 5"></polyline>
1391
+ <line x1="12" y1="19" x2="20" y2="19"></line>
1392
+ </svg>
1393
+ TermBeam
1394
+ </div>
1395
+ <div class="side-panel-version" id="side-panel-version"></div>
1303
1396
  <button class="side-panel-close" id="side-panel-close" title="Close">×</button>
1304
1397
  </div>
1398
+ <div class="side-panel-section-title">Sessions</div>
1305
1399
  <div class="side-panel-list" id="side-panel-list"></div>
1306
1400
  <div style="padding: 8px; border-top: 1px solid var(--border)">
1307
1401
  <button
@@ -1493,29 +1587,28 @@
1493
1587
  <div id="copy-toast">Copied!</div>
1494
1588
 
1495
1589
  <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>
1590
+ <div class="key-row">
1591
+ <button class="key-btn special" data-key="&#x1b;" title="Escape">Esc</button>
1592
+ <button class="key-btn special" id="select-btn" title="Copy text">Copy</button>
1593
+ <button class="key-btn special" id="paste-btn" title="Paste from clipboard">Paste</button>
1594
+ <button class="key-btn special" data-key="&#x1b;OH" title="Home">Home</button>
1595
+ <button class="key-btn special" data-key="&#x1b;OF" title="End">End</button>
1596
+ <button class="key-btn icon-btn" data-key="&#x1b;[A" title="Up">↑</button>
1597
+ <button class="key-btn icon-btn key-enter" data-key="enter" title="Enter / Return">
1598
+
1599
+ </button>
1600
+ </div>
1601
+ <div class="key-row">
1602
+ <button class="key-btn modifier" id="ctrl-btn" title="Toggle Ctrl modifier">Ctrl</button>
1603
+ <button class="key-btn modifier" id="shift-btn" title="Toggle Shift modifier">Shift</button>
1604
+ <button class="key-btn special" data-key="&#x09;" title="Autocomplete">Tab</button>
1605
+ <button class="key-btn special key-danger" data-key="&#x03;" title="Interrupt process">
1606
+ ^C
1607
+ </button>
1608
+ <button class="key-btn icon-btn" data-key="&#x1b;[D" title="Left">←</button>
1609
+ <button class="key-btn icon-btn" data-key="&#x1b;[B" title="Down">↓</button>
1610
+ <button class="key-btn icon-btn" data-key="&#x1b;[C" title="Right">→</button>
1611
+ </div>
1519
1612
  </div>
1520
1613
 
1521
1614
  <div id="reconnect-overlay">
@@ -2030,9 +2123,9 @@
2030
2123
  const keyboardOpen = keyboardHeight > 50;
2031
2124
  if (keyboardOpen) {
2032
2125
  keyBar.style.bottom = keyboardHeight + 'px';
2033
- keyBar.style.height = '52px';
2126
+ keyBar.style.height = '80px';
2034
2127
  keyBar.style.paddingBottom = '0px';
2035
- terminalsWrapper.style.bottom = 52 + keyboardHeight + 'px';
2128
+ terminalsWrapper.style.bottom = 80 + keyboardHeight + 'px';
2036
2129
  } else {
2037
2130
  keyBar.style.bottom = '0px';
2038
2131
  keyBar.style.height = '';
@@ -2112,6 +2205,7 @@
2112
2205
  .then((r) => r.json())
2113
2206
  .then((d) => {
2114
2207
  document.getElementById('version-text').textContent = 'v' + d.version;
2208
+ document.getElementById('side-panel-version').textContent = 'v' + d.version;
2115
2209
  })
2116
2210
  .catch(() => {});
2117
2211
  }
@@ -2265,7 +2359,18 @@
2265
2359
  // Terminal input → WebSocket
2266
2360
  term.onData((input) => {
2267
2361
  if (ms.ws && ms.ws.readyState === 1) {
2268
- ms.ws.send(JSON.stringify({ type: 'input', data: input }));
2362
+ let data = input;
2363
+ if (ctrlActive && input.length === 1) {
2364
+ const code = input.toLowerCase().charCodeAt(0);
2365
+ // a-z → Ctrl+letter (0x01-0x1a)
2366
+ if (code >= 97 && code <= 122) {
2367
+ data = String.fromCharCode(code - 96);
2368
+ }
2369
+ clearModifiers();
2370
+ } else if (shiftActive && !ctrlActive) {
2371
+ clearModifiers();
2372
+ }
2373
+ ms.ws.send(JSON.stringify({ type: 'input', data }));
2269
2374
  }
2270
2375
  });
2271
2376
 
@@ -2830,18 +2935,65 @@
2830
2935
  }
2831
2936
 
2832
2937
  // ===== Key Bar =====
2938
+ // Modifier state (shared with terminal onData)
2939
+ let ctrlActive = false;
2940
+ let shiftActive = false;
2941
+ function clearModifiers() {
2942
+ ctrlActive = false;
2943
+ shiftActive = false;
2944
+ const ctrlBtn = document.getElementById('ctrl-btn');
2945
+ const shiftBtn = document.getElementById('shift-btn');
2946
+ if (ctrlBtn) ctrlBtn.classList.remove('active');
2947
+ if (shiftBtn) shiftBtn.classList.remove('active');
2948
+ }
2949
+
2833
2950
  function setupKeyBar() {
2834
2951
  const keyBar = document.getElementById('key-bar');
2952
+ const ctrlBtn = document.getElementById('ctrl-btn');
2953
+ const shiftBtn = document.getElementById('shift-btn');
2835
2954
  let repeatTimer = null;
2836
2955
  let repeatInterval = null;
2837
2956
 
2957
+ function toggleModifier(which) {
2958
+ if (which === 'ctrl') {
2959
+ ctrlActive = !ctrlActive;
2960
+ ctrlBtn.classList.toggle('active', ctrlActive);
2961
+ } else {
2962
+ shiftActive = !shiftActive;
2963
+ shiftBtn.classList.toggle('active', shiftActive);
2964
+ }
2965
+ }
2966
+
2967
+ function applyModifiers(key) {
2968
+ if (!ctrlActive && !shiftActive) return key;
2969
+ // Modifier param: Shift=2, Ctrl=5, Ctrl+Shift=6
2970
+ const mod = ctrlActive && shiftActive ? 6 : ctrlActive ? 5 : 2;
2971
+ // Arrow keys: \x1b[X → \x1b[1;{mod}X
2972
+ const csiMatch = key.match(/^\x1b\[([ABCD])$/);
2973
+ if (csiMatch) return '\x1b[1;' + mod + csiMatch[1];
2974
+ // Home/End: \x1bOH/\x1bOF → \x1b[1;{mod}H/F
2975
+ const ssMatch = key.match(/^\x1bO([HF])$/);
2976
+ if (ssMatch) return '\x1b[1;' + mod + ssMatch[1];
2977
+ // Tab with Shift → reverse tab
2978
+ if (key === '\x09' && shiftActive && !ctrlActive) return '\x1b[Z';
2979
+ return key;
2980
+ }
2981
+
2982
+ function flashBtn(btn) {
2983
+ btn.classList.add('flash');
2984
+ setTimeout(() => btn.classList.remove('flash'), 120);
2985
+ }
2986
+
2838
2987
  function sendKey(btn) {
2839
2988
  if (!btn || !btn.dataset.key) return;
2840
- const data = btn.dataset.key === 'enter' ? '\r' : btn.dataset.key;
2989
+ flashBtn(btn);
2990
+ let data = btn.dataset.key === 'enter' ? '\r' : btn.dataset.key;
2991
+ data = applyModifiers(data);
2841
2992
  const ms = managed.get(activeId);
2842
2993
  if (ms && ms.ws && ms.ws.readyState === 1) {
2843
2994
  ms.ws.send(JSON.stringify({ type: 'input', data }));
2844
2995
  }
2996
+ clearModifiers();
2845
2997
  }
2846
2998
 
2847
2999
  function stopRepeat() {
@@ -2859,6 +3011,37 @@
2859
3011
  }, 400);
2860
3012
  }
2861
3013
 
3014
+ ctrlBtn.addEventListener('click', (e) => {
3015
+ e.preventDefault();
3016
+ e.stopPropagation();
3017
+ toggleModifier('ctrl');
3018
+ });
3019
+ shiftBtn.addEventListener('click', (e) => {
3020
+ e.preventDefault();
3021
+ e.stopPropagation();
3022
+ toggleModifier('shift');
3023
+ });
3024
+ ctrlBtn.addEventListener('mousedown', (e) => e.preventDefault());
3025
+ shiftBtn.addEventListener('mousedown', (e) => e.preventDefault());
3026
+ ctrlBtn.addEventListener(
3027
+ 'touchstart',
3028
+ (e) => {
3029
+ e.preventDefault();
3030
+ keyBarTouched = true;
3031
+ toggleModifier('ctrl');
3032
+ },
3033
+ { passive: false },
3034
+ );
3035
+ shiftBtn.addEventListener(
3036
+ 'touchstart',
3037
+ (e) => {
3038
+ e.preventDefault();
3039
+ keyBarTouched = true;
3040
+ toggleModifier('shift');
3041
+ },
3042
+ { passive: false },
3043
+ );
3044
+
2862
3045
  let keyBarTouched = false;
2863
3046
  keyBar.addEventListener('mousedown', (e) => {
2864
3047
  if (keyBarTouched) {
@@ -2874,24 +3057,45 @@
2874
3057
  keyBar.addEventListener('mouseup', stopRepeat);
2875
3058
  keyBar.addEventListener('mouseleave', stopRepeat);
2876
3059
 
3060
+ // Touch handling: allow native scroll when swiping, fire key only on tap
3061
+ const SWIPE_THRESHOLD = 10;
3062
+ let touchStartX = 0;
3063
+ let touchBtn = null;
3064
+ let touchMoved = false;
3065
+
2877
3066
  keyBar.addEventListener(
2878
3067
  'touchstart',
2879
3068
  (e) => {
2880
3069
  keyBarTouched = true;
2881
- const btn = e.target.closest('.key-btn');
2882
- if (btn && btn.dataset.key) {
2883
- e.preventDefault();
2884
- startRepeat(btn);
3070
+ touchBtn = e.target.closest('.key-btn');
3071
+ touchMoved = false;
3072
+ touchStartX = e.touches[0].clientX;
3073
+ // Don't preventDefault — let the browser handle scroll
3074
+ },
3075
+ { passive: true },
3076
+ );
3077
+ keyBar.addEventListener(
3078
+ 'touchmove',
3079
+ (e) => {
3080
+ if (Math.abs(e.touches[0].clientX - touchStartX) > SWIPE_THRESHOLD) {
3081
+ touchMoved = true;
3082
+ stopRepeat();
2885
3083
  }
2886
3084
  },
2887
- { passive: false },
3085
+ { passive: true },
2888
3086
  );
2889
3087
  keyBar.addEventListener('touchend', (e) => {
2890
- const btn = e.target.closest('.key-btn');
2891
- if (btn && btn.dataset.key) e.preventDefault();
3088
+ if (!touchMoved && touchBtn && touchBtn.dataset.key) {
3089
+ e.preventDefault();
3090
+ sendKey(touchBtn);
3091
+ }
3092
+ stopRepeat();
3093
+ touchBtn = null;
3094
+ });
3095
+ keyBar.addEventListener('touchcancel', () => {
2892
3096
  stopRepeat();
3097
+ touchBtn = null;
2893
3098
  });
2894
- keyBar.addEventListener('touchcancel', stopRepeat);
2895
3099
 
2896
3100
  keyBar.addEventListener('click', (e) => {
2897
3101
  const btn = e.target.closest('.key-btn');
@@ -3059,7 +3263,21 @@
3059
3263
  });
3060
3264
 
3061
3265
  selectBtn.addEventListener('mousedown', (e) => e.preventDefault());
3062
- selectBtn.addEventListener('click', openSelectOverlay);
3266
+ selectBtn.addEventListener(
3267
+ 'touchend',
3268
+ (e) => {
3269
+ e.preventDefault();
3270
+ const ms = managed.get(activeId);
3271
+ if (ms) ms.term.blur();
3272
+ openSelectOverlay();
3273
+ },
3274
+ { passive: false },
3275
+ );
3276
+ selectBtn.addEventListener('click', () => {
3277
+ const ms = managed.get(activeId);
3278
+ if (ms) ms.term.blur();
3279
+ openSelectOverlay();
3280
+ });
3063
3281
 
3064
3282
  document.getElementById('select-copy').addEventListener('click', () => {
3065
3283
  // Copy finger selection if any, otherwise copy all loaded text