commit 1c3f2df80b8b4955630e3111f52ec1fd73a2d2de
parent 002e0b2e0f744e80721001ce8cd041187429462f
Author: Hunter
Date: Wed, 17 Jun 2026 15:58:21 -0400
remove smooth scrolling
Diffstat:
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();
+ });
}
}