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:
parent
11e7b14c17
commit
c05b31d1c6
@ -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": {
|
||||||
|
|||||||
@ -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 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user