Skip to content

Commit 15786fd

Browse files
committed
feat: support collapsing project sidebar!
1 parent a0a5b1d commit 15786fd

File tree

3 files changed

+174
-126
lines changed

3 files changed

+174
-126
lines changed

src/App.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ function App() {
150150
);
151151
const [workspaceModalOpen, setWorkspaceModalOpen] = useState(false);
152152
const [workspaceModalProject, setWorkspaceModalProject] = useState<string | null>(null);
153+
const [sidebarCollapsed, setSidebarCollapsed] = usePersistedState("sidebarCollapsed", false);
153154

154155
useEffect(() => {
155156
void loadProjects();
@@ -362,7 +363,7 @@ function App() {
362363
[selectedWorkspace, projects, workspaceMetadata, setSelectedWorkspace]
363364
);
364365

365-
// Handle workspace navigation keyboard shortcuts
366+
// Handle keyboard shortcuts
366367
useEffect(() => {
367368
const handleKeyDown = (e: KeyboardEvent) => {
368369
if (matchesKeybind(e, KEYBINDS.NEXT_WORKSPACE)) {
@@ -371,12 +372,15 @@ function App() {
371372
} else if (matchesKeybind(e, KEYBINDS.PREV_WORKSPACE)) {
372373
e.preventDefault();
373374
handleNavigateWorkspace("prev");
375+
} else if (matchesKeybind(e, KEYBINDS.TOGGLE_SIDEBAR)) {
376+
e.preventDefault();
377+
setSidebarCollapsed((prev) => !prev);
374378
}
375379
};
376380

377381
window.addEventListener("keydown", handleKeyDown);
378382
return () => window.removeEventListener("keydown", handleKeyDown);
379-
}, [handleNavigateWorkspace]);
383+
}, [handleNavigateWorkspace, setSidebarCollapsed]);
380384

381385
return (
382386
<>
@@ -394,6 +398,8 @@ function App() {
394398
onRemoveProject={(path) => void handleRemoveProject(path)}
395399
onRemoveWorkspace={handleRemoveWorkspace}
396400
onRenameWorkspace={handleRenameWorkspace}
401+
collapsed={sidebarCollapsed}
402+
onToggleCollapsed={() => setSidebarCollapsed((prev) => !prev)}
397403
/>
398404
<MainContent>
399405
<AppHeader>

src/components/ProjectSidebar.tsx

Lines changed: 163 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ import { abbreviatePath } from "@/utils/ui/pathAbbreviation";
99
import { TooltipWrapper, Tooltip } from "./Tooltip";
1010

1111
// Styled Components
12-
const SidebarContainer = styled.div`
13-
width: 280px;
12+
const SidebarContainer = styled.div<{ collapsed?: boolean }>`
13+
width: ${(props) => (props.collapsed ? "32px" : "280px")};
1414
height: 100vh;
1515
background: #252526;
1616
border-right: 1px solid #1e1e1e;
1717
display: flex;
1818
flex-direction: column;
1919
flex-shrink: 0;
2020
font-family: var(--font-primary);
21+
transition: width 0.2s ease;
22+
overflow: hidden;
2123
`;
2224

2325
const SidebarHeader = styled.div`
@@ -58,6 +60,28 @@ const AddProjectBtn = styled.button`
5860
}
5961
`;
6062

63+
const CollapseButton = styled.button`
64+
width: 100%;
65+
height: 36px;
66+
background: transparent;
67+
color: #888;
68+
border: none;
69+
border-top: 1px solid #1e1e1e;
70+
cursor: pointer;
71+
font-size: 14px;
72+
display: flex;
73+
align-items: center;
74+
justify-content: center;
75+
padding: 0;
76+
transition: all 0.2s;
77+
margin-top: auto;
78+
79+
&:hover {
80+
background: #2a2a2b;
81+
color: #ccc;
82+
}
83+
`;
84+
6185
const ProjectsList = styled.div`
6286
flex: 1;
6387
overflow-y: auto;
@@ -337,6 +361,8 @@ interface ProjectSidebarProps {
337361
workspaceId: string,
338362
newName: string
339363
) => Promise<{ success: boolean; error?: string }>;
364+
collapsed: boolean;
365+
onToggleCollapsed: () => void;
340366
}
341367

342368
const ProjectSidebar: React.FC<ProjectSidebarProps> = ({
@@ -349,6 +375,8 @@ const ProjectSidebar: React.FC<ProjectSidebarProps> = ({
349375
onRemoveProject,
350376
onRemoveWorkspace,
351377
onRenameWorkspace,
378+
collapsed,
379+
onToggleCollapsed,
352380
}) => {
353381
// Store as array in localStorage, convert to Set for usage
354382
const [expandedProjectsArray, setExpandedProjectsArray] = usePersistedState<string[]>(
@@ -455,130 +483,141 @@ const ProjectSidebar: React.FC<ProjectSidebarProps> = ({
455483
}, [selectedWorkspace, onAddWorkspace]);
456484

457485
return (
458-
<SidebarContainer>
459-
<SidebarHeader>
460-
<h2>Projects</h2>
461-
<AddProjectBtn onClick={onAddProject} title="Add Project">
462-
+
463-
</AddProjectBtn>
464-
</SidebarHeader>
465-
<ProjectsList>
466-
{projects.size === 0 ? (
467-
<EmptyState>
468-
<p>No projects</p>
469-
<AddFirstProjectBtn onClick={onAddProject}>Add Project</AddFirstProjectBtn>
470-
</EmptyState>
471-
) : (
472-
Array.from(projects.entries()).map(([projectPath, config]) => (
473-
<ProjectGroup key={projectPath}>
474-
<ProjectItem onClick={() => toggleProject(projectPath)}>
475-
<ExpandIcon expanded={expandedProjects.has(projectPath)}></ExpandIcon>
476-
<ProjectInfo>
477-
<ProjectName>{getProjectName(projectPath)}</ProjectName>
478-
<TooltipWrapper inline>
479-
<ProjectPath>{abbreviatePath(projectPath)}</ProjectPath>
480-
<Tooltip className="tooltip" align="left">
481-
{projectPath}
482-
</Tooltip>
483-
</TooltipWrapper>
484-
</ProjectInfo>
485-
<TooltipWrapper inline>
486-
<RemoveBtn
487-
onClick={(e) => {
488-
e.stopPropagation();
489-
onRemoveProject(projectPath);
490-
}}
491-
>
492-
×
493-
</RemoveBtn>
494-
<Tooltip className="tooltip" align="right">
495-
Remove project
496-
</Tooltip>
497-
</TooltipWrapper>
498-
</ProjectItem>
499-
500-
{expandedProjects.has(projectPath) && (
501-
<WorkspacesContainer>
502-
<WorkspaceHeader>
503-
<AddWorkspaceBtn onClick={() => onAddWorkspace(projectPath)}>
504-
+ New Workspace
505-
{selectedWorkspace?.projectPath === projectPath &&
506-
` (${formatKeybind(KEYBINDS.NEW_WORKSPACE)})`}
507-
</AddWorkspaceBtn>
508-
</WorkspaceHeader>
509-
{config.workspaces.map((workspace) => {
510-
const projectName = getProjectName(projectPath);
511-
const metadata = workspaceMetadata.get(workspace.path);
512-
if (!metadata) return null; // Skip if metadata not loaded yet
513-
514-
const workspaceId = metadata.id;
515-
const displayName = getWorkspaceDisplayName(workspace.path);
516-
const isActive = false; // Simplified - no active state tracking
517-
const isEditing = editingWorkspaceId === workspaceId;
518-
519-
return (
520-
<WorkspaceItem
521-
key={workspace.path}
522-
selected={selectedWorkspace?.workspacePath === workspace.path}
523-
onClick={() =>
524-
onSelectWorkspace({
525-
projectPath,
526-
projectName,
527-
workspacePath: workspace.path,
528-
workspaceId,
529-
})
530-
}
486+
<SidebarContainer collapsed={collapsed}>
487+
{!collapsed && (
488+
<>
489+
<SidebarHeader>
490+
<h2>Projects</h2>
491+
<AddProjectBtn onClick={onAddProject} title="Add Project">
492+
+
493+
</AddProjectBtn>
494+
</SidebarHeader>
495+
<ProjectsList>
496+
{projects.size === 0 ? (
497+
<EmptyState>
498+
<p>No projects</p>
499+
<AddFirstProjectBtn onClick={onAddProject}>Add Project</AddFirstProjectBtn>
500+
</EmptyState>
501+
) : (
502+
Array.from(projects.entries()).map(([projectPath, config]) => (
503+
<ProjectGroup key={projectPath}>
504+
<ProjectItem onClick={() => toggleProject(projectPath)}>
505+
<ExpandIcon expanded={expandedProjects.has(projectPath)}></ExpandIcon>
506+
<ProjectInfo>
507+
<ProjectName>{getProjectName(projectPath)}</ProjectName>
508+
<TooltipWrapper inline>
509+
<ProjectPath>{abbreviatePath(projectPath)}</ProjectPath>
510+
<Tooltip className="tooltip" align="left">
511+
{projectPath}
512+
</Tooltip>
513+
</TooltipWrapper>
514+
</ProjectInfo>
515+
<TooltipWrapper inline>
516+
<RemoveBtn
517+
onClick={(e) => {
518+
e.stopPropagation();
519+
onRemoveProject(projectPath);
520+
}}
531521
>
532-
<StatusIndicator active={isActive} title="AI Assistant" />
533-
<BranchIcon></BranchIcon>
534-
{isEditing ? (
535-
<WorkspaceNameInput
536-
value={editingName}
537-
onChange={(e) => setEditingName(e.target.value)}
538-
onKeyDown={(e) => handleRenameKeyDown(e, workspaceId)}
539-
onBlur={() => void confirmRename(workspaceId)}
540-
autoFocus
541-
onClick={(e) => e.stopPropagation()}
542-
/>
543-
) : (
544-
<WorkspaceName
545-
onDoubleClick={(e) => {
546-
e.stopPropagation();
547-
startRenaming(workspaceId, displayName);
548-
}}
549-
title="Double-click to rename"
550-
>
551-
{displayName}
552-
</WorkspaceName>
553-
)}
554-
<TooltipWrapper inline>
555-
<WorkspaceRemoveBtn
556-
onClick={(e) => {
557-
e.stopPropagation();
558-
void handleRemoveWorkspace(workspaceId);
559-
}}
522+
×
523+
</RemoveBtn>
524+
<Tooltip className="tooltip" align="right">
525+
Remove project
526+
</Tooltip>
527+
</TooltipWrapper>
528+
</ProjectItem>
529+
530+
{expandedProjects.has(projectPath) && (
531+
<WorkspacesContainer>
532+
<WorkspaceHeader>
533+
<AddWorkspaceBtn onClick={() => onAddWorkspace(projectPath)}>
534+
+ New Workspace
535+
{selectedWorkspace?.projectPath === projectPath &&
536+
` (${formatKeybind(KEYBINDS.NEW_WORKSPACE)})`}
537+
</AddWorkspaceBtn>
538+
</WorkspaceHeader>
539+
{config.workspaces.map((workspace) => {
540+
const projectName = getProjectName(projectPath);
541+
const metadata = workspaceMetadata.get(workspace.path);
542+
if (!metadata) return null; // Skip if metadata not loaded yet
543+
544+
const workspaceId = metadata.id;
545+
const displayName = getWorkspaceDisplayName(workspace.path);
546+
const isActive = false; // Simplified - no active state tracking
547+
const isEditing = editingWorkspaceId === workspaceId;
548+
549+
return (
550+
<WorkspaceItem
551+
key={workspace.path}
552+
selected={selectedWorkspace?.workspacePath === workspace.path}
553+
onClick={() =>
554+
onSelectWorkspace({
555+
projectPath,
556+
projectName,
557+
workspacePath: workspace.path,
558+
workspaceId,
559+
})
560+
}
560561
>
561-
×
562-
</WorkspaceRemoveBtn>
563-
<Tooltip className="tooltip" align="right">
564-
Remove workspace
565-
</Tooltip>
566-
</TooltipWrapper>
567-
{isEditing && renameError && (
568-
<WorkspaceErrorContainer>{renameError}</WorkspaceErrorContainer>
569-
)}
570-
{!isEditing && removeError?.workspaceId === workspaceId && (
571-
<WorkspaceErrorContainer>{removeError.error}</WorkspaceErrorContainer>
572-
)}
573-
</WorkspaceItem>
574-
);
575-
})}
576-
</WorkspacesContainer>
577-
)}
578-
</ProjectGroup>
579-
))
580-
)}
581-
</ProjectsList>
562+
<StatusIndicator active={isActive} title="AI Assistant" />
563+
<BranchIcon></BranchIcon>
564+
{isEditing ? (
565+
<WorkspaceNameInput
566+
value={editingName}
567+
onChange={(e) => setEditingName(e.target.value)}
568+
onKeyDown={(e) => handleRenameKeyDown(e, workspaceId)}
569+
onBlur={() => void confirmRename(workspaceId)}
570+
autoFocus
571+
onClick={(e) => e.stopPropagation()}
572+
/>
573+
) : (
574+
<WorkspaceName
575+
onDoubleClick={(e) => {
576+
e.stopPropagation();
577+
startRenaming(workspaceId, displayName);
578+
}}
579+
title="Double-click to rename"
580+
>
581+
{displayName}
582+
</WorkspaceName>
583+
)}
584+
<TooltipWrapper inline>
585+
<WorkspaceRemoveBtn
586+
onClick={(e) => {
587+
e.stopPropagation();
588+
void handleRemoveWorkspace(workspaceId);
589+
}}
590+
>
591+
×
592+
</WorkspaceRemoveBtn>
593+
<Tooltip className="tooltip" align="right">
594+
Remove workspace
595+
</Tooltip>
596+
</TooltipWrapper>
597+
{isEditing && renameError && (
598+
<WorkspaceErrorContainer>{renameError}</WorkspaceErrorContainer>
599+
)}
600+
{!isEditing && removeError?.workspaceId === workspaceId && (
601+
<WorkspaceErrorContainer>{removeError.error}</WorkspaceErrorContainer>
602+
)}
603+
</WorkspaceItem>
604+
);
605+
})}
606+
</WorkspacesContainer>
607+
)}
608+
</ProjectGroup>
609+
))
610+
)}
611+
</ProjectsList>
612+
</>
613+
)}
614+
<TooltipWrapper inline>
615+
<CollapseButton onClick={onToggleCollapsed}>{collapsed ? "»" : "«"}</CollapseButton>
616+
<Tooltip className="tooltip" align="center">
617+
{collapsed ? "Expand sidebar" : "Collapse sidebar"} (
618+
{formatKeybind(KEYBINDS.TOGGLE_SIDEBAR)})
619+
</Tooltip>
620+
</TooltipWrapper>
582621
</SidebarContainer>
583622
);
584623
};

src/utils/ui/keybinds.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,7 @@ export const KEYBINDS = {
132132

133133
/** Navigate to previous workspace in current project */
134134
PREV_WORKSPACE: { key: "k", ctrl: true },
135+
136+
/** Toggle sidebar visibility */
137+
TOGGLE_SIDEBAR: { key: "P", ctrl: true, shift: true },
135138
} as const;

0 commit comments

Comments
 (0)