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.
- package/package.json +1 -1
- package/public/terminal.html +307 -89
package/package.json
CHANGED
package/public/terminal.html
CHANGED
|
@@ -36,9 +36,10 @@
|
|
|
36
36
|
--danger: #f14c4c;
|
|
37
37
|
--danger-hover: #d73a3a;
|
|
38
38
|
--success: #89d185;
|
|
39
|
-
--key-bg: #
|
|
40
|
-
--key-border: #
|
|
41
|
-
--key-shadow: rgba(0, 0, 0, 0.
|
|
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: #
|
|
60
|
-
--key-border: #
|
|
61
|
-
--key-shadow: rgba(0, 0, 0, 0.
|
|
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(
|
|
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(
|
|
454
|
+
height: calc(80px + env(safe-area-inset-bottom, 0px));
|
|
453
455
|
display: flex;
|
|
454
|
-
|
|
455
|
-
background:
|
|
456
|
+
flex-direction: column;
|
|
457
|
+
background: #1c1c1e;
|
|
456
458
|
border-top: 1px solid var(--border);
|
|
457
|
-
padding:
|
|
458
|
-
calc(
|
|
459
|
-
gap:
|
|
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:
|
|
478
|
+
height: 34px;
|
|
468
479
|
background: var(--key-bg);
|
|
469
|
-
color:
|
|
470
|
-
border:
|
|
471
|
-
border-radius:
|
|
472
|
-
font-size:
|
|
473
|
-
font-weight:
|
|
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:
|
|
492
|
+
padding: 0 6px;
|
|
483
493
|
flex: 1 1 0;
|
|
484
|
-
gap: 0;
|
|
485
494
|
line-height: 1;
|
|
486
495
|
transition:
|
|
487
|
-
background 0.
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
|
494
|
-
|
|
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
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
|
504
|
-
|
|
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
|
|
550
|
+
.key-btn.key-enter {
|
|
509
551
|
background: var(--accent);
|
|
510
552
|
color: #fff;
|
|
511
|
-
|
|
512
|
-
transform: scale(0.93);
|
|
513
|
-
box-shadow: none;
|
|
553
|
+
font-size: 20px;
|
|
514
554
|
}
|
|
515
|
-
.key-btn.
|
|
516
|
-
|
|
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:
|
|
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
|
-
|
|
1126
|
-
|
|
1127
|
-
padding: 12px 14px;
|
|
1175
|
+
flex-direction: column;
|
|
1176
|
+
padding: 16px 14px 12px;
|
|
1128
1177
|
border-bottom: 1px solid var(--border);
|
|
1129
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
<
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
</button>
|
|
1590
|
+
<div class="key-row">
|
|
1591
|
+
<button class="key-btn special" data-key="" 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="OH" title="Home">Home</button>
|
|
1595
|
+
<button class="key-btn special" data-key="OF" title="End">End</button>
|
|
1596
|
+
<button class="key-btn icon-btn" data-key="[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="	" title="Autocomplete">Tab</button>
|
|
1605
|
+
<button class="key-btn special key-danger" data-key="" title="Interrupt process">
|
|
1606
|
+
^C
|
|
1607
|
+
</button>
|
|
1608
|
+
<button class="key-btn icon-btn" data-key="[D" title="Left">←</button>
|
|
1609
|
+
<button class="key-btn icon-btn" data-key="[B" title="Down">↓</button>
|
|
1610
|
+
<button class="key-btn icon-btn" data-key="[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 = '
|
|
2126
|
+
keyBar.style.height = '80px';
|
|
2034
2127
|
keyBar.style.paddingBottom = '0px';
|
|
2035
|
-
terminalsWrapper.style.bottom =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
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:
|
|
3085
|
+
{ passive: true },
|
|
2888
3086
|
);
|
|
2889
3087
|
keyBar.addEventListener('touchend', (e) => {
|
|
2890
|
-
|
|
2891
|
-
|
|
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(
|
|
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
|