feat(ui): sidebar project tree, status bar and a tabbed workbench for workspaces
The Workspace page's lane strip moves into the sidebar as a project tree: workspaces grouped by git repository (new GET /api/projects, cached in memory, POST /api/projects/refresh), untracked worktrees with Adopt, inline rename, drag/Alt+arrow ordering and a right-click row menu. The selected workspace is the URL's ?lane=, one shared lane list feeds the tree, the new bottom status bar and the page. The Changes panel gains an activity bar (Files / Changes / Sessions, with resume), and split panes get a header.
This commit is contained in:
@@ -73,11 +73,13 @@ import {
|
||||
Sun,
|
||||
Monitor,
|
||||
Moon,
|
||||
FolderTree,
|
||||
} from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { api } from "../lib/api";
|
||||
import { eventBus } from "../lib/eventBus";
|
||||
import { useTheme } from "../hooks/useTheme";
|
||||
import { ProjectTree } from "./ProjectTree";
|
||||
import type { UpdateStatusPayload, WSMessage } from "../lib/types";
|
||||
|
||||
function isUpdatePayload(x: unknown): x is UpdateStatusPayload {
|
||||
@@ -174,9 +176,18 @@ interface SidebarProps {
|
||||
wsConnected: boolean;
|
||||
collapsed: boolean;
|
||||
onToggle: () => void;
|
||||
/** The connection details modal, opened from here or from the status bar. */
|
||||
connectionOpen: boolean;
|
||||
onConnectionOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
export function Sidebar({
|
||||
wsConnected,
|
||||
collapsed,
|
||||
onToggle,
|
||||
connectionOpen,
|
||||
onConnectionOpenChange,
|
||||
}: SidebarProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { theme, mode, setMode, toggleTheme } = useTheme();
|
||||
// Track whether nav items are clipped by overflow so we can render
|
||||
@@ -187,7 +198,6 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
const [updateStatus, setUpdateStatus] = useState<UpdateStatusPayload | null>(null);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [checkError, setCheckError] = useState(false);
|
||||
const [statusModalOpen, setStatusModalOpen] = useState(false);
|
||||
const [connectedSince, setConnectedSince] = useState<number | null>(
|
||||
wsConnected ? Date.now() : null
|
||||
);
|
||||
@@ -464,6 +474,21 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
{/* The project → workspace tree scrolls with the nav. In the rail it
|
||||
is one icon that opens the sidebar instead. */}
|
||||
{collapsed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
title={t("lanes:projects.title")}
|
||||
aria-label={t("lanes:projects.title")}
|
||||
className="flex w-full items-center justify-center rounded-lg border border-transparent px-2 py-2.5 text-fg-secondary transition-colors hover:bg-surface-3 hover:text-fg-primary"
|
||||
>
|
||||
<FolderTree className="w-4 h-4" />
|
||||
</button>
|
||||
) : (
|
||||
<ProjectTree />
|
||||
)}
|
||||
</nav>
|
||||
{!collapsed && navOverflow.up && (
|
||||
<button
|
||||
@@ -620,41 +645,43 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
<div
|
||||
className={`px-3 pt-3 pb-4 border-t border-border space-y-2.5 flex-shrink-0 ${collapsed ? "px-2" : ""}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStatusModalOpen(true)}
|
||||
aria-label={t("nav:connectionDetails")}
|
||||
title={t("nav:connectionDetails")}
|
||||
className={`rounded-lg border border-border bg-surface-2 hover:bg-surface-3 transition-colors text-left cursor-pointer ${
|
||||
collapsed
|
||||
? "w-8 h-8 mx-auto flex items-center justify-center p-0"
|
||||
: "block w-full px-2.5 py-2"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center text-xs ${collapsed ? "justify-center" : "justify-between gap-2"}`}
|
||||
{collapsed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onConnectionOpenChange(true)}
|
||||
aria-label={t("nav:connectionDetails")}
|
||||
title={t("nav:connectionDetails")}
|
||||
className={`rounded-lg border border-border bg-surface-2 hover:bg-surface-3 transition-colors text-left cursor-pointer ${
|
||||
collapsed
|
||||
? "w-8 h-8 mx-auto flex items-center justify-center p-0"
|
||||
: "block w-full px-2.5 py-2"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-flex items-center gap-2 ${
|
||||
wsConnected ? "text-status-success" : "text-fg-muted"
|
||||
}`}
|
||||
<div
|
||||
className={`flex items-center text-xs ${collapsed ? "justify-center" : "justify-between gap-2"}`}
|
||||
>
|
||||
{wsConnected ? (
|
||||
<Wifi className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
) : (
|
||||
<WifiOff className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className={`inline-flex items-center gap-2 ${
|
||||
wsConnected ? "text-status-success" : "text-fg-muted"
|
||||
}`}
|
||||
>
|
||||
{wsConnected ? (
|
||||
<Wifi className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
) : (
|
||||
<WifiOff className="w-3.5 h-3.5 flex-shrink-0" />
|
||||
)}
|
||||
{!collapsed && (
|
||||
<span className="font-medium">
|
||||
{wsConnected ? t("nav:live") : t("nav:disconnected")}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{!collapsed && (
|
||||
<span className="font-medium">
|
||||
{wsConnected ? t("nav:live") : t("nav:disconnected")}
|
||||
</span>
|
||||
<span className="text-[11px] font-medium text-fg-muted">{`v${__APP_VERSION__}`}</span>
|
||||
)}
|
||||
</span>
|
||||
{!collapsed && (
|
||||
<span className="text-[11px] font-medium text-fg-muted">{`v${__APP_VERSION__}`}</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
{collapsed ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -704,8 +731,8 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
|
||||
</div>
|
||||
|
||||
<ConnectionStatusModal
|
||||
open={statusModalOpen}
|
||||
onClose={() => setStatusModalOpen(false)}
|
||||
open={connectionOpen}
|
||||
onClose={() => onConnectionOpenChange(false)}
|
||||
wsConnected={wsConnected}
|
||||
connectedSince={connectedSince}
|
||||
eventCountRef={eventCountRef}
|
||||
|
||||
Reference in New Issue
Block a user