myrlin-workbook 0.9.31 → 0.9.32

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myrlin-workbook",
3
- "version": "0.9.31",
3
+ "version": "0.9.32",
4
4
  "description": "Browser-based project manager for Claude Code sessions - session discovery, multi-terminal, cost tracking, docs, and kanban board",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -240,6 +240,7 @@ class CWMApp {
240
240
  logoutBtn: document.getElementById('logout-btn'),
241
241
  themeToggleBtn: document.getElementById('theme-toggle-btn'),
242
242
  themeDropdown: document.getElementById('theme-dropdown'),
243
+ vkbToggleBtn: document.getElementById('vkb-toggle-btn'),
243
244
  scaleDownBtn: document.getElementById('scale-down-btn'),
244
245
  scaleUpBtn: document.getElementById('scale-up-btn'),
245
246
 
@@ -559,6 +560,43 @@ class CWMApp {
559
560
  });
560
561
  }
561
562
 
563
+ // Virtual keyboard toggle. inputmode="none" is a no-op on devices without
564
+ // a soft keyboard, so we apply it unconditionally instead of trying to
565
+ // detect "is mobile" (which is unreliable across browsers).
566
+ this._vkbDisabled = localStorage.getItem('cwm_vkb_disabled') === '1';
567
+ this._applyVkbState();
568
+ if (this.els.vkbToggleBtn) {
569
+ this.els.vkbToggleBtn.addEventListener('click', () => {
570
+ this._vkbDisabled = !this._vkbDisabled;
571
+ localStorage.setItem('cwm_vkb_disabled', this._vkbDisabled ? '1' : '0');
572
+ this._applyVkbState();
573
+ });
574
+ // Watch for newly-created xterm helper textareas and apply state to them.
575
+ const obs = new MutationObserver((muts) => {
576
+ if (!this._vkbDisabled) return;
577
+ for (const m of muts) {
578
+ for (const n of m.addedNodes) {
579
+ if (n.nodeType !== 1) continue;
580
+ if (n.matches && n.matches('.xterm-helper-textarea')) {
581
+ n.setAttribute('inputmode', 'none');
582
+ } else if (n.querySelectorAll) {
583
+ n.querySelectorAll('.xterm-helper-textarea').forEach(t => t.setAttribute('inputmode', 'none'));
584
+ }
585
+ }
586
+ }
587
+ });
588
+ obs.observe(document.body, { childList: true, subtree: true });
589
+ }
590
+
591
+ // While any HTML5 drag is active, mark <body> so the mobile sidebar
592
+ // backdrop becomes pointer-events: none. Without this, on mobile the
593
+ // backdrop occludes terminal panes for elementFromPoint, so the
594
+ // DragDropTouch polyfill never fires dragover/drop on the pane behind it.
595
+ document.addEventListener('dragstart', () => document.body.classList.add('cwm-dragging'), true);
596
+ const clearDragging = () => document.body.classList.remove('cwm-dragging');
597
+ document.addEventListener('dragend', clearDragging, true);
598
+ document.addEventListener('drop', clearDragging, true);
599
+
562
600
  // Sidebar toggle (mobile)
563
601
  this.els.sidebarToggle.addEventListener('click', () => this.toggleSidebar());
564
602
 
@@ -3533,6 +3571,22 @@ class CWMApp {
3533
3571
  });
3534
3572
  }
3535
3573
 
3574
+ _applyVkbState() {
3575
+ const btn = this.els && this.els.vkbToggleBtn;
3576
+ const active = !!this._vkbDisabled;
3577
+ if (btn) {
3578
+ btn.classList.toggle('active', active);
3579
+ btn.setAttribute('aria-pressed', active ? 'true' : 'false');
3580
+ btn.title = active
3581
+ ? 'On-screen keyboard disabled (click to enable)'
3582
+ : 'Disable on-screen keyboard (hardware keyboard only)';
3583
+ }
3584
+ document.querySelectorAll('.xterm-helper-textarea').forEach(t => {
3585
+ if (active) t.setAttribute('inputmode', 'none');
3586
+ else t.removeAttribute('inputmode');
3587
+ });
3588
+ }
3589
+
3536
3590
  // Legacy alias for any remaining callers
3537
3591
  toggleTheme() {
3538
3592
  const current = document.documentElement.dataset.theme || 'mocha';
@@ -5037,7 +5091,7 @@ class CWMApp {
5037
5091
  panel._wsId = ws.id;
5038
5092
 
5039
5093
  panel.replaceChildren();
5040
- const container = document.createElement('div');
5094
+ container = document.createElement('div');
5041
5095
  container.className = 'files-container';
5042
5096
 
5043
5097
  const sidebar = document.createElement('div');
@@ -5379,7 +5433,7 @@ class CWMApp {
5379
5433
 
5380
5434
  panel.textContent = '';
5381
5435
 
5382
- const container = document.createElement('div');
5436
+ container = document.createElement('div');
5383
5437
  container.className = 'git-panel-container';
5384
5438
 
5385
5439
  const left = document.createElement('div');
@@ -7519,15 +7573,15 @@ class CWMApp {
7519
7573
  let startX = 0;
7520
7574
  let startWidth = 0;
7521
7575
 
7522
- const onMouseMove = (e) => {
7576
+ const onMove = (clientX) => {
7523
7577
  if (!isResizing) return;
7524
- const dx = e.clientX - startX;
7578
+ const dx = clientX - startX;
7525
7579
  const newWidth = Math.max(180, Math.min(600, startWidth + dx));
7526
7580
  sidebar.style.width = newWidth + 'px';
7527
7581
  sidebar.style.transition = 'none'; // disable transition during drag
7528
7582
  };
7529
7583
 
7530
- const onMouseUp = () => {
7584
+ const onEnd = () => {
7531
7585
  if (!isResizing) return;
7532
7586
  isResizing = false;
7533
7587
  handle.classList.remove('active');
@@ -7547,24 +7601,32 @@ class CWMApp {
7547
7601
  });
7548
7602
 
7549
7603
  document.removeEventListener('mousemove', onMouseMove);
7550
- document.removeEventListener('mouseup', onMouseUp);
7604
+ document.removeEventListener('mouseup', onEnd);
7605
+ document.removeEventListener('touchmove', onTouchMove);
7606
+ document.removeEventListener('touchend', onEnd);
7607
+ document.removeEventListener('touchcancel', onEnd);
7551
7608
  };
7552
7609
 
7553
- handle.addEventListener('mousedown', (e) => {
7554
- // Don't resize if sidebar is collapsed
7555
- if (sidebar.classList.contains('collapsed')) return;
7610
+ const onMouseMove = (e) => onMove(e.clientX);
7611
+ const onTouchMove = (e) => { e.preventDefault(); onMove(e.touches[0].clientX); };
7556
7612
 
7557
- e.preventDefault();
7613
+ const startResize = (clientX) => {
7614
+ if (sidebar.classList.contains('collapsed')) return;
7558
7615
  isResizing = true;
7559
- startX = e.clientX;
7616
+ startX = clientX;
7560
7617
  startWidth = sidebar.getBoundingClientRect().width;
7561
7618
  handle.classList.add('active');
7562
7619
  document.body.style.cursor = 'col-resize';
7563
7620
  document.body.style.userSelect = 'none';
7564
-
7565
7621
  document.addEventListener('mousemove', onMouseMove);
7566
- document.addEventListener('mouseup', onMouseUp);
7567
- });
7622
+ document.addEventListener('mouseup', onEnd);
7623
+ document.addEventListener('touchmove', onTouchMove, { passive: false });
7624
+ document.addEventListener('touchend', onEnd);
7625
+ document.addEventListener('touchcancel', onEnd);
7626
+ };
7627
+
7628
+ handle.addEventListener('mousedown', (e) => { e.preventDefault(); startResize(e.clientX); });
7629
+ handle.addEventListener('touchstart', (e) => { e.preventDefault(); startResize(e.touches[0].clientX); }, { passive: false });
7568
7630
  }
7569
7631
 
7570
7632
  initSidebarSectionResize() {
@@ -9938,9 +10000,11 @@ class CWMApp {
9938
10000
  pane.classList.remove('drag-over');
9939
10001
  console.log('[DnD] Drop on pane', slotIdx, 'types:', Array.from(e.dataTransfer.types));
9940
10002
 
9941
- // Terminal pane swap/reposition - drag a pane header onto another pane
10003
+ // Terminal pane swap/reposition - drag a pane header onto another pane.
10004
+ // Use truthy check: native DataTransfer.getData returns '' for missing
10005
+ // keys, but the touch polyfill returns undefined — both must skip.
9942
10006
  const swapSource = e.dataTransfer.getData('cwm/terminal-swap');
9943
- if (swapSource !== '') {
10007
+ if (swapSource) {
9944
10008
  const srcSlot = parseInt(swapSource, 10);
9945
10009
  if (srcSlot !== slotIdx) {
9946
10010
  this.swapTerminalPanes(srcSlot, slotIdx);
@@ -11418,9 +11482,12 @@ class CWMApp {
11418
11482
  grid.addEventListener('touchmove', (e) => {
11419
11483
  // Only intercept when terminal is the active view on mobile
11420
11484
  if (!document.body.classList.contains('terminal-active')) return;
11421
- // Allow the touch event for xterm's internal scroll handling,
11422
- // but stop it from propagating to the page/body scroll.
11423
- e.stopPropagation();
11485
+ // Scope this to the xterm viewport only otherwise we eat touchmoves on
11486
+ // pane headers / resize handles and break things like the
11487
+ // DragDropTouch polyfill (which listens on document in bubble phase).
11488
+ // We don't stop propagation here anymore to allow index.html's hack
11489
+ // preventing the polyfill from breaking xterm's native scroll on desktop touch.
11490
+ // (The polyfill needs to be suppressed, index.html does it on document level).
11424
11491
  }, { passive: true });
11425
11492
  }
11426
11493
 
@@ -11429,7 +11496,8 @@ class CWMApp {
11429
11496
  * Only active on mobile. Scoped to terminal-grid to avoid sidebar conflicts.
11430
11497
  */
11431
11498
  initTerminalPaneSwipe() {
11432
- if (window.innerWidth > 768) return;
11499
+ // Enable touch pane swipe on any touch-capable device, not just phones.
11500
+ if (!('ontouchstart' in window) && navigator.maxTouchPoints === 0) return;
11433
11501
 
11434
11502
  const grid = this.els.terminalGrid;
11435
11503
  if (!grid) return;
@@ -11487,50 +11555,73 @@ class CWMApp {
11487
11555
  }
11488
11556
 
11489
11557
  _setupResizeDrag(handle, direction) {
11490
- handle.addEventListener('mousedown', (e) => {
11491
- e.preventDefault();
11492
- e.stopPropagation();
11493
-
11558
+ const start = (clientX, clientY, isTouch) => {
11494
11559
  const grid = this.els.terminalGrid;
11495
11560
  const gridRect = grid.getBoundingClientRect();
11496
11561
 
11497
- // Create a full-screen overlay to capture mouse events during drag
11498
- const overlay = document.createElement('div');
11499
- overlay.style.cssText = `position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;cursor:${direction === 'col' ? 'col-resize' : 'row-resize'};`;
11500
- document.body.appendChild(overlay);
11562
+ // Mouse drags use a full-screen overlay to keep the resize cursor and
11563
+ // capture stray mouse events. Touch drags don't need it (no cursor;
11564
+ // touch tracking persists outside the handle naturally).
11565
+ let overlay = null;
11566
+ if (!isTouch) {
11567
+ overlay = document.createElement('div');
11568
+ overlay.style.cssText = `position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;cursor:${direction === 'col' ? 'col-resize' : 'row-resize'};`;
11569
+ document.body.appendChild(overlay);
11570
+ }
11501
11571
 
11502
11572
  handle.classList.add('active');
11503
11573
 
11504
- const onMove = (e) => {
11574
+ const move = (cx, cy) => {
11505
11575
  if (direction === 'col') {
11506
- const ratio = (e.clientX - gridRect.left) / gridRect.width;
11576
+ const ratio = (cx - gridRect.left) / gridRect.width;
11507
11577
  const clamped = Math.max(0.15, Math.min(0.85, ratio));
11508
11578
  this._gridColSizes = [clamped, 1 - clamped];
11509
11579
  } else {
11510
- const ratio = (e.clientY - gridRect.top) / gridRect.height;
11580
+ const ratio = (cy - gridRect.top) / gridRect.height;
11511
11581
  const clamped = Math.max(0.15, Math.min(0.85, ratio));
11512
11582
  this._gridRowSizes = [clamped, 1 - clamped];
11513
11583
  }
11514
11584
  this._applyGridSizes();
11515
11585
  };
11516
11586
 
11517
- const onUp = () => {
11587
+ const onMouseMove = (e) => move(e.clientX, e.clientY);
11588
+ const onTouchMove = (e) => {
11589
+ e.preventDefault();
11590
+ move(e.touches[0].clientX, e.touches[0].clientY);
11591
+ };
11592
+ // Capture phase, because the terminal-grid has a bubble-phase touchmove
11593
+ // listener that calls stopPropagation() — would otherwise eat our event.
11594
+ const touchOpts = { passive: false, capture: true };
11595
+ const onEnd = () => {
11518
11596
  handle.classList.remove('active');
11519
- overlay.remove();
11520
- document.removeEventListener('mousemove', onMove);
11521
- document.removeEventListener('mouseup', onUp);
11522
- // Refit all terminals after resize completes
11523
- // safeFit() handles both the fit and sending resize to the server
11524
- this.terminalPanes.forEach(tp => {
11525
- if (tp) tp.safeFit();
11526
- });
11527
- // Persist split ratios for this tab group
11597
+ if (overlay) overlay.remove();
11598
+ document.removeEventListener('mousemove', onMouseMove);
11599
+ document.removeEventListener('mouseup', onEnd);
11600
+ document.removeEventListener('touchmove', onTouchMove, touchOpts);
11601
+ document.removeEventListener('touchend', onEnd);
11602
+ document.removeEventListener('touchcancel', onEnd);
11603
+ this.terminalPanes.forEach(tp => { if (tp) tp.safeFit(); });
11528
11604
  this.saveTerminalLayout();
11529
11605
  };
11530
11606
 
11531
- document.addEventListener('mousemove', onMove);
11532
- document.addEventListener('mouseup', onUp);
11607
+ document.addEventListener('mousemove', onMouseMove);
11608
+ document.addEventListener('mouseup', onEnd);
11609
+ document.addEventListener('touchmove', onTouchMove, touchOpts);
11610
+ document.addEventListener('touchend', onEnd);
11611
+ document.addEventListener('touchcancel', onEnd);
11612
+ };
11613
+
11614
+ handle.addEventListener('mousedown', (e) => {
11615
+ e.preventDefault();
11616
+ e.stopPropagation();
11617
+ start(e.clientX, e.clientY, false);
11533
11618
  });
11619
+ handle.addEventListener('touchstart', (e) => {
11620
+ e.preventDefault();
11621
+ e.stopPropagation();
11622
+ const t = e.touches[0];
11623
+ start(t.clientX, t.clientY, true);
11624
+ }, { passive: false });
11534
11625
  }
11535
11626
 
11536
11627
  /**
@@ -13232,7 +13323,7 @@ class CWMApp {
13232
13323
  e.preventDefault();
13233
13324
  hdr.classList.remove('tab-drag-over');
13234
13325
  const swapSource = e.dataTransfer.getData('cwm/terminal-swap');
13235
- if (swapSource !== '') {
13326
+ if (swapSource) {
13236
13327
  const srcSlot = parseInt(swapSource, 10);
13237
13328
  const folderTabs = this._tabGroups.filter(g => g.folderId === folderId);
13238
13329
  if (folderTabs.length > 0 && folderTabs[0].id !== this._activeGroupId) {
@@ -13322,7 +13413,7 @@ class CWMApp {
13322
13413
 
13323
13414
  // Handle terminal pane drop - move terminal to this tab group
13324
13415
  const swapSource = e.dataTransfer.getData('cwm/terminal-swap');
13325
- if (swapSource !== '') {
13416
+ if (swapSource) {
13326
13417
  const srcSlot = parseInt(swapSource, 10);
13327
13418
  const targetGroupId = tab.dataset.groupId;
13328
13419
  if (targetGroupId !== this._activeGroupId) {
@@ -9,6 +9,7 @@
9
9
  <link rel="icon" type="image/png" sizes="32x32" href="favicon-32.png">
10
10
  <link rel="icon" type="image/png" sizes="192x192" href="favicon-192.png">
11
11
  <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
12
+ <link rel="manifest" href="/manifest.webmanifest">
12
13
  <link rel="preconnect" href="https://fonts.googleapis.com">
13
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
15
  <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400;1,500&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
@@ -182,6 +183,13 @@
182
183
  <button class="theme-option" data-theme="gruvbox-light"><span class="theme-swatch" style="background:#fbf1c7;border:1px solid #d5c4a1"></span>Gruvbox Light</button>
183
184
  </div>
184
185
  </div>
186
+ <button class="btn btn-ghost btn-icon btn-sm vkb-toggle-btn" id="vkb-toggle-btn" title="Disable on-screen keyboard (hardware keyboard only)" aria-pressed="false">
187
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
188
+ <rect x="1.5" y="4" width="13" height="8" rx="1.5"/>
189
+ <path d="M4 7h.01M6 7h.01M8 7h.01M10 7h.01M12 7h.01M5 9.5h6"/>
190
+ <line class="vkb-slash" x1="2.5" y1="13.5" x2="13.5" y2="2.5" stroke-width="1.6"/>
191
+ </svg>
192
+ </button>
185
193
  <button class="btn btn-ghost btn-icon btn-sm" id="pair-mobile-btn" title="Pair Mobile Device">
186
194
  <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">
187
195
  <rect x="4" y="1" width="8" height="14" rx="1.5"/>
@@ -1682,11 +1690,37 @@
1682
1690
 
1683
1691
  <script src="vendor/lucide.bundle.js"></script>
1684
1692
  <script src="vendor/material-icons.bundle.js"></script>
1693
+ <!-- Block the DragDropTouch polyfill from observing touch events that
1694
+ happen inside the xterm.js viewport. The polyfill always dispatches a
1695
+ synthetic mousemove on every touchmove; xterm preventDefault's mousemove,
1696
+ which makes the polyfill cancel the touchmove and kill native scrolling.
1697
+ This script registers its document-bubble listeners *before* the polyfill
1698
+ loads, so stopImmediatePropagation here suppresses the polyfill's
1699
+ same-phase listener for the xterm case only. -->
1700
+ <script>
1701
+ (function () {
1702
+ var inXterm = function (el) {
1703
+ return el && el.closest && el.closest('.xterm-viewport, .xterm-screen');
1704
+ };
1705
+ var stop = function (e) { if (inXterm(e.target)) e.stopImmediatePropagation(); };
1706
+ document.addEventListener('touchstart', stop, { passive: true });
1707
+ document.addEventListener('touchmove', stop, { passive: true });
1708
+ document.addEventListener('touchend', stop, { passive: true });
1709
+ document.addEventListener('touchcancel', stop, { passive: true });
1710
+ })();
1711
+ </script>
1712
+ <!-- Polyfills HTML5 drag-and-drop on touch devices (translates touch -> drag events) -->
1713
+ <script type="module" src="vendor/drag-drop-touch.esm.min.js?autoload"></script>
1685
1714
  <script src="vendor/qrcode.min.js"></script>
1686
1715
  <script src="vendor/xterm/xterm.min.js"></script>
1687
1716
  <script src="vendor/xterm-addon-fit/xterm-addon-fit.min.js"></script>
1688
1717
  <script src="vendor/xterm-addon-web-links/xterm-addon-web-links.min.js"></script>
1689
1718
  <script src="terminal.js"></script>
1690
1719
  <script src="app.js"></script>
1720
+ <script>
1721
+ if ('serviceWorker' in navigator) {
1722
+ window.addEventListener('load', () => navigator.serviceWorker.register('/sw.js').catch(() => {}));
1723
+ }
1724
+ </script>
1691
1725
  </body>
1692
1726
  </html>
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "myrlin's workbook",
3
+ "short_name": "Workbook",
4
+ "description": "Manage Claude Code sessions",
5
+ "start_url": "/",
6
+ "scope": "/",
7
+ "display": "standalone",
8
+ "background_color": "#1e1e2e",
9
+ "theme_color": "#1e1e2e",
10
+ "icons": [
11
+ { "src": "/logo.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
12
+ { "src": "/logo.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }
13
+ ]
14
+ }
@@ -1364,6 +1364,10 @@ textarea.input {
1364
1364
  color: var(--mauve) !important;
1365
1365
  }
1366
1366
 
1367
+ .vkb-toggle-btn .vkb-slash { display: none; }
1368
+ .vkb-toggle-btn.active { color: var(--mauve) !important; }
1369
+ .vkb-toggle-btn.active .vkb-slash { display: inline; }
1370
+
1367
1371
 
1368
1372
  /* ─── Main Content ──────────────────────────────────────────── */
1369
1373
 
@@ -3228,6 +3232,12 @@ textarea.input {
3228
3232
  display: none;
3229
3233
  }
3230
3234
 
3235
+ /* During a drag, let elementFromPoint fall through to drop targets behind
3236
+ the backdrop (otherwise the touch polyfill can't hit terminal panes). */
3237
+ body.cwm-dragging .sidebar-backdrop {
3238
+ pointer-events: none;
3239
+ }
3240
+
3231
3241
 
3232
3242
  /* ─── Workspace color picker in modal ───────────────────────── */
3233
3243
 
@@ -5142,6 +5152,9 @@ textarea.input {
5142
5152
  /* Terminal pane drag-to-reposition */
5143
5153
  .terminal-pane-header {
5144
5154
  cursor: grab;
5155
+ /* Touch: let DragDropTouch polyfill do long-press-to-drag without the
5156
+ browser claiming the gesture for scroll/zoom on the underlying terminal. */
5157
+ touch-action: none;
5145
5158
  }
5146
5159
  .terminal-pane-empty .terminal-pane-header {
5147
5160
  cursor: default;
@@ -5260,6 +5273,13 @@ html.activity-indicators-disabled .terminal-pane-activity {
5260
5273
  z-index: 10;
5261
5274
  background: transparent;
5262
5275
  transition: background 0.15s ease;
5276
+ touch-action: none;
5277
+ }
5278
+ /* Wider invisible hit area for touch — visual stays at the handle's own width. */
5279
+ .terminal-resize-handle::before {
5280
+ content: "";
5281
+ position: absolute;
5282
+ inset: -8px 0;
5263
5283
  }
5264
5284
  .terminal-resize-col {
5265
5285
  top: 0;
@@ -5267,12 +5287,14 @@ html.activity-indicators-disabled .terminal-pane-activity {
5267
5287
  height: 100%;
5268
5288
  cursor: col-resize;
5269
5289
  }
5290
+ .terminal-resize-col::before { inset: 0 -8px; }
5270
5291
  .terminal-resize-row {
5271
5292
  left: 0;
5272
5293
  width: 100%;
5273
5294
  height: 6px;
5274
5295
  cursor: row-resize;
5275
5296
  }
5297
+ .terminal-resize-row::before { inset: -8px 0; }
5276
5298
  .terminal-resize-handle:hover {
5277
5299
  background: rgba(203, 166, 247, 0.4);
5278
5300
  }
@@ -5438,6 +5460,10 @@ html.activity-indicators-disabled .terminal-pane-activity {
5438
5460
  white-space: nowrap;
5439
5461
  transition: all var(--transition-fast);
5440
5462
  position: relative;
5463
+ /* Touch: let DragDropTouch polyfill handle long-press-to-drag without the
5464
+ browser claiming the gesture for the scrollable tab strip.
5465
+ */
5466
+ touch-action: none;
5441
5467
  }
5442
5468
 
5443
5469
  .terminal-group-tab:hover {
@@ -0,0 +1,3 @@
1
+ self.addEventListener('install', () => self.skipWaiting());
2
+ self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));
3
+ self.addEventListener('fetch', () => {});
@@ -720,10 +720,9 @@ class TerminalPane {
720
720
  * Type mode: textarea is writable, keyboard appears for input
721
721
  */
722
722
  _isMobile() {
723
- // Use width-based check matching the CSS media query, NOT touch detection.
724
- // Touch-enabled desktops (Windows laptops) have 'ontouchstart' but should
725
- // NOT get mobile treatment - they have keyboards and wide screens.
726
- return window.innerWidth <= 768;
723
+ // Enable touch scroll/type modes for any device with touch capability,
724
+ // including tablets (not just narrow screens).
725
+ return ('ontouchstart' in window) || navigator.maxTouchPoints > 0;
727
726
  }
728
727
 
729
728
  /**
@@ -874,6 +873,9 @@ class TerminalPane {
874
873
  }
875
874
 
876
875
  if (isScrolling) {
876
+ // Prevent default browser scroll (e.g. pull-to-refresh on Chrome mobile/tablet)
877
+ if (e.cancelable) e.preventDefault();
878
+
877
879
  // Scroll via xterm.js API so ydisp stays in sync (prevents snap-back on output)
878
880
  scrollByPixels(deltaY);
879
881
  // Track velocity for momentum (smoothed exponential average)
@@ -908,11 +910,12 @@ class TerminalPane {
908
910
  };
909
911
 
910
912
  // Use CAPTURE phase to intercept before xterm.js gets the events.
911
- // Non-passive so we can prevent xterm from seeing the events in scroll mode.
912
- container.addEventListener('touchstart', onTouchStart, { capture: true, passive: true });
913
- container.addEventListener('touchmove', onTouchMove, { capture: true, passive: true });
914
- container.addEventListener('touchend', onTouchEnd, { capture: true, passive: true });
915
- container.addEventListener('touchcancel', onTouchEnd, { capture: true, passive: true });
913
+ // Non-passive so we can prevent xterm from seeing the events in scroll mode
914
+ // AND prevent browser pull-to-refresh (overscroll) on mobile/tablet.
915
+ container.addEventListener('touchstart', onTouchStart, { capture: true, passive: false });
916
+ container.addEventListener('touchmove', onTouchMove, { capture: true, passive: false });
917
+ container.addEventListener('touchend', onTouchEnd, { capture: true, passive: false });
918
+ container.addEventListener('touchcancel', onTouchEnd, { capture: true, passive: false });
916
919
 
917
920
  // Store cleanup function for dispose()
918
921
  this._touchScrollCleanup = () => {
@@ -0,0 +1 @@
1
+ function a(s,t=!1){let e=s.touches[0];return{x:t?e.pageX:e.clientX,y:t?e.pageY:e.clientY}}function _(s,t,e){for(let o=0;o<e.length;o++){let n=e[o];s[n]=t[n]}}function f(s,t,e){let o=["altKey","ctrlKey","metaKey","shiftKey"],n=["pageX","pageY","clientX","clientY","screenX","screenY","offsetX","offsetY"],i=new Event(s,{bubbles:!0,cancelable:!0}),h=t.touches[0];return i.button=0,i.which=i.buttons=1,_(i,t,o),_(i,h,n),E(i,e),i}function E(s,t){let e=t.getBoundingClientRect();s.offsetX===void 0&&(s.offsetX=s.clientX-e.x,s.offsetY=s.clientY-e.y),s.layerX===void 0&&(s.layerX=s.pageX-e.left,s.layerY=s.pageY-e.top)}function c(s,t){if(D(t),s instanceof HTMLCanvasElement){let e=t;e.width=s.width,e.height=s.height,e.getContext("2d").drawImage(s,0,0)}y(s,t),t.style.pointerEvents="none";for(let e=0;e<s.children.length;e++)c(s.children[e],t.children[e])}function y(s,t){let e=getComputedStyle(s);for(let o of e)o.includes("transition")||(t.style[o]=e[o]);Object.keys(t.dataset).forEach(o=>delete t.dataset[o])}function D(s){["id","class","style","draggable"].forEach(function(t){s.removeAttribute(t)})}var r=class{_dropEffect;_effectAllowed;_data;_dragDropTouch;constructor(t){this._dropEffect="move",this._effectAllowed="all",this._data={},this._dragDropTouch=t}get dropEffect(){return this._dropEffect}set dropEffect(t){this._dropEffect=t}get effectAllowed(){return this._effectAllowed}set effectAllowed(t){this._effectAllowed=t}get types(){return Object.keys(this._data)}clearData(t){t!==null?delete this._data[t.toLowerCase()]:this._data={}}getData(t){let e=t.toLowerCase(),o=this._data[e];return e==="text"&&o==null&&(o=this._data["text/plain"]),o}setData(t,e){this._data[t.toLowerCase()]=e}setDragImage(t,e,o){this._dragDropTouch.setDragImage(t,e,o)}};var{round:m}=Math,b={allowDragScroll:!0,contextMenuDelayMS:900,dragImageOpacity:.5,dragScrollPercentage:10,dragScrollSpeed:10,dragThresholdPixels:5,forceListen:!1,isPressHoldMode:!1,pressHoldDelayMS:400,pressHoldMargin:25,pressHoldThresholdPixels:0},d=class{_dragRoot;_dropRoot;_dragSource;_lastTouch;_lastTarget;_ptDown;_isDragEnabled;_isDropZone;_dataTransfer;_img;_imgCustom;_imgOffset;_pressHoldIntervalId;configuration;constructor(t=document,e=document,o){for(this.configuration={...b,...o||{}},this._dragRoot=t,this._dropRoot=e;!this._dropRoot.elementFromPoint&&this._dropRoot.parentNode;)this._dropRoot=this._dropRoot.parentNode;this._dragSource=null,this._lastTouch=null,this._lastTarget=null,this._ptDown=null,this._isDragEnabled=!1,this._isDropZone=!1,this._dataTransfer=new r(this),this._img=null,this._imgCustom=null,this._imgOffset={x:0,y:0},this.listen()}listen(){if(navigator.maxTouchPoints===0&&!this.configuration.forceListen)return;let t={passive:!1,capture:!1};this._dragRoot.addEventListener("touchstart",this._touchstart.bind(this),t),this._dragRoot.addEventListener("touchmove",this._touchmove.bind(this),t),this._dragRoot.addEventListener("touchend",this._touchend.bind(this)),this._dragRoot.addEventListener("touchcancel",this._touchend.bind(this))}setDragImage(t,e,o){this._imgCustom=t,this._imgOffset={x:e,y:o}}_touchstart(t){if(this._shouldHandle(t)){this._reset();let e=this._closestDraggable(t.target);e&&t.target&&!this._dispatchEvent(t,"mousemove",t.target)&&!this._dispatchEvent(t,"mousedown",t.target)&&(this._dragSource=e,this._ptDown=a(t),this._lastTouch=t,setTimeout(()=>{this._dragSource===e&&this._img===null&&this._dispatchEvent(t,"contextmenu",e)&&this._reset()},this.configuration.contextMenuDelayMS),this.configuration.isPressHoldMode?this._pressHoldIntervalId=setTimeout(()=>{this._isDragEnabled=!0,this._touchmove(t)},this.configuration.pressHoldDelayMS):t.isTrusted||t.target!==this._lastTarget&&(this._lastTarget=t.target))}}_touchmove(t){if(this._shouldCancelPressHoldMove(t)){this._reset();return}if(this._shouldHandleMove(t)||this._shouldHandlePressHoldMove(t)){let e=this._getTarget(t);if(this._dispatchEvent(t,"mousemove",e)){this._lastTouch=t,t.preventDefault();return}if(this._dragSource&&!this._img&&this._shouldStartDragging(t)){if(this._dispatchEvent(this._lastTouch,"dragstart",this._dragSource)){this._dragSource=null;return}this._createImage(t),this._dispatchEvent(t,"dragenter",e)}if(this._img&&this._dragSource&&(this._lastTouch=t,t.preventDefault(),this._dispatchEvent(t,"drag",this._dragSource),e!==this._lastTarget&&(this._lastTarget&&this._dispatchEvent(this._lastTouch,"dragleave",this._lastTarget),this._dispatchEvent(t,"dragenter",e),this._lastTarget=e),this._moveImage(t),this._isDropZone=this._dispatchEvent(t,"dragover",e),this.configuration.allowDragScroll)){let o=this._getHotRegionDelta(t);globalThis.scrollBy(o.x,o.y)}}}_touchend(t){if(!(this._lastTouch&&t.target&&this._lastTarget)){this._reset();return}if(this._shouldHandle(t)){if(this._dispatchEvent(this._lastTouch,"mouseup",t.target)){t.preventDefault();return}this._img||(this._dragSource=null,this._dispatchEvent(this._lastTouch,"click",t.target)),this._destroyImage(),this._dragSource&&(t.type.indexOf("cancel")<0&&this._isDropZone&&this._dispatchEvent(this._lastTouch,"drop",this._lastTarget),this._dispatchEvent(this._lastTouch,"dragend",this._dragSource),this._reset())}}_shouldHandle(t){return t&&!t.defaultPrevented&&t.touches&&t.touches.length<2}_shouldHandleMove(t){return!this.configuration.isPressHoldMode&&this._shouldHandle(t)}_shouldHandlePressHoldMove(t){return this.configuration.isPressHoldMode&&this._isDragEnabled&&t&&t.touches&&t.touches.length}_shouldCancelPressHoldMove(t){return this.configuration.isPressHoldMode&&!this._isDragEnabled&&this._getDelta(t)>this.configuration.pressHoldMargin}_shouldStartDragging(t){let e=this._getDelta(t);return this.configuration.isPressHoldMode?e>=this.configuration.pressHoldThresholdPixels:e>this.configuration.dragThresholdPixels}_reset(){this._destroyImage(),this._dragSource=null,this._lastTouch=null,this._lastTarget=null,this._ptDown=null,this._isDragEnabled=!1,this._isDropZone=!1,this._dataTransfer=new r(this),clearTimeout(this._pressHoldIntervalId)}_getDelta(t){if(!this._ptDown)return 0;let{x:e,y:o}=this._ptDown,n=a(t);return((n.x-e)**2+(n.y-o)**2)**.5}_getHotRegionDelta(t){let{clientX:e,clientY:o}=t.touches[0],{innerWidth:n,innerHeight:i}=globalThis,{dragScrollPercentage:h,dragScrollSpeed:l}=this.configuration,u=h/100,g=1-u,v=e<n*u?-l:e>n*g?+l:0,T=o<i*u?-l:o>i*g?+l:0;return{x:v,y:T}}_getTarget(t){let e=a(t),o=this._dropRoot.elementFromPoint(e.x,e.y);for(;o&&getComputedStyle(o).pointerEvents=="none";)o=o.parentElement;return o}_createImage(t){this._img&&this._destroyImage();let e=this._imgCustom||this._dragSource;if(this._img=e.cloneNode(!0),c(e,this._img),this._img.style.top=this._img.style.left="-9999px",!this._imgCustom){let o=e.getBoundingClientRect(),n=a(t);this._imgOffset={x:n.x-o.left,y:n.y-o.top},this._img.style.opacity=`${this.configuration.dragImageOpacity}`}this._moveImage(t),document.body.appendChild(this._img)}_destroyImage(){this._img&&this._img.parentElement&&this._img.parentElement.removeChild(this._img),this._img=null,this._imgCustom=null}_moveImage(t){requestAnimationFrame(()=>{if(this._img){let e=a(t,!0),o=this._img.style;o.position="absolute",o.pointerEvents="none",o.zIndex="999999",o.left=`${m(e.x-this._imgOffset.x)}px`,o.top=`${m(e.y-this._imgOffset.y)}px`}})}_dispatchEvent(t,e,o){if(!(t&&o))return!1;let n=f(e,t,o);return n.dataTransfer=this._dataTransfer,o.dispatchEvent(n),n.defaultPrevented}_closestDraggable(t){for(let e=t;e!==null;e=e.parentElement)if(e.draggable)return e;return null}};function p(s=document,t=document,e){new d(s,t,e)}import.meta.url.includes("?autoload")?p(document,document,{forceListen:!0}):globalThis.DragDropTouch={enable:function(s=document,t=document,e){p(s,t,e)}};export{p as enableDragDropTouch};