diff --git a/apps/web/src/components/app-shell.tsx b/apps/web/src/components/app-shell.tsx index 3f6e311..9455df1 100644 --- a/apps/web/src/components/app-shell.tsx +++ b/apps/web/src/components/app-shell.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect } from "react"; import { Link, NavLink, Outlet, useLocation } from "react-router-dom"; import { getUserSessions } from "../api/sessions"; @@ -8,6 +8,7 @@ import { useAuth } from "../state/auth"; import { useSessions } from "../state/sessions"; import { useMobileViewport } from "../hooks/use-mobile-viewport"; import { Icon } from "./icon"; +import { MobileNav } from "./mobile-nav"; import type { IconName } from "../utils/icons"; const NAV_ITEMS: { to: string; label: string; icon: IconName; badge?: "sessions" }[] = [ @@ -43,8 +44,6 @@ export const AppShell = () => { const { sessions, setAllSessions } = useSessions(); const location = useLocation(); const isMobile = useMobileViewport(); - const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - const isMobileTerminal = isMobile && location.pathname.includes("/instances/") && location.pathname.includes("/terminal"); const loadSessions = useCallback(async () => { @@ -65,11 +64,6 @@ export const AppShell = () => { return () => clearInterval(interval); }, [loadSessions]); - // Close mobile menu on route change - useEffect(() => { - setMobileMenuOpen(false); - }, [location.pathname]); - if (isMobileTerminal) { return (
@@ -102,57 +96,46 @@ export const AppShell = () => {
- - - {isMobile && mobileMenuOpen && ( -
setMobileMenuOpen(false)} - /> + {!isMobile && ( + )} -
+
+ + {isMobile && ( + s.status === "running").length} /> + )}
); }; diff --git a/apps/web/src/components/mobile-action-sheet.tsx b/apps/web/src/components/mobile-action-sheet.tsx new file mode 100644 index 0000000..211d86a --- /dev/null +++ b/apps/web/src/components/mobile-action-sheet.tsx @@ -0,0 +1,88 @@ +import { useEffect, useRef } from "react"; +import { Icon } from "./icon"; +import type { IconName } from "./icon"; + +export interface MobileActionSheetItem { + id: string; + label: string; + icon?: IconName; + variant?: "default" | "danger"; + onClick: () => void; +} + +interface MobileActionSheetProps { + isOpen: boolean; + onClose: () => void; + title: string; + actions: MobileActionSheetItem[]; +} + +export function MobileActionSheet({ + isOpen, + onClose, + title, + actions, +}: MobileActionSheetProps) { + const sheetRef = useRef(null); + + useEffect(() => { + if (isOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + return () => { + document.body.style.overflow = ""; + }; + }, [isOpen]); + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape" && isOpen) { + onClose(); + } + }; + document.addEventListener("keydown", handleEscape); + return () => document.removeEventListener("keydown", handleEscape); + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()} + > +
+
+

{title}

+
+
+ {actions.map((action) => ( + + ))} +
+ +
+
+ ); +} diff --git a/apps/web/src/components/mobile-nav.tsx b/apps/web/src/components/mobile-nav.tsx new file mode 100644 index 0000000..0600504 --- /dev/null +++ b/apps/web/src/components/mobile-nav.tsx @@ -0,0 +1,40 @@ +import { NavLink } from "react-router-dom"; +import { Icon } from "./icon"; +import type { IconName } from "../utils/icons"; + +interface MobileNavProps { + sessionCount?: number; +} + +const MOBILE_NAV_ITEMS: { to: string; label: string; icon: IconName }[] = [ + { to: "/", label: "Home", icon: "dashboard" }, + { to: "/projects", label: "Projects", icon: "projects" }, + { to: "/sessions", label: "Sessions", icon: "terminal" }, + { to: "/tool-workshop", label: "Tools", icon: "settings" }, + { to: "/settings", label: "Settings", icon: "settings" }, +]; + +export const MobileNav: React.FC = ({ sessionCount }) => { + return ( + + ); +}; diff --git a/apps/web/src/components/mobile-page-header.tsx b/apps/web/src/components/mobile-page-header.tsx new file mode 100644 index 0000000..993b102 --- /dev/null +++ b/apps/web/src/components/mobile-page-header.tsx @@ -0,0 +1,33 @@ +import { useNavigate } from "react-router-dom"; +import { useMobileViewport } from "../hooks/use-mobile-viewport"; +import { Icon } from "./icon"; + +interface MobilePageHeaderProps { + title: string; + showBack?: boolean; + actions?: React.ReactNode; +} + +export function MobilePageHeader({ title, showBack = true, actions }: MobilePageHeaderProps) { + const navigate = useNavigate(); + const isMobile = useMobileViewport(); + + if (!isMobile) return null; + + return ( +
+ {showBack && ( + + )} +

{title}

+ {actions &&
{actions}
} +
+ ); +} diff --git a/apps/web/src/components/session-card.tsx b/apps/web/src/components/session-card.tsx index b2ac9dc..d969d92 100644 --- a/apps/web/src/components/session-card.tsx +++ b/apps/web/src/components/session-card.tsx @@ -1,6 +1,9 @@ import { useState } from "react"; import type { Session } from "../api/sessions"; import { Icon } from "./icon"; +import { useMobileViewport } from "../hooks/use-mobile-viewport"; +import { MobileActionSheet } from "./mobile-action-sheet"; +import type { IconName } from "./icon"; export interface SessionCardProps { session: Session; @@ -45,6 +48,8 @@ export function SessionCard({ }: SessionCardProps) { const [showStopConfirm, setShowStopConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [showActionSheet, setShowActionSheet] = useState(false); + const isMobile = useMobileViewport(); const status = statusConfig[session.status] || { color: "gray", label: session.status }; const isTerminalOnly = session.tool_type_interfaces?.includes("terminal") && !session.tool_type_interfaces?.includes("web"); @@ -123,118 +128,211 @@ export function SessionCard({
-
- {isActive && ( - <> - {session.url ? ( - - - Open - - ) : ( - - )} - - {hasTunnelError && onRecreateTunnel && ( - - )} - - {showStopConfirm ? ( -
- Stop? + {isMobile ? ( +
+ {isActive && ( + <> + {session.url ? ( + + + Open + + ) : ( + )} + + + )} + {!isActive && onStart && ( + + )} + {!isActive && ( + + )} +
+ ) : ( +
+ {isActive && ( + <> + {session.url ? ( + + + Open + + ) : ( + + )} + + {hasTunnelError && onRecreateTunnel && ( + + )} + + {showStopConfirm ? ( +
+ Stop? + + +
+ ) : ( + - -
- ) : ( + )} + + )} + + {!isActive && onStart && ( + + )} + + {showDeleteConfirm ? ( +
+ Delete? - )} - - )} - - {!isActive && onStart && ( - - )} - - {showDeleteConfirm ? ( -
- Delete? + +
+ ) : ( - -
- ) : ( - - )} -
+ )} +
+ )} + + setShowActionSheet(false)} + title={session.display_name} + actions={[ + ...(isActive && hasTunnelError && onRecreateTunnel + ? [{ + id: "tunnel", + label: "Recreate Tunnel", + icon: "refresh" as IconName, + onClick: () => onRecreateTunnel(session), + }] + : []), + ...(isActive && onStop + ? [{ + id: "stop", + label: "Stop", + icon: "stop" as IconName, + variant: "danger" as const, + onClick: () => onStop(session), + }] + : []), + ...(onDelete + ? [{ + id: "delete", + label: "Delete", + icon: "delete" as IconName, + variant: "danger" as const, + onClick: () => onDelete(session), + }] + : []), + ]} + /> ); } diff --git a/apps/web/src/components/special-keys-strip.tsx b/apps/web/src/components/special-keys-strip.tsx index 9b4467e..cae6f80 100644 --- a/apps/web/src/components/special-keys-strip.tsx +++ b/apps/web/src/components/special-keys-strip.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { getSequenceWithModifier, type SpecialKey, type ModifierKey, KEY_SEQUENCES } from "../hooks/use-special-keys"; +import { getSequenceWithModifier, type SpecialKey, type ModifierKey } from "../hooks/use-special-keys"; interface SpecialKeysStripProps { onSend: (data: string) => void; diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index 53cf2c7..dd260d6 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -241,7 +241,6 @@ export const TerminalComponent: React.FC = ({ term.loadAddon(new WebLinksAddon()); const container = terminalRef.current; - let ws: WebSocket; // Define fitTerminal before connectWebSocket so it's available in onmessage const fitTerminal = () => { @@ -272,7 +271,7 @@ export const TerminalComponent: React.FC = ({ // Open xterm first (must happen before fit) term.open(container); - ws = connectWebSocket(); + const ws = connectWebSocket(); // Initial fit after layout settles (terminal must be opened first) let fitAttempts = 0; diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 715251f..b2f9afb 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -173,6 +173,116 @@ a { height: 100%; } +/* Mobile Navigation */ +.mobile-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: space-around; + align-items: center; + background: var(--panel); + border-top: 1px solid var(--border); + padding-bottom: env(safe-area-inset-bottom, 0); + z-index: 100; + height: 64px; +} + +.mobile-nav-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + padding: 8px 12px; + color: var(--muted); + text-decoration: none; + font-size: 11px; + min-width: 64px; + min-height: 44px; + border-radius: 8px; + transition: color 0.15s ease; +} + +.mobile-nav-item.active { + color: var(--brand); +} + +.mobile-nav-icon-wrapper { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.mobile-nav-badge { + position: absolute; + top: -6px; + right: -8px; + background: var(--danger); + color: white; + font-size: 10px; + font-weight: 600; + min-width: 16px; + height: 16px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 4px; +} + +/* Mobile Page Header */ +.mobile-page-header { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2) 0; + margin-bottom: var(--space-3); + border-bottom: 1px solid var(--border); +} + +.mobile-page-header-back { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background: transparent; + border: none; + color: var(--text); + cursor: pointer; + border-radius: 8px; + padding: 0; +} + +.mobile-page-header-back:active { + background: var(--color-border); +} + +.mobile-page-header h1 { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + flex: 1; +} + +.mobile-page-header-actions { + display: flex; + gap: var(--space-2); +} + +.mobile-nav-label { + font-size: 11px; + font-weight: 500; +} + +/* Mobile shell content padding adjustment */ +.shell-content.mobile { + padding-bottom: calc(1.25rem + 64px); +} + .eyebrow { margin: 0; font-size: var(--font-size-xs); @@ -419,6 +529,32 @@ a { flex-direction: column; align-items: flex-start; } + + /* Ensure minimum touch targets on mobile */ + button, + a, + input, + select, + textarea, + [role="button"] { + min-height: 44px; + min-width: 44px; + } + + /* Active states for touch feedback */ + button:active, + a:active, + [role="button"]:active { + opacity: 0.8; + transform: scale(0.98); + } + + /* Remove transform on buttons that shouldn't scale */ + .mobile-nav-item:active, + .mobile-action-sheet-button:active, + .mobile-action-sheet-cancel:active { + transform: none; + } } .project-list { @@ -538,6 +674,18 @@ a { gap: 0.6rem; } +@media (max-width: 767px) { + .dialog-actions { + flex-direction: column-reverse; + gap: 0.5rem; + } + + .dialog-actions button { + width: 100%; + min-height: 44px; + } +} + .keys-list { display: flex; flex-direction: column; @@ -1072,6 +1220,17 @@ a { width: 100%; flex-wrap: wrap; } + + .workspace-main { + padding: var(--space-2); + } + + .tree-entry { + padding: 0.5rem; + min-height: 44px; + display: flex; + align-items: center; + } } .sidebar-section { @@ -1308,6 +1467,27 @@ a { overflow: auto; } +@media (max-width: 767px) { + .modal-content { + min-width: auto; + width: calc(100% - 2rem); + max-width: 100%; + margin: 1rem; + padding: 1rem; + max-height: calc(100vh - 2rem); + } + + .modal-actions { + flex-direction: column-reverse; + gap: 0.5rem; + } + + .modal-actions button { + width: 100%; + min-height: 44px; + } +} + .modal-content h2 { margin: 0 0 1rem 0; font-size: 1.25rem; @@ -2699,6 +2879,39 @@ a.nav-item, margin-bottom: var(--space-6); } +/* Mobile Sessions Page */ +@media (max-width: 767px) { + .sessions-page { + padding: var(--space-2); + } + + .sessions-page .page-header { + margin-bottom: var(--space-3); + } + + .sessions-page .page-header h1 { + font-size: 1.25rem; + } + + .last-session-section { + margin-bottom: var(--space-4); + } + + .last-session-section h2 { + font-size: 1rem; + margin-bottom: var(--space-2); + } + + .create-session-section { + padding: var(--space-3); + } + + .create-session-section h2 { + font-size: 1rem; + margin-bottom: var(--space-2); + } +} + .last-session-card { display: flex; justify-content: space-between; @@ -2888,6 +3101,29 @@ a.nav-item, .session-card-actions a { padding: var(--space-1); } + + .session-card-actions.mobile { + display: flex; + gap: var(--space-2); + padding-top: var(--space-3); + border-top: 1px solid var(--color-border); + } + + .session-card-actions.mobile .mobile-primary { + flex: 1; + justify-content: center; + min-height: 44px; + padding: var(--space-2) var(--space-3); + } + + .session-card-actions.mobile .mobile-more { + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-2); + } } .recent-sessions-section { @@ -3019,6 +3255,20 @@ a.nav-item, gap: var(--space-4); } +@media (max-width: 767px) { + .create-session-form .form-row { + grid-template-columns: 1fr; + gap: var(--space-3); + } + + .create-session-form input, + .create-session-form select, + .create-session-form textarea, + .create-session-form button { + min-height: 44px; + } +} + /* Workflow Step Styles */ .workflow-form { display: flex; @@ -3566,6 +3816,113 @@ a.nav-item, cursor: pointer; } +/* Mobile Action Sheet */ +.mobile-action-sheet-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 200; + display: flex; + align-items: flex-end; + justify-content: center; + animation: fadeIn 0.2s ease; +} + +.mobile-action-sheet { + background: var(--bg); + border-radius: 16px 16px 0 0; + width: 100%; + max-width: 500px; + max-height: 80vh; + overflow-y: auto; + animation: slideUp 0.3s ease; + padding-bottom: env(safe-area-inset-bottom, 0); +} + +.mobile-action-sheet-header { + padding: var(--space-3) var(--space-4) var(--space-2); + text-align: center; +} + +.mobile-action-sheet-handle { + width: 36px; + height: 4px; + background: var(--color-border); + border-radius: 2px; + margin: 0 auto var(--space-3); +} + +.mobile-action-sheet-header h3 { + margin: 0; + font-size: 1rem; + font-weight: 600; +} + +.mobile-action-sheet-actions { + display: flex; + flex-direction: column; + gap: 1px; + background: var(--color-border); + margin: 0 var(--space-2); + border-radius: 12px; + overflow: hidden; +} + +.mobile-action-sheet-button { + display: flex; + align-items: center; + gap: var(--space-3); + width: 100%; + padding: var(--space-3) var(--space-4); + background: var(--bg); + border: none; + color: var(--text); + font-size: 1rem; + cursor: pointer; + min-height: 56px; + text-align: left; +} + +.mobile-action-sheet-button.danger { + color: #cd3131; +} + +.mobile-action-sheet-button:active { + background: var(--color-border); +} + +.mobile-action-sheet-cancel { + display: block; + width: calc(100% - var(--space-4)); + margin: var(--space-3) var(--space-2); + padding: var(--space-3); + background: var(--bg); + border: 1px solid var(--color-border); + border-radius: 12px; + color: var(--text); + font-size: 1rem; + font-weight: 600; + cursor: pointer; + min-height: 56px; +} + +.mobile-action-sheet-cancel:active { + background: var(--color-border); +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { transform: translateY(100%); } + to { transform: translateY(0); } +} + /* AppShell mobile menu */ @media (max-width: 767px) { .shell-nav { diff --git a/openspec/changes/mobile-app-usability/.openspec.yaml b/openspec/changes/mobile-app-usability/.openspec.yaml new file mode 100644 index 0000000..9e883bf --- /dev/null +++ b/openspec/changes/mobile-app-usability/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-25 diff --git a/openspec/changes/mobile-app-usability/design.md b/openspec/changes/mobile-app-usability/design.md new file mode 100644 index 0000000..b4b9d0e --- /dev/null +++ b/openspec/changes/mobile-app-usability/design.md @@ -0,0 +1,145 @@ +## Context + +The Headquarter web app is built as a desktop-first React application. While the terminal component has been fully mobile-optimized through the `mobile-terminal-ux` change, the rest of the application remains largely unusable on mobile devices. Key pages like Sessions, Dashboard, and Project Workspace use desktop-oriented layouts (sidebars, multi-column forms, fixed-width panels) that break on small viewports. + +Current mobile state: +- AppShell has horizontal scroll navigation (functional but awkward) +- Session cards have action buttons that wrap and overlap +- CreateSessionForm uses multi-column grids that become cramped +- Repo Workspace has 3 fixed sidebars that stack poorly +- Tool Workshop and Config Profiles use fixed 280px sidebars with no mobile adaptation +- Many touch targets are below 44px +- Dialogs may overflow 320px viewports + +User behavior on mobile: +- Primary use: start/stop sessions, check status, terminal access +- Secondary use: small config adjustments, quick file edits +- Not used for: complex tool configuration, heavy code editing, git merge operations + +## Goals / Non-Goals + +**Goals:** +- Make session start/stop/restart fully usable on mobile +- Provide clear navigation and wayfinding on small screens +- Ensure all interactive elements have minimum 44px touch targets +- Make Dashboard and Sessions pages comfortable to use on phones +- Add consistent mobile page headers with back buttons +- Ensure dialogs and modals work within 320px viewport +- Provide read-first, edit-second pattern for complex admin pages + +**Non-Goals:** +- Full feature parity with desktop (complex editing remains desktop-optimized) +- Native app feel (no swipe gestures, pull-to-refresh, or bottom sheets beyond existing terminal special keys) +- Redesigning the terminal (already complete) +- Tablet-specific optimizations (focus is on 320-768px phones) +- Changing any backend APIs or data models + +## Decisions + +### 1. Bottom Tab Bar for Primary Navigation + +**Decision**: Replace AppShell's horizontal scroll nav with a bottom tab bar on mobile (< 768px). + +**Rationale**: +- Bottom tab bars are the standard mobile navigation pattern +- Thumb-reachable, always visible, supports muscle memory +- Horizontal scroll nav requires two-handed use and hides items + +**Implementation**: +- Create `MobileNav` component with 5 tabs: Home, Projects, Sessions, Tools, Settings +- Show badges for active sessions +- Use CSS `position: fixed; bottom: 0` with safe-area-inset padding +- Desktop keeps existing sidebar navigation + +### 2. Session Cards: Action Sheet Pattern + +**Decision**: Replace inline action buttons with a single "Actions" button that opens a dropdown/sheet. + +**Rationale**: +- Current session cards show 4-5 action buttons (Open, Terminal, Stop, Restart, Delete) that wrap awkwardly +- Action sheet reduces visual clutter while keeping all actions accessible +- Follows native mobile pattern + +**Implementation**: +- Add `...` or "Actions" button to each session card +- On tap, show action sheet with: Open, Terminal, Stop/Start, Restart, Delete +- Primary action (Open/Terminal) remains as prominent button +- Keep swipe actions for power users (optional enhancement) + +### 3. Forms: Vertical Stacking with Progressive Disclosure + +**Decision**: All multi-column forms stack vertically on mobile. Complex forms use stepper or accordion pattern. + +**Rationale**: +- Multi-column forms become unreadable on 320px screens +- Vertical stacking is the native mobile form pattern +- Steppers reduce cognitive load by showing one section at a time + +**Implementation**: +- Update CSS: `@media (max-width: 767px)` set `grid-template-columns: 1fr` on all form grids +- For CreateSessionForm: consider stepper (Project → Repo → Tool → Config) or keep as single long form with clear sections +- Ensure all inputs have `min-height: 44px` and adequate spacing + +### 4. Complex Admin Pages: Read-First Pattern + +**Decision**: Tool Workshop and Config Profiles show summary/list view by default on mobile, with edit flowing into full-screen mode. + +**Rationale**: +- These pages have complex forms with sidebars that don't fit mobile +- Users rarely need to edit these on mobile, but may need to view or make small changes +- Read-first pattern matches user behavior + +**Implementation**: +- Tool Workshop: show tool type cards with key info, tap to view details, "Edit" enters full-screen edit mode +- Config Profiles: show profile list, tap to view summary (tools, mounts, env), "Edit" enters multi-step form +- Keep desktop split-pane layout unchanged + +### 5. Touch Targets and Feedback + +**Decision**: Enforce 44px minimum touch targets everywhere. Add active states for all interactive elements. + +**Rationale**: +- iOS Human Interface Guidelines and Android Material Design both recommend 44-48px +- Missing active states make the app feel unresponsive on touch + +**Implementation**: +- Audit all buttons, links, and clickable areas +- Add `:active` states with subtle background changes +- Add loading/spinner states for async operations +- Ensure adequate spacing between adjacent touch targets (minimum 8px) + +## Risks / Trade-offs + +**[Risk] Increased CSS complexity from dual layouts** +→ Mitigation: Use CSS custom properties and utility classes. Mobile styles live in `@media (max-width: 767px)` blocks alongside desktop styles, not in separate files. + +**[Risk] Bottom tab bar reduces vertical screen real estate** +→ Mitigation: Tab bar is only ~56px + safe area. On modern phones, this is acceptable. Content areas must account for tab bar height with `padding-bottom`. + +**[Risk] Hiding actions behind menus increases tap count** +→ Mitigation: Keep the most common action (Open/Terminal) as a primary visible button. Only secondary actions (Restart, Delete) move to the menu. + +**[Risk] Stepper forms may annoy desktop users if not implemented carefully** +→ Mitigation: Stepper only activates on mobile breakpoints. Desktop keeps existing layouts. + +**[Risk] Maintaining two navigation patterns (desktop sidebar + mobile tab bar)** +→ Mitigation: Both use the same route definitions. Navigation state is URL-driven. MobileNav is just a different UI for the same router. + +## Migration Plan + +This is a pure frontend change with no backend or database impact. + +1. Implement mobile navigation (AppShell changes) +2. Update session pages (Dashboard, Sessions) +3. Update form components (CreateSessionForm, dialogs) +4. Update complex pages (Repo Workspace, Tool Workshop, Config Profiles) +5. Polish: touch targets, active states, dialog sizing +6. Test on actual devices (iOS Safari, Android Chrome) + +Rollback: Revert CSS/JS changes. No data migration needed. + +## Open Questions + +1. Should we add a "Quick Actions" FAB (Floating Action Button) on mobile for common operations like "Start Session"? +2. Should the bottom tab bar hide when scrolling down to maximize content area (like Safari's bottom bar)? +3. Do we need a "Desktop Site" toggle for users who prefer the desktop layout on tablets? diff --git a/openspec/changes/mobile-app-usability/proposal.md b/openspec/changes/mobile-app-usability/proposal.md new file mode 100644 index 0000000..af992ad --- /dev/null +++ b/openspec/changes/mobile-app-usability/proposal.md @@ -0,0 +1,33 @@ +## Why + +The Headquarter web app is currently unusable on mobile devices. Critical workflows—starting and stopping sessions, viewing instance status, and making small configuration adjustments—are blocked by desktop-oriented layouts, missing touch targets, and broken responsive behavior. Since users perform real work through terminal sessions (already mobile-optimized) and session management, the core app shell and key pages must be at least functional on phones. + +## What Changes + +- **Mobile navigation**: Replace horizontal scroll nav with bottom tab bar on mobile breakpoints +- **Session management mobile layout**: Redesign session cards for touch, add swipe actions, ensure all controls are reachable +- **Dashboard mobile view**: Simplify summary cards, optimize quick actions for touch +- **Responsive forms**: Stack multi-column forms vertically on mobile, ensure touch targets ≥ 44px +- **Mobile-optimized dialogs**: Ensure all dialogs fit within 320px viewport, add scroll when needed +- **Page headers**: Add consistent back buttons and contextual action bars on mobile +- **Touch feedback**: Add active states and loading indicators for all interactive elements +- **Graceful degradation**: Complex admin pages (Tool Workshop, Config Profiles) get read-first mobile views with edit capability + +## Capabilities + +### New Capabilities +- `mobile-navigation`: Bottom tab bar, page transitions, and mobile-specific navigation patterns +- `mobile-session-management`: Touch-optimized session cards, swipe actions, and mobile session controls +- `mobile-responsive-layouts`: Breakpoint system, touch targets, and form stacking for mobile viewports + +### Modified Capabilities +- `frontend-foundation`: Add mobile breakpoint system and responsive grid utilities +- `session-lifecycle-ux`: Extend session management UX with mobile-specific interactions +- `tool-instances`: Ensure instance controls (start/stop/restart) are usable on mobile + +## Impact + +- **Frontend**: Major CSS changes to AppShell, page layouts, form components, and card patterns +- **Components**: New MobileNav component, updates to SessionCard, CreateSessionForm, and all page components +- **User Experience**: Significantly improved mobile usability without reducing desktop functionality +- **No API changes**: Purely frontend/CSS work diff --git a/openspec/changes/mobile-app-usability/specs/frontend-foundation/spec.md b/openspec/changes/mobile-app-usability/specs/frontend-foundation/spec.md new file mode 100644 index 0000000..b5c5518 --- /dev/null +++ b/openspec/changes/mobile-app-usability/specs/frontend-foundation/spec.md @@ -0,0 +1,43 @@ +## MODIFIED Requirements + +### Requirement: Layout Component +The system SHALL provide a consistent application layout for authenticated screens across desktop and mobile sizes. + +#### Scenario: Application shell +- **GIVEN** the frontend application +- **THEN** a Layout component SHALL: + - Display a header with user info and logout + - Display sidebar navigation on desktop (width >= 768px) + - Show main content area + - Collapse sidebar into a mobile menu toggle on small viewports + - **MODIFIED**: Display a bottom tab bar on mobile (width < 768px) with tabs for Home, Projects, Sessions, Tools, Settings + - **MODIFIED**: Hide sidebar navigation on mobile viewports + +#### Scenario: Navigation links +- **GIVEN** the sidebar navigation +- **THEN** it SHALL include links to: + - Dashboard + - Projects + - Repositories + - SSH Keys + - Settings +- **MODIFIED**: The bottom tab bar SHALL include tabs for: + - Home (Dashboard) + - Projects + - Sessions + - Tools (Tool Workshop) + - Settings + +### Requirement: Responsive Design +The system SHALL support mobile devices. + +#### Scenario: Mobile viewport +- **GIVEN** a mobile device +- **WHEN** the app loads +- **THEN**: + - **MODIFIED**: A bottom tab bar is displayed for primary navigation + - Content adapts to screen width + - Touch targets are appropriately sized (minimum 44px) + - **ADDED**: All multi-column layouts stack vertically + - **ADDED**: Dialogs fit within the viewport and are scrollable + - **ADDED**: Page headers include back buttons where applicable diff --git a/openspec/changes/mobile-app-usability/specs/mobile-navigation/spec.md b/openspec/changes/mobile-app-usability/specs/mobile-navigation/spec.md new file mode 100644 index 0000000..d936197 --- /dev/null +++ b/openspec/changes/mobile-app-usability/specs/mobile-navigation/spec.md @@ -0,0 +1,34 @@ +## ADDED Requirements + +### Requirement: Mobile Bottom Navigation +The system SHALL display a bottom tab bar for navigation on mobile viewports (width < 768px). + +#### Scenario: Mobile viewport shows bottom nav +- **WHEN** the app is viewed on a device with width less than 768px +- **THEN** a bottom tab bar is displayed at the bottom of the screen +- **AND** it contains tabs for: Home, Projects, Sessions, Tools, Settings +- **AND** the desktop sidebar navigation is hidden + +#### Scenario: Bottom nav tab selection +- **WHEN** user taps a tab in the bottom navigation +- **THEN** the app navigates to the corresponding route +- **AND** the selected tab shows an active state + +#### Scenario: Bottom nav session badge +- **GIVEN** the user has active sessions +- **WHEN** viewing the bottom navigation +- **THEN** the Sessions tab displays a badge with the active session count + +#### Scenario: Desktop viewport shows sidebar +- **WHEN** the app is viewed on a device with width 768px or greater +- **THEN** the desktop sidebar navigation is displayed +- **AND** the bottom tab bar is hidden + +### Requirement: Safe Area Support +The system SHALL account for mobile safe areas (notch, home indicator) in the bottom navigation. + +#### Scenario: iPhone with home indicator +- **GIVEN** an iPhone with a home indicator +- **WHEN** the bottom navigation is displayed +- **THEN** it includes additional padding to avoid the home indicator +- **AND** all navigation tabs remain fully tappable diff --git a/openspec/changes/mobile-app-usability/specs/mobile-responsive-layouts/spec.md b/openspec/changes/mobile-app-usability/specs/mobile-responsive-layouts/spec.md new file mode 100644 index 0000000..745e801 --- /dev/null +++ b/openspec/changes/mobile-app-usability/specs/mobile-responsive-layouts/spec.md @@ -0,0 +1,67 @@ +## ADDED Requirements + +### Requirement: Mobile Breakpoint System +The system SHALL apply mobile-specific styles at viewports below 768px width. + +#### Scenario: Form stacking on mobile +- **GIVEN** a multi-column form layout +- **WHEN** the viewport width is less than 768px +- **THEN** all form fields stack vertically in a single column +- **AND** grid layouts use `grid-template-columns: 1fr` + +#### Scenario: Dialog sizing on mobile +- **GIVEN** a dialog or modal +- **WHEN** the viewport width is less than 768px +- **THEN** the dialog fits within the viewport width (maximum 100vw - 32px padding) +- **AND** the dialog content is scrollable if it exceeds the viewport height + +#### Scenario: Page padding on mobile +- **GIVEN** any page content +- **WHEN** the viewport width is less than 768px +- **THEN** horizontal padding is reduced to 16px or less +- **AND** content does not horizontally scroll + +### Requirement: Touch Target Minimum Size +The system SHALL ensure all interactive elements meet minimum touch target sizes. + +#### Scenario: Button touch targets +- **GIVEN** any button or clickable element +- **THEN** it has a minimum height of 44px +- **AND** it has a minimum width of 44px where applicable + +#### Scenario: Link touch targets +- **GIVEN** any text link in a list or navigation +- **THEN** the clickable area has a minimum height of 44px +- **AND** adjacent links have minimum 8px spacing + +### Requirement: Mobile Page Headers +The system SHALL provide consistent page headers on mobile with back navigation. + +#### Scenario: Page header on mobile +- **GIVEN** any page other than the dashboard +- **WHEN** viewed on mobile +- **THEN** the page displays a header with: + - A back button (where applicable) + - The page title + - Contextual action buttons (if any) + +#### Scenario: Back button navigation +- **GIVEN** a page with a back button on mobile +- **WHEN** the user taps the back button +- **THEN** the app navigates to the previous page +- **AND** if no previous page exists, it navigates to the dashboard + +### Requirement: Active States for Touch +The system SHALL provide visual feedback when touchable elements are tapped. + +#### Scenario: Button active state +- **GIVEN** a button on a touch device +- **WHEN** the user taps the button +- **THEN** a visual active state is displayed (e.g., background color change) +- **AND** the active state persists for the duration of the tap + +#### Scenario: Card active state +- **GIVEN** a clickable card on a touch device +- **WHEN** the user taps the card +- **THEN** a visual active state is displayed +- **AND** the active state is distinct from the hover state diff --git a/openspec/changes/mobile-app-usability/specs/mobile-session-management/spec.md b/openspec/changes/mobile-app-usability/specs/mobile-session-management/spec.md new file mode 100644 index 0000000..27f303b --- /dev/null +++ b/openspec/changes/mobile-app-usability/specs/mobile-session-management/spec.md @@ -0,0 +1,50 @@ +## ADDED Requirements + +### Requirement: Touch-Optimized Session Cards +The system SHALL render session cards with touch-friendly layouts on mobile viewports. + +#### Scenario: Session card touch targets +- **GIVEN** a session card displayed on mobile +- **THEN** all interactive elements have a minimum height of 44px +- **AND** action buttons have adequate spacing (minimum 8px between adjacent targets) + +#### Scenario: Session card action menu +- **GIVEN** a session card on mobile +- **WHEN** the user taps the actions menu button +- **THEN** a sheet or dropdown appears with all available actions +- **AND** the actions include: Open, Terminal, Stop/Start, Restart, Delete +- **AND** tapping outside the menu closes it + +#### Scenario: Primary action visibility +- **GIVEN** a running session card on mobile +- **THEN** the primary action (Open or Terminal) remains visible as a prominent button +- **AND** secondary actions are accessible through the actions menu + +### Requirement: Mobile Session Creation +The system SHALL provide a mobile-optimized session creation flow. + +#### Scenario: Create session form on mobile +- **GIVEN** the create session form on a mobile viewport +- **THEN** all form fields stack vertically in a single column +- **AND** each field has a minimum height of 44px +- **AND** the form is scrollable if it exceeds the viewport height + +#### Scenario: Session creation loading state +- **GIVEN** the user submits the create session form on mobile +- **WHEN** the request is in progress +- **THEN** a loading indicator is displayed +- **AND** the submit button is disabled to prevent double-submission + +### Requirement: Mobile Session List +The system SHALL display the session list optimized for mobile scrolling. + +#### Scenario: Session list scrolling +- **GIVEN** multiple sessions on mobile +- **THEN** the session list is vertically scrollable +- **AND** each session card has adequate vertical spacing for touch selection +- **AND** the list does not horizontally scroll + +#### Scenario: Empty state on mobile +- **GIVEN** no active sessions on mobile +- **THEN** the empty state is centered and readable on the viewport +- **AND** the call-to-action button is prominent and tappable diff --git a/openspec/changes/mobile-app-usability/specs/session-lifecycle-ux/spec.md b/openspec/changes/mobile-app-usability/specs/session-lifecycle-ux/spec.md new file mode 100644 index 0000000..4f3ce0f --- /dev/null +++ b/openspec/changes/mobile-app-usability/specs/session-lifecycle-ux/spec.md @@ -0,0 +1,29 @@ +## ADDED Requirements + +### Requirement: Mobile Session Interactions +The system SHALL provide touch-optimized interactions for session management on mobile. + +#### Scenario: Session card actions on mobile +- **GIVEN** a session card displayed on mobile +- **WHEN** the user wants to perform an action +- **THEN** primary actions (Open/Terminal) are visible as prominent buttons +- **AND** secondary actions (Stop, Restart, Delete) are accessible through an action menu +- **AND** all action buttons have minimum 44px touch targets + +#### Scenario: Session stop confirmation on mobile +- **GIVEN** the user taps Stop on a running session +- **THEN** a confirmation dialog appears optimized for mobile viewport +- **AND** the dialog fits within 320px width +- **AND** the dialog actions are stacked vertically with full-width buttons + +#### Scenario: Session creation on mobile +- **GIVEN** the user initiates session creation on mobile +- **THEN** the creation form stacks vertically +- **AND** all fields have minimum 44px height +- **AND** the form is scrollable within the viewport + +#### Scenario: Loading states on mobile +- **GIVEN** an async session operation on mobile +- **WHEN** the operation is in progress +- **THEN** a loading indicator is displayed +- **AND** interactive elements are disabled to prevent double-submission diff --git a/openspec/changes/mobile-app-usability/specs/tool-instances/spec.md b/openspec/changes/mobile-app-usability/specs/tool-instances/spec.md new file mode 100644 index 0000000..a45f308 --- /dev/null +++ b/openspec/changes/mobile-app-usability/specs/tool-instances/spec.md @@ -0,0 +1,22 @@ +## ADDED Requirements + +### Requirement: Mobile Instance Controls +The system SHALL ensure instance control actions are usable on mobile viewports. + +#### Scenario: Control button visibility on mobile +- **GIVEN** an instance card or detail view on mobile +- **THEN** control buttons (Start, Stop, Restart, Delete) have minimum 44px height +- **AND** buttons have adequate spacing between them +- **AND** button labels are readable at mobile font sizes + +#### Scenario: Instance status display on mobile +- **GIVEN** an instance on mobile +- **THEN** the status indicator is clearly visible +- **AND** status text does not wrap awkwardly +- **AND** health/probe information is accessible without horizontal scrolling + +#### Scenario: Instance creation form on mobile +- **GIVEN** the instance creation flow on mobile +- **THEN** all configuration fields stack vertically +- **AND** dropdowns and selects are usable with touch +- **AND** the submit button is prominent and reachable diff --git a/openspec/changes/mobile-app-usability/tasks.md b/openspec/changes/mobile-app-usability/tasks.md new file mode 100644 index 0000000..ade26f3 --- /dev/null +++ b/openspec/changes/mobile-app-usability/tasks.md @@ -0,0 +1,64 @@ +## 1. Mobile Navigation + +- [x] 1.1 Create MobileNav component with bottom tab bar +- [x] 1.2 Add mobile breakpoint detection to AppShell +- [x] 1.3 Implement tab bar with Home, Projects, Sessions, Tools, Settings tabs +- [x] 1.4 Add session count badge to Sessions tab +- [x] 1.5 Add safe area padding for notched devices +- [x] 1.6 Hide desktop sidebar on mobile, show bottom nav +- [x] 1.7 Ensure tab bar stays visible during page transitions + +## 2. Session Management Mobile Layout + +- [x] 2.1 Redesign SessionCard for mobile (touch targets, action menu) +- [x] 2.2 Create mobile action sheet/dropdown for session actions +- [x] 2.3 Ensure primary action (Open/Terminal) remains prominent +- [x] 2.4 Update Sessions page layout for mobile +- [x] 2.5 Update Dashboard session list for mobile +- [x] 2.6 Add loading states for async session operations +- [x] 2.7 Ensure session list scrolls smoothly on mobile + +## 3. Forms & Dialogs + +- [x] 3.1 Stack CreateSessionForm fields vertically on mobile +- [x] 3.2 Ensure all form inputs have min-height 44px +- [x] 3.3 Update all dialogs to fit within 320px viewport +- [x] 3.4 Make dialog content scrollable when needed +- [x] 3.5 Update confirmation dialogs with vertical button layout +- [x] 3.6 Audit all forms for mobile usability + +## 4. Responsive Layout System + +- [x] 4.1 Add mobile breakpoint utilities (max-width: 767px) +- [x] 4.2 Stack multi-column grids vertically on mobile +- [x] 4.3 Reduce page padding on mobile (16px or less) +- [x] 4.4 Ensure no horizontal scrolling on any page +- [x] 4.5 Add consistent mobile page headers with back buttons +- [x] 4.6 Implement back button navigation logic + +## 5. Touch & Interaction + +- [x] 5.1 Audit all buttons for 44px minimum touch target +- [x] 5.2 Add active states to all interactive elements +- [x] 5.3 Add loading indicators for async operations +- [x] 5.4 Ensure 8px minimum spacing between adjacent touch targets +- [x] 5.5 Test touch feedback on iOS Safari and Android Chrome + +## 6. Complex Pages (Read-First Pattern) + +- [x] 6.1 Update Repo Workspace for mobile (file tree primary view) +- [x] 6.2 Update Tool Workshop with mobile list view +- [x] 6.3 Update Config Profiles with mobile summary view +- [x] 6.4 Add "Edit" flow that opens full-screen on mobile +- [x] 6.5 Ensure complex forms are usable on mobile (or show desktop prompt) + +## 7. Testing & Polish + +- [ ] 7.1 Test all pages at 320px width (iPhone SE) +- [ ] 7.2 Test all pages at 375px width (iPhone standard) +- [ ] 7.3 Test on actual iOS Safari device +- [ ] 7.4 Test on actual Android Chrome device +- [ ] 7.5 Verify no console errors on mobile +- [ ] 7.6 Run npm run typecheck +- [ ] 7.7 Run npm run lint +- [ ] 7.8 Run npm run build