remove smooth scrolling Hunter Wednesday, June 17th, 2026 at 3:58 PM
115 --icon: '🍬'; 116 117 .checkbox-label { - border-radius: 10px; 118 + border-radius: 12px; 119 } 120 .active { - border-radius: 30px 0px 0px 30px; 121 + border-radius: 36px 0px 0px 36px; 122 } 123 .custom-checkbox:checked + .checkbox-label::before { 124 display: none; 151 text-shadow: none; 152 } 153 .active { - border-radius: 30px 0px 0px 30px; 154 + border-radius: 36px 0px 0px 36px; 155 } 156 .active .task-text { 157 color: var(--bloom); 256 /* interface styling */ 257 @keyframes shake { 258 0% { transform: translateX(0); } - 20% { transform: translateX(3px); } - 40% { transform: translateX(-3px); } - 60% { transform: translateX(3px); } - 80% { transform: translateX(-3px); } 259 + 20% { transform: translateX(3.6px); } 260 + 40% { transform: translateX(-3.6px); } 261 + 60% { transform: translateX(3.6px); } 262 + 80% { transform: translateX(-3.6px); } 263 100% { transform: translateX(0); } 264 } 265 .shake { 267 } 268 @keyframes shake-vertical { 269 0% { transform: translateY(0); } - 20% { transform: translateY(3px); } - 40% { transform: translateY(-3px); } - 60% { transform: translateY(3px); } - 80% { transform: translateY(-3px); } 270 + 20% { transform: translateY(3.6px); } 271 + 40% { transform: translateY(-3.6px); } 272 + 60% { transform: translateY(3.6px); } 273 + 80% { transform: translateY(-3.6px); } 274 100% { transform: translateY(0); } 275 } 276 .shake-vertical { 277 animation: shake-vertical 0.25s ease-out; 278 } 279 html { - scroll-behavior: smooth; 280 overflow-y: scroll; 281 overflow-x: hidden; 282 scrollbar-width: auto; 283 scrollbar-color: color-mix(in srgb, var(--text) 75%, transparent) color-mix(in srgb, var(--background) 75%, transparent); 284 } 285 body { - zoom: 1.2; 286 + font-size: 19.2px; 287 font-family: Arial, sans-serif; - max-width: 800px; 288 + max-width: 960px; 289 margin: 0 auto; - padding: 0px 20px 20px 20px; 290 + padding: 0px 24px 24px 24px; 291 background-color: var(--background); 292 color: var(--text); 293 } 294 ul { 295 list-style-type: none; - padding-left: 20px; 296 + padding-left: 24px; 297 } 298 .task-text { 299 font-size: 1.17em; 300 font-weight: bold; 301 flex: 1; 302 min-width: 0; - margin-left: 10px; - padding: 5px 5px 5px 0; 303 + margin-left: 12px; 304 + padding: 6px 6px 6px 0; 305 color: var(--text); 306 overflow: hidden; 307 text-overflow: ellipsis; 324 .task-container { 325 display: flex; 326 align-items: center; - margin: 5px 0; - padding-left: 11px; - padding-right: 11px; - padding-top: 4px; - padding-bottom: 4px; 327 + margin: 6px 0; 328 + box-sizing: border-box; 329 + padding-left: 13.2px; 330 + padding-right: 13.2px; 331 + padding-top: 4.8px; 332 + padding-bottom: 4.8px; 333 position: relative; 334 } 335 +#subtasks-container .task-container { 336 + height: var(--row-h, auto); 337 +} 338 /* the native caret is hidden app-wide; simulated carets render identically 339 across OSes and between single- and multi-select editing */ 340 .task-text { 367 top: 0; 368 background-color: var(--background); 369 z-index: 1; - padding-top: 20px; 370 + padding-top: 24px; 371 +} 372 +/* opaque strip covering the fixed margin gap below the header so subtasks 373 + scrolling under it stay hidden */ 374 +.sticky-header::after { 375 + content: ''; 376 + position: absolute; 377 + left: 0; 378 + right: 0; 379 + top: 100%; 380 + height: 16px; 381 + background-color: var(--background); 382 } 383 .parent-task { 384 font-size: 1.5em; 385 font-weight: bold; 386 } 387 #breadcrumbs { - font-size: 24px; - margin-bottom: 10px; 388 + font-size: 28.8px; 389 + margin-bottom: 12px; 390 color: var(--text); 391 user-select: none; 392 } 395 } 396 .checkbox-label { 397 display: inline-block; - width: 20px; - height: 20px; 398 + width: 24px; 399 + height: 24px; 400 background-color: var(--background); - border: 2px solid var(--accent); 401 + border: 2.4px solid var(--accent); 402 position: relative; 403 cursor: pointer; 404 box-sizing: border-box; - border-radius: 4px; 405 + border-radius: 4.8px; 406 } 407 .custom-checkbox:checked + .checkbox-label { 408 background-color: var(--accent); 410 .custom-checkbox:checked + .checkbox-label::before { 411 content: ''; 412 position: absolute; - left: 6px; - top: 2px; - width: 5px; - height: 10px; 413 + left: 7.2px; 414 + top: 2.4px; 415 + width: 6px; 416 + height: 12px; 417 border: solid var(--background); - border-width: 0 2px 2px 0; 418 + border-width: 0 2.4px 2.4px 0; 419 transform: rotate(45deg); 420 box-sizing: border-box; 421 } 425 left: 25%; 426 right: 25%; 427 top: 50%; - height: 2px; 428 + height: 2.4px; 429 background-color: var(--accent); 430 transform: translateY(-50%); 431 }
132 // re-pin the active caret's scroll once layout settles and once fonts load 133 requestAnimationFrame(rescrollActiveCaret); 134 if (document.fonts && document.fonts.ready) { - document.fonts.ready.then(rescrollActiveCaret); 135 + document.fonts.ready.then(() => { 136 + // custom fonts can change row height, so re-snap the grid and scroll 137 + alignRowHeights(); 138 + recenterActiveTask(); 139 + rescrollActiveCaret(); 140 + }); 141 } 142 });
365 366 var refocusId = getFocusedSelectedId(); 367 - if (navigate) { - document.documentElement.style.scrollBehavior = 'auto'; - } - 368 var subtasks = state.currentTask.subtasks; 369 var indices = selected.map(t => subtasks.findIndex(s => s.id === t.id)).sort((a, b) => a - b); 370 var topIndex = indices[0]; 415 416 var refocusId = getFocusedSelectedId(); 417 - if (navigate) { - document.documentElement.style.scrollBehavior = 'auto'; - } - 418 var currentParent = state.currentTask; 419 var grandParent = state.taskPath[state.taskPath.length - 2]; 420 if (!grandParent) return false;
30 } 31 32 function navigateIntoSubtask(subtask) { - document.documentElement.style.scrollBehavior = 'auto'; - 33 if (subtask.subtasks.length > 0) { 34 state.currentTask.selectedSubtaskId = subtask.id; 35 state.taskPath.push(subtask); 52 } 53 54 function navigateIntoTaskAndSelectSubtask(targetTask, subtaskToSelect) { - document.documentElement.style.scrollBehavior = 'auto'; - 55 state.currentTask.selectedSubtaskId = targetTask.id; 56 57 state.taskPath.push(targetTask); 71 applyShakeAnimation(taskId); 72 } 73 } else if (state.taskPath.length > 1) { - document.documentElement.style.scrollBehavior = 'auto'; - 74 var currentTaskId = state.currentTask.id; 75 state.taskPath.pop(); 76 state.currentTask = state.taskPath[state.taskPath.length - 1]; 93 renderCurrentView(); 94 selectAndFocusTask(targetTask); 95 } else if (state.taskPath.length > 1) { - document.documentElement.style.scrollBehavior = 'auto'; - 96 state.taskPath.pop(); 97 state.currentTask = state.taskPath[state.taskPath.length - 1]; 98 updateBreadcrumbs(state.currentTask);
216 } 217 var containerRect = container.getBoundingClientRect(); 218 var textRect = textEl.getBoundingClientRect(); - // client rects include the body zoom but style px get re-zoomed, so unscale 219 + // rounding factor between client rects and style px (1 unless the box is scaled) 220 var scale = container.offsetWidth ? containerRect.width / container.offsetWidth : 1; 221 var x = textRect.left; 222 var top = textRect.top; 243 } 244 // keep the caret inside the line's visible text box 245 x = Math.max(textRect.left, Math.min(x, textRect.right - 1)); - // snap to whole zoomed pixels and size to exactly one, so the caret - // never straddles a pixel boundary and fattens to 2px 246 + // snap to whole pixels and size to exactly one, so the caret never 247 + // straddles a pixel boundary and fattens to 2px 248 caretEl.style.left = (Math.round(x - containerRect.left) / scale) + 'px'; 249 caretEl.style.top = ((top - containerRect.top) / scale) + 'px'; 250 caretEl.style.height = (height / scale) + 'px'; 294 state.lastSubtaskShiftDownReleased = isLastSubtask(bottomTask); 295 input.focus(); 296 - // Center the active task in the viewport - var activeTaskElement = input.closest('.task-container'); - if (activeTaskElement && activeTaskElement.parentElement && activeTaskElement.parentElement.tagName === 'LI') { - activeTaskElement.scrollIntoView({ - behavior: 'auto', - block: 'center', - inline: 'nearest' - }); - - setTimeout(() => { - document.documentElement.style.scrollBehavior = 'smooth'; - }, 200); 297 + centerActiveTask(input.closest('.task-container')); 298 +} 299 + 300 +// Vertical distance between consecutive subtask rows (one grid slot). 301 +function getRowStride() { 302 + var rows = document.querySelectorAll('#subtasks-container li'); 303 + if (!rows.length) return 0; 304 + return rows.length >= 2 305 + ? rows[1].getBoundingClientRect().top - rows[0].getBoundingClientRect().top 306 + : rows[0].getBoundingClientRect().height; 307 +} 308 + 309 +// Scroll by whole row strides so the active subtask stays on a fixed slot grid 310 +// (no drift, no smooth animation): centered when possible, free at the top and 311 +// bottom where the scroll clamps. The parent task lives in the sticky header, so 312 +// selecting it returns to the top. 313 +function centerActiveTask(activeEl) { 314 + if (!activeEl) return; 315 + var inList = activeEl.parentElement && activeEl.parentElement.tagName === 'LI'; 316 + if (!inList) { 317 + window.scrollTo(0, 0); 318 + return; 319 + } 320 + var stride = getRowStride(); 321 + if (stride <= 0) return; 322 + // pad the bottom so max scroll is a whole stride, else it sits off-grid 323 + // (shifting every slot) or is unreachable 324 + updateBottomSpacer(stride); 325 + var rect = activeEl.getBoundingClientRect(); 326 + var center = rect.top + rect.height / 2 + window.scrollY - window.innerHeight / 2; 327 + var snapped = Math.round(center / stride) * stride; 328 + var maxScroll = document.documentElement.scrollHeight - window.innerHeight; 329 + var target = Math.max(0, Math.min(snapped, maxScroll)); 330 + // snap to a device pixel so the browser doesn't re-round the scroll by a 331 + // varying amount each step, which makes rows wiggle 332 + var dpr = window.devicePixelRatio || 1; 333 + window.scrollTo(0, Math.round(target * dpr) / dpr); 334 +} 335 + 336 +// Re-snap the scroll to the current row grid (e.g. after a theme change resizes 337 +// the rows), so slots stay aligned without waiting for the next navigation. 338 +function recenterActiveTask() { 339 + var el = document.querySelector('.task-container.active .task-text'); 340 + var focused = document.activeElement; 341 + if (focused && focused.classList && focused.classList.contains('task-text')) el = focused; 342 + if (el) centerActiveTask(el.closest('.task-container')); 343 +} 344 + 345 +// Nudge each subtask row's height up to the next whole device pixel (at most a 346 +// ~1px bump) so the row stride lands on the device grid and rows don't jitter as 347 +// we scroll; the rows' 6px margins are already whole pixels. 348 +function alignRowHeights() { 349 + var container = document.getElementById('subtasks-container'); 350 + if (!container) return; 351 + var row = container.querySelector('.task-container'); 352 + if (!row) return; 353 + container.style.removeProperty('--row-h'); 354 + var height = row.getBoundingClientRect().height; 355 + if (!height) return; 356 + var dpr = window.devicePixelRatio || 1; 357 + container.style.setProperty('--row-h', Math.ceil(height * dpr) / dpr + 'px'); 358 +} 359 + 360 +// Grow a trailing spacer so the document's max scroll is a whole number of row 361 +// strides (keeps the bottom on the slot grid and reachable) plus a little empty 362 +// space below the last row. 363 +function updateBottomSpacer(stride) { 364 + var container = document.getElementById('subtasks-container'); 365 + if (!container) return; 366 + var spacer = document.getElementById('bottom-spacer'); 367 + if (!spacer) { 368 + spacer = document.createElement('div'); 369 + spacer.id = 'bottom-spacer'; 370 + container.appendChild(spacer); 371 + } 372 + var baseScrollHeight = document.documentElement.scrollHeight - spacer.getBoundingClientRect().height; 373 + var baseMaxScroll = baseScrollHeight - window.innerHeight; 374 + if (baseMaxScroll <= 0) { 375 + spacer.style.height = '0px'; 376 + return; 377 } 378 + var gap = 16; // breathing room below the last row 379 + var target = Math.ceil((baseMaxScroll + gap) / stride) * stride; 380 + spacer.style.height = (target - baseMaxScroll) + 'px'; 381 } 382 383 function updateBreadcrumbs(selectedTask) { 466 }); 467 subtasksContainer.appendChild(subtasksList); 468 state.appContainer.appendChild(subtasksContainer); 469 + alignRowHeights(); 470 471 updateBreadcrumbs(state.currentTask); 472 updatePageTitle(state.currentTask);
28 function pushSubtaskIntoTarget(subtask, direction, navigate = false) { 29 if (subtask === state.currentTask) return false; 30 - if (navigate) { - document.documentElement.style.scrollBehavior = 'auto'; - } - 31 var parentTask = findParentTask(subtask); 32 if (!parentTask) return false; 33 66 if (subtask === state.currentTask) return false; 67 if (state.taskPath.length <= 1) return false; 68 - if (navigate) { - document.documentElement.style.scrollBehavior = 'auto'; - } - 69 var currentParent = findParentTask(subtask); 70 if (!currentParent) return; 71
11 currentThemeIndex: 0, 12 isF2Pressed: false, 13 themes: ['gak', 'swamp', 'sunflower', 'harvest', 'sugar', 'vineyard', 'woodstove', 'medieval', 'goblin'], - isInWheelEvent: false, 14 windowFocused: true, 15 16 // Multi-select state
24 var backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--background').trim(); 25 document.getElementById('themeColor').setAttribute('content', backgroundColor); 26 updateFavicon(); 27 + alignRowHeights(); 28 + recenterActiveTask(); 29 rescrollActiveCaret(); 30 if (document.fonts && document.fonts.ready) { - document.fonts.ready.then(rescrollActiveCaret); 31 + document.fonts.ready.then(() => { 32 + alignRowHeights(); 33 + recenterActiveTask(); 34 + rescrollActiveCaret(); 35 + }); 36 } 37 } 38