From 4ea2e3659dff86483c2098d594aff221c409c135 Mon Sep 17 00:00:00 2001 From: Developer Date: Sun, 7 Jun 2026 14:09:28 +0000 Subject: [PATCH] feat: add mobile support for Workspaces page with workspace creation - WorkspacesPage.tsx: detect mobile viewport, show MobileListView / MobileDetailView / MobileFAB - Mobile list: tap workspace to view details (branch, status, path, instances, sync time) - Mobile detail: view workspace fields with Edit and Delete actions - Mobile FAB: opens inline WorkspaceCreateForm - New styles/pages/workspaces.css with responsive grid and card styles - Import workspaces.css in main.tsx Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass --- apps/web/src/main.tsx | 1 + apps/web/src/pages/WorkspacesPage.tsx | 111 ++++++++++++++++++++ apps/web/src/styles/pages/workspaces.css | 126 +++++++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 apps/web/src/styles/pages/workspaces.css diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 84170da..dd76ffb 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -15,6 +15,7 @@ import "./styles/pages/projects.css"; import "./styles/pages/sessions.css"; import "./styles/pages/ssh-keys.css"; import "./styles/pages/workspace-detail.css"; +import "./styles/pages/workspaces.css"; ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/apps/web/src/pages/WorkspacesPage.tsx b/apps/web/src/pages/WorkspacesPage.tsx index 1339631..dbadfb2 100644 --- a/apps/web/src/pages/WorkspacesPage.tsx +++ b/apps/web/src/pages/WorkspacesPage.tsx @@ -2,14 +2,23 @@ import { useState } from "react"; import { Icon } from "../components/icon"; +import { useMobileViewport } from "../hooks/use-mobile-viewport"; import { useWorkspaces } from "../hooks/use-workspaces"; import { useWorkspaceActions } from "../hooks/use-workspace-actions"; import { WorkspaceCard } from "../components/features/workspace/workspace-card"; import { WorkspaceCreateForm } from "../components/features/workspace/workspace-create-form"; +import { MobileListView } from "../components/features/mobile/mobile-list-view"; +import { MobileDetailView } from "../components/features/mobile/mobile-detail-view"; +import { MobileFAB } from "../components/features/mobile/mobile-fab"; import { ToolStarter } from "../components/features/tool/tool-starter"; import type { Workspace } from "../types/workspace"; +type MobileView = "list" | "detail" | "create"; + export function WorkspacesPage() { + const isMobile = useMobileViewport(); + const [mobileView, setMobileView] = useState("list"); + const [selectedWorkspace, setSelectedWorkspace] = useState(null); const [showCreate, setShowCreate] = useState(false); const [startWorkspace, setStartWorkspace] = useState(null); @@ -28,6 +37,108 @@ export function WorkspacesPage() { ); }; + /* ── Mobile views ── */ + if (isMobile) { + if (mobileView === "create") { + return ( +
+ { + setMobileView("list"); + await refresh(); + }} + onCancel={() => setMobileView("list")} + /> +
+ ); + } + + if (mobileView === "detail" && selectedWorkspace) { + const ws = selectedWorkspace; + return ( + { + setStartWorkspace(ws); + }} + onDelete={() => { + void handleDelete(ws); + setMobileView("list"); + setSelectedWorkspace(null); + }} + onBack={() => { + setMobileView("list"); + setSelectedWorkspace(null); + }} + /> + ); + } + + /* Mobile list view */ + return ( +
+
+

Workspaces

+ {workspaces.length} workspace{workspaces.length !== 1 ? "s" : ""} +
+ + {error &&
{error}
} + + {loading && workspaces.length === 0 ? ( +
Loading workspaces...
+ ) : ( + ({ + id: ws.id, + title: ws.name, + subtitle: `${ws.project_name} · ${ws.repo_name} · ${ws.branch}`, + status: ws.status, + }))} + onItemClick={(id) => { + const ws = workspaces.find((w) => w.id === id); + if (ws) { + setSelectedWorkspace(ws); + setMobileView("detail"); + } + }} + emptyMessage="No workspaces yet" + /> + )} + + setMobileView("create")} + label="Create workspace" + /> + + {startWorkspace && ( +
setStartWorkspace(null)}> +
e.stopPropagation()}> +

Start Tool

+ { + setStartWorkspace(null); + void refresh(); + }} + onCancel={() => setStartWorkspace(null)} + /> +
+
+ )} +
+ ); + } + + /* ── Desktop view ── */ return (
diff --git a/apps/web/src/styles/pages/workspaces.css b/apps/web/src/styles/pages/workspaces.css new file mode 100644 index 0000000..663149d --- /dev/null +++ b/apps/web/src/styles/pages/workspaces.css @@ -0,0 +1,126 @@ +/** Workspaces page styles */ + +/* Desktop */ +.workspaces-page { + padding: var(--space-4); +} + +.workspaces-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: var(--space-4); + margin-top: var(--space-4); +} + +/* Workspace card */ +.workspace-card { + padding: var(--space-4); + border: 1px solid var(--border); + border-radius: 12px; + background: var(--panel); + transition: box-shadow 0.15s ease; +} + +.workspace-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +.workspace-card.loading { + opacity: 0.6; + pointer-events: none; +} + +.workspace-header-link { + display: block; + text-decoration: none; + color: inherit; +} + +.workspace-header { + display: flex; + align-items: center; + gap: var(--space-2); + margin-bottom: var(--space-2); +} + +.workspace-header h4 { + margin: 0; + font-size: 1.1rem; + flex: 1; +} + +.workspace-meta { + margin-bottom: var(--space-3); +} + +.workspace-meta p { + margin: 0.15rem 0; + font-size: 0.875rem; + color: var(--muted); +} + +.workspace-actions { + display: flex; + gap: var(--space-2); + flex-wrap: wrap; +} + +/* Status badges */ +.status-badge { + font-size: 0.75rem; + font-weight: 600; + padding: 0.15rem 0.5rem; + border-radius: 999px; + text-transform: capitalize; +} + +.status-ready { + background: var(--success-light); + color: var(--success); +} + +.status-syncing { + background: var(--warning-light); + color: var(--warning); +} + +.status-error { + background: var(--danger-light); + color: var(--danger); +} + +/* Empty state */ +.empty-state { + text-align: center; + padding: var(--space-10) var(--space-4); + color: var(--muted); +} + +.empty-state p { + margin-bottom: var(--space-4); +} + +/* Mobile */ +@media (max-width: 767px) { + .workspaces-page { + padding: var(--space-2); + } + + .workspaces-grid { + grid-template-columns: 1fr; + gap: var(--space-3); + } + + .workspace-card { + padding: var(--space-3); + } + + .workspace-actions { + flex-direction: column; + } + + .workspace-actions .btn { + width: 100%; + justify-content: center; + } +}