diff --git a/.gitignore b/.gitignore index 113b7b2..9c88b99 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ docus/.nuxt/ docus/.output/ docus/.data/ docus/.nitro/ +bin/launcher/ # OS .DS_Store diff --git a/bin/host/src-tauri/tauri.conf.json b/bin/host/src-tauri/tauri.conf.json index 3ce152a..ea2cdef 100644 --- a/bin/host/src-tauri/tauri.conf.json +++ b/bin/host/src-tauri/tauri.conf.json @@ -1,26 +1,27 @@ { - "$schema": "https://schema.tauri.app/config/2", - "productName": "FORGE Host", - "version": "0.1.0", - "identifier": "com.idsolutions.forge.host", - "build": { - "frontendDist": "../src" - }, - "app": { - "withGlobalTauri": true, - "windows": [ - { - "title": "FORGE Host", - "width": 1280, - "height": 720, - "minWidth": 980, - "minHeight": 640 - } - ], - "security": { - "csp": null - } - }, + "$schema": "https://schema.tauri.app/config/2", + "productName": "FORGE Host", + "version": "0.1.0", + "identifier": "com.idsolutions.forge.host", + "build": { + "frontendDist": "../src" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "FORGE Host", + "width": 1280, + "height": 720, + "minWidth": 980, + "minHeight": 640, + "maximizable": false + } + ], + "security": { + "csp": null + } + }, "bundle": { "active": true, "targets": "all", diff --git a/bin/host/src/app.js b/bin/host/src/app.js index 919fa1f..e3ce2d9 100644 --- a/bin/host/src/app.js +++ b/bin/host/src/app.js @@ -6,12 +6,14 @@ const serviceNames = { const subtitles = { overview: "Start, stop, and check local Forge hosting services.", - settings: "Edit process command, arguments, working directory, and health port.", + settings: + "Edit process command, arguments, working directory, and health port.", logs: "Recent stdout, stderr, and host supervisor events.", }; let snapshot = null; let refreshTimer = null; +let lastRenderedLogText = ""; let activeSettingsService = "surrealdb"; let configDirty = false; let activeEditorPath = ""; @@ -28,7 +30,10 @@ document.querySelectorAll(".nav-button").forEach((button) => { button.addEventListener("click", () => { const view = button.dataset.view; if (button.dataset.service) { - if (configDirty && button.dataset.service !== activeSettingsService) { + if ( + configDirty && + button.dataset.service !== activeSettingsService + ) { persistActiveSettingsForm(); } activeSettingsService = button.dataset.service; @@ -38,9 +43,13 @@ document.querySelectorAll(".nav-button").forEach((button) => { } activeSettingsService = "surrealdb"; } - document.querySelectorAll(".nav-button").forEach((item) => item.classList.remove("active")); + document + .querySelectorAll(".nav-button") + .forEach((item) => item.classList.remove("active")); button.classList.add("active"); - Object.entries(views).forEach(([key, element]) => element.classList.toggle("active", key === view)); + Object.entries(views).forEach(([key, element]) => + element.classList.toggle("active", key === view), + ); setHeader(view); renderSettings(true); syncBulkActionsVisibility(); @@ -52,9 +61,29 @@ document.getElementById("refreshButton").addEventListener("click", refresh); document.getElementById("saveButton").addEventListener("click", saveConfig); document.getElementById("startAllButton").addEventListener("click", startAll); document.getElementById("stopAllButton").addEventListener("click", stopAll); -document.getElementById("editorCloseButton").addEventListener("click", closeConfigEditor); -document.getElementById("editorReloadButton").addEventListener("click", () => runAction(() => openConfigEditor(activeEditorPath))); -document.getElementById("editorSaveButton").addEventListener("click", () => runAction(saveConfigEditor)); +document + .getElementById("editorCloseButton") + .addEventListener("click", closeConfigEditor); +document + .getElementById("editorReloadButton") + .addEventListener("click", () => + runAction(() => openConfigEditor(activeEditorPath)), + ); +document + .getElementById("editorSaveButton") + .addEventListener("click", () => runAction(saveConfigEditor)); + +async function loadAppVersion() { + const getVersion = window.__TAURI__?.app?.getVersion; + if (!getVersion) return; + + try { + document.getElementById("appVersion").textContent = + `v${await getVersion()}`; + } catch (error) { + console.warn("Unable to load app version:", error); + } +} async function refresh() { if (isSettingsViewActive() && configDirty) { @@ -89,7 +118,9 @@ async function startAll() { const serviceNames = Object.keys(snapshot.config).filter( (name) => - snapshot.config[name].enabled && (name === "surrealdb" || snapshot.config[name].command.trim() !== "") + snapshot.config[name].enabled && + (name === "surrealdb" || + snapshot.config[name].command.trim() !== ""), ); for (const name of serviceNames) { @@ -106,7 +137,9 @@ async function startAll() { async function stopAll() { if (!snapshot) return; - const serviceNames = Object.keys(snapshot.config).filter((name) => snapshot.config[name].enabled); + const serviceNames = Object.keys(snapshot.config).filter( + (name) => snapshot.config[name].enabled, + ); for (const name of serviceNames) { const status = snapshot.statuses.find((s) => s.name === name); if (status && status.running) { @@ -141,7 +174,9 @@ async function saveConfig() { function tauriInvoke(command, payload) { const invoke = window.__TAURI__?.core?.invoke; if (!invoke) { - return Promise.reject("Tauri invoke API is not available in this window."); + return Promise.reject( + "Tauri invoke API is not available in this window.", + ); } return invoke(command, payload); } @@ -157,32 +192,43 @@ function render() { } function setHeader(view) { - const title = view === "settings" ? serviceNames[activeSettingsService] : activeNavLabel(); + const title = + view === "settings" + ? serviceNames[activeSettingsService] + : activeNavLabel(); document.getElementById("viewTitle").textContent = title; document.getElementById("viewSubtitle").textContent = subtitles[view]; } function activeNavLabel() { - return document.querySelector(".nav-button.active span:last-child")?.textContent.trim() || "Overview"; + return ( + document + .querySelector(".nav-button.active span:last-child") + ?.textContent.trim() || "Overview" + ); } function syncSidebarStatus() { - document.querySelectorAll(".nav-button[data-service], .nav-button[data-view='settings']:not([data-service])").forEach((button) => { - const service = button.dataset.service || "surrealdb"; - const config = snapshot.config[service]; - const status = snapshot.statuses.find((s) => s.name === service); - const enabled = config?.enabled; - const running = status?.running; - const healthy = status?.healthy; + document + .querySelectorAll( + ".nav-button[data-service], .nav-button[data-view='settings']:not([data-service])", + ) + .forEach((button) => { + const service = button.dataset.service || "surrealdb"; + const config = snapshot.config[service]; + const status = snapshot.statuses.find((s) => s.name === service); + const enabled = config?.enabled; + const running = status?.running; + const healthy = status?.healthy; - const isDisabled = !enabled; - const isOnline = enabled && running && healthy; - const isStopped = enabled && !isOnline; + const isDisabled = !enabled; + const isOnline = enabled && running && healthy; + const isStopped = enabled && !isOnline; - button.classList.toggle("service-enabled", Boolean(isOnline)); - button.classList.toggle("service-disabled", Boolean(isDisabled)); - button.classList.toggle("service-stopped", Boolean(isStopped)); - }); + button.classList.toggle("service-enabled", Boolean(isOnline)); + button.classList.toggle("service-disabled", Boolean(isDisabled)); + button.classList.toggle("service-stopped", Boolean(isStopped)); + }); } function renderServices() { @@ -192,31 +238,44 @@ function renderServices() { snapshot.statuses.forEach((status) => { const node = template.content.firstElementChild.cloneNode(true); - node.querySelector("h2").textContent = serviceNames[status.name] || status.name; + node.querySelector("h2").textContent = + serviceNames[status.name] || status.name; const pingPill = node.querySelector(".ping-pill"); const pingDisplay = resolvePingDisplay(status); pingPill.textContent = pingDisplay.label; pingPill.classList.remove("good", "warn", "bad", "unavailable"); pingPill.classList.add(pingDisplay.className); - node.querySelector(".command-line").textContent = status.command || "No command configured"; + node.querySelector(".command-line").textContent = + status.command || "No command configured"; const pill = node.querySelector(".status-pill"); pill.textContent = status.running ? "Running" : "Stopped"; pill.classList.add(status.running ? "running" : "stopped"); - node.querySelector(".pid").textContent = status.pid ? String(status.pid) : "-"; + node.querySelector(".pid").textContent = status.pid + ? String(status.pid) + : "-"; node.querySelector(".health").textContent = status.health; - node.querySelector(".enabled").textContent = status.enabled ? "Yes" : "No"; + node.querySelector(".enabled").textContent = status.enabled + ? "Yes" + : "No"; const start = node.querySelector(".start"); const stop = node.querySelector(".stop"); if (!start || !stop) { - throw new Error("Service card template is missing start or stop controls."); + throw new Error( + "Service card template is missing start or stop controls.", + ); } - start.disabled = status.running || !status.enabled || !status.configured; + start.disabled = + status.running || !status.enabled || !status.configured; stop.disabled = !status.running; - start.addEventListener("click", () => runAction(() => startService(status.name))); - stop.addEventListener("click", () => runAction(() => stopService(status.name))); + start.addEventListener("click", () => + runAction(() => startService(status.name)), + ); + stop.addEventListener("click", () => + runAction(() => stopService(status.name)), + ); grid.appendChild(node); }); @@ -256,7 +315,9 @@ function renderSettings(force = false) { form.className = "settings-form"; form.innerHTML = renderSettingsPanel(name, config); - form.querySelectorAll("input[data-service], input[data-arg-key], select[data-arg-key]").forEach((input) => { + form.querySelectorAll( + "input[data-service], input[data-arg-key], select[data-arg-key]", + ).forEach((input) => { input.addEventListener("input", () => { configDirty = true; }); @@ -267,20 +328,30 @@ function renderSettings(force = false) { form.querySelectorAll("[data-picker]").forEach((button) => { button.addEventListener("click", () => { - runAction(() => pickPath(button.dataset.picker, button.dataset.target, button.dataset.serviceTarget)); + runAction(() => + pickPath( + button.dataset.picker, + button.dataset.target, + button.dataset.serviceTarget, + ), + ); }); }); form.querySelectorAll("[data-create-server-config]").forEach((button) => { button.addEventListener("click", () => { - const template = document.querySelector("[data-config-template]")?.value || "server"; + const template = + document.querySelector("[data-config-template]")?.value || + "server"; runAction(() => createArmaServerConfig(template)); }); }); form.querySelectorAll("[data-edit-server-config]").forEach((button) => { button.addEventListener("click", () => { - const path = document.querySelector('[data-arg-key="config"]')?.value.trim(); + const path = document + .querySelector('[data-arg-key="config"]') + ?.value.trim(); if (!path) { alert("Select or create a server config first."); return; @@ -310,7 +381,11 @@ function renderSettings(force = false) { if (name === "surrealdb" && !surrealInstallInfo) { loadSurrealInstallInfo(); - } else if (name === "surrealdb" && surrealInstallInfo && !surrealInstallInfo.latest) { + } else if ( + name === "surrealdb" && + surrealInstallInfo && + !surrealInstallInfo.latest + ) { loadLatestSurrealVersion(); } } @@ -319,6 +394,10 @@ function isSettingsViewActive() { return views.settings.classList.contains("active"); } +function isLogsViewActive() { + return views.logs.classList.contains("active"); +} + function syncRefreshTimer() { if (refreshTimer) { clearInterval(refreshTimer); @@ -326,22 +405,32 @@ function syncRefreshTimer() { } if (!isSettingsViewActive()) { - refreshTimer = setInterval(refresh, 2500); + refreshTimer = setInterval(refresh, isLogsViewActive() ? 30000 : 2500); } } function syncBulkActionsVisibility() { const bulkActions = document.getElementById("bulkActions"); - bulkActions.style.display = views.overview.classList.contains("active") ? "flex" : "none"; + bulkActions.style.display = views.overview.classList.contains("active") + ? "flex" + : "none"; const stopAllButton = document.getElementById("stopAllButton"); - const hasRunningService = Boolean(snapshot?.statuses.some((status) => status.running)); + const hasRunningService = Boolean( + snapshot?.statuses.some((status) => status.running), + ); stopAllButton.disabled = !hasRunningService; } function renderLogs() { const output = document.getElementById("logOutput"); - output.textContent = snapshot.logs.length ? snapshot.logs.join("\n") : "No host logs yet."; + const nextLogText = snapshot.logs.length + ? snapshot.logs.join("\n") + : "No host logs yet."; + if (nextLogText === lastRenderedLogText) return; + + lastRenderedLogText = nextLogText; + output.textContent = nextLogText; output.scrollTop = output.scrollHeight; } @@ -350,6 +439,7 @@ function renderError(error) { document.getElementById("configPath").textContent = "Unable to load"; document.getElementById("serviceGrid").replaceChildren(); document.getElementById("configForm").replaceChildren(); + lastRenderedLogText = message; document.getElementById("logOutput").textContent = message; } @@ -418,7 +508,9 @@ function renderSurrealInstallControls() { const installed = surrealInstallInfo?.installed; const version = surrealInstallInfo?.version || "Not installed"; const latest = surrealInstallInfo?.latest || "Checking..."; - const path = surrealInstallInfo?.path || "SurrealDB will be installed to the user-local default path."; + const path = + surrealInstallInfo?.path || + "SurrealDB will be installed to the user-local default path."; return `
@@ -455,17 +547,23 @@ function renderSurrealInstallControls() { } function selectedSurrealInstallVersion() { - const mode = document.querySelector("[data-surreal-version-mode]")?.value || "3"; + const mode = + document.querySelector("[data-surreal-version-mode]")?.value || "3"; if (mode !== "custom") { return mode; } - return document.querySelector("[data-surreal-custom-version]")?.value.trim() || "3"; + return ( + document.querySelector("[data-surreal-custom-version]")?.value.trim() || + "3" + ); } function syncSurrealCustomVersion() { const custom = document.querySelector(".install-custom"); if (!custom) return; - const isCustom = document.querySelector("[data-surreal-version-mode]")?.value === "custom"; + const isCustom = + document.querySelector("[data-surreal-version-mode]")?.value === + "custom"; custom.hidden = !isCustom; if (isCustom) { custom.querySelector("input")?.focus(); @@ -475,7 +573,12 @@ function syncSurrealCustomVersion() { async function loadSurrealInstallInfo() { try { surrealInstallInfo = await tauriInvoke("get_surrealdb_install_info"); - if (activeSettingsService === "surrealdb" && isSettingsViewActive() && !configDirty && !isSurrealInstallFocused()) { + if ( + activeSettingsService === "surrealdb" && + isSettingsViewActive() && + !configDirty && + !isSurrealInstallFocused() + ) { renderSettings(true); } await loadLatestSurrealVersion(); @@ -493,12 +596,25 @@ async function loadLatestSurrealVersion() { try { const latest = await tauriInvoke("get_latest_surrealdb_version"); surrealInstallInfo = { ...(surrealInstallInfo || {}), latest }; - if (activeSettingsService === "surrealdb" && isSettingsViewActive() && !configDirty && !isSurrealInstallFocused()) { + if ( + activeSettingsService === "surrealdb" && + isSettingsViewActive() && + !configDirty && + !isSurrealInstallFocused() + ) { renderSettings(true); } } catch (error) { - surrealInstallInfo = { ...(surrealInstallInfo || {}), latest: "Unavailable" }; - if (activeSettingsService === "surrealdb" && isSettingsViewActive() && !configDirty && !isSurrealInstallFocused()) { + surrealInstallInfo = { + ...(surrealInstallInfo || {}), + latest: "Unavailable", + }; + if ( + activeSettingsService === "surrealdb" && + isSettingsViewActive() && + !configDirty && + !isSurrealInstallFocused() + ) { renderSettings(true); } } @@ -685,8 +801,10 @@ function toggleArgument(label, key, value) { } function readArgumentFields(service, container) { - const value = (key) => container.querySelector(`[data-arg-key="${key}"]`)?.value.trim() || ""; - const checked = (key) => Boolean(container.querySelector(`[data-arg-key="${key}"]`)?.checked); + const value = (key) => + container.querySelector(`[data-arg-key="${key}"]`)?.value.trim() || ""; + const checked = (key) => + Boolean(container.querySelector(`[data-arg-key="${key}"]`)?.checked); if (service === "surrealdb") { const args = []; if (value("subcommand")) args.push(value("subcommand")); @@ -707,7 +825,8 @@ function readArgumentFields(service, container) { if (value("mods")) args.push(`-mod=${value("mods")}`); if (value("serverMods")) args.push(`-serverMod=${value("serverMods")}`); checked("autoInit") && args.push("-autoInit"); - checked("reportNonNetworkObject") && args.push("-reportNonNetworkObject"); + checked("reportNonNetworkObject") && + args.push("-reportNonNetworkObject"); args.push(checked("battleye") ? "-battleye" : "-noBattlEye"); args.push(...splitMiscArgs(value("misc"))); return args; @@ -796,7 +915,10 @@ function parseArmaArgs(args) { } function splitMiscArgs(value) { - return value.split(/\s+/).map((arg) => arg.trim()).filter(Boolean); + return value + .split(/\s+/) + .map((arg) => arg.trim()) + .filter(Boolean); } async function pickPath(kind, targetSelector, service) { @@ -818,7 +940,9 @@ async function pickPath(kind, targetSelector, service) { input.value = finalPath; if (kind === "file" && targetSelector.includes('data-field="command"')) { - const workingDir = document.querySelector(`[data-service="${service}"][data-field="working_dir"]`); + const workingDir = document.querySelector( + `[data-service="${service}"][data-field="working_dir"]`, + ); if (workingDir) { workingDir.value = finalPath.replace(/[\\/][^\\/]+$/, ""); } @@ -845,10 +969,14 @@ function pickerFilters(kind) { async function createArmaServerConfig(template) { const save = window.__TAURI__?.dialog?.save; if (!save) { - throw new Error("Tauri save dialog API is not available in this window."); + throw new Error( + "Tauri save dialog API is not available in this window.", + ); } - const workingDir = document.querySelector('[data-service="arma"][data-field="working_dir"]')?.value.trim(); + const workingDir = document + .querySelector('[data-service="arma"][data-field="working_dir"]') + ?.value.trim(); const defaultPath = workingDir ? `${workingDir}\\server.cfg` : "server.cfg"; const selected = await save({ defaultPath, @@ -856,7 +984,10 @@ async function createArmaServerConfig(template) { }); if (!selected) return; - await tauriInvoke("create_arma_server_config", { path: selected, template }); + await tauriInvoke("create_arma_server_config", { + path: selected, + template, + }); const input = document.querySelector('[data-arg-key="config"]'); if (input) { input.value = selected; @@ -871,7 +1002,9 @@ function resolveArmaConfigPath(path) { return path; } - const workingDir = document.querySelector('[data-service="arma"][data-field="working_dir"]')?.value.trim(); + const workingDir = document + .querySelector('[data-service="arma"][data-field="working_dir"]') + ?.value.trim(); return workingDir ? `${workingDir}\\${path}` : path; } @@ -906,11 +1039,15 @@ function normalizePickedExecutable(service, path) { } if (fileName === "arma3_x64.exe" || fileName === "arma3.exe") { - alert("That is the Arma 3 client executable. Forge Host will use arma3server_x64.exe from the same directory instead."); + alert( + "That is the Arma 3 client executable. Forge Host will use arma3server_x64.exe from the same directory instead.", + ); return path.replace(/[\\/][^\\/]+$/, "\\arma3server_x64.exe"); } - alert("Select arma3server_x64.exe for dedicated server hosting. The app will block client executables at launch."); + alert( + "Select arma3server_x64.exe for dedicated server hosting. The app will block client executables at launch.", + ); return path; } @@ -944,3 +1081,4 @@ function selectedAttr(selected) { refresh(); syncRefreshTimer(); +loadAppVersion(); diff --git a/bin/host/src/index.html b/bin/host/src/index.html index e2993e3..55dbbf1 100644 --- a/bin/host/src/index.html +++ b/bin/host/src/index.html @@ -1,145 +1,225 @@ + + + + FORGE Host + + - - - - FORGE Host - - + +
+ - -
- - -
-
-
-

Overview

-

Local service control for Forge server hosting.

-
-
- + -
-
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+ v0.1.0 +
+
+ + - - -
-
+
+
-
-
-
- -
-
-
- - - - - - + + diff --git a/bin/host/src/public/layer0.png b/bin/host/src/public/layer0.png new file mode 100644 index 0000000..d338dff Binary files /dev/null and b/bin/host/src/public/layer0.png differ diff --git a/bin/host/src/styles.css b/bin/host/src/styles.css index 9cceead..17d653a 100644 --- a/bin/host/src/styles.css +++ b/bin/host/src/styles.css @@ -1,601 +1,752 @@ :root { - color-scheme: dark; - --bg: #0b0d10; - --panel: #15181e; - --panel-soft: #1c2028; - --line: #343a45; - --text: #f3f0e8; - --muted: #aba79f; - --gold: #ffe681; - --green: #21d07a; - --red: #ff5b66; - --blue: #69a7ff; + color-scheme: dark; + --primary-bg: #18181b; + --panel-bg: rgba(24, 24, 27, 0.95); + --nav-bg: rgba(0, 0, 0, 0.5); + --surface: rgba(0, 0, 0, 0.5); + --surface-strong: rgba(0, 0, 0, 0.72); + --surface-soft: rgba(255, 255, 255, 0.06); + --accent-color: #fde68a; + --text-primary: #f6f6f6; + --text-secondary: rgba(246, 246, 246, 0.76); + --border-color: rgba(253, 230, 138, 0.22); + --border-muted: rgba(255, 255, 255, 0.1); + --shadow-dark: rgba(0, 0, 0, 0.5); + --green: #22c55e; + --red: #ef4444; + --blue: #69a7ff; } * { - box-sizing: border-box; + box-sizing: border-box; +} + +html, +body { + height: 100%; } body { - margin: 0; - min-width: 940px; - min-height: 620px; - overflow: hidden; - background: var(--bg); - color: var(--text); - font-family: Inter, "Segoe UI", Arial, sans-serif; + margin: 0; + min-width: 980px; + min-height: 640px; + overflow: hidden; + background: var(--primary-bg); + color: var(--text-primary); + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, + Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; } button, input, textarea, select { - font: inherit; + font: inherit; } button { - border: 1px solid rgba(255, 230, 129, 0.42); - background: rgba(255, 230, 129, 0.08); - color: var(--gold); - height: 38px; - padding: 0 14px; - cursor: pointer; - border-radius: 6px; + height: 42px; + padding: 0 14px; + border: 1px solid rgba(253, 230, 138, 0.35); + border-radius: 6px; + background: rgba(253, 230, 138, 0.08); + color: var(--accent-color); + cursor: pointer; + transition: + border-color 180ms ease, + background-color 180ms ease, + color 180ms ease, + box-shadow 180ms ease, + transform 180ms ease; } -button:hover { - border-color: var(--gold); - background: rgba(255, 230, 129, 0.14); +button:hover:not(:disabled) { + border-color: rgba(253, 230, 138, 0.7); + background: rgba(253, 230, 138, 0.13); + box-shadow: 0 0 18px rgba(253, 230, 138, 0.18); } button:disabled { - cursor: default; - opacity: 0.42; + cursor: not-allowed; + opacity: 0.55; } button.primary { - background: var(--gold); - color: #101114; - border-color: var(--gold); - font-weight: 700; + border-color: var(--accent-color); + background: var(--accent-color); + color: #0f0f0f; + font-weight: 700; } button.primary:hover:not(:disabled) { - background: #fff0a8; - border-color: #fff0a8; - box-shadow: 0 0 0 3px rgba(255, 230, 129, 0.14); + color: #fff; + box-shadow: 0 0 20px rgba(253, 230, 138, 0.6); + transform: translateY(-1px); } button.icon-button { - display: grid; - place-items: center; - width: 42px; - min-width: 42px; - height: 42px; - padding: 0; - font-size: 18px; - line-height: 1; + display: grid; + width: 42px; + min-width: 42px; + height: 42px; + place-items: center; + padding: 0; } .shell { - display: grid; - grid-template-columns: 260px 1fr; - height: 100vh; + display: grid; + grid-template-columns: 80px minmax(0, 1fr); + width: 100vw; + height: 100vh; } .rail { - display: flex; - flex-direction: column; - gap: 28px; - padding: 24px 18px; - background: #101216; - border-right: 1px solid #252a33; -} - -.brand { - display: flex; - align-items: center; - gap: 12px; - font-size: 18px; - font-weight: 800; - color: var(--gold); -} - -.brand-mark { - display: grid; - width: 38px; - height: 38px; - place-items: center; - background: var(--gold); - color: #111216; - border-radius: 6px; + position: relative; + z-index: 20; + display: flex; + flex-direction: column; + align-items: center; + padding: 3rem 0 1rem; + background: var(--nav-bg); + border-right: 1px solid rgba(253, 230, 138, 0.1); + box-shadow: + 0 4px 20px rgba(0, 0, 0, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.05); + backdrop-filter: blur(8px) saturate(150%); + -webkit-backdrop-filter: blur(8px) saturate(150%); } .nav { - display: grid; - gap: 10px; + display: flex; + flex-direction: column; + gap: 1rem; } .nav-button { - display: grid; - grid-template-columns: 28px 1fr auto; - align-items: center; - width: 100%; - height: 48px; - text-align: left; - color: var(--muted); - border-color: #3a3d2c; - background: transparent; + position: relative; + display: grid; + width: 50px; + min-width: 50px; + height: 50px; + place-items: center; + padding: 0; + border: 2px solid rgba(253, 230, 138, 0.3); + border-radius: 8px; + background: transparent; + color: rgba(246, 246, 246, 0.7); +} + +.nav-button span:last-child { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} + +.nav-button:hover:not(:disabled) { + border-color: rgba(253, 230, 138, 0.6); + background: rgba(253, 230, 138, 0.1); + color: var(--accent-color); + transform: translateX(2px); } .nav-button.active { - color: #111216; - border-color: var(--gold); - background: var(--gold); - font-weight: 800; + border-color: var(--accent-color); + background: var(--accent-color); + color: #0f0f0f; + box-shadow: 0 0 20px rgba(253, 230, 138, 0.4); } .nav-button[data-view="settings"]::after { - content: ""; - width: 12px; - height: 12px; - border-radius: 50%; - background: var(--muted); - transition: background-color 180ms ease, box-shadow 180ms ease; + position: absolute; + right: -5px; + bottom: -5px; + content: ""; + width: 12px; + height: 12px; + border-radius: 50%; + background: rgba(246, 246, 246, 0.45); + transition: + background-color 180ms ease, + box-shadow 180ms ease; } .nav-button.service-enabled::after { - background: var(--green); - animation: gumball-ping-green 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + background: var(--green); + animation: ping-green 2s cubic-bezier(0, 0, 0.2, 1) infinite; } .nav-button.service-stopped::after { - background: var(--red); - animation: gumball-ping-red 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + background: var(--red); + animation: ping-red 2s cubic-bezier(0, 0, 0.2, 1) infinite; } .nav-button.service-disabled::after { - background: #1a1d23; - border: 1px solid rgba(255, 230, 129, 0.38); - animation: gumball-ping-gold 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + background: transparent; + border: 1px solid rgba(253, 230, 138, 0.38); + animation: ping-inward-shadow 2s cubic-bezier(0, 0, 0.2, 1) infinite; } .svg-icon { - display: block; - width: 18px; - height: 18px; - flex: 0 0 auto; - background: currentColor; - -webkit-mask: var(--icon-url) center / contain no-repeat; - mask: var(--icon-url) center / contain no-repeat; + display: block; + width: 22px; + height: 22px; + flex: 0 0 auto; + background: currentColor; + -webkit-mask: var(--icon-url) center / contain no-repeat; + mask: var(--icon-url) center / contain no-repeat; } .icon-browse { - --icon-url: url("./public/browse.svg"); + --icon-url: url("./public/browse.svg"); } .icon-close { - --icon-url: url("./public/close.svg"); + --icon-url: url("./public/close.svg"); } .icon-dashboard { - --icon-url: url("./public/dashboard.svg"); + --icon-url: url("./public/dashboard.svg"); } .icon-database { - --icon-url: url("./public/database.svg"); + --icon-url: url("./public/database.svg"); } .icon-edit { - --icon-url: url("./public/edit.svg"); + --icon-url: url("./public/edit.svg"); } .icon-logs { - --icon-url: url("./public/logs.svg"); + --icon-url: url("./public/logs.svg"); } .icon-icom { - --icon-url: url("./public/icom.svg"); + --icon-url: url("./public/icom.svg"); } .icon-new { - --icon-url: url("./public/new.svg"); + --icon-url: url("./public/new.svg"); } .icon-refresh { - --icon-url: url("./public/refresh.svg"); + --icon-url: url("./public/refresh.svg"); } .icon-save { - --icon-url: url("./public/save.svg"); + --icon-url: url("./public/save.svg"); } .icon-server { - --icon-url: url("./public/server.svg"); + --icon-url: url("./public/server.svg"); } .icon-play { - --icon-url: url("./public/play.svg"); + --icon-url: url("./public/play.svg"); } .icon-stop { - --icon-url: url("./public/stop.svg"); -} - -.config-path { - margin-top: auto; - display: grid; - gap: 8px; - color: var(--muted); - font-size: 12px; -} - -.config-path strong { - overflow-wrap: anywhere; - color: var(--text); - font-weight: 500; + --icon-url: url("./public/stop.svg"); } .workspace { - display: grid; - grid-template-rows: auto 1fr; - min-width: 0; - min-height: 0; - background: - linear-gradient(rgba(11, 13, 16, 0.88), rgba(11, 13, 16, 0.94)), - url("./public/forge.png") center / 512px no-repeat; + position: relative; + display: grid; + grid-template-columns: 325px minmax(0, 1fr); + min-width: 0; + min-height: 0; + overflow: hidden; + background: + center center / cover no-repeat url("./public/layer0.png"), + var(--primary-bg); +} + +.workspace::before { + position: absolute; + inset: 0; + content: ""; + background: + radial-gradient( + circle at 50% 50%, + rgba(253, 230, 138, 0.08), + transparent 28% + ), + linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0.18)); + opacity: 0.45; + pointer-events: none; } .topbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 18px; - padding: 28px 32px 20px; - border-bottom: 1px solid #252a33; + position: relative; + z-index: 5; + display: flex; + flex-direction: column; + gap: 1.25rem; + min-width: 0; + height: 100vh; + padding: 40px 20px 20px; + overflow: hidden; + background: var(--panel-bg); + border-right: 1px solid rgba(253, 230, 138, 0.08); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +.topbar::after { + position: absolute; + left: 50%; + bottom: 0; + z-index: -1; + width: 210px; + height: 250px; + content: ""; + background: bottom center / contain no-repeat url("./public/forge.png"); + opacity: 0.12; + filter: drop-shadow(0 0 20px var(--shadow-dark)); + transform: translateX(-50%); + pointer-events: none; +} + +.view-heading { + position: relative; + padding: 0.9rem 1rem; + border: 1px solid var(--border-color); + border-radius: 8px; + background: rgba(24, 24, 27, 0.86); + box-shadow: 0 4px 20px var(--shadow-dark); } .topbar h1 { - margin: 0; - font-size: 28px; - line-height: 1.1; - color: var(--gold); + margin: 0; + color: var(--accent-color); + font-size: 1.5rem; + font-weight: 600; + line-height: 1.15; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); } .topbar p { - margin: 8px 0 0; - color: var(--muted); + margin: 0.65rem 0 0; + color: var(--text-secondary); + font-size: 0.92rem; + line-height: 1.45; +} + +.top-actions, +.bulk-actions { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + flex-wrap: wrap; } .top-actions { - display: flex; - gap: 10px; - align-items: center; + position: relative; + z-index: 2; } -.bulk-actions { - display: flex; - align-items: center; - gap: 8px; +.top-actions .icon-button, +.card-actions .icon-button, +.input-action .icon-button, +.editor-actions .icon-button, +.editor-header .icon-button { + border-radius: 8px; } -.top-actions .icon-button { - width: 42px; - min-width: 42px; - height: 42px; - border-radius: 8px; - transition: border-color 140ms ease, background-color 140ms ease, color 140ms ease, box-shadow 140ms ease; +.config-path { + position: relative; + z-index: 2; + display: grid; + gap: 0.55rem; + margin-top: auto; + color: var(--text-secondary); + font-size: 0.78rem; } -.card-actions .icon-button { - width: 36px; - min-width: 36px; - height: 36px; - border-radius: 10px; - transition: border-color 140ms ease, background-color 140ms ease, color 140ms ease; +.config-path span { + color: var(--accent-color); + font-weight: 700; +} + +.config-path strong { + padding: 0.75rem; + overflow-wrap: anywhere; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + background: rgba(0, 0, 0, 0.4); + color: var(--text-primary); + font-family: "Cascadia Mono", Consolas, monospace; + font-size: 0.75rem; + font-weight: 500; + line-height: 1.45; +} + +.version-display { + position: fixed; + right: 20px; + bottom: 20px; + z-index: 50; +} + +.version-text { + display: inline-flex; + align-items: center; + min-height: 32px; + padding: 0.5rem 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + background: rgba(0, 0, 0, 0.4); + color: var(--text-secondary); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + font-family: "Courier New", monospace; + font-size: 0.8rem; + transition: + border-color 180ms ease, + background-color 180ms ease, + color 180ms ease, + box-shadow 180ms ease; +} + +.version-text:hover { + border-color: rgba(253, 230, 138, 0.3); + background: rgba(0, 0, 0, 0.6); + color: var(--accent-color); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); } .view { - display: none; - min-height: 0; - max-height: 100%; - overflow: auto; - padding: 28px 32px 36px; + position: relative; + z-index: 2; + display: none; + grid-column: 2; + grid-row: 1; + min-height: 0; + max-height: 100vh; + overflow: auto; + padding: 2rem; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.view::-webkit-scrollbar { + display: none; } .view.active { - display: block; + display: block; +} + +#logsView { + overflow: hidden; } .service-grid { - display: grid; - grid-template-columns: repeat(3, minmax(220px, 1fr)); - gap: 18px; + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: 1rem; + max-width: 1120px; } .service-card, -.settings-panel { - border: 1px solid rgba(255, 230, 129, 0.22); - background: rgba(15, 17, 21, 0.86); - border-radius: 8px; +.settings-panel, +.log-output, +.surreal-install, +.no-results-content { + border: 1px solid var(--border-muted); + border-radius: 8px; + background: var(--surface); + box-shadow: + 0 4px 16px var(--shadow-dark), + inset 0 1px 0 rgba(255, 255, 255, 0.05); + backdrop-filter: blur(8px) saturate(150%); + -webkit-backdrop-filter: blur(8px) saturate(150%); } .service-card { - display: grid; - gap: 22px; - padding: 20px; + display: grid; + gap: 1.35rem; + min-height: 245px; + padding: 1.25rem; + transition: + border-color 180ms ease, + box-shadow 180ms ease, + transform 180ms ease; +} + +.service-card:hover { + border-color: rgba(253, 230, 138, 0.32); + box-shadow: + 0 6px 24px rgba(0, 0, 0, 0.58), + inset 0 1px 0 rgba(255, 255, 255, 0.06); + transform: translateY(-1px); } .service-card header { - display: flex; - justify-content: space-between; - gap: 14px; + display: flex; + justify-content: space-between; + gap: 1rem; } .service-card h2, .settings-panel h2 { - margin: 0; - font-size: 19px; - color: var(--text); + margin: 0; + color: var(--text-primary); + font-size: 1.12rem; + font-weight: 650; } .service-title-row { - display: flex; - align-items: center; - gap: 8px; -} - -.ping-pill { - display: inline-flex; - align-items: center; - min-height: 22px; - padding: 2px 8px; - border-radius: 999px; - border: 1px solid rgba(114, 119, 128, 0.55); - background: rgba(114, 119, 128, 0.14); - color: #c1c6cf; - font-size: 12px; - font-weight: 700; - letter-spacing: 0.01em; -} - -.ping-pill.good { - border-color: rgba(33, 208, 122, 0.45); - background: rgba(33, 208, 122, 0.14); - color: #92f7c6; -} - -.ping-pill.warn { - border-color: rgba(255, 230, 129, 0.5); - background: rgba(255, 230, 129, 0.14); - color: #ffe9a8; -} - -.ping-pill.bad { - border-color: rgba(255, 91, 102, 0.5); - background: rgba(255, 91, 102, 0.14); - color: #ffb2b8; -} - -.ping-pill.unavailable { - border-color: rgba(114, 119, 128, 0.55); - background: rgba(114, 119, 128, 0.14); - color: #c1c6cf; + display: flex; + align-items: center; + gap: 0.55rem; + flex-wrap: wrap; } .command-line { - margin: 8px 0 0; - color: var(--muted); - overflow-wrap: anywhere; + margin: 0.55rem 0 0; + overflow-wrap: anywhere; + color: var(--text-secondary); + font-size: 0.88rem; + line-height: 1.45; +} + +.ping-pill { + display: inline-flex; + align-items: center; + min-height: 22px; + padding: 2px 8px; + border: 1px solid rgba(114, 119, 128, 0.55); + border-radius: 999px; + background: rgba(114, 119, 128, 0.14); + color: #c1c6cf; + font-size: 0.74rem; + font-weight: 700; +} + +.ping-pill.good { + border-color: rgba(253, 230, 138, 0.45); + background: rgba(253, 230, 138, 0.14); + color: var(--accent-color); +} + +.ping-pill.warn { + border-color: rgba(253, 230, 138, 0.5); + background: rgba(253, 230, 138, 0.1); + color: #ffe9a8; +} + +.ping-pill.bad { + border-color: rgba(239, 68, 68, 0.5); + background: rgba(239, 68, 68, 0.14); + color: #ffb2b8; +} + +.ping-pill.unavailable { + border-color: rgba(114, 119, 128, 0.55); + background: rgba(114, 119, 128, 0.14); + color: #c1c6cf; } .status-pill { - align-self: start; - min-width: 82px; - padding: 6px 8px; - border-radius: 999px; - text-align: center; - font-size: 12px; - font-weight: 800; - color: #101114; - background: var(--muted); + align-self: start; + min-width: 82px; + padding: 6px 8px; + border-radius: 999px; + background: rgba(246, 246, 246, 0.55); + color: #101114; + text-align: center; + font-size: 0.74rem; + font-weight: 600; } .status-pill.running { - background: var(--green); + background: var(--accent-color); + box-shadow: 0 0 10px rgba(253, 230, 138, 0.35); } .status-pill.stopped { - background: var(--red); + background: var(--red); + color: #fff; } .metrics { - display: grid; - gap: 12px; - margin: 0; + display: grid; + gap: 0.75rem; + margin: 0; } .metrics div { - display: flex; - justify-content: space-between; - gap: 12px; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); - padding-bottom: 9px; + display: flex; + justify-content: space-between; + gap: 0.75rem; + padding-bottom: 0.6rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); } .metrics dt { - color: var(--muted); + color: var(--text-secondary); } .metrics dd { - margin: 0; - text-align: right; + margin: 0; + text-align: right; } .card-actions { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 12px; - margin-top: auto; + display: flex; + align-items: center; + gap: 0.75rem; + margin-top: auto; } .bulk-actions .start, .card-actions .start { - color: #ffe48a; - border-color: rgba(255, 230, 129, 0.42); - background: rgba(255, 230, 129, 0.08); -} - -.bulk-actions .start:hover:not(:disabled), -.card-actions .start:hover:not(:disabled) { - border-color: #ffe48a; - background: rgba(255, 230, 129, 0.16); - box-shadow: 0 0 0 3px rgba(255, 230, 129, 0.1); + border-color: rgba(253, 230, 138, 0.42); + background: rgba(253, 230, 138, 0.08); + color: var(--accent-color); } .bulk-actions .stop, -.card-actions .stop { - color: #ff9ca3; - border-color: rgba(255, 91, 102, 0.45); - background: rgba(255, 91, 102, 0.08); +.card-actions .stop, +.editor-header #editorCloseButton { + border-color: rgba(239, 68, 68, 0.45); + background: rgba(239, 68, 68, 0.08); + color: #ff9ca3; } .bulk-actions .stop:hover:not(:disabled), -.card-actions .stop:hover:not(:disabled) { - border-color: #ff9ca3; - background: rgba(255, 91, 102, 0.16); - box-shadow: 0 0 0 3px rgba(255, 91, 102, 0.1); +.card-actions .stop:hover:not(:disabled), +.editor-header #editorCloseButton:hover:not(:disabled) { + border-color: #ff9ca3; + background: rgba(239, 68, 68, 0.15); + box-shadow: 0 0 18px rgba(239, 68, 68, 0.16); } .bulk-actions .icon-button:disabled, .card-actions .icon-button:disabled { - cursor: not-allowed; - opacity: 1; - color: #727780; - border-color: rgba(114, 119, 128, 0.5); - background: rgba(114, 119, 128, 0.12); + opacity: 1; + border-color: rgba(114, 119, 128, 0.5); + background: rgba(114, 119, 128, 0.12); + color: #727780; } .settings-form { - display: block; - padding-bottom: 44px; + display: block; + padding-bottom: 2.75rem; } .settings-panel { - padding: 20px; - display: none; + display: none; + padding: 1.25rem; } .settings-panel.active { - display: block; + display: block; } .settings-header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 18px; - margin-bottom: 18px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.15rem; } .toggle-row { - display: flex; - align-items: center; - gap: 10px; - color: var(--muted); - cursor: pointer; - user-select: none; + display: flex; + align-items: center; + gap: 0.65rem; + color: var(--text-secondary); + cursor: pointer; + user-select: none; } .toggle-row input { - position: absolute; - opacity: 0; - pointer-events: none; + position: absolute; + opacity: 0; + pointer-events: none; } .switch-track { - position: relative; - display: inline-flex; - align-items: center; - width: 48px; - height: 26px; - padding: 3px; - border: 1px solid rgba(114, 119, 128, 0.55); - border-radius: 999px; - background: rgba(114, 119, 128, 0.22); - transition: border-color 140ms ease, background-color 140ms ease, box-shadow 140ms ease; + position: relative; + display: inline-flex; + align-items: center; + width: 48px; + height: 26px; + padding: 3px; + border: 1px solid rgba(114, 119, 128, 0.55); + border-radius: 999px; + background: rgba(114, 119, 128, 0.22); + transition: + border-color 140ms ease, + background-color 140ms ease, + box-shadow 140ms ease; } .switch-thumb { - width: 18px; - height: 18px; - border-radius: 999px; - background: #d4d0c6; - box-shadow: 0 2px 7px rgba(0, 0, 0, 0.35); - transform: translateX(0); - transition: transform 140ms ease, background-color 140ms ease; + width: 18px; + height: 18px; + border-radius: 999px; + background: #d4d0c6; + box-shadow: 0 2px 7px rgba(0, 0, 0, 0.35); + transform: translateX(0); + transition: + transform 140ms ease, + background-color 140ms ease; } .toggle-row:hover .switch-track { - border-color: rgba(255, 230, 129, 0.6); + border-color: rgba(253, 230, 138, 0.6); } .toggle-row input:focus-visible + .switch-track { - box-shadow: 0 0 0 3px rgba(255, 230, 129, 0.18); + box-shadow: 0 0 0 3px rgba(253, 230, 138, 0.18); } .toggle-row input:checked + .switch-track { - border-color: var(--gold); - background: var(--gold); + border-color: var(--accent-color); + background: var(--accent-color); } .toggle-row input:checked + .switch-track .switch-thumb { - background: #101114; - transform: translateX(22px); + background: #101114; + transform: translateX(22px); } .field-grid { - display: grid; - grid-template-columns: 2fr 1fr 1fr; - gap: 14px; + display: grid; + grid-template-columns: 2fr 1fr 1fr; + gap: 0.9rem; } -.field { - display: grid; - gap: 8px; +.field, +.arg-field { + display: grid; + gap: 0.5rem; } -.field.full { - grid-column: 1 / -1; +.field.full, +.arg-field.full { + grid-column: 1 / -1; } .field.with-button { - min-width: 0; -} - -.input-action { - display: grid; - grid-template-columns: minmax(0, 1fr) 42px; - gap: 8px; -} - -.input-action.action-one { - grid-template-columns: minmax(0, 1fr) 42px; -} - -.input-action.action-three { - grid-template-columns: minmax(0, 1fr) repeat(3, 42px); -} - -.input-action button { - height: 42px; + min-width: 0; } .field label, -.arg-field label { - color: var(--muted); - font-size: 13px; +.arg-field label, +.install-version span, +.install-custom span { + color: var(--text-secondary); + font-size: 0.82rem; } .field input, @@ -603,255 +754,341 @@ button.icon-button { .field select, .arg-field input, .arg-field select { - width: 100%; - border: 1px solid var(--line); - background: rgba(0, 0, 0, 0.48); - color: var(--text); - border-radius: 6px; - padding: 10px 12px; + width: 100%; + min-height: 42px; + padding: 10px 12px; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 6px; + outline: 0; + background: rgba(0, 0, 0, 0.48); + color: var(--text-primary); + transition: + border-color 140ms ease, + box-shadow 140ms ease, + background-color 140ms ease; +} + +.field input:focus, +.field textarea:focus, +.field select:focus, +.arg-field input:focus, +.arg-field select:focus { + border-color: rgba(253, 230, 138, 0.58); + box-shadow: 0 0 0 3px rgba(253, 230, 138, 0.1); + background: rgba(0, 0, 0, 0.62); } .field textarea { - min-height: 72px; - resize: vertical; + min-height: 72px; + resize: vertical; +} + +.input-action { + display: grid; + grid-template-columns: minmax(0, 1fr) 42px; + gap: 0.5rem; +} + +.input-action.action-three { + grid-template-columns: minmax(0, 1fr) repeat(3, 42px); } .args-list { - display: grid; - grid-template-columns: repeat(3, minmax(180px, 1fr)); - gap: 14px; -} - -.arg-field { - display: grid; - gap: 8px; -} - -.arg-field.full { - grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(3, minmax(180px, 1fr)); + gap: 0.9rem; } .arg-toggle { - display: flex; - align-items: center; - gap: 10px; - min-height: 42px; - margin-top: 21px; - color: var(--muted); + display: flex; + align-items: center; + gap: 0.65rem; + min-height: 42px; + margin-top: 21px; + color: var(--text-secondary); } .arg-toggle input { - width: 18px; - height: 18px; - accent-color: var(--gold); + width: 18px; + height: 18px; + accent-color: var(--accent-color); } .surreal-install { - padding: 14px 16px; - border: 1px solid rgba(255, 230, 129, 0.18); - border-radius: 8px; - background: rgba(255, 230, 129, 0.04); + display: grid; + gap: 1rem; + padding: 0.9rem 1rem; + background: rgba(253, 230, 138, 0.04); } .install-summary { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; - margin-bottom: 12px; + display: grid; + grid-template-columns: minmax(0, 2fr) minmax(220px, 1fr); + gap: 1.5rem; + margin-bottom: 0; } -.install-summary div { - display: grid; - gap: 5px; - min-width: 0; +.install-summary div, +.install-version, +.install-custom { + display: grid; + gap: 0.35rem; + min-width: 0; } .install-summary strong { - color: var(--text); - overflow-wrap: anywhere; + overflow-wrap: anywhere; + color: var(--text-primary); } .install-summary span { - color: var(--muted); - overflow-wrap: anywhere; + overflow-wrap: anywhere; + color: var(--text-secondary); + font-size: 0.9rem; + line-height: 1.45; +} + +.install-summary div:first-child span { + font-family: "Cascadia Mono", Consolas, monospace; + font-size: 0.82rem; } .install-action { - display: flex; - align-items: end; - gap: 10px; - flex-wrap: wrap; -} - -.install-version, -.install-custom { - display: grid; - gap: 6px; + display: grid; + grid-template-columns: 120px minmax(220px, 1fr) auto; + align-items: end; + gap: 0.65rem; } .install-version { - width: 120px; + min-width: 0; } .install-custom { - width: 180px; -} - -.install-version span, -.install-custom span { - color: var(--muted); - font-size: 13px; -} - -.install-action button { - height: 42px; + min-width: 0; } .log-output { - min-height: 100%; - margin: 0; - padding: 18px; - border: 1px solid #252a33; - background: rgba(0, 0, 0, 0.68); - color: #d7f7df; - border-radius: 8px; - white-space: pre-wrap; - overflow-wrap: anywhere; - font-family: "Cascadia Mono", Consolas, monospace; - font-size: 13px; - line-height: 1.55; + height: calc(100vh - 4rem); + min-height: 0; + margin: 0; + padding: 1.1rem; + overflow: auto; + overflow-wrap: anywhere; + white-space: pre-wrap; + background: var(--surface-strong); + color: #d7f7df; + font-family: "Cascadia Mono", Consolas, monospace; + font-size: 0.82rem; + line-height: 1.55; + scrollbar-color: rgba(253, 230, 138, 0.45) rgba(255, 255, 255, 0.08); +} + +.log-output::-webkit-scrollbar { + width: 12px; +} + +.log-output::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.06); +} + +.log-output::-webkit-scrollbar-thumb { + border: 3px solid transparent; + border-radius: 999px; + background: rgba(253, 230, 138, 0.48); + background-clip: content-box; +} + +.log-output::-webkit-scrollbar-thumb:hover { + background: rgba(253, 230, 138, 0.72); + background-clip: content-box; } .editor-overlay { - position: fixed; - inset: 0; - display: grid; - place-items: center; - padding: 28px; - background: rgba(0, 0, 0, 0.68); - z-index: 20; + position: fixed; + inset: 0; + z-index: 100; + display: grid; + place-items: center; + padding: 1.75rem; + background: + radial-gradient( + circle at 72% 56%, + rgba(253, 230, 138, 0.08), + transparent 34% + ), + rgba(0, 0, 0, 0.68); + backdrop-filter: blur(10px) saturate(120%); + -webkit-backdrop-filter: blur(10px) saturate(120%); } .editor-overlay[hidden] { - display: none; + display: none; } .editor-modal { - display: grid; - grid-template-rows: auto minmax(0, 1fr) auto; - width: min(1180px, 92vw); - height: min(760px, 86vh); - border: 1px solid rgba(255, 230, 129, 0.3); - background: #0e1116; - border-radius: 8px; - overflow: hidden; + display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; + width: min(1040px, 88vw); + height: min(680px, 82vh); + overflow: hidden; + border: 1px solid var(--border-color); + border-radius: 8px; + background: rgba(24, 24, 27, 0.9); + box-shadow: + 0 18px 60px rgba(0, 0, 0, 0.68), + inset 0 1px 0 rgba(255, 255, 255, 0.05); + backdrop-filter: blur(12px) saturate(150%); + -webkit-backdrop-filter: blur(12px) saturate(150%); } .editor-header, .editor-actions { - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; - padding: 16px 18px; - border-bottom: 1px solid #252a33; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.9rem; + padding: 1rem 1.15rem; + background: rgba(0, 0, 0, 0.28); + border-bottom: 1px solid rgba(253, 230, 138, 0.12); } .editor-header { - position: relative; - padding-right: 64px; + position: relative; + min-height: 82px; + padding: 1.1rem 4rem 1rem 1.15rem; } .editor-header #editorCloseButton { - position: absolute; - top: 12px; - right: 16px; - width: 34px; - min-width: 34px; - height: 34px; - color: #ff9ca3; - border-color: rgba(255, 91, 102, 0.45); - background: rgba(255, 91, 102, 0.08); + position: absolute; + top: 1rem; + right: 1.15rem; + width: 36px; + min-width: 36px; + height: 36px; } .editor-header #editorCloseButton .svg-icon { - width: 16px; - height: 16px; -} - -.editor-header #editorCloseButton:hover { - border-color: #ff9ca3; - background: rgba(255, 91, 102, 0.15); + width: 16px; + height: 16px; } .editor-actions { - justify-content: flex-end; - border-top: 1px solid #252a33; - border-bottom: 0; + justify-content: flex-end; + padding: 0.9rem 1.15rem; + background: rgba(0, 0, 0, 0.28); + border-top: 1px solid rgba(253, 230, 138, 0.12); + border-bottom: 0; } .editor-header h2 { - margin: 0; - color: var(--gold); + margin: 0; + color: var(--accent-color); + font-size: 1.5rem; + font-weight: 700; + line-height: 1.1; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); } .editor-header p { - margin: 6px 0 0; - color: var(--muted); - overflow-wrap: anywhere; + margin: 0.75rem 0 0; + overflow-wrap: anywhere; + color: var(--text-secondary); + font-family: "Cascadia Mono", Consolas, monospace; + font-size: 0.82rem; + line-height: 1.35; } #configEditor { - width: 100%; - height: 100%; - border: 0; - outline: 0; - resize: none; - padding: 18px; - background: #050609; - color: #e7f7df; - font-family: "Cascadia Mono", Consolas, monospace; - font-size: 14px; - line-height: 1.5; + width: 100%; + height: 100%; + padding: 1.25rem 1.15rem; + border: 0; + border-block: 1px solid rgba(255, 255, 255, 0.06); + outline: 0; + resize: none; + background: rgba(0, 0, 0, 0.58); + color: var(--text-primary); + caret-color: var(--accent-color); + font-family: "Cascadia Mono", Consolas, monospace; + font-size: 0.88rem; + font-weight: 500; + line-height: 1.55; + scrollbar-color: rgba(253, 230, 138, 0.45) rgba(255, 255, 255, 0.08); } -@keyframes gumball-ping-green { - 0%, 100% { - box-shadow: 0 0 0px rgba(33, 208, 122, 0.2); - } - 50% { - box-shadow: 0 0 12px rgba(33, 208, 122, 0.6); - } +#configEditor::selection { + background: rgba(253, 230, 138, 0.26); + color: #fff; } -@keyframes gumball-ping-red { - 0%, 100% { - box-shadow: 0 0 0px rgba(255, 91, 102, 0.2); - } - 50% { - box-shadow: 0 0 12px rgba(255, 91, 102, 0.62); - } +#configEditor::-webkit-scrollbar { + width: 12px; } -@keyframes gumball-ping-gold { - 0%, 100% { - box-shadow: 0 0 0px rgba(255, 230, 129, 0.16); - } - 50% { - box-shadow: 0 0 12px rgba(255, 230, 129, 0.55); - } +#configEditor::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.06); } -@media (max-width: 1050px) { - .service-grid { - grid-template-columns: 1fr; - } - - .field-grid { - grid-template-columns: 1fr; - } - - .args-list { - grid-template-columns: 1fr; - } +#configEditor::-webkit-scrollbar-thumb { + border: 3px solid transparent; + border-radius: 999px; + background: rgba(253, 230, 138, 0.48); + background-clip: content-box; +} + +#configEditor::-webkit-scrollbar-thumb:hover { + background: rgba(253, 230, 138, 0.72); + background-clip: content-box; +} + +@keyframes ping-shadow { + 0% { + box-shadow: 0 0 rgba(253, 230, 138, 0.7); + } + + 100% { + box-shadow: 0 0 0 10px rgba(253, 230, 138, 0); + } +} + +@keyframes ping-inward-shadow { + 0% { + box-shadow: 0 0 0 10px rgba(253, 230, 138, 0); + } + + 100% { + box-shadow: 0 0 rgba(253, 230, 138, 0.3); + } +} + +@keyframes ping-red { + 0% { + box-shadow: 0 0 rgba(239, 68, 68, 0.6); + } + + 100% { + box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); + } +} + +@keyframes ping-green { + 0% { + box-shadow: 0 0 rgba(34, 197, 94, 0.6); + } + + 100% { + box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); + } +} + +@media (max-width: 1120px) { + .service-grid { + grid-template-columns: 1fr; + } + + .field-grid, + .args-list { + grid-template-columns: 1fr; + } }