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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user