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 <noreply@anthropic.com>
This commit is contained in:
mARTin 2026-03-25 18:57:15 +01:00
parent 11e7b14c17
commit c05b31d1c6
2 changed files with 92 additions and 1 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "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", "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

@ -1873,6 +1873,8 @@ export default function WeeklyView() {
const [showMobileFabSheet, setShowMobileFabSheet] = useState(false); const [showMobileFabSheet, setShowMobileFabSheet] = useState(false);
const [showMobileFabMenu, setShowMobileFabMenu] = useState(false); const [showMobileFabMenu, setShowMobileFabMenu] = useState(false);
const [mobileStickyDay, setMobileStickyDay] = useState<string | null>(null);
const [mobileStickyDayVisible, setMobileStickyDayVisible] = useState(false);
const [showHeaderMore, setShowHeaderMore] = useState(false); const [showHeaderMore, setShowHeaderMore] = useState(false);
const [fabTaskTitle, setFabTaskTitle] = useState(""); const [fabTaskTitle, setFabTaskTitle] = useState("");
@ -3079,6 +3081,68 @@ export default function WeeklyView() {
return () => clearInterval(interval); 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 // Compute the goal date key: for "week" scope, normalize to Monday of that week; for "day", use the exact date
const getGoalDateKey = useCallback( const getGoalDateKey = useCallback(
(date: Date): string => { (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"}`} className={`weekly-container ${darkMode ? "dark-mode" : ""} font-size-${fontSize.toLowerCase()} ${viewStyle}-view ${showTimeGrid ? "time-grid-on" : "time-grid-off"}`}
style={containerStyle} style={containerStyle}
> >
{/* Mobile sticky day indicator */}
{isMobile && mobileStickyDay && mobileStickyDayVisible && (
<div
className="mobile-sticky-day-bar"
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 90,
background: darkMode ? 'rgba(26,26,46,0.95)' : 'rgba(255,255,255,0.95)',
backdropFilter: 'blur(8px)',
WebkitBackdropFilter: 'blur(8px)',
borderBottom: `1px solid ${darkMode ? '#333' : '#e5e7eb'}`,
padding: '6px 16px',
fontSize: '0.82rem',
fontWeight: 700,
color: darkMode ? '#e5e7eb' : '#333',
fontFamily: 'var(--weekly-font-headline, var(--weekly-font))',
textTransform: 'uppercase',
letterSpacing: '0.02em',
}}
>
{mobileStickyDay}
</div>
)}
{/* Quick Settings Sidebar (TeuxDeux-style) */} {/* Quick Settings Sidebar (TeuxDeux-style) */}
{showQuickSettings && ( {showQuickSettings && (
<> <>