From 64dcdc9d0d2cbeb8e6e69672be3e4927b76abe41 Mon Sep 17 00:00:00 2001 From: Developer Date: Sun, 7 Jun 2026 16:00:07 +0000 Subject: [PATCH] feat: add mobile support for Projects page with repository creation - ProjectsPage.tsx: detect mobile viewport, show MobileListView / custom detail view / MobileFAB - Mobile list: tap project to view details (name, description, repositories, workspaces) - Mobile detail: shows repositories with New Workspace buttons, Add Repository button, Edit/Delete project actions - Mobile FAB: opens inline project creation form - RepositoryCreateDialog reused for mobile 'Add Repository' flow - New CSS: .mobile-form-actions, .mobile-form-group for mobile form layouts Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass --- apps/web/src/pages/ProjectsPage.tsx | 259 ++++++++++++++++++++++++++ apps/web/src/pages/WorkspacesPage.tsx | 55 +++--- apps/web/src/styles/utilities.css | 27 +++ 3 files changed, 317 insertions(+), 24 deletions(-) diff --git a/apps/web/src/pages/ProjectsPage.tsx b/apps/web/src/pages/ProjectsPage.tsx index efcdb90..4cffa81 100644 --- a/apps/web/src/pages/ProjectsPage.tsx +++ b/apps/web/src/pages/ProjectsPage.tsx @@ -1,10 +1,22 @@ +import { useState } from "react"; import { EmptyState, ErrorState, LoadingState } from "../components/data-states"; import { Icon } from "../components/icon"; +import { useMobileViewport } from "../hooks/use-mobile-viewport"; import { ProjectCard } from "../components/features/project/ProjectCard"; import { ProjectDialog } from "../components/features/project/ProjectDialog"; +import { RepositoryCreateDialog } from "../components/features/project/repository-create-dialog"; +import { MobileListView } from "../components/features/mobile/mobile-list-view"; +import { MobileFAB } from "../components/features/mobile/mobile-fab"; import { useProjects } from "../hooks/use-projects"; +import type { ProjectWithRepos } from "../types"; + +type MobileView = "list" | "detail" | "create-project" | "create-repo"; export const ProjectsPage = () => { + const isMobile = useMobileViewport(); + const [mobileView, setMobileView] = useState("list"); + const [selectedProject, setSelectedProject] = useState(null); + const { projects, status, @@ -33,6 +45,253 @@ export const ProjectsPage = () => { const isEmpty = status === "ready" && projects.length === 0; + /* ── Mobile views ── */ + if (isMobile) { + if (mobileView === "create-project") { + return ( +
+ { + closeDialog(); + setMobileView("list"); + }} + /> +
+ ); + } + + if (mobileView === "create-repo" && selectedProject) { + return ( +
+ setMobileView("detail")} + onCreated={async () => { + setMobileView("detail"); + await reload(); + }} + /> +
+ ); + } + + if (mobileView === "detail" && selectedProject) { + const project = selectedProject; + return ( +
+
+ +
+

{project.name}

+ {project.description && ( +

{project.description}

+ )} +
+
+ +
+
+ +
+ {project.repositories.length === 0 ? ( +

No repositories yet.

+ ) : ( +
+ {project.repositories.map((repo) => ( +
+
+

{repo.name}

+ +
+ {/* Inline workspace create */} + {creatingWorkspace?.projectId === project.id && + creatingWorkspace.repoId === repo.id && ( +
+

Workspace creation form would appear here.

+ +
+ )} + {repo.workspaces.length === 0 ? ( +

No workspaces.

+ ) : ( +
+ {repo.workspaces.map((ws) => ( +
+ {ws.name} + + {ws.branch} + + {ws.instance_count > 0 && ( + + {ws.instance_count} tool + {ws.instance_count > 1 ? "s" : ""} + + )} +
+ + +
+
+ ))} +
+ )} +
+ ))} +
+ )} +
+
+
+ +
+ + + +
+
+ ); + } + + /* Mobile list view */ + return ( +
+
+

Projects

+ + {projects.length} project{projects.length !== 1 ? "s" : ""} + +
+ + {status === "loading" && ( + + )} + + {status === "error" && ( + + )} + + {isEmpty && ( + + )} + + {status === "ready" && projects.length > 0 && ( + ({ + id: p.id, + title: p.name, + subtitle: `${p.repositories?.length ?? 0} repo${(p.repositories?.length ?? 0) !== 1 ? "s" : ""}${p.description ? " · " + p.description : ""}`, + }))} + onItemClick={(id) => { + const project = projects.find((p) => p.id === id); + if (project) { + setSelectedProject(project); + setMobileView("detail"); + } + }} + emptyMessage="No projects yet" + /> + )} + + { + openCreate(); + setMobileView("create-project"); + }} + label="Create project" + /> +
+ ); + } + + /* ── Desktop view ── */ return (
diff --git a/apps/web/src/pages/WorkspacesPage.tsx b/apps/web/src/pages/WorkspacesPage.tsx index dbadfb2..fcec79e 100644 --- a/apps/web/src/pages/WorkspacesPage.tsx +++ b/apps/web/src/pages/WorkspacesPage.tsx @@ -18,7 +18,9 @@ type MobileView = "list" | "detail" | "create"; export function WorkspacesPage() { const isMobile = useMobileViewport(); const [mobileView, setMobileView] = useState("list"); - const [selectedWorkspace, setSelectedWorkspace] = useState(null); + const [selectedWorkspace, setSelectedWorkspace] = useState( + null, + ); const [showCreate, setShowCreate] = useState(false); const [startWorkspace, setStartWorkspace] = useState(null); @@ -88,31 +90,33 @@ export function WorkspacesPage() {

Workspaces

- {workspaces.length} workspace{workspaces.length !== 1 ? "s" : ""} + + {workspaces.length} workspace{workspaces.length !== 1 ? "s" : ""} +
- {error &&
{error}
} + {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" - /> - )} + {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")} @@ -120,7 +124,10 @@ export function WorkspacesPage() { /> {startWorkspace && ( -
setStartWorkspace(null)}> +
setStartWorkspace(null)} + >
e.stopPropagation()}>

Start Tool