From 8fc778cf3056b675844c87da649876dfd8ae4d92 Mon Sep 17 00:00:00 2001 From: Ashish Huddar Date: Tue, 24 Mar 2026 00:55:37 +0530 Subject: [PATCH] chore(web): add mobile-responsive layout for dashboard and session views Add proper mobile breakpoints (768px, 480px) with structural layout changes: - Sidebar: auto-hide on mobile with hamburger toggle and overlay drawer - Dashboard hero: stack stats cards and controls vertically on mobile - Kanban board: stack columns vertically instead of horizontal scroll - Session cards: flexible height with 44px minimum touch targets - Session detail: responsive header metadata and full-width terminal - Global: phone-specific breakpoint (480px) for single-column layouts Desktop layout remains completely unchanged. Closes #633 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/web/src/app/globals.css | 242 ++++++++++++++++++ packages/web/src/components/Dashboard.tsx | 25 ++ .../web/src/components/ProjectSidebar.tsx | 13 +- 3 files changed, 278 insertions(+), 2 deletions(-) diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css index b592cdb0a..6bb52a6e8 100644 --- a/packages/web/src/app/globals.css +++ b/packages/web/src/app/globals.css @@ -1874,3 +1874,245 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { min-height: 244px; } } + +/* ── Mobile hamburger toggle ─────────────────────────────────────────── */ + +.mobile-menu-toggle { + display: none; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + flex-shrink: 0; + border: 1px solid var(--color-border-default); + background: var(--color-bg-surface); + color: var(--color-text-secondary); + cursor: pointer; + transition: + border-color 0.12s ease, + background 0.12s ease, + color 0.12s ease; +} + +.mobile-menu-toggle:hover { + border-color: var(--color-border-strong); + background: var(--color-hover-overlay); + color: var(--color-text-primary); +} + +/* ── Mobile breakpoint — 768px (tablet / landscape phone) ────────────── */ + +@media (max-width: 767px) { + /* -- Hamburger button visible -- */ + .mobile-menu-toggle { + display: inline-flex; + } + + /* -- Sidebar: off-canvas drawer -- */ + .project-sidebar, + .project-sidebar.project-sidebar--collapsed { + position: fixed; + top: 0; + left: 0; + z-index: var(--z-overlay); + height: 100vh; + height: 100dvh; + width: 280px; + transform: translateX(-100%); + transition: transform 0.25s ease; + } + + .project-sidebar--mobile-open { + transform: translateX(0); + } + + .sidebar-mobile-backdrop { + position: fixed; + inset: 0; + z-index: calc(var(--z-overlay) - 1); + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); + -webkit-backdrop-filter: blur(2px); + } + + /* -- Dashboard main -- */ + .dashboard-main { + padding-left: 12px; + padding-right: 12px; + padding-top: 12px; + padding-bottom: 12px; + } + + /* -- Hero section stacks vertically -- */ + .dashboard-hero__content { + flex-direction: column; + padding: 12px; + gap: 12px; + } + + .dashboard-hero__primary { + flex-direction: column; + flex-basis: auto; + gap: 12px; + } + + .dashboard-stat-cards { + width: 100%; + margin-left: 0; + } + + .dashboard-hero__meta { + width: 100%; + margin-left: 0; + } + + /* -- Kanban board: stack columns vertically -- */ + .kanban-board { + flex-direction: column; + height: auto; + min-height: auto; + overflow-x: visible; + gap: 12px; + } + + .kanban-column { + flex: none; + width: 100%; + min-width: 0; + max-width: none; + } + + .kanban-column-body { + max-height: 400px; + } + + .board-section-head { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .board-section-head__legend { + flex-wrap: wrap; + } + + /* -- Session cards: flexible height + touch targets -- */ + .session-card--fixed { + height: auto; + min-height: 180px; + } + + .session-card--alert-frame { + min-height: 220px; + } + + .session-card--merge-frame { + min-height: 200px; + } + + .session-card__control, + .session-card__merge-control, + .session-card__terminate, + .done-restore-btn { + min-height: 44px; + min-width: 44px; + } + + .session-card__footer { + min-height: 44px; + } + + .session-card__footer a, + .session-card__footer button { + min-height: 44px; + display: inline-flex; + align-items: center; + } + + /* -- Session detail page -- */ + .session-detail-page .mx-auto { + padding-left: 12px; + padding-right: 12px; + } + + .session-page-header { + padding: 12px; + } + + .session-page-header__meta { + flex-direction: column; + gap: 6px; + } + + .session-page-header__crumbs { + font-size: 10px; + } + + .session-detail-link-pill { + min-height: 36px; + } + + /* -- Touch targets for interactive elements -- */ + .orchestrator-btn, + .board-legend-item { + min-height: 44px; + } + + .project-sidebar__item { + min-height: 44px; + display: flex; + align-items: center; + } + + .project-sidebar__session { + min-height: 44px; + display: flex; + align-items: center; + } + + /* -- PR table: horizontal scroll on mobile -- */ + .mx-auto.max-w-\[900px\] { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + /* -- Alert banners -- */ + .dashboard-alert { + font-size: 11px; + padding: 10px 12px; + } +} + +/* ── Phone breakpoint — 480px ────────────────────────────────────────── */ + +@media (max-width: 480px) { + .dashboard-title { + font-size: 20px; + } + + .dashboard-stat-cards { + flex-direction: column; + } + + .dashboard-stat-card { + width: 100%; + } + + .kanban-column-body { + max-height: 300px; + } + + .kanban-column__title { + font-size: 16px; + } + + .session-page-header h1 { + font-size: 15px; + } + + /* Session detail terminal full-width */ + .session-detail-page .mx-auto { + padding-left: 8px; + padding-right: 8px; + } +} diff --git a/packages/web/src/components/Dashboard.tsx b/packages/web/src/components/Dashboard.tsx index b30cf7bde..ad67cd9ce 100644 --- a/packages/web/src/components/Dashboard.tsx +++ b/packages/web/src/components/Dashboard.tsx @@ -70,6 +70,7 @@ export function Dashboard({ const [spawningProjectIds, setSpawningProjectIds] = useState([]); const [spawnErrors, setSpawnErrors] = useState>({}); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const showSidebar = projects.length > 1; const allProjectsView = showSidebar && projectId === undefined; @@ -82,6 +83,10 @@ export function Dashboard({ setActiveOrchestrators((current) => mergeOrchestrators(current, orchestratorLinks)); }, [orchestratorLinks]); + useEffect(() => { + setMobileMenuOpen(false); + }, [searchParams]); + const grouped = useMemo(() => { const zones: Record = { merge: [], @@ -267,6 +272,8 @@ export function Dashboard({ activeSessionId={activeSessionId} collapsed={sidebarCollapsed} onToggleCollapsed={() => setSidebarCollapsed((current) => !current)} + mobileOpen={mobileMenuOpen} + onMobileClose={() => setMobileMenuOpen(false)} /> )}
@@ -274,6 +281,24 @@ export function Dashboard({
+ {showSidebar && ( + + )}
diff --git a/packages/web/src/components/ProjectSidebar.tsx b/packages/web/src/components/ProjectSidebar.tsx index c0735fe82..8d7d830e0 100644 --- a/packages/web/src/components/ProjectSidebar.tsx +++ b/packages/web/src/components/ProjectSidebar.tsx @@ -15,6 +15,8 @@ interface ProjectSidebarProps { activeSessionId: string | undefined; collapsed?: boolean; onToggleCollapsed?: () => void; + mobileOpen?: boolean; + onMobileClose?: () => void; } type ProjectHealth = "red" | "yellow" | "green" | "gray"; @@ -96,6 +98,8 @@ function ProjectSidebarInner({ activeSessionId, collapsed = false, onToggleCollapsed, + mobileOpen = false, + onMobileClose, }: ProjectSidebarProps) { const router = useRouter(); const pathname = usePathname(); @@ -156,7 +160,7 @@ function ProjectSidebarInner({ if (collapsed) { return ( -