mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
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>
174 lines
5.8 KiB
TypeScript
174 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
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";
|
|
import { Label } from "@multica/ui/components/ui/label";
|
|
import { Button } from "@multica/ui/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
} from "@multica/ui/components/ui/dialog";
|
|
import { Card, CardContent } from "@multica/ui/components/ui/card";
|
|
import { useCreateWorkspace } from "@multica/core/workspace/mutations";
|
|
import {
|
|
WORKSPACE_SLUG_CONFLICT_ERROR,
|
|
WORKSPACE_SLUG_FORMAT_ERROR,
|
|
WORKSPACE_SLUG_REGEX,
|
|
isWorkspaceSlugConflict,
|
|
nameToWorkspaceSlug,
|
|
} 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("");
|
|
const [slug, setSlug] = useState("");
|
|
const [slugServerError, setSlugServerError] = useState<string | null>(null);
|
|
const slugTouched = useRef(false);
|
|
|
|
const slugValidationError =
|
|
slug.length > 0 && !WORKSPACE_SLUG_REGEX.test(slug)
|
|
? WORKSPACE_SLUG_FORMAT_ERROR
|
|
: null;
|
|
const slugError = slugValidationError ?? slugServerError;
|
|
|
|
const canSubmit =
|
|
name.trim().length > 0 && slug.trim().length > 0 && !slugError;
|
|
|
|
const handleNameChange = (value: string) => {
|
|
setName(value);
|
|
if (!slugTouched.current) {
|
|
setSlug(nameToWorkspaceSlug(value));
|
|
setSlugServerError(null);
|
|
}
|
|
};
|
|
|
|
const handleSlugChange = (value: string) => {
|
|
slugTouched.current = true;
|
|
setSlug(value);
|
|
setSlugServerError(null);
|
|
};
|
|
|
|
const handleCreate = () => {
|
|
if (!canSubmit) return;
|
|
createWorkspace.mutate(
|
|
{ name: name.trim(), slug: slug.trim() },
|
|
{
|
|
onSuccess: () => {
|
|
onClose();
|
|
router.push("/onboarding");
|
|
},
|
|
onError: (error) => {
|
|
if (isWorkspaceSlugConflict(error)) {
|
|
setSlugServerError(WORKSPACE_SLUG_CONFLICT_ERROR);
|
|
toast.error("Choose a different workspace URL");
|
|
return;
|
|
}
|
|
toast.error("Failed to create workspace");
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Dialog
|
|
open
|
|
onOpenChange={(v) => {
|
|
if (!v) onClose();
|
|
}}
|
|
>
|
|
<DialogContent
|
|
finalFocus={false}
|
|
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-12 left-12 text-muted-foreground"
|
|
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
|
|
onClick={onClose}
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
Back
|
|
</Button>
|
|
|
|
<div className="flex w-full max-w-md flex-col items-center gap-6">
|
|
<div className="text-center">
|
|
<DialogTitle className="text-2xl font-semibold">
|
|
Create a new workspace
|
|
</DialogTitle>
|
|
<DialogDescription className="mt-2">
|
|
Workspaces are shared environments where teams can work on
|
|
projects and issues.
|
|
</DialogDescription>
|
|
</div>
|
|
|
|
<Card className="w-full">
|
|
<CardContent className="space-y-4 pt-6">
|
|
<div className="space-y-1.5">
|
|
<Label>Workspace Name</Label>
|
|
<Input
|
|
autoFocus
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => handleNameChange(e.target.value)}
|
|
placeholder="My Workspace"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label>Workspace URL</Label>
|
|
<div className="flex items-center gap-0 rounded-md border bg-background focus-within:ring-2 focus-within:ring-ring">
|
|
<span className="pl-3 text-sm text-muted-foreground select-none">
|
|
multica.ai/
|
|
</span>
|
|
<Input
|
|
type="text"
|
|
value={slug}
|
|
onChange={(e) => handleSlugChange(e.target.value)}
|
|
placeholder="my-workspace"
|
|
className="border-0 shadow-none focus-visible:ring-0"
|
|
onKeyDown={(e) => e.key === "Enter" && handleCreate()}
|
|
/>
|
|
</div>
|
|
{slugError && (
|
|
<p className="text-xs text-destructive">{slugError}</p>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Button
|
|
className="w-full"
|
|
size="lg"
|
|
onClick={handleCreate}
|
|
disabled={createWorkspace.isPending || !canSubmit}
|
|
>
|
|
{createWorkspace.isPending ? "Creating..." : "Create workspace"}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|