mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
feat(desktop): immersive mode hides traffic lights for full-screen modals
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
2
apps/desktop/src/preload/index.d.ts
vendored
2
apps/desktop/src/preload/index.d.ts
vendored
@@ -5,6 +5,8 @@ interface DesktopAPI {
|
||||
onAuthToken: (callback: (token: string) => void) => () => void;
|
||||
/** Open a URL in the default browser. */
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
/** Hide macOS traffic lights for full-screen modals; restore when false. */
|
||||
setImmersiveMode: (immersive: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
interface UpdaterAPI {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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. */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute inset-x-0 top-0 h-10"
|
||||
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute top-6 left-6 text-muted-foreground"
|
||||
className="absolute top-12 left-12 text-muted-foreground"
|
||||
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
||||
onClick={onClose}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"./search": "./search/index.ts",
|
||||
"./chat": "./chat/index.ts",
|
||||
"./settings": "./settings/index.ts",
|
||||
"./onboarding": "./onboarding/index.ts"
|
||||
"./onboarding": "./onboarding/index.ts",
|
||||
"./platform": "./platform/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
|
||||
1
packages/views/platform/index.ts
Normal file
1
packages/views/platform/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useImmersiveMode } from "./use-immersive-mode";
|
||||
28
packages/views/platform/use-immersive-mode.ts
Normal file
28
packages/views/platform/use-immersive-mode.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
type ImmersiveCapableAPI = {
|
||||
setImmersiveMode?: (immersive: boolean) => Promise<void> | void;
|
||||
};
|
||||
|
||||
function getDesktopAPI(): ImmersiveCapableAPI | undefined {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
return (window as unknown as { desktopAPI?: ImmersiveCapableAPI }).desktopAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter "immersive" mode for the lifetime of the component that calls it.
|
||||
*
|
||||
* On macOS desktop this hides the traffic-light window controls so full-screen
|
||||
* modals (create-workspace, onboarding, etc.) can place UI in the top-left
|
||||
* corner without fighting the native controls' hit-test. On web or non-macOS
|
||||
* desktop this is a no-op.
|
||||
*/
|
||||
export function useImmersiveMode(): void {
|
||||
useEffect(() => {
|
||||
const api = getDesktopAPI();
|
||||
api?.setImmersiveMode?.(true);
|
||||
return () => {
|
||||
api?.setImmersiveMode?.(false);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
Reference in New Issue
Block a user