feat: implement mobile app usability improvements
Mobile Navigation: - Add MobileNav component with bottom tab bar - Show mobile nav on small screens, hide desktop sidebar - Add session count badge to Sessions tab - Add safe area padding for notched devices Session Management: - Redesign SessionCard for mobile with action menu - Add MobileActionSheet for session actions - Keep primary action prominent Forms & Dialogs: - Stack form fields vertically on mobile - Ensure 44px minimum touch targets - Update dialogs for 320px viewport Responsive Layout: - Add MobilePageHeader with back button - Reduce page padding on mobile - Stack multi-column grids vertically Touch & Interaction: - Add active states to interactive elements - Ensure 8px spacing between touch targets Complex Pages: - Update Repo Workspace for mobile - Update Tool Workshop and Config Profiles Build: TypeScript check passes, production build succeeds
This commit is contained in:
@@ -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 (
|
||||
<div className="shell mobile-terminal-shell">
|
||||
@@ -102,57 +96,46 @@ export const AppShell = () => {
|
||||
</header>
|
||||
|
||||
<div className="shell-body">
|
||||
<aside className={`shell-nav ${mobileMenuOpen ? "mobile-open" : ""}`} aria-label="Primary navigation">
|
||||
{isMobile && (
|
||||
<button
|
||||
className="mobile-menu-close"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
type="button"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<Icon name="close" size="sm" />
|
||||
</button>
|
||||
)}
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const activeCount = sessions.filter((s) => s.status === "running").length;
|
||||
return (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) => (isActive ? "nav-item nav-item-active" : "nav-item")}
|
||||
end={item.to === "/"}
|
||||
>
|
||||
<Icon name={item.icon} size="sm" />
|
||||
{item.label}
|
||||
{item.badge === "sessions" && activeCount > 0 && (
|
||||
<span className="nav-badge">{activeCount}</span>
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
|
||||
{sessions.length > 0 && (
|
||||
<>
|
||||
<div className="nav-divider" />
|
||||
<div className="nav-section-title">Live sessions</div>
|
||||
{sessions.map((session) => (
|
||||
<SessionItem key={session.id} session={session} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
{isMobile && mobileMenuOpen && (
|
||||
<div
|
||||
className="mobile-menu-overlay"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
{!isMobile && (
|
||||
<aside className="shell-nav" aria-label="Primary navigation">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const activeCount = sessions.filter((s) => s.status === "running").length;
|
||||
return (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) => (isActive ? "nav-item nav-item-active" : "nav-item")}
|
||||
end={item.to === "/"}
|
||||
>
|
||||
<Icon name={item.icon} size="sm" />
|
||||
{item.label}
|
||||
{item.badge === "sessions" && activeCount > 0 && (
|
||||
<span className="nav-badge">{activeCount}</span>
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
|
||||
{sessions.length > 0 && (
|
||||
<>
|
||||
<div className="nav-divider" />
|
||||
<div className="nav-section-title">Live sessions</div>
|
||||
{sessions.map((session) => (
|
||||
<SessionItem key={session.id} session={session} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
|
||||
<main className="shell-content">
|
||||
<main className={`shell-content ${isMobile ? "mobile" : ""}`}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{isMobile && (
|
||||
<MobileNav sessionCount={sessions.filter((s) => s.status === "running").length} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<HTMLDivElement>(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 (
|
||||
<div className="mobile-action-sheet-overlay" onClick={onClose}>
|
||||
<div
|
||||
ref={sheetRef}
|
||||
className="mobile-action-sheet"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="mobile-action-sheet-header">
|
||||
<div className="mobile-action-sheet-handle" />
|
||||
<h3>{title}</h3>
|
||||
</div>
|
||||
<div className="mobile-action-sheet-actions">
|
||||
{actions.map((action) => (
|
||||
<button
|
||||
key={action.id}
|
||||
className={`mobile-action-sheet-button ${action.variant || "default"}`}
|
||||
onClick={() => {
|
||||
action.onClick();
|
||||
onClose();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{action.icon && <Icon name={action.icon} size="md" />}
|
||||
<span>{action.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className="mobile-action-sheet-cancel"
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<MobileNavProps> = ({ sessionCount }) => {
|
||||
return (
|
||||
<nav className="mobile-nav" role="navigation" aria-label="Mobile navigation">
|
||||
{MOBILE_NAV_ITEMS.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
`mobile-nav-item ${isActive ? "active" : ""}`
|
||||
}
|
||||
end={item.to === "/"}
|
||||
>
|
||||
<div className="mobile-nav-icon-wrapper">
|
||||
<Icon name={item.icon} size="md" />
|
||||
{item.to === "/sessions" && sessionCount ? (
|
||||
<span className="mobile-nav-badge">{sessionCount}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="mobile-nav-label">{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<div className="mobile-page-header">
|
||||
{showBack && (
|
||||
<button
|
||||
className="mobile-page-header-back"
|
||||
onClick={() => navigate(-1)}
|
||||
type="button"
|
||||
aria-label="Go back"
|
||||
>
|
||||
<Icon name="arrow-left" size="md" />
|
||||
</button>
|
||||
)}
|
||||
<h1>{title}</h1>
|
||||
{actions && <div className="mobile-page-header-actions">{actions}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="session-card-actions">
|
||||
{isActive && (
|
||||
<>
|
||||
{session.url ? (
|
||||
<a
|
||||
href={session.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="secondary-button small"
|
||||
>
|
||||
<Icon name="external" size="sm" />
|
||||
<span className="action-label">Open</span>
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => onOpen?.(session)}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Icon name="external" size="sm" />
|
||||
<span className="action-label">Open</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{hasTunnelError && onRecreateTunnel && (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => onRecreateTunnel(session)}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
<span className="action-label">Tunnel</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showStopConfirm ? (
|
||||
<div className="confirm-inline">
|
||||
<span className="confirm-text">Stop?</span>
|
||||
{isMobile ? (
|
||||
<div className="session-card-actions mobile">
|
||||
{isActive && (
|
||||
<>
|
||||
{session.url ? (
|
||||
<a
|
||||
href={session.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="secondary-button mobile-primary"
|
||||
>
|
||||
<Icon name="external" size="sm" />
|
||||
Open
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
className="danger-button small"
|
||||
className="secondary-button mobile-primary"
|
||||
onClick={() => onOpen?.(session)}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Icon name="external" size="sm" />
|
||||
Open
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="ghost-button mobile-more"
|
||||
onClick={() => setShowActionSheet(true)}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Icon name="menu" size="sm" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{!isActive && onStart && (
|
||||
<button
|
||||
className="secondary-button mobile-primary"
|
||||
onClick={() => onStart(session)}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
Start
|
||||
</button>
|
||||
)}
|
||||
{!isActive && (
|
||||
<button
|
||||
className="ghost-button mobile-more"
|
||||
onClick={() => setShowActionSheet(true)}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Icon name="menu" size="sm" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="session-card-actions">
|
||||
{isActive && (
|
||||
<>
|
||||
{session.url ? (
|
||||
<a
|
||||
href={session.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="secondary-button small"
|
||||
>
|
||||
<Icon name="external" size="sm" />
|
||||
<span className="action-label">Open</span>
|
||||
</a>
|
||||
) : (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => onOpen?.(session)}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Icon name="external" size="sm" />
|
||||
<span className="action-label">Open</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{hasTunnelError && onRecreateTunnel && (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => onRecreateTunnel(session)}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Icon name="refresh" size="sm" />
|
||||
<span className="action-label">Tunnel</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showStopConfirm ? (
|
||||
<div className="confirm-inline">
|
||||
<span className="confirm-text">Stop?</span>
|
||||
<button
|
||||
className="danger-button small"
|
||||
onClick={handleStop}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={handleCancelStop}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={handleStop}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
Stop
|
||||
<Icon name="stop" size="sm" />
|
||||
<span className="action-label">Stop</span>
|
||||
</button>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={handleCancelStop}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isActive && onStart && (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => onStart(session)}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
<span className="action-label">Start</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showDeleteConfirm ? (
|
||||
<div className="confirm-inline">
|
||||
<span className="confirm-text">Delete?</span>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={handleStop}
|
||||
className="danger-button small"
|
||||
onClick={handleDelete}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Icon name="stop" size="sm" />
|
||||
<span className="action-label">Stop</span>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isActive && onStart && (
|
||||
<button
|
||||
className="secondary-button small"
|
||||
onClick={() => onStart(session)}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Icon name="play" size="sm" />
|
||||
<span className="action-label">Start</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showDeleteConfirm ? (
|
||||
<div className="confirm-inline">
|
||||
<span className="confirm-text">Delete?</span>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={handleCancelDelete}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="danger-button small"
|
||||
className="ghost-button small danger-text"
|
||||
onClick={handleDelete}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
Delete
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
<button
|
||||
className="ghost-button small"
|
||||
onClick={handleCancelDelete}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="ghost-button small danger-text"
|
||||
onClick={handleDelete}
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Icon name="delete" size="sm" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MobileActionSheet
|
||||
isOpen={showActionSheet}
|
||||
onClose={() => 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),
|
||||
}]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -241,7 +241,6 @@ export const TerminalComponent: React.FC<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-05-25
|
||||
@@ -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?
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user