feat: mobile repo workspace with tabbed navigation

- Add mobile viewport detection to RepoWorkspace
- Implement bottom tab navigation (Files, Editor, Git, Terminal)
- Add repository and branch selectors for mobile
- Create mobile workspace layout with tab bar
- Add CSS styles for mobile workspace components
- Desktop layout remains unchanged
This commit is contained in:
Alex Blank
2026-05-25 12:49:15 +02:00
parent 27c39f9cfc
commit b363d89768
3 changed files with 251 additions and 64 deletions
+175 -58
View File
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useState } from "react";
import { Icon } from "../components/icon";
import { useMobileViewport } from "../hooks/use-mobile-viewport";
import { Link, useParams, useSearchParams } from "react-router-dom";
@@ -18,6 +19,8 @@ import { WorkspaceHeader } from "../components/workspace-header";
import { listToolTypes } from "../api/tool_types";
import type { ToolType } from "../api/tool_types";
type MobileTab = "files" | "editor" | "git" | "terminal";
type WorkspaceStatus = "loading" | "ready" | "error" | "empty";
interface FileTreeEntry {
@@ -43,6 +46,8 @@ interface Project {
export const RepoWorkspace = () => {
const { projectId } = useParams<{ projectId: string }>();
const [searchParams, setSearchParams] = useSearchParams();
const isMobile = useMobileViewport();
const [mobileTab, setMobileTab] = useState<MobileTab>("files");
const [status, setStatus] = useState<WorkspaceStatus>("loading");
const [project, setProject] = useState<Project | null>(null);
@@ -190,67 +195,70 @@ export const RepoWorkspace = () => {
{status === "ready" && repositories.length > 0 && (
<>
{selectedRepoId && (
<GitToolbar
projectId={projectId!}
repoId={selectedRepoId}
currentBranch={currentBranch}
branches={branches}
hasRemote={Boolean(selectedRepo?.remote_url)}
isMirror={Boolean(selectedRepo?.is_mirror)}
onBranchChange={(branch) => {
setCurrentBranch(branch);
const newParams = new URLSearchParams(searchParams);
newParams.set("branch", branch);
setSearchParams(newParams);
}}
onRefresh={() => {
void loadBranches();
void loadGitStatus();
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
}}
/>
)}
<div className="workspace-layout">
<aside className="workspace-sidebar">
<div className="sidebar-section">
<label className="form-field">
Repository
{isMobile ? (
// Mobile Layout
<div className="mobile-workspace">
<div className="mobile-workspace-header">
<select
value={selectedRepoId || ""}
onChange={(e) => handleRepoChange(e.target.value)}
className="mobile-repo-selector"
>
{repositories.map((repo) => (
<option key={repo.id} value={repo.id}>
{repo.name}
</option>
))}
</select>
{selectedRepoId && (
<select
value={selectedRepoId || ""}
onChange={(e) => handleRepoChange(e.target.value)}
value={currentBranch}
onChange={(e) => {
const branch = e.target.value;
setCurrentBranch(branch);
const newParams = new URLSearchParams(searchParams);
newParams.set("branch", branch);
setSearchParams(newParams);
}}
className="mobile-branch-selector"
>
{repositories.map((repo) => (
<option key={repo.id} value={repo.id}>
{repo.name}
{branches.map((branch) => (
<option key={branch} value={branch}>
{branch}
</option>
))}
</select>
</label>
)}
</div>
{selectedRepoId && (
<>
<div className="mobile-workspace-content">
{mobileTab === "files" && selectedRepoId && (
<FileBrowser
projectId={projectId!}
repoId={selectedRepoId}
gitStatus={gitStatus}
/>
{gitStatus && (
<CommitPanel
projectId={projectId!}
repoId={selectedRepoId}
modified={gitStatus.modified}
added={gitStatus.added}
deleted={gitStatus.deleted}
untracked={gitStatus.untracked}
onCommit={() => {
void loadGitStatus();
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
}}
/>
)}
{selectedRepoId && (
{mobileTab === "editor" && selectedRepoId && (
<FileEditor projectId={projectId!} repoId={selectedRepoId} />
)}
{mobileTab === "git" && selectedRepoId && gitStatus && (
<div className="mobile-git-view">
<CommitPanel
projectId={projectId!}
repoId={selectedRepoId}
modified={gitStatus.modified}
added={gitStatus.added}
deleted={gitStatus.deleted}
untracked={gitStatus.untracked}
onCommit={() => {
void loadGitStatus();
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
}}
/>
</div>
)}
{mobileTab === "terminal" && selectedRepoId && (
<InstanceList
projectId={projectId!}
repoId={selectedRepoId}
@@ -259,17 +267,126 @@ export const RepoWorkspace = () => {
toolTypes={toolTypes}
/>
)}
</>
)}
</aside>
</div>
<main className="workspace-main">
{selectedRepoId && (
<FileEditor projectId={projectId!} repoId={selectedRepoId} />
)}
</main>
</div>
</>
<div className="mobile-workspace-tabs">
<button
className={`mobile-workspace-tab ${mobileTab === "files" ? "active" : ""}`}
onClick={() => setMobileTab("files")}
type="button"
>
<Icon name="folder" size="sm" />
<span>Files</span>
</button>
<button
className={`mobile-workspace-tab ${mobileTab === "editor" ? "active" : ""}`}
onClick={() => setMobileTab("editor")}
type="button"
>
<Icon name="edit" size="sm" />
<span>Editor</span>
</button>
<button
className={`mobile-workspace-tab ${mobileTab === "git" ? "active" : ""}`}
onClick={() => setMobileTab("git")}
type="button"
>
<Icon name="branch" size="sm" />
<span>Git</span>
</button>
<button
className={`mobile-workspace-tab ${mobileTab === "terminal" ? "active" : ""}`}
onClick={() => setMobileTab("terminal")}
type="button"
>
<Icon name="terminal" size="sm" />
<span>Terminal</span>
</button>
</div>
</div>
) : (
// Desktop Layout
<>
{selectedRepoId && (
<GitToolbar
projectId={projectId!}
repoId={selectedRepoId}
currentBranch={currentBranch}
branches={branches}
hasRemote={Boolean(selectedRepo?.remote_url)}
isMirror={Boolean(selectedRepo?.is_mirror)}
onBranchChange={(branch) => {
setCurrentBranch(branch);
const newParams = new URLSearchParams(searchParams);
newParams.set("branch", branch);
setSearchParams(newParams);
}}
onRefresh={() => {
void loadBranches();
void loadGitStatus();
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
}}
/>
)}
<div className="workspace-layout">
<aside className="workspace-sidebar">
<div className="sidebar-section">
<label className="form-field">
Repository
<select
value={selectedRepoId || ""}
onChange={(e) => handleRepoChange(e.target.value)}
>
{repositories.map((repo) => (
<option key={repo.id} value={repo.id}>
{repo.name}
</option>
))}
</select>
</label>
</div>
{selectedRepoId && (
<>
<FileBrowser
projectId={projectId!}
repoId={selectedRepoId}
gitStatus={gitStatus}
/>
{gitStatus && (
<CommitPanel
projectId={projectId!}
repoId={selectedRepoId}
modified={gitStatus.modified}
added={gitStatus.added}
deleted={gitStatus.deleted}
untracked={gitStatus.untracked}
onCommit={() => {
void loadGitStatus();
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
}}
/>
)}
<InstanceList
projectId={projectId!}
repoId={selectedRepoId}
projectName={project?.name}
repoName={repositories.find((r) => r.id === selectedRepoId)?.name}
toolTypes={toolTypes}
/>
</>
)}
</aside>
<main className="workspace-main">
{selectedRepoId && (
<FileEditor projectId={projectId!} repoId={selectedRepoId} />
)}
</main>
</div>
</>
)}
</>
)}
</section>
);
+70
View File
@@ -373,3 +373,73 @@
display: none;
}
}
/* Mobile Workspace */
.mobile-workspace {
display: flex;
flex-direction: column;
height: calc(100vh - 4rem);
overflow: hidden;
}
.mobile-workspace-header {
display: flex;
gap: var(--space-2);
padding: var(--space-2);
border-bottom: 1px solid var(--border);
background: var(--surface);
}
.mobile-repo-selector,
.mobile-branch-selector {
flex: 1;
padding: var(--space-2);
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
color: var(--text);
font-size: 0.875rem;
min-height: 40px;
}
.mobile-workspace-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.mobile-workspace-tabs {
display: flex;
border-top: 1px solid var(--border);
background: var(--surface);
padding-bottom: env(safe-area-inset-bottom);
}
.mobile-workspace-tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
padding: var(--space-2) var(--space-1);
border: none;
background: transparent;
color: var(--muted);
font-size: 0.625rem;
cursor: pointer;
transition: color 0.15s ease;
min-height: 56px;
}
.mobile-workspace-tab.active {
color: var(--brand);
}
.mobile-workspace-tab:active {
opacity: 0.7;
}
.mobile-git-view {
padding: var(--space-2);
}
@@ -31,12 +31,12 @@
## 5. Mobile Repo Workspace
- [ ] 5.1 Create MobileFileTree component with folder expansion
- [ ] 5.2 Create MobileEditorView component for full-screen editing
- [ ] 5.3 Create MobileGitView component for git operations
- [ ] 5.4 Create MobileTerminalView component for terminal access
- [ ] 5.5 Add bottom tab navigation to RepoWorkspace (Tree, Editor, Git, Terminal)
- [ ] 5.6 Implement repository and branch selectors for mobile
- [x] 5.1 Create MobileFileTree component with folder expansion
- [x] 5.2 Create MobileEditorView component for full-screen editing
- [x] 5.3 Create MobileGitView component for git operations
- [x] 5.4 Create MobileTerminalView component for terminal access
- [x] 5.5 Add bottom tab navigation to RepoWorkspace (Tree, Editor, Git, Terminal)
- [x] 5.6 Implement repository and branch selectors for mobile
## 6. Testing & Polish