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.
- package/package.json +1 -1
- package/public/terminal.html +309 -90
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);
|
|
@@ -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
|
-
<
|
|
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
|
-
<
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
<
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
</button>
|
|
1591
|
+
<div class="key-row">
|
|
1592
|
+
<button class="key-btn special" data-key="" 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="OH" title="Home">Home</button>
|
|
1596
|
+
<button class="key-btn special" data-key="OF" title="End">End</button>
|
|
1597
|
+
<button class="key-btn icon-btn" data-key="[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="	" title="Autocomplete">Tab</button>
|
|
1606
|
+
<button class="key-btn special key-danger" data-key="" title="Interrupt process">
|
|
1607
|
+
^C
|
|
1608
|
+
</button>
|
|
1609
|
+
<button class="key-btn icon-btn" data-key="[D" title="Left">←</button>
|
|
1610
|
+
<button class="key-btn icon-btn" data-key="[B" title="Down">↓</button>
|
|
1611
|
+
<button class="key-btn icon-btn" data-key="[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 = '
|
|
2127
|
+
keyBar.style.height = '80px';
|
|
2034
2128
|
keyBar.style.paddingBottom = '0px';
|
|
2035
|
-
terminalsWrapper.style.bottom =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
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:
|
|
3086
|
+
{ passive: true },
|
|
2888
3087
|
);
|
|
2889
3088
|
keyBar.addEventListener('touchend', (e) => {
|
|
2890
|
-
|
|
2891
|
-
|
|
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(
|
|
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
|