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 { useCallback, useEffect, useState } from "react";
|
||||||
import { Icon } from "../components/icon";
|
import { Icon } from "../components/icon";
|
||||||
|
import { useMobileViewport } from "../hooks/use-mobile-viewport";
|
||||||
|
|
||||||
import { Link, useParams, useSearchParams } from "react-router-dom";
|
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 { listToolTypes } from "../api/tool_types";
|
||||||
import type { ToolType } from "../api/tool_types";
|
import type { ToolType } from "../api/tool_types";
|
||||||
|
|
||||||
|
type MobileTab = "files" | "editor" | "git" | "terminal";
|
||||||
|
|
||||||
type WorkspaceStatus = "loading" | "ready" | "error" | "empty";
|
type WorkspaceStatus = "loading" | "ready" | "error" | "empty";
|
||||||
|
|
||||||
interface FileTreeEntry {
|
interface FileTreeEntry {
|
||||||
@@ -43,6 +46,8 @@ interface Project {
|
|||||||
export const RepoWorkspace = () => {
|
export const RepoWorkspace = () => {
|
||||||
const { projectId } = useParams<{ projectId: string }>();
|
const { projectId } = useParams<{ projectId: string }>();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const isMobile = useMobileViewport();
|
||||||
|
const [mobileTab, setMobileTab] = useState<MobileTab>("files");
|
||||||
|
|
||||||
const [status, setStatus] = useState<WorkspaceStatus>("loading");
|
const [status, setStatus] = useState<WorkspaceStatus>("loading");
|
||||||
const [project, setProject] = useState<Project | null>(null);
|
const [project, setProject] = useState<Project | null>(null);
|
||||||
@@ -190,67 +195,70 @@ export const RepoWorkspace = () => {
|
|||||||
|
|
||||||
{status === "ready" && repositories.length > 0 && (
|
{status === "ready" && repositories.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{selectedRepoId && (
|
{isMobile ? (
|
||||||
<GitToolbar
|
// Mobile Layout
|
||||||
projectId={projectId!}
|
<div className="mobile-workspace">
|
||||||
repoId={selectedRepoId}
|
<div className="mobile-workspace-header">
|
||||||
currentBranch={currentBranch}
|
<select
|
||||||
branches={branches}
|
value={selectedRepoId || ""}
|
||||||
hasRemote={Boolean(selectedRepo?.remote_url)}
|
onChange={(e) => handleRepoChange(e.target.value)}
|
||||||
isMirror={Boolean(selectedRepo?.is_mirror)}
|
className="mobile-repo-selector"
|
||||||
onBranchChange={(branch) => {
|
>
|
||||||
setCurrentBranch(branch);
|
{repositories.map((repo) => (
|
||||||
const newParams = new URLSearchParams(searchParams);
|
<option key={repo.id} value={repo.id}>
|
||||||
newParams.set("branch", branch);
|
{repo.name}
|
||||||
setSearchParams(newParams);
|
</option>
|
||||||
}}
|
))}
|
||||||
onRefresh={() => {
|
</select>
|
||||||
void loadBranches();
|
{selectedRepoId && (
|
||||||
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
|
<select
|
||||||
value={selectedRepoId || ""}
|
value={currentBranch}
|
||||||
onChange={(e) => handleRepoChange(e.target.value)}
|
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) => (
|
{branches.map((branch) => (
|
||||||
<option key={repo.id} value={repo.id}>
|
<option key={branch} value={branch}>
|
||||||
{repo.name}
|
{branch}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedRepoId && (
|
<div className="mobile-workspace-content">
|
||||||
<>
|
{mobileTab === "files" && selectedRepoId && (
|
||||||
<FileBrowser
|
<FileBrowser
|
||||||
projectId={projectId!}
|
projectId={projectId!}
|
||||||
repoId={selectedRepoId}
|
repoId={selectedRepoId}
|
||||||
gitStatus={gitStatus}
|
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
|
<InstanceList
|
||||||
projectId={projectId!}
|
projectId={projectId!}
|
||||||
repoId={selectedRepoId}
|
repoId={selectedRepoId}
|
||||||
@@ -259,17 +267,126 @@ export const RepoWorkspace = () => {
|
|||||||
toolTypes={toolTypes}
|
toolTypes={toolTypes}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
)}
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main className="workspace-main">
|
<div className="mobile-workspace-tabs">
|
||||||
{selectedRepoId && (
|
<button
|
||||||
<FileEditor projectId={projectId!} repoId={selectedRepoId} />
|
className={`mobile-workspace-tab ${mobileTab === "files" ? "active" : ""}`}
|
||||||
)}
|
onClick={() => setMobileTab("files")}
|
||||||
</main>
|
type="button"
|
||||||
</div>
|
>
|
||||||
</>
|
<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>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -373,3 +373,73 @@
|
|||||||
display: none;
|
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. Mobile Repo Workspace
|
||||||
|
|
||||||
- [ ] 5.1 Create MobileFileTree component with folder expansion
|
- [x] 5.1 Create MobileFileTree component with folder expansion
|
||||||
- [ ] 5.2 Create MobileEditorView component for full-screen editing
|
- [x] 5.2 Create MobileEditorView component for full-screen editing
|
||||||
- [ ] 5.3 Create MobileGitView component for git operations
|
- [x] 5.3 Create MobileGitView component for git operations
|
||||||
- [ ] 5.4 Create MobileTerminalView component for terminal access
|
- [x] 5.4 Create MobileTerminalView component for terminal access
|
||||||
- [ ] 5.5 Add bottom tab navigation to RepoWorkspace (Tree, Editor, Git, Terminal)
|
- [x] 5.5 Add bottom tab navigation to RepoWorkspace (Tree, Editor, Git, Terminal)
|
||||||
- [ ] 5.6 Implement repository and branch selectors for mobile
|
- [x] 5.6 Implement repository and branch selectors for mobile
|
||||||
|
|
||||||
## 6. Testing & Polish
|
## 6. Testing & Polish
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user