Skip to content

Commit e2bb6c1

Browse files
committed
feat: add sidebar toggle controls
1 parent 9e305fd commit e2bb6c1

8 files changed

Lines changed: 334 additions & 5 deletions

File tree

src/App.tsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ import { useWorkspaceRefreshOnFocus } from "./features/workspaces/hooks/useWorks
4949
import { useWorkspaceRestore } from "./features/workspaces/hooks/useWorkspaceRestore";
5050
import { useResizablePanels } from "./features/layout/hooks/useResizablePanels";
5151
import { useLayoutMode } from "./features/layout/hooks/useLayoutMode";
52+
import { useSidebarToggles } from "./features/layout/hooks/useSidebarToggles";
53+
import {
54+
RightPanelCollapseButton,
55+
SidebarCollapseButton,
56+
TitlebarExpandControls,
57+
} from "./features/layout/components/SidebarToggleControls";
5258
import { useAppSettings } from "./features/settings/hooks/useAppSettings";
5359
import { useUpdater } from "./features/update/hooks/useUpdater";
5460
import { useComposerImages } from "./features/composer/hooks/useComposerImages";
@@ -111,6 +117,23 @@ function MainApp() {
111117
const isCompact = layoutMode !== "desktop";
112118
const isTablet = layoutMode === "tablet";
113119
const isPhone = layoutMode === "phone";
120+
const {
121+
sidebarCollapsed,
122+
rightPanelCollapsed,
123+
collapseSidebar,
124+
expandSidebar,
125+
collapseRightPanel,
126+
expandRightPanel,
127+
} = useSidebarToggles({ isCompact });
128+
const sidebarToggleProps = {
129+
isCompact,
130+
sidebarCollapsed,
131+
rightPanelCollapsed,
132+
onCollapseSidebar: collapseSidebar,
133+
onExpandSidebar: expandSidebar,
134+
onCollapseRightPanel: collapseRightPanel,
135+
onExpandRightPanel: expandRightPanel,
136+
};
114137
const [centerMode, setCenterMode] = useState<"chat" | "diff">("chat");
115138
const [selectedDiffPath, setSelectedDiffPath] = useState<string | null>(null);
116139
const [gitPanelMode, setGitPanelMode] = useState<"diff" | "log" | "issues">(
@@ -644,6 +667,8 @@ function MainApp() {
644667
isPhone ? " layout-phone" : ""
645668
}${isTablet ? " layout-tablet" : ""}${
646669
reduceTransparency ? " reduced-transparency" : ""
670+
}${!isCompact && sidebarCollapsed ? " sidebar-collapsed" : ""}${
671+
!isCompact && rightPanelCollapsed ? " right-panel-collapsed" : ""
647672
}`;
648673
const {
649674
sidebarNode,
@@ -766,6 +791,9 @@ function MainApp() {
766791
onCopyThread: handleCopyThread,
767792
onToggleTerminal: handleToggleTerminal,
768793
showTerminalButton: !isCompact,
794+
mainHeaderActionsNode: !isCompact && !rightPanelCollapsed ? (
795+
<RightPanelCollapseButton {...sidebarToggleProps} />
796+
) : null,
769797
filePanelMode,
770798
onToggleFilePanel: () => {
771799
setFilePanelMode((prev) => (prev === "git" ? "files" : "git"));
@@ -879,13 +907,26 @@ function MainApp() {
879907
onGoProjects: () => setActiveTab("projects"),
880908
});
881909

910+
const desktopTopbarLeftNodeWithToggle = !isCompact ? (
911+
<div className="topbar-leading">
912+
<SidebarCollapseButton {...sidebarToggleProps} />
913+
{desktopTopbarLeftNode}
914+
</div>
915+
) : (
916+
desktopTopbarLeftNode
917+
);
918+
882919
return (
883920
<div
884921
className={appClassName}
885922
style={
886923
{
887-
"--sidebar-width": `${sidebarWidth}px`,
888-
"--right-panel-width": `${rightPanelWidth}px`,
924+
"--sidebar-width": `${
925+
isCompact ? sidebarWidth : sidebarCollapsed ? 0 : sidebarWidth
926+
}px`,
927+
"--right-panel-width": `${
928+
isCompact ? rightPanelWidth : rightPanelCollapsed ? 0 : rightPanelWidth
929+
}px`,
889930
"--plan-panel-height": `${planPanelHeight}px`,
890931
"--terminal-panel-height": `${terminalPanelHeight}px`,
891932
"--debug-panel-height": `${debugPanelHeight}px`,
@@ -894,6 +935,7 @@ function MainApp() {
894935
}
895936
>
896937
<div className="drag-strip" id="titlebar" data-tauri-drag-region />
938+
<TitlebarExpandControls {...sidebarToggleProps} />
897939
{isPhone ? (
898940
<PhoneLayout
899941
approvalToastsNode={approvalToastsNode}
@@ -939,7 +981,7 @@ function MainApp() {
939981
homeNode={homeNode}
940982
showHome={showHome}
941983
showWorkspace={Boolean(activeWorkspace && !showHome)}
942-
topbarLeftNode={desktopTopbarLeftNode}
984+
topbarLeftNode={desktopTopbarLeftNodeWithToggle}
943985
centerMode={centerMode}
944986
messagesNode={messagesNode}
945987
gitDiffViewerNode={gitDiffViewerNode}

src/features/app/components/MainHeader.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Check, ChevronDown, Copy, Terminal } from "lucide-react";
33
import { revealItemInDir } from "@tauri-apps/plugin-opener";
44
import { openWorkspaceIn } from "../../../services/tauri";
55
import type { BranchInfo, WorkspaceInfo } from "../../../types";
6+
import type { ReactNode } from "react";
67
import { OPEN_APP_STORAGE_KEY, type OpenAppId } from "../constants";
78
import { getStoredOpenAppId } from "../utils/openApp";
89
import cursorIcon from "../../../assets/app-icons/cursor.png";
@@ -25,6 +26,7 @@ type MainHeaderProps = {
2526
onToggleTerminal: () => void;
2627
isTerminalOpen: boolean;
2728
showTerminalButton?: boolean;
29+
extraActionsNode?: ReactNode;
2830
};
2931

3032
type OpenTarget = {
@@ -50,6 +52,7 @@ export function MainHeader({
5052
onToggleTerminal,
5153
isTerminalOpen,
5254
showTerminalButton = true,
55+
extraActionsNode,
5356
}: MainHeaderProps) {
5457
const [menuOpen, setMenuOpen] = useState(false);
5558
const [infoOpen, setInfoOpen] = useState(false);
@@ -414,6 +417,7 @@ export function MainHeader({
414417
<Check className="main-header-icon-check" size={14} />
415418
</span>
416419
</button>
420+
{extraActionsNode}
417421
</div>
418422
</header>
419423
);
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {
2+
PanelLeftClose,
3+
PanelLeftOpen,
4+
PanelRightClose,
5+
PanelRightOpen,
6+
} from "lucide-react";
7+
8+
type SidebarToggleControlsProps = {
9+
isCompact: boolean;
10+
sidebarCollapsed: boolean;
11+
rightPanelCollapsed: boolean;
12+
onCollapseSidebar: () => void;
13+
onExpandSidebar: () => void;
14+
onCollapseRightPanel: () => void;
15+
onExpandRightPanel: () => void;
16+
};
17+
18+
export function SidebarCollapseButton({
19+
isCompact,
20+
sidebarCollapsed,
21+
onCollapseSidebar,
22+
}: SidebarToggleControlsProps) {
23+
if (isCompact || sidebarCollapsed) {
24+
return null;
25+
}
26+
return (
27+
<button
28+
type="button"
29+
className="ghost main-header-action"
30+
onClick={onCollapseSidebar}
31+
data-tauri-drag-region="false"
32+
aria-label="Hide threads sidebar"
33+
title="Hide threads sidebar"
34+
>
35+
<PanelLeftClose size={14} aria-hidden />
36+
</button>
37+
);
38+
}
39+
40+
export function RightPanelCollapseButton({
41+
isCompact,
42+
rightPanelCollapsed,
43+
onCollapseRightPanel,
44+
}: SidebarToggleControlsProps) {
45+
if (isCompact || rightPanelCollapsed) {
46+
return null;
47+
}
48+
return (
49+
<button
50+
type="button"
51+
className="ghost main-header-action"
52+
onClick={onCollapseRightPanel}
53+
data-tauri-drag-region="false"
54+
aria-label="Hide git sidebar"
55+
title="Hide git sidebar"
56+
>
57+
<PanelRightClose size={14} aria-hidden />
58+
</button>
59+
);
60+
}
61+
62+
export function TitlebarExpandControls({
63+
isCompact,
64+
sidebarCollapsed,
65+
rightPanelCollapsed,
66+
onExpandSidebar,
67+
onExpandRightPanel,
68+
}: SidebarToggleControlsProps) {
69+
if (isCompact || (!sidebarCollapsed && !rightPanelCollapsed)) {
70+
return null;
71+
}
72+
return (
73+
<div className="titlebar-controls">
74+
{sidebarCollapsed && (
75+
<div className="titlebar-toggle titlebar-toggle-left">
76+
<button
77+
type="button"
78+
className="ghost main-header-action"
79+
onClick={onExpandSidebar}
80+
data-tauri-drag-region="false"
81+
aria-label="Show threads sidebar"
82+
title="Show threads sidebar"
83+
>
84+
<PanelLeftOpen size={14} aria-hidden />
85+
</button>
86+
</div>
87+
)}
88+
{rightPanelCollapsed && (
89+
<div className="titlebar-toggle titlebar-toggle-right">
90+
<button
91+
type="button"
92+
className="ghost main-header-action"
93+
onClick={onExpandRightPanel}
94+
data-tauri-drag-region="false"
95+
aria-label="Show git sidebar"
96+
title="Show git sidebar"
97+
>
98+
<PanelRightOpen size={14} aria-hidden />
99+
</button>
100+
</div>
101+
)}
102+
</div>
103+
);
104+
}

src/features/layout/hooks/useLayoutNodes.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ type LayoutNodesOptions = {
111111
onCopyThread: () => void | Promise<void>;
112112
onToggleTerminal: () => void;
113113
showTerminalButton: boolean;
114+
mainHeaderActionsNode?: ReactNode;
114115
centerMode: "chat" | "diff";
115116
onExitDiff: () => void;
116117
activeTab: "projects" | "codex" | "git" | "log";
@@ -357,6 +358,7 @@ export function useLayoutNodes(options: LayoutNodesOptions): LayoutNodesResult {
357358
onToggleTerminal={options.onToggleTerminal}
358359
isTerminalOpen={options.terminalOpen}
359360
showTerminalButton={options.showTerminalButton}
361+
extraActionsNode={options.mainHeaderActionsNode}
360362
/>
361363
) : null;
362364

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { useEffect, useState } from "react";
2+
3+
const SIDEBAR_COLLAPSED_KEY = "codexmonitor.sidebarCollapsed";
4+
const RIGHT_PANEL_COLLAPSED_KEY = "codexmonitor.rightPanelCollapsed";
5+
6+
type UseSidebarTogglesOptions = {
7+
isCompact: boolean;
8+
};
9+
10+
function readStoredBool(key: string) {
11+
if (typeof window === "undefined") {
12+
return false;
13+
}
14+
return window.localStorage.getItem(key) === "true";
15+
}
16+
17+
export function useSidebarToggles({ isCompact }: UseSidebarTogglesOptions) {
18+
const [sidebarCollapsed, setSidebarCollapsed] = useState(() =>
19+
readStoredBool(SIDEBAR_COLLAPSED_KEY),
20+
);
21+
const [rightPanelCollapsed, setRightPanelCollapsed] = useState(() =>
22+
readStoredBool(RIGHT_PANEL_COLLAPSED_KEY),
23+
);
24+
25+
useEffect(() => {
26+
window.localStorage.setItem(
27+
SIDEBAR_COLLAPSED_KEY,
28+
String(sidebarCollapsed),
29+
);
30+
}, [sidebarCollapsed]);
31+
32+
useEffect(() => {
33+
window.localStorage.setItem(
34+
RIGHT_PANEL_COLLAPSED_KEY,
35+
String(rightPanelCollapsed),
36+
);
37+
}, [rightPanelCollapsed]);
38+
39+
const collapseSidebar = () => {
40+
if (!isCompact) {
41+
setSidebarCollapsed(true);
42+
}
43+
};
44+
45+
const expandSidebar = () => {
46+
if (!isCompact) {
47+
setSidebarCollapsed(false);
48+
}
49+
};
50+
51+
const collapseRightPanel = () => {
52+
if (!isCompact) {
53+
setRightPanelCollapsed(true);
54+
}
55+
};
56+
57+
const expandRightPanel = () => {
58+
if (!isCompact) {
59+
setRightPanelCollapsed(false);
60+
}
61+
};
62+
63+
return {
64+
sidebarCollapsed,
65+
rightPanelCollapsed,
66+
collapseSidebar,
67+
expandSidebar,
68+
collapseRightPanel,
69+
expandRightPanel,
70+
};
71+
}

src/styles/base.css

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@
5454
--border-accent-soft: rgba(100, 200, 255, 0.3);
5555
--text-accent: rgba(164, 195, 255, 0.7);
5656
--shadow-accent: rgba(92, 168, 255, 0.28);
57+
--titlebar-inset-left: 72px;
58+
--titlebar-height: 24px;
59+
--titlebar-toggle-size: 28px;
60+
--titlebar-toggle-offset: 4px;
61+
--titlebar-toggle-title-offset: -6px;
62+
--titlebar-toggle-side-gap: 12px;
63+
--topbar-compact-padding: 16px;
5764
--status-success: rgba(120, 235, 190, 0.95);
5865
--status-warning: rgba(255, 175, 85, 0.95);
5966
--status-error: rgba(255, 110, 110, 0.95);
@@ -203,17 +210,47 @@ body {
203210
position: relative;
204211
transform: scale(var(--ui-scale, 1));
205212
transform-origin: top left;
213+
--main-topbar-height: 44px;
214+
transition: grid-template-columns 220ms ease;
206215
}
207216

208217
.drag-strip {
209218
position: absolute;
210219
top: 0;
211220
left: 0;
212221
right: 0;
213-
height: 24px;
222+
height: var(--titlebar-height, 24px);
214223
z-index: 2;
215224
}
216225

226+
.titlebar-controls {
227+
position: absolute;
228+
top: 0;
229+
left: 0;
230+
right: 0;
231+
height: var(--main-topbar-height, 44px);
232+
z-index: 4;
233+
pointer-events: none;
234+
}
235+
236+
.titlebar-toggle {
237+
position: absolute;
238+
top: 50%;
239+
-webkit-app-region: no-drag;
240+
pointer-events: auto;
241+
transform: translateY(calc(-50% + var(--titlebar-toggle-offset, 0px)));
242+
transition: opacity 160ms ease;
243+
}
244+
245+
246+
.titlebar-toggle-left {
247+
left: calc(10px + var(--titlebar-inset-left, 0px));
248+
}
249+
250+
.titlebar-toggle-right {
251+
right: 10px;
252+
}
253+
217254
.sidebar-resizer {
218255
position: absolute;
219256
top: 0;
@@ -222,6 +259,7 @@ body {
222259
width: 8px;
223260
cursor: col-resize;
224261
z-index: 3;
262+
transition: opacity 160ms ease;
225263
}
226264

227265
.sidebar-resizer::after {

0 commit comments

Comments
 (0)