diff --git a/crates/cratebay-daemon/src/main.rs b/crates/cratebay-daemon/src/main.rs index 308c6ec..22383f0 100644 --- a/crates/cratebay-daemon/src/main.rs +++ b/crates/cratebay-daemon/src/main.rs @@ -1,3 +1,9 @@ +// 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/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/package-lock.json b/crates/cratebay-gui/package-lock.json index 4fa003c..b2cbf7c 100644 --- a/crates/cratebay-gui/package-lock.json +++ b/crates/cratebay-gui/package-lock.json @@ -11,8 +11,14 @@ "@tauri-apps/api": "^2.10.1", "@tauri-apps/cli": "^2.10.0", "@tauri-apps/plugin-dialog": "^2.6.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.576.0", + "radix-ui": "^1.4.3", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -1105,6 +1111,44 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://mirrors.tencent.com/npm/@humanfs/core/-/core-0.19.1.tgz", @@ -1196,91 +1240,1589 @@ "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.0.1" + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://mirrors.tencent.com/npm/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://mirrors.tencent.com/npm/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://mirrors.tencent.com/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://mirrors.tencent.com/npm/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "dev": true, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.34.0" + "use-sync-external-store": "^1.5.0" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://mirrors.tencent.com/npm/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", - "dev": true, - "dependencies": { - "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://mirrors.tencent.com/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://mirrors.tencent.com/npm/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://mirrors.tencent.com/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "engines": { - "node": ">=6.0.0" + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://mirrors.tencent.com/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://mirrors.tencent.com/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://mirrors.tencent.com/npm/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -2125,7 +3667,7 @@ "version": "19.2.14", "resolved": "https://mirrors.tencent.com/npm/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "dependencies": { "csstype": "^3.2.2" } @@ -2134,7 +3676,7 @@ "version": "19.2.3", "resolved": "https://mirrors.tencent.com/npm/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2680,6 +4222,18 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://mirrors.tencent.com/npm/aria-query/-/aria-query-5.3.0.tgz", @@ -2883,6 +4437,27 @@ "node": ">=8" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://mirrors.tencent.com/npm/color-convert/-/color-convert-2.0.1.tgz", @@ -2980,7 +4555,7 @@ "version": "3.2.3", "resolved": "https://mirrors.tencent.com/npm/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true + "devOptional": true }, "node_modules/data-urls": { "version": "6.0.1", @@ -3057,6 +4632,12 @@ "node": ">=6" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://mirrors.tencent.com/npm/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -3492,6 +5073,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://mirrors.tencent.com/npm/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4055,6 +5645,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.576.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.576.0.tgz", + "integrity": "sha512-koNxU14BXrxUfZQ9cUaP0ES1uyPZKYDjk31FQZB6dQ/x+tXk979sVAn9ppZ/pVeJJyOxVM8j1E+8QEuSc02Vug==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://mirrors.tencent.com/npm/lz-string/-/lz-string-1.5.0.tgz", @@ -4373,6 +5972,83 @@ "node": ">=6" } }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://mirrors.tencent.com/npm/react/-/react-19.2.4.tgz", @@ -4410,6 +6086,75 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://mirrors.tencent.com/npm/redent/-/redent-3.0.0.tgz", @@ -4569,6 +6314,16 @@ "node": ">=8" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://mirrors.tencent.com/npm/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4678,6 +6433,16 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://mirrors.tencent.com/npm/tinybench/-/tinybench-2.9.0.tgz", @@ -4811,6 +6576,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://mirrors.tencent.com/npm/type-check/-/type-check-0.4.0.tgz", @@ -4908,6 +6679,58 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://mirrors.tencent.com/npm/vite/-/vite-7.3.1.tgz", diff --git a/crates/cratebay-gui/package.json b/crates/cratebay-gui/package.json index 4f18b4b..afea2b0 100644 --- a/crates/cratebay-gui/package.json +++ b/crates/cratebay-gui/package.json @@ -17,8 +17,14 @@ "@tauri-apps/api": "^2.10.1", "@tauri-apps/cli": "^2.10.0", "@tauri-apps/plugin-dialog": "^2.6.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.576.0", + "radix-ui": "^1.4.3", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0" }, "devDependencies": { "@eslint/js": "^9.39.1", 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/src/lib.rs b/crates/cratebay-gui/src-tauri/src/lib.rs index 06158ef..e7a4c2f 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::(); @@ -1460,7 +1482,7 @@ async fn volume_list() -> Result, String> { .map_err(|e| e.to_string())?; let volumes = resp.volumes.unwrap_or_default(); - Ok(volumes + let mut out: Vec = volumes .into_iter() .map(|v| VolumeInfo { name: v.name, @@ -1471,7 +1493,10 @@ async fn volume_list() -> Result, String> { options: v.options, scope: v.scope.map(|s| format!("{:?}", s)).unwrap_or_default(), }) - .collect()) + .collect(); + // Docker doesn't guarantee ordering; keep it stable to avoid UI jitter on refresh. + out.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(out) } #[derive(Debug, Serialize)] diff --git a/crates/cratebay-gui/src-tauri/tauri.conf.json b/crates/cratebay-gui/src-tauri/tauri.conf.json index fe74252..3a4473a 100644 --- a/crates/cratebay-gui/src-tauri/tauri.conf.json +++ b/crates/cratebay-gui/src-tauri/tauri.conf.json @@ -10,22 +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, - "tooltip": "CrateBay" - }, "security": { "csp": "default-src 'self'; img-src 'self' https: data:; script-src 'self'; style-src 'self' 'unsafe-inline'" } 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..9570312 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; @@ -193,12 +192,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 +229,10 @@ gap: 6px; } +.platform-windows .topbar-right { + -webkit-app-region: no-drag; +} + .status-pill { display: flex; align-items: center; @@ -233,6 +245,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; @@ -251,53 +305,75 @@ padding: 20px 24px; } +/* Page transition */ +@keyframes pageEnter { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: none; } +} +.page-transition { + animation: pageEnter 0.2s cubic-bezier(0.22, 1, 0.36, 1); + will-change: opacity; +} + /* === 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 { @@ -310,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 { @@ -344,7 +415,7 @@ } .dash-card-label { - font-size: 12px; + font-size: 13px; font-weight: 500; color: var(--text2); } @@ -355,1905 +426,4699 @@ 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-card-footer { - margin-top: auto; - padding-top: 12px; - border-top: 1px solid var(--border); +.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 .dash-card-label { - white-space: nowrap; +.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; } -.card-name { - font-size: 14px; - font-weight: 600; - color: var(--text); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.view-all:hover { + background: var(--purple-dim); } -.card-meta { +/* Section label */ +.section-title { font-size: 11px; - color: var(--text2); - margin-top: 2px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + font-weight: 600; + color: var(--text3); + text-transform: uppercase; + letter-spacing: 0.8px; + margin-bottom: 10px; + padding-left: 2px; } -.card-status { - display: flex; - align-items: center; - gap: 5px; - flex-shrink: 0; - margin-right: 8px; -} +.section-title:not(:first-child) { margin-top: 24px; } -.card-status .dot { - width: 8px; - height: 8px; +/* === Running Section (Dashboard) === */ +.dash-running-section { + margin-top: 20px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 14px; + overflow: hidden; } -.card-status .dot.running { box-shadow: 0 0 6px var(--green); } - -.card-status span { - font-size: 11px; - font-weight: 500; - color: var(--text2); +.dash-section-header { + display: flex; + align-items: center; + 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; - border-radius: 10px; - background: var(--purple-dim); +/* -- Card main row -- */ +.card-main { display: flex; align-items: center; - justify-content: center; - flex-shrink: 0; + gap: 14px; + padding: 14px 16px; } -.setting-icon svg { +.container-card .card-icon { + width: 38px; + height: 38px; + 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 { 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; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; + justify-content: center; +} + +.empty-state .empty-icon svg { + width: 24px; height: 24px; + stroke: var(--purple); fill: none; + stroke-width: 2; +} + +.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); } } +.spinner-sm { width: 14px; height: 14px; } + +.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 8px 32px rgba(0,0,0,0.25), 0 0 0 1px rgba(0,0,0,0.08); + 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; + gap: 8px; +} + +.modal-title-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 6px; + background: var(--purple-dim); + margin-right: 8px; + flex-shrink: 0; +} +.modal-title-icon svg { + width: 12px; + height: 12px; + stroke: var(--purple); + fill: none; + stroke-width: 2.5; +} + +/* === Run Container Modal === */ +.run-modal-backdrop { + align-items: center; + justify-content: center; + overflow-y: auto; +} + +.run-modal { + max-width: 520px; + width: min(520px, 96vw); +} + +.run-modal-backdrop .run-modal { + margin-top: 0; + margin-bottom: 0; + max-height: calc(100vh - 40px); + display: flex; + flex-direction: column; +} + +.run-modal-body { + padding: 0 !important; + flex: 1; + min-height: 0; + max-height: none; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; +} +.run-modal-body::-webkit-scrollbar { + width: 4px; +} +.run-modal-body::-webkit-scrollbar-track { + background: transparent; +} +.run-modal-body::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} +.run-modal-body::-webkit-scrollbar-thumb:hover { + background: var(--text3); +} +.run-modal-body .form { + gap: 0; +} + +.run-section { + padding: 14px 18px; + border-bottom: 1px solid var(--border); +} +.run-section:last-child { + border-bottom: none; +} + +.run-section-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; + font-weight: 800; + color: var(--text); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 12px; +} + +.run-section-icon { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 5px; + background: var(--purple-dim); + flex-shrink: 0; +} +.run-section-icon svg { + width: 12px; + height: 12px; + stroke: var(--purple); + fill: none; + stroke-width: 2; +} + +.run-field { + display: flex; + flex-direction: column; + gap: 5px; + margin-bottom: 10px; +} +.run-field:last-child { + margin-bottom: 0; +} +.run-field label { + font-size: 11px; + font-weight: 700; + color: var(--text2); +} +.run-field .input { + width: 100%; +} +.run-required { + color: var(--red); + margin-left: 2px; +} + +.run-resources-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.run-toggle-row { + display: flex; + align-items: center; + gap: 10px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px dashed color-mix(in srgb, var(--border) 60%, transparent); +} +.run-toggle-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; +} +.run-toggle-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; +} +.run-toggle-row input[type="checkbox"]:checked { + background: var(--purple); + border-color: var(--purple); +} +.run-toggle-row input[type="checkbox"]:checked::after { + transform: translateX(16px); + background: white; +} +.run-toggle-row label { + font-size: 12px; + color: var(--text2); + cursor: pointer; + user-select: none; +} + +.run-env-hint { + font-size: 11px; + color: var(--text3); + margin-bottom: 10px; + line-height: 1.4; +} + +.run-env-list { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 10px; +} + +.run-env-row { + display: flex; + align-items: center; + gap: 6px; +} +.run-env-row .input { + flex: 1; + min-width: 0; +} +.run-env-eq { + color: var(--text3); + font-weight: 700; + font-size: 13px; + flex-shrink: 0; + width: 14px; + text-align: center; +} +.run-env-delete { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 6px; + border: 1px solid transparent; + background: transparent; + color: var(--text3); + cursor: pointer; + transition: all 0.15s ease; + flex-shrink: 0; + padding: 0; +} +.run-env-delete svg { + width: 14px; + height: 14px; + stroke: currentColor; + fill: none; + stroke-width: 2; +} +.run-env-delete:hover { + color: var(--red); + background: var(--red-dim); + border-color: color-mix(in srgb, var(--red) 30%, transparent); +} + +.run-env-add { + align-self: flex-start; +} + +.run-error { + display: flex; + align-items: center; + gap: 8px; + margin: 0 18px 14px; + padding: 8px 12px; + background: var(--red-dim); + border: 1px solid color-mix(in srgb, var(--red) 25%, transparent); + border-radius: 8px; + color: var(--red); + font-size: 12px; + line-height: 1.4; +} +.run-error-icon { + flex-shrink: 0; + display: flex; +} +.run-error-icon svg { + width: 16px; + height: 16px; + stroke: var(--red); + fill: none; + stroke-width: 2; +} + +.run-result { + margin: 0 18px 14px; + padding: 12px; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 10px; +} +.run-result-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + font-weight: 800; + color: var(--text2); + margin-bottom: 8px; +} +.run-result-title svg { + width: 14px; + height: 14px; + stroke: var(--cyan); + fill: none; + stroke-width: 2; +} +.run-result-code { + display: flex; + gap: 8px; + align-items: center; +} +.run-result-code code { + flex: 1; + display: block; + padding: 8px 10px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--cyan); + font-family: 'Geist Mono', ui-monospace, monospace; + font-size: 11px; + overflow-x: auto; + word-break: break-all; +} + +.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; } -.error-msg-title { - font-size: 15px; +.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); +.modal-head-icon.purple { + background: var(--purple-dim); +} + +.modal-head-icon.green { + background: rgba(52, 211, 153, 0.1); +} + +.modal-head-icon-svg { + width: 16px; + height: 16px; + display: flex; +} + +.modal-head-icon-svg svg { + width: 16px; + height: 16px; + stroke: currentColor; fill: none; stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; } -.panel.full-width { - grid-column: 1 / -1; +.modal-head-icon.purple .modal-head-icon-svg { + color: var(--purple); +} + +.modal-head-icon.green .modal-head-icon-svg { + color: var(--green); } -.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); +.modal-title-lg { + font-weight: 900; + font-size: 14px; } -.hint { +.modal-title-sub { + font-weight: 400; color: var(--text2); - font-size: 12px; - line-height: 1.5; + margin-left: 6px; } -/* 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; +.modal-body-padded { + padding: 20px; } -.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-footer-padded { + padding: 14px 18px; + gap: 8px; } -.form .row.inline input[type="checkbox"]:checked, -.setting-row input[type="checkbox"]:checked { - background: var(--purple); - border-color: var(--purple); +/* Create VM form enhancements */ +.form-spaced { + gap: 16px; } -.form .row.inline input[type="checkbox"]:checked::after, -.setting-row input[type="checkbox"]:checked::after { - transform: translateX(16px); - background: white; +.input-lg { + height: 36px; } -.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); +/* 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; - overflow: hidden; } -.img-card-bottom { - display: flex; - align-items: center; - justify-content: space-between; - margin-top: 12px; - padding-top: 10px; - border-top: 1px solid var(--border); - gap: 8px; +.os-image-progress-track { + height: 5px; + border-radius: 3px; + background: var(--border); + overflow: hidden; } -.img-card-stats { - display: flex; - gap: 12px; +.os-image-progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--purple), var(--cyan)); + transition: width 0.3s ease; + border-radius: 3px; } -.img-stat { +.os-image-progress-text { + font-size: 10px; + color: var(--text3); + margin-top: 3px; display: flex; - align-items: center; - gap: 4px; - font-size: 11px; - color: var(--text2); - font-weight: 500; + justify-content: space-between; } -.img-stat-icon { - width: 13px; - height: 13px; - stroke: var(--text3); - fill: none; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; +.os-image-progress-pct { + font-weight: 600; + font-family: 'Geist Mono', ui-monospace, monospace; } -.img-card-actions { +/* OS image actions */ +.os-image-actions { display: flex; gap: 6px; + align-items: center; flex-shrink: 0; } -/* === VMs page === */ -.vm-list { +.os-image-downloading { + font-size: 11px; + font-weight: 600; + color: var(--purple); display: flex; - flex-direction: column; - gap: 8px; + align-items: center; + gap: 4px; } -.vm-item { - background: var(--surface); - border: 1px solid var(--border); - border-radius: 12px; - overflow: hidden; - transition: border-color 0.2s, box-shadow 0.2s; +.os-image-downloading .spinner { + width: 12px; + height: 12px; + border-width: 1.5px; } -.vm-item:hover { - border-color: color-mix(in srgb, var(--purple) 30%, transparent); +.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; + align-items: center; + gap: 4px; } -.vm-item-expanded { +.os-image-ready-badge .dot { + width: 5px; + height: 5px; +} + +.btn-download { + color: var(--purple); border-color: var(--purple); - box-shadow: 0 0 0 1px var(--purple-dim); } -.vm-item-running { - border-left: 3px solid var(--green); +/* 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-row { +/* Form label with icon */ +.form-label-icon { display: flex; align-items: center; - gap: 12px; - padding: 14px 16px; - cursor: pointer; - user-select: none; + gap: 4px; } -.vm-item-row:hover { - background: var(--hover); +/* Create button spinner */ +.btn-spinner { + width: 14px; + height: 14px; + border-width: 1.5px; + margin-right: 4px; } -.vm-icon { - width: 36px; - height: 36px; +/* Console modal enhancements */ +.console-viewer-padded { + padding: 0 14px 14px; +} + +.console-viewer-lg { border-radius: 10px; + min-height: 320px; + max-height: 500px; +} + +.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 ab61424..f534bac 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,10 +327,19 @@ function App() { {containers.connected ? t("connected") : t("disconnected")}
+ {isWindows && ( +
+ + + +
+ )}
- {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 0000000..504be86 Binary files /dev/null and b/crates/cratebay-gui/src/assets/fonts/GeistMono-Variable.woff2 differ diff --git a/crates/cratebay-gui/src/assets/fonts/GeistSans-Variable.woff2 b/crates/cratebay-gui/src/assets/fonts/GeistSans-Variable.woff2 new file mode 100644 index 0000000..93552f5 Binary files /dev/null and b/crates/cratebay-gui/src/assets/fonts/GeistSans-Variable.woff2 differ diff --git a/crates/cratebay-gui/src/components/ui/alert-dialog.tsx b/crates/cratebay-gui/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..dad3273 --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/alert-dialog.tsx @@ -0,0 +1,196 @@ +"use client" + +import * as React from "react" +import { AlertDialog as AlertDialogPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" +}) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogAction({ + className, + variant = "default", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + variant = "outline", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +} diff --git a/crates/cratebay-gui/src/components/ui/alert.tsx b/crates/cratebay-gui/src/components/ui/alert.tsx new file mode 100644 index 0000000..f99164e --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/crates/cratebay-gui/src/components/ui/badge.tsx b/crates/cratebay-gui/src/components/ui/badge.tsx new file mode 100644 index 0000000..6eb2a05 --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/badge.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", + outline: + "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + link: "text-primary underline-offset-4 [a&]:hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/crates/cratebay-gui/src/components/ui/button.tsx b/crates/cratebay-gui/src/components/ui/button.tsx new file mode 100644 index 0000000..4d38506 --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/button.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/crates/cratebay-gui/src/components/ui/card.tsx b/crates/cratebay-gui/src/components/ui/card.tsx new file mode 100644 index 0000000..acf57dc --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/crates/cratebay-gui/src/components/ui/checkbox.tsx b/crates/cratebay-gui/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..98ac83e --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import { CheckIcon } from "lucide-react" +import { Checkbox as CheckboxPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/crates/cratebay-gui/src/components/ui/collapsible.tsx b/crates/cratebay-gui/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..2f7a4e7 --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/collapsible.tsx @@ -0,0 +1,33 @@ +"use client" + +import { Collapsible as CollapsiblePrimitive } from "radix-ui" + +function Collapsible({ + ...props +}: React.ComponentProps) { + return +} + +function CollapsibleTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CollapsibleContent({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/crates/cratebay-gui/src/components/ui/dialog.tsx b/crates/cratebay-gui/src/components/ui/dialog.tsx new file mode 100644 index 0000000..e5f0a96 --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/dialog.tsx @@ -0,0 +1,156 @@ +import * as React from "react" +import { XIcon } from "lucide-react" +import { Dialog as DialogPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean +}) { + return ( +
+ {children} + {showCloseButton && ( + + + + )} +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/crates/cratebay-gui/src/components/ui/input.tsx b/crates/cratebay-gui/src/components/ui/input.tsx new file mode 100644 index 0000000..f1124ae --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/crates/cratebay-gui/src/components/ui/progress.tsx b/crates/cratebay-gui/src/components/ui/progress.tsx new file mode 100644 index 0000000..4954fb0 --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/progress.tsx @@ -0,0 +1,36 @@ +"use client" + +import * as React from "react" +import { Progress as ProgressPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Progress({ + className, + value, + style, + ...props +}: React.ComponentProps & { style?: React.CSSProperties }) { + return ( + + )?.["--progress-color"] || undefined, + }} + /> + + ) +} + +export { Progress } diff --git a/crates/cratebay-gui/src/components/ui/scroll-area.tsx b/crates/cratebay-gui/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..73c4eb1 --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/scroll-area.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import { ScrollArea as ScrollAreaPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/crates/cratebay-gui/src/components/ui/select.tsx b/crates/cratebay-gui/src/components/ui/select.tsx new file mode 100644 index 0000000..c0dc712 --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/select.tsx @@ -0,0 +1,190 @@ +"use client" + +import * as React from "react" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" +import { Select as SelectPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "item-aligned", + align = "center", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/crates/cratebay-gui/src/components/ui/separator.tsx b/crates/cratebay-gui/src/components/ui/separator.tsx new file mode 100644 index 0000000..db91e97 --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/separator.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import { Separator as SeparatorPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/crates/cratebay-gui/src/components/ui/sonner.tsx b/crates/cratebay-gui/src/components/ui/sonner.tsx new file mode 100644 index 0000000..476216a --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/sonner.tsx @@ -0,0 +1,35 @@ +import { + CircleCheckIcon, + InfoIcon, + Loader2Icon, + OctagonXIcon, + TriangleAlertIcon, +} from "lucide-react" +import { Toaster as Sonner, type ToasterProps } from "sonner" + +const Toaster = ({ theme = "light", ...props }: ToasterProps) => { + return ( + , + info: , + warning: , + error: , + loading: , + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)", + } as React.CSSProperties + } + {...props} + /> + ) +} + +export { Toaster } diff --git a/crates/cratebay-gui/src/components/ui/table.tsx b/crates/cratebay-gui/src/components/ui/table.tsx new file mode 100644 index 0000000..a47749b --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/table.tsx @@ -0,0 +1,114 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( + {envVars.map((ev, i) => ( - - + + ))} diff --git a/crates/cratebay-gui/src/pages/Dashboard.tsx b/crates/cratebay-gui/src/pages/Dashboard.tsx index eafae5c..b730b2a 100644 --- a/crates/cratebay-gui/src/pages/Dashboard.tsx +++ b/crates/cratebay-gui/src/pages/Dashboard.tsx @@ -102,103 +102,140 @@ export function Dashboard({ const hasRunning = running.length > 0 || runningVms.length > 0 + const memPercent = totals.totalMemoryLimitMb > 0 + ? (totals.totalMemoryUsageMb / totals.totalMemoryLimitMb) * 100 + : 0 + const cpuClamped = Math.min(totals.totalCpuPercent, 100) + return (
+ {/* Navigation overview cards */}
onNavigate("containers")}> -
{I.box}
-
+
+
{I.box}
+
+ {running.length > 0 + ? {running.length} {t("runningCount")} + : {t("noRunning") || "Idle"}} +
+
+
{containers.length}
{t("containers")}
-
- {running.length > 0 && {running.length} {t("runningCount")}} -
+
onNavigate("vms")}> -
{I.server}
-
+
+
{I.server}
+
+ {vmsRunningCount > 0 + ? {vmsRunningCount} {t("runningCount")} + : {t("noRunning") || "Idle"}} +
+
+
{vmsCount}
{t("vms")}
-
- {vmsCount > 0 && {vmsRunningCount} {t("runningCount")}} -
+
onNavigate("images")}> -
{I.layers}
-
+
+
{I.layers}
+
+ {imgResultsCount > 0 && {imgResultsCount} {t("searchResults")}} +
+
+
{installedImagesCount}
{t("images")}
-
- {imgResultsCount > 0 && {imgResultsCount} {t("searchResults")}} -
+
onNavigate("settings")}> -
{I.settings}
-
+
+
{I.settings}
+
+ + + {connected ? "Docker " + t("connected") : t("disconnected")} + +
+
+
{connected ? "OK" : "--"}
{t("system")}
-
- - {connected ? "Docker " + t("connected") : t("disconnected")} -
+
- {hasRunning && ( - <> -
-
- {I.cpu} + {/* Resource monitoring strip */} + {hasRunning && ( +
+
+
{I.cpu}
+
+
+ {t("cpuUsage")} + {totals.totalCpuPercent.toFixed(1)}%
-
-
{t("cpuUsage")}
-
{totals.totalCpuPercent.toFixed(1)}%
+
+
-
-
- {I.memory} -
-
-
{t("memoryUsage")}
-
- {totals.totalMemoryUsageMb.toFixed(0)} MB -
+
+
+
{I.memory}
+
+
+ {t("memoryUsage")} + {totals.totalMemoryUsageMb.toFixed(0)} / {totals.totalMemoryLimitMb.toFixed(0)} MB
-
-
- of {totals.totalMemoryLimitMb.toFixed(0)} MB -
+
+
- - )} -
- - {running.length > 0 && <> -
{t("running")} ({running.length})
- {running.slice(0, 5).map(c => ( -
-
{I.box}
-
-
{c.name}
-
{c.image} · {c.ports || c.id}
-
-
- - {c.status} +
+
+ )} + + {running.length > 0 && ( +
+
+
+
{I.play}
+ {t("running")} + {running.length}
+ {running.length > 5 && ( +
onNavigate("containers")}> + {t("viewAll")} {I.chevronRight} +
+ )}
- ))} - {running.length > 5 && ( -
onNavigate("containers")}> - {t("viewAll")} ({running.length}) +
+ {running.slice(0, 5).map((c, idx) => ( +
+
{idx + 1}
+
{I.box}
+
+
{c.name}
+
+ {c.image} + {c.ports && {c.ports}} +
+
+
+ + {c.status} +
+
+ ))}
- )} - } +
+ )}
) } diff --git a/crates/cratebay-gui/src/pages/Images.tsx b/crates/cratebay-gui/src/pages/Images.tsx index 079ba39..114b5f8 100644 --- a/crates/cratebay-gui/src/pages/Images.tsx +++ b/crates/cratebay-gui/src/pages/Images.tsx @@ -202,13 +202,12 @@ export function Images({ <>
setLocalFilter(e.target.value)} /> -
+
@@ -218,58 +217,77 @@ export function Images({
{localLoading ? ( -
{t("loading")}
+
{t("loading")}
) : filteredImages.length === 0 ? ( -
{t("noLocalImages")}
+
+
{I.layers}
+

{t("noLocalImages")}

+
) : ( - filteredImages.map((img, idx) => ( -
-
- {I.layers} -
-
-
- {img.repo_tags.length > 0 - ? img.repo_tags.join(", ") - : ":"} +
+ {filteredImages.map((img, idx) => ( +
+
+
{I.layers}
+
+
+ {img.repo_tags.length > 0 + ? img.repo_tags.join(", ") + : ":"} +
+
+ {t("imageId")}: {img.id.slice(0, 16)} + + {img.size_human} + + {formatCreated(img.created)} +
+
-
- {t("imageId")}: {img.id} · {t("imageSize")}: {img.size_human} · {t("imageCreated")}: {formatCreated(img.created)} +
+
+ + + + +
+
+
-
- - - - -
-
- )) + ))} +
)} )} @@ -313,7 +331,7 @@ export function Images({ {/* Search results - card list */} {imgResults.length === 0 ? (
-
{I.layers}
+
{I.globe}

{t("searchHint")}

Docker Hub · Quay.io · GitHub Container Registry

@@ -327,7 +345,7 @@ export function Images({ {r.official && {t("official")}}
{r.reference}
-
{r.description || "-"}
+ {r.description &&
{r.description}
}
@@ -341,7 +359,9 @@ export function Images({
- +
diff --git a/crates/cratebay-gui/src/pages/Kubernetes.tsx b/crates/cratebay-gui/src/pages/Kubernetes.tsx index 301e3d3..c109060 100644 --- a/crates/cratebay-gui/src/pages/Kubernetes.tsx +++ b/crates/cratebay-gui/src/pages/Kubernetes.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from "react" +import { type JSX, useState, useEffect, useCallback } from "react" import { invoke } from "@tauri-apps/api/core" import { I } from "../icons" import { ErrorInline } from "../components/ErrorDisplay" @@ -116,27 +116,32 @@ export function Kubernetes({ t }: KubernetesProps) { const isInstalled = status?.installed ?? false const isRunning = status?.running ?? false + const tabCounts: Record = { + overview: 0, + pods: pods.length, + services: services.length, + deployments: deployments.length, + } + + const statusClass = (s: string) => + s === "Running" ? "running" : s === "Pending" ? "pending" : "failed" + return (
{/* Toolbar */}
- {isRunning && ( <> -
+
)} -
+
{error && setError("")} />} {k8sError && setK8sError("")} />} {/* K3s Cluster Status Card */} -
-
- {I.kubernetes} -

{t("k3sCluster")}

- - +
+
+
+ {I.kubernetes} +
+
+

{t("k3sCluster")}

+
+ {status?.version ? `v${status.version}` : t("notInstalled")} +
+
+ + {isRunning ? t("running") : t("stopped")}
-
-
-
{t("clusterStatus")}
-
- {isInstalled ? ( - {t("installed")} - ) : ( - {t("notInstalled")} - )} +
+
+
+
{t("clusterStatus")}
+
+ {isInstalled ? ( + {t("installed")} + ) : ( + {t("notInstalled")} + )} +
-
-
-
{t("k3sVersion")}
-
- {status?.version || "-"} +
+
{t("k3sVersion")}
+
+ {status?.version || "-"} +
-
-
-
{t("nodeCount")}
-
- {isRunning ? status?.node_count ?? 0 : "-"} +
+
{t("nodeCount")}
+
+ {isRunning ? status?.node_count ?? 0 : "-"} +
-
-
-
{t("kubeconfig")}
-
- {status?.kubeconfig_path || "-"} +
+
{t("kubeconfig")}
+
+ {status?.kubeconfig_path || "-"} +
{/* Action Buttons */} -
+
{!isInstalled && ( - ))} -
- {k8sLoading && {t("loading")}} +
- {/* Overview Tab */} - {tab === "overview" && ( -
-
setTab("pods")}> -
{t("pods")}
-
{pods.length}
-
-
setTab("services")}> -
{t("services")}
-
{services.length}
-
-
setTab("deployments")}> -
{t("deployments")}
-
{deployments.length}
-
-
-
{t("namespace")}
-
{namespaces.length}
+ {/* Tabs */} +
+ {(["overview", "pods", "services", "deployments"] as K8sTab[]).map((t2) => { + const tabIcons: Record = { + overview: I.dashboard, + pods: I.box, + services: I.globe, + deployments: I.layers, + } + return ( + + ) + })} +
+ +
+ {/* Overview Tab */} + {tab === "overview" && ( +
+
setTab("pods")}> +
+
{I.box}
+
{t("pods")}
+
+
+
{pods.length}
+
+ {pods.filter(p => p.status === "Running").length} {t("running").toLowerCase()} +
+
+
+
+
setTab("services")}> +
+
{I.globe}
+
{t("services")}
+
+
+
{services.length}
+
+ {services.filter(s => s.service_type === "ClusterIP").length} ClusterIP +
+
+
+
+
setTab("deployments")}> +
+
{I.layers}
+
{t("deployments")}
+
+
+
{deployments.length}
+
+ {deployments.filter(d => d.available > 0).length} {t("available").toLowerCase()} +
+
+
+
+
+
+
{I.server}
+
{t("namespace")}
+
+
+
{namespaces.length}
+
+ {namespace || t("allNamespaces").toLowerCase()} +
+
+
+
-
- )} + )} - {/* Pods Tab */} - {tab === "pods" && ( -
- {pods.length === 0 ? ( -

{t("noPods")}

- ) : ( -
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/crates/cratebay-gui/src/components/ui/tabs.tsx b/crates/cratebay-gui/src/components/ui/tabs.tsx new file mode 100644 index 0000000..b463afd --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/tabs.tsx @@ -0,0 +1,91 @@ +"use client" + +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Tabs as TabsPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + orientation = "horizontal", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +const tabsListVariants = cva( + "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none", + { + variants: { + variant: { + default: "bg-muted", + line: "gap-1 bg-transparent", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function TabsList({ + className, + variant = "default", + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } diff --git a/crates/cratebay-gui/src/components/ui/tooltip.tsx b/crates/cratebay-gui/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..d4d2254 --- /dev/null +++ b/crates/cratebay-gui/src/components/ui/tooltip.tsx @@ -0,0 +1,55 @@ +import * as React from "react" +import { Tooltip as TooltipPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/crates/cratebay-gui/src/hooks/__tests__/useVolumes.test.tsx b/crates/cratebay-gui/src/hooks/__tests__/useVolumes.test.tsx new file mode 100644 index 0000000..3fe92c9 --- /dev/null +++ b/crates/cratebay-gui/src/hooks/__tests__/useVolumes.test.tsx @@ -0,0 +1,92 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { renderHook, waitFor, act } from "@testing-library/react" +import { invoke } from "@tauri-apps/api/core" +import { useVolumes } from "../useVolumes" +import type { VolumeInfo } from "../../types" + +function mkVol(overrides: Partial): VolumeInfo { + return { + name: "vol", + driver: "local", + mountpoint: "/var/lib/docker/volumes/vol/_data", + created_at: "2024-01-01T00:00:00Z", + labels: {}, + options: {}, + scope: "local", + ...overrides, + } +} + +function deferred() { + let resolve!: (value: T) => void + let reject!: (reason?: unknown) => void + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + return { promise, resolve, reject } +} + +describe("useVolumes", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("sorts volumes by name (case-insensitive, numeric-aware)", async () => { + vi.mocked(invoke).mockResolvedValueOnce([ + mkVol({ name: "b" }), + mkVol({ name: "a" }), + ]) + + const { result, unmount } = renderHook(() => useVolumes()) + await waitFor(() => expect(result.current.loading).toBe(false)) + + expect(result.current.volumes.map(v => v.name)).toEqual(["a", "b"]) + unmount() + }) + + it("sorts volumes with numeric ordering (vol-2 before vol-10)", async () => { + vi.mocked(invoke).mockResolvedValueOnce([ + mkVol({ name: "vol-2" }), + mkVol({ name: "vol-10" }), + mkVol({ name: "vol-1" }), + ]) + + const { result, unmount } = renderHook(() => useVolumes()) + await waitFor(() => expect(result.current.loading).toBe(false)) + + expect(result.current.volumes.map(v => v.name)).toEqual(["vol-1", "vol-2", "vol-10"]) + unmount() + }) + + it("prevents stale responses from overwriting newer results", async () => { + const d1 = deferred() + const d2 = deferred() + + vi.mocked(invoke) + .mockImplementationOnce(async () => await d1.promise) + .mockImplementationOnce(async () => await d2.promise) + + const { result, unmount } = renderHook(() => useVolumes()) + + // Ensure the initial poll kicked off (useEffect -> fetchVolumes -> invoke). + await waitFor(() => expect(vi.mocked(invoke)).toHaveBeenCalledTimes(1)) + + // Trigger a second fetch while the first is still in-flight. + act(() => { + void result.current.fetchVolumes() + }) + await waitFor(() => expect(vi.mocked(invoke)).toHaveBeenCalledTimes(2)) + + // Resolve the newer request first. + d2.resolve([mkVol({ name: "new" })]) + await waitFor(() => expect(result.current.volumes.map(v => v.name)).toEqual(["new"])) + + // Resolve the older request later; it must NOT overwrite the state. + d1.resolve([mkVol({ name: "old" })]) + await waitFor(() => expect(result.current.volumes.map(v => v.name)).toEqual(["new"])) + + unmount() + }) +}) + diff --git a/crates/cratebay-gui/src/hooks/useVolumes.ts b/crates/cratebay-gui/src/hooks/useVolumes.ts index 055b293..d5c8988 100644 --- a/crates/cratebay-gui/src/hooks/useVolumes.ts +++ b/crates/cratebay-gui/src/hooks/useVolumes.ts @@ -1,20 +1,71 @@ -import { useState, useEffect, useCallback } from "react" +import { useState, useEffect, useCallback, useRef } from "react" import { invoke } from "@tauri-apps/api/core" import type { VolumeInfo } from "../types" +const volumeNameCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }) + +function compareStrings(a: string, b: string): number { + const primary = volumeNameCollator.compare(a, b) + if (primary !== 0) return primary + // Deterministic tie-breaker when collator considers strings equal (e.g. different casing). + return a.localeCompare(b) +} + +function sortVolumesByName(vols: VolumeInfo[]): VolumeInfo[] { + return [...vols].sort((a, b) => compareStrings(a?.name ?? "", b?.name ?? "")) +} + +function normalizeKv(obj: Record | null | undefined): string { + if (!obj) return "" + const entries = Object.entries(obj) + entries.sort(([ka], [kb]) => compareStrings(ka, kb)) + return entries.map(([k, v]) => `${k}=${v}`).join("\n") +} + +function volumeSignature(v: VolumeInfo): string { + // Include all fields to avoid skipping updates when data changes but UI doesn't. + return [ + v?.name ?? "", + v?.driver ?? "", + v?.mountpoint ?? "", + v?.created_at ?? "", + v?.scope ?? "", + normalizeKv(v?.labels), + normalizeKv(v?.options), + ].join("\u0000") +} + +function areVolumesListEqual(prev: VolumeInfo[], next: VolumeInfo[]): boolean { + if (prev === next) return true + if (prev.length !== next.length) return false + for (let i = 0; i < prev.length; i++) { + if (volumeSignature(prev[i]) !== volumeSignature(next[i])) return false + } + return true +} + export function useVolumes() { const [volumes, setVolumes] = useState([]) const [error, setError] = useState("") const [loading, setLoading] = useState(true) + const latestReqId = useRef(0) const fetchVolumes = useCallback(async () => { + const reqId = ++latestReqId.current try { const result = await invoke("volume_list") - setVolumes(result) + const next = sortVolumesByName(Array.isArray(result) ? result : []) + + // Only allow the latest in-flight request to update state (prevents stale overwrite). + if (reqId !== latestReqId.current) return + + setVolumes(prev => (areVolumesListEqual(prev, next) ? prev : next)) setError("") } catch (e) { + if (reqId !== latestReqId.current) return setError(String(e)) } finally { + if (reqId !== latestReqId.current) return setLoading(false) } }, []) 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/crates/cratebay-gui/src/index.css b/crates/cratebay-gui/src/index.css index c8ca1e8..55e53ad 100644 --- a/crates/cratebay-gui/src/index.css +++ b/crates/cratebay-gui/src/index.css @@ -1,5 +1,21 @@ +@font-face { + font-family: 'Geist Sans'; + src: url('./assets/fonts/GeistSans-Variable.woff2') format('woff2-variations'); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Geist Mono'; + src: url('./assets/fonts/GeistMono-Variable.woff2') format('woff2-variations'); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + :root { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif; line-height: 1.5; font-weight: 400; font-synthesis: none; diff --git a/crates/cratebay-gui/src/lib/utils.ts b/crates/cratebay-gui/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/crates/cratebay-gui/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/crates/cratebay-gui/src/pages/Containers.tsx b/crates/cratebay-gui/src/pages/Containers.tsx index db33103..7f7bcdf 100644 --- a/crates/cratebay-gui/src/pages/Containers.tsx +++ b/crates/cratebay-gui/src/pages/Containers.tsx @@ -316,86 +316,95 @@ export function Containers({ const childClass = opts?.child ? " container-child" : "" const stats = isRunning ? containerStats[c.id] : undefined return ( -
-
{I.box}
-
-
{name}
-
{c.image} · {meta}
-
- {isRunning && stats && ( -
-
- {I.cpu} - {stats.cpu_percent.toFixed(1)}% -
-
- {I.memory} - {stats.memory_usage_mb.toFixed(0)} +
+
+
{I.box}
+
+
{name}
+
{c.image} · {meta}
+
+ {isRunning && stats && ( +
+
+ {I.cpu} + {stats.cpu_percent.toFixed(1)}% +
+
+ {I.memory} + {stats.memory_usage_mb.toFixed(0)} MB +
+ )} +
+ + {c.status}
- )} -
- - {c.status}
+
{isRunning && ( )} - {isRunning ? ( - - ) : ( - - )} -
+
+
+ {isRunning ? ( + + ) : ( + + )} + +
+
) } @@ -439,19 +448,21 @@ export function Containers({ }} title={expanded ? "Collapse" : "Expand"} > -
{I.box}
-
-
{g.key}
-
- {t("running")}: {g.runningCount} · {t("stopped")}: {g.stoppedCount} +
+
{I.box}
+
+
{g.key}
+
+ {t("running")}: {g.runningCount} · {t("stopped")}: {g.stoppedCount} +
+
+
+ + {hasRunning ? t("running") : t("stopped")} +
+ -
-
- - {hasRunning ? t("running") : t("stopped")} -
-
{expanded && ( @@ -466,89 +477,120 @@ export function Containers({ {/* Run Container Modal */} {showRunModal && ( -
setShowRunModal(false)}> -
e.stopPropagation()} style={{ maxWidth: 480 }}> +
setShowRunModal(false)}> +
e.stopPropagation()}>
-
{t("runContainer")}
+
+ {I.play} + {t("runContainer")} +
-
+
-
- - setRunImage(e.target.value)} placeholder="nginx:latest" autoFocus /> -
-
- - setRunName(e.target.value)} placeholder="web" /> -
-
-
- - setRunCpus(e.target.value === "" ? "" : Number(e.target.value))} /> + {/* Image & Name Section */} +
+
+ {I.box} + {t("image")} +
+
+ + setRunImage(e.target.value)} placeholder="nginx:latest" autoFocus />
-
- - setRunMem(e.target.value === "" ? "" : Number(e.target.value))} /> +
+ + setRunName(e.target.value)} placeholder="my-container" />
-
- setRunPull(e.target.checked)} /> - {t("pullBeforeRun")} + + {/* Resources Section */} +
+
+ {I.cpu} + {t("cpus")} & {t("memoryMb")} +
+
+
+ + setRunCpus(e.target.value === "" ? "" : Number(e.target.value))} placeholder="—" /> +
+
+ + setRunMem(e.target.value === "" ? "" : Number(e.target.value))} placeholder="—" /> +
+
+
+ setRunPull(e.target.checked)} id="run-pull-toggle" /> + +
-
- -
{t("envVarsHint")}
- {runEnvVars.map((env, i) => ( -
- { - const updated = [...runEnvVars] - updated[i] = { ...updated[i], key: e.target.value } - setRunEnvVars(updated) - }} - placeholder={t("envKey")} - /> - = - { - const updated = [...runEnvVars] - updated[i] = { ...updated[i], value: e.target.value } - setRunEnvVars(updated) - }} - placeholder={t("envValue")} - /> - + + {/* Environment Variables Section */} +
+
+ {I.settings} + {t("envVars")} +
+
{t("envVarsHint")}
+ {runEnvVars.length > 0 && ( +
+ {runEnvVars.map((env, i) => ( +
+ { + const updated = [...runEnvVars] + updated[i] = { ...updated[i], key: e.target.value } + setRunEnvVars(updated) + }} + placeholder={t("envKey")} + /> + = + { + const updated = [...runEnvVars] + updated[i] = { ...updated[i], value: e.target.value } + setRunEnvVars(updated) + }} + placeholder={t("envValue")} + /> + +
+ ))}
- ))} + )}
- {runError &&
{runError}
} + + {/* Error & Result */} + {runError && ( +
+ {I.alertCircle} + {runError} +
+ )} {runResult && ( -
-
{t("loginCommand")}
-
+
+
{I.terminal} {t("loginCommand")}
+
{runResult.login_cmd}
-
@@ -597,8 +643,8 @@ export function Containers({
{ev.key}{ev.value}{ev.key}{ev.value}
- - - - - - - - - - - - - {pods.map((pod) => ( - - - - - - - - - - ))} - -
{t("name")}{t("namespace")}{t("status")}{t("ready")}{t("restarts")}{t("age")}{t("actions")}
{pod.name}{pod.namespace} - - {pod.status} - - {pod.ready}{pod.restarts}{pod.age} - -
- )} -
- )} + {/* Pods Tab */} + {tab === "pods" && ( +
+ {pods.length === 0 ? ( +
+
{I.box}
+
{t("noPods")}
+
No pod resources found in the current namespace
+
+ ) : ( + <> +
+ {pods.length} {t("pods")} + + {pods.filter(p => p.status === "Running").length} {t("running").toLowerCase()} +
+
+ + + + + + + + + + + + + + {pods.map((pod) => ( + + + + + + + + + + ))} + +
{t("name")}{t("namespace")}{t("status")}{t("ready")}{t("restarts")}{t("age")}{t("actions")}
{pod.name}{pod.namespace} + + + {pod.status} + + {pod.ready} + 0 ? " warn" : ""}`}> + {pod.restarts} + + {pod.age} + +
+
+ + )} +
+ )} - {/* Services Tab */} - {tab === "services" && ( -
- {services.length === 0 ? ( -

{t("noServices")}

- ) : ( - - - - - - - - - - - - {services.map((svc) => ( - - - - - - - - ))} - -
{t("name")}{t("namespace")}{t("type")}{t("clusterIp")}{t("ports")}
{svc.name}{svc.namespace}{svc.service_type}{svc.cluster_ip}{svc.ports}
- )} -
- )} + {/* Services Tab */} + {tab === "services" && ( +
+ {services.length === 0 ? ( +
+
{I.globe}
+
{t("noServices")}
+
No service resources found in the current namespace
+
+ ) : ( + <> +
+ {services.length} {t("services")} +
+
+ + + + + + + + + + + + {services.map((svc) => ( + + + + + + + + ))} + +
{t("name")}{t("namespace")}{t("type")}{t("clusterIp")}{t("ports")}
{svc.name}{svc.namespace} + {svc.service_type} + {svc.cluster_ip}{svc.ports}
+
+ + )} +
+ )} - {/* Deployments Tab */} - {tab === "deployments" && ( -
- {deployments.length === 0 ? ( -

{t("noDeployments")}

- ) : ( - - - - - - - - - - - - - {deployments.map((dep) => ( - - - - - - - - - ))} - -
{t("name")}{t("namespace")}{t("ready")}{t("upToDate")}{t("available")}{t("age")}
{dep.name}{dep.namespace}{dep.ready}{dep.up_to_date}{dep.available}{dep.age}
- )} -
- )} + {/* Deployments Tab */} + {tab === "deployments" && ( +
+ {deployments.length === 0 ? ( +
+
{I.layers}
+
{t("noDeployments")}
+
No deployment resources found in the current namespace
+
+ ) : ( + <> +
+ {deployments.length} {t("deployments")} + + {deployments.filter(d => d.available > 0).length} {t("available").toLowerCase()} +
+
+ + + + + + + + + + + + + {deployments.map((dep) => ( + + + + + + + + + ))} + +
{t("name")}{t("namespace")}{t("ready")}{t("upToDate")}{t("available")}{t("age")}
{dep.name}{dep.namespace} + 0 ? "ok" : "warn"}`}>{dep.ready} + {dep.up_to_date} + 0 ? "ok" : "warn"}`}>{dep.available} + {dep.age}
+
+ + )} +
+ )} +
)} {/* Pod Logs Modal */} {logPod && ( -
setLogPod(null)}> +
setLogPod(null)}>
e.stopPropagation()} > -
-

- {t("podLogs")}: {logPod.name} -

- +
+
+
{I.terminal}
+
+

{t("podLogs")}

+ {logPod.name} +
+
+
+ + + {logPod.status} + +
+ + + +
+
+
+ {logsLoading ? ( +
+
+ {t("loading")} +
+ ) : ( +
+                  {podLogs || t("noLogs")}
+                
+ )}
-
-              {logsLoading ? t("loading") : podLogs || t("noLogs")}
-            
)}
) } - -const thStyle: React.CSSProperties = { - textAlign: "left", - padding: "8px 12px", - fontWeight: 600, - fontSize: 12, - opacity: 0.7, - whiteSpace: "nowrap", -} - -const tdStyle: React.CSSProperties = { - padding: "8px 12px", - whiteSpace: "nowrap", -} diff --git a/crates/cratebay-gui/src/pages/Vms.tsx b/crates/cratebay-gui/src/pages/Vms.tsx index 7ae6713..89757d0 100644 --- a/crates/cratebay-gui/src/pages/Vms.tsx +++ b/crates/cratebay-gui/src/pages/Vms.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useCallback } from "react" +import { type JSX, useState, useEffect, useRef, useCallback } from "react" import { invoke } from "@tauri-apps/api/core" import { I } from "../icons" import { ErrorInline } from "../components/ErrorDisplay" @@ -110,7 +110,6 @@ export function Vms({ // Poll console data when modal is open useEffect(() => { if (!consoleVmId) return - // Initial full load // eslint-disable-next-line react-hooks/set-state-in-effect setConsoleData("") let currentOffset = 0 @@ -201,29 +200,38 @@ export function Vms({ setShowCreateModal(false) } + // Tab config for cleaner rendering + const tabConfig: { key: typeof activeTab; icon: JSX.Element; label: string }[] = [ + { key: "info", icon: I.cpu, label: t("status") }, + { key: "ssh", icon: I.terminal, label: t("loginCommand") }, + { key: "mounts", icon: I.hardDrive, label: t("virtiofs") }, + { key: "ports", icon: I.globe, label: t("portForwarding") }, + { key: "console", icon: I.fileText, label: t("console") }, + ] + return (
{/* Toolbar */}
- - -
-
{t("vmHint")}
+
+
{t("vmHint")}
{vmError && setVmError("")} />} {/* VM List */} {vms.length === 0 ? ( -
+
{I.server}

{t("noVms")}

{t("createFirstVm")}

-
@@ -233,16 +241,41 @@ export function Vms({ const isRunning = vm.state === "running" const isExpanded = expandedVmId === vm.id const stats = isRunning ? vmStatsMap[vm.id] : undefined + const isActing = vmActing === vm.id return ( -
+
+ {/* Main row */}
setExpandedVmId(isExpanded ? null : vm.id)}> -
{I.server}
+
{I.server}
{vm.name}
-
- {vm.cpus} vCPU · {vm.memory_mb} MB · {vm.disk_gb} GB - {vm.rosetta_enabled && " · Rosetta"} - {vm.os_image && ` · ${vm.os_image}`} +
+ + {I.cpu} + {vm.cpus} vCPU + + · + + {I.memory} + {vm.memory_mb} MB + + · + + {I.hardDrive} + {vm.disk_gb} GB + + {vm.rosetta_enabled && ( + <> + · + Rosetta + + )} + {vm.os_image && ( + <> + · + {vm.os_image} + + )}
{isRunning && stats && (
@@ -254,187 +287,323 @@ export function Vms({
)}
-
- - {vm.state} + + {/* Status badge */} +
+ + {vm.state}
+ + {/* Actions */}
{isRunning ? ( - + ) : ( - + )} - - - + + +
+
+
{isExpanded ? I.chevronDown : I.chevronRight}
{/* Expanded detail panel */} {isExpanded && ( -
-
- - - - - +
+ {/* Tab navigation with icons */} +
+ {tabConfig.map(tab => ( + + ))}
+ {/* === Info Tab === */} {activeTab === "info" && ( -
-
-
-
ID
+
+
+
+
+ {I.server} + ID +
{vm.id}
-
-
{t("cpus")}
-
{vm.cpus} vCPU
+
+
+ {I.cpu} + {t("cpus")} +
+
{vm.cpus} vCPU
-
-
{t("memoryMb")}
-
{vm.memory_mb} MB
+
+
+ {I.memory} + {t("memoryMb")} +
+
{vm.memory_mb} MB
-
-
{t("diskGb")}
-
{vm.disk_gb} GB
+
+
+ {I.hardDrive} + {t("diskGb")} +
+
{vm.disk_gb} GB
-
+
Rosetta
-
{vm.rosetta_enabled ? "ON" : "OFF"}
+
+ + + {vm.rosetta_enabled ? "ON" : "OFF"} + +
{vm.os_image && ( -
+
{t("osImage")}
{vm.os_image}
)} -
+
{t("state")}
- - {vm.state} + + + {vm.state} +
)} + {/* === SSH Tab === */} {activeTab === "ssh" && ( -
-
-
- - setVmLoginUser(e.target.value)} /> -
-
- - setVmLoginHost(e.target.value)} /> +
+
+
+ {I.terminal} + SSH {t("loginCommand")}
-
- - setVmLoginPort(e.target.value === "" ? "" : Number(e.target.value))} /> +
+
+ + setVmLoginUser(e.target.value)} placeholder="root" /> +
+
+
+ + setVmLoginHost(e.target.value)} placeholder="localhost" /> +
+
+ + setVmLoginPort(e.target.value === "" ? "" : Number(e.target.value))} + placeholder="22" /> +
+
+
+ +
+
{t("vmLoginHint")}
- -
{t("vmLoginHint")}
)} + {/* === Mounts Tab === */} {activeTab === "mounts" && ( -
+
{isRunning && ( -
- {t("virtiofsRestartNotice")} +
+ {I.alertCircle} + {t("virtiofsRestartNotice")}
)} + + {/* Existing mounts */} {vm.mounts?.length > 0 && ( -
- {vm.mounts.map(m => ( -
- {m.tag} - {m.host_path} → {m.guest_path} - {m.read_only ? "RO" : "RW"} - -
- ))} +
+
+ {t("virtiofs")} ({vm.mounts.length}) +
+
+ {vm.mounts.map(m => ( +
+ {m.tag} + + {m.host_path} + + {m.guest_path} + + {m.read_only ? "RO" : "RW"} + +
+ ))} +
)} -
0 ? 12 : 0 }}> -
- - { setMountVmId(vm.id); setMountTag(e.target.value) }} placeholder="code" /> -
-
- - { setMountVmId(vm.id); setMountHostPath(e.target.value) }} placeholder="/Users/me/code" /> -
-
- - { setMountVmId(vm.id); setMountGuestPath(e.target.value) }} placeholder="/mnt/code" /> + + {/* Add mount form */} +
+
+ {I.plus} + {t("addMount")}
-
- setMountReadonly(e.target.checked)} /> - {t("readOnly")} +
+
+ + { setMountVmId(vm.id); setMountTag(e.target.value) }} + placeholder="code" /> +
+
+
+ + { setMountVmId(vm.id); setMountHostPath(e.target.value) }} + placeholder="/Users/me/code" /> +
+
+ + { setMountVmId(vm.id); setMountGuestPath(e.target.value) }} + placeholder="/mnt/code" /> +
+
+
+ setMountReadonly(e.target.checked)} /> + +
+
+ +
+
{t("virtiofsHint")}
+
{t("virtiofsGuestHint")}
- -
{t("virtiofsHint")}
-
{t("virtiofsGuestHint")}
)} + {/* === Console Tab === */} {activeTab === "console" && ( -
-
-
-
{t("vmHint")}
)} + {/* === Port Forwarding Tab === */} {activeTab === "ports" && ( -
+
+ {/* Existing port forwards */} {vm.port_forwards?.length > 0 && ( -
- {vm.port_forwards.map(pf => ( -
- {pf.host_port} - {pf.host_port} → {pf.guest_port} - {pf.protocol.toUpperCase()} - -
- ))} +
+
+ {t("portForwarding")} ({vm.port_forwards.length}) +
+
+ {vm.port_forwards.map(pf => ( +
+ + {I.globe} + :{pf.host_port} + + + + :{pf.guest_port} + + {pf.protocol.toUpperCase()} + +
+ ))} +
)} -
0 ? 12 : 0 }}> -
- - { setPfVmId(vm.id); setPfHostPort(e.target.value === "" ? "" : Number(e.target.value)) }} placeholder="8080" /> -
-
- - { setPfVmId(vm.id); setPfGuestPort(e.target.value === "" ? "" : Number(e.target.value)) }} placeholder="80" /> + + {/* Add port forward form */} +
+
+ {I.plus} + {t("addPortForward")}
-
- - +
+
+
+ + { setPfVmId(vm.id); setPfHostPort(e.target.value === "" ? "" : Number(e.target.value)) }} + placeholder="8080" /> +
+
+ + { setPfVmId(vm.id); setPfGuestPort(e.target.value === "" ? "" : Number(e.target.value)) }} + placeholder="80" /> +
+
+ + +
+
+
+ +
+
{t("portForwardHint")}
- -
{t("portForwardHint")}
)} @@ -446,31 +615,35 @@ export function Vms({
)} - {/* Create VM Modal */} + {/* ============ Create VM Modal ============ */} {showCreateModal && (
setShowCreateModal(false)}> -
e.stopPropagation()} style={{ maxWidth: 520 }}> -
-
{t("createVm")}
+
e.stopPropagation()}> +
+
+
+ {I.server} +
+
{t("createVm")}
+
- +
-
-
+
+
+ {/* VM Name */}
- - setVmName(e.target.value)} placeholder="myvm" autoFocus /> + + setVmName(e.target.value)} + placeholder="myvm" autoFocus />
{/* OS Image selector */}
- - setSelectedOsImage(e.target.value)}> {osImages.map(img => (
{/* OS Image list with download/delete actions */} -
- {osImages.map(img => { - const isDownloading = downloadingImage === img.id - const progressPct = downloadProgress && downloadProgress.image_id === img.id && downloadProgress.bytes_total > 0 - ? Math.min(100, (downloadProgress.bytes_downloaded / downloadProgress.bytes_total) * 100) - : 0 - return ( -
-
-
{img.name}
-
- {img.arch} · {t("osImageVersion")} {img.version} · {formatBytes(img.size_bytes)} + {osImages.length > 0 && ( +
+
Available Images
+ {osImages.map(img => { + const isDownloading = downloadingImage === img.id + const progressPct = downloadProgress && downloadProgress.image_id === img.id && downloadProgress.bytes_total > 0 + ? Math.min(100, (downloadProgress.bytes_downloaded / downloadProgress.bytes_total) * 100) + : 0 + return ( +
+ {/* Icon */} +
+ {I.hardDrive}
- {isDownloading && ( -
-
-
-
-
- {t("osImageProgress")}: {progressPct.toFixed(1)}% -
+ + {/* Info */} +
+
{img.name}
+
+ {img.arch} + · + v{img.version} + · + {formatBytes(img.size_bytes)}
- )} -
-
- {img.status === "not_downloaded" && ( - - )} - {img.status === "downloading" && ( - {t("osImageDownloading")} - )} - {img.status === "ready" && ( - <> - - {t("osImageReady")} - - - - )} + )} + {img.status === "downloading" && ( + + + {t("osImageDownloading")} + + )} + {img.status === "ready" && ( + <> + + + {t("osImageReady")} + + + + )} +
-
- ) - })} -
+ ) + })} +
+ )} + {/* Hardware specs */} +
Hardware Configuration
- - setVmCpus(Number(e.target.value) || 2)} /> + + setVmCpus(Number(e.target.value) || 2)} />
- - setVmMem(Number(e.target.value) || 2048)} /> + + setVmMem(Number(e.target.value) || 2048)} />
- - setVmDisk(Number(e.target.value) || 20)} /> + + setVmDisk(Number(e.target.value) || 20)} />
- setVmRosetta(e.target.checked)} /> - {t("enableRosetta")} + setVmRosetta(e.target.checked)} /> +
-
- - +
)} - {/* Console Modal */} + {/* ============ Console Modal ============ */} {consoleVmId && (
-
e.stopPropagation()} style={{ maxWidth: 800, width: "96vw" }}> -
-
{t("vmConsole")} — {consoleVmName}
+
e.stopPropagation()}> +
+
+
+ {I.terminal} +
+
+ {t("vmConsole")} + — {consoleVmName} +
+
- +
-
-
- {consoleData ? ( -
{consoleData}
- ) : ( -
{t("noConsoleOutput")}
- )} +
+
+ {consoleData ? ( +
{consoleData}
+ ) : ( +
+ {I.terminal} + {t("noConsoleOutput")} +
+ )} +
-
- +
+
diff --git a/crates/cratebay-gui/src/pages/Volumes.tsx b/crates/cratebay-gui/src/pages/Volumes.tsx index c1cad3b..d8a2559 100644 --- a/crates/cratebay-gui/src/pages/Volumes.tsx +++ b/crates/cratebay-gui/src/pages/Volumes.tsx @@ -1,4 +1,4 @@ -import { useState } from "react" +import { useState, useMemo } from "react" import { I } from "../icons" import { ErrorBanner } from "../components/ErrorDisplay" import { EmptyState } from "../components/EmptyState" @@ -30,6 +30,17 @@ export function Volumes({ const [inspectLoading, setInspectLoading] = useState(false) const [confirmDelete, setConfirmDelete] = useState("") + const [search, setSearch] = useState("") + + const filtered = useMemo(() => { + if (!search.trim()) return volumes + const q = search.toLowerCase() + return volumes.filter(v => + v.name.toLowerCase().includes(q) || + v.driver.toLowerCase().includes(q) || + v.mountpoint?.toLowerCase().includes(q) + ) + }, [volumes, search]) const openCreateModal = () => { setCreateName("") @@ -86,6 +97,11 @@ export function Volumes({ } } + const labelsCount = (v: VolumeInfo) => { + if (!v.labels) return 0 + return Object.keys(v.labels).length + } + if (loading) { return
{t("loading")}
} @@ -102,16 +118,24 @@ export function Volumes({ return (
+ {/* Toolbar */}
-
- -
+ {/* Volume List */} {volumes.length === 0 ? ( + ) : filtered.length === 0 ? ( + ) : ( - volumes.map(v => ( -
-
- {I.hardDrive} -
-
-
{v.name}
-
- {t("driver")}: {v.driver} · {t("mountpoint")}: {v.mountpoint || "-"} - {v.created_at ? ` · ${formatDate(v.created_at)}` : ""} +
+ {filtered.map(v => { + const lc = labelsCount(v) + return ( +
+
+
+ {I.hardDrive} +
+
+
{v.name}
+
+ {v.driver} + + {v.scope || "local"} + {lc > 0 && ( + <> + + {lc} {lc === 1 ? "label" : "labels"} + + )} + {v.created_at && ( + <> + + {formatDate(v.created_at)} + + )} +
+
+
+
+
+ + +
+
+ +
-
-
- - -
-
- )) + ) + })} +
)} {/* Create Volume Modal */} {showCreateModal && (
setShowCreateModal(false)}> -
e.stopPropagation()} style={{ maxWidth: 480 }}> +
e.stopPropagation()}>
{t("createVolume")}
- +
@@ -183,11 +252,11 @@ export function Volumes({
- {createError &&
{createError}
} + {createError &&
{createError}
}
- - +
@@ -198,18 +267,90 @@ export function Volumes({ {/* Inspect Volume Modal */} {inspectVolume && (
setInspectVolume(null)}> -
e.stopPropagation()} style={{ maxWidth: 640 }}> +
e.stopPropagation()}>
{t("volumeDetails")} — {inspectVolume.name}
- + +
-
-
{JSON.stringify(inspectVolume, null, 2)}
+
+
+
+
+
+ +
{inspectVolume.name}
+
+
+ +
{inspectVolume.driver}
+
+
+
+
+ +
{inspectVolume.scope || "local"}
+
+
+ +
{formatDate(inspectVolume.created_at)}
+
+
+
+ + + {inspectVolume.mountpoint || "-"} + +
+ {inspectVolume.labels && Object.keys(inspectVolume.labels).length > 0 && ( +
+ +
+ {Object.entries(inspectVolume.labels).map(([k, val]) => ( + + {k} + = + {val} + + ))} +
+
+ )} + {inspectVolume.options && Object.keys(inspectVolume.options).length > 0 && ( +
+ +
+ {Object.entries(inspectVolume.options).map(([k, val]) => ( + + {k} + = + {val} + + ))} +
+
+ )} +
+
+
+ RAW JSON +
+
{JSON.stringify(inspectVolume, null, 2)}
- +
@@ -218,21 +359,22 @@ export function Volumes({ {/* Confirm Delete Modal */} {confirmDelete && (
setConfirmDelete("")}> -
e.stopPropagation()} style={{ maxWidth: 420 }}> +
e.stopPropagation()}>
{t("deleteVolume")}
- +
-

{t("confirmDeleteVolume")}

-

{confirmDelete}

+

{t("confirmDeleteVolume")}

+

{confirmDelete}

- - +
diff --git a/crates/cratebay-gui/src/pages/__tests__/Dashboard.test.tsx b/crates/cratebay-gui/src/pages/__tests__/Dashboard.test.tsx index ab970d9..dbb6423 100644 --- a/crates/cratebay-gui/src/pages/__tests__/Dashboard.test.tsx +++ b/crates/cratebay-gui/src/pages/__tests__/Dashboard.test.tsx @@ -182,13 +182,15 @@ describe("Dashboard", () => { ) - expect(screen.getByText(t("totalResources"))).toBeInTheDocument() + expect(screen.getByText(t("cpuUsage"))).toBeInTheDocument() + expect(screen.getByText(t("memoryUsage"))).toBeInTheDocument() }) it("does not show resource panel when nothing is running", () => { render() - expect(screen.queryByText(t("totalResources"))).not.toBeInTheDocument() + expect(screen.queryByText(t("cpuUsage"))).not.toBeInTheDocument() + expect(screen.queryByText(t("memoryUsage"))).not.toBeInTheDocument() }) it("shows image results count", () => { diff --git a/crates/cratebay-gui/tsconfig.app.json b/crates/cratebay-gui/tsconfig.app.json index a9b5a59..876d27b 100644 --- a/crates/cratebay-gui/tsconfig.app.json +++ b/crates/cratebay-gui/tsconfig.app.json @@ -22,7 +22,13 @@ "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + + /* Path aliases */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } }, "include": ["src"] } diff --git a/crates/cratebay-gui/vite.config.ts b/crates/cratebay-gui/vite.config.ts index b9fd478..9beecdc 100644 --- a/crates/cratebay-gui/vite.config.ts +++ b/crates/cratebay-gui/vite.config.ts @@ -1,9 +1,15 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import path from 'path' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, server: { host: '127.0.0.1', port: 5173, 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 diff --git a/website/script.js b/website/script.js index 209f74f..d032370 100644 --- a/website/script.js +++ b/website/script.js @@ -36,16 +36,16 @@ 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.', + '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.', feat3Title: 'Image & Volume Management', feat3Desc: - 'Search and pull images from Docker Hub and private registries. Manage volumes with inspect, create, prune, and mount operations.', + 'Search and pull images from Docker Hub and private registries. Manage volumes with inspect, create, prune, and mount operations. Volume lists remain consistently ordered during live refresh.', feat4Title: 'Resource Monitoring', feat4Desc: 'Real-time CPU, memory, network, and disk I/O monitoring for every container and VM. Spot issues instantly.', @@ -171,13 +171,13 @@ statSize: '应用大小', featLabel: '核心功能', featTitle: '容器管理,一应俱全。', - featDesc: 'CrateBay 为 Docker 容器、Linux 虚拟机和 Kubernetes 集群提供统一、原生速度的管理体验——零额外开销。', + featDesc: 'CrateBay 为 Docker 容器、Linux 虚拟机和 Kubernetes 集群提供统一、原生速度的管理体验——零额外开销。系统托盘提供快捷操作,并展示实时运行数量。', feat1Title: 'Docker 容器管理', - feat1Desc: '全生命周期控制——创建、启动、停止、重启、删除。实时日志流和进入运行中容器的终端。', + feat1Desc: '全生命周期控制——创建、启动、停止、重启、删除。实时日志流和进入运行中容器的终端。端口映射信息在实时刷新时保持稳定显示。', feat2Title: 'Linux 虚拟机', feat2Desc: '通过 Virtualization.framework (macOS)、KVM (Linux)、Hyper-V (Windows) 原生支持虚拟机,秒级启动。', feat3Title: '镜像与卷管理', - feat3Desc: '从 Docker Hub 和私有仓库搜索和拉取镜像。管理卷的检查、创建、清理和挂载操作。', + feat3Desc: '从 Docker Hub 和私有仓库搜索和拉取镜像。管理卷的检查、创建、清理和挂载操作。卷列表在实时刷新时保持稳定排序。', feat4Title: '资源监控', feat4Desc: '实时监控每个容器和虚拟机的 CPU、内存、网络和磁盘 I/O。即时发现问题。', feat5Title: '端口转发 & VirtioFS',