From c05b31d1c6ecd83a96235cb1ae6284d6f8472319 Mon Sep 17 00:00:00 2001 From: mARTin Date: Wed, 25 Mar 2026 18:57:15 +0100 Subject: [PATCH] feat: mobile sticky day bar overlay using IntersectionObserver Instead of relying on CSS position:sticky (unreliable on iOS Safari), adds a fixed overlay bar at the top of the viewport that shows the current day name as the user scrolls through stacked day columns. Uses IntersectionObserver to track which day header is near the viewport top. Appears after scrolling past 80px, with blur backdrop. v1.75.0 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/components/WeeklyView.tsx | 91 +++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 1dea7e7..e29d4d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.74.8", + "version": "1.75.0", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index e052c34..6a6714f 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -1873,6 +1873,8 @@ export default function WeeklyView() { const [showMobileFabSheet, setShowMobileFabSheet] = useState(false); const [showMobileFabMenu, setShowMobileFabMenu] = useState(false); + const [mobileStickyDay, setMobileStickyDay] = useState(null); + const [mobileStickyDayVisible, setMobileStickyDayVisible] = useState(false); const [showHeaderMore, setShowHeaderMore] = useState(false); const [fabTaskTitle, setFabTaskTitle] = useState(""); @@ -3079,6 +3081,68 @@ export default function WeeklyView() { return () => clearInterval(interval); }, []); + // Mobile: track which day column is at the top of the viewport via IntersectionObserver + useEffect(() => { + if (!isMobile || !gridRef.current) return; + const headers = gridRef.current.querySelectorAll('.weekly-day-header'); + if (!headers.length) return; + + const observer = new IntersectionObserver( + (entries) => { + // Find the last header that is intersecting (at the top of viewport) + let topHeader: Element | null = null; + let topY = Infinity; + entries.forEach(entry => { + if (entry.isIntersecting || entry.boundingClientRect.top < 100) { + if (entry.boundingClientRect.top < topY) { + topY = entry.boundingClientRect.top; + topHeader = entry.target; + } + } + }); + + // Also check which header is closest to top when scrolled past + if (!topHeader) { + headers.forEach(h => { + const rect = h.getBoundingClientRect(); + if (rect.top < 100 && rect.top > topY - 200) { + topY = rect.top; + topHeader = h; + } + }); + } + + if (topHeader) { + const col = (topHeader as Element).closest('.weekly-day-column'); + const dateStr = col?.getAttribute('data-date'); + if (dateStr) { + const d = new Date(dateStr + 'T00:00:00'); + const dayNames = language === 'de' + ? ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'] + : ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + const label = `${dayNames[d.getDay()]}, ${d.getDate()}.${d.getMonth() + 1}.`; + setMobileStickyDay(label); + } + } + }, + { threshold: [0, 0.1, 0.5, 1], rootMargin: '-48px 0px 0px 0px' } + ); + + headers.forEach(h => observer.observe(h)); + return () => observer.disconnect(); + }, [isMobile, currentWeekStart, viewDays, language]); + + // Show/hide sticky day bar based on scroll position + useEffect(() => { + if (!isMobile) return; + const handleScroll = () => { + setMobileStickyDayVisible(window.scrollY > 80); + }; + window.addEventListener('scroll', handleScroll, { passive: true }); + handleScroll(); + return () => window.removeEventListener('scroll', handleScroll); + }, [isMobile]); + // Compute the goal date key: for "week" scope, normalize to Monday of that week; for "day", use the exact date const getGoalDateKey = useCallback( (date: Date): string => { @@ -6291,6 +6355,33 @@ export default function WeeklyView() { className={`weekly-container ${darkMode ? "dark-mode" : ""} font-size-${fontSize.toLowerCase()} ${viewStyle}-view ${showTimeGrid ? "time-grid-on" : "time-grid-off"}`} style={containerStyle} > + {/* Mobile sticky day indicator */} + {isMobile && mobileStickyDay && mobileStickyDayVisible && ( +
+ {mobileStickyDay} +
+ )} + {/* Quick Settings Sidebar (TeuxDeux-style) */} {showQuickSettings && ( <>