feat: fade/blur header text on hover + fix iOS scroll freeze during calendar fetch

- Header center (KW/year/goal/quote) now fades to 20% opacity and blurs
  on hover so the controls behind it are readable on smaller screens
- Fix iOS scroll freeze: touchmove listener on grid slots was non-passive,
  causing Safari to wait ~20s (iCloud sync duration) before allowing scroll;
  now attaches a passive listener initially and only upgrades to non-passive
  when the long-press drag actually activates (after 500ms hold)

v1.77.0
This commit is contained in:
mARTin 2026-03-29 14:43:23 +02:00
parent bf61733317
commit 517798ce6c
2 changed files with 37 additions and 21 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "name": "my-weekly-todo-list",
"version": "1.76.2", "version": "1.77.0",
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -4110,38 +4110,34 @@ export default function WeeklyView() {
// Stored as refs so the onTouchStart can attach/detach them only when needed // Stored as refs so the onTouchStart can attach/detach them only when needed
// (avoids a permanent non-passive touchmove listener that breaks scroll/sticky) // (avoids a permanent non-passive touchmove listener that breaks scroll/sticky)
const touchMoveHandlerRef = useRef<((e: TouchEvent) => void) | null>(null); const touchMoveHandlerRef = useRef<((e: TouchEvent) => void) | null>(null);
const touchMoveDragHandlerRef = useRef<((e: TouchEvent) => void) | null>(null);
const touchEndHandlerRef = useRef<(() => void) | null>(null); const touchEndHandlerRef = useRef<(() => void) | null>(null);
const cleanupTouchListeners = useCallback(() => { const cleanupTouchListeners = useCallback(() => {
if (touchMoveHandlerRef.current) { if (touchMoveHandlerRef.current) {
document.removeEventListener('touchmove', touchMoveHandlerRef.current); document.removeEventListener('touchmove', touchMoveHandlerRef.current);
touchMoveHandlerRef.current = null; touchMoveHandlerRef.current = null;
} }
if (touchMoveDragHandlerRef.current) {
document.removeEventListener('touchmove', touchMoveDragHandlerRef.current);
touchMoveDragHandlerRef.current = null;
}
if (touchEndHandlerRef.current) { if (touchEndHandlerRef.current) {
document.removeEventListener('touchend', touchEndHandlerRef.current); document.removeEventListener('touchend', touchEndHandlerRef.current);
document.removeEventListener('touchcancel', touchEndHandlerRef.current); document.removeEventListener('touchcancel', touchEndHandlerRef.current);
touchEndHandlerRef.current = null; touchEndHandlerRef.current = null;
} }
}, []); }, []);
const attachTouchListeners = useCallback(() => {
cleanupTouchListeners();
const handleTouchMove = (e: TouchEvent) => {
const lp = touchLongPressRef.current;
if (!lp) return;
const touch = e.touches[0];
if (!lp.activated) {
// Before long-press fires: cancel if finger moves too much (scrolling)
if (Math.abs(touch.clientX - lp.startX) > 10 || Math.abs(touch.clientY - lp.startY) > 10) {
clearTimeout(lp.timerId);
touchLongPressRef.current = null;
cleanupTouchListeners();
}
return;
}
// Called when long-press activates: swap passive cancel-listener for non-passive drag-listener
const upgradeToDragListeners = useCallback(() => {
if (touchMoveHandlerRef.current) {
document.removeEventListener('touchmove', touchMoveHandlerRef.current);
touchMoveHandlerRef.current = null;
}
const handleDragMove = (e: TouchEvent) => {
// Long-press active: prevent scrolling and track slot under finger // Long-press active: prevent scrolling and track slot under finger
e.preventDefault(); e.preventDefault();
const touch = e.touches[0];
const el = document.elementFromPoint(touch.clientX, touch.clientY); const el = document.elementFromPoint(touch.clientX, touch.clientY);
const slotEl = el?.closest('[data-slot]') as HTMLElement | null; const slotEl = el?.closest('[data-slot]') as HTMLElement | null;
if (slotEl) { if (slotEl) {
@ -4158,6 +4154,24 @@ export default function WeeklyView() {
} }
} }
}; };
touchMoveDragHandlerRef.current = handleDragMove;
document.addEventListener('touchmove', handleDragMove, { passive: false });
}, []);
const attachTouchListeners = useCallback(() => {
cleanupTouchListeners();
// Phase 1: passive listener — only cancels long-press if finger moves (no scroll blocking)
const handlePassiveTouchMove = (e: TouchEvent) => {
const lp = touchLongPressRef.current;
if (!lp || lp.activated) return;
const touch = e.touches[0];
if (Math.abs(touch.clientX - lp.startX) > 10 || Math.abs(touch.clientY - lp.startY) > 10) {
clearTimeout(lp.timerId);
touchLongPressRef.current = null;
cleanupTouchListeners();
}
};
const handleTouchEnd = () => { const handleTouchEnd = () => {
const lp = touchLongPressRef.current; const lp = touchLongPressRef.current;
@ -4194,9 +4208,9 @@ export default function WeeklyView() {
}); });
}; };
touchMoveHandlerRef.current = handleTouchMove; touchMoveHandlerRef.current = handlePassiveTouchMove;
touchEndHandlerRef.current = handleTouchEnd; touchEndHandlerRef.current = handleTouchEnd;
document.addEventListener('touchmove', handleTouchMove, { passive: false }); document.addEventListener('touchmove', handlePassiveTouchMove, { passive: true });
document.addEventListener('touchend', handleTouchEnd); document.addEventListener('touchend', handleTouchEnd);
document.addEventListener('touchcancel', handleTouchEnd); document.addEventListener('touchcancel', handleTouchEnd);
}, [cleanupTouchListeners]); }, [cleanupTouchListeners]);
@ -6827,7 +6841,7 @@ export default function WeeklyView() {
</div> </div>
{/* CENTER SECTION: Week/Year, Goal, Focus Mode - Reveal on Hover */} {/* CENTER SECTION: Week/Year, Goal, Focus Mode - Reveal on Hover */}
<div className="flex items-center justify-center gap-6 absolute left-1/2 transform -translate-x-1/2 group z-10 opacity-100" style={{ pointerEvents: "auto" }}> <div className="flex items-center justify-center gap-6 absolute left-1/2 transform -translate-x-1/2 z-0 transition-all duration-300 group-hover:opacity-20 group-hover:blur-[2px]">
{/* Week & Year */} {/* Week & Year */}
<div className="whitespace-nowrap flex items-center gap-2"> <div className="whitespace-nowrap flex items-center gap-2">
{/* Clickable Week & Year */} {/* Clickable Week & Year */}
@ -8092,6 +8106,8 @@ export default function WeeklyView() {
if (touchLongPressRef.current) { if (touchLongPressRef.current) {
touchLongPressRef.current.activated = true; touchLongPressRef.current.activated = true;
} }
// Upgrade from passive to non-passive touchmove now that drag is active
upgradeToDragListeners();
const ds = `${slotDate.getFullYear()}-${String(slotDate.getMonth() + 1).padStart(2, '0')}-${String(slotDate.getDate()).padStart(2, '0')}`; const ds = `${slotDate.getFullYear()}-${String(slotDate.getMonth() + 1).padStart(2, '0')}-${String(slotDate.getDate()).padStart(2, '0')}`;
setSlotDragSelection({ dateStr: ds, startSlot: slotName, endSlot: slotName }); setSlotDragSelection({ dateStr: ds, startSlot: slotName, endSlot: slotName });
}, 500); }, 500);