diff --git a/app/src/components/layout/Sidebar.test.tsx b/app/src/components/layout/Sidebar.test.tsx index 5800c91..be5c22c 100644 --- a/app/src/components/layout/Sidebar.test.tsx +++ b/app/src/components/layout/Sidebar.test.tsx @@ -8,6 +8,9 @@ vi.mock("../../store/appState", () => ({ selector({ sidebarView: "projects", setSidebarView: vi.fn(), + sidebarCollapsed: false, + setSidebarCollapsed: vi.fn(), + toggleSidebarCollapsed: vi.fn(), }) ), })); diff --git a/app/src/components/layout/Sidebar.tsx b/app/src/components/layout/Sidebar.tsx index 2eb6ac6..2c3bbfc 100644 --- a/app/src/components/layout/Sidebar.tsx +++ b/app/src/components/layout/Sidebar.tsx @@ -1,15 +1,100 @@ +import type { ReactNode } from "react"; import { useShallow } from "zustand/react/shallow"; import { useAppState } from "../../store/appState"; import ProjectList from "../projects/ProjectList"; import McpPanel from "../mcp/McpPanel"; import SettingsPanel from "../settings/SettingsPanel"; +type SidebarView = "projects" | "mcp" | "settings"; + +const RAIL_ICONS: { view: SidebarView; label: string; icon: ReactNode }[] = [ + { + view: "projects", + label: "Projects", + icon: ( + + + + ), + }, + { + view: "mcp", + label: "MCP", + icon: ( + + + + + + + ), + }, + { + view: "settings", + label: "Settings", + icon: ( + + + + + ), + }, +]; + export default function Sidebar() { - const { sidebarView, setSidebarView } = useAppState( - useShallow(s => ({ sidebarView: s.sidebarView, setSidebarView: s.setSidebarView })) + const { sidebarView, setSidebarView, sidebarCollapsed, setSidebarCollapsed, toggleSidebarCollapsed } = useAppState( + useShallow(s => ({ + sidebarView: s.sidebarView, + setSidebarView: s.setSidebarView, + sidebarCollapsed: s.sidebarCollapsed, + setSidebarCollapsed: s.setSidebarCollapsed, + toggleSidebarCollapsed: s.toggleSidebarCollapsed, + })) ); - const tabCls = (view: typeof sidebarView) => + if (sidebarCollapsed) { + const railBtn = (view: SidebarView, label: string, icon: ReactNode) => { + const active = sidebarView === view; + return ( + + ); + }; + + return ( +
+ +
+ {RAIL_ICONS.map(({ view, label, icon }) => railBtn(view, label, icon))} +
+
+ ); + } + + const tabCls = (view: SidebarView) => `flex-1 px-3 py-2 text-sm font-medium transition-colors ${ sidebarView === view ? "text-[var(--accent)] border-b-2 border-[var(--accent)]" @@ -29,6 +114,16 @@ export default function Sidebar() { + {/* Content */} diff --git a/app/src/store/appState.ts b/app/src/store/appState.ts index 37de139..bf9cdb0 100644 --- a/app/src/store/appState.ts +++ b/app/src/store/appState.ts @@ -1,6 +1,24 @@ import { create } from "zustand"; import type { Project, TerminalSession, AppSettings, UpdateInfo, ImageUpdateInfo, McpServer } from "../lib/types"; +const SIDEBAR_COLLAPSED_KEY = "triple-c.sidebar.collapsed"; + +function loadSidebarCollapsed(): boolean { + try { + return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === "1"; + } catch { + return false; + } +} + +function persistSidebarCollapsed(value: boolean) { + try { + localStorage.setItem(SIDEBAR_COLLAPSED_KEY, value ? "1" : "0"); + } catch { + // ignore — storage may be unavailable + } +} + interface AppState { // Projects projects: Project[]; @@ -28,6 +46,9 @@ interface AppState { setTerminalHasSelection: (has: boolean) => void; sidebarView: "projects" | "mcp" | "settings"; setSidebarView: (view: "projects" | "mcp" | "settings") => void; + sidebarCollapsed: boolean; + setSidebarCollapsed: (collapsed: boolean) => void; + toggleSidebarCollapsed: () => void; dockerAvailable: boolean | null; setDockerAvailable: (available: boolean | null) => void; imageExists: boolean | null; @@ -106,6 +127,17 @@ export const useAppState = create((set) => ({ setTerminalHasSelection: (has) => set({ terminalHasSelection: has }), sidebarView: "projects", setSidebarView: (view) => set({ sidebarView: view }), + sidebarCollapsed: loadSidebarCollapsed(), + setSidebarCollapsed: (collapsed) => { + persistSidebarCollapsed(collapsed); + set({ sidebarCollapsed: collapsed }); + }, + toggleSidebarCollapsed: () => + set((state) => { + const next = !state.sidebarCollapsed; + persistSidebarCollapsed(next); + return { sidebarCollapsed: next }; + }), dockerAvailable: null, setDockerAvailable: (available) => set({ dockerAvailable: available }), imageExists: null,