Compare commits

...

13 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek 398dc3128e refac 2025-07-29 13:39:05 +04:00
Timothy Jaeryang Baek d991908205 refac 2025-07-29 13:34:49 +04:00
Timothy Jaeryang Baek 2528096e07 fix: windows utf-8 issue 2025-07-29 13:32:38 +04:00
Timothy Jaeryang Baek aeba0e7c22 refac: logging 2025-07-29 13:23:30 +04:00
Timothy Jaeryang Baek ffbbb8f5b2 refac 2025-07-29 13:14:06 +04:00
Timothy Jaeryang Baek 119409a2dc refac 2025-07-29 13:08:39 +04:00
Timothy Jaeryang Baek cb564555dc refac 2025-07-29 12:54:24 +04:00
Timothy Jaeryang Baek 00d739d0d0 refac 2025-07-29 12:51:54 +04:00
Timothy Jaeryang Baek eebd30e29e enh: logging 2025-07-29 12:47:33 +04:00
Timothy Jaeryang Baek 8b62c3c993 refac 2025-07-29 12:46:36 +04:00
Timothy Jaeryang Baek c6189bb7cc enh: logging 2025-07-29 12:46:01 +04:00
Timothy Jaeryang Baek ce3766d3c2 feat: factory reset 2025-07-29 12:44:50 +04:00
Timothy Jaeryang Baek e40766430a fix: windows python path 2025-07-29 12:08:09 +04:00
10 changed files with 275 additions and 113 deletions
+22 -12
View File
@@ -17,10 +17,8 @@ import {
import path, { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import icon from "../../resources/icon.png?asset";
import trayIconImage from "../../resources/assets/tray.png?asset";
import {
getLogFilePath,
checkUrlAndOpen,
getConfig,
getServerLog,
@@ -30,12 +28,19 @@ import {
isPythonInstalled,
isUvInstalled,
openUrl,
resetApp,
setConfig,
startServer,
stopAllServers,
uninstallPython,
} from "./utils";
import log from "electron-log";
log.transports.file.resolvePathFn = () => getLogFilePath("main");
import icon from "../../resources/icon.png?asset";
import trayIconImage from "../../resources/assets/tray.png?asset";
// Main application logic
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
@@ -267,7 +272,7 @@ const startServerHandler = async () => {
updateTrayMenu("Open WebUI: Starting...", null);
console.log("Server started successfully:", SERVER_URL, SERVER_PID);
log.info("Server started successfully:", SERVER_URL, SERVER_PID);
SERVER_STATUS = "started";
mainWindow?.webContents.send("main:data", {
@@ -374,7 +379,7 @@ if (!gotTheLock) {
// Some APIs can only be used after this event occurs.
app.whenReady().then(async () => {
CONFIG = await getConfig(); // Load initial config
console.log("Initial Config:", CONFIG);
log.info("Initial Config:", CONFIG);
// Set app user model id for windows
electronApp.setAppUserModelId("com.openwebui.desktop");
@@ -386,14 +391,14 @@ if (!gotTheLock) {
});
// IPC test
ipcMain.on("ping", () => console.log("pong"));
ipcMain.on("ping", () => log.info("pong"));
ipcMain.handle("get:version", async () => {
return app.getVersion();
});
ipcMain.handle("install:python", async (event) => {
console.log("Installing package...");
log.info("Installing package...");
try {
const res = await installPython();
if (res) {
@@ -425,7 +430,7 @@ if (!gotTheLock) {
});
ipcMain.handle("install:package", async (event) => {
console.log("Installing package...");
log.info("Installing package...");
try {
const res = await installPackage("open-webui");
if (res) {
@@ -449,7 +454,7 @@ if (!gotTheLock) {
ipcMain.handle("status:package", async (event) => {
const packageStatus = await isPackageInstalled("open-webui");
console.log("Package Status:", packageStatus);
log.info("Package Status:", packageStatus);
return packageStatus;
});
@@ -482,6 +487,11 @@ if (!gotTheLock) {
return SERVER_STATUS;
});
ipcMain.handle("app:reset", async (event) => {
await stopServerHandler();
return await resetApp();
});
ipcMain.handle("get:config", async (event) => {
return await getConfig();
});
@@ -494,7 +504,7 @@ if (!gotTheLock) {
if (!url) {
throw new Error("No URL provided to open in browser.");
}
console.log("Opening URL in browser:", url);
log.info("Opening URL in browser:", url);
if (url.startsWith("http://0.0.0.0")) {
url = url.replace("http://0.0.0.0", "http://localhost");
}
@@ -503,7 +513,7 @@ if (!gotTheLock) {
});
ipcMain.handle("notification", async (event, { title, body }) => {
console.log("Received notification:", title, body);
log.info("Received notification:", title, body);
const notification = new Notification({
title: title,
body: body,
@@ -515,7 +525,7 @@ if (!gotTheLock) {
if (isPackageInstalled("open-webui")) {
if (CONFIG?.autoUpdate ?? true) {
try {
console.log("Checking for updates...");
log.info("Checking for updates...");
updateTrayMenu(
"Open WebUI: Checking for updates...",
null
+112 -43
View File
@@ -12,6 +12,18 @@ import { app, shell, Notification } from "electron";
import { execFileSync, exec, spawn, execSync, execFile } from "child_process";
import log from "electron-log";
log.transports.file.resolvePathFn = () => getLogFilePath("main");
const serverLogger = log.create({ logId: "server" });
serverLogger.transports.file.resolvePath = () => getLogFilePath(`server`);
export const getLogFilePath = (name: string = "main"): string => {
const logDir = path.join(getUserDataPath(), "logs");
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
return path.join(logDir, `${name}.log`);
};
export const getAppPath = (): string => {
let appPath = app.getAppPath();
@@ -59,7 +71,7 @@ export const openUrl = (url: string) => {
throw new Error("No URL provided to open in browser.");
}
console.log("Opening URL in browser:", url);
log.info("Opening URL in browser:", url);
if (url.startsWith("http://0.0.0.0")) {
url = url.replace("http://0.0.0.0", "http://localhost");
}
@@ -215,7 +227,7 @@ export const downloadFileWithProgress = async (
// Write to file
fs.writeFileSync(downloadPath, buffer);
console.log("File downloaded successfully:", downloadPath);
log.info("File downloaded successfully:", downloadPath);
return downloadPath;
}
} catch (error) {
@@ -257,13 +269,13 @@ const downloadPython = async (onProgress = null) => {
const url = generateDownloadUrl();
const downloadPath = getPythonDownloadPath();
console.log(`🔍 Detected system: ${os.platform()} ${os.arch()}`);
console.log(`📁 Download path: ${downloadPath}`);
console.log(`🔗 URL: ${url}`);
log.info(`🔍 Detected system: ${os.platform()} ${os.arch()}`);
log.info(`📁 Download path: ${downloadPath}`);
log.info(`🔗 URL: ${url}`);
// Check if file already exists
if (fs.existsSync(downloadPath)) {
console.log(`✅ File already exists: ${downloadPath}`);
log.info(`✅ File already exists: ${downloadPath}`);
return downloadPath;
}
@@ -273,7 +285,7 @@ const downloadPython = async (onProgress = null) => {
downloadPath,
onProgress
);
console.log(`✅ Python downloaded successfully to: ${result}`);
log.info(`✅ Python downloaded successfully to: ${result}`);
return result;
} catch (error) {
console.error(`❌ Download failed: ${error?.message}`);
@@ -311,7 +323,7 @@ export const installPython = async (
}
await downloadPython((progress, downloaded, total) => {
console.log(
log.info(
`Downloading Python: ${progress.toFixed(2)}% (${downloaded} of ${total} bytes)`
);
log.info(
@@ -325,7 +337,7 @@ export const installPython = async (
}
installationPath = installationPath || getPythonInstallationPath();
console.log(installationPath, pythonDownloadPath);
log.info(installationPath, pythonDownloadPath);
try {
const userDataPath = getUserDataPath();
@@ -346,9 +358,12 @@ export const installPython = async (
encoding: "utf-8",
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
});
console.log("Successfully installed uv package");
log.info("Successfully installed uv package");
return true; // Return true to indicate success
} else {
@@ -361,7 +376,7 @@ export const installPython = async (
export const getPythonExecutablePath = (envPath: string) => {
if (process.platform === "win32") {
return path.normalize(path.join(envPath, "Scripts", "python.exe"));
return path.normalize(path.join(envPath, "python.exe"));
} else {
return path.normalize(path.join(envPath, "bin", "python"));
}
@@ -387,9 +402,12 @@ export const isPythonInstalled = (installationPath?: string) => {
encoding: "utf-8",
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
});
console.log("Installed Python Version:", pythonVersion.trim());
log.info("Installed Python Version:", pythonVersion.trim());
return true; // Return true to indicate success
} catch (error) {
@@ -406,10 +424,13 @@ export const isUvInstalled = (installationPath?: string) => {
encoding: "utf-8",
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
});
console.log("Installed uv Version:", result.trim());
log.info("Installed uv Version:", result.trim());
return true; // Return true if uv is installed
} catch (error) {
log.error(
@@ -446,6 +467,44 @@ export const uninstallPython = (installationPath?: string): boolean => {
return true;
};
export const resetApp = async (): Promise<void> => {
await uninstallPython();
log.info("Uninstalled Python environment");
// remove /data folder
const dataPath = getOpenWebUIDataPath();
if (fs.existsSync(dataPath)) {
try {
fs.rmSync(dataPath, { recursive: true });
log.info("Removed data directory:", dataPath);
} catch (error) {
log.error("Failed to remove data directory:", error);
}
}
// remove config file
const configPath = path.join(getUserDataPath(), "config.json");
if (fs.existsSync(configPath)) {
try {
fs.unlinkSync(configPath);
log.info("Removed config file:", configPath);
} catch (error) {
log.error("Failed to remove config file:", error);
}
}
// remove secret key file
const secretKeyPath = path.join(getOpenWebUIDataPath(), ".key");
if (fs.existsSync(secretKeyPath)) {
try {
fs.unlinkSync(secretKeyPath);
log.info("Removed secret key file:", secretKeyPath);
} catch (error) {
log.error("Failed to remove secret key file:", error);
}
}
};
////////////////////////////////////////////////
//
// Fixes code-signing issues in macOS by applying ad-hoc signatures to extracted environment files.
@@ -513,13 +572,16 @@ export const installPackage = (
{
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
}
);
// Function to handle logging output
const onLog = (data: any) => {
console.log(data);
log.info(data);
};
// Listen to stdout and stderr for logging
@@ -528,7 +590,7 @@ export const installPackage = (
// Handle the exit event
commandProcess.on("exit", (code) => {
console.log(`Child exited with code ${code}`);
log.info(`Child exited with code ${code}`);
if (code !== 0) {
log.error(`Failed to install open-webui: ${code}`);
@@ -563,19 +625,22 @@ export const isPackageInstalled = (packageName: string): boolean => {
encoding: "utf-8",
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
}
);
if (info.includes(`Name: ${packageName}`)) {
console.log(`Package ${packageName} is installed.`);
log.info(`Package ${packageName} is installed.`);
return true; // Return true to indicate success
} else {
console.log(`Package ${packageName} is not installed.`);
log.info(`Package ${packageName} is not installed.`);
return false; // Return false to indicate failure
}
} catch (error) {
console.log("Failed to execute Python binary");
log.info("Failed to execute Python binary");
return false; // Return false to indicate failure
}
};
@@ -603,7 +668,7 @@ export const startServer = async (
throw new Error("open-webui package is not installed");
const pythonPath = getPythonPath();
console.log(`Using Python at: ${pythonPath}`);
log.info(`Using Python at: ${pythonPath}`);
const openWebUIPath = path.join(path.dirname(pythonPath), "open-webui");
@@ -622,17 +687,17 @@ export const startServer = async (
let desiredPort = port || 8080;
let availablePort = desiredPort;
console.log(`Checking port availability starting from ${availablePort}...`);
log.info(`Checking port availability starting from ${availablePort}...`);
while (await portInUse(availablePort, host)) {
availablePort++;
console.log(`Port ${availablePort} is in use, trying next port...`);
log.info(`Port ${availablePort} is in use, trying next port...`);
if (availablePort > desiredPort + 100) {
throw new Error("No available ports found");
}
}
commandArgs.push("--port", availablePort.toString());
console.log(
log.info(
"Starting Open-WebUI server...",
pythonPath,
commandArgs.join(" ")
@@ -640,7 +705,12 @@ export const startServer = async (
const childProcess = spawn(pythonPath, commandArgs, {
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env },
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
});
if (!childProcess.pid) {
@@ -653,31 +723,30 @@ export const startServer = async (
const appendLog = (source: string) => (data: Buffer) => {
const logLine = data.toString().trim();
const tag = `[${source}][PID:${childProcess.pid}]:`;
logLines.push(`${tag} ${logLine}`);
// (Optional) also log to main process console
// console.log(`${tag} ${logLine}`);
const line = `[${source}][PID:${childProcess.pid}]: ${logLine}`;
logLines.push(line);
serverLogger.info(line); // Log to console
};
childProcess.stdout?.on("data", appendLog("stdout"));
childProcess.stderr?.on("data", appendLog("stderr"));
childProcess.on("close", (code, signal) => {
logLines.push(
`[process][PID:${childProcess.pid}] Exited with code ${code} signal ${signal}`
);
const line = `[process][PID:${childProcess.pid}] Exited with code ${code} signal ${signal}`;
serverLogger.info(line);
logLines.push(line);
serverPIDs.delete(childProcess.pid);
// Note: we keep the logs available until manually cleared
});
childProcess.on("error", (err) => {
logLines.push(
`[process][PID:${childProcess.pid}] Error: ${err.message}`
);
const line = `[process][PID:${childProcess.pid}] Error: ${err.message}`;
serverLogger.error(line);
logLines.push(line);
});
// Compute URL directly, do not try to parse logs
let effectiveHost = host;
if (!expose && host === "0.0.0.0") effectiveHost = "127.0.0.1";
const url = `http://${effectiveHost}:${availablePort}`;
console.log(`Server started with PID: ${childProcess.pid}, URL: ${url}`);
log.info(`Server started with PID: ${childProcess.pid}, URL: ${url}`);
return { url, pid: childProcess.pid };
};
@@ -686,7 +755,7 @@ export const startServer = async (
* Terminates all server processes.
*/
export async function stopAllServers(): Promise<void> {
console.log("Stopping all servers...");
log.info("Stopping all servers...");
for (const pid of Array.from(serverPIDs)) {
try {
terminateProcessTree(pid);
@@ -696,7 +765,7 @@ export async function stopAllServers(): Promise<void> {
console.error(`Error stopping server with PID ${pid}:`, error);
}
}
console.log("All servers stopped successfully.");
log.info("All servers stopped successfully.");
}
/**
* Kills a process tree by PID.
@@ -705,7 +774,7 @@ function terminateProcessTree(pid: number): void {
if (process.platform === "win32") {
try {
execSync(`taskkill /PID ${pid} /T /F`);
console.log(
log.info(
`Terminated server process tree (PID: ${pid}) on Windows.`
);
} catch (error) {
@@ -717,7 +786,7 @@ function terminateProcessTree(pid: number): void {
} else {
try {
process.kill(-pid, "SIGKILL");
console.log(
log.info(
`Terminated server process tree (PID: ${pid}) on Unix-like OS.`
);
} catch (error) {
@@ -741,7 +810,7 @@ export const checkUrlAndOpen = async (
url: string,
callback: Function = async () => {}
) => {
const maxAttempts = 150; // 5 minutes with 2-second intervals
const maxAttempts = 1800; // 60 minutes with 2-second intervals
const interval = 2000; // 2 seconds
let attempts = 0;
@@ -760,20 +829,20 @@ export const checkUrlAndOpen = async (
const pollUrl = async () => {
while (attempts < maxAttempts) {
attempts++;
console.log(
log.info(
`Checking URL availability (attempt ${attempts}/${maxAttempts}): ${url}`
);
try {
const isAvailable = await checkUrl();
if (isAvailable) {
console.log("URL is now available, opening browser...");
log.info("URL is now available, opening browser...");
await openUrl(url);
await callback(); // Call the provided callback function
return; // Exit the polling loop
}
} catch (error) {
console.log(`Error checking URL: ${error}`);
log.info(`Error checking URL: ${error}`);
}
// Wait before next attempt
@@ -782,7 +851,7 @@ export const checkUrlAndOpen = async (
// If we exit the loop without success
if (attempts >= maxAttempts) {
console.log("URL check timed out after 5 minutes");
log.info("URL check timed out after 5 minutes");
}
};
+10
View File
@@ -132,6 +132,16 @@ const api = {
return await ipcRenderer.invoke("server:info");
},
resetApp: async () => {
if (!isLocalSource()) {
throw new Error(
"Access restricted: This operation is only allowed in a local environment."
);
}
return await ipcRenderer.invoke("app:reset");
},
startServer: async () => {
if (!isLocalSource()) {
throw new Error(
+1 -1
View File
@@ -57,7 +57,7 @@
{:else if installed === false}
<Installation bind:installed />
{:else}
<Controls />
<Controls bind:installed />
{/if}
</main>
+74
View File
@@ -1,5 +1,79 @@
@import "tailwindcss";
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
@layer base {
html,
pre {
font-family:
-apple-system, BlinkMacSystemFont, "Inter", "Vazirmatn",
ui-sans-serif, system-ui, "Segoe UI", Roboto, Ubuntu, Cantarell,
"Noto Sans", sans-serif, "Helvetica Neue", Arial,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Noto Color Emoji";
}
pre {
white-space: pre-wrap;
}
button {
@apply cursor-pointer;
}
input::placeholder,
textarea::placeholder {
color: theme(--color-gray-400);
}
input[type="checkbox"] {
@apply appearance-none size-3.5 align-middle bg-white border border-gray-300 rounded transition cursor-pointer focus:ring-2 focus:ring-blue-500 focus:outline-none dark:bg-gray-800 dark:border-gray-600 self-center;
/* Center the custom mark */
display: inline-block;
position: relative;
}
input[type="checkbox"]:checked {
@apply bg-blue-600 border-blue-600;
}
input[type="checkbox"]:after {
content: "";
display: block;
width: 100%;
height: 100%;
/* Hide by default */
opacity: 0;
transition: opacity 0.2s;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
/* SVG checkmark as background image */
background: url('data:image/svg+xml;utf8,<svg viewBox="0 0 16 16" fill="none" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M4 8l3 3l5-5"/></svg>')
center/80% no-repeat;
}
input[type="checkbox"]:checked:after {
opacity: 1;
}
}
@custom-variant hover (&:hover);
@font-face {
font-family: "Archivo";
src: url("./lib/assets/fonts/Archivo-Variable.ttf");
@@ -7,6 +7,8 @@
import About from "./Controls/About.svelte";
import { info } from "../stores";
let { installed = $bindable(false) } = $props();
let selectedTab = $state("general");
onMount(async () => {});
@@ -104,7 +106,7 @@
class="flex-1 pt-1 sm:mt-0 overflow-y-scroll pr-1 scrollbar-hidden"
>
{#if selectedTab === "general"}
<General info={$info} />
<General bind:installed info={$info} />
{:else if selectedTab === "about"}
<About />
{/if}
@@ -7,7 +7,7 @@
import Spinner from "../common/Spinner.svelte";
import ConfirmDialog from "../common/ConfirmDialog.svelte";
let { info } = $props();
let { info, installed = $bindable(false) } = $props();
let config = $state(null);
@@ -48,16 +48,19 @@
</script>
<ConfirmDialog
bind:showConfirm
bind:show={showConfirm}
title="Factory Reset"
message="Are you sure you want to reset the app? This will remove all configurations, user data, and the bundled Python environment, restoring the app to its original state."
confirmLabel="Reset"
onConfirm={async () => {
try {
installed = null;
config = null;
await window.electronAPI.resetApp();
toast.success(
"App has been reset successfully. Please restart the app."
);
toast.success("App has been reset successfully.");
// refresh the page to apply changes
window.location.reload();
} catch (error) {
toast.error("Failed to reset the app");
}
@@ -11,20 +11,12 @@
import galaxyImage from "../assets/images/galaxy.jpg";
import greenImage from "../assets/images/green.jpg";
import adamImage from "../assets/images/adam.jpg";
import earthImage from "../assets/images/earth.jpg";
import nasaImage from "../assets/images/nasa.jpg";
import neomImage from "../assets/images/neom.jpg";
let { installed = $bindable() } = $props();
let images = [
galaxyImage,
greenImage,
adamImage,
earthImage,
nasaImage,
neomImage,
];
let images = [galaxyImage, greenImage, adamImage, nasaImage, neomImage];
let mounted = $state(false);
let currentTime = Date.now();
@@ -218,7 +210,7 @@
{`Continue`}
</div>
<!-- <button
<div
class="text-xs mt-3 text-gray-500 cursor-pointer"
in:fly={{
delay: 500,
@@ -226,8 +218,16 @@
y: 10,
}}
>
To connect to an existing server, click here.
</button> -->
By continuing, you agree to our
<button
class="underline"
onclick={() => {
window.electronAPI.openInBrowser(
"https://github.com/open-webui/desktop/blob/main/LICENSE"
);
}}>license agreement</button
>.
</div>
{/if}
</div>
</div>
@@ -7,19 +7,11 @@
import galaxyImage from "../assets/images/galaxy.jpg";
import greenImage from "../assets/images/green.jpg";
import adamImage from "../assets/images/adam.jpg";
import earthImage from "../assets/images/earth.jpg";
import nasaImage from "../assets/images/nasa.jpg";
import neomImage from "../assets/images/neom.jpg";
import { fly } from "svelte/transition";
let images = [
galaxyImage,
greenImage,
adamImage,
earthImage,
nasaImage,
neomImage,
];
let images = [galaxyImage, greenImage, adamImage, nasaImage, neomImage];
let startTime = $state(null);
let currentTime = $state(null);
@@ -8,23 +8,43 @@
import { flyAndScale } from "../../utils/transitions";
import { marked } from "marked";
export let title = "";
export let message = "";
let {
title,
message,
cancelLabel = "Cancel",
confirmLabel = "Confirm",
onConfirm = () => {},
input,
inputPlaceholder,
inputValue,
show = $bindable(false),
} = $props();
export let cancelLabel = "Cancel";
export let confirmLabel = "Confirm";
$effect(() => {
if (show) {
init();
}
});
export let onConfirm = () => {};
$effect(() => {
if (mounted) {
if (show && modalElement) {
document.body.appendChild(modalElement);
focusTrap = FocusTrap.createFocusTrap(modalElement);
focusTrap.activate();
export let input = false;
export let inputPlaceholder = "";
export let inputValue = "";
window.addEventListener("keydown", handleKeyDown);
document.body.style.overflow = "hidden";
} else if (modalElement) {
focusTrap.deactivate();
export let show = false;
window.removeEventListener("keydown", handleKeyDown);
document.body.removeChild(modalElement);
$: if (show) {
init();
}
document.body.style.overflow = "unset";
}
}
});
let modalElement = null;
let mounted = false;
@@ -57,24 +77,6 @@
mounted = true;
});
$: if (mounted) {
if (show && modalElement) {
document.body.appendChild(modalElement);
focusTrap = FocusTrap.createFocusTrap(modalElement);
focusTrap.activate();
window.addEventListener("keydown", handleKeyDown);
document.body.style.overflow = "hidden";
} else if (modalElement) {
focusTrap.deactivate();
window.removeEventListener("keydown", handleKeyDown);
document.body.removeChild(modalElement);
document.body.style.overflow = "unset";
}
}
onDestroy(() => {
show = false;
if (focusTrap) {