feat: windows support

This commit is contained in:
zhom
2026-02-15 11:48:59 +04:00
parent dd5afac951
commit 63453331ff
46 changed files with 2445 additions and 328 deletions
@@ -39,6 +39,7 @@ interface CamoufoxConfigDialogProps {
config: CamoufoxConfig,
) => Promise<void>;
isRunning?: boolean;
crossOsUnlocked?: boolean;
}
export function CamoufoxConfigDialog({
@@ -48,6 +49,7 @@ export function CamoufoxConfigDialog({
onSave,
onSaveWayfern,
isRunning = false,
crossOsUnlocked = false,
}: CamoufoxConfigDialogProps) {
// Use union type to support both Camoufox and Wayfern configs
const [config, setConfig] = useState<CamoufoxConfig | WayfernConfig>(() => ({
@@ -160,6 +162,7 @@ export function CamoufoxConfigDialog({
onConfigChange={updateConfig}
forceAdvanced={true}
readOnly={isRunning}
crossOsUnlocked={crossOsUnlocked}
/>
) : (
<SharedCamoufoxConfigForm
@@ -168,6 +171,7 @@ export function CamoufoxConfigDialog({
forceAdvanced={true}
readOnly={isRunning}
browserType="camoufox"
crossOsUnlocked={crossOsUnlocked}
/>
)}
</div>
+24
View File
@@ -71,6 +71,7 @@ interface CreateProfileDialogProps {
groupId?: string;
}) => Promise<void>;
selectedGroupId?: string;
crossOsUnlocked?: boolean;
}
interface BrowserOption {
@@ -106,6 +107,7 @@ export function CreateProfileDialog({
onClose,
onCreateProfile,
selectedGroupId,
crossOsUnlocked = false,
}: CreateProfileDialogProps) {
const [profileName, setProfileName] = useState("");
const [currentStep, setCurrentStep] = useState<
@@ -677,6 +679,16 @@ export function CreateProfileDialog({
</RippleButton>
</div>
)}
{!isLoadingReleaseTypes &&
!releaseTypesError &&
!getBestAvailableVersion("wayfern") && (
<div className="flex gap-3 items-center p-3 rounded-md border border-yellow-500/50 bg-yellow-500/10">
<p className="text-sm text-yellow-500">
Wayfern is not available on your platform
yet.
</p>
</div>
)}
{!isLoadingReleaseTypes &&
!releaseTypesError &&
!isBrowserCurrentlyDownloading("wayfern") &&
@@ -732,6 +744,7 @@ export function CreateProfileDialog({
config={wayfernConfig}
onConfigChange={updateWayfernConfig}
isCreating
crossOsUnlocked={crossOsUnlocked}
/>
</div>
) : selectedBrowser === "camoufox" ? (
@@ -763,6 +776,16 @@ export function CreateProfileDialog({
</RippleButton>
</div>
)}
{!isLoadingReleaseTypes &&
!releaseTypesError &&
!getBestAvailableVersion("camoufox") && (
<div className="flex gap-3 items-center p-3 rounded-md border border-yellow-500/50 bg-yellow-500/10">
<p className="text-sm text-yellow-500">
Camoufox is not available on your platform
yet.
</p>
</div>
)}
{!isLoadingReleaseTypes &&
!releaseTypesError &&
!isBrowserCurrentlyDownloading("camoufox") &&
@@ -819,6 +842,7 @@ export function CreateProfileDialog({
onConfigChange={updateCamoufoxConfig}
isCreating
browserType="camoufox"
crossOsUnlocked={crossOsUnlocked}
/>
</div>
) : (
+41 -22
View File
@@ -1,6 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuLock } from "react-icons/lu";
import MultipleSelector, { type Option } from "@/components/multiple-selector";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Checkbox } from "@/components/ui/checkbox";
@@ -29,6 +31,7 @@ interface SharedCamoufoxConfigFormProps {
forceAdvanced?: boolean; // Force advanced mode (for editing)
readOnly?: boolean; // Flag to indicate if the form should be read-only
browserType?: "camoufox" | "wayfern"; // Browser type to customize form options
crossOsUnlocked?: boolean; // Allow selecting non-current OS (paid feature)
}
// Determine if fingerprint editing should be disabled
@@ -118,7 +121,9 @@ export function SharedCamoufoxConfigForm({
forceAdvanced = false,
readOnly = false,
browserType = "camoufox",
crossOsUnlocked = false,
}: SharedCamoufoxConfigFormProps) {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState(
forceAdvanced ? "manual" : "automatic",
);
@@ -128,7 +133,6 @@ export function SharedCamoufoxConfigForm({
// Get selected OS (defaults to current OS)
const selectedOS = config.os || currentOS;
const isOSDifferent = selectedOS !== currentOS;
// Set screen resolution to user's screen size when creating a new profile
useEffect(() => {
@@ -227,18 +231,25 @@ export function SharedCamoufoxConfigForm({
<SelectValue placeholder="Select operating system" />
</SelectTrigger>
<SelectContent>
<SelectItem value="windows">{osLabels.windows}</SelectItem>
<SelectItem value="macos">{osLabels.macos}</SelectItem>
<SelectItem value="linux">{osLabels.linux}</SelectItem>
{(["windows", "macos", "linux"] as CamoufoxOS[]).map((os) => {
const isDisabled = os !== currentOS && !crossOsUnlocked;
return (
<SelectItem key={os} value={os} disabled={isDisabled}>
<span className="flex items-center gap-2">
{osLabels[os]}
{isDisabled && (
<LuLock className="w-3 h-3 text-muted-foreground" />
)}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
{isOSDifferent && (
<Alert className="border-yellow-500/50 bg-yellow-500/10">
<AlertDescription className="text-yellow-600 dark:text-yellow-400">
Warning: Selecting an OS different from your current system (
{osLabels[currentOS]}) increases the risk of detection. Websites
can detect mismatches between your fingerprint and actual system
behavior.
{selectedOS !== currentOS && crossOsUnlocked && (
<Alert className="mt-2">
<AlertDescription>
{t("fingerprint.crossOsWarning")}
</AlertDescription>
</Alert>
)}
@@ -994,19 +1005,27 @@ export function SharedCamoufoxConfigForm({
<SelectValue placeholder="Select operating system" />
</SelectTrigger>
<SelectContent>
<SelectItem value="windows">{osLabels.windows}</SelectItem>
<SelectItem value="macos">{osLabels.macos}</SelectItem>
<SelectItem value="linux">{osLabels.linux}</SelectItem>
{(["windows", "macos", "linux"] as CamoufoxOS[]).map((os) => {
const isDisabled = os !== currentOS && !crossOsUnlocked;
return (
<SelectItem key={os} value={os} disabled={isDisabled}>
<span className="flex items-center gap-2">
{osLabels[os]}
{isDisabled && (
<LuLock className="w-3 h-3 text-muted-foreground" />
)}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
{isOSDifferent && (
<Alert className="border-yellow-500/50 bg-yellow-500/10">
<AlertDescription className="text-yellow-600 dark:text-yellow-400">
Warning: Selecting an OS different from your current
system ({osLabels[currentOS]}) increases the risk of
detection. Websites with advanced protections can detect
mismatches between your fingerprint and actual system
behavior.
{selectedOS !== currentOS && crossOsUnlocked && (
<Alert className="mt-2">
<AlertDescription>
Cross-OS fingerprinting has limitations. System-level APIs
may still reflect your actual operating system, and some
features may have degraded performance.
</AlertDescription>
</Alert>
)}
+297 -82
View File
@@ -2,6 +2,7 @@
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuEye, LuEyeOff } from "react-icons/lu";
import { LoadingButton } from "@/components/loading-button";
import { Button } from "@/components/ui/button";
@@ -15,11 +16,13 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useCloudAuth } from "@/hooks/use-cloud-auth";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import type { SyncSettings } from "@/types";
@@ -29,6 +32,9 @@ interface SyncConfigDialogProps {
}
export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
const { t } = useTranslation();
// Self-hosted state
const [serverUrl, setServerUrl] = useState("");
const [token, setToken] = useState("");
const [isLoading, setIsLoading] = useState(false);
@@ -36,6 +42,24 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
const [isTesting, setIsTesting] = useState(false);
const [showToken, setShowToken] = useState(false);
// Cloud auth state
const {
user,
isLoggedIn,
isLoading: isCloudLoading,
requestOtp,
verifyOtp,
logout,
} = useCloudAuth();
const [email, setEmail] = useState("");
const [otpCode, setOtpCode] = useState("");
const [codeSent, setCodeSent] = useState(false);
const [isSendingCode, setIsSendingCode] = useState(false);
const [isVerifying, setIsVerifying] = useState(false);
// Default to self-hosted tab if self-hosted is configured and not cloud-logged-in
const [activeTab, setActiveTab] = useState<string>("cloud");
const loadSettings = useCallback(async () => {
setIsLoading(true);
try {
@@ -52,9 +76,19 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
useEffect(() => {
if (isOpen) {
void loadSettings();
setCodeSent(false);
setOtpCode("");
setEmail("");
}
}, [isOpen, loadSettings]);
// If self-hosted is configured and not cloud-logged-in, default to self-hosted tab
useEffect(() => {
if (!isCloudLoading && !isLoggedIn && serverUrl && token) {
setActiveTab("self-hosted");
}
}, [isCloudLoading, isLoggedIn, serverUrl, token]);
const handleTestConnection = useCallback(async () => {
if (!serverUrl) {
showErrorToast("Please enter a server URL");
@@ -112,102 +146,283 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
}
}, []);
const handleSendCode = useCallback(async () => {
if (!email) return;
setIsSendingCode(true);
try {
await requestOtp(email);
setCodeSent(true);
showSuccessToast(t("sync.cloud.codeSent"));
} catch (error) {
console.error("Failed to send OTP:", error);
showErrorToast(String(error));
} finally {
setIsSendingCode(false);
}
}, [email, requestOtp, t]);
const handleVerifyOtp = useCallback(async () => {
if (!email || !otpCode) return;
setIsVerifying(true);
try {
await verifyOtp(email, otpCode);
showSuccessToast(t("sync.cloud.loginSuccess"));
// Restart sync service with cloud credentials
try {
await invoke("restart_sync_service");
} catch (e) {
console.error("Failed to restart sync service:", e);
}
} catch (error) {
console.error("OTP verification failed:", error);
showErrorToast(String(error));
} finally {
setIsVerifying(false);
}
}, [email, otpCode, verifyOtp, t]);
const handleCloudLogout = useCallback(async () => {
try {
await logout();
showSuccessToast(t("sync.cloud.logoutSuccess"));
// Restart sync service (will fall back to self-hosted or stop)
try {
await invoke("restart_sync_service");
} catch (e) {
console.error("Failed to restart sync service:", e);
}
} catch (error) {
console.error("Failed to logout:", error);
showErrorToast(String(error));
}
}, [logout, t]);
const isConnected = Boolean(serverUrl && token);
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Sync Service</DialogTitle>
<DialogDescription>
Configure connection to a sync server to synchronize your profiles
across devices.
</DialogDescription>
<DialogTitle>{t("sync.title")}</DialogTitle>
<DialogDescription>{t("sync.description")}</DialogDescription>
</DialogHeader>
{isLoading ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
</div>
) : (
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="sync-server-url">Server URL</Label>
<Input
id="sync-server-url"
placeholder="https://sync.example.com"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="w-full">
<TabsTrigger value="cloud" className="flex-1">
{t("sync.cloud.tabLabel")}
</TabsTrigger>
<TabsTrigger value="self-hosted" className="flex-1">
{t("sync.cloud.selfHostedTabLabel")}
</TabsTrigger>
</TabsList>
<div className="space-y-2">
<Label htmlFor="sync-token">Access Token</Label>
<div className="relative">
<Input
id="sync-token"
type={showToken ? "text" : "password"}
placeholder="Enter your sync token"
value={token}
onChange={(e) => setToken(e.target.value)}
className="pr-10"
/>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setShowToken(!showToken)}
className="absolute right-3 top-1/2 p-1 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
aria-label={showToken ? "Hide token" : "Show token"}
>
{showToken ? (
<LuEyeOff className="w-4 h-4 text-muted-foreground hover:text-foreground" />
) : (
<LuEye className="w-4 h-4 text-muted-foreground hover:text-foreground" />
)}
</button>
</TooltipTrigger>
<TooltipContent>
{showToken ? "Hide token" : "Show token"}
</TooltipContent>
</Tooltip>
<TabsContent value="cloud">
{isCloudLoading ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
</div>
</div>
) : isLoggedIn && user ? (
<div className="grid gap-4 py-4">
<div className="flex gap-2 items-center text-sm">
<div className="w-2 h-2 rounded-full bg-green-500" />
{t("sync.cloud.connected")}
</div>
{isConnected && (
<div className="flex gap-2 items-center text-sm text-muted-foreground">
<div className="w-2 h-2 rounded-full bg-green-500" />
Connected
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("sync.cloud.email")}
</span>
<span>{user.email}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("sync.cloud.plan")}
</span>
<span className="capitalize">
{user.plan}
{user.planPeriod ? ` (${user.planPeriod})` : ""}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("sync.cloud.profiles")}
</span>
<span>
{t("sync.cloud.profileUsage", {
used: user.cloudProfilesUsed,
limit: user.profileLimit,
})}
</span>
</div>
</div>
<div className="flex gap-2 pt-2">
<Button variant="outline" className="flex-1" asChild>
<a
href="https://donutbrowser.com/account"
target="_blank"
rel="noopener noreferrer"
>
{t("sync.cloud.manageAccount")}
</a>
</Button>
<Button
variant="outline"
className="flex-1"
onClick={handleCloudLogout}
>
{t("sync.cloud.logout")}
</Button>
</div>
</div>
) : (
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="cloud-email">{t("sync.cloud.email")}</Label>
<div className="flex gap-2">
<Input
id="cloud-email"
type="email"
placeholder={t("sync.cloud.emailPlaceholder")}
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !codeSent) {
void handleSendCode();
}
}}
/>
<LoadingButton
onClick={handleSendCode}
isLoading={isSendingCode}
disabled={!email || codeSent}
variant="outline"
>
{t("sync.cloud.sendCode")}
</LoadingButton>
</div>
</div>
{codeSent && (
<div className="space-y-2">
<Label htmlFor="cloud-otp">
{t("sync.cloud.verificationCode")}
</Label>
<Input
id="cloud-otp"
placeholder={t("sync.cloud.codePlaceholder")}
value={otpCode}
onChange={(e) => setOtpCode(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
void handleVerifyOtp();
}
}}
/>
<LoadingButton
onClick={handleVerifyOtp}
isLoading={isVerifying}
disabled={!otpCode}
className="w-full"
>
{isVerifying
? t("sync.cloud.loggingIn")
: t("sync.cloud.verifyAndLogin")}
</LoadingButton>
</div>
)}
</div>
)}
</div>
)}
</TabsContent>
<DialogFooter className="flex gap-2">
{isConnected && (
<Button
variant="outline"
onClick={handleDisconnect}
disabled={isSaving}
>
Disconnect
</Button>
)}
<Button
variant="outline"
onClick={handleTestConnection}
disabled={isTesting || !serverUrl}
>
{isTesting ? "Testing..." : "Test Connection"}
</Button>
<LoadingButton
onClick={handleSave}
isLoading={isSaving}
disabled={!serverUrl || !token}
>
Save
</LoadingButton>
</DialogFooter>
<TabsContent value="self-hosted">
{isLoading ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
</div>
) : (
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="sync-server-url">{t("sync.serverUrl")}</Label>
<Input
id="sync-server-url"
placeholder={t("sync.serverUrlPlaceholder")}
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="sync-token">{t("sync.token")}</Label>
<div className="relative">
<Input
id="sync-token"
type={showToken ? "text" : "password"}
placeholder={t("sync.tokenPlaceholder")}
value={token}
onChange={(e) => setToken(e.target.value)}
className="pr-10"
/>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setShowToken(!showToken)}
className="absolute right-3 top-1/2 p-1 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
aria-label={showToken ? "Hide token" : "Show token"}
>
{showToken ? (
<LuEyeOff className="w-4 h-4 text-muted-foreground hover:text-foreground" />
) : (
<LuEye className="w-4 h-4 text-muted-foreground hover:text-foreground" />
)}
</button>
</TooltipTrigger>
<TooltipContent>
{showToken ? "Hide token" : "Show token"}
</TooltipContent>
</Tooltip>
</div>
</div>
{isConnected && (
<div className="flex gap-2 items-center text-sm text-muted-foreground">
<div className="w-2 h-2 rounded-full bg-green-500" />
{t("sync.status.connected")}
</div>
)}
</div>
)}
<DialogFooter className="flex gap-2">
{isConnected && (
<Button
variant="outline"
onClick={handleDisconnect}
disabled={isSaving}
>
Disconnect
</Button>
)}
<Button
variant="outline"
onClick={handleTestConnection}
disabled={isTesting || !serverUrl}
>
{isTesting ? "Testing..." : "Test Connection"}
</Button>
<LoadingButton
onClick={handleSave}
isLoading={isSaving}
disabled={!serverUrl || !token}
>
Save
</LoadingButton>
</DialogFooter>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
);
+22 -2
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuLock } from "react-icons/lu";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Checkbox } from "@/components/ui/checkbox";
@@ -28,6 +29,7 @@ interface WayfernConfigFormProps {
isCreating?: boolean;
forceAdvanced?: boolean;
readOnly?: boolean;
crossOsUnlocked?: boolean;
}
const isFingerprintEditingDisabled = (config: WayfernConfig): boolean => {
@@ -57,7 +59,9 @@ export function WayfernConfigForm({
isCreating = false,
forceAdvanced = false,
readOnly = false,
crossOsUnlocked = false,
}: WayfernConfigFormProps) {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState(
forceAdvanced ? "manual" : "automatic",
);
@@ -157,7 +161,7 @@ export function WayfernConfigForm({
{(
["windows", "macos", "linux", "android", "ios"] as WayfernOS[]
).map((os) => {
const isDisabled = os !== currentOS;
const isDisabled = os !== currentOS && !crossOsUnlocked;
return (
<SelectItem key={os} value={os} disabled={isDisabled}>
<span className="flex items-center gap-2">
@@ -171,6 +175,13 @@ export function WayfernConfigForm({
})}
</SelectContent>
</Select>
{selectedOS !== currentOS && crossOsUnlocked && (
<Alert className="mt-2">
<AlertDescription>
{t("fingerprint.crossOsWarning")}
</AlertDescription>
</Alert>
)}
</div>
{/* Randomize Fingerprint Option */}
@@ -943,7 +954,7 @@ export function WayfernConfigForm({
"ios",
] as WayfernOS[]
).map((os) => {
const isDisabled = os !== currentOS;
const isDisabled = os !== currentOS && !crossOsUnlocked;
return (
<SelectItem key={os} value={os} disabled={isDisabled}>
<span className="flex items-center gap-2">
@@ -957,6 +968,15 @@ export function WayfernConfigForm({
})}
</SelectContent>
</Select>
{selectedOS !== currentOS && crossOsUnlocked && (
<Alert className="mt-2">
<AlertDescription>
Cross-OS fingerprinting has limitations. System-level APIs
may still reflect your actual operating system, and some
features may have degraded performance.
</AlertDescription>
</Alert>
)}
</div>
{/* Randomize Fingerprint Option */}
+98 -19
View File
@@ -3,17 +3,20 @@
import { getCurrentWindow } from "@tauri-apps/api/window";
import { useEffect, useState } from "react";
type Platform = "macos" | "windows" | "linux";
function detectPlatform(): Platform {
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes("mac")) return "macos";
if (userAgent.includes("win")) return "windows";
return "linux";
}
export function WindowDragArea() {
const [isMacOS, setIsMacOS] = useState(false);
const [platform, setPlatform] = useState<Platform | null>(null);
useEffect(() => {
// Check if we're on macOS using user agent detection
const checkPlatform = () => {
const userAgent = navigator.userAgent.toLowerCase();
setIsMacOS(userAgent.includes("mac"));
};
checkPlatform();
setPlatform(detectPlatform());
}, []);
const handlePointerDown = (e: React.PointerEvent) => {
@@ -33,21 +36,97 @@ export function WindowDragArea() {
void startDrag();
};
// Only render on macOS and when no dialogs are open
if (!isMacOS) {
// Linux: system decorations handle everything
if (!platform || platform === "linux") {
return null;
}
// macOS: transparent drag area overlay
if (platform === "macos") {
return (
<button
type="button"
className="fixed top-0 right-0 left-0 h-10 bg-transparent border-0 z-[999999] select-none"
data-window-drag-area="true"
onPointerDown={handlePointerDown}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
}}
/>
);
}
// Windows: custom title bar with drag area + minimize/close buttons
const handleMinimize = async () => {
try {
await getCurrentWindow().minimize();
} catch (error) {
console.error("Failed to minimize window:", error);
}
};
const handleClose = async () => {
try {
await getCurrentWindow().close();
} catch (error) {
console.error("Failed to close window:", error);
}
};
return (
<button
type="button"
className="fixed top-0 right-0 left-0 h-10 bg-transparent border-0 z-[999999] select-none"
<div
className="fixed top-0 right-0 left-0 h-10 z-[999999] flex items-center select-none"
data-window-drag-area="true"
onPointerDown={handlePointerDown}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
}}
/>
>
{/* Draggable area */}
<button
type="button"
className="flex-1 h-full bg-transparent border-0 cursor-default"
onPointerDown={handlePointerDown}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
}}
/>
{/* Window control buttons */}
<div className="flex items-center h-full">
<button
type="button"
onClick={handleMinimize}
className="flex items-center justify-center w-12 h-full hover:bg-muted/50 transition-colors text-muted-foreground hover:text-foreground"
>
<svg
width="10"
height="1"
viewBox="0 0 10 1"
fill="currentColor"
role="img"
aria-label="Minimize"
>
<rect width="10" height="1" />
</svg>
</button>
<button
type="button"
onClick={handleClose}
className="flex items-center justify-center w-12 h-full hover:bg-destructive/90 transition-colors text-muted-foreground hover:text-destructive-foreground"
>
<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
stroke="currentColor"
strokeWidth="1.2"
role="img"
aria-label="Close"
>
<line x1="1" y1="1" x2="9" y2="9" />
<line x1="9" y1="1" x2="1" y2="9" />
</svg>
</button>
</div>
</div>
);
}