commit 1c3f2df80b8b4955630e3111f52ec1fd73a2d2de
parent 002e0b2e0f744e80721001ce8cd041187429462f
Author: Hunter
Date:   Wed, 17 Jun 2026 15:58:21 -0400

remove smooth scrolling

Diffstat:
Mresources/main.css | 86++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mresources/main.js | 7++++++-
Mresources/multi-select.js | 8--------
Mresources/navigation.js | 8--------
Mresources/render.js | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mresources/reorganize.js | 8--------
Mresources/state.js | 1-
Mresources/theme.js | 8+++++++-
8 files changed, 150 insertions(+), 78 deletions(-)

diff --git a/resources/main.css b/resources/main.css @@ -115,10 +115,10 @@ --icon: '🍬'; .checkbox-label { - border-radius: 10px; + border-radius: 12px; } .active { - border-radius: 30px 0px 0px 30px; + border-radius: 36px 0px 0px 36px; } .custom-checkbox:checked + .checkbox-label::before { display: none; @@ -151,7 +151,7 @@ text-shadow: none; } .active { - border-radius: 30px 0px 0px 30px; + border-radius: 36px 0px 0px 36px; } .active .task-text { color: var(--bloom); @@ -256,10 +256,10 @@ /* interface styling */ @keyframes shake { 0% { transform: translateX(0); } - 20% { transform: translateX(3px); } - 40% { transform: translateX(-3px); } - 60% { transform: translateX(3px); } - 80% { transform: translateX(-3px); } + 20% { transform: translateX(3.6px); } + 40% { transform: translateX(-3.6px); } + 60% { transform: translateX(3.6px); } + 80% { transform: translateX(-3.6px); } 100% { transform: translateX(0); } } .shake { @@ -267,42 +267,41 @@ } @keyframes shake-vertical { 0% { transform: translateY(0); } - 20% { transform: translateY(3px); } - 40% { transform: translateY(-3px); } - 60% { transform: translateY(3px); } - 80% { transform: translateY(-3px); } + 20% { transform: translateY(3.6px); } + 40% { transform: translateY(-3.6px); } + 60% { transform: translateY(3.6px); } + 80% { transform: translateY(-3.6px); } 100% { transform: translateY(0); } } .shake-vertical { animation: shake-vertical 0.25s ease-out; } html { - scroll-behavior: smooth; overflow-y: scroll; overflow-x: hidden; scrollbar-width: auto; scrollbar-color: color-mix(in srgb, var(--text) 75%, transparent) color-mix(in srgb, var(--background) 75%, transparent); } body { - zoom: 1.2; + font-size: 19.2px; font-family: Arial, sans-serif; - max-width: 800px; + max-width: 960px; margin: 0 auto; - padding: 0px 20px 20px 20px; + padding: 0px 24px 24px 24px; background-color: var(--background); color: var(--text); } ul { list-style-type: none; - padding-left: 20px; + padding-left: 24px; } .task-text { font-size: 1.17em; font-weight: bold; flex: 1; min-width: 0; - margin-left: 10px; - padding: 5px 5px 5px 0; + margin-left: 12px; + padding: 6px 6px 6px 0; color: var(--text); overflow: hidden; text-overflow: ellipsis; @@ -325,13 +324,17 @@ ul { .task-container { display: flex; align-items: center; - margin: 5px 0; - padding-left: 11px; - padding-right: 11px; - padding-top: 4px; - padding-bottom: 4px; + margin: 6px 0; + box-sizing: border-box; + padding-left: 13.2px; + padding-right: 13.2px; + padding-top: 4.8px; + padding-bottom: 4.8px; position: relative; } +#subtasks-container .task-container { + height: var(--row-h, auto); +} /* the native caret is hidden app-wide; simulated carets render identically across OSes and between single- and multi-select editing */ .task-text { @@ -364,15 +367,26 @@ ul { top: 0; background-color: var(--background); z-index: 1; - padding-top: 20px; + padding-top: 24px; +} +/* opaque strip covering the fixed margin gap below the header so subtasks + scrolling under it stay hidden */ +.sticky-header::after { + content: ''; + position: absolute; + left: 0; + right: 0; + top: 100%; + height: 16px; + background-color: var(--background); } .parent-task { font-size: 1.5em; font-weight: bold; } #breadcrumbs { - font-size: 24px; - margin-bottom: 10px; + font-size: 28.8px; + margin-bottom: 12px; color: var(--text); user-select: none; } @@ -381,14 +395,14 @@ ul { } .checkbox-label { display: inline-block; - width: 20px; - height: 20px; + width: 24px; + height: 24px; background-color: var(--background); - border: 2px solid var(--accent); + border: 2.4px solid var(--accent); position: relative; cursor: pointer; box-sizing: border-box; - border-radius: 4px; + border-radius: 4.8px; } .custom-checkbox:checked + .checkbox-label { background-color: var(--accent); @@ -396,12 +410,12 @@ ul { .custom-checkbox:checked + .checkbox-label::before { content: ''; position: absolute; - left: 6px; - top: 2px; - width: 5px; - height: 10px; + left: 7.2px; + top: 2.4px; + width: 6px; + height: 12px; border: solid var(--background); - border-width: 0 2px 2px 0; + border-width: 0 2.4px 2.4px 0; transform: rotate(45deg); box-sizing: border-box; } @@ -411,7 +425,7 @@ ul { left: 25%; right: 25%; top: 50%; - height: 2px; + height: 2.4px; background-color: var(--accent); transform: translateY(-50%); } diff --git a/resources/main.js b/resources/main.js @@ -132,6 +132,11 @@ document.addEventListener('DOMContentLoaded', function() { // re-pin the active caret's scroll once layout settles and once fonts load requestAnimationFrame(rescrollActiveCaret); if (document.fonts && document.fonts.ready) { - document.fonts.ready.then(rescrollActiveCaret); + document.fonts.ready.then(() => { + // custom fonts can change row height, so re-snap the grid and scroll + alignRowHeights(); + recenterActiveTask(); + rescrollActiveCaret(); + }); } }); diff --git a/resources/multi-select.js b/resources/multi-select.js @@ -365,10 +365,6 @@ function pushMultiSelectedIntoTarget(direction, navigate = false) { var refocusId = getFocusedSelectedId(); - if (navigate) { - document.documentElement.style.scrollBehavior = 'auto'; - } - var subtasks = state.currentTask.subtasks; var indices = selected.map(t => subtasks.findIndex(s => s.id === t.id)).sort((a, b) => a - b); var topIndex = indices[0]; @@ -419,10 +415,6 @@ function pullMultiSelectedOutLayer(navigate = false) { var refocusId = getFocusedSelectedId(); - if (navigate) { - document.documentElement.style.scrollBehavior = 'auto'; - } - var currentParent = state.currentTask; var grandParent = state.taskPath[state.taskPath.length - 2]; if (!grandParent) return false; diff --git a/resources/navigation.js b/resources/navigation.js @@ -30,8 +30,6 @@ function navigateTasks(direction) { } function navigateIntoSubtask(subtask) { - document.documentElement.style.scrollBehavior = 'auto'; - if (subtask.subtasks.length > 0) { state.currentTask.selectedSubtaskId = subtask.id; state.taskPath.push(subtask); @@ -54,8 +52,6 @@ function navigateIntoSubtask(subtask) { } function navigateIntoTaskAndSelectSubtask(targetTask, subtaskToSelect) { - document.documentElement.style.scrollBehavior = 'auto'; - state.currentTask.selectedSubtaskId = targetTask.id; state.taskPath.push(targetTask); @@ -75,8 +71,6 @@ function navigateToParentTask() { applyShakeAnimation(taskId); } } else if (state.taskPath.length > 1) { - document.documentElement.style.scrollBehavior = 'auto'; - var currentTaskId = state.currentTask.id; state.taskPath.pop(); state.currentTask = state.taskPath[state.taskPath.length - 1]; @@ -99,8 +93,6 @@ function navigateToParentTaskAndSelectTask(targetTask) { renderCurrentView(); selectAndFocusTask(targetTask); } else if (state.taskPath.length > 1) { - document.documentElement.style.scrollBehavior = 'auto'; - state.taskPath.pop(); state.currentTask = state.taskPath[state.taskPath.length - 1]; updateBreadcrumbs(state.currentTask); diff --git a/resources/render.js b/resources/render.js @@ -216,7 +216,7 @@ function positionSimCaret(container, offset, hidden) { } var containerRect = container.getBoundingClientRect(); var textRect = textEl.getBoundingClientRect(); - // client rects include the body zoom but style px get re-zoomed, so unscale + // rounding factor between client rects and style px (1 unless the box is scaled) var scale = container.offsetWidth ? containerRect.width / container.offsetWidth : 1; var x = textRect.left; var top = textRect.top; @@ -243,8 +243,8 @@ function positionSimCaret(container, offset, hidden) { } // keep the caret inside the line's visible text box 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 + // snap to whole pixels and size to exactly one, so the caret never + // straddles a pixel boundary and fattens to 2px caretEl.style.left = (Math.round(x - containerRect.left) / scale) + 'px'; caretEl.style.top = ((top - containerRect.top) / scale) + 'px'; caretEl.style.height = (height / scale) + 'px'; @@ -294,19 +294,90 @@ function setActiveTask(input, task) { state.lastSubtaskShiftDownReleased = isLastSubtask(bottomTask); input.focus(); - // 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); + centerActiveTask(input.closest('.task-container')); +} + +// Vertical distance between consecutive subtask rows (one grid slot). +function getRowStride() { + var rows = document.querySelectorAll('#subtasks-container li'); + if (!rows.length) return 0; + return rows.length >= 2 + ? rows[1].getBoundingClientRect().top - rows[0].getBoundingClientRect().top + : rows[0].getBoundingClientRect().height; +} + +// Scroll by whole row strides so the active subtask stays on a fixed slot grid +// (no drift, no smooth animation): centered when possible, free at the top and +// bottom where the scroll clamps. The parent task lives in the sticky header, so +// selecting it returns to the top. +function centerActiveTask(activeEl) { + if (!activeEl) return; + var inList = activeEl.parentElement && activeEl.parentElement.tagName === 'LI'; + if (!inList) { + window.scrollTo(0, 0); + return; + } + var stride = getRowStride(); + if (stride <= 0) return; + // pad the bottom so max scroll is a whole stride, else it sits off-grid + // (shifting every slot) or is unreachable + updateBottomSpacer(stride); + var rect = activeEl.getBoundingClientRect(); + var center = rect.top + rect.height / 2 + window.scrollY - window.innerHeight / 2; + var snapped = Math.round(center / stride) * stride; + var maxScroll = document.documentElement.scrollHeight - window.innerHeight; + var target = Math.max(0, Math.min(snapped, maxScroll)); + // snap to a device pixel so the browser doesn't re-round the scroll by a + // varying amount each step, which makes rows wiggle + var dpr = window.devicePixelRatio || 1; + window.scrollTo(0, Math.round(target * dpr) / dpr); +} + +// Re-snap the scroll to the current row grid (e.g. after a theme change resizes +// the rows), so slots stay aligned without waiting for the next navigation. +function recenterActiveTask() { + var el = document.querySelector('.task-container.active .task-text'); + var focused = document.activeElement; + if (focused && focused.classList && focused.classList.contains('task-text')) el = focused; + if (el) centerActiveTask(el.closest('.task-container')); +} + +// Nudge each subtask row's height up to the next whole device pixel (at most a +// ~1px bump) so the row stride lands on the device grid and rows don't jitter as +// we scroll; the rows' 6px margins are already whole pixels. +function alignRowHeights() { + var container = document.getElementById('subtasks-container'); + if (!container) return; + var row = container.querySelector('.task-container'); + if (!row) return; + container.style.removeProperty('--row-h'); + var height = row.getBoundingClientRect().height; + if (!height) return; + var dpr = window.devicePixelRatio || 1; + container.style.setProperty('--row-h', Math.ceil(height * dpr) / dpr + 'px'); +} + +// Grow a trailing spacer so the document's max scroll is a whole number of row +// strides (keeps the bottom on the slot grid and reachable) plus a little empty +// space below the last row. +function updateBottomSpacer(stride) { + var container = document.getElementById('subtasks-container'); + if (!container) return; + var spacer = document.getElementById('bottom-spacer'); + if (!spacer) { + spacer = document.createElement('div'); + spacer.id = 'bottom-spacer'; + container.appendChild(spacer); + } + var baseScrollHeight = document.documentElement.scrollHeight - spacer.getBoundingClientRect().height; + var baseMaxScroll = baseScrollHeight - window.innerHeight; + if (baseMaxScroll <= 0) { + spacer.style.height = '0px'; + return; } + var gap = 16; // breathing room below the last row + var target = Math.ceil((baseMaxScroll + gap) / stride) * stride; + spacer.style.height = (target - baseMaxScroll) + 'px'; } function updateBreadcrumbs(selectedTask) { @@ -395,6 +466,7 @@ function renderCurrentView() { }); subtasksContainer.appendChild(subtasksList); state.appContainer.appendChild(subtasksContainer); + alignRowHeights(); updateBreadcrumbs(state.currentTask); updatePageTitle(state.currentTask); diff --git a/resources/reorganize.js b/resources/reorganize.js @@ -28,10 +28,6 @@ function moveSubtask(subtask, direction) { function pushSubtaskIntoTarget(subtask, direction, navigate = false) { if (subtask === state.currentTask) return false; - if (navigate) { - document.documentElement.style.scrollBehavior = 'auto'; - } - var parentTask = findParentTask(subtask); if (!parentTask) return false; @@ -70,10 +66,6 @@ function pullSubtaskOutLayer(subtask, navigate = false) { if (subtask === state.currentTask) return false; if (state.taskPath.length <= 1) return false; - if (navigate) { - document.documentElement.style.scrollBehavior = 'auto'; - } - var currentParent = findParentTask(subtask); if (!currentParent) return; diff --git a/resources/state.js b/resources/state.js @@ -11,7 +11,6 @@ var state = { currentThemeIndex: 0, isF2Pressed: false, themes: ['gak', 'swamp', 'sunflower', 'harvest', 'sugar', 'vineyard', 'woodstove', 'medieval', 'goblin'], - isInWheelEvent: false, windowFocused: true, // Multi-select state diff --git a/resources/theme.js b/resources/theme.js @@ -24,9 +24,15 @@ function setTheme(theme) { var backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--background').trim(); document.getElementById('themeColor').setAttribute('content', backgroundColor); updateFavicon(); + alignRowHeights(); + recenterActiveTask(); rescrollActiveCaret(); if (document.fonts && document.fonts.ready) { - document.fonts.ready.then(rescrollActiveCaret); + document.fonts.ready.then(() => { + alignRowHeights(); + recenterActiveTask(); + rescrollActiveCaret(); + }); } }