Skip to content

Commit c6b3600

Browse files
committed
feat: add desktop workspace fullscreen toggle
1 parent f35b027 commit c6b3600

7 files changed

Lines changed: 185 additions & 4 deletions

File tree

packages/web/src/features/topbar/index.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,51 @@ describe("TopBar", () => {
130130
expect(screen.getByRole("button", { name: "Hide Terminal" })).toHaveClass("topbar-btn--active");
131131
expect(screen.getByRole("button", { name: "Show Files" })).toHaveClass("topbar-btn--muted");
132132
});
133+
134+
it("renders the fullscreen toggle immediately to the right of settings when supported", () => {
135+
const store = createStore();
136+
store.set(localeAtom, "en");
137+
store.set(workspacesLoadStateAtom, "ready");
138+
139+
render(
140+
<Provider store={store}>
141+
<TopBar
142+
fullscreenController={{
143+
supported: true,
144+
isFullscreen: false,
145+
enterFullscreen: vi.fn(),
146+
exitFullscreen: vi.fn(),
147+
toggleFullscreen: vi.fn(),
148+
}}
149+
/>
150+
</Provider>
151+
);
152+
153+
const settingsButton = screen.getByTestId("settings-open");
154+
const fullscreenButton = screen.getByRole("button", { name: "Enter Fullscreen" });
155+
156+
expect(settingsButton.nextElementSibling).toBe(fullscreenButton);
157+
});
158+
159+
it("hides the fullscreen toggle when the controller reports unsupported", () => {
160+
const store = createStore();
161+
store.set(localeAtom, "en");
162+
store.set(workspacesLoadStateAtom, "ready");
163+
164+
render(
165+
<Provider store={store}>
166+
<TopBar
167+
fullscreenController={{
168+
supported: false,
169+
isFullscreen: false,
170+
enterFullscreen: vi.fn(),
171+
exitFullscreen: vi.fn(),
172+
toggleFullscreen: vi.fn(),
173+
}}
174+
/>
175+
</Provider>
176+
);
177+
178+
expect(screen.queryByRole("button", { name: "Enter Fullscreen" })).toBeNull();
179+
});
133180
});

packages/web/src/features/topbar/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ import { useNavigate } from "react-router-dom";
1212
import { commandPaletteOpenAtom } from "../../atoms/app-ui";
1313
import { orderedWorkspacesAtom, resolvedActiveWorkspaceIdAtom } from "../../atoms/workspaces";
1414
import { useTranslation } from "../../lib/i18n";
15+
import type { WorkspaceFullscreenController } from "../workspace/actions/use-workspace-fullscreen";
1516
import { sidebarCollapsedAtom, terminalPanelVisibleAtom } from "../workspace/atoms";
17+
import { WorkspaceFullscreenButton } from "../workspace/components/workspace-fullscreen-button";
1618
import { WorkspaceLaunchModal } from "../workspace/views/shared/workspace-launch-modal";
1719
import { ConnectionStatus } from "./components/connection-status";
1820
import { WorkspaceTab } from "./components/tab";
1921

22+
interface TopBarProps {
23+
fullscreenController?: WorkspaceFullscreenController;
24+
}
25+
2026
/**
2127
* TopBar Component
2228
*
@@ -25,7 +31,7 @@ import { WorkspaceTab } from "./components/tab";
2531
* - Left: Workspace tabs + Add button
2632
* - Right: ConnectionStatus, Quick Actions, Terminal toggle, Files toggle, Settings
2733
*/
28-
export const TopBar: FC = () => {
34+
export const TopBar: FC<TopBarProps> = ({ fullscreenController }) => {
2935
const t = useTranslation();
3036
const navigate = useNavigate();
3137
const workspaceList = useAtomValue(orderedWorkspacesAtom);
@@ -95,6 +101,12 @@ export const TopBar: FC = () => {
95101
>
96102
<Settings size={14} />
97103
</button>
104+
<WorkspaceFullscreenButton
105+
controller={fullscreenController}
106+
className="topbar-btn"
107+
iconSize={14}
108+
dataTestId="workspace-fullscreen-open"
109+
/>
98110
</div>
99111
{workspaceLaunchOpen ? (
100112
<WorkspaceLaunchModal onClose={() => setWorkspaceLaunchOpen(false)} />
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { fireEvent, render, screen } from "@testing-library/react";
2+
import { createStore, Provider } from "jotai";
3+
import { describe, expect, it, vi } from "vitest";
4+
import { localeAtom } from "../../../atoms/app-ui";
5+
import { WorkspaceFullscreenButton } from "./workspace-fullscreen-button";
6+
7+
function renderWithEnglish(ui: React.ReactElement) {
8+
const store = createStore();
9+
store.set(localeAtom, "en");
10+
11+
return render(<Provider store={store}>{ui}</Provider>);
12+
}
13+
14+
describe("WorkspaceFullscreenButton", () => {
15+
it("renders nothing when fullscreen is unsupported", () => {
16+
const { container } = renderWithEnglish(
17+
<WorkspaceFullscreenButton
18+
controller={{
19+
supported: false,
20+
isFullscreen: false,
21+
enterFullscreen: vi.fn(),
22+
exitFullscreen: vi.fn(),
23+
toggleFullscreen: vi.fn(),
24+
}}
25+
className="topbar-btn"
26+
iconSize={14}
27+
/>
28+
);
29+
30+
expect(container).toBeEmptyDOMElement();
31+
});
32+
33+
it("renders enter state and calls toggle", () => {
34+
const toggleFullscreen = vi.fn();
35+
36+
renderWithEnglish(
37+
<WorkspaceFullscreenButton
38+
controller={{
39+
supported: true,
40+
isFullscreen: false,
41+
enterFullscreen: vi.fn(),
42+
exitFullscreen: vi.fn(),
43+
toggleFullscreen,
44+
}}
45+
className="topbar-btn"
46+
iconSize={14}
47+
/>
48+
);
49+
50+
fireEvent.click(screen.getByRole("button", { name: "Enter Fullscreen" }));
51+
52+
expect(toggleFullscreen).toHaveBeenCalledTimes(1);
53+
});
54+
55+
it("switches to exit state when fullscreen is active", () => {
56+
renderWithEnglish(
57+
<WorkspaceFullscreenButton
58+
controller={{
59+
supported: true,
60+
isFullscreen: true,
61+
enterFullscreen: vi.fn(),
62+
exitFullscreen: vi.fn(),
63+
toggleFullscreen: vi.fn(),
64+
}}
65+
className="topbar-btn"
66+
iconSize={14}
67+
/>
68+
);
69+
70+
expect(screen.getByRole("button", { name: "Exit Fullscreen" })).toBeInTheDocument();
71+
});
72+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Maximize2, Minimize2 } from "lucide-react";
2+
import { useTranslation } from "../../../lib/i18n";
3+
import type { WorkspaceFullscreenController } from "../actions/use-workspace-fullscreen";
4+
5+
interface WorkspaceFullscreenButtonProps {
6+
controller?: WorkspaceFullscreenController;
7+
className: string;
8+
iconSize: number;
9+
dataTestId?: string;
10+
}
11+
12+
export function WorkspaceFullscreenButton({
13+
controller,
14+
className,
15+
iconSize,
16+
dataTestId,
17+
}: WorkspaceFullscreenButtonProps) {
18+
const t = useTranslation();
19+
20+
if (!controller?.supported) {
21+
return null;
22+
}
23+
24+
const label = controller.isFullscreen
25+
? t("tooltip.exit_fullscreen")
26+
: t("tooltip.enter_fullscreen");
27+
const Icon = controller.isFullscreen ? Minimize2 : Maximize2;
28+
29+
return (
30+
<button
31+
type="button"
32+
className={className}
33+
aria-label={label}
34+
title={label}
35+
data-testid={dataTestId}
36+
onClick={() => {
37+
void controller.toggleFullscreen();
38+
}}
39+
>
40+
<Icon size={iconSize} />
41+
</button>
42+
);
43+
}

packages/web/src/features/workspace/views/desktop/workspace-desktop-view.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { useSetAtom } from "jotai";
22
import { FilePlus, FolderPlus, GitBranch, RefreshCw } from "lucide-react";
3-
import type { FC } from "react";
3+
import { type FC, useRef } from "react";
44
import { useTranslation } from "../../../../lib/i18n";
55
import { AgentPanes } from "../../../agent-panes";
66
import { CodeEditorHost } from "../../../code-editor/views/shared/code-editor-host";
77
import { TerminalPanel } from "../../../terminal-panel";
88
import { TopBar } from "../../../topbar";
99
import { useGitDiffViewerActions } from "../../actions/use-git-actions";
10+
import { useWorkspaceFullscreen } from "../../actions/use-workspace-fullscreen";
1011
import { useWorkspaceScreenModel } from "../../actions/use-workspace-screen-model";
1112
import { activeFilePathAtomFamily } from "../../atoms";
1213
import { FileTreePanel } from "../shared/file-tree-panel";
@@ -15,6 +16,8 @@ import { GitPanel } from "../shared/git-panel";
1516
import { GitStatusBar } from "../shared/git-status-bar";
1617

1718
export const WorkspaceDesktopView: FC = () => {
19+
const fullscreenRootRef = useRef<HTMLDivElement>(null);
20+
const fullscreenController = useWorkspaceFullscreen(fullscreenRootRef);
1821
const t = useTranslation();
1922
const {
2023
createRequest,
@@ -62,8 +65,8 @@ export const WorkspaceDesktopView: FC = () => {
6265
};
6366

6467
return (
65-
<div className="workspace-page">
66-
<TopBar />
68+
<div ref={fullscreenRootRef} className="workspace-page">
69+
<TopBar fullscreenController={fullscreenController} />
6770

6871
<div className="workspace-body">
6972
{!focusMode && !sidebarCollapsed && (

packages/web/src/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,8 @@
636636
"close_pane": "Close panel",
637637
"new_workspace": "New workspace",
638638
"quick_actions": "Quick Actions",
639+
"enter_fullscreen": "Enter Fullscreen",
640+
"exit_fullscreen": "Exit Fullscreen",
639641
"show_terminal": "Show Terminal",
640642
"hide_terminal": "Hide Terminal",
641643
"show_files": "Show Files",

packages/web/src/locales/zh.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,8 @@
636636
"close_pane": "关闭面板",
637637
"new_workspace": "新建工作区",
638638
"quick_actions": "快捷操作",
639+
"enter_fullscreen": "进入全屏",
640+
"exit_fullscreen": "退出全屏",
639641
"show_terminal": "显示终端",
640642
"hide_terminal": "隐藏终端",
641643
"show_files": "显示文件",

0 commit comments

Comments
 (0)