experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(page): improve drag-from-anywhere and resize robustness

+135 -27
+23 -3
app/page/index.html
··· 116 116 z-index: 50; 117 117 } 118 118 119 - /* Resize handle */ 119 + /* Resize handle — larger hit area for easier grabbing */ 120 120 .resize-handle { 121 121 position: absolute; 122 - width: 16px; 123 - height: 16px; 122 + width: 24px; 123 + height: 24px; 124 124 right: 0; 125 125 bottom: 0; 126 126 cursor: se-resize; 127 127 z-index: 10; 128 + /* Prevent pointer capture from being interrupted */ 129 + touch-action: none; 130 + } 131 + 132 + /* Visual resize grip indicator (three diagonal lines) */ 133 + .resize-handle::after { 134 + content: ''; 135 + position: absolute; 136 + right: 4px; 137 + bottom: 4px; 138 + width: 10px; 139 + height: 10px; 140 + border-right: 2px solid rgba(128, 128, 128, 0.5); 141 + border-bottom: 2px solid rgba(128, 128, 128, 0.5); 142 + border-radius: 0 0 2px 0; 143 + } 144 + 145 + .resize-handle:hover::after { 146 + border-right-color: rgba(128, 128, 128, 0.8); 147 + border-bottom-color: rgba(128, 128, 128, 0.8); 128 148 } 129 149 130 150 /* Mode indicator - subtle corner badge */
+112 -24
app/page/page.js
··· 6 6 * are position:absolute siblings, positioned by JS via updatePositions() using 7 7 * a `bounds` object. The navbar sits ABOVE the webview with an 8px gap. 8 8 * 9 - * - Custom drag: mousedown on navbar moves bounds 10 - * - Custom resize: mousedown on resize handle changes bounds size 9 + * - Custom drag: instant on navbar background, hold-150ms-to-drag anywhere else 10 + * - Custom resize: pointer-captured resize handle (robust even if cursor leaves window) 11 11 * - Hover trigger zone above navbar reveals/hides navbar 12 12 * - Cmd+L shows navbar with URL focus 13 13 */ ··· 81 81 triggerZone.style.top = `${triggerTop}px`; 82 82 triggerZone.style.width = `${width}px`; 83 83 84 - // Resize handle — bottom-right corner of the webview 85 - resizeHandle.style.left = `${x + width - 16}px`; 86 - resizeHandle.style.top = `${y + height - 16}px`; 84 + // Resize handle — bottom-right corner of the webview (24x24) 85 + resizeHandle.style.left = `${x + width - 24}px`; 86 + resizeHandle.style.top = `${y + height - 24}px`; 87 87 88 88 // Mode indicator — top-right corner of the webview 89 89 if (modeIndicator) { ··· 119 119 // Start initialization 120 120 initWebview(); 121 121 122 - // --- Custom drag (navbar) --- 122 + // --- Custom drag --- 123 + // Two modes: 124 + // 1. Navbar background: instant drag (no hold delay) — primary purpose of navbar 125 + // 2. Anywhere else (webview, buttons, url text, empty space): hold ~150ms then drag 126 + // A quick click (<150ms) on non-navbar areas passes through to the underlying element. 123 127 124 128 let isDragging = false; 125 129 let dragStartX = 0; ··· 127 131 let dragStartBoundsX = 0; 128 132 let dragStartBoundsY = 0; 129 133 130 - navbar.addEventListener('mousedown', (e) => { 131 - // Don't start drag on buttons or URL text 132 - if (e.target.closest('.nav-btn') || e.target.closest('.url-text')) return; 134 + // Hold-to-drag state 135 + const DRAG_HOLD_THRESHOLD = 150; // ms before hold-drag activates 136 + let holdDragTimer = null; 137 + let holdDragPending = false; 138 + let holdDragStartScreenX = 0; 139 + let holdDragStartScreenY = 0; 140 + 141 + function startDrag(screenX, screenY) { 133 142 isDragging = true; 134 - dragStartX = e.screenX; 135 - dragStartY = e.screenY; 143 + dragStartX = screenX; 144 + dragStartY = screenY; 136 145 dragStartBoundsX = bounds.x; 137 146 dragStartBoundsY = bounds.y; 147 + document.body.style.cursor = 'grabbing'; 148 + } 149 + 150 + function cancelHoldDrag() { 151 + if (holdDragTimer) { 152 + clearTimeout(holdDragTimer); 153 + holdDragTimer = null; 154 + } 155 + holdDragPending = false; 156 + } 157 + 158 + // Instant drag on navbar background (excluding buttons and URL text) 159 + navbar.addEventListener('mousedown', (e) => { 160 + if (e.target.closest('.nav-btn') || e.target.closest('.url-text')) return; 161 + startDrag(e.screenX, e.screenY); 138 162 navbar.style.cursor = 'grabbing'; 139 163 e.preventDefault(); 140 164 }); 141 165 166 + // Hold-to-drag from anywhere on the document 167 + document.addEventListener('mousedown', (e) => { 168 + // Skip if resize handle was clicked 169 + if (e.target.closest('.resize-handle')) return; 170 + // Skip if already dragging (navbar instant drag handled above) 171 + if (isDragging) return; 172 + // Only primary button 173 + if (e.button !== 0) return; 174 + 175 + holdDragPending = true; 176 + holdDragStartScreenX = e.screenX; 177 + holdDragStartScreenY = e.screenY; 178 + 179 + holdDragTimer = setTimeout(() => { 180 + if (holdDragPending) { 181 + startDrag(holdDragStartScreenX, holdDragStartScreenY); 182 + holdDragPending = false; 183 + } 184 + }, DRAG_HOLD_THRESHOLD); 185 + }); 186 + 187 + // Cancel hold-drag on mouseup before threshold 188 + document.addEventListener('mouseup', () => { 189 + cancelHoldDrag(); 190 + }); 191 + 142 192 // --- Custom resize (resize handle) --- 193 + // Uses pointer capture to keep receiving events even if cursor leaves the window. 143 194 144 195 let isResizing = false; 145 196 let resizeStartX = 0; ··· 147 198 let resizeStartWidth = 0; 148 199 let resizeStartHeight = 0; 149 200 150 - resizeHandle.addEventListener('mousedown', (e) => { 201 + resizeHandle.addEventListener('pointerdown', (e) => { 202 + if (e.button !== 0) return; 151 203 isResizing = true; 152 204 resizeStartX = e.screenX; 153 205 resizeStartY = e.screenY; 154 206 resizeStartWidth = bounds.width; 155 207 resizeStartHeight = bounds.height; 208 + document.body.style.cursor = 'se-resize'; 209 + // Capture pointer so events continue even if cursor leaves the window 210 + resizeHandle.setPointerCapture(e.pointerId); 156 211 e.preventDefault(); 157 212 e.stopPropagation(); 158 213 }); 159 214 160 - // --- Shared mousemove/mouseup for drag and resize --- 215 + resizeHandle.addEventListener('pointermove', (e) => { 216 + if (!isResizing) return; 217 + // Safety: if no button is held, cancel resize 218 + if (e.buttons === 0) { 219 + cancelResize(); 220 + return; 221 + } 222 + const dx = e.screenX - resizeStartX; 223 + const dy = e.screenY - resizeStartY; 224 + bounds.width = Math.max(MIN_WIDTH, resizeStartWidth + dx); 225 + bounds.height = Math.max(MIN_HEIGHT, resizeStartHeight + dy); 226 + updatePositions(); 227 + }); 228 + 229 + resizeHandle.addEventListener('pointerup', (e) => { 230 + if (isResizing) { 231 + cancelResize(); 232 + resizeHandle.releasePointerCapture(e.pointerId); 233 + } 234 + }); 235 + 236 + // Also release on pointer cancel (e.g., system interruption) 237 + resizeHandle.addEventListener('pointercancel', (e) => { 238 + if (isResizing) { 239 + cancelResize(); 240 + } 241 + }); 242 + 243 + function cancelResize() { 244 + isResizing = false; 245 + document.body.style.cursor = ''; 246 + } 247 + 248 + // --- Shared mousemove/mouseup for drag --- 161 249 162 250 document.addEventListener('mousemove', (e) => { 251 + // Safety: if no button is held during drag, cancel it 252 + if (isDragging && e.buttons === 0) { 253 + isDragging = false; 254 + document.body.style.cursor = ''; 255 + navbar.style.cursor = 'grab'; 256 + return; 257 + } 258 + 163 259 if (isDragging) { 164 260 const dx = e.screenX - dragStartX; 165 261 const dy = e.screenY - dragStartY; 166 262 bounds.x = dragStartBoundsX + dx; 167 263 bounds.y = dragStartBoundsY + dy; 168 264 updatePositions(); 169 - } else if (isResizing) { 170 - const dx = e.screenX - resizeStartX; 171 - const dy = e.screenY - resizeStartY; 172 - bounds.width = Math.max(MIN_WIDTH, resizeStartWidth + dx); 173 - bounds.height = Math.max(MIN_HEIGHT, resizeStartHeight + dy); 174 - updatePositions(); 175 265 } 176 266 }); 177 267 178 268 document.addEventListener('mouseup', () => { 179 269 if (isDragging) { 180 270 isDragging = false; 271 + document.body.style.cursor = ''; 181 272 navbar.style.cursor = 'grab'; 182 - } 183 - if (isResizing) { 184 - isResizing = false; 185 273 } 186 274 }); 187 275 ··· 225 313 } 226 314 227 315 function hide() { 228 - // Don't hide while dragging 229 - if (isDragging) return; 316 + // Don't hide while dragging or about to drag 317 + if (isDragging || holdDragPending) return; 230 318 if (hideTimer) { 231 319 clearTimeout(hideTimer); 232 320 hideTimer = null;