From d28a02c5b50095a84af231c6eb1c3d08b7cc637b Mon Sep 17 00:00:00 2001 From: mARTin Date: Sat, 14 Mar 2026 23:39:17 +0100 Subject: [PATCH] feat: optimize mobile UX with simplified header and quick actions Replace crowded 7-element mobile header with clean 3-element layout: hamburger menu (left), tappable date display (center), search (right). All touch targets are 44px+ per Apple HIG. Move navigation, quick actions, view controls, and utilities into a new mobile quick actions panel at the top of the settings sidebar. Add safe area insets for notch and home indicator on iOS devices. v1.34.0 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/app/globals.css | 124 +++++++----- src/components/WeeklyView.tsx | 357 ++++++++++++++++++++-------------- 3 files changed, 294 insertions(+), 189 deletions(-) diff --git a/package.json b/package.json index b7948e9..f48e4eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.33.8", + "version": "1.34.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/app/globals.css b/src/app/globals.css index 1431119..ff4fec8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3847,65 +3847,99 @@ h3 { } /* --- Mobile overflow menu --- */ -.mobile-overflow-menu { - position: absolute; - right: 0; - top: 100%; - margin-top: 4px; - background: white; - border: 1px solid #e5e7eb; - border-radius: 12px; - box-shadow: 0 8px 30px rgba(0,0,0,0.15); - z-index: 1000; - padding: 8px; - min-width: 220px; - max-height: 70vh; - overflow-y: auto; -} -.dark .mobile-overflow-menu { - background: #1f2937; - border-color: #374151; -} -.mobile-overflow-menu button, -.mobile-overflow-menu .mobile-menu-item { +/* --- Mobile Header (simplified 3-element layout) --- */ +.mobile-header { display: flex; align-items: center; - gap: 10px; + justify-content: space-between; + padding: 4px 12px; + padding-top: calc(4px + env(safe-area-inset-top, 0px)); + min-height: 48px; + gap: 8px; + position: sticky; + top: 0; + z-index: 100; +} +.mobile-header-btn { + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 10px; + border: none; + background: transparent; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} +.mobile-header-btn:active { + background: rgba(0, 0, 0, 0.06); + transform: scale(0.92); +} +.dark-mode .mobile-header-btn:active { + background: rgba(255, 255, 255, 0.08); +} +.mobile-header-date { + flex: 1; + text-align: center; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + border: none; + background: transparent; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + white-space: nowrap; +} +.mobile-header-date:active { + opacity: 0.7; +} + +/* --- Mobile Quick Actions in Sidebar --- */ +.mobile-quick-nav-btn { + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 10px; + border: none; + background: var(--weekly-settings-toggle-bg, #f3f4f6); + color: var(--weekly-text, #333); + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} +.mobile-quick-nav-btn:active { + background: var(--weekly-teal, #0ea5e9); + color: #fff; + transform: scale(0.92); +} +.mobile-quick-action-btn { + display: flex; + align-items: center; + gap: 12px; width: 100%; + min-height: 48px; padding: 10px 12px; border: none; background: transparent; - border-radius: 8px; - font-size: 0.85rem; - color: #374151; + border-radius: 10px; + font-size: 0.95rem; + color: var(--weekly-text, #374151); cursor: pointer; text-align: left; + -webkit-tap-highlight-color: transparent; } -.dark .mobile-overflow-menu button, -.dark .mobile-overflow-menu .mobile-menu-item { - color: #e5e7eb; -} -.mobile-overflow-menu button:active, -.mobile-overflow-menu .mobile-menu-item:active { - background: #f3f4f6; -} -.dark .mobile-overflow-menu button:active, -.dark .mobile-overflow-menu .mobile-menu-item:active { - background: #374151; -} -.mobile-menu-divider { - height: 1px; - background: #e5e7eb; - margin: 4px 8px; -} -.dark .mobile-menu-divider { - background: #374151; +.mobile-quick-action-btn:active { + background: var(--weekly-settings-toggle-bg, #f3f4f6); } /* --- Floating Action Button --- */ .mobile-fab { position: fixed; - bottom: 24px; + bottom: calc(24px + env(safe-area-inset-bottom, 0px)); right: 24px; width: 56px; height: 56px; diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index b5324e0..8afe3af 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -20,6 +20,7 @@ import FocusModeOverlay from "./FocusModeOverlay"; import { LayoutGrid, Calendar, + ChevronDown, ChevronLeft, ChevronRight, ChevronsLeft, @@ -1737,10 +1738,10 @@ export default function WeeklyView() { // Mobile detection const [isMobile, setIsMobile] = useState(false); - const [showMobileMenu, setShowMobileMenu] = useState(false); + const [showMobileFabSheet, setShowMobileFabSheet] = useState(false); const [fabTaskTitle, setFabTaskTitle] = useState(""); - const mobileMenuRef = useRef(null); + const fabTextareaRef = useRef(null); // Moved state definitions to the top @@ -5533,153 +5534,51 @@ export default function WeeklyView() { {/* Mobile Header */} {isMobile && ( -
- {/* Left: Navigation */} -
- - - - - -
+
+ {/* Left: Menu button */} + - {/* Center: Week info */} -
setShowDatePicker(true)} style={{ fontFamily: profile.cwFontFamily || "Inter", + fontSize: profile.cwFontSize || "0.9rem", fontWeight: Number(profile.cwFontWeight || "700"), color: adjustColorForDarkMode(profile.cwColor || (darkMode ? "#e5e7eb" : "#333333"), darkMode), }}> - {(isLoading || isSyncing || syncStatus === "syncing") ? ( -
- ) : syncError ? ( - - ) : null} -
- {profile.headerDisplay === "none" ? ( - null - ) : profile.headerDisplay === "date" ? ( - {(() => { - const today = new Date(); - const isTodayInWeek = getVisibleDays().some(d => - d.getDate() === today.getDate() && - d.getMonth() === today.getMonth() && - d.getFullYear() === today.getFullYear() - ); - const refDate = isTodayInWeek ? today : getCWReferenceDate(getVisibleDays()); - return refDate.toLocaleDateString(language, { day: '2-digit', month: '2-digit', year: 'numeric' }); - })()} - ) : profile.headerDisplay === "month_year" ? ( - {getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { month: 'long', year: 'numeric' })} - ) : profile.headerDisplay === "custom" ? ( - {formatCustomHeader(profile.headerCustomFormat || "KW WW | YYYY", getVisibleDays(), language, t)} - ) : ( - <> - KW{getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")} - {getCWReferenceDate(getVisibleDays()).getFullYear()} - - )} -
-
+ {(isLoading || isSyncing || syncStatus === "syncing") && ( +
+ )} + {syncError && } + + {profile.headerDisplay === "none" ? "" : + profile.headerDisplay === "date" ? (() => { + const today = new Date(); + const isTodayInWeek = getVisibleDays().some(d => + d.getDate() === today.getDate() && + d.getMonth() === today.getMonth() && + d.getFullYear() === today.getFullYear() + ); + const refDate = isTodayInWeek ? today : getCWReferenceDate(getVisibleDays()); + return refDate.toLocaleDateString(language, { day: '2-digit', month: '2-digit', year: 'numeric' }); + })() : + profile.headerDisplay === "month_year" ? + getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { month: 'long', year: 'numeric' }) : + profile.headerDisplay === "custom" ? + formatCustomHeader(profile.headerCustomFormat || "KW WW | YYYY", getVisibleDays(), language, t) : + profile.headerDisplay === "month" ? + getCWReferenceDate(getVisibleDays()).toLocaleDateString(language, { month: "long" }) : + `KW ${getWeekNumber(getCWReferenceDate(getVisibleDays())).toString().padStart(2, "0")} | ${getCWReferenceDate(getVisibleDays()).getFullYear()}` + } + + + - {/* Right: Settings + Overflow */} -
- -
- - {showMobileMenu && ( -
setShowMobileMenu(false)}> - - -
- - -
- - - -
-
- Days to show -
- {[1, 3, 5, 7].map((num) => ( - - ))} -
-
- {showTimeGrid && ( -
- Slot duration -
- {[15, 30, 60].map((d) => ( - - ))} -
-
- )} -
- - -
- -
- )} -
-
+ {/* Right: Search */} +
)} @@ -8204,6 +8103,35 @@ export default function WeeklyView() { saveKanbanStages={saveKanbanStages} profile={profile} setProfile={setProfile} + isMobile={isMobile} + mobileActions={isMobile ? { + goToPrevWeek, + goToPrevDay, + goToToday, + goToNextDay, + goToNextWeek, + onJumpToDate: () => setShowDatePicker(true), + onAddCalendarEvent: () => { + const now = new Date(); + setCalendarEventModal({ isOpen: true, event: undefined, initialDate: now, initialStartTime: `${String(now.getHours()).padStart(2, "0")}:00` }); + }, + onRecurringTasks: () => setIsRecurringTasksOpen(true), + onToggleNextTask: () => { const newVal = !showNextTask; setShowNextTask(newVal); saveSetting("showNextTask", newVal); }, + onFocusMode: () => setShowFocusMode(true), + onToggleDarkMode: () => setDarkMode(!darkMode), + onUndo: handleUndo, + onRedo: handleRedo, + onRefresh: () => { fetchCalendarEvents(true); fetchTasks(); }, + darkMode, + showNextTask, + undoCount, + redoCount, + viewDays, + onViewDaysChange: (num: number) => { setViewDays(num); savedViewDaysRef.current = num; saveSetting("viewDays", num); }, + showTimeGrid, + cellDuration, + onCellDurationChange: (d: CellDuration) => { setCellDuration(d); saveSetting("cellDuration", d); }, + } : undefined} /> ) } @@ -9480,6 +9408,33 @@ interface SettingsSidebarProps { onProjectsChanged: () => void; kanbanStages: KanbanStage[]; saveKanbanStages: (stages: KanbanStage[]) => Promise; + // Mobile quick actions + isMobile?: boolean; + mobileActions?: { + goToPrevWeek: () => void; + goToPrevDay: () => void; + goToToday: () => void; + goToNextDay: () => void; + goToNextWeek: () => void; + onJumpToDate: () => void; + onAddCalendarEvent: () => void; + onRecurringTasks: () => void; + onToggleNextTask: () => void; + onFocusMode: () => void; + onToggleDarkMode: () => void; + onUndo: () => void; + onRedo: () => void; + onRefresh: () => void; + darkMode: boolean; + showNextTask: boolean; + undoCount: number; + redoCount: number; + viewDays: number; + onViewDaysChange: (days: number) => void; + showTimeGrid: boolean; + cellDuration: CellDuration; + onCellDurationChange: (d: CellDuration) => void; + }; } // Notes Sidebar Component interface NotesSidebarProps { @@ -9702,6 +9657,8 @@ function SettingsSidebar({ saveKanbanStages, profile, setProfile, + isMobile: isMobileSidebar, + mobileActions, }: SettingsSidebarProps) { const [activeTab, setActiveTab] = useState< "calendar" | "general" | "account" | "styling" | "motivation" | "about" | "localisation" @@ -10162,6 +10119,120 @@ function SettingsSidebar({
+ {/* Mobile Quick Actions Panel */} + {isMobileSidebar && mobileActions && ( +
+ {/* Navigation Row */} +
+ + + + + +
+ + {/* Quick Action Buttons */} +
+ + + + + + +
+ + {/* View Controls */} +
+
+ {t.days || "Days"} +
+ {[1, 3, 5, 7].map((num) => ( + + ))} +
+
+ {mobileActions.showTimeGrid && ( +
+ {t.slot || "Slot"} +
+ {[15, 30, 60].map((d) => ( + + ))} +
+
+ )} +
+ + {/* Utility Row */} +
+ + + +
+
+ )} +