From 6f26e1117c85a51a53f983fcdce839062bc7f7b5 Mon Sep 17 00:00:00 2001 From: Hammer Date: Wed, 28 Jan 2026 19:53:02 +0000 Subject: [PATCH] feat: mobile-responsive layout with collapsible sidebar --- dist/index.html | 4 +- src/components/Layout.tsx | 36 ++++++-- src/components/Sidebar.tsx | 155 ++++++++++++++++++++++++++++------ src/components/TaskDetail.tsx | 2 +- src/index.css | 4 + src/pages/Admin.tsx | 10 +-- src/pages/Project.tsx | 4 +- 7 files changed, 173 insertions(+), 42 deletions(-) diff --git a/dist/index.html b/dist/index.html index c9f6db00..9e15ea1a 100644 --- a/dist/index.html +++ b/dist/index.html @@ -6,8 +6,8 @@ Todo App - - + +
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index a23fa950..a367d7d7 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,5 +1,6 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { Outlet, Navigate } from 'react-router-dom'; +import { Menu } from 'lucide-react'; import { Sidebar } from './Sidebar'; import { TaskDetail } from './TaskDetail'; import { useAuthStore } from '@/stores/auth'; @@ -8,6 +9,7 @@ import { useTasksStore } from '@/stores/tasks'; export function Layout() { const { isAuthenticated, isLoading } = useAuthStore(); const { fetchProjects, fetchLabels, selectedTask, setSelectedTask } = useTasksStore(); + const [sidebarOpen, setSidebarOpen] = useState(false); useEffect(() => { if (isAuthenticated) { @@ -33,10 +35,34 @@ export function Layout() { return (
- -
- -
+ {/* Desktop sidebar — always visible on md+ */} +
+ +
+ + {/* Mobile sidebar — overlay drawer */} + setSidebarOpen(false)} + /> + +
+ {/* Mobile top bar */} +
+ +

Todo App

+
+ +
+ +
+
+ {selectedTask && ( void; +} + +export function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) { const location = useLocation(); const navigate = useNavigate(); const { user, logout } = useAuthStore(); @@ -36,6 +42,9 @@ export function Sidebar() { }); const newProjectInputRef = useRef(null); + // Determine if this is the mobile overlay instance + const isMobileInstance = mobileOpen !== undefined; + useEffect(() => { if (showNewProject && newProjectInputRef.current) { newProjectInputRef.current.focus(); @@ -50,6 +59,13 @@ export function Sidebar() { } catch {} }; + const handleNavClick = () => { + // Close mobile sidebar when a nav item is clicked + if (isMobileInstance && onMobileClose) { + onMobileClose(); + } + }; + const handleCreateProject = async () => { if (!newProjectName.trim() || isCreatingProject) return; setIsCreatingProject(true); @@ -59,6 +75,7 @@ export function Sidebar() { setNewProjectColor(PROJECT_COLORS[0]); setShowNewProject(false); navigate(`/project/${project.id}`); + handleNavClick(); } catch (error) { console.error('Failed to create project:', error); } finally { @@ -86,7 +103,75 @@ export function Sidebar() { { path: '/upcoming', icon: CalendarDays, label: 'Upcoming', color: '#8b5cf6' }, ]; - // Collapsed sidebar + // Mobile overlay instance + if (isMobileInstance) { + return ( + <> + {/* Backdrop */} +
+ {/* Drawer */} + + + ); + } + + // Desktop: Collapsed sidebar if (isCollapsed) { return ( ); } - + + // Desktop: Expanded sidebar return ( + ); + + // Shared nav content for both desktop expanded and mobile drawer + function renderNavContent() { + return ( + <> {/* Main nav items */}
{navItems.map((item) => ( e.stopPropagation()} + onClick={(e) => { + e.stopPropagation(); + handleNavClick(); + }} > @@ -351,6 +471,7 @@ export function Sidebar() { )}
- - - {/* Bottom section */} -
- {user?.role === 'admin' && ( - - - Admin - - )} - -
- - ); + + ); + } } diff --git a/src/components/TaskDetail.tsx b/src/components/TaskDetail.tsx index c9a8b528..a0b3784c 100644 --- a/src/components/TaskDetail.tsx +++ b/src/components/TaskDetail.tsx @@ -145,7 +145,7 @@ export function TaskDetail({ task, onClose }: TaskDetailProps) { {/* Panel */}
{/* Header */}
diff --git a/src/index.css b/src/index.css index 27dac5f6..319e7c38 100644 --- a/src/index.css +++ b/src/index.css @@ -1,6 +1,10 @@ @import "tailwindcss"; +/* Disable automatic dark mode - force light mode */ +@custom-variant dark (&:is(.dark *)); + :root { + color-scheme: light; --color-primary: #3b82f6; --color-primary-dark: #2563eb; --color-danger: #ef4444; diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index bdd87ec2..5e65c08d 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -173,8 +173,8 @@ export function AdminPage() {
Loading...
) : activeTab === 'users' ? ( /* Users list */ -
- +
+
@@ -311,7 +311,7 @@ export function AdminPage() { {inviteError && (

{inviteError}

)} -
+
-
Name
+
+
diff --git a/src/pages/Project.tsx b/src/pages/Project.tsx index 241f883d..28efcefc 100644 --- a/src/pages/Project.tsx +++ b/src/pages/Project.tsx @@ -98,8 +98,8 @@ export function ProjectPage() { return (
{/* Header */} -
-
+
+
Name