From c692e91347b7e6f0146c814771a2c34b609d2e8e Mon Sep 17 00:00:00 2001
From: su-fen <>
Date: Tue, 3 Mar 2026 19:51:31 +0800
Subject: [PATCH 1/6] perf(windows): optimize daemon startup and GUI window
style for Windows
- Hide console window for cratebay-daemon on Windows release builds via windows_subsystem attribute
- Replace native window decorations with custom titlebar and window controls (minimize/maximize/close)
- Move window config to platform-specific tauri.windows.conf.json
- Add drag region support for custom titlebar
- Add Windows release build script
---
crates/cratebay-daemon/src/main.rs | 3 +
.../src-tauri/capabilities/default.json | 6 +-
crates/cratebay-gui/src-tauri/tauri.conf.json | 11 --
.../src-tauri/tauri.windows.conf.json | 16 ++
crates/cratebay-gui/src/App.css | 55 +++++++
crates/cratebay-gui/src/App.tsx | 31 +++-
crates/cratebay-gui/src/icons.tsx | 5 +
scripts/build-release-windows.sh | 142 ++++++++++++++++++
8 files changed, 254 insertions(+), 15 deletions(-)
create mode 100644 crates/cratebay-gui/src-tauri/tauri.windows.conf.json
create mode 100644 scripts/build-release-windows.sh
diff --git a/crates/cratebay-daemon/src/main.rs b/crates/cratebay-daemon/src/main.rs
index 308c6ec..a9a47ba 100644
--- a/crates/cratebay-daemon/src/main.rs
+++ b/crates/cratebay-daemon/src/main.rs
@@ -1,3 +1,6 @@
+// Prevents additional console window on Windows in release builds (e.g. when launched by GUI).
+#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
+
use std::sync::Arc;
use tracing::info;
diff --git a/crates/cratebay-gui/src-tauri/capabilities/default.json b/crates/cratebay-gui/src-tauri/capabilities/default.json
index 96d0efe..717aaa6 100644
--- a/crates/cratebay-gui/src-tauri/capabilities/default.json
+++ b/crates/cratebay-gui/src-tauri/capabilities/default.json
@@ -7,6 +7,10 @@
],
"permissions": [
"core:default",
- "dialog:default"
+ "dialog:default",
+ "core:window:allow-minimize",
+ "core:window:allow-toggle-maximize",
+ "core:window:allow-is-maximized",
+ "core:window:allow-close"
]
}
diff --git a/crates/cratebay-gui/src-tauri/tauri.conf.json b/crates/cratebay-gui/src-tauri/tauri.conf.json
index fe74252..a5c169d 100644
--- a/crates/cratebay-gui/src-tauri/tauri.conf.json
+++ b/crates/cratebay-gui/src-tauri/tauri.conf.json
@@ -10,17 +10,6 @@
"beforeBuildCommand": "npm run build"
},
"app": {
- "windows": [
- {
- "title": "CrateBay",
- "width": 960,
- "height": 640,
- "minWidth": 680,
- "minHeight": 480,
- "resizable": true,
- "fullscreen": false
- }
- ],
"trayIcon": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true,
diff --git a/crates/cratebay-gui/src-tauri/tauri.windows.conf.json b/crates/cratebay-gui/src-tauri/tauri.windows.conf.json
new file mode 100644
index 0000000..30f0e46
--- /dev/null
+++ b/crates/cratebay-gui/src-tauri/tauri.windows.conf.json
@@ -0,0 +1,16 @@
+{
+ "app": {
+ "windows": [
+ {
+ "title": "CrateBay",
+ "width": 1400,
+ "height": 800,
+ "minWidth": 1100,
+ "minHeight": 650,
+ "resizable": true,
+ "fullscreen": false,
+ "decorations": false
+ }
+ ]
+ }
+}
diff --git a/crates/cratebay-gui/src/App.css b/crates/cratebay-gui/src/App.css
index ef22fd5..4b01e3c 100644
--- a/crates/cratebay-gui/src/App.css
+++ b/crates/cratebay-gui/src/App.css
@@ -193,12 +193,21 @@
flex-shrink: 0;
}
+/* Windows: topbar is draggable (custom titlebar) */
+.platform-windows .topbar {
+ -webkit-app-region: drag;
+}
+
.topbar-left {
display: flex;
align-items: center;
gap: 12px;
}
+.platform-windows .topbar-left {
+ -webkit-app-region: no-drag;
+}
+
.topbar-left h1 {
font-size: 16px;
font-weight: 700;
@@ -221,6 +230,10 @@
gap: 6px;
}
+.platform-windows .topbar-right {
+ -webkit-app-region: no-drag;
+}
+
.status-pill {
display: flex;
align-items: center;
@@ -233,6 +246,48 @@
color: var(--text2);
}
+/* === Window controls === */
+.window-controls {
+ display: flex;
+ align-items: center;
+ margin-left: 8px;
+}
+
+.win-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 32px;
+ border: none;
+ background: transparent;
+ color: var(--text2);
+ cursor: pointer;
+ padding: 0;
+ border-radius: 4px;
+ transition: background 0.15s, color 0.15s;
+}
+
+.win-btn:hover {
+ background: var(--hover);
+ color: var(--text);
+}
+
+.win-btn-close:hover {
+ background: #e81123;
+ color: #fff;
+}
+
+.win-btn svg {
+ width: 12px;
+ height: 12px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 1.5;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
.status-pill .dot,
.dot {
width: 7px;
diff --git a/crates/cratebay-gui/src/App.tsx b/crates/cratebay-gui/src/App.tsx
index ab61424..35f4386 100644
--- a/crates/cratebay-gui/src/App.tsx
+++ b/crates/cratebay-gui/src/App.tsx
@@ -1,5 +1,6 @@
-import { useState, useEffect } from "react"
+import { useState, useEffect, useCallback } from "react"
import { invoke } from "@tauri-apps/api/core"
+import { getCurrentWindow } from "@tauri-apps/api/window"
import { messages } from "./i18n/messages"
import { I } from "./icons"
import { useContainers } from "./hooks/useContainers"
@@ -39,6 +40,23 @@ function App() {
const vmHook = useVms()
const volumeHook = useVolumes()
+ // Window controls (Windows only — macOS keeps native titlebar)
+ const isWindows = navigator.userAgent.includes("Windows")
+ const appWindow = getCurrentWindow()
+ const [maximized, setMaximized] = useState(false)
+
+ useEffect(() => {
+ if (!isWindows) return
+ const unlisten = appWindow.onResized(async () => {
+ setMaximized(await appWindow.isMaximized())
+ })
+ return () => { unlisten.then(f => f()) }
+ }, [appWindow, isWindows])
+
+ const handleMinimize = useCallback(() => appWindow.minimize(), [appWindow])
+ const handleMaximize = useCallback(() => appWindow.toggleMaximize(), [appWindow])
+ const handleClose = useCallback(() => appWindow.close(), [appWindow])
+
// Installed (local) Docker images count for Dashboard
const [installedImagesCount, setInstalledImagesCount] = useState(0)
useEffect(() => {
@@ -251,7 +269,7 @@ function App() {
}
return (
-
+
-
+
{pageNames[activePage]}
{activePage === "containers" && containers.running.length > 0 && (
@@ -309,6 +327,13 @@ function App() {
{containers.connected ? t("connected") : t("disconnected")}
+ {isWindows && (
+
+
+
+
+
+ )}
diff --git a/crates/cratebay-gui/src/icons.tsx b/crates/cratebay-gui/src/icons.tsx
index eeff9f7..fc81869 100644
--- a/crates/cratebay-gui/src/icons.tsx
+++ b/crates/cratebay-gui/src/icons.tsx
@@ -22,4 +22,9 @@ export const I = {
fileText:
,
hardDrive:
,
kubernetes:
,
+ // Window controls
+ winMinimize:
,
+ winMaximize:
,
+ winRestore:
,
+ winClose:
,
}
diff --git a/scripts/build-release-windows.sh b/scripts/build-release-windows.sh
new file mode 100644
index 0000000..60eeec7
--- /dev/null
+++ b/scripts/build-release-windows.sh
@@ -0,0 +1,142 @@
+#!/usr/bin/env bash
+#
+# build-release-windows.sh — Build CrateBay release artifacts for Windows
+#
+# Produces:
+# dist/cratebay.exe — CLI binary
+# dist/cratebay-daemon.exe — Daemon binary
+# dist/CrateBay_
_x64.msi — MSI installer (GUI + daemon)
+# dist/CrateBay__x64-setup.exe — NSIS installer (GUI + daemon)
+#
+# Prerequisites:
+# - Rust stable toolchain (MSVC)
+# - Node.js + npm
+# - protoc (Protocol Buffers compiler)
+#
+# Usage:
+# bash scripts/build-release-windows.sh [--skip-gui]
+#
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$REPO_ROOT"
+
+VERSION="0.1.0"
+ARCH="x86_64"
+RUST_TARGET="x86_64-pc-windows-msvc"
+
+GUI_CRATE="crates/cratebay-gui"
+TAURI_DIR="$GUI_CRATE/src-tauri"
+DIST_DIR="$REPO_ROOT/dist"
+
+SKIP_GUI=false
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --skip-gui) SKIP_GUI=true; shift ;;
+ *) echo "Unknown argument: $1"; exit 2 ;;
+ esac
+done
+
+echo "=== CrateBay Windows Release Build ==="
+echo " Version : $VERSION"
+echo " Arch : $ARCH"
+echo " Target : $RUST_TARGET"
+echo ""
+
+# ── Step 1: Build daemon & CLI ───────────────────────────────────────────────
+echo "── [1/5] Building daemon and CLI (release) ──"
+cargo build --release -p cratebay-daemon -p cratebay-cli
+
+echo " ✓ target/release/cratebay.exe"
+echo " ✓ target/release/cratebay-daemon.exe"
+
+# Verify binaries exist
+for bin in cratebay.exe cratebay-daemon.exe; do
+ if [[ ! -f "target/release/$bin" ]]; then
+ echo "ERROR: target/release/$bin not found"
+ exit 1
+ fi
+done
+
+if [[ "$SKIP_GUI" == "true" ]]; then
+ echo ""
+ echo "── [2/5] Skipping frontend dependencies (--skip-gui) ──"
+ echo "── [3/5] Skipping Tauri build (--skip-gui) ──"
+else
+ # ── Step 2: Install frontend dependencies ────────────────────────────────
+ echo ""
+ echo "── [2/5] Installing frontend dependencies ──"
+ (cd "$GUI_CRATE" && npm ci)
+
+ # ── Step 3: Build Tauri app ──────────────────────────────────────────────
+ echo ""
+ echo "── [3/5] Building Tauri app ──"
+ (cd "$GUI_CRATE" && npx tauri build)
+fi
+
+# ── Step 4: Collect CLI & daemon binaries ────────────────────────────────────
+echo ""
+echo "── [4/5] Collecting CLI & daemon binaries ──"
+mkdir -p "$DIST_DIR"
+
+cp "target/release/cratebay.exe" "$DIST_DIR/cratebay.exe"
+echo " ✓ $DIST_DIR/cratebay.exe"
+
+cp "target/release/cratebay-daemon.exe" "$DIST_DIR/cratebay-daemon.exe"
+echo " ✓ $DIST_DIR/cratebay-daemon.exe"
+
+# ── Step 5: Collect GUI installers ───────────────────────────────────────────
+echo ""
+echo "── [5/5] Collecting GUI installers ──"
+
+if [[ "$SKIP_GUI" == "true" ]]; then
+ echo " Skipped (--skip-gui)"
+else
+ FOUND_INSTALLER=false
+
+ # Collect MSI installer
+ for msi in target/release/bundle/msi/*.msi; do
+ if [[ -f "$msi" ]]; then
+ BASENAME="$(basename "$msi")"
+ cp "$msi" "$DIST_DIR/$BASENAME"
+ echo " ✓ $DIST_DIR/$BASENAME"
+ FOUND_INSTALLER=true
+ fi
+ done
+
+ # Collect NSIS installer
+ for nsis in target/release/bundle/nsis/*.exe; do
+ if [[ -f "$nsis" ]]; then
+ BASENAME="$(basename "$nsis")"
+ cp "$nsis" "$DIST_DIR/$BASENAME"
+ echo " ✓ $DIST_DIR/$BASENAME"
+ FOUND_INSTALLER=true
+ fi
+ done
+
+ if [[ "$FOUND_INSTALLER" == "false" ]]; then
+ echo " WARNING: No MSI or NSIS installers found under target/release/bundle/"
+ fi
+fi
+
+# ── Summary ──────────────────────────────────────────────────────────────────
+echo ""
+echo "=== Build Complete ==="
+echo ""
+echo "Artifacts in $DIST_DIR:"
+
+# List artifacts with sizes
+for f in "$DIST_DIR"/*; do
+ if [[ -f "$f" ]]; then
+ SIZE=$(du -h "$f" | awk '{print $1}')
+ printf " %-50s %s\n" "$(basename "$f")" "$SIZE"
+ fi
+done
+
+echo ""
+echo "Next steps:"
+echo " 1. Test CLI: ./dist/cratebay.exe status"
+echo " 2. Test daemon: ./dist/cratebay-daemon.exe"
+if [[ "$SKIP_GUI" == "false" ]]; then
+ echo " 3. Install GUI: double-click the MSI or NSIS installer"
+fi
From 93f2be7a4c670ff6a1058ac5bfefd8add54e7931 Mon Sep 17 00:00:00 2001
From: su-fen <>
Date: Tue, 3 Mar 2026 20:13:59 +0800
Subject: [PATCH 2/6] fix(windows): prevent duplicate tray icon on Windows
- Remove trayIcon from base tauri.conf.json to avoid creating two tray icons when the app also creates one programmatically
- Update website feature descriptions to mention system tray quick actions
---
crates/cratebay-gui/src-tauri/tauri.conf.json | 5 -----
website/script.js | 4 ++--
2 files changed, 2 insertions(+), 7 deletions(-)
diff --git a/crates/cratebay-gui/src-tauri/tauri.conf.json b/crates/cratebay-gui/src-tauri/tauri.conf.json
index a5c169d..3a4473a 100644
--- a/crates/cratebay-gui/src-tauri/tauri.conf.json
+++ b/crates/cratebay-gui/src-tauri/tauri.conf.json
@@ -10,11 +10,6 @@
"beforeBuildCommand": "npm run build"
},
"app": {
- "trayIcon": {
- "iconPath": "icons/icon.png",
- "iconAsTemplate": true,
- "tooltip": "CrateBay"
- },
"security": {
"csp": "default-src 'self'; img-src 'self' https: data:; script-src 'self'; style-src 'self' 'unsafe-inline'"
}
diff --git a/website/script.js b/website/script.js
index 209f74f..c4f5b5e 100644
--- a/website/script.js
+++ b/website/script.js
@@ -36,7 +36,7 @@
featLabel: 'Features',
featTitle: 'Everything you need to manage containers and beyond.',
featDesc:
- 'CrateBay brings a unified, native-speed experience for Docker containers, Linux VMs, and Kubernetes clusters \u2014 without the overhead.',
+ 'CrateBay brings a unified, native-speed experience for Docker containers, Linux VMs, and Kubernetes clusters \u2014 without the overhead. Stay productive with system tray quick actions and live running counts.',
feat1Title: 'Docker Container Management',
feat1Desc:
'Full lifecycle control \u2014 create, start, stop, restart, remove. Real-time log streaming and exec into running containers.',
@@ -171,7 +171,7 @@
statSize: '应用大小',
featLabel: '核心功能',
featTitle: '容器管理,一应俱全。',
- featDesc: 'CrateBay 为 Docker 容器、Linux 虚拟机和 Kubernetes 集群提供统一、原生速度的管理体验——零额外开销。',
+ featDesc: 'CrateBay 为 Docker 容器、Linux 虚拟机和 Kubernetes 集群提供统一、原生速度的管理体验——零额外开销。系统托盘提供快捷操作,并展示实时运行数量。',
feat1Title: 'Docker 容器管理',
feat1Desc: '全生命周期控制——创建、启动、停止、重启、删除。实时日志流和进入运行中容器的终端。',
feat2Title: 'Linux 虚拟机',
From a3f35aa56ed175f925b59b453f8d8459068e7eb0 Mon Sep 17 00:00:00 2001
From: su-fen <>
Date: Tue, 3 Mar 2026 20:31:22 +0800
Subject: [PATCH 3/6] fix(gui): stabilize port mapping display order in
Dashboard and Containers
- Sort published port pairs by (public, private) before formatting to prevent order flickering on each refresh
- Add unit tests for format_published_ports
- Update website feature descriptions to reflect stable port mapping display
---
crates/cratebay-gui/src-tauri/src/lib.rs | 38 +++++++++++++++++++-----
website/script.js | 4 +--
2 files changed, 32 insertions(+), 10 deletions(-)
diff --git a/crates/cratebay-gui/src-tauri/src/lib.rs b/crates/cratebay-gui/src-tauri/src/lib.rs
index 06158ef..73ea16e 100644
--- a/crates/cratebay-gui/src-tauri/src/lib.rs
+++ b/crates/cratebay-gui/src-tauri/src/lib.rs
@@ -298,6 +298,31 @@ pub struct ContainerInfo {
ports: String,
}
+fn format_published_ports(mut pairs: Vec<(u16, u16)>) -> String {
+ pairs.sort_unstable();
+ pairs
+ .into_iter()
+ .map(|(public, private)| format!("{}:{}", public, private))
+ .collect::>()
+ .join(", ")
+}
+
+#[cfg(test)]
+mod tests {
+ use super::format_published_ports;
+
+ #[test]
+ fn format_published_ports_sorts_by_public_then_private() {
+ let out = format_published_ports(vec![(443, 443), (80, 8080), (80, 80), (8080, 80)]);
+ assert_eq!(out, "80:80, 80:8080, 443:443, 8080:80");
+ }
+
+ #[test]
+ fn format_published_ports_empty_is_empty() {
+ assert_eq!(format_published_ports(vec![]), "");
+ }
+}
+
#[derive(Serialize)]
pub struct VolumeInfo {
name: String,
@@ -362,16 +387,13 @@ async fn list_containers() -> Result, String> {
Ok(containers
.into_iter()
.map(|c| {
- let ports = c
+ let published = c
.ports
.unwrap_or_default()
- .iter()
- .filter_map(|p| {
- p.public_port
- .map(|pub_p| format!("{}:{}", pub_p, p.private_port))
- })
- .collect::>()
- .join(", ");
+ .into_iter()
+ .filter_map(|p| p.public_port.map(|public| (public, p.private_port)))
+ .collect::>();
+ let ports = format_published_ports(published);
let full_id = c.id.unwrap_or_default();
let id = full_id.chars().take(12).collect::();
diff --git a/website/script.js b/website/script.js
index c4f5b5e..03d898d 100644
--- a/website/script.js
+++ b/website/script.js
@@ -39,7 +39,7 @@
'CrateBay brings a unified, native-speed experience for Docker containers, Linux VMs, and Kubernetes clusters \u2014 without the overhead. Stay productive with system tray quick actions and live running counts.',
feat1Title: 'Docker Container Management',
feat1Desc:
- 'Full lifecycle control \u2014 create, start, stop, restart, remove. Real-time log streaming and exec into running containers.',
+ 'Full lifecycle control \u2014 create, start, stop, restart, remove. Real-time log streaming and exec into running containers. Port mappings remain consistently ordered during live refresh.',
feat2Title: 'Linux Virtual Machines',
feat2Desc:
'Native VM support via Virtualization.framework (macOS), KVM (Linux), and Hyper-V (Windows). Boot in seconds.',
@@ -173,7 +173,7 @@
featTitle: '容器管理,一应俱全。',
featDesc: 'CrateBay 为 Docker 容器、Linux 虚拟机和 Kubernetes 集群提供统一、原生速度的管理体验——零额外开销。系统托盘提供快捷操作,并展示实时运行数量。',
feat1Title: 'Docker 容器管理',
- feat1Desc: '全生命周期控制——创建、启动、停止、重启、删除。实时日志流和进入运行中容器的终端。',
+ feat1Desc: '全生命周期控制——创建、启动、停止、重启、删除。实时日志流和进入运行中容器的终端。端口映射信息在实时刷新时保持稳定显示。',
feat2Title: 'Linux 虚拟机',
feat2Desc: '通过 Virtualization.framework (macOS)、KVM (Linux)、Hyper-V (Windows) 原生支持虚拟机,秒级启动。',
feat3Title: '镜像与卷管理',
From 79af452aecb9d1672f25e360762d98313e904f46 Mon Sep 17 00:00:00 2001
From: su-fen
Date: Tue, 3 Mar 2026 23:14:58 +0800
Subject: [PATCH 4/6] feat(gui): optimize UI layout, typography, and animations
- Replace Inter/Fira Code/JetBrains Mono with Geist Sans + Geist Mono
variable fonts, bundled locally for offline desktop app support
- Remove Google Fonts remote dependency
- Unify font-family across all CSS and inline styles
- Enhance page layouts, component styles, and transition animations
- Add shadcn/ui component library (new-york style) with Tailwind CSS
- Refine Kubernetes, VMs, Volumes, Images, Containers, and Dashboard pages
---
crates/cratebay-gui/components.json | 23 +
crates/cratebay-gui/src/App.css | 5068 ++++++++++++-----
crates/cratebay-gui/src/App.tsx | 4 +-
.../src/assets/fonts/GeistMono-Variable.woff2 | Bin 0 -> 31368 bytes
.../src/assets/fonts/GeistSans-Variable.woff2 | Bin 0 -> 28400 bytes
.../src/components/ui/alert-dialog.tsx | 196 +
.../cratebay-gui/src/components/ui/alert.tsx | 66 +
.../cratebay-gui/src/components/ui/badge.tsx | 48 +
.../cratebay-gui/src/components/ui/button.tsx | 64 +
.../cratebay-gui/src/components/ui/card.tsx | 92 +
.../src/components/ui/checkbox.tsx | 30 +
.../src/components/ui/collapsible.tsx | 33 +
.../cratebay-gui/src/components/ui/dialog.tsx | 156 +
.../cratebay-gui/src/components/ui/input.tsx | 21 +
.../src/components/ui/progress.tsx | 36 +
.../src/components/ui/scroll-area.tsx | 58 +
.../cratebay-gui/src/components/ui/select.tsx | 190 +
.../src/components/ui/separator.tsx | 26 +
.../cratebay-gui/src/components/ui/sonner.tsx | 35 +
.../cratebay-gui/src/components/ui/table.tsx | 114 +
.../cratebay-gui/src/components/ui/tabs.tsx | 91 +
.../src/components/ui/tooltip.tsx | 55 +
crates/cratebay-gui/src/index.css | 18 +-
crates/cratebay-gui/src/lib/utils.ts | 6 +
crates/cratebay-gui/src/pages/Containers.tsx | 109 +-
crates/cratebay-gui/src/pages/Dashboard.tsx | 165 +-
crates/cratebay-gui/src/pages/Images.tsx | 126 +-
crates/cratebay-gui/src/pages/Kubernetes.tsx | 596 +-
crates/cratebay-gui/src/pages/Vms.tsx | 675 ++-
crates/cratebay-gui/src/pages/Volumes.tsx | 244 +-
.../src/pages/__tests__/Dashboard.test.tsx | 6 +-
31 files changed, 6380 insertions(+), 1971 deletions(-)
create mode 100644 crates/cratebay-gui/components.json
create mode 100644 crates/cratebay-gui/src/assets/fonts/GeistMono-Variable.woff2
create mode 100644 crates/cratebay-gui/src/assets/fonts/GeistSans-Variable.woff2
create mode 100644 crates/cratebay-gui/src/components/ui/alert-dialog.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/alert.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/badge.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/button.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/card.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/checkbox.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/collapsible.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/dialog.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/input.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/progress.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/scroll-area.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/select.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/separator.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/sonner.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/table.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/tabs.tsx
create mode 100644 crates/cratebay-gui/src/components/ui/tooltip.tsx
create mode 100644 crates/cratebay-gui/src/lib/utils.ts
diff --git a/crates/cratebay-gui/components.json b/crates/cratebay-gui/components.json
new file mode 100644
index 0000000..c3085d6
--- /dev/null
+++ b/crates/cratebay-gui/components.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "rtl": false,
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "registries": {}
+}
diff --git a/crates/cratebay-gui/src/App.css b/crates/cratebay-gui/src/App.css
index 4b01e3c..d849136 100644
--- a/crates/cratebay-gui/src/App.css
+++ b/crates/cratebay-gui/src/App.css
@@ -4,14 +4,13 @@
Style: Clean, modern, own identity
======================================== */
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
/* === Dark theme (default) === */
.app {
display: flex;
height: 100vh;
overflow: hidden;
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+ font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 13px;
--bg: #0f111a;
--surface: #1a1d2e;
@@ -306,53 +305,75 @@
padding: 20px 24px;
}
+/* Page transition */
+@keyframes pageEnter {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+.page-transition {
+ animation: pageEnter 0.2s cubic-bezier(0.22, 1, 0.36, 1);
+ will-change: opacity, transform;
+}
+
/* === Dashboard === */
.dashboard {}
+/* --- Navigation overview cards --- */
.dash-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
- gap: 16px;
- margin-bottom: 24px;
+ gap: 14px;
+ margin-bottom: 16px;
}
@media (max-width: 1200px) {
- .dash-cards {
- grid-template-columns: repeat(2, 1fr);
- }
+ .dash-cards { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 768px) {
- .dash-cards {
- grid-template-columns: 1fr;
- }
+ .dash-cards { grid-template-columns: 1fr; }
}
.dash-card {
background: var(--surface);
border: 1px solid var(--border);
- border-radius: 12px;
+ border-radius: 14px;
padding: 16px;
display: flex;
flex-direction: column;
- gap: 10px;
+ justify-content: space-between;
+ gap: 16px;
cursor: pointer;
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.dash-card:hover {
border-color: var(--purple);
- box-shadow: 0 0 0 1px var(--purple-dim);
+ box-shadow: 0 4px 16px rgba(139, 92, 246, 0.08), 0 0 0 1px var(--purple-dim);
+ transform: translateY(-1px);
+}
+
+.dash-card-top {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.dash-card-bottom {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
}
.dash-card-icon {
- width: 36px;
- height: 36px;
+ width: 38px;
+ height: 38px;
border-radius: 10px;
background: var(--purple-dim);
display: flex;
align-items: center;
justify-content: center;
+ flex-shrink: 0;
}
.dash-card-icon svg {
@@ -365,30 +386,25 @@
stroke-linejoin: round;
}
-.dash-card-header {
- display: flex;
- flex-direction: column;
- gap: 8px;
+.dash-card-icon.icon-cyan {
+ background: var(--cyan-dim);
}
-
-.dash-card-title {
- font-size: 11px;
- font-weight: 600;
- color: var(--text2);
- text-transform: uppercase;
- letter-spacing: 0.5px;
+.dash-card-icon.icon-cyan svg {
+ stroke: var(--cyan);
}
-.dash-card-footer {
- margin-top: auto;
- padding-top: 12px;
- border-top: 1px solid var(--border);
+.dash-card-icon.icon-green {
+ background: rgba(52, 211, 153, 0.1);
+}
+.dash-card-icon.icon-green svg {
+ stroke: var(--green);
}
-.dash-card-info {
- display: flex;
- align-items: baseline;
- gap: 8px;
+.dash-card-icon.icon-neutral {
+ background: var(--surface2);
+}
+.dash-card-icon.icon-neutral svg {
+ stroke: var(--text2);
}
.dash-card-value {
@@ -399,7 +415,7 @@
}
.dash-card-label {
- font-size: 12px;
+ font-size: 13px;
font-weight: 500;
color: var(--text2);
}
@@ -410,1905 +426,4389 @@
gap: 6px;
font-size: 11px;
color: var(--text2);
- min-height: 18px;
+ min-height: 20px;
}
.dash-card-sub .dot {
- width: 6px;
- height: 6px;
+ width: 7px;
+ height: 7px;
}
.dash-running {
+ display: flex;
+ align-items: center;
+ gap: 5px;
color: var(--green);
- font-weight: 500;
+ font-weight: 600;
+ font-size: 11px;
}
-.dash-badge {
- font-size: 10px;
- font-weight: 600;
- padding: 1px 6px;
- border-radius: 4px;
- background: var(--surface2);
+.dash-idle {
color: var(--text3);
+ font-weight: 500;
}
-.view-all {
- text-align: center;
- padding: 10px;
- font-size: 12px;
+.dash-status {
+ display: flex;
+ align-items: center;
+ gap: 5px;
font-weight: 500;
- color: var(--purple);
- cursor: pointer;
- border-radius: 8px;
- transition: background 0.15s;
}
+.dash-status.online { color: var(--green); }
+.dash-status.offline { color: var(--text3); }
-.view-all:hover {
+.dash-badge {
+ font-size: 10px;
+ font-weight: 600;
+ padding: 2px 7px;
+ border-radius: 6px;
background: var(--purple-dim);
+ color: var(--purple);
}
-/* Section label */
-.section-title {
- font-size: 11px;
- font-weight: 600;
- color: var(--text3);
- text-transform: uppercase;
- letter-spacing: 0.8px;
- margin-bottom: 10px;
- padding-left: 2px;
+/* --- Resource monitoring strip --- */
+.dash-resources {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 14px;
+ margin-bottom: 24px;
}
-.section-title:not(:first-child) { margin-top: 24px; }
+@media (max-width: 768px) {
+ .dash-resources { grid-template-columns: 1fr; }
+}
-/* === Container cards === */
-.container-card {
+.dash-res-card {
display: flex;
align-items: center;
gap: 14px;
- padding: 12px 14px;
background: var(--surface);
border: 1px solid var(--border);
- border-radius: 10px;
- margin-bottom: 8px;
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
- cursor: default;
-}
-
-.container-card:hover {
- border-color: var(--purple);
- box-shadow: 0 0 0 1px var(--purple-dim);
+ border-radius: 12px;
+ padding: 14px 16px;
}
-.container-card .card-icon {
- width: 38px;
- height: 38px;
+.dash-res-icon {
+ width: 36px;
+ height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
- background: linear-gradient(135deg, var(--purple), var(--purple-hover));
}
-
-.container-card .card-icon svg {
+.dash-res-icon svg {
width: 18px;
height: 18px;
- stroke: white;
fill: none;
stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
}
-
-.container-card .card-icon.stopped {
- background: var(--surface2);
+.dash-res-icon.purple {
+ background: var(--purple-dim);
}
-
-.container-card .card-icon.stopped svg {
- stroke: var(--text3);
+.dash-res-icon.purple svg {
+ stroke: var(--purple);
+}
+.dash-res-icon.cyan {
+ background: var(--cyan-dim);
+}
+.dash-res-icon.cyan svg {
+ stroke: var(--cyan);
}
-.card-body {
+.dash-res-body {
flex: 1;
min-width: 0;
-}
-
-.card-stats {
display: flex;
flex-direction: column;
- gap: 6px;
- margin-right: 12px;
- flex-shrink: 0;
- justify-content: center;
+ gap: 8px;
}
-.card-stats .stat-item {
+.dash-res-header {
display: flex;
- align-items: center;
- gap: 4px;
+ align-items: baseline;
+ justify-content: space-between;
+}
+
+.dash-res-title {
font-size: 11px;
+ font-weight: 600;
color: var(--text2);
+ text-transform: uppercase;
+ letter-spacing: 0.4px;
}
-.card-stats .stat-icon {
- width: 14px;
- height: 14px;
- display: flex;
- align-items: center;
- justify-content: center;
- opacity: 0.6;
- flex-shrink: 0;
+.dash-res-value {
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--text);
}
-.card-stats .stat-icon svg {
- width: 12px;
- height: 12px;
- stroke: currentColor;
- fill: none;
- stroke-width: 2;
+.dash-res-bar {
+ width: 100%;
+ height: 5px;
+ border-radius: 3px;
+ background: var(--surface2);
+ overflow: hidden;
}
-.card-stats .stat-value {
- font-weight: 600;
- color: var(--text);
- font-family: 'SF Mono', 'Consolas', monospace;
- font-size: 11px;
+.dash-res-bar-fill {
+ height: 100%;
+ border-radius: 3px;
+ transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
+}
+.dash-res-bar-fill.purple {
+ background: linear-gradient(90deg, var(--purple), var(--purple-hover));
+}
+.dash-res-bar-fill.cyan {
+ background: linear-gradient(90deg, var(--cyan), #06b6d4);
}
-.dash-card-footer {
- margin-top: auto;
- padding-top: 12px;
- border-top: 1px solid var(--border);
+.view-all {
+ text-align: center;
+ padding: 10px;
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--purple);
+ cursor: pointer;
+ border-radius: 8px;
+ transition: background 0.15s;
}
-.dash-card-footer .dash-card-label {
- white-space: nowrap;
+.view-all:hover {
+ background: var(--purple-dim);
}
-.card-name {
- font-size: 14px;
+/* Section label */
+.section-title {
+ font-size: 11px;
font-weight: 600;
- color: var(--text);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+ color: var(--text3);
+ text-transform: uppercase;
+ letter-spacing: 0.8px;
+ margin-bottom: 10px;
+ padding-left: 2px;
}
-.card-meta {
- font-size: 11px;
- color: var(--text2);
- margin-top: 2px;
+.section-title:not(:first-child) { margin-top: 24px; }
+
+/* === Running Section (Dashboard) === */
+.dash-running-section {
+ margin-top: 20px;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 14px;
overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
}
-.card-status {
+.dash-section-header {
display: flex;
align-items: center;
- gap: 5px;
- flex-shrink: 0;
- margin-right: 8px;
-}
-
-.card-status .dot {
- width: 8px;
- height: 8px;
-}
-
-.card-status .dot.running { box-shadow: 0 0 6px var(--green); }
-
-.card-status span {
- font-size: 11px;
- font-weight: 500;
- color: var(--text2);
+ justify-content: space-between;
+ padding: 14px 18px 12px;
+ border-bottom: 1px solid var(--border);
}
-.card-actions {
+.dash-section-left {
display: flex;
- gap: 4px;
- flex-shrink: 0;
+ align-items: center;
+ gap: 10px;
}
-.card-actions .action-btn {
+.dash-section-icon {
width: 28px;
height: 28px;
- border-radius: 6px;
- border: 1px solid var(--border);
- background: transparent;
- color: var(--text3);
- cursor: pointer;
+ border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
- transition: all 0.15s ease;
+ background: rgba(52, 211, 153, 0.12);
}
-.card-actions .action-btn:hover {
- border-color: var(--purple);
- color: var(--purple);
- background: var(--purple-dim);
+.dash-section-icon svg {
+ width: 14px;
+ height: 14px;
+ stroke: var(--green);
+ fill: none;
+ stroke-width: 2;
}
-.card-actions .action-btn.danger:hover {
- border-color: var(--red);
- color: var(--red);
- background: var(--red-dim);
+.dash-section-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text);
+ letter-spacing: 0.2px;
}
-.card-actions .action-btn:disabled {
- opacity: 0.3;
- cursor: not-allowed;
+.dash-section-count {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--green);
+ background: rgba(52, 211, 153, 0.12);
+ padding: 2px 8px;
+ border-radius: 10px;
+ line-height: 1.4;
}
-.card-actions .action-btn svg {
- width: 14px; height: 14px;
- stroke: currentColor; fill: none;
+.dash-section-action {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--purple);
+ cursor: pointer;
+ padding: 4px 10px;
+ border-radius: 6px;
+ transition: background 0.15s, color 0.15s;
+}
+
+.dash-section-action:hover {
+ background: var(--purple-dim);
+}
+
+.dash-section-action svg {
+ width: 14px;
+ height: 14px;
+ stroke: currentColor;
+ fill: none;
stroke-width: 2;
}
-/* === Container groups === */
-.container-group-header {
- cursor: pointer;
- user-select: none;
+/* Running list items */
+.dash-running-list {
+ padding: 4px 0;
}
-.container-group-header .card-icon {
- background: var(--surface2);
+.dash-running-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 18px;
+ transition: background 0.15s;
+ cursor: default;
+ position: relative;
}
-.container-group-header .card-icon svg {
- stroke: var(--purple);
+.dash-running-item:hover {
+ background: var(--surface2);
}
-.container-group-header.expanded {
- border-color: var(--purple);
- box-shadow: 0 0 0 1px var(--purple-dim);
+.dash-running-item:not(:last-child)::after {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ left: 60px;
+ right: 18px;
+ height: 1px;
+ background: var(--border);
+ opacity: 0.5;
}
-.group-chevron {
- width: 30px;
- height: 30px;
- border-radius: 8px;
+.dash-running-index {
+ width: 22px;
+ height: 22px;
+ border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
+ font-size: 11px;
+ font-weight: 700;
color: var(--text3);
+ background: var(--surface2);
flex-shrink: 0;
+ font-family: 'Geist Mono', ui-monospace, monospace;
}
-.container-group-header:hover .group-chevron {
- color: var(--purple);
- background: var(--purple-dim);
+.dash-running-icon {
+ width: 34px;
+ height: 34px;
+ border-radius: 9px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ background: linear-gradient(135deg, var(--purple), var(--purple-hover));
+ box-shadow: 0 2px 8px rgba(139, 92, 246, 0.2);
}
-.group-chevron svg {
+.dash-running-icon svg {
width: 16px;
height: 16px;
- stroke: currentColor;
+ stroke: white;
fill: none;
stroke-width: 2;
- stroke-linecap: round;
- stroke-linejoin: round;
-}
-
-.container-group-children {
- margin-left: 18px;
- padding-left: 12px;
- border-left: 1px dashed var(--border);
}
-.container-group-children .container-card {
- margin-bottom: 6px;
-}
-
-.container-card.container-child {
- background: var(--surface);
+.dash-running-body {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
}
-/* === Empty / Getting Started === */
-.empty-state {
- text-align: center;
- padding: 60px 20px;
+.dash-running-name {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ line-height: 1.3;
}
-.empty-state .empty-icon {
- width: 48px;
- height: 48px;
- margin: 0 auto 16px;
- border-radius: 14px;
- background: var(--purple-dim);
+.dash-running-meta {
display: flex;
align-items: center;
- justify-content: center;
+ gap: 0;
+ font-size: 11px;
+ color: var(--text2);
+ overflow: hidden;
+ white-space: nowrap;
}
-.empty-state .empty-icon svg {
- width: 24px; height: 24px;
- stroke: var(--purple); fill: none;
- stroke-width: 2;
+.dash-running-image {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 200px;
}
-.empty-state h3 {
- font-size: 15px;
- font-weight: 600;
- margin: 0 0 6px;
- color: var(--text);
+.dash-running-ports {
+ flex-shrink: 0;
+ color: var(--cyan);
+ font-weight: 500;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 10px;
}
-.empty-state p {
- font-size: 13px;
- color: var(--text2);
- margin: 0;
+.dash-running-ports::before {
+ content: "·";
+ color: var(--text3);
+ margin: 0 6px;
+ font-family: inherit;
}
-.empty-state code {
- display: inline-block;
- margin-top: 12px;
- padding: 8px 16px;
- background: var(--surface2);
- border: 1px solid var(--border);
+.dash-running-pill {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-shrink: 0;
+ padding: 4px 10px;
border-radius: 8px;
- font-family: 'Fira Code', 'SF Mono', monospace;
- font-size: 12px;
- color: var(--cyan);
+ background: rgba(52, 211, 153, 0.08);
+ border: 1px solid rgba(52, 211, 153, 0.12);
}
-/* === Settings === */
-.settings {
- display: flex;
- flex-direction: column;
- gap: 8px;
- width: 100%;
- max-width: 640px;
- margin: 0 auto;
+.dash-running-pill .dot {
+ width: 7px;
+ height: 7px;
+ box-shadow: 0 0 6px var(--green);
}
-.settings-section-title {
+.dash-running-pill span:last-child {
font-size: 11px;
- font-weight: 700;
- color: var(--text3);
- text-transform: uppercase;
- letter-spacing: 0.8px;
- margin-bottom: 4px;
- padding-left: 2px;
+ font-weight: 500;
+ color: var(--green);
}
-.settings-section-title:not(:first-child) {
- margin-top: 16px;
+/* Responsive: Running section */
+@media (max-width: 768px) {
+ .dash-running-item {
+ padding: 10px 14px;
+ gap: 10px;
+ }
+ .dash-running-image {
+ max-width: 120px;
+ }
+ .dash-running-pill span:last-child {
+ display: none;
+ }
+ .dash-running-index {
+ display: none;
+ }
}
-.setting-row {
+/* === Container cards === */
+.container-card {
display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 16px 20px;
+ flex-direction: column;
+ gap: 0;
+ padding: 0;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
+ margin-bottom: 8px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
- gap: 16px;
+ cursor: default;
+ overflow: hidden;
}
-.setting-row:hover {
- border-color: color-mix(in srgb, var(--purple) 40%, transparent);
- box-shadow: 0 0 0 1px var(--purple-dim);
+.container-card:hover {
+ border-color: var(--purple);
+ box-shadow: 0 2px 12px rgba(139, 92, 246, 0.06), 0 0 0 1px var(--purple-dim);
}
-.setting-info {
- flex: 1;
- min-width: 0;
+.container-card.stopped {
+ opacity: 0.75;
+}
+.container-card.stopped:hover {
+ opacity: 1;
}
-.setting-icon {
- width: 36px;
- height: 36px;
+/* -- Card main row -- */
+.card-main {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 14px 16px;
+}
+
+.container-card .card-icon {
+ width: 38px;
+ height: 38px;
border-radius: 10px;
- background: var(--purple-dim);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
+ background: linear-gradient(135deg, var(--purple), var(--purple-hover));
}
-.setting-icon svg {
+.container-card .card-icon svg {
width: 18px;
height: 18px;
- stroke: var(--purple);
+ stroke: white;
fill: none;
stroke-width: 2;
- stroke-linecap: round;
- stroke-linejoin: round;
}
-.setting-label {
- font-size: 13px;
+.container-card .card-icon.stopped {
+ background: var(--surface2);
+}
+
+.container-card .card-icon.stopped svg {
+ stroke: var(--text3);
+}
+
+.card-body {
+ flex: 1;
+ min-width: 0;
+}
+
+.card-name {
+ font-size: 14px;
font-weight: 600;
color: var(--text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.setting-desc {
+.card-meta {
font-size: 11px;
color: var(--text2);
margin-top: 2px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.setting-row select {
- background: var(--surface2);
- color: var(--text);
- border: 1px solid var(--border);
- padding: 8px 14px;
- border-radius: 10px;
- font-size: 12px;
- font-family: inherit;
- font-weight: 500;
- cursor: pointer;
- outline: none;
- transition: border-color 0.2s, box-shadow 0.2s;
- min-width: 120px;
+.card-stats {
+ display: flex;
+ gap: 14px;
flex-shrink: 0;
+ align-items: center;
}
-.setting-row select:focus {
- border-color: var(--purple);
- box-shadow: 0 0 0 3px var(--purple-dim);
+.card-stats .stat-item {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 11px;
+ color: var(--text2);
+ background: var(--surface2);
+ padding: 3px 8px;
+ border-radius: 6px;
}
-/* === States === */
-.loading {
+.card-stats .stat-icon {
+ width: 14px;
+ height: 14px;
display: flex;
align-items: center;
justify-content: center;
- padding: 60px;
+ opacity: 0.6;
+ flex-shrink: 0;
+}
+
+.card-stats .stat-icon svg {
+ width: 12px;
+ height: 12px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+}
+
+.card-stats .stat-value {
+ font-weight: 600;
+ color: var(--text);
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 11px;
+}
+
+.card-status {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-shrink: 0;
+}
+
+.card-status .dot {
+ width: 8px;
+ height: 8px;
+}
+
+.card-status .dot.running { box-shadow: 0 0 6px var(--green); }
+
+.card-status span {
+ font-size: 11px;
+ font-weight: 500;
color: var(--text2);
- font-size: 13px;
- gap: 10px;
}
-.spinner {
- width: 18px;
+/* -- Card actions row -- */
+.card-actions {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 16px;
+ border-top: 1px solid var(--border);
+ background: rgba(0, 0, 0, 0.02);
+}
+
+.app.light .card-actions {
+ background: rgba(0, 0, 0, 0.015);
+}
+
+.card-actions-group {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+}
+
+.card-actions-sep {
+ width: 1px;
height: 18px;
- border: 2px solid var(--border);
- border-top-color: var(--purple);
- border-radius: 50%;
- animation: spin 0.6s linear infinite;
+ background: var(--border);
+ margin: 0 4px;
+ flex-shrink: 0;
}
-@keyframes spin { to { transform: rotate(360deg); } }
+.card-actions .action-btn {
+ height: 26px;
+ padding: 0 8px;
+ border-radius: 6px;
+ border: 1px solid transparent;
+ background: transparent;
+ color: var(--text3);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ transition: all 0.15s ease;
+ font-size: 11px;
+ font-weight: 500;
+ font-family: inherit;
+}
-.error-msg {
+.card-actions .action-btn .action-label {
+ display: none;
+}
+
+@media (min-width: 900px) {
+ .card-actions .action-btn .action-label {
+ display: inline;
+ }
+}
+
+.card-actions .action-btn:hover {
+ border-color: var(--purple);
+ color: var(--purple);
+ background: var(--purple-dim);
+}
+
+.card-actions .action-btn.warn:hover {
+ border-color: #f59e0b;
+ color: #f59e0b;
+ background: rgba(245, 158, 11, 0.08);
+}
+
+.card-actions .action-btn.success:hover {
+ border-color: var(--green);
+ color: var(--green);
+ background: rgba(52, 211, 153, 0.08);
+}
+
+.card-actions .action-btn.danger:hover {
+ border-color: var(--red);
+ color: var(--red);
+ background: var(--red-dim);
+}
+
+.card-actions .action-btn:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+}
+
+.card-actions .action-btn svg {
+ width: 14px; height: 14px;
+ stroke: currentColor; fill: none;
+ stroke-width: 2;
+}
+
+/* === Container groups === */
+.container-group-header {
+ cursor: pointer;
+ user-select: none;
+}
+
+.container-group-header .card-main {
+ padding: 14px 16px;
+}
+
+.container-group-header .card-icon {
+ background: var(--surface2);
+}
+
+.container-group-header .card-icon svg {
+ stroke: var(--purple);
+}
+
+.container-group-header.expanded {
+ border-color: var(--purple);
+ box-shadow: 0 0 0 1px var(--purple-dim);
+}
+
+/* Group header has no actions row, hide the border */
+.container-group-header .card-actions {
+ display: none;
+}
+
+.group-chevron {
+ width: 30px;
+ height: 30px;
+ border-radius: 8px;
display: flex;
- flex-direction: column;
align-items: center;
justify-content: center;
- padding: 48px 24px;
- gap: 12px;
+ color: var(--text3);
+ flex-shrink: 0;
}
-.error-msg-icon {
+.container-group-header:hover .group-chevron {
+ color: var(--purple);
+ background: var(--purple-dim);
+}
+
+.group-chevron svg {
+ width: 16px;
+ height: 16px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.container-group-children {
+ margin-left: 18px;
+ padding-left: 12px;
+ border-left: 2px solid var(--border);
+}
+
+.container-group-children .container-card {
+ margin-bottom: 6px;
+}
+
+.container-card.container-child {
+ background: var(--surface);
+}
+
+/* === Empty / Getting Started === */
+.empty-state {
+ text-align: center;
+ padding: 60px 20px;
+}
+
+.empty-state .empty-icon {
width: 48px;
height: 48px;
+ margin: 0 auto 16px;
border-radius: 14px;
- background: rgba(248, 113, 113, 0.1);
+ background: var(--purple-dim);
display: flex;
align-items: center;
justify-content: center;
}
-.error-msg-icon svg {
- width: 24px;
- height: 24px;
- stroke: var(--red);
- fill: none;
+.empty-state .empty-icon svg {
+ width: 24px; height: 24px;
+ stroke: var(--purple); fill: none;
stroke-width: 2;
- stroke-linecap: round;
- stroke-linejoin: round;
}
-.error-msg-title {
- font-size: 15px;
+.empty-state h3 {
+ font-size: 15px;
+ font-weight: 600;
+ margin: 0 0 6px;
+ color: var(--text);
+}
+
+.empty-state p {
+ font-size: 13px;
+ color: var(--text2);
+ margin: 0;
+}
+
+.empty-state code {
+ display: inline-block;
+ margin-top: 12px;
+ padding: 8px 16px;
+ background: var(--surface2);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 12px;
+ color: var(--cyan);
+}
+
+/* === Settings === */
+.settings {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ width: 100%;
+ max-width: 640px;
+ margin: 0 auto;
+}
+
+.settings-section-title {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--text3);
+ text-transform: uppercase;
+ letter-spacing: 0.8px;
+ margin-bottom: 4px;
+ padding-left: 2px;
+}
+
+.settings-section-title:not(:first-child) {
+ margin-top: 16px;
+}
+
+.setting-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 20px;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+ gap: 16px;
+}
+
+.setting-row:hover {
+ border-color: color-mix(in srgb, var(--purple) 40%, transparent);
+ box-shadow: 0 0 0 1px var(--purple-dim);
+}
+
+.setting-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.setting-icon {
+ width: 36px;
+ height: 36px;
+ border-radius: 10px;
+ background: var(--purple-dim);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.setting-icon svg {
+ width: 18px;
+ height: 18px;
+ stroke: var(--purple);
+ fill: none;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.setting-label {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text);
+}
+
+.setting-desc {
+ font-size: 11px;
+ color: var(--text2);
+ margin-top: 2px;
+}
+
+.setting-row select {
+ background: var(--surface2);
+ color: var(--text);
+ border: 1px solid var(--border);
+ padding: 8px 14px;
+ border-radius: 10px;
+ font-size: 12px;
+ font-family: inherit;
+ font-weight: 500;
+ cursor: pointer;
+ outline: none;
+ transition: border-color 0.2s, box-shadow 0.2s;
+ min-width: 120px;
+ flex-shrink: 0;
+}
+
+.setting-row select:focus {
+ border-color: var(--purple);
+ box-shadow: 0 0 0 3px var(--purple-dim);
+}
+
+/* === States === */
+.loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 60px;
+ color: var(--text2);
+ font-size: 13px;
+ gap: 10px;
+}
+
+.spinner {
+ width: 18px;
+ height: 18px;
+ border: 2px solid var(--border);
+ border-top-color: var(--purple);
+ border-radius: 50%;
+ animation: spin 0.6s linear infinite;
+}
+
+@keyframes spin { to { transform: rotate(360deg); } }
+
+.error-msg {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 48px 24px;
+ gap: 12px;
+}
+
+.error-msg-icon {
+ width: 48px;
+ height: 48px;
+ border-radius: 14px;
+ background: rgba(248, 113, 113, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.error-msg-icon svg {
+ width: 24px;
+ height: 24px;
+ stroke: var(--red);
+ fill: none;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.error-msg-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--text);
+}
+
+.error-msg-text {
+ font-size: 12px;
+ color: var(--text2);
+ max-width: 420px;
+ text-align: center;
+ line-height: 1.5;
+ word-break: break-word;
+}
+
+.error-msg-action {
+ margin-top: 4px;
+}
+
+.error-inline {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ background: rgba(248, 113, 113, 0.06);
+ border: 1px solid rgba(248, 113, 113, 0.2);
+ color: var(--red);
+ padding: 12px 14px;
+ border-radius: 12px;
+ font-size: 12px;
+ white-space: pre-wrap;
+ line-height: 1.5;
+}
+
+.error-inline-icon {
+ width: 18px;
+ height: 18px;
+ flex-shrink: 0;
+ margin-top: 1px;
+}
+
+.error-inline-icon svg {
+ width: 18px;
+ height: 18px;
+ stroke: var(--red);
+ fill: none;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.error-inline-text {
+ flex: 1;
+ min-width: 0;
+}
+
+.error-inline-dismiss {
+ background: none;
+ border: none;
+ color: var(--red);
+ cursor: pointer;
+ padding: 0;
+ font-size: 16px;
+ line-height: 1;
+ opacity: 0.6;
+ transition: opacity 0.15s;
+ flex-shrink: 0;
+}
+
+.error-inline-dismiss:hover {
+ opacity: 1;
+}
+
+/* === Common controls === */
+.page { display: flex; flex-direction: column; gap: 14px; }
+
+.toolbar {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.input,
+.select {
+ background: var(--surface);
+ color: var(--text);
+ border: 1px solid var(--border);
+ height: 32px;
+ padding: 6px 10px;
+ border-radius: 8px;
+ outline: none;
+ font-size: 12px;
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
+ box-sizing: border-box;
+}
+
+.input { min-width: 0; }
+
+.input:focus,
+.select:focus {
+ border-color: var(--purple);
+ box-shadow: 0 0 0 3px var(--purple-dim);
+}
+
+.btn {
+ border: 1px solid var(--border);
+ background: var(--surface);
+ color: var(--text);
+ height: 32px;
+ padding: 6px 12px;
+ border-radius: 8px;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ box-sizing: border-box;
+}
+
+.btn:hover { border-color: var(--purple); background: var(--purple-dim); color: var(--purple); }
+.btn:disabled { opacity: 0.45; cursor: not-allowed; }
+.btn.primary { background: var(--purple); border-color: var(--purple); color: white; }
+.btn.primary:hover { background: var(--purple-hover); border-color: var(--purple-hover); color: white; }
+.btn.sm { height: 28px; padding: 4px 10px; border-radius: 6px; font-size: 11px; }
+.btn.xs { height: 24px; padding: 2px 8px; border-radius: 6px; font-size: 11px; }
+
+.icon svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 2; }
+
+.icon-btn {
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ border: 1px solid var(--border);
+ background: var(--surface);
+ color: var(--text2);
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.15s ease;
+}
+
+.icon-btn:hover { border-color: var(--purple); color: var(--purple); background: var(--purple-dim); }
+.icon-btn svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2; }
+
+.grid2 {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 14px;
+ align-items: start;
+}
+
+.grid3 {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 14px;
+ align-items: start;
+}
+
+.panel {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ padding: 18px;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.panel:hover {
+ border-color: color-mix(in srgb, var(--purple) 25%, transparent);
+}
+
+.panel-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--text);
+ margin-bottom: 14px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid var(--border);
+}
+
+.panel-title-icon {
+ width: 24px;
+ height: 24px;
+ border-radius: 6px;
+ background: var(--purple-dim);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.panel-title-icon svg {
+ width: 14px;
+ height: 14px;
+ stroke: var(--purple);
+ fill: none;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.panel.full-width {
+ grid-column: 1 / -1;
+}
+
+.panel-subtitle {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--text2);
+ margin-top: 16px;
+ margin-bottom: 8px;
+ padding-top: 12px;
+ border-top: 1px solid var(--border);
+}
+
+.hint {
+ color: var(--text2);
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+/* error-inline styles moved to States section above */
+
+.form { display: flex; flex-direction: column; gap: 10px; }
+.form .row { display: flex; flex-direction: column; gap: 6px; }
+.form .row label { font-size: 11px; font-weight: 700; color: var(--text2); }
+.form .row.two { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
+.form .row.inline { flex-direction: row; align-items: center; gap: 8px; color: var(--text2); font-size: 12px; }
+
+/* Custom checkbox / toggle */
+.form .row.inline input[type="checkbox"],
+.setting-row input[type="checkbox"] {
+ appearance: none;
+ -webkit-appearance: none;
+ width: 36px;
+ height: 20px;
+ border-radius: 10px;
+ background: var(--surface2);
+ border: 1px solid var(--border);
+ position: relative;
+ cursor: pointer;
+ transition: background 0.2s, border-color 0.2s;
+ flex-shrink: 0;
+}
+
+.form .row.inline input[type="checkbox"]::after,
+.setting-row input[type="checkbox"]::after {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: var(--text3);
+ transition: transform 0.2s, background 0.2s;
+}
+
+.form .row.inline input[type="checkbox"]:checked,
+.setting-row input[type="checkbox"]:checked {
+ background: var(--purple);
+ border-color: var(--purple);
+}
+
+.form .row.inline input[type="checkbox"]:checked::after,
+.setting-row input[type="checkbox"]:checked::after {
+ transform: translateX(16px);
+ background: white;
+}
+
+.table { display: flex; flex-direction: column; gap: 6px; }
+.tr {
+ display: grid;
+ grid-template-columns: 90px minmax(160px, 1.5fr) 70px 90px minmax(100px, 2fr) 150px;
+ gap: 10px;
+ padding: 10px 12px;
+ border: 1px solid var(--border);
+ background: var(--surface2);
+ border-radius: 10px;
+ align-items: center;
+ font-size: 12px;
+ transition: border-color 0.15s, background 0.15s;
+}
+.tr:not(.head):hover {
+ border-color: color-mix(in srgb, var(--purple) 30%, transparent);
+ background: color-mix(in srgb, var(--purple) 3%, var(--surface2));
+}
+.tr.head {
+ background: transparent;
+ border-style: dashed;
+ color: var(--text2);
+ font-weight: 800;
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
+}
+
+/* === Images page === */
+.toolbar-search { flex: 1; min-width: 120px; }
+.toolbar-spacer { flex: 1; }
+
+/* === Local image items === */
+.image-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.image-item {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ overflow: hidden;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.image-item:hover {
+ border-color: var(--purple);
+ box-shadow: 0 2px 12px rgba(139, 92, 246, 0.06), 0 0 0 1px var(--purple-dim);
+}
+
+.image-item-main {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 14px 16px;
+}
+
+.image-item-icon {
+ width: 38px;
+ height: 38px;
+ border-radius: 10px;
+ background: var(--surface2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.image-item-icon svg {
+ width: 18px;
+ height: 18px;
+ stroke: var(--cyan);
+ fill: none;
+ stroke-width: 2;
+}
+
+.image-item-body {
+ flex: 1;
+ min-width: 0;
+}
+
+.image-item-name {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+}
+
+.image-item-meta {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 3px;
+ font-size: 11px;
+ color: var(--text2);
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+.image-item-meta .meta-sep {
+ width: 3px;
+ height: 3px;
+ border-radius: 50%;
+ background: var(--text3);
+ flex-shrink: 0;
+}
+
+.image-item-actions {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 16px;
+ border-top: 1px solid var(--border);
+ background: rgba(0, 0, 0, 0.02);
+}
+
+.app.light .image-item-actions {
+ background: rgba(0, 0, 0, 0.015);
+}
+
+.image-item-actions-group {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+}
+
+.image-item-actions-sep {
+ width: 1px;
+ height: 18px;
+ background: var(--border);
+ margin: 0 4px;
+ flex-shrink: 0;
+}
+
+.image-item-actions .action-btn {
+ height: 26px;
+ padding: 0 8px;
+ border-radius: 6px;
+ border: 1px solid transparent;
+ background: transparent;
+ color: var(--text3);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ transition: all 0.15s ease;
+ font-size: 11px;
+ font-weight: 500;
+ font-family: inherit;
+}
+
+.image-item-actions .action-btn .action-label {
+ display: none;
+}
+
+@media (min-width: 900px) {
+ .image-item-actions .action-btn .action-label {
+ display: inline;
+ }
+}
+
+.image-item-actions .action-btn:hover {
+ border-color: var(--purple);
+ color: var(--purple);
+ background: var(--purple-dim);
+}
+
+.image-item-actions .action-btn.danger:hover {
+ border-color: var(--red);
+ color: var(--red);
+ background: var(--red-dim);
+}
+
+.image-item-actions .action-btn:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+}
+
+.image-item-actions .action-btn svg {
+ width: 14px; height: 14px;
+ stroke: currentColor; fill: none;
+ stroke-width: 2;
+}
+
+/* === Image search & tags === */
+
+.img-tags-bar {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 12px 14px;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+}
+
+.img-tags-label {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--text2);
+ white-space: nowrap;
+ padding-top: 5px;
+}
+
+.img-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 12px;
+}
+
+.img-card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
+ cursor: default;
+}
+
+.img-card:hover {
+ border-color: var(--purple);
+ box-shadow: 0 4px 16px rgba(139, 92, 246, 0.08), 0 0 0 1px var(--purple-dim);
+ transform: translateY(-1px);
+}
+
+.img-card-top { flex: 1; }
+
+.img-card-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-bottom: 6px;
+}
+
+.img-card-name {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text);
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ word-break: break-all;
+ line-height: 1.3;
+}
+
+.img-official {
+ font-size: 10px;
+ font-weight: 700;
+ padding: 2px 7px;
+ border-radius: 6px;
+ background: rgba(52, 211, 153, 0.12);
+ color: var(--green);
+}
+
+.img-card-desc {
+ font-size: 12px;
+ color: var(--text2);
+ margin-top: 6px;
+ line-height: 1.4;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.img-card-bottom {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 14px;
+ padding-top: 12px;
+ border-top: 1px solid var(--border);
+ gap: 10px;
+}
+
+.img-card-stats {
+ display: flex;
+ gap: 12px;
+}
+
+.img-stat {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 11px;
+ color: var(--text2);
+ font-weight: 500;
+}
+
+.img-stat-icon {
+ width: 13px;
+ height: 13px;
+ stroke: var(--text3);
+ fill: none;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.img-card-actions {
+ display: flex;
+ gap: 6px;
+ flex-shrink: 0;
+}
+
+/* === VMs page === */
+.vm-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.vm-item {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ overflow: hidden;
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.vm-item:hover {
+ border-color: color-mix(in srgb, var(--purple) 30%, transparent);
+}
+
+.vm-item-expanded {
+ border-color: var(--purple);
+ box-shadow: 0 0 0 1px var(--purple-dim);
+}
+
+.vm-item-running {
+ border-left: 3px solid var(--green);
+}
+
+.vm-item-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 14px 16px;
+ cursor: pointer;
+ user-select: none;
+}
+
+.vm-item-row:hover {
+ background: var(--hover);
+}
+
+.vm-icon {
+ width: 36px;
+ height: 36px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ background: linear-gradient(135deg, var(--purple), var(--purple-hover));
+}
+
+.vm-icon svg {
+ width: 18px;
+ height: 18px;
+ stroke: white;
+ fill: none;
+ stroke-width: 2;
+}
+
+.vm-icon.stopped {
+ background: var(--surface2);
+}
+
+.vm-icon.stopped svg {
+ stroke: var(--text3);
+}
+
+.vm-item-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.vm-item-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text);
+}
+
+.vm-item-meta {
+ font-size: 11px;
+ color: var(--text2);
+ margin-top: 2px;
+}
+
+.vm-item-status {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ flex-shrink: 0;
+}
+
+.vm-item-status span {
+ font-size: 11px;
+ font-weight: 500;
+ color: var(--text2);
+}
+
+.vm-item-actions {
+ display: flex;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
+.vm-item-actions .action-btn {
+ width: 28px;
+ height: 28px;
+ border-radius: 6px;
+ border: 1px solid var(--border);
+ background: transparent;
+ color: var(--text3);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.15s ease;
+}
+
+.vm-item-actions .action-btn svg {
+ width: 14px;
+ height: 14px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+}
+
+.vm-item-actions .action-btn:hover {
+ border-color: var(--purple);
+ color: var(--purple);
+ background: var(--purple-dim);
+}
+
+.vm-item-actions .action-btn.danger:hover {
+ border-color: var(--red);
+ color: var(--red);
+ background: var(--red-dim);
+}
+
+.vm-item-actions .action-btn:disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
+}
+
+.vm-chevron {
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text3);
+ flex-shrink: 0;
+}
+
+.vm-chevron svg {
+ width: 16px;
+ height: 16px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+/* VM detail panel */
+.vm-detail {
+ border-top: 1px solid var(--border);
+ background: var(--bg);
+}
+
+.vm-detail-tabs {
+ display: flex;
+ gap: 0;
+ border-bottom: 1px solid var(--border);
+ padding: 0 16px;
+}
+
+.vm-tab {
+ padding: 10px 16px;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text2);
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ cursor: pointer;
+ transition: color 0.15s, border-color 0.15s;
+}
+
+.vm-tab:hover { color: var(--text); }
+
+.vm-tab.active {
+ color: var(--purple);
+ border-bottom-color: var(--purple);
+}
+
+.vm-detail-content {
+ padding: 16px;
+}
+
+.vm-stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: 10px;
+}
+
+.vm-stat-card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 12px;
+}
+
+.vm-stat-label {
+ font-size: 10px;
+ font-weight: 700;
+ color: var(--text3);
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
+}
+
+.vm-stat-value {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text);
+ margin-top: 4px;
+ display: flex;
+ align-items: center;
+}
+
+.vm-detail-form {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ max-width: 480px;
+}
+
+.vm-form-row {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.vm-form-row label {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--text2);
+}
+
+.vm-form-row .input {
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.vm-form-check {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--text2);
+ font-size: 12px;
+}
+
+.vm-mount-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.vm-mount-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 10px;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ font-size: 11px;
+ flex-wrap: wrap;
+}
+
+.vm-mount-tag {
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-weight: 600;
+ color: var(--cyan);
+}
+
+.vm-mount-path {
+ flex: 1;
+ color: var(--text2);
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.vm-mount-mode {
+ font-size: 10px;
+ font-weight: 700;
+ padding: 1px 5px;
+ border-radius: 4px;
+ background: var(--surface2);
+ color: var(--text3);
+}
+
+.badge {
+ display: inline-flex;
+ padding: 2px 8px;
+ border-radius: 999px;
+ background: var(--cyan-dim);
+ color: var(--cyan);
+ font-size: 11px;
+ font-weight: 800;
+ width: fit-content;
+}
+
+.mono { font-family: 'Geist Mono', ui-monospace, monospace; font-size: 12px; }
+.right { text-align: right; }
+.grow { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+.tags { display: flex; flex-wrap: wrap; gap: 8px; }
+.tag {
+ padding: 6px 10px;
+ border-radius: 999px;
+ border: 1px solid var(--border);
+ background: var(--surface2);
+ color: var(--text2);
+ cursor: pointer;
+ font-size: 12px;
+}
+.tag:hover { border-color: var(--purple); color: var(--purple); background: var(--purple-dim); }
+
+.result { border-top: 1px solid var(--border); padding-top: 10px; margin-top: 10px; }
+.result-title { font-size: 11px; font-weight: 800; color: var(--text2); margin-bottom: 6px; }
+.result-code { display: flex; gap: 8px; align-items: center; }
+.result-code code {
+ flex: 1;
+ display: block;
+ padding: 10px 12px;
+ background: var(--surface2);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ color: var(--cyan);
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ overflow-x: auto;
+}
+
+.sub { color: var(--text3); font-weight: 600; font-size: 11px; }
+.muted { color: var(--text2); }
+
+.mounts { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
+.mount { display: flex; gap: 10px; align-items: center; }
+.mount .mono { min-width: 64px; }
+
+/* === Modal & toast === */
+.modal-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.55);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 999;
+ padding: 20px;
+}
+
+.modal {
+ width: min(720px, 96vw);
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ box-shadow: 0 20px 60px rgba(0,0,0,0.45);
+ overflow: hidden;
+}
+
+.modal-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 14px;
+ border-bottom: 1px solid var(--border);
+}
+
+.modal-title { font-weight: 900; font-size: 13px; }
+.modal-actions { display: flex; gap: 6px; align-items: center; }
+
+.modal-body {
+ padding: 14px;
+ margin: 0;
+ color: var(--text2);
+ font-size: 12px;
+}
+
+.modal-pre {
+ padding: 14px;
+ margin: 0;
+ white-space: pre-wrap;
+ color: var(--text2);
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 12px;
+}
+
+.modal-footer {
+ padding: 12px 14px;
+ border-top: 1px solid var(--border);
+ display: flex;
+ justify-content: flex-end;
+}
+
+.toast {
+ position: fixed;
+ bottom: 18px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--surface);
+ border: 1px solid var(--border);
+ color: var(--text);
+ padding: 10px 14px;
+ border-radius: 999px;
+ box-shadow: 0 12px 40px rgba(0,0,0,0.35);
+ z-index: 1000;
+ font-size: 12px;
+ font-weight: 700;
+}
+
+/* === Update banner === */
+.update-banner {
+ position: fixed;
+ top: 0;
+ left: 220px;
+ right: 0;
+ z-index: 900;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 16px;
+ background: var(--purple-dim);
+ border-bottom: 1px solid var(--purple);
+ font-size: 13px;
+ gap: 12px;
+}
+
+.update-banner-content {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex: 1;
+ min-width: 0;
+}
+
+.update-banner-icon {
+ width: 18px;
+ height: 18px;
+ flex-shrink: 0;
+ stroke: var(--purple);
+ fill: none;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.update-banner-text {
+ color: var(--text);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.update-banner-text strong {
+ color: var(--purple);
+}
+
+.update-banner-link {
+ background: var(--purple);
+ color: #fff;
+ border: none;
+ padding: 4px 12px;
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: background 0.15s;
+}
+
+.update-banner-link:hover {
+ background: var(--purple-hover);
+}
+
+.update-banner-dismiss {
+ background: none;
+ border: none;
+ color: var(--text2);
+ font-size: 18px;
+ cursor: pointer;
+ padding: 0 4px;
+ opacity: 0.7;
+ flex-shrink: 0;
+ line-height: 1;
+}
+
+.update-banner-dismiss:hover {
+ opacity: 1;
+ color: var(--text);
+}
+
+/* Update result row in settings */
+.update-result .setting-icon svg {
+ width: 20px;
+ height: 20px;
+ stroke: var(--purple);
+ fill: none;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.update-notes {
+ max-height: 80px;
+ overflow-y: auto;
+ white-space: pre-wrap;
+}
+
+.btn-accent {
+ background: var(--purple) !important;
+ color: #fff !important;
+}
+
+.btn-accent:hover {
+ background: var(--purple-hover) !important;
+}
+
+/* === Log viewer === */
+.log-viewer {
+ background: #0a0c14;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ max-height: 400px;
+ min-height: 120px;
+ overflow: auto;
+}
+
+.log-content {
+ margin: 0;
+ padding: 12px 14px;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 11px;
+ line-height: 1.6;
+ color: #c8d6e5;
+ white-space: pre-wrap;
+ word-break: break-all;
+}
+
+.app.light .log-viewer {
+ background: #f1f5f9;
+}
+
+.app.light .log-content {
+ color: #1e293b;
+}
+
+/* === Container exec terminal === */
+.exec-modal-body {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ padding: 0;
+}
+
+.exec-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 14px;
+ border-bottom: 1px solid var(--border);
+ background: var(--surface2);
+}
+
+.exec-toolbar .input {
+ flex: 1;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 12px;
+ background: var(--bg);
+ color: var(--cyan);
+ border-color: var(--border);
+}
+
+.exec-toolbar .input:focus {
+ border-color: var(--purple);
+ box-shadow: 0 0 0 3px var(--purple-dim);
+}
+
+.exec-output {
+ background: #0a0c14;
+ color: #c8d0e0;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 12px;
+ line-height: 1.6;
+ padding: 14px;
+ min-height: 200px;
+ max-height: 360px;
+ overflow-y: auto;
+ white-space: pre-wrap;
+ word-break: break-all;
+}
+
+.app.light .exec-output {
+ background: #f0f2f6;
+ color: #1e293b;
+}
+
+.exec-entry {
+ margin-bottom: 8px;
+}
+
+.exec-prompt {
+ color: var(--green);
+ font-weight: 600;
+}
+
+.exec-result {
+ color: #c8d0e0;
+ margin-top: 2px;
+}
+
+.app.light .exec-result {
+ color: #334155;
+}
+
+.exec-error-text {
+ color: var(--red);
+ margin-top: 2px;
+}
+
+.exec-copy-bar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 14px;
+ border-top: 1px solid var(--border);
+ background: var(--surface2);
+ font-size: 11px;
+ color: var(--text2);
+}
+
+.exec-copy-bar code {
+ flex: 1;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 11px;
+ color: var(--cyan);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* === VM Console === */
+.console-viewer {
+ background: #0a0c14;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ min-height: 300px;
+ max-height: 480px;
+ overflow: auto;
+ position: relative;
+}
+
+.console-content {
+ margin: 0;
+ padding: 14px 16px;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 12px;
+ line-height: 1.6;
+ color: #34d399;
+ white-space: pre-wrap;
+ word-break: break-all;
+ min-height: 260px;
+}
+
+.console-empty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 260px;
+ color: var(--text3);
+ font-size: 13px;
+}
+
+.console-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 14px;
+ border-bottom: 1px solid var(--border);
+ background: var(--surface2);
+}
+
+.console-toolbar-right {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.console-auto-scroll {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ color: var(--text2);
+ cursor: pointer;
+ user-select: none;
+}
+
+.console-auto-scroll input[type="checkbox"] {
+ appearance: auto;
+ -webkit-appearance: auto;
+ width: 14px;
+ height: 14px;
+ cursor: pointer;
+ accent-color: var(--purple);
+}
+
+.app.light .console-viewer {
+ background: #f1f5f9;
+}
+
+.app.light .console-content {
+ color: #059669;
+}
+
+/* === Tabs === */
+.tabs {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 20px;
+ border-bottom: 1px solid var(--border);
+}
+
+.tab {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 16px;
+ border: none;
+ background: transparent;
+ color: var(--text2);
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: all 0.2s ease;
+ margin-bottom: -1px;
+}
+
+.tab:hover {
+ color: var(--text);
+ background: var(--hover);
+}
+
+.tab.active {
+ color: var(--purple);
+ border-bottom-color: var(--purple);
+}
+
+.tab .icon {
+ width: 18px;
+ height: 18px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.tab .icon svg {
+ width: 18px;
+ height: 18px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+}
+
+/* === Responsive === */
+
+/* Large screens — cap content width to prevent over-stretching on ultra-wide monitors */
+@media (min-width: 1600px) {
+ .content {
+ max-width: 1440px;
+ margin: 0 auto;
+ }
+}
+
+/* Medium-large screens — reduce grid density */
+@media (max-width: 1200px) {
+ .dash-cards { grid-template-columns: repeat(3, 1fr); }
+ .grid3 { grid-template-columns: 1fr 1fr; }
+}
+
+@media (max-width: 900px) {
+ .grid3 { grid-template-columns: 1fr; }
+ .img-layout { grid-template-columns: 1fr; }
+ .vm-layout { grid-template-columns: 1fr; }
+ .img-sidebar { order: -1; }
+ .vm-card-specs { grid-template-columns: repeat(2, 1fr); }
+}
+
+@media (max-width: 700px) {
+ .sidebar { width: 56px; min-width: 56px; }
+ .sidebar-header .brand-name,
+ .sidebar-header .brand-version,
+ .nav-label,
+ .nav-badge,
+ .nav-count { display: none; }
+ .sidebar-header { justify-content: center; padding: 16px 0; }
+ .nav-item { justify-content: center; padding: 10px; }
+ .card-meta, .card-status span { display: none; }
+ .dash-cards { grid-template-columns: 1fr; }
+ .grid2 { grid-template-columns: 1fr; }
+ .grid3 { grid-template-columns: 1fr; }
+ .settings { max-width: 100%; }
+ .input { min-width: 0; width: 100%; }
+ .toolbar { flex-wrap: wrap; }
+ .toolbar .input { flex: 1; min-width: 120px; }
+ .img-layout { grid-template-columns: 1fr; }
+ .vm-layout { grid-template-columns: 1fr; }
+ .vm-card-specs { grid-template-columns: repeat(2, 1fr); }
+ .vm-card-actions { flex-wrap: wrap; }
+ .input-action-row { flex-wrap: wrap; }
+ .input-action-row .input { min-width: 100%; }
+}
+
+/* === Resource stats bars === */
+.stats-bar-row {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+}
+
+.stats-bar-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+}
+
+.stats-bar-label {
+ font-size: 10px;
+ font-weight: 700;
+ color: var(--text3);
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
+ white-space: nowrap;
+ min-width: 28px;
+}
+
+.stats-bar-track {
+ width: 60px;
+ height: 4px;
+ border-radius: 2px;
+ background: var(--surface2);
+ overflow: hidden;
+ flex-shrink: 0;
+}
+
+.stats-bar-fill {
+ height: 100%;
+ border-radius: 2px;
+ transition: width 0.4s ease, background 0.4s ease;
+ min-width: 0;
+}
+
+.stats-bar-value {
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--text2);
+ white-space: nowrap;
+ min-width: 36px;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+}
+
+/* Container card with stats layout */
+.container-card-with-stats {
+ flex-direction: column;
+ gap: 0;
+ padding: 0;
+}
+
+.container-card-with-stats .container-card-main {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 12px 14px;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.container-card:not(.container-card-with-stats) .container-card-main {
+ display: contents;
+}
+
+.container-card-stats {
+ display: flex;
+ gap: 16px;
+ padding: 10px 14px;
+ border-top: 1px solid var(--border);
+ background: var(--surface2);
+ border-radius: 0 0 10px 10px;
+}
+
+.stat-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: var(--text2);
+}
+
+.stat-icon {
+ width: 16px;
+ height: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.6;
+}
+
+.stat-icon svg {
+ width: 14px;
+ height: 14px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+}
+
+.stat-value {
+ font-weight: 600;
+ color: var(--text);
+ font-family: 'Geist Mono', ui-monospace, monospace;
+}
+
+/* VM item stats */
+.vm-item-stats {
+ margin-top: 6px;
+}
+
+/* Dashboard resources panel */
+.dash-resources-panel {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 16px;
+ margin-bottom: 8px;
+}
+
+.dash-resources-panel .section-title {
+ margin-bottom: 12px;
+ margin-top: 0;
+}
+
+.dash-resources-bars {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.dash-resources-bars .stats-bar-item {
+ gap: 10px;
+}
+
+.dash-resources-bars .stats-bar-label {
+ min-width: 50px;
+}
+
+.dash-resources-bars .stats-bar-track {
+ width: 100%;
+ flex: 1;
+ height: 6px;
+ border-radius: 3px;
+}
+
+.dash-resources-bars .stats-bar-value {
+ min-width: 60px;
+ text-align: right;
+}
+
+/* ── Volumes Page ── */
+
+.vol-modal-xs {
+ max-width: 420px;
+}
+
+.vol-modal-sm {
+ max-width: 480px;
+}
+
+.vol-modal-md {
+ max-width: 640px;
+}
+
+.vol-modal-body-flush {
+ padding: 0;
+}
+
+.vol-confirm-text {
+ margin: 0;
+}
+
+.vol-error {
+ color: var(--red);
+ margin-top: 8px;
+}
+
+.vol-footer-btn {
+ margin-left: 8px;
+}
+
+.vol-btn-danger {
+ background: var(--red);
+ border-color: var(--red);
+}
+
+/* ── Enhanced VM Page Styles ── */
+
+/* Toolbar hint (subtle) */
+.toolbar .hint-subtle {
+ font-size: 11px;
+ opacity: 0.7;
+}
+
+/* Empty state enhancements */
+.empty-state-lg {
+ padding: 80px 20px;
+}
+
+.empty-state-lg .empty-icon {
+ width: 56px;
+ height: 56px;
+ border-radius: 16px;
+ margin-bottom: 20px;
+}
+
+.empty-state-lg h3 {
+ font-size: 16px;
+ margin-bottom: 8px;
+}
+
+.empty-state-lg p {
+ max-width: 320px;
+ margin: 0 auto;
+ line-height: 1.6;
+}
+
+.empty-state-lg .btn {
+ margin-top: 20px;
+}
+
+/* VM item acting state */
+.vm-item-acting {
+ opacity: 0.7;
+ pointer-events: none;
+}
+
+/* VM meta row with icons */
+.vm-item-meta-rich {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-wrap: wrap;
+}
+
+.vm-meta-spec {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+}
+
+.vm-meta-icon {
+ width: 12px;
+ height: 12px;
+ display: inline-flex;
+ opacity: 0.5;
+}
+
+.vm-meta-icon svg {
+ width: 12px;
+ height: 12px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+}
+
+.vm-meta-sep {
+ color: var(--text3);
+}
+
+.vm-meta-rosetta {
+ font-size: 10px;
+ font-weight: 700;
+ padding: 1px 6px;
+ border-radius: 4px;
+ background: var(--cyan-dim);
+ color: var(--cyan);
+}
+
+/* Enhanced status badge (pill style) */
+.vm-status-pill {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+ border-radius: 20px;
+ flex-shrink: 0;
+}
+
+.vm-status-pill.running {
+ background: rgba(52, 211, 153, 0.1);
+}
+
+.vm-status-pill.stopped {
+ background: var(--surface2);
+}
+
+.vm-status-pill .dot.running {
+ box-shadow: 0 0 6px var(--green);
+}
+
+.vm-status-label {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: capitalize;
+}
+
+.vm-status-label.running {
+ color: var(--green);
+}
+
+.vm-status-label.stopped {
+ color: var(--text3);
+}
+
+/* Action button color hints */
+.vm-item-actions .action-btn.action-stop {
+ color: var(--red);
+}
+
+.vm-item-actions .action-btn.action-start {
+ color: var(--green);
+}
+
+.vm-item-actions .action-btn.action-stop:disabled,
+.vm-item-actions .action-btn.action-start:disabled {
+ color: var(--text3);
+}
+
+/* Actions separator */
+.vm-actions-sep {
+ width: 1px;
+ height: 18px;
+ background: var(--border);
+ margin: 0 2px;
+ flex-shrink: 0;
+}
+
+/* VM detail panel animation */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(-4px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.vm-detail-animated {
+ animation: fadeIn 0.15s ease;
+}
+
+/* Tab with icons */
+.vm-detail-tabs-enhanced {
+ gap: 0;
+ background: var(--surface);
+}
+
+.vm-tab-icon {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 10px 14px;
+ font-size: 12px;
+ transition: all 0.15s ease;
+}
+
+.vm-tab-icon-svg {
+ width: 14px;
+ height: 14px;
+ display: flex;
+}
+
+.vm-tab-icon-svg svg {
+ width: 14px;
+ height: 14px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+}
+
+/* Detail content padding */
+.vm-detail-content-padded {
+ padding: 20px;
+}
+
+/* Stats grid enhanced */
+.vm-stats-grid-enhanced {
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ gap: 12px;
+}
+
+/* Stat card enhanced */
+.vm-stat-card-enhanced {
+ background: var(--surface);
+ border-radius: 12px;
+ padding: 14px;
+}
+
+.vm-stat-label-icon {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-bottom: 6px;
+}
+
+.vm-stat-label-icon-svg {
+ width: 12px;
+ height: 12px;
+ display: flex;
+ opacity: 0.5;
+}
+
+.vm-stat-label-icon-svg svg {
+ width: 12px;
+ height: 12px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+}
+
+.vm-stat-value-lg {
+ font-size: 20px;
+ font-weight: 700;
+}
+
+.vm-stat-value-unit {
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--text2);
+}
+
+/* Status/toggle pill (inline) */
+.vm-inline-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 3px 10px;
+ border-radius: 20px;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.vm-inline-pill.on {
+ background: rgba(52, 211, 153, 0.1);
+ color: var(--green);
+}
+
+.vm-inline-pill.off {
+ background: var(--surface2);
+ color: var(--text3);
+}
+
+.vm-inline-pill .dot {
+ width: 6px;
+ height: 6px;
+}
+
+/* State pill (running/stopped) */
+.vm-state-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 3px 10px;
+ border-radius: 20px;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.vm-state-pill.running {
+ background: rgba(52, 211, 153, 0.1);
+ color: var(--green);
+}
+
+.vm-state-pill.stopped {
+ background: var(--red-dim);
+ color: var(--red);
+}
+
+.vm-state-pill .dot {
+ width: 7px;
+ height: 7px;
+}
+
+.vm-state-pill .dot.running {
+ box-shadow: 0 0 6px var(--green);
+}
+
+/* Section card (used for SSH, mounts, ports forms) */
+.vm-section-card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 20px;
+ max-width: 480px;
+}
+
+.vm-section-card.wide {
+ max-width: 520px;
+}
+
+.vm-section-card-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 16px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--border);
+}
+
+.vm-section-card-icon {
+ width: 16px;
+ height: 16px;
+ display: flex;
+ color: var(--purple);
+}
+
+.vm-section-card-icon svg {
+ width: 16px;
+ height: 16px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+}
+
+.vm-section-card-title {
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--text);
+}
+
+/* Form layout helpers */
+.vm-form-grid-2 {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12px;
+}
+
+.vm-form-grid-2-narrow {
+ display: grid;
+ grid-template-columns: 1fr 100px;
+ gap: 12px;
+}
+
+.vm-form-grid-3 {
+ display: grid;
+ grid-template-columns: 1fr 1fr 100px;
+ gap: 12px;
+}
+
+.vm-form-row-spaced {
+ gap: 5px;
+}
+
+.vm-form-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 4px;
+}
+
+.vm-detail-form-spaced {
+ gap: 14px;
+ max-width: 100%;
+}
+
+/* Warning banner (used for virtiofs restart notice) */
+.vm-warning-banner {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 14px;
+ border-radius: 10px;
+ margin-bottom: 16px;
+ background: rgba(240, 192, 64, 0.08);
+ border: 1px solid rgba(240, 192, 64, 0.2);
+}
+
+.vm-warning-banner-icon {
+ width: 16px;
+ height: 16px;
+ display: flex;
+ color: #f0c040;
+ flex-shrink: 0;
+}
+
+.vm-warning-banner-icon svg {
+ width: 16px;
+ height: 16px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+}
+
+.vm-warning-banner-text {
+ font-size: 12px;
+ color: #f0c040;
+ line-height: 1.4;
+}
+
+/* Section label (used above mount/port lists) */
+.vm-section-label {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--text3);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 8px;
+}
+
+/* Mount list enhanced */
+.vm-mount-item-grid {
+ padding: 10px 12px;
+ border-radius: 10px;
+ display: grid;
+ grid-template-columns: auto 1fr auto auto;
+ gap: 10px;
+ align-items: center;
+}
+
+.vm-mount-tag-pill {
+ font-family: 'Geist Mono', ui-monospace, monospace;
font-weight: 600;
- color: var(--text);
+ color: var(--cyan);
+ padding: 2px 8px;
+ border-radius: 6px;
+ background: var(--cyan-dim);
+ font-size: 11px;
}
-.error-msg-text {
+.vm-mount-path-arrow {
font-size: 12px;
- color: var(--text2);
- max-width: 420px;
- text-align: center;
- line-height: 1.5;
- word-break: break-word;
}
-.error-msg-action {
- margin-top: 4px;
+.vm-mount-path-arrow .path-text {
+ color: var(--text);
}
-.error-inline {
- display: flex;
- align-items: flex-start;
- gap: 10px;
- background: rgba(248, 113, 113, 0.06);
- border: 1px solid rgba(248, 113, 113, 0.2);
- color: var(--red);
- padding: 12px 14px;
- border-radius: 12px;
- font-size: 12px;
- white-space: pre-wrap;
- line-height: 1.5;
+.vm-mount-path-arrow .path-arrow {
+ color: var(--text3);
+ margin: 0 6px;
}
-.error-inline-icon {
- width: 18px;
- height: 18px;
- flex-shrink: 0;
- margin-top: 1px;
+.vm-mount-mode-badge {
+ font-size: 10px;
+ font-weight: 700;
+ padding: 2px 6px;
+ border-radius: 4px;
}
-.error-inline-icon svg {
- width: 18px;
- height: 18px;
- stroke: var(--red);
- fill: none;
- stroke-width: 2;
- stroke-linecap: round;
- stroke-linejoin: round;
+.vm-mount-mode-badge.rw {
+ background: rgba(52, 211, 153, 0.1);
+ color: var(--green);
}
-.error-inline-text {
- flex: 1;
- min-width: 0;
+.vm-mount-mode-badge.ro {
+ background: var(--red-dim);
+ color: var(--red);
}
-.error-inline-dismiss {
- background: none;
- border: none;
- color: var(--red);
- cursor: pointer;
- padding: 0;
- font-size: 16px;
- line-height: 1;
- opacity: 0.6;
- transition: opacity 0.15s;
- flex-shrink: 0;
+/* Port forward item */
+.vm-pf-host {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text);
}
-.error-inline-dismiss:hover {
- opacity: 1;
+.vm-pf-icon {
+ width: 14px;
+ height: 14px;
+ display: flex;
+ color: var(--purple);
+ opacity: 0.7;
}
-/* === Common controls === */
-.page { display: flex; flex-direction: column; gap: 14px; }
+.vm-pf-icon svg {
+ width: 14px;
+ height: 14px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+}
-.toolbar {
+.vm-pf-mapping {
display: flex;
- gap: 8px;
align-items: center;
- flex-wrap: wrap;
+ gap: 6px;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 12px;
+ color: var(--text2);
}
-.input,
-.select {
- background: var(--surface);
- color: var(--text);
- border: 1px solid var(--border);
- height: 32px;
- padding: 6px 10px;
- border-radius: 8px;
- outline: none;
- font-size: 12px;
- transition: border-color 0.15s ease, box-shadow 0.15s ease;
- box-sizing: border-box;
+.vm-pf-mapping .pf-arrow {
+ color: var(--text3);
}
-.input { min-width: 0; }
+.vm-pf-mapping .pf-guest {
+ color: var(--text);
+}
-.input:focus,
-.select:focus {
- border-color: var(--purple);
- box-shadow: 0 0 0 3px var(--purple-dim);
+.vm-pf-protocol {
+ font-size: 10px;
+ font-weight: 700;
+ padding: 2px 8px;
+ border-radius: 4px;
+ background: var(--purple-dim);
+ color: var(--purple);
}
-.btn {
- border: 1px solid var(--border);
- background: var(--surface);
- color: var(--text);
- height: 32px;
- padding: 6px 12px;
- border-radius: 8px;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.15s ease;
- display: inline-flex;
- align-items: center;
- gap: 6px;
- box-sizing: border-box;
+/* Remove button (small with icon) */
+.btn-remove {
+ height: 24px;
}
-.btn:hover { border-color: var(--purple); background: var(--purple-dim); color: var(--purple); }
-.btn:disabled { opacity: 0.45; cursor: not-allowed; }
-.btn.primary { background: var(--purple); border-color: var(--purple); color: white; }
-.btn.primary:hover { background: var(--purple-hover); border-color: var(--purple-hover); color: white; }
-.btn.sm { height: 28px; padding: 4px 10px; border-radius: 6px; font-size: 11px; }
-.btn.xs { height: 24px; padding: 2px 8px; border-radius: 6px; font-size: 11px; }
+.btn-remove-icon {
+ width: 12px;
+ height: 12px;
+ display: flex;
+}
-.icon svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 2; }
+.btn-remove-icon svg {
+ width: 12px;
+ height: 12px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+}
-.icon-btn {
- width: 32px;
- height: 32px;
+/* Guest hint code block */
+.vm-code-hint {
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 11px;
+ background: var(--surface2);
+ padding: 8px 10px;
border-radius: 8px;
+ color: var(--cyan);
border: 1px solid var(--border);
+}
+
+/* Console tab card */
+.vm-console-card {
background: var(--surface);
- color: var(--text2);
- cursor: pointer;
- display: inline-flex;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 20px;
+ text-align: center;
+}
+
+.vm-console-card-icon {
+ width: 44px;
+ height: 44px;
+ border-radius: 12px;
+ margin: 0 auto 14px;
+ background: var(--purple-dim);
+ display: flex;
align-items: center;
justify-content: center;
- transition: all 0.15s ease;
}
-.icon-btn:hover { border-color: var(--purple); color: var(--purple); background: var(--purple-dim); }
-.icon-btn svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2; }
+.vm-console-card-icon-svg {
+ width: 22px;
+ height: 22px;
+ display: flex;
+ color: var(--purple);
+}
-.grid2 {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 14px;
- align-items: start;
+.vm-console-card-icon-svg svg {
+ width: 22px;
+ height: 22px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
}
-.grid3 {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- gap: 14px;
- align-items: start;
+.vm-console-card-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text);
+ margin-bottom: 6px;
}
-.panel {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 14px;
- padding: 18px;
- transition: border-color 0.2s ease, box-shadow 0.2s ease;
+.vm-console-card-hint {
+ max-width: 320px;
+ margin: 0 auto 16px;
}
-.panel:hover {
- border-color: color-mix(in srgb, var(--purple) 25%, transparent);
+/* Mount list section */
+.vm-list-section {
+ margin-bottom: 20px;
}
-.panel-title {
+/* Modal enhancements */
+.modal-head-enhanced {
+ padding: 14px 18px;
+}
+
+.modal-head-left {
display: flex;
align-items: center;
- gap: 8px;
- font-size: 13px;
- font-weight: 700;
- color: var(--text);
- margin-bottom: 14px;
- padding-bottom: 10px;
- border-bottom: 1px solid var(--border);
+ gap: 10px;
}
-.panel-title-icon {
- width: 24px;
- height: 24px;
- border-radius: 6px;
- background: var(--purple-dim);
+.modal-head-icon {
+ width: 28px;
+ height: 28px;
+ border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
- flex-shrink: 0;
}
-.panel-title-icon svg {
- width: 14px;
- height: 14px;
- stroke: var(--purple);
- fill: none;
- stroke-width: 2;
- stroke-linecap: round;
- stroke-linejoin: round;
+.modal-head-icon.purple {
+ background: var(--purple-dim);
}
-.panel.full-width {
- grid-column: 1 / -1;
+.modal-head-icon.green {
+ background: rgba(52, 211, 153, 0.1);
}
-.panel-subtitle {
+.modal-head-icon-svg {
+ width: 16px;
+ height: 16px;
display: flex;
- align-items: center;
- gap: 6px;
- font-size: 11px;
- font-weight: 700;
- color: var(--text2);
- margin-top: 16px;
- margin-bottom: 8px;
- padding-top: 12px;
- border-top: 1px solid var(--border);
}
-.hint {
- color: var(--text2);
- font-size: 12px;
- line-height: 1.5;
+.modal-head-icon-svg svg {
+ width: 16px;
+ height: 16px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+}
+
+.modal-head-icon.purple .modal-head-icon-svg {
+ color: var(--purple);
}
-/* error-inline styles moved to States section above */
+.modal-head-icon.green .modal-head-icon-svg {
+ color: var(--green);
+}
-.form { display: flex; flex-direction: column; gap: 10px; }
-.form .row { display: flex; flex-direction: column; gap: 6px; }
-.form .row label { font-size: 11px; font-weight: 700; color: var(--text2); }
-.form .row.two { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
-.form .row.inline { flex-direction: row; align-items: center; gap: 8px; color: var(--text2); font-size: 12px; }
+.modal-title-lg {
+ font-weight: 900;
+ font-size: 14px;
+}
-/* Custom checkbox / toggle */
-.form .row.inline input[type="checkbox"],
-.setting-row input[type="checkbox"] {
- appearance: none;
- -webkit-appearance: none;
- width: 36px;
- height: 20px;
- border-radius: 10px;
- background: var(--surface2);
- border: 1px solid var(--border);
- position: relative;
- cursor: pointer;
- transition: background 0.2s, border-color 0.2s;
- flex-shrink: 0;
+.modal-title-sub {
+ font-weight: 400;
+ color: var(--text2);
+ margin-left: 6px;
}
-.form .row.inline input[type="checkbox"]::after,
-.setting-row input[type="checkbox"]::after {
- content: '';
- position: absolute;
- top: 2px;
- left: 2px;
- width: 14px;
- height: 14px;
- border-radius: 50%;
- background: var(--text3);
- transition: transform 0.2s, background 0.2s;
+.modal-body-padded {
+ padding: 20px;
}
-.form .row.inline input[type="checkbox"]:checked,
-.setting-row input[type="checkbox"]:checked {
- background: var(--purple);
- border-color: var(--purple);
+.modal-footer-padded {
+ padding: 14px 18px;
+ gap: 8px;
}
-.form .row.inline input[type="checkbox"]:checked::after,
-.setting-row input[type="checkbox"]:checked::after {
- transform: translateX(16px);
- background: white;
+/* Create VM form enhancements */
+.form-spaced {
+ gap: 16px;
}
-.table { display: flex; flex-direction: column; gap: 6px; }
-.tr {
- display: grid;
- grid-template-columns: 90px minmax(160px, 1.5fr) 70px 90px minmax(100px, 2fr) 150px;
- gap: 10px;
- padding: 10px 12px;
- border: 1px solid var(--border);
+.input-lg {
+ height: 36px;
+}
+
+/* OS Image list container */
+.os-image-list-container {
background: var(--surface2);
+ border: 1px solid var(--border);
border-radius: 10px;
- align-items: center;
- font-size: 12px;
- transition: border-color 0.15s, background 0.15s;
-}
-.tr:not(.head):hover {
- border-color: color-mix(in srgb, var(--purple) 30%, transparent);
- background: color-mix(in srgb, var(--purple) 3%, var(--surface2));
+ overflow: hidden;
}
-.tr.head {
- background: transparent;
- border-style: dashed;
- color: var(--text2);
- font-weight: 800;
- font-size: 11px;
+
+.os-image-list-header {
+ padding: 8px 12px;
+ font-size: 10px;
+ font-weight: 700;
+ color: var(--text3);
text-transform: uppercase;
- letter-spacing: 0.3px;
+ letter-spacing: 0.5px;
+ border-bottom: 1px solid var(--border);
}
-/* === Images page === */
-.toolbar-search { flex: 1; min-width: 120px; }
-
-.img-tags-bar {
+.os-image-list-item {
display: flex;
- align-items: flex-start;
+ align-items: center;
gap: 10px;
- padding: 12px 14px;
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 12px;
+ padding: 10px 12px;
+ transition: background 0.15s;
}
-.img-tags-label {
- font-size: 11px;
- font-weight: 700;
- color: var(--text2);
- white-space: nowrap;
- padding-top: 5px;
+.os-image-list-item:not(:last-child) {
+ border-bottom: 1px solid var(--border);
}
-.img-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
- gap: 10px;
+.os-image-list-icon {
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
}
-.img-card {
+.os-image-list-icon.ready {
+ background: rgba(52, 211, 153, 0.1);
+}
+
+.os-image-list-icon:not(.ready) {
background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 12px;
- padding: 14px;
+}
+
+.os-image-list-icon-svg {
+ width: 16px;
+ height: 16px;
display: flex;
- flex-direction: column;
- justify-content: space-between;
- transition: border-color 0.2s, box-shadow 0.2s;
- cursor: default;
}
-.img-card:hover {
- border-color: color-mix(in srgb, var(--purple) 40%, transparent);
- box-shadow: 0 0 0 1px var(--purple-dim);
+.os-image-list-icon-svg svg {
+ width: 16px;
+ height: 16px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
}
-.img-card-top { flex: 1; }
+.os-image-list-icon.ready .os-image-list-icon-svg {
+ color: var(--green);
+}
-.img-card-header {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-bottom: 6px;
+.os-image-list-icon:not(.ready) .os-image-list-icon-svg {
+ color: var(--text3);
}
-.img-card-name {
- font-size: 13px;
+.os-image-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.os-image-name {
font-weight: 600;
+ font-size: 12px;
color: var(--text);
- font-family: 'Fira Code', 'SF Mono', monospace;
- word-break: break-all;
- line-height: 1.3;
}
-.img-official {
- font-size: 10px;
- font-weight: 700;
- padding: 2px 6px;
- border-radius: 4px;
- background: var(--green);
- color: white;
+.os-image-meta {
+ font-size: 11px;
+ color: var(--text2);
+ margin-top: 1px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
}
-.img-card-desc {
- font-size: 12px;
- color: var(--text2);
+/* Progress bar enhanced */
+.os-image-progress {
margin-top: 6px;
- line-height: 1.4;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
+}
+
+.os-image-progress-track {
+ height: 5px;
+ border-radius: 3px;
+ background: var(--border);
overflow: hidden;
}
-.img-card-bottom {
+.os-image-progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, var(--purple), var(--cyan));
+ transition: width 0.3s ease;
+ border-radius: 3px;
+}
+
+.os-image-progress-text {
+ font-size: 10px;
+ color: var(--text3);
+ margin-top: 3px;
display: flex;
- align-items: center;
justify-content: space-between;
- margin-top: 12px;
- padding-top: 10px;
- border-top: 1px solid var(--border);
- gap: 8px;
}
-.img-card-stats {
+.os-image-progress-pct {
+ font-weight: 600;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+}
+
+/* OS image actions */
+.os-image-actions {
display: flex;
- gap: 12px;
+ gap: 6px;
+ align-items: center;
+ flex-shrink: 0;
}
-.img-stat {
+.os-image-downloading {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--purple);
display: flex;
align-items: center;
gap: 4px;
- font-size: 11px;
- color: var(--text2);
- font-weight: 500;
}
-.img-stat-icon {
- width: 13px;
- height: 13px;
- stroke: var(--text3);
- fill: none;
- stroke-width: 2;
- stroke-linecap: round;
- stroke-linejoin: round;
+.os-image-downloading .spinner {
+ width: 12px;
+ height: 12px;
+ border-width: 1.5px;
}
-.img-card-actions {
+.os-image-ready-badge {
+ font-size: 11px;
+ font-weight: 600;
+ padding: 2px 8px;
+ border-radius: 20px;
+ color: var(--green);
+ background: rgba(52, 211, 153, 0.1);
display: flex;
- gap: 6px;
- flex-shrink: 0;
+ align-items: center;
+ gap: 4px;
}
-/* === VMs page === */
-.vm-list {
- display: flex;
- flex-direction: column;
- gap: 8px;
+.os-image-ready-badge .dot {
+ width: 5px;
+ height: 5px;
}
-.vm-item {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 12px;
- overflow: hidden;
- transition: border-color 0.2s, box-shadow 0.2s;
+.btn-download {
+ color: var(--purple);
+ border-color: var(--purple);
}
-.vm-item:hover {
- border-color: color-mix(in srgb, var(--purple) 30%, transparent);
+/* Hardware config section divider */
+.form-section-divider {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--text3);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-top: 4px;
+ padding-top: 12px;
+ border-top: 1px solid var(--border);
}
-.vm-item-expanded {
- border-color: var(--purple);
- box-shadow: 0 0 0 1px var(--purple-dim);
+/* Form label with icon */
+.form-label-icon {
+ display: flex;
+ align-items: center;
+ gap: 4px;
}
-.vm-item-running {
- border-left: 3px solid var(--green);
+/* Create button spinner */
+.btn-spinner {
+ width: 14px;
+ height: 14px;
+ border-width: 1.5px;
+ margin-right: 4px;
}
-.vm-item-row {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 14px 16px;
- cursor: pointer;
- user-select: none;
+/* Console modal enhancements */
+.console-viewer-padded {
+ padding: 0 14px 14px;
}
-.vm-item-row:hover {
- background: var(--hover);
+.console-viewer-lg {
+ border-radius: 10px;
+ min-height: 320px;
+ max-height: 500px;
}
-.vm-icon {
- width: 36px;
- height: 36px;
- border-radius: 10px;
+.console-empty-enhanced {
display: flex;
+ flex-direction: column;
align-items: center;
justify-content: center;
- flex-shrink: 0;
- background: linear-gradient(135deg, var(--purple), var(--purple-hover));
+ gap: 10px;
+ min-height: 320px;
}
-.vm-icon svg {
- width: 18px;
- height: 18px;
- stroke: white;
+.console-empty-icon {
+ width: 28px;
+ height: 28px;
+ display: flex;
+ color: var(--text3);
+ opacity: 0.5;
+}
+
+.console-empty-icon svg {
+ width: 28px;
+ height: 28px;
+ stroke: currentColor;
fill: none;
stroke-width: 2;
}
-.vm-icon.stopped {
- background: var(--surface2);
+.vm-form-row .input-full {
+ width: 100%;
}
-.vm-icon.stopped svg {
- stroke: var(--text3);
+/* Modal size variants */
+.modal-create-vm {
+ max-width: 560px;
}
-.vm-item-info {
- flex: 1;
- min-width: 0;
+.modal-console {
+ max-width: 840px;
+ width: 96vw;
}
-.vm-item-name {
- font-size: 14px;
- font-weight: 600;
- color: var(--text);
+.vol-btn-danger:hover {
+ background: var(--red);
+ border-color: var(--red);
+ opacity: 0.9;
}
-.vm-item-meta {
- font-size: 11px;
- color: var(--text2);
- margin-top: 2px;
+.vol-raw-json {
+ max-height: 200px;
}
-.vm-item-status {
- display: flex;
- align-items: center;
- gap: 5px;
- flex-shrink: 0;
+.vol-inspect-body {
+ padding: 14px;
}
-.vm-item-status span {
- font-size: 11px;
+.vol-inspect-value {
+ color: var(--text);
font-weight: 500;
- color: var(--text2);
}
-.vm-item-actions {
+.vol-inspect-value.normal {
+ color: var(--text);
+ font-weight: 400;
+}
+
+.vol-mountpoint {
+ font-size: 11px;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ color: var(--cyan);
+ background: var(--surface2);
+ padding: 6px 10px;
+ border-radius: 8px;
+ border: 1px solid var(--border);
+ word-break: break-all;
+ display: block;
+}
+
+.vol-kv-list {
display: flex;
+ flex-direction: column;
gap: 4px;
- flex-shrink: 0;
}
-.vm-item-actions .action-btn {
- width: 28px;
- height: 28px;
+.vol-kv-item {
+ font-size: 11px;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ color: var(--text2);
+ background: var(--surface2);
+ padding: 4px 8px;
border-radius: 6px;
border: 1px solid var(--border);
- background: transparent;
+}
+
+.vol-kv-key {
+ color: var(--purple);
+}
+
+.vol-kv-sep {
color: var(--text3);
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.15s ease;
}
-.vm-item-actions .action-btn svg {
- width: 14px;
- height: 14px;
- stroke: currentColor;
- fill: none;
- stroke-width: 2;
+.vol-kv-val {
+ color: var(--cyan);
}
-.vm-item-actions .action-btn:hover {
- border-color: var(--purple);
- color: var(--purple);
- background: var(--purple-dim);
+.vol-raw-header {
+ border-top: 1px solid var(--border);
+ padding: 8px 14px;
}
-.vm-item-actions .action-btn.danger:hover {
- border-color: var(--red);
- color: var(--red);
- background: var(--red-dim);
+.vol-raw-label {
+ font-size: 11px;
+ color: var(--text3);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
}
-.vm-item-actions .action-btn:disabled {
- opacity: 0.3;
- cursor: not-allowed;
+.vol-delete-name {
+ margin: 8px 0 0;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text);
+ background: var(--surface2);
+ padding: 8px 12px;
+ border-radius: 8px;
+ border: 1px solid var(--border);
}
-.vm-chevron {
- width: 24px;
- height: 24px;
+.vol-delete-btn-icon {
+ margin-left: 4px;
+}
+
+/* === Kubernetes page === */
+.k8s-cluster-card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ overflow: hidden;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.k8s-cluster-card:hover {
+ border-color: color-mix(in srgb, var(--purple) 25%, transparent);
+}
+
+.k8s-cluster-header {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 20px 22px;
+ border-bottom: 1px solid var(--border);
+}
+
+.k8s-cluster-icon {
+ width: 42px;
+ height: 42px;
+ border-radius: 12px;
+ background: linear-gradient(135deg, var(--purple), var(--purple-hover));
display: flex;
align-items: center;
justify-content: center;
- color: var(--text3);
flex-shrink: 0;
}
-.vm-chevron svg {
- width: 16px;
- height: 16px;
- stroke: currentColor;
+.k8s-cluster-icon svg {
+ width: 22px;
+ height: 22px;
+ stroke: white;
fill: none;
- stroke-width: 2;
+ stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
-/* VM detail panel */
-.vm-detail {
- border-top: 1px solid var(--border);
- background: var(--bg);
+.k8s-cluster-icon.stopped {
+ background: var(--surface2);
}
-.vm-detail-tabs {
+.k8s-cluster-icon.stopped svg {
+ stroke: var(--text3);
+}
+
+.k8s-cluster-title {
+ flex: 1;
+ min-width: 0;
+}
+
+.k8s-cluster-title h2 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--text);
+ line-height: 1.2;
+}
+
+.k8s-cluster-title-sub {
display: flex;
- gap: 0;
- border-bottom: 1px solid var(--border);
- padding: 0 16px;
+ align-items: center;
+ gap: 8px;
+ margin-top: 3px;
+ font-size: 12px;
+ color: var(--text2);
}
-.vm-tab {
- padding: 10px 16px;
+.k8s-status-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 12px;
+ border-radius: 20px;
font-size: 12px;
font-weight: 600;
- color: var(--text2);
- background: none;
- border: none;
- border-bottom: 2px solid transparent;
- cursor: pointer;
- transition: color 0.15s, border-color 0.15s;
+ flex-shrink: 0;
}
-.vm-tab:hover { color: var(--text); }
+.k8s-status-badge.running {
+ background: rgba(52, 211, 153, 0.1);
+ color: var(--green);
+}
-.vm-tab.active {
- color: var(--purple);
- border-bottom-color: var(--purple);
+.k8s-status-badge.stopped {
+ background: var(--red-dim);
+ color: var(--red);
}
-.vm-detail-content {
- padding: 16px;
+.k8s-status-badge .dot {
+ width: 7px;
+ height: 7px;
+ box-shadow: 0 0 6px currentColor;
}
-.vm-stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
- gap: 10px;
+.k8s-cluster-body {
+ padding: 20px 22px;
}
-.vm-stat-card {
+.k8s-cluster-actions {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ padding: 16px 22px;
+ border-top: 1px solid var(--border);
+ background: rgba(0, 0, 0, 0.02);
+}
+
+.app.light .k8s-cluster-actions {
+ background: rgba(0, 0, 0, 0.015);
+}
+
+/* K8s Dashboard */
+.k8s-dashboard {
background: var(--surface);
border: 1px solid var(--border);
- border-radius: 10px;
- padding: 12px;
+ border-radius: 14px;
+ overflow: hidden;
+ transition: border-color 0.2s ease;
}
-.vm-stat-label {
- font-size: 10px;
- font-weight: 700;
- color: var(--text3);
- text-transform: uppercase;
- letter-spacing: 0.3px;
+.k8s-dashboard:hover {
+ border-color: color-mix(in srgb, var(--purple) 25%, transparent);
}
-.vm-stat-value {
- font-size: 14px;
- font-weight: 600;
- color: var(--text);
- margin-top: 4px;
+/* Dashboard Header */
+.k8s-dashboard-header {
display: flex;
align-items: center;
+ justify-content: space-between;
+ padding: 16px 22px;
+ border-bottom: 1px solid var(--border);
+ background: color-mix(in srgb, var(--purple) 3%, var(--surface));
}
-.vm-detail-form {
+.k8s-dashboard-title {
display: flex;
- flex-direction: column;
+ align-items: center;
gap: 10px;
- max-width: 480px;
}
-.vm-form-row {
+.k8s-dashboard-title-icon {
+ width: 30px;
+ height: 30px;
+ border-radius: 8px;
+ background: var(--purple-dim);
display: flex;
- flex-direction: column;
- gap: 4px;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.k8s-dashboard-title-icon svg {
+ width: 16px;
+ height: 16px;
+ stroke: var(--purple);
+ fill: none;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
}
-.vm-form-row label {
- font-size: 11px;
+.k8s-dashboard-title h3 {
+ margin: 0;
+ font-size: 14px;
font-weight: 700;
- color: var(--text2);
-}
-
-.vm-form-row .input {
- width: 100%;
- box-sizing: border-box;
+ color: var(--text);
}
-.vm-form-check {
+.k8s-dashboard-actions {
display: flex;
align-items: center;
gap: 8px;
- color: var(--text2);
- font-size: 12px;
}
-.vm-mount-list {
+/* Tabs */
+.k8s-tabs {
display: flex;
- flex-direction: column;
- gap: 6px;
+ gap: 2px;
+ border-bottom: 1px solid var(--border);
+ padding: 0 18px;
+ align-items: stretch;
+ background: var(--surface);
}
-.vm-mount-item {
+.k8s-tab {
+ padding: 11px 16px;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text3);
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ cursor: pointer;
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
+ font-family: inherit;
display: flex;
align-items: center;
- gap: 8px;
- padding: 8px 10px;
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 8px;
- font-size: 11px;
- flex-wrap: wrap;
+ gap: 7px;
+ position: relative;
}
-.vm-mount-tag {
- font-family: 'Fira Code', monospace;
- font-weight: 600;
- color: var(--cyan);
+.k8s-tab:hover {
+ color: var(--text);
+ background: var(--hover);
}
-.vm-mount-path {
- flex: 1;
- color: var(--text2);
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
+.k8s-tab.active {
+ color: var(--purple);
+ border-bottom-color: var(--purple);
}
-.vm-mount-mode {
+.k8s-tab-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+}
+
+.k8s-tab-icon svg {
+ width: 14px;
+ height: 14px;
+ stroke: currentColor;
+ fill: none;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.k8s-tab-count {
font-size: 10px;
font-weight: 700;
- padding: 1px 5px;
- border-radius: 4px;
+ min-width: 20px;
+ height: 18px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 9px;
background: var(--surface2);
color: var(--text3);
+ padding: 0 5px;
+ transition: background 0.15s, color 0.15s;
}
-.badge {
- display: inline-flex;
- padding: 2px 8px;
- border-radius: 999px;
- background: var(--cyan-dim);
- color: var(--cyan);
- font-size: 11px;
- font-weight: 800;
- width: fit-content;
-}
-
-.mono { font-family: 'Fira Code', 'SF Mono', ui-monospace, monospace; font-size: 12px; }
-.right { text-align: right; }
-.grow { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-
-.tags { display: flex; flex-wrap: wrap; gap: 8px; }
-.tag {
- padding: 6px 10px;
- border-radius: 999px;
- border: 1px solid var(--border);
- background: var(--surface2);
- color: var(--text2);
- cursor: pointer;
- font-size: 12px;
+.k8s-tab.active .k8s-tab-count {
+ background: var(--purple-dim);
+ color: var(--purple);
}
-.tag:hover { border-color: var(--purple); color: var(--purple); background: var(--purple-dim); }
-.result { border-top: 1px solid var(--border); padding-top: 10px; margin-top: 10px; }
-.result-title { font-size: 11px; font-weight: 800; color: var(--text2); margin-bottom: 6px; }
-.result-code { display: flex; gap: 8px; align-items: center; }
-.result-code code {
+.k8s-tab-spacer {
flex: 1;
- display: block;
- padding: 10px 12px;
- background: var(--surface2);
- border: 1px solid var(--border);
- border-radius: 10px;
- color: var(--cyan);
- font-family: 'Fira Code', 'SF Mono', ui-monospace, monospace;
- overflow-x: auto;
}
-.sub { color: var(--text3); font-weight: 600; font-size: 11px; }
-.muted { color: var(--text2); }
-
-.mounts { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
-.mount { display: flex; gap: 10px; align-items: center; }
-.mount .mono { min-width: 64px; }
-
-/* === Modal & toast === */
-.modal-backdrop {
- position: fixed;
- inset: 0;
- background: rgba(0,0,0,0.55);
+.k8s-tab-loading {
display: flex;
align-items: center;
- justify-content: center;
- z-index: 999;
- padding: 20px;
+ gap: 8px;
+ font-size: 11px;
+ color: var(--text3);
+ padding-right: 6px;
}
-.modal {
- width: min(720px, 96vw);
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 14px;
- box-shadow: 0 20px 60px rgba(0,0,0,0.45);
- overflow: hidden;
+.k8s-tab-content {
+ padding: 22px;
}
-.modal-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 14px;
- border-bottom: 1px solid var(--border);
+/* K8s Overview Cards */
+.k8s-overview-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 14px;
}
-.modal-title { font-weight: 900; font-size: 13px; }
-.modal-actions { display: flex; gap: 6px; align-items: center; }
-
-.modal-body {
- padding: 14px;
- margin: 0;
- color: var(--text2);
- font-size: 12px;
+@media (max-width: 1200px) {
+ .k8s-overview-grid { grid-template-columns: repeat(2, 1fr); }
}
-.modal-pre {
- padding: 14px;
- margin: 0;
- white-space: pre-wrap;
- color: var(--text2);
- font-family: 'Fira Code', 'SF Mono', ui-monospace, monospace;
- font-size: 12px;
+@media (max-width: 768px) {
+ .k8s-overview-grid { grid-template-columns: 1fr; }
}
-.modal-footer {
- padding: 12px 14px;
- border-top: 1px solid var(--border);
+.k8s-overview-card {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 0;
display: flex;
- justify-content: flex-end;
+ flex-direction: column;
+ cursor: pointer;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
+ overflow: hidden;
+ position: relative;
}
-.toast {
- position: fixed;
- bottom: 18px;
- left: 50%;
- transform: translateX(-50%);
- background: var(--surface);
- border: 1px solid var(--border);
- color: var(--text);
- padding: 10px 14px;
- border-radius: 999px;
- box-shadow: 0 12px 40px rgba(0,0,0,0.35);
- z-index: 1000;
- font-size: 12px;
- font-weight: 700;
+.k8s-overview-card:hover {
+ border-color: var(--purple);
+ box-shadow: 0 4px 20px rgba(139, 92, 246, 0.1), 0 0 0 1px var(--purple-dim);
+ transform: translateY(-2px);
}
-/* === Update banner === */
-.update-banner {
- position: fixed;
- top: 0;
- left: 220px;
- right: 0;
- z-index: 900;
+.k8s-overview-card-header {
display: flex;
align-items: center;
- justify-content: space-between;
- padding: 8px 16px;
- background: var(--purple-dim);
- border-bottom: 1px solid var(--purple);
- font-size: 13px;
- gap: 12px;
+ gap: 10px;
+ padding: 14px 16px 0;
}
-.update-banner-content {
+.k8s-overview-card-icon {
+ width: 34px;
+ height: 34px;
+ border-radius: 9px;
display: flex;
align-items: center;
- gap: 10px;
- flex: 1;
- min-width: 0;
+ justify-content: center;
+ flex-shrink: 0;
}
-.update-banner-icon {
- width: 18px;
- height: 18px;
- flex-shrink: 0;
- stroke: var(--purple);
+.k8s-overview-card-icon svg {
+ width: 17px;
+ height: 17px;
+ stroke: currentColor;
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
-.update-banner-text {
- color: var(--text);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.update-banner-text strong {
+.k8s-overview-card-icon.purple {
+ background: var(--purple-dim);
color: var(--purple);
}
-.update-banner-link {
- background: var(--purple);
- color: #fff;
- border: none;
- padding: 4px 12px;
- border-radius: 6px;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- white-space: nowrap;
- transition: background 0.15s;
-}
-
-.update-banner-link:hover {
- background: var(--purple-hover);
+.k8s-overview-card-icon.cyan {
+ background: var(--cyan-dim);
+ color: var(--cyan);
}
-.update-banner-dismiss {
- background: none;
- border: none;
- color: var(--text2);
- font-size: 18px;
- cursor: pointer;
- padding: 0 4px;
- opacity: 0.7;
- flex-shrink: 0;
- line-height: 1;
+.k8s-overview-card-icon.green {
+ background: rgba(52, 211, 153, 0.1);
+ color: var(--green);
}
-.update-banner-dismiss:hover {
- opacity: 1;
+.k8s-overview-card-icon.neutral {
+ background: var(--surface2);
+ color: var(--text2);
+}
+
+.k8s-overview-card-body {
+ padding: 12px 16px 16px;
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+}
+
+.k8s-overview-card-value {
+ font-size: 30px;
+ font-weight: 800;
color: var(--text);
+ line-height: 1;
+ letter-spacing: -0.5px;
}
-/* Update result row in settings */
-.update-result .setting-icon svg {
- width: 20px;
- height: 20px;
- stroke: var(--purple);
- fill: none;
- stroke-width: 2;
- stroke-linecap: round;
- stroke-linejoin: round;
+.k8s-overview-card-label {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text2);
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
}
-.update-notes {
- max-height: 80px;
- overflow-y: auto;
- white-space: pre-wrap;
+.k8s-overview-card-hint {
+ font-size: 11px;
+ color: var(--text3);
+ font-weight: 500;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.btn-accent {
- background: var(--purple) !important;
- color: #fff !important;
+/* Bottom accent bar */
+.k8s-overview-card-accent {
+ height: 3px;
+ width: 100%;
+ opacity: 0;
+ transition: opacity 0.2s ease;
}
-.btn-accent:hover {
- background: var(--purple-hover) !important;
+.k8s-overview-card:hover .k8s-overview-card-accent {
+ opacity: 1;
}
-/* === Log viewer === */
-.log-viewer {
- background: #0a0c14;
- border: 1px solid var(--border);
- border-radius: 10px;
- max-height: 400px;
- min-height: 120px;
- overflow: auto;
+.k8s-overview-card-accent.purple {
+ background: linear-gradient(90deg, var(--purple), color-mix(in srgb, var(--purple) 40%, transparent));
}
-.log-content {
- margin: 0;
- padding: 12px 14px;
- font-family: 'Fira Code', 'SF Mono', ui-monospace, monospace;
- font-size: 11px;
- line-height: 1.6;
- color: #c8d6e5;
- white-space: pre-wrap;
- word-break: break-all;
+.k8s-overview-card-accent.cyan {
+ background: linear-gradient(90deg, var(--cyan), color-mix(in srgb, var(--cyan) 40%, transparent));
}
-.app.light .log-viewer {
- background: #f1f5f9;
+.k8s-overview-card-accent.green {
+ background: linear-gradient(90deg, var(--green), color-mix(in srgb, var(--green) 40%, transparent));
}
-.app.light .log-content {
- color: #1e293b;
+.k8s-overview-card-accent.neutral {
+ background: linear-gradient(90deg, var(--text3), color-mix(in srgb, var(--text3) 40%, transparent));
}
-/* === Container exec terminal === */
-.exec-modal-body {
+/* K8s Table Wrapper */
+.k8s-table-wrap {
display: flex;
flex-direction: column;
gap: 0;
- padding: 0;
}
-.exec-toolbar {
+.k8s-table-info {
display: flex;
align-items: center;
- gap: 8px;
- padding: 10px 14px;
- border-bottom: 1px solid var(--border);
- background: var(--surface2);
+ gap: 6px;
+ font-size: 12px;
+ color: var(--text2);
+ font-weight: 500;
+ padding-bottom: 14px;
}
-.exec-toolbar .input {
- flex: 1;
- font-family: 'Fira Code', 'SF Mono', ui-monospace, monospace;
- font-size: 12px;
- background: var(--bg);
- color: var(--cyan);
- border-color: var(--border);
+.k8s-table-info-count {
+ font-weight: 700;
+ color: var(--text);
+ font-size: 13px;
}
-.exec-toolbar .input:focus {
- border-color: var(--purple);
- box-shadow: 0 0 0 3px var(--purple-dim);
+.k8s-table-info-sep {
+ width: 3px;
+ height: 3px;
+ border-radius: 50%;
+ background: var(--text3);
+ margin: 0 4px;
}
-.exec-output {
- background: #0a0c14;
- color: #c8d0e0;
- font-family: 'Fira Code', 'SF Mono', ui-monospace, monospace;
- font-size: 12px;
- line-height: 1.6;
- padding: 14px;
- min-height: 200px;
- max-height: 360px;
- overflow-y: auto;
- white-space: pre-wrap;
- word-break: break-all;
+.k8s-table-info-running {
+ color: var(--green);
+ font-weight: 600;
}
-.app.light .exec-output {
- background: #f0f2f6;
- color: #1e293b;
+/* K8s Tables */
+.k8s-table-scroll {
+ overflow-x: auto;
+ border: 1px solid var(--border);
+ border-radius: 10px;
}
-.exec-entry {
- margin-bottom: 8px;
+.k8s-table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ font-size: 13px;
}
-.exec-prompt {
- color: var(--green);
- font-weight: 600;
+.k8s-table thead th {
+ text-align: left;
+ padding: 10px 14px;
+ font-size: 10px;
+ font-weight: 700;
+ color: var(--text3);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ border-bottom: 1px solid var(--border);
+ white-space: nowrap;
+ background: color-mix(in srgb, var(--surface2) 50%, var(--surface));
+ position: sticky;
+ top: 0;
+ z-index: 1;
}
-.exec-result {
- color: #c8d0e0;
- margin-top: 2px;
+.k8s-table thead th:first-child {
+ border-top-left-radius: 10px;
}
-.app.light .exec-result {
- color: #334155;
+.k8s-table thead th:last-child {
+ border-top-right-radius: 10px;
}
-.exec-error-text {
- color: var(--red);
- margin-top: 2px;
+.k8s-table thead th.th-actions {
+ text-align: right;
+ padding-right: 18px;
}
-.exec-copy-bar {
- display: flex;
- align-items: center;
- gap: 8px;
+.k8s-table tbody tr {
+ transition: background 0.1s ease;
+}
+
+.k8s-table tbody tr:hover {
+ background: var(--hover);
+}
+
+.k8s-table tbody td {
padding: 10px 14px;
- border-top: 1px solid var(--border);
- background: var(--surface2);
- font-size: 11px;
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
+ white-space: nowrap;
+ vertical-align: middle;
color: var(--text2);
}
-.exec-copy-bar code {
- flex: 1;
- font-family: 'Fira Code', 'SF Mono', ui-monospace, monospace;
- font-size: 11px;
- color: var(--cyan);
+.k8s-table tbody td.td-name {
+ color: var(--text);
+ font-weight: 600;
+ max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
- white-space: nowrap;
}
-/* === VM Console === */
-.console-viewer {
- background: #0a0c14;
- border: 1px solid var(--border);
- border-radius: 10px;
- min-height: 300px;
- max-height: 480px;
- overflow: auto;
- position: relative;
+.k8s-table tbody td.td-age {
+ color: var(--text3);
+ font-size: 12px;
}
-.console-content {
- margin: 0;
- padding: 14px 16px;
- font-family: 'Fira Code', 'SF Mono', ui-monospace, monospace;
- font-size: 12px;
- line-height: 1.6;
- color: #34d399;
- white-space: pre-wrap;
- word-break: break-all;
- min-height: 260px;
+.k8s-table tbody td.td-actions {
+ text-align: right;
+ padding-right: 18px;
}
-.console-empty {
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 260px;
- color: var(--text3);
- font-size: 13px;
+.k8s-table tbody tr:last-child td {
+ border-bottom: none;
}
-.console-toolbar {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 10px 14px;
- border-bottom: 1px solid var(--border);
- background: var(--surface2);
+.k8s-table tbody tr:last-child td:first-child {
+ border-bottom-left-radius: 10px;
}
-.console-toolbar-right {
- margin-left: auto;
- display: flex;
+.k8s-table tbody tr:last-child td:last-child {
+ border-bottom-right-radius: 10px;
+}
+
+/* Namespace Badge */
+.k8s-ns-badge {
+ display: inline-flex;
align-items: center;
- gap: 8px;
+ padding: 2px 8px;
+ border-radius: 6px;
+ font-size: 11px;
+ font-weight: 600;
+ background: var(--surface2);
+ color: var(--text2);
}
-.console-auto-scroll {
- display: flex;
+/* Pod Status */
+.k8s-pod-status {
+ display: inline-flex;
align-items: center;
gap: 6px;
+ padding: 3px 10px;
+ border-radius: 20px;
font-size: 11px;
- color: var(--text2);
- cursor: pointer;
- user-select: none;
+ font-weight: 600;
}
-.console-auto-scroll input[type="checkbox"] {
- appearance: auto;
- -webkit-appearance: auto;
- width: 14px;
- height: 14px;
- cursor: pointer;
- accent-color: var(--purple);
+.k8s-pod-status.running {
+ background: rgba(52, 211, 153, 0.1);
+ color: var(--green);
}
-.app.light .console-viewer {
- background: #f1f5f9;
+.k8s-pod-status.pending {
+ background: rgba(251, 191, 36, 0.1);
+ color: #fbbf24;
}
-.app.light .console-content {
- color: #059669;
+.k8s-pod-status.failed,
+.k8s-pod-status.error {
+ background: var(--red-dim);
+ color: var(--red);
}
-/* === Tabs === */
-.tabs {
- display: flex;
- gap: 4px;
- margin-bottom: 20px;
- border-bottom: 1px solid var(--border);
+.k8s-pod-status .status-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: currentColor;
+ box-shadow: 0 0 6px currentColor;
}
-.tab {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 10px 16px;
- border: none;
- background: transparent;
- color: var(--text2);
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- border-bottom: 2px solid transparent;
- transition: all 0.2s ease;
- margin-bottom: -1px;
+.k8s-pod-status.running .status-dot {
+ animation: k8s-pulse 2s ease-in-out infinite;
}
-.tab:hover {
- color: var(--text);
- background: var(--hover);
+@keyframes k8s-pulse {
+ 0%, 100% { opacity: 1; box-shadow: 0 0 4px currentColor; }
+ 50% { opacity: 0.6; box-shadow: 0 0 8px currentColor; }
}
-.tab.active {
- color: var(--purple);
- border-bottom-color: var(--purple);
+/* Restarts Badge */
+.k8s-restarts {
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+}
+
+.k8s-restarts.warn {
+ color: #fbbf24;
}
-.tab .icon {
- width: 18px;
- height: 18px;
- display: flex;
+/* Service Type Badge */
+.k8s-svc-type {
+ display: inline-flex;
align-items: center;
- justify-content: center;
+ padding: 2px 8px;
+ border-radius: 6px;
+ font-size: 11px;
+ font-weight: 600;
+ background: var(--purple-dim);
+ color: var(--purple);
}
-.tab .icon svg {
- width: 18px;
- height: 18px;
- stroke: currentColor;
- fill: none;
- stroke-width: 2;
+.k8s-svc-type.nodeport {
+ background: var(--cyan-dim);
+ color: var(--cyan);
}
-/* === Responsive === */
-
-/* Large screens — cap content width to prevent over-stretching on ultra-wide monitors */
-@media (min-width: 1600px) {
- .content {
- max-width: 1440px;
- margin: 0 auto;
- }
+.k8s-svc-type.loadbalancer {
+ background: rgba(52, 211, 153, 0.1);
+ color: var(--green);
}
-/* Medium-large screens — reduce grid density */
-@media (max-width: 1200px) {
- .dash-cards { grid-template-columns: repeat(3, 1fr); }
- .grid3 { grid-template-columns: 1fr 1fr; }
+/* Deployment Ready/Available Badge */
+.k8s-dep-ready, .k8s-dep-avail {
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
}
-@media (max-width: 900px) {
- .grid3 { grid-template-columns: 1fr; }
- .img-layout { grid-template-columns: 1fr; }
- .vm-layout { grid-template-columns: 1fr; }
- .img-sidebar { order: -1; }
- .vm-card-specs { grid-template-columns: repeat(2, 1fr); }
+.k8s-dep-ready.ok, .k8s-dep-avail.ok {
+ color: var(--green);
}
-@media (max-width: 700px) {
- .sidebar { width: 56px; min-width: 56px; }
- .sidebar-header .brand-name,
- .sidebar-header .brand-version,
- .nav-label,
- .nav-badge,
- .nav-count { display: none; }
- .sidebar-header { justify-content: center; padding: 16px 0; }
- .nav-item { justify-content: center; padding: 10px; }
- .card-meta, .card-status span { display: none; }
- .dash-cards { grid-template-columns: 1fr; }
- .grid2 { grid-template-columns: 1fr; }
- .grid3 { grid-template-columns: 1fr; }
- .settings { max-width: 100%; }
- .input { min-width: 0; width: 100%; }
- .toolbar { flex-wrap: wrap; }
- .toolbar .input { flex: 1; min-width: 120px; }
- .img-layout { grid-template-columns: 1fr; }
- .vm-layout { grid-template-columns: 1fr; }
- .vm-card-specs { grid-template-columns: repeat(2, 1fr); }
- .vm-card-actions { flex-wrap: wrap; }
- .input-action-row { flex-wrap: wrap; }
- .input-action-row .input { min-width: 100%; }
+.k8s-dep-ready.warn, .k8s-dep-avail.warn {
+ color: var(--red);
}
-/* === Resource stats bars === */
-.stats-bar-row {
+/* K8s Empty State */
+.k8s-empty {
display: flex;
- gap: 12px;
+ flex-direction: column;
align-items: center;
+ justify-content: center;
+ padding: 56px 20px;
+ gap: 10px;
}
-.stats-bar-item {
+.k8s-empty-icon {
+ width: 52px;
+ height: 52px;
+ border-radius: 14px;
+ background: var(--surface2);
display: flex;
align-items: center;
- gap: 6px;
- min-width: 0;
+ justify-content: center;
+ margin-bottom: 4px;
}
-.stats-bar-label {
- font-size: 10px;
- font-weight: 700;
- color: var(--text3);
- text-transform: uppercase;
- letter-spacing: 0.3px;
- white-space: nowrap;
- min-width: 28px;
+.k8s-empty-icon svg {
+ width: 24px;
+ height: 24px;
+ stroke: var(--text3);
+ fill: none;
+ stroke-width: 1.5;
+ stroke-linecap: round;
+ stroke-linejoin: round;
}
-.stats-bar-track {
- width: 60px;
- height: 4px;
- border-radius: 2px;
- background: var(--surface2);
- overflow: hidden;
- flex-shrink: 0;
+.k8s-empty-text {
+ font-size: 14px;
+ color: var(--text2);
+ font-weight: 600;
}
-.stats-bar-fill {
- height: 100%;
- border-radius: 2px;
- transition: width 0.4s ease, background 0.4s ease;
- min-width: 0;
+.k8s-empty-sub {
+ font-size: 12px;
+ color: var(--text3);
+ font-weight: 400;
}
-.stats-bar-value {
- font-size: 10px;
- font-weight: 600;
- color: var(--text2);
- white-space: nowrap;
- min-width: 36px;
- font-family: 'Fira Code', 'SF Mono', ui-monospace, monospace;
+/* K8s Namespace Selector */
+.k8s-ns-select {
+ background: var(--surface);
+ color: var(--text);
+ border: 1px solid var(--border);
+ height: 32px;
+ padding: 6px 10px;
+ border-radius: 8px;
+ outline: none;
+ font-size: 12px;
+ font-family: inherit;
+ font-weight: 500;
+ cursor: pointer;
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
-/* Container card with stats layout */
-.container-card-with-stats {
- flex-direction: column;
- gap: 0;
- padding: 0;
+.k8s-ns-select:focus {
+ border-color: var(--purple);
+ box-shadow: 0 0 0 3px var(--purple-dim);
}
-.container-card-with-stats .container-card-main {
+/* K8s Logs Modal */
+.k8s-logs-modal {
+ width: min(920px, 96vw);
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(139, 92, 246, 0.1);
+ overflow: hidden;
display: flex;
- align-items: center;
- gap: 14px;
- padding: 12px 14px;
- width: 100%;
- box-sizing: border-box;
-}
-
-.container-card:not(.container-card-with-stats) .container-card-main {
- display: contents;
+ flex-direction: column;
+ max-height: 82vh;
}
-.container-card-stats {
+.k8s-logs-header {
display: flex;
- gap: 16px;
- padding: 10px 14px;
- border-top: 1px solid var(--border);
- background: var(--surface2);
- border-radius: 0 0 10px 10px;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 20px;
+ border-bottom: 1px solid var(--border);
+ gap: 12px;
+ background: color-mix(in srgb, var(--purple) 3%, var(--surface));
}
-.stat-item {
+.k8s-logs-title {
display: flex;
align-items: center;
- gap: 6px;
- font-size: 12px;
- color: var(--text2);
+ gap: 10px;
+ min-width: 0;
}
-.stat-icon {
- width: 16px;
- height: 16px;
+.k8s-logs-title-icon {
+ width: 30px;
+ height: 30px;
+ border-radius: 8px;
+ background: var(--purple-dim);
display: flex;
align-items: center;
justify-content: center;
- opacity: 0.6;
+ flex-shrink: 0;
}
-.stat-icon svg {
- width: 14px;
- height: 14px;
- stroke: currentColor;
+.k8s-logs-title-icon svg {
+ width: 15px;
+ height: 15px;
+ stroke: var(--purple);
fill: none;
stroke-width: 2;
}
-.stat-value {
- font-weight: 600;
+.k8s-logs-title-text {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.k8s-logs-title h3 {
+ margin: 0;
+ font-size: 13px;
+ font-weight: 700;
color: var(--text);
- font-family: 'SF Mono', 'Consolas', monospace;
+ line-height: 1.2;
}
-/* VM item stats */
-.vm-item-stats {
- margin-top: 6px;
+.k8s-logs-title .logs-pod-name {
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 11px;
+ color: var(--text3);
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 280px;
}
-/* Dashboard resources panel */
-.dash-resources-panel {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 12px;
- padding: 16px;
- margin-bottom: 8px;
+.k8s-logs-actions {
+ display: flex;
+ gap: 6px;
+ align-items: center;
+ flex-shrink: 0;
}
-.dash-resources-panel .section-title {
- margin-bottom: 12px;
- margin-top: 0;
+.k8s-logs-actions-sep {
+ width: 1px;
+ height: 18px;
+ background: var(--border);
+ margin: 0 4px;
}
-.dash-resources-bars {
+.k8s-logs-body {
+ flex: 1;
+ overflow: auto;
+ background: #080a12;
+ min-height: 200px;
+ max-height: 64vh;
+}
+
+.k8s-logs-content {
+ margin: 0;
+ padding: 18px 20px;
+ white-space: pre-wrap;
+ word-break: break-all;
+ color: #c4c9d4;
+ font-family: 'Geist Mono', ui-monospace, monospace;
+ font-size: 12px;
+ line-height: 1.7;
+ tab-size: 4;
+}
+
+.k8s-logs-loading {
display: flex;
- flex-direction: column;
+ align-items: center;
+ justify-content: center;
gap: 10px;
+ padding: 48px;
+ color: var(--text3);
+ font-size: 13px;
}
-.dash-resources-bars .stats-bar-item {
- gap: 10px;
+.k8s-toolbar-sep {
+ width: 1px;
+ height: 20px;
+ background: var(--border);
+ margin: 0 4px;
+ flex-shrink: 0;
}
-.dash-resources-bars .stats-bar-label {
- min-width: 50px;
+.k8s-stat-kubeconfig {
+ font-size: 11px;
+ word-break: break-all;
}
-.dash-resources-bars .stats-bar-track {
- width: 100%;
- flex: 1;
- height: 6px;
- border-radius: 3px;
+.k8s-overview-card.no-click {
+ cursor: default;
}
-.dash-resources-bars .stats-bar-value {
- min-width: 60px;
- text-align: right;
+.k8s-overview-card.no-click:hover {
+ border-color: var(--border);
+ box-shadow: none;
+ transform: none;
+}
+
+.k8s-overview-card.no-click:hover .k8s-overview-card-accent {
+ opacity: 0;
+}
+
+.k8s-stat-installed {
+ color: var(--green);
+}
+
+.k8s-stat-not-installed {
+ color: var(--text3);
}
@media (prefers-reduced-motion: reduce) {
diff --git a/crates/cratebay-gui/src/App.tsx b/crates/cratebay-gui/src/App.tsx
index 35f4386..f534bac 100644
--- a/crates/cratebay-gui/src/App.tsx
+++ b/crates/cratebay-gui/src/App.tsx
@@ -337,7 +337,9 @@ function App() {
- {renderPage()}
+
+ {renderPage()}
+
diff --git a/crates/cratebay-gui/src/assets/fonts/GeistMono-Variable.woff2 b/crates/cratebay-gui/src/assets/fonts/GeistMono-Variable.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..504be8626156df9ac8d3defd4b584ee42b80170d
GIT binary patch
literal 31368
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
bc`|gPtUlrE67$kiG_iB-ycPZ
zrnSod{~1X~9JR1jcO9N*G!Y_XLCDI$k!ewy-C@jvfE8hbSyiLPP+Rh3wz}0*W$MVZ
zff2H+rZJ`AXIc_9Aqk{a2dtUgf!s3ab_hw+EeNCa0>NdA&Tl;M!V?wp(kY`ypu=TC
z*>6L;alkYF{^fi|yFG3={cpkCCM0qU_`)~IRfO+jsQVFPiy->cV>2WcQ$HRmd&9TSYU}>k<>=&DX|t=
z23CL|W@F_?Yy|XSt;JXLDHdWj7AC{fbpC5;TA-z&2G!JJ(x~0V?vmVHl1scv8*M5g
zTk$^_e=t$_`@r^@IR2e7QDMxO`OoXpdhe4y2xtI3#*i1$KI9g=%&O3;68Z224vyh6D`p)=NtBty-{U@5vp*cjyO-m@Y=#YIw8gtDrNTy2E`
z6q@7F0oz*Q8_LaMCUFA6G~n&J$nR8T#kEGou!6_te{yR5q?VI9fOW7;8KAa2{9x{fFKQ_!lU(XYO6mp`E| 8aXB| ;Q#OEb_=CI
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 |