From 7dad45d4440e5b8d05fa048a4e2a9eb8a896dec8 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:41:49 +0800 Subject: [PATCH] feat(desktop): immersive mode hides traffic lights for full-screen modals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full-screen modals (create-workspace) covered the app titlebar, so the Back button landed on top of the macOS traffic lights — where native hit-test always wins and the button couldn't be clicked. The modal also swallowed the window's drag region. Introduce a desktop IPC channel window:setImmersive that calls BrowserWindow.setWindowButtonVisibility, exposed through the existing desktopAPI preload bridge. A small useImmersiveMode() hook in @multica/views/platform toggles it for the component's lifetime and is a no-op on web / non-macOS. CreateWorkspaceModal now: - calls useImmersiveMode() so traffic lights disappear while it's open - adds a transparent top h-10 drag strip to restore window dragging - moves the Back button from top-6 left-6 to top-12 left-12 with an explicit no-drag region so clicks always reach it Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/desktop/src/main/index.ts | 8 ++++++ apps/desktop/src/preload/index.d.ts | 2 ++ apps/desktop/src/preload/index.ts | 3 ++ packages/views/modals/create-workspace.tsx | 18 +++++++++++- packages/views/package.json | 3 +- packages/views/platform/index.ts | 1 + packages/views/platform/use-immersive-mode.ts | 28 +++++++++++++++++++ 7 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 packages/views/platform/index.ts create mode 100644 packages/views/platform/use-immersive-mode.ts diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 3fae852f9..09fa2c5fe 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -113,6 +113,14 @@ if (!gotTheLock) { return shell.openExternal(url); }); + // IPC: toggle immersive mode — hides the macOS traffic lights so full-screen + // modals (create-workspace, onboarding) can place UI in the top-left corner + // without fighting the native window controls' hit-test. + ipcMain.handle("window:setImmersive", (_event, immersive: boolean) => { + if (process.platform !== "darwin") return; + mainWindow?.setWindowButtonVisibility(!immersive); + }); + createWindow(); setupAutoUpdater(() => mainWindow); diff --git a/apps/desktop/src/preload/index.d.ts b/apps/desktop/src/preload/index.d.ts index d15c31f37..04b3e7878 100644 --- a/apps/desktop/src/preload/index.d.ts +++ b/apps/desktop/src/preload/index.d.ts @@ -5,6 +5,8 @@ interface DesktopAPI { onAuthToken: (callback: (token: string) => void) => () => void; /** Open a URL in the default browser. */ openExternal: (url: string) => Promise; + /** Hide macOS traffic lights for full-screen modals; restore when false. */ + setImmersiveMode: (immersive: boolean) => Promise; } interface UpdaterAPI { diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 470e4ac58..2bf34acce 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -13,6 +13,9 @@ const desktopAPI = { }, /** Open a URL in the default browser */ openExternal: (url: string) => ipcRenderer.invoke("shell:openExternal", url), + /** Toggle immersive mode — hide macOS traffic lights for full-screen modals */ + setImmersiveMode: (immersive: boolean) => + ipcRenderer.invoke("window:setImmersive", immersive), }; const updaterAPI = { diff --git a/packages/views/modals/create-workspace.tsx b/packages/views/modals/create-workspace.tsx index 4eb33a1cb..860f5379e 100644 --- a/packages/views/modals/create-workspace.tsx +++ b/packages/views/modals/create-workspace.tsx @@ -2,6 +2,7 @@ import { useRef, useState } from "react"; import { useNavigation } from "../navigation"; +import { useImmersiveMode } from "../platform"; import { toast } from "sonner"; import { ArrowLeft } from "lucide-react"; import { Input } from "@multica/ui/components/ui/input"; @@ -24,6 +25,11 @@ import { } from "../workspace/slug"; export function CreateWorkspaceModal({ onClose }: { onClose: () => void }) { + // This modal is full-screen, so it covers the app titlebar. On macOS desktop + // we hide the traffic lights for its lifetime so the Back button in the top- + // left corner isn't stolen by the native controls' hit-test. No-op elsewhere. + useImmersiveMode(); + const router = useNavigation(); const createWorkspace = useCreateWorkspace(); const [name, setName] = useState(""); @@ -87,10 +93,20 @@ export function CreateWorkspaceModal({ onClose }: { onClose: () => void }) { showCloseButton={false} className="inset-0 flex h-full w-full max-w-none sm:max-w-none translate-0 flex-col items-center justify-center rounded-none bg-background ring-0 shadow-none" > + {/* Top drag region — restores window-drag ability that the full-screen + modal would otherwise swallow. Transparent; web browsers ignore the + -webkit-app-region property, so this is safe cross-platform. */} +
+