feat: add responsive header with overflow menu for tablet

Desktop shows all action buttons inline. On tablet (≤1024px), secondary
actions collapse into a dropdown overflow menu with proper touch targets,
dark mode support, and fade-in animation.

v1.42.0
This commit is contained in:
mARTin 2026-03-17 10:55:52 +01:00
parent 18da568b9f
commit 0248cfbb6c
3 changed files with 136 additions and 142 deletions

View File

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

@ -3693,6 +3693,90 @@ h3 {
opacity: 0; opacity: 0;
} }
/* Responsive header sections */
.header-desktop-only {
display: flex;
}
.header-tablet-only {
display: none;
}
@media (max-width: 1024px) {
.header-desktop-only {
display: none !important;
}
.header-tablet-only {
display: block !important;
}
/* Make controls visible on tablet (no hover on touch devices) */
.weekly-header-controls {
opacity: 1 !important;
transition: none !important;
}
}
/* Header overflow menu (tablet) */
.header-overflow-menu {
position: absolute;
top: 100%;
right: 0;
z-index: 100;
min-width: 200px;
padding: 6px;
margin-top: 4px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
display: flex;
flex-direction: column;
gap: 2px;
animation: menuFadeIn 0.15s ease;
}
.dark-mode .header-overflow-menu {
background: #1f2937;
border-color: #374151;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
.header-overflow-menu button {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 12px;
border: none;
background: transparent;
border-radius: 8px;
font-size: 13px;
color: #374151;
cursor: pointer;
transition: background 0.15s;
}
.header-overflow-menu button:hover {
background: #f3f4f6;
}
.header-overflow-menu button:disabled {
opacity: 0.35;
cursor: default;
}
.dark-mode .header-overflow-menu button {
color: #d1d5db;
}
.dark-mode .header-overflow-menu button:hover {
background: #374151;
}
@keyframes menuFadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
/* List View (formerly Grid/App View - no vertical lines) */ /* List View (formerly Grid/App View - no vertical lines) */
.list-view.time-grid-off .weekly-day-column { .list-view.time-grid-off .weekly-day-column {
border-right: none !important; border-right: none !important;

View File

@ -1795,6 +1795,7 @@ 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 [showHeaderMore, setShowHeaderMore] = useState(false);
const [fabTaskTitle, setFabTaskTitle] = useState(""); const [fabTaskTitle, setFabTaskTitle] = useState("");
const fabTextareaRef = useRef<HTMLTextAreaElement>(null); const fabTextareaRef = useRef<HTMLTextAreaElement>(null);
@ -6021,163 +6022,72 @@ export default function WeeklyView() {
</div> </div>
{/* RIGHT SECTION: Navigation & Tools */} {/* RIGHT SECTION: Navigation & Tools */}
<div className="weekly-header-controls flex-shrink-0 flex items-center gap-2 sm:gap-3 transition-opacity duration-300 opacity-0 group-hover:opacity-100" style={{ zIndex: 10 }}> <div className="weekly-header-controls flex-shrink-0 flex items-center gap-1.5 sm:gap-2 transition-opacity duration-300 opacity-0 group-hover:opacity-100" style={{ zIndex: 10 }}>
{/* Undo/Redo */} {/* Secondary actions — hidden on tablet, visible on desktop */}
<button <div className="header-desktop-only flex items-center gap-1.5">
onClick={handleUndo} <button onClick={handleUndo} disabled={undoCount === 0} className="weekly-btn-icon disabled:opacity-30 disabled:cursor-default" title="Undo (Ctrl+Z)">
disabled={undoCount === 0} <Undo2 size={17} className="text-gray-600 hover:text-black transition-colors" />
className="weekly-btn-icon disabled:opacity-30 disabled:cursor-default"
title="Undo (Ctrl+Z)"
>
<Undo2 size={18} className="text-gray-600 hover:text-black transition-colors" />
</button> </button>
<button <button onClick={handleRedo} disabled={redoCount === 0} className="weekly-btn-icon disabled:opacity-30 disabled:cursor-default" title="Redo (Ctrl+Y)">
onClick={handleRedo} <Redo2 size={17} className="text-gray-600 hover:text-black transition-colors" />
disabled={redoCount === 0}
className="weekly-btn-icon disabled:opacity-30 disabled:cursor-default"
title="Redo (Ctrl+Y)"
>
<Redo2 size={18} className="text-gray-600 hover:text-black transition-colors" />
</button> </button>
{/* Add Event Button */} <button onClick={() => { const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); }} className="weekly-btn-icon" title="Add Calendar Event">
<button <Plus size={17} className="text-gray-600 hover:text-black transition-colors" />
onClick={() => {
const now = new Date();
setCalendarEventModal({
isOpen: true,
event: undefined,
initialDate: now,
initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00`,
});
}}
className="weekly-btn-icon"
title="Add Calendar Event"
>
<Plus
size={18}
className="text-gray-600 hover:text-black transition-colors"
/>
</button> </button>
<button <button onClick={() => { const newVal = !showNextTask; setShowNextTask(newVal); saveSetting("showNextTask", newVal); }} className={`weekly-btn-icon ${showNextTask ? "active" : ""}`} title={showNextTask ? "Showing Next Task" : "Showing Goal"}>
onClick={() => { {showNextTask ? <Play size={17} className="text-teal-600" /> : <Target size={17} className="text-gray-400" />}
const newVal = !showNextTask;
setShowNextTask(newVal);
saveSetting("showNextTask", newVal);
}}
className={`weekly-btn-icon ${showNextTask ? "active" : ""}`}
title={showNextTask ? "Showing Next Task" : "Showing Goal"}
>
{showNextTask ? (
<Play size={18} className="text-teal-600" />
) : (
<Target size={18} className="text-gray-400" />
)}
</button> </button>
<button onClick={() => setShowFocusMode(true)} className="weekly-btn-icon" title="Enter Focus Mode">
{/* Focus Mode Toggle */} <Zap size={17} className="text-gray-600 hover:text-yellow-500 transition-colors" />
<button
onClick={() => setShowFocusMode(true)}
className="weekly-btn-icon"
title="Enter Focus Mode"
>
<Zap
size={18}
className="text-gray-600 hover:text-yellow-500 transition-colors"
/>
</button> </button>
<button onClick={() => setDarkMode(!darkMode)} className="weekly-btn-icon" title={darkMode ? "Light Mode" : "Dark Mode"}>
{/* Day/Night Mode Switch */} {darkMode ? <Sun size={17} className="text-yellow-500" /> : <Moon size={17} className="text-gray-500" />}
<button
onClick={() => setDarkMode(!darkMode)}
className="p-1.5 rounded-md hover:bg-gray-100 transition-colors"
title={darkMode ? "Switch to Light Mode" : "Switch to Dark Mode"}
>
{darkMode ? (
<Sun size={18} className="text-yellow-500" />
) : (
<Moon size={18} className="text-gray-500" />
)}
</button>
{/* Navigation Controls */}
<div className="flex items-center bg-gray-100 rounded-lg p-0.5">
<button
className="p-1 hover:bg-white hover:shadow-sm rounded text-gray-500 hover:text-black transition-all"
onClick={goToPrevWeek}
title="Previous Week"
>
<ChevronsLeft size={16} />
</button>
<button
className="p-1 hover:bg-white hover:shadow-sm rounded text-gray-500 hover:text-black transition-all"
onClick={goToPrevDay}
title="Previous Day"
>
<ChevronLeft size={16} />
</button>
<button
className="px-3 py-1 text-xs font-bold text-gray-600 hover:text-black hover:bg-white hover:shadow-sm rounded transition-all"
onClick={goToToday}
title="Go to Today"
>
Today
</button>
<button
className="p-1 hover:bg-white hover:shadow-sm rounded text-gray-500 hover:text-black transition-all"
onClick={goToNextDay}
title="Next Day"
>
<ChevronLeft size={16} className="rotate-180" />
</button>
<button
className="p-1 hover:bg-white hover:shadow-sm rounded text-gray-500 hover:text-black transition-all"
onClick={goToNextWeek}
title="Next Week"
>
<ChevronsLeft size={16} className="rotate-180" />
</button> </button>
</div> </div>
{/* Navigation Controls — always visible */}
<div className="flex items-center bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5">
<button className="p-1 hover:bg-white dark:hover:bg-gray-700 hover:shadow-sm rounded text-gray-500 hover:text-black dark:hover:text-white transition-all" onClick={goToPrevWeek} title="Previous Week"><ChevronsLeft size={15} /></button>
<button className="p-1 hover:bg-white dark:hover:bg-gray-700 hover:shadow-sm rounded text-gray-500 hover:text-black dark:hover:text-white transition-all" onClick={goToPrevDay} title="Previous Day"><ChevronLeft size={15} /></button>
<button className="px-2 py-1 text-xs font-bold text-gray-600 dark:text-gray-400 hover:text-black dark:hover:text-white hover:bg-white dark:hover:bg-gray-700 hover:shadow-sm rounded transition-all" onClick={goToToday} title="Go to Today">Today</button>
<button className="p-1 hover:bg-white dark:hover:bg-gray-700 hover:shadow-sm rounded text-gray-500 hover:text-black dark:hover:text-white transition-all" onClick={goToNextDay} title="Next Day"><ChevronLeft size={15} className="rotate-180" /></button>
<button className="p-1 hover:bg-white dark:hover:bg-gray-700 hover:shadow-sm rounded text-gray-500 hover:text-black dark:hover:text-white transition-all" onClick={goToNextWeek} title="Next Week"><ChevronsLeft size={15} className="rotate-180" /></button>
</div>
{/* Search */} {/* Always visible: Search, Settings, User */}
<button <button className="weekly-btn-icon" onClick={() => setIsSearchOpen(true)} title="Search"><Search size={17} /></button>
className="p-1.5 hover:bg-gray-100 rounded-md text-gray-500 hover:text-black transition-colors" <button className="weekly-btn-icon" onClick={() => setShowSettings(true)} title="Settings"><Settings size={17} /></button>
onClick={() => setIsSearchOpen(true)}
title="Search"
>
<Search size={18} />
</button>
{/* Recurring Tasks */} {/* Overflow menu — visible on tablet, hidden on desktop */}
<button <div className="header-tablet-only" style={{ position: "relative" }}>
className="p-1.5 hover:bg-gray-100 rounded-md text-gray-500 hover:text-black transition-colors" <button className="weekly-btn-icon" onClick={() => setShowHeaderMore(!showHeaderMore)} title="More">
onClick={() => setIsRecurringTasksOpen(true)} <MoreVertical size={17} />
title="Recurring Tasks"
>
<Repeat size={18} />
</button> </button>
{showHeaderMore && (
<>
<div style={{ position: "fixed", inset: 0, zIndex: 99 }} onClick={() => setShowHeaderMore(false)} />
<div className="header-overflow-menu">
<button onClick={() => { handleUndo(); setShowHeaderMore(false); }} disabled={undoCount === 0}><Undo2 size={16} /> <span>Undo</span></button>
<button onClick={() => { handleRedo(); setShowHeaderMore(false); }} disabled={redoCount === 0}><Redo2 size={16} /> <span>Redo</span></button>
<button onClick={() => { const now = new Date(); setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); setShowHeaderMore(false); }}><Plus size={16} /> <span>{language === "de" ? "Termin erstellen" : "Add Event"}</span></button>
<button onClick={() => { setIsRecurringTasksOpen(true); setShowHeaderMore(false); }}><Repeat size={16} /> <span>{language === "de" ? "Wiederkehrend" : "Recurring"}</span></button>
<button onClick={() => { const nv = !showNextTask; setShowNextTask(nv); saveSetting("showNextTask", nv); setShowHeaderMore(false); }}>{showNextTask ? <Play size={16} /> : <Target size={16} />} <span>{showNextTask ? "Next Task" : "Goal"}</span></button>
<button onClick={() => { setShowFocusMode(true); setShowHeaderMore(false); }}><Zap size={16} /> <span>{language === "de" ? "Fokus" : "Focus"}</span></button>
<button onClick={() => { setDarkMode(!darkMode); setShowHeaderMore(false); }}>{darkMode ? <Sun size={16} /> : <Moon size={16} />} <span>{darkMode ? "Light" : "Dark"}</span></button>
</div>
</>
)}
</div>
<button {/* Recurring Tasks — desktop only */}
className="p-1.5 hover:bg-gray-100 rounded-md text-gray-500 hover:text-black transition-colors" <button className="weekly-btn-icon header-desktop-only" onClick={() => setIsRecurringTasksOpen(true)} title="Recurring Tasks"><Repeat size={17} /></button>
onClick={() => setShowSettings(true)}
title="Settings"
>
<Settings size={18} />
</button>
{/* User Menu */} {/* User Menu */}
<UserMenu <UserMenu
userEmail={session?.user?.email} userEmail={session?.user?.email}
onOpenSettings={() => setShowSettings(true)} onOpenSettings={() => setShowSettings(true)}
language={profile.language} language={profile.language}
trigger={ trigger={<button className="weekly-btn-icon" title="User Menu"><User size={17} /></button>}
<button
className="p-1.5 hover:bg-gray-100 rounded-md text-gray-500 hover:text-black transition-colors"
title="User Menu"
>
<User size={18} />
</button>
}
/> />
</div> </div>
</header> </header>