From af9147ec75aaba9d8e08e01bc86a6a04f56f28af Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 04:32:15 +0000 Subject: [PATCH] Add admin panel web UI (Svelte 5) Dashboard with launch stats, instance manager with Modrinth/CurseForge search and zip import, player and group management, launcher design editor with live preview, and sign-in settings. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS --- panel/web/index.html | 14 + panel/web/package-lock.json | 1255 +++++++++++++++++ panel/web/package.json | 26 + panel/web/public/favicon.svg | 1 + panel/web/src/App.svelte | 80 ++ panel/web/src/app.css | 311 ++++ panel/web/src/components/Avatar.svelte | 16 + panel/web/src/components/BarChart.svelte | 77 + panel/web/src/components/ColorInput.svelte | 19 + panel/web/src/components/ImageInput.svelte | 66 + .../web/src/components/LauncherPreview.svelte | 136 ++ panel/web/src/components/Modal.svelte | 44 + panel/web/src/components/Sidebar.svelte | 90 ++ panel/web/src/components/Toasts.svelte | 28 + panel/web/src/components/Toggle.svelte | 26 + panel/web/src/components/VersionPicker.svelte | 96 ++ panel/web/src/lib/api.ts | 73 + panel/web/src/lib/router.svelte.ts | 18 + panel/web/src/lib/session.svelte.ts | 34 + panel/web/src/lib/toast.svelte.ts | 19 + panel/web/src/lib/types.ts | 80 ++ panel/web/src/main.ts | 11 + panel/web/src/pages/Branding.svelte | 252 ++++ panel/web/src/pages/Dashboard.svelte | 150 ++ panel/web/src/pages/InstanceEditor.svelte | 560 ++++++++ panel/web/src/pages/Instances.svelte | 147 ++ panel/web/src/pages/Login.svelte | 59 + panel/web/src/pages/Settings.svelte | 109 ++ panel/web/src/pages/Users.svelte | 247 ++++ panel/web/svelte.config.js | 3 + panel/web/tsconfig.json | 15 + panel/web/vite.config.ts | 22 + 32 files changed, 4084 insertions(+) create mode 100644 panel/web/index.html create mode 100644 panel/web/package-lock.json create mode 100644 panel/web/package.json create mode 100644 panel/web/public/favicon.svg create mode 100644 panel/web/src/App.svelte create mode 100644 panel/web/src/app.css create mode 100644 panel/web/src/components/Avatar.svelte create mode 100644 panel/web/src/components/BarChart.svelte create mode 100644 panel/web/src/components/ColorInput.svelte create mode 100644 panel/web/src/components/ImageInput.svelte create mode 100644 panel/web/src/components/LauncherPreview.svelte create mode 100644 panel/web/src/components/Modal.svelte create mode 100644 panel/web/src/components/Sidebar.svelte create mode 100644 panel/web/src/components/Toasts.svelte create mode 100644 panel/web/src/components/Toggle.svelte create mode 100644 panel/web/src/components/VersionPicker.svelte create mode 100644 panel/web/src/lib/api.ts create mode 100644 panel/web/src/lib/router.svelte.ts create mode 100644 panel/web/src/lib/session.svelte.ts create mode 100644 panel/web/src/lib/toast.svelte.ts create mode 100644 panel/web/src/lib/types.ts create mode 100644 panel/web/src/main.ts create mode 100644 panel/web/src/pages/Branding.svelte create mode 100644 panel/web/src/pages/Dashboard.svelte create mode 100644 panel/web/src/pages/InstanceEditor.svelte create mode 100644 panel/web/src/pages/Instances.svelte create mode 100644 panel/web/src/pages/Login.svelte create mode 100644 panel/web/src/pages/Settings.svelte create mode 100644 panel/web/src/pages/Users.svelte create mode 100644 panel/web/svelte.config.js create mode 100644 panel/web/tsconfig.json create mode 100644 panel/web/vite.config.ts diff --git a/panel/web/index.html b/panel/web/index.html new file mode 100644 index 0000000..c45db83 --- /dev/null +++ b/panel/web/index.html @@ -0,0 +1,14 @@ + + + + + + + + ScopeNet Panel + + +
+ + + diff --git a/panel/web/package-lock.json b/panel/web/package-lock.json new file mode 100644 index 0000000..551f9c3 --- /dev/null +++ b/panel/web/package-lock.json @@ -0,0 +1,1255 @@ +{ + "name": "scopenet-panel-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "scopenet-panel-web", + "version": "0.1.0", + "devDependencies": { + "@fontsource-variable/inter": "^5.3.0", + "@fontsource-variable/jetbrains-mono": "^5.3.0", + "@fontsource-variable/outfit": "^5.3.0", + "@fontsource-variable/plus-jakarta-sans": "^5.3.0", + "@fontsource-variable/space-grotesk": "^5.3.0", + "@lucide/svelte": "^1.48.0", + "@sveltejs/vite-plugin-svelte": "^7.3.1", + "@tsconfig/svelte": "^5.0.8", + "svelte": "^5.57.1", + "svelte-check": "^4.7.6", + "typescript": "~5.9.0", + "vite": "^8.3.1" + } + }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/jetbrains-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", + "integrity": "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/outfit": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/outfit/-/outfit-5.3.0.tgz", + "integrity": "sha512-fPb5XnYK0k7SbR++HEmm4+ZqDszmLKQ+sPRUr8oQ/KNC0vy6r6hazJFe8iYozv4cqlHbvJ/JECkJWrfDodUMVQ==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/plus-jakarta-sans": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/plus-jakarta-sans/-/plus-jakarta-sans-5.3.0.tgz", + "integrity": "sha512-/l/4r0yyWK9JzAlmA0LiYgGgmJe/Gswt4jTJEzr5QhJfwMbvJZDmYWyW0M4X7yCK69BajMVlV/Lx8g7WDc1+sw==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/space-grotesk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/space-grotesk/-/space-grotesk-5.3.0.tgz", + "integrity": "sha512-2IxmvfB08i9vnGB3Ym/AXvhRE+8XOjWMXIyDum03c+tPwH0FUoMNQfGpU8NXPxjbws0Vvss3AH0Zqt4oJBBAdw==", + "dev": true, + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@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://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@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/@lucide/svelte": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.48.0.tgz", + "integrity": "sha512-bLimVDjehxu3fpu9XEUpqEkBk2sJAHAT2sVcLHhlM70EdNsXuj9ZnYENWv5xgw3+05h4i5TirAxTiTgnZZf8gw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "svelte": "^5" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", + "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", + "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", + "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", + "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", + "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", + "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", + "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", + "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", + "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", + "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", + "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", + "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", + "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", + "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", + "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.13.tgz", + "integrity": "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.3.tgz", + "integrity": "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.3.1.tgz", + "integrity": "sha512-ZPsLN8B1e/En+Ak5s4V7srFDT532oS0qieLsQwu63NGKsS+iAjoO2Js1BochlHlglcU+Pt7WAO3C5Ee+4f6gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^1.0.0", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@tsconfig/svelte": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz", + "integrity": "sha512-UkNnw1/oFEfecR8ypyHIQuWYdkPvHiwcQ78sh+ymIiYoF+uc5H1UBetbjyqT+vgGJ3qQN6nhucJviX6HesWtKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.4.0.tgz", + "integrity": "sha512-CQPcneEHw/czj32uwQC52nYnax8H4SXt25FCM+LIwGZ49cMIxpOj3bP8wmzcI+Z0HIy5KkClaPhRLfCySzwHPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rolldown": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", + "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.151.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.11", + "@rolldown/binding-android-arm64": "1.2.11", + "@rolldown/binding-darwin-arm64": "1.2.11", + "@rolldown/binding-darwin-x64": "1.2.11", + "@rolldown/binding-freebsd-x64": "1.2.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", + "@rolldown/binding-linux-arm64-gnu": "1.2.11", + "@rolldown/binding-linux-arm64-musl": "1.2.11", + "@rolldown/binding-linux-ppc64-gnu": "1.2.11", + "@rolldown/binding-linux-s390x-gnu": "1.2.11", + "@rolldown/binding-linux-x64-gnu": "1.2.11", + "@rolldown/binding-linux-x64-musl": "1.2.11", + "@rolldown/binding-openharmony-arm64": "1.2.11", + "@rolldown/binding-win32-arm64-msvc": "1.2.11", + "@rolldown/binding-win32-x64-msvc": "1.2.11" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "5.57.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.57.1.tgz", + "integrity": "sha512-Uqj49lWKB+iSSnneuwiYYJ7MZgkB+eXr0LXBhv4uDuAkXqnWmq65Sxflfvp0Lc6MdKjMUxGaeOKWJqz5SNiVIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/sourcemap-codec": "^1.6.0", + "@sveltejs/acorn-typescript": "^1.0.13", + "@types/estree": "^1.0.9", + "acorn": "^8.18.0", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.9.2", + "esm-env": "^1.2.1", + "esrap": "^2.3.6", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.6.tgz", + "integrity": "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.3", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/svelte/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", + "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.9", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.5.tgz", + "integrity": "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/panel/web/package.json b/panel/web/package.json new file mode 100644 index 0000000..107c9a5 --- /dev/null +++ b/panel/web/package.json @@ -0,0 +1,26 @@ +{ + "name": "scopenet-panel-web", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@fontsource-variable/inter": "^5.3.0", + "@fontsource-variable/jetbrains-mono": "^5.3.0", + "@fontsource-variable/outfit": "^5.3.0", + "@fontsource-variable/plus-jakarta-sans": "^5.3.0", + "@fontsource-variable/space-grotesk": "^5.3.0", + "@lucide/svelte": "^1.48.0", + "@sveltejs/vite-plugin-svelte": "^7.3.1", + "@tsconfig/svelte": "^5.0.8", + "svelte": "^5.57.1", + "svelte-check": "^4.7.6", + "typescript": "~5.9.0", + "vite": "^8.3.1" + } +} diff --git a/panel/web/public/favicon.svg b/panel/web/public/favicon.svg new file mode 100644 index 0000000..a36e405 --- /dev/null +++ b/panel/web/public/favicon.svg @@ -0,0 +1 @@ + diff --git a/panel/web/src/App.svelte b/panel/web/src/App.svelte new file mode 100644 index 0000000..7a22647 --- /dev/null +++ b/panel/web/src/App.svelte @@ -0,0 +1,80 @@ + + +{#if checking} +
+{:else if !session.user} + +{:else} +
+ +
+ {#key route.name + route.params.join('/')} + {#if route.name === 'instances' && route.params[0]} + + {:else if route.name === 'instances'} + + {:else if route.name === 'users'} + + {:else if route.name === 'branding'} + (brand = b)} /> + {:else if route.name === 'settings'} + + {:else} + + {/if} + {/key} +
+
+{/if} + + + + diff --git a/panel/web/src/app.css b/panel/web/src/app.css new file mode 100644 index 0000000..ab3620f --- /dev/null +++ b/panel/web/src/app.css @@ -0,0 +1,311 @@ +:root { + --bg: #090a12; + --bg-2: #0e1019; + --surface: #12141f; + --surface-2: #181b29; + --surface-3: #20243a; + --line: rgba(255, 255, 255, 0.07); + --line-strong: rgba(255, 255, 255, 0.13); + --text: #eef0ff; + --text-2: #b7bbdb; + --muted: #7d82a8; + --accent: #8b6cff; + --accent-2: #22d3ee; + --accent-soft: rgba(139, 108, 255, 0.14); + --good: #34d399; + --warn: #fbbf24; + --bad: #f43f5e; + --radius: 14px; + --radius-sm: 9px; + --shadow: 0 20px 60px -20px rgba(0, 0, 0, 0.6); + --font: 'Outfit Variable', system-ui, sans-serif; + --mono: 'JetBrains Mono Variable', ui-monospace, monospace; + color-scheme: dark; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + height: 100%; + background: var(--bg); + color: var(--text); + font-family: var(--font); + font-size: 15px; + -webkit-font-smoothing: antialiased; +} + +body { + background: + radial-gradient(1200px 600px at 110% -10%, rgba(139, 108, 255, 0.12), transparent 60%), + radial-gradient(900px 500px at -10% 110%, rgba(34, 211, 238, 0.07), transparent 60%), + var(--bg); + background-attachment: fixed; +} + +a { + color: var(--accent-2); + text-decoration: none; +} + +h1, h2, h3, h4 { + margin: 0; + font-weight: 650; + letter-spacing: -0.01em; +} + +h1 { font-size: 1.75rem; } +h2 { font-size: 1.2rem; } +h3 { font-size: 1rem; } + +p { margin: 0; } + +code, .mono { + font-family: var(--mono); + font-size: 0.85em; +} + +::selection { + background: rgba(139, 108, 255, 0.4); +} + +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-thumb { background: var(--surface-3); border-radius: 10px; border: 2px solid var(--bg); } +::-webkit-scrollbar-track { background: transparent; } + +/* ---------- layout primitives ---------- */ + +.page { + padding: 32px 40px 64px; + max-width: 1320px; + margin: 0 auto; + animation: fade-in 0.25s ease; +} + +.page-head { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + margin-bottom: 28px; + flex-wrap: wrap; +} + +.page-head p { + color: var(--muted); + margin-top: 6px; +} + +.row { display: flex; gap: 12px; align-items: center; } +.row.wrap { flex-wrap: wrap; } +.col { display: flex; flex-direction: column; gap: 12px; } +.spacer { flex: 1; } +.grid { display: grid; gap: 16px; } +.muted { color: var(--muted); } +.small { font-size: 0.85rem; } +.tiny { font-size: 0.75rem; } + +.card { + background: linear-gradient(180deg, rgba(255, 255, 255, 0.025), rgba(255, 255, 255, 0.01)), var(--surface); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 20px; +} + +.card.hover { + transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s; +} +.card.hover:hover { + border-color: var(--line-strong); + transform: translateY(-2px); + box-shadow: var(--shadow); +} + +.section-title { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 16px; +} + +.section-title .hint { + margin-left: auto; + font-size: 0.8rem; + color: var(--muted); +} + +/* ---------- controls ---------- */ + +button, .btn { + font: inherit; + cursor: pointer; + border: 1px solid var(--line-strong); + background: var(--surface-2); + color: var(--text); + border-radius: var(--radius-sm); + padding: 9px 16px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-weight: 550; + transition: background 0.15s, border-color 0.15s, transform 0.08s, box-shadow 0.15s, opacity 0.15s; + white-space: nowrap; +} +button:hover { background: var(--surface-3); } +button:active { transform: translateY(1px); } +button:disabled { opacity: 0.5; cursor: not-allowed; } +button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +button.primary { + background: linear-gradient(135deg, var(--accent), #6a4cf0); + border-color: transparent; + box-shadow: 0 8px 24px -10px rgba(139, 108, 255, 0.8); +} +button.primary:hover { filter: brightness(1.1); } + +button.ghost { background: transparent; border-color: transparent; color: var(--text-2); } +button.ghost:hover { background: rgba(255, 255, 255, 0.05); color: var(--text); } +button.danger { color: #ff8a9e; border-color: rgba(244, 63, 94, 0.3); background: rgba(244, 63, 94, 0.08); } +button.danger:hover { background: rgba(244, 63, 94, 0.16); } +button.icon { padding: 8px; } +button.sm { padding: 6px 11px; font-size: 0.85rem; } + +input, select, textarea { + font: inherit; + width: 100%; + background: var(--bg-2); + color: var(--text); + border: 1px solid var(--line-strong); + border-radius: var(--radius-sm); + padding: 10px 12px; + transition: border-color 0.15s, background 0.15s; +} +input:hover, select:hover, textarea:hover { border-color: rgba(255, 255, 255, 0.2); } +input:focus, select:focus, textarea:focus { border-color: var(--accent); background: #0c0e18; outline: none; } +textarea { resize: vertical; min-height: 80px; line-height: 1.5; } +select { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%); background-position: calc(100% - 18px) 55%, calc(100% - 13px) 55%; background-size: 5px 5px; background-repeat: no-repeat; padding-right: 34px; } +input[type='color'] { padding: 0; width: 42px; height: 42px; border-radius: 10px; cursor: pointer; overflow: hidden; } +input[type='color']::-webkit-color-swatch-wrapper { padding: 0; } +input[type='color']::-webkit-color-swatch { border: none; } +input[type='range'] { padding: 0; accent-color: var(--accent); border: none; background: transparent; } +input[type='checkbox'] { width: auto; accent-color: var(--accent); } + +label.field { + display: flex; + flex-direction: column; + gap: 7px; + font-size: 0.85rem; + color: var(--text-2); + font-weight: 500; +} +label.field .help { color: var(--muted); font-weight: 400; font-size: 0.78rem; } + +.badge { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 9px; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; + background: rgba(255, 255, 255, 0.06); + color: var(--text-2); + border: 1px solid var(--line); + white-space: nowrap; +} +.badge.accent { background: var(--accent-soft); color: #c4b5ff; border-color: rgba(139, 108, 255, 0.3); } +.badge.good { background: rgba(52, 211, 153, 0.12); color: #6ee7b7; border-color: rgba(52, 211, 153, 0.25); } +.badge.warn { background: rgba(251, 191, 36, 0.12); color: #fcd34d; border-color: rgba(251, 191, 36, 0.25); } +.badge.bad { background: rgba(244, 63, 94, 0.12); color: #fda4af; border-color: rgba(244, 63, 94, 0.25); } + +.segmented { + display: inline-flex; + background: var(--bg-2); + border: 1px solid var(--line-strong); + border-radius: var(--radius-sm); + padding: 3px; + gap: 2px; +} +.segmented button { + border: none; + background: transparent; + padding: 7px 14px; + border-radius: 7px; + color: var(--muted); + font-size: 0.88rem; +} +.segmented button.active { background: var(--surface-3); color: var(--text); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } + +.tabs { + display: flex; + gap: 4px; + border-bottom: 1px solid var(--line); + margin-bottom: 24px; + overflow-x: auto; +} +.tabs button { + background: none; + border: none; + border-bottom: 2px solid transparent; + border-radius: 0; + color: var(--muted); + padding: 12px 16px; +} +.tabs button:hover { color: var(--text); background: none; } +.tabs button.active { color: var(--text); border-bottom-color: var(--accent); } + +table.table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} +.table th { + text-align: left; + color: var(--muted); + font-weight: 500; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 10px 14px; + border-bottom: 1px solid var(--line); +} +.table td { + padding: 12px 14px; + border-bottom: 1px solid var(--line); + vertical-align: middle; +} +.table tr:last-child td { border-bottom: none; } +.table tbody tr { transition: background 0.1s; } +.table tbody tr:hover { background: rgba(255, 255, 255, 0.02); } + +.empty { + text-align: center; + padding: 56px 20px; + color: var(--muted); +} +.empty h3 { color: var(--text); margin: 12px 0 6px; } + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} + +.spin { animation: spin 0.9s linear infinite; } + +@keyframes spin { to { transform: rotate(360deg); } } +@keyframes fade-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } } + +@media (max-width: 900px) { + .page { padding: 20px 16px 48px; } +} diff --git a/panel/web/src/components/Avatar.svelte b/panel/web/src/components/Avatar.svelte new file mode 100644 index 0000000..e1c171c --- /dev/null +++ b/panel/web/src/components/Avatar.svelte @@ -0,0 +1,16 @@ + + +{#if failed} + {name.slice(0, 1).toUpperCase()} +{:else} + (failed = true)} /> +{/if} + + diff --git a/panel/web/src/components/BarChart.svelte b/panel/web/src/components/BarChart.svelte new file mode 100644 index 0000000..c3abb3b --- /dev/null +++ b/panel/web/src/components/BarChart.svelte @@ -0,0 +1,77 @@ + + +
+ + {#each ticks as t} + + {t.toLocaleString()} + {/each} + {#each data as d, i} + {@const x = PAD_L + i * band + (band - barW) / 2} + + {#if i % 2 === data.length % 2 || data.length < 8} + {fmtDay(d.day)} + {/if} + (hover = i)} onmouseleave={() => (hover = null)} + /> + {/each} + + {#if hover !== null} + {@const d = data[hover]} +
+ {d.launches.toLocaleString()} launch{d.launches === 1 ? '' : 'es'} + {fmtDay(d.day)} +
+ {/if} + + + {#each data as d}{/each} +
{label}
{d.day}{d.launches}
+
+ + diff --git a/panel/web/src/components/ColorInput.svelte b/panel/web/src/components/ColorInput.svelte new file mode 100644 index 0000000..bddef74 --- /dev/null +++ b/panel/web/src/components/ColorInput.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/panel/web/src/components/ImageInput.svelte b/panel/web/src/components/ImageInput.svelte new file mode 100644 index 0000000..8ebbf9f --- /dev/null +++ b/panel/web/src/components/ImageInput.svelte @@ -0,0 +1,66 @@ + + +
+ {label} +
+ +
+ + {#if help}{help}{/if} +
+ {#if value} + + {/if} +
+ +
+ + diff --git a/panel/web/src/components/LauncherPreview.svelte b/panel/web/src/components/LauncherPreview.svelte new file mode 100644 index 0000000..bfa00dc --- /dev/null +++ b/panel/web/src/components/LauncherPreview.svelte @@ -0,0 +1,136 @@ + + +
+
+
+ {#if b.background.kind === 'video' && b.background.url} + + + {/if} +
+
+ +
+ {#if b.logo_url}{:else}{/if} + {b.name} + + +
+ +
+ {instanceName.slice(0, 1)} + C + V + + + +
+ +
+
+
1.21.1Fabric
+

{instanceName}

+

{b.tagline}

+
+
+ + {#if b.features.server_status} + 42/100 online + {/if} +
+ {#if b.features.news && news.length} +
+ News + {#each news as n} +
+ {#if n.image_url}{/if} +
+ {#if n.tag}{n.tag}{/if} + {n.title || 'Untitled'} + {n.body} +
+
+ {/each} +
+ {/if} +
+
+
+ + diff --git a/panel/web/src/components/Modal.svelte b/panel/web/src/components/Modal.svelte new file mode 100644 index 0000000..1952d2f --- /dev/null +++ b/panel/web/src/components/Modal.svelte @@ -0,0 +1,44 @@ + + + + +{#if open} + +{/if} + + diff --git a/panel/web/src/components/Sidebar.svelte b/panel/web/src/components/Sidebar.svelte new file mode 100644 index 0000000..c6a4613 --- /dev/null +++ b/panel/web/src/components/Sidebar.svelte @@ -0,0 +1,90 @@ + + + + + diff --git a/panel/web/src/components/Toasts.svelte b/panel/web/src/components/Toasts.svelte new file mode 100644 index 0000000..d0e7fb1 --- /dev/null +++ b/panel/web/src/components/Toasts.svelte @@ -0,0 +1,28 @@ + + +
+ {#each toasts as t (t.id)} +
+ {#if t.kind === 'ok'}{:else if t.kind === 'error'}{:else}{/if} + {t.text} + +
+ {/each} +
+ + diff --git a/panel/web/src/components/Toggle.svelte b/panel/web/src/components/Toggle.svelte new file mode 100644 index 0000000..71b9ace --- /dev/null +++ b/panel/web/src/components/Toggle.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/panel/web/src/components/VersionPicker.svelte b/panel/web/src/components/VersionPicker.svelte new file mode 100644 index 0000000..fc49fb8 --- /dev/null +++ b/panel/web/src/components/VersionPicker.svelte @@ -0,0 +1,96 @@ + + +
+ + + {#if showLoader} +
+ Mod loader +
+ {#each LOADERS as l} + + {/each} +
+
+ + {#if loader !== 'vanilla'} + + {/if} + {/if} +
+ + diff --git a/panel/web/src/lib/api.ts b/panel/web/src/lib/api.ts new file mode 100644 index 0000000..d85b3db --- /dev/null +++ b/panel/web/src/lib/api.ts @@ -0,0 +1,73 @@ +import { logout, session } from './session.svelte'; + +export class ApiError extends Error { + constructor(message: string, public status: number) { + super(message); + } +} + +type Options = { method?: string; body?: unknown; form?: FormData }; + +export async function api(path: string, opts: Options = {}): Promise { + const headers: Record = {}; + if (session.token) headers.Authorization = `Bearer ${session.token}`; + let body: BodyInit | undefined; + if (opts.form) body = opts.form; + else if (opts.body !== undefined) { + headers['Content-Type'] = 'application/json'; + body = JSON.stringify(opts.body); + } + const res = await fetch(path, { method: opts.method ?? (body ? 'POST' : 'GET'), headers, body }); + const text = await res.text(); + let data: any = null; + try { + data = text ? JSON.parse(text) : null; + } catch { + data = text; + } + if (!res.ok) { + if (res.status === 401 && session.token) logout(); + throw new ApiError(data?.error ?? `Request failed (${res.status})`, res.status); + } + return data as T; +} + +export const get = (p: string) => api(p); +export const post = (p: string, body?: unknown) => api(p, { method: 'POST', body: body ?? {} }); +export const put = (p: string, body: unknown) => api(p, { method: 'PUT', body }); +export const patch = (p: string, body: unknown) => api(p, { method: 'PATCH', body }); +export const del = (p: string) => api(p, { method: 'DELETE' }); + +export async function uploadMedia(file: File): Promise { + const form = new FormData(); + form.append('file', file); + const res = await api<{ url: string }>('/api/admin/uploads', { method: 'POST', form }); + return res.url; +} + +export function formatBytes(n: number): string { + if (!n) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB']; + const i = Math.min(units.length - 1, Math.floor(Math.log(n) / Math.log(1024))); + return `${(n / 1024 ** i).toFixed(i ? 1 : 0)} ${units[i]}`; +} + +export function timeAgo(iso?: string | null): string { + if (!iso) return 'never'; + const s = (Date.now() - new Date(iso).getTime()) / 1000; + if (s < 60) return 'just now'; + if (s < 3600) return `${Math.floor(s / 60)}m ago`; + if (s < 86400) return `${Math.floor(s / 3600)}h ago`; + if (s < 86400 * 30) return `${Math.floor(s / 86400)}d ago`; + return new Date(iso).toLocaleDateString(); +} + +export const LOADERS = [ + { id: 'vanilla', label: 'Vanilla' }, + { id: 'fabric', label: 'Fabric' }, + { id: 'quilt', label: 'Quilt' }, + { id: 'forge', label: 'Forge' }, + { id: 'neoforge', label: 'NeoForge' }, +] as const; + +export const loaderLabel = (id: string) => LOADERS.find((l) => l.id === id)?.label ?? id; diff --git a/panel/web/src/lib/router.svelte.ts b/panel/web/src/lib/router.svelte.ts new file mode 100644 index 0000000..bf791ba --- /dev/null +++ b/panel/web/src/lib/router.svelte.ts @@ -0,0 +1,18 @@ +// Tiny hash router: #/instances/abc → { name: 'instances', params: ['abc'] } +function parse() { + const parts = location.hash.replace(/^#\/?/, '').split('/').filter(Boolean).map(decodeURIComponent); + return { name: parts[0] ?? 'dashboard', params: parts.slice(1) }; +} + +export const route = $state(parse()); + +window.addEventListener('hashchange', () => { + const r = parse(); + route.name = r.name; + route.params = r.params; + window.scrollTo({ top: 0 }); +}); + +export function go(path: string) { + location.hash = '#/' + path.replace(/^\//, ''); +} diff --git a/panel/web/src/lib/session.svelte.ts b/panel/web/src/lib/session.svelte.ts new file mode 100644 index 0000000..eeb0bae --- /dev/null +++ b/panel/web/src/lib/session.svelte.ts @@ -0,0 +1,34 @@ +const KEY = 'scopenet.panel.token'; + +function read(): string { + try { + return localStorage.getItem(KEY) ?? ''; + } catch { + return ''; + } +} + +export interface Me { + id: number; + username: string; + uuid: string; + role: string; + groups: string[]; +} + +export const session = $state<{ token: string; user: Me | null }>({ token: read(), user: null }); + +export function setToken(token: string) { + session.token = token; + try { + if (token) localStorage.setItem(KEY, token); + else localStorage.removeItem(KEY); + } catch { + /* storage unavailable: session lasts until reload */ + } +} + +export function logout() { + setToken(''); + session.user = null; +} diff --git a/panel/web/src/lib/toast.svelte.ts b/panel/web/src/lib/toast.svelte.ts new file mode 100644 index 0000000..120aea3 --- /dev/null +++ b/panel/web/src/lib/toast.svelte.ts @@ -0,0 +1,19 @@ +export type Toast = { id: number; kind: 'ok' | 'error' | 'info'; text: string }; + +export const toasts = $state([]); +let next = 1; + +export function toast(text: string, kind: Toast['kind'] = 'ok') { + const id = next++; + toasts.push({ id, kind, text }); + setTimeout(() => dismiss(id), kind === 'error' ? 6000 : 3200); +} + +export function dismiss(id: number) { + const i = toasts.findIndex((t) => t.id === id); + if (i >= 0) toasts.splice(i, 1); +} + +export function toastError(e: unknown) { + toast(e instanceof Error ? e.message : String(e), 'error'); +} diff --git a/panel/web/src/lib/types.ts b/panel/web/src/lib/types.ts new file mode 100644 index 0000000..9c4bf6d --- /dev/null +++ b/panel/web/src/lib/types.ts @@ -0,0 +1,80 @@ +export interface MemoryDefaults { min_mb: number; max_mb: number } +export interface ServerEntry { name: string; address: string; port: number; auto_join: boolean; inject: boolean } + +export interface Instance { + id: string; + name: string; + description: string; + icon_url: string | null; + banner_url: string | null; + mc_version: string; + loader: string; + loader_version: string | null; + source_kind: string; + source_label: string; + source_ref: any; + visibility: 'public' | 'members' | 'groups'; + allowed_groups: string[]; + memory: MemoryDefaults; + jvm_args: string; + server: ServerEntry | null; + featured: boolean; + enabled: boolean; + sort: number; + revision: number; + created_at: string; + updated_at: string; + file_count: number; + total_size: number; + missing_count: number; +} + +export interface FileRow { path: string; url: string; sha1: string; size: number; origin: string; note: string | null } + +export interface NewsItem { id: string; title: string; body: string; image_url: string | null; link: string | null; date: string | null; pinned: boolean; tag: string | null } +export interface SocialLink { label: string; url: string; icon: string } + +export interface Branding { + name: string; + tagline: string; + logo_url: string | null; + icon_url: string | null; + background: { kind: 'gradient' | 'image' | 'video'; url: string | null; blur: number; dim: number }; + colors: { accent: string; accent_2: string; background: string; surface: string; text: string; muted: string; success: string; danger: string }; + font: string; + radius: number; + glass: boolean; + version_label: string; + news: NewsItem[]; + links: SocialLink[]; + features: { news: boolean; server_status: boolean; allow_user_theme: boolean; allow_advanced_java: boolean }; + custom_css: string; +} + +export interface User { + id: number; + username: string; + email: string | null; + role: 'admin' | 'player'; + status: 'active' | 'pending' | 'disabled'; + created_at: string; + last_login: string | null; + uuid: string; + groups: string[]; +} + +export interface Group { id: number; name: string; color: string; members: number } + +export interface Settings { + auth: { + panel_accounts: boolean; + registration: 'closed' | 'open' | 'approval'; + microsoft: boolean; + microsoft_client_id: string | null; + offline_local: boolean; + }; + curseforge_api_key?: string | null; + launcher_download_url: string | null; + curseforge_key_set: boolean; + curseforge_key_from_env: boolean; +} diff --git a/panel/web/src/main.ts b/panel/web/src/main.ts new file mode 100644 index 0000000..a888b19 --- /dev/null +++ b/panel/web/src/main.ts @@ -0,0 +1,11 @@ +import '@fontsource-variable/outfit'; +import '@fontsource-variable/jetbrains-mono'; +// Launcher fonts, so the design preview matches exactly. +import '@fontsource-variable/inter'; +import '@fontsource-variable/space-grotesk'; +import '@fontsource-variable/plus-jakarta-sans'; +import './app.css'; +import { mount } from 'svelte'; +import App from './App.svelte'; + +mount(App, { target: document.getElementById('app')! }); diff --git a/panel/web/src/pages/Branding.svelte b/panel/web/src/pages/Branding.svelte new file mode 100644 index 0000000..8c31b5c --- /dev/null +++ b/panel/web/src/pages/Branding.svelte @@ -0,0 +1,252 @@ + + +
+
+
+

Launcher design

+

Everything here updates every player's launcher live — no rebuild or reinstall.

+
+
+ {#if dirty}{/if} + +
+
+ + {#if b} +
+
+
+ {#each sections as s}{/each} +
+ + {#if section === 'identity'} +
+ + + + + +
+ {:else if section === 'colors'} +
+
+ {#each presets as p} + + {/each} +
+
+ + + + + + + + +
+
+ {:else if section === 'background'} +
+
+ {#each ['gradient', 'image', 'video'] as k} + + {/each} +
+ {#if b.background.kind !== 'gradient'} + + {:else} +

An animated gradient built from your accent colours. The lightest option on player PCs.

+ {/if} + + +
+ {:else if section === 'style'} +
+
+ Font +
+ {#each fonts as f} + + {/each} +
+
+ + +
+ {:else if section === 'news'} +
+ + {#each b.news as n, i (n.id)} +
(dragIndex = i)} ondragover={(e) => e.preventDefault()} ondrop={() => dropNews(i)}> +
+ + + + +
+ +
+ + + +
+ +
+ {:else} +

No news yet

Posts show up on the launcher home screen.

+ {/each} +
+ {:else if section === 'links'} +
+ {#each b.links as l, i} + + {/each} + +
+ {:else if section === 'features'} +
+ + + + +
+ {:else if section === 'css'} +
+

Injected into the launcher after the built-in styles. Useful variables: --accent, --surface, --radius.

+ +
+ {/if} +
+ +
+
+ Live preview + +
+
+
+ {/if} +
+ + diff --git a/panel/web/src/pages/Dashboard.svelte b/panel/web/src/pages/Dashboard.svelte new file mode 100644 index 0000000..8ec6d3f --- /dev/null +++ b/panel/web/src/pages/Dashboard.svelte @@ -0,0 +1,150 @@ + + +
+
+
+

{greeting}, {session.user?.username}

+

Here's what's happening with your launcher.

+
+ {#if stats?.launcher_download_url} + Launcher download + {/if} +
+ +
+ {#each [ + { label: 'Active players (7d)', value: stats?.players_7d, icon: Activity, tone: 'accent' }, + { label: 'Accounts', value: stats?.users, icon: Users, tone: 'cyan' }, + { label: 'Awaiting approval', value: stats?.pending, icon: UserCheck, tone: 'warn', href: '#/users' }, + { label: 'Instances', value: stats?.instances, icon: Boxes, tone: 'green', href: '#/instances' }, + ] as s} + + + {s.value ?? '—'} + {s.label} + + {/each} +
+ +
+
+
+

Game launches

+ {total.toLocaleString()} in the last 14 days +
+ {#if stats} + + {:else} +
+ {/if} +
+ +
+

Getting started

+
+ {#each steps as s} + + {#if s.done}{:else}{/if} + {s.label} + + + {/each} +
+
+ +
+

Popular instances

+ {#if stats?.top_instances?.length} +
+ {#each stats.top_instances as t} + {@const pct = (t.launches / stats.top_instances[0].launches) * 100} +
+ {nameOf(t.instance_id)} + + {t.launches} +
+ {/each} +
+ {:else} +

No launches yet. Stats appear once players start the game.

+ {/if} +
+ +
+

Recent activity

+ {#if stats?.recent?.length} +
+ {#each stats.recent as r} +
+ + {r.username ?? 'Someone'} launched {nameOf(r.instance_id)} + {timeAgo(r.at)} +
+ {/each} +
+ {:else} +

Nothing yet.

+ {/if} +
+
+
+ + diff --git a/panel/web/src/pages/InstanceEditor.svelte b/panel/web/src/pages/InstanceEditor.svelte new file mode 100644 index 0000000..4ae5b37 --- /dev/null +++ b/panel/web/src/pages/InstanceEditor.svelte @@ -0,0 +1,560 @@ + + +{#if form && inst} +
+ All instances +
+
+
{#if form.icon_url}{:else}{/if}
+
+

{inst.name}

+

+ {inst.mc_version} + {loaderLabel(inst.loader)} {inst.loader_version?.replace(`${inst.mc_version}-`, '') ?? ''} + {inst.source_label || 'Vanilla'} + revision {inst.revision} +

+
+
+ +
+ +
+ {#each tabs as t} + + {/each} +
+ + {#if tab === 'general'} +
+
+

Details

+ + + + +
+
+

Performance defaults

+

Players can change these in their settings; these are the starting values.

+ + + +
+
+ {:else if tab === 'source'} +
+ +
+ {inst.source_label || 'Vanilla'} + Minecraft {inst.mc_version} · {loaderLabel(inst.loader)} {inst.loader_version ?? ''} · {inst.file_count} files ({formatBytes(inst.total_size)}) +
+ {#if inst.source_kind !== 'vanilla'} + + {/if} +
+ +
+ + + + +
+ + {#if sourceMode === 'version'} +
+

+ {#if inst.source_kind !== 'vanilla'}Changing versions here overrides what the modpack declared — only do this if you know the pack supports it.{:else}Pick the game version and (optionally) a mod loader. Add mods in the Files tab.{/if} +

+ +
+ {:else if sourceMode === 'upload'} +
+
{ e.preventDefault(); dragging = true; }} + ondragleave={() => (dragging = false)} + ondrop={onDrop} + onclick={() => document.getElementById('pack-file')?.click()} + onkeydown={(e) => e.key === 'Enter' && document.getElementById('pack-file')?.click()} + > + + {#if uploadFile} + {uploadFile.name}{formatBytes(uploadFile.size)} + {:else} + Drop a modpack here + .mrpack, CurseForge .zip, or any instance folder zipped up + {/if} +
+ (uploadFile = (e.target as HTMLInputElement).files?.[0] ?? null)} /> +
+ Plain zip? Choose its Minecraft version and loader +
+
+ +
+ {:else} +
+
{ e.preventDefault(); search(); }}> + + +
+ + {#if selected} +
+ +
+ {#if selected.icon}{/if} +

{selected.title}

{selected.text}

+
+ {#if loadingVersions} +

Loading versions…

+ {:else} +
+ {#each versions as v} +
+
{v.name}{v.meta}
+ {v.date ? new Date(v.date).toLocaleDateString() : ''} + +
+ {/each} +
+ {/if} +
+ {:else if results.length} +
+ {#each results as r} + + {/each} +
+ {:else} +

+ {sourceMode === 'curseforge' ? 'CurseForge needs an API key (Settings → Integrations). ' : ''}Search for a modpack, then pick a version. Mods download straight from the CDN; configs are hosted by this panel. +

+ {/if} +
+ {/if} + {:else if tab === 'files'} + {#if missing.length} +
+
{missing.length} file{missing.length > 1 ? 's' : ''} need a manual upload
+

These CurseForge authors block third-party downloads. Download each file and upload it below into the same folder — it replaces the placeholder automatically.

+ {#each missing as m} +
+ {m.path} + + {#if noteLink(m.note)} Download page{/if} +
+ {/each} +
+ {/if} + +
+
+

{files.length} files

+ + + {#if folder === 'custom'}{/if} + + +
+ {#if files.length} +
+ + + + {#each files as f (f.path)} + + + + + + + {/each} + +
PathSourceSize
{f.path}{#if !f.url}missing{:else}{f.origin === 'pack' ? 'CDN' : f.origin === 'upload' ? 'uploaded' : 'config'}{/if}{formatBytes(f.size)}
+
+ {:else} +

No extra files. Vanilla instances don't need any — add mods, configs or resource packs here.

+ {/if} +
+ {:else if tab === 'server' && form.server} +
+

Built-in server

+

Adds your server to the in-game multiplayer list and shows its live status in the launcher.

+
+ + + +
+ + + {#if form.server.address} + + {/if} +
+ {:else if tab === 'access'} +
+
+

Who can see this instance?

+
+ {#each [ + { id: 'public', icon: Globe, title: 'Everyone', text: 'Including offline and Microsoft accounts' }, + { id: 'members', icon: UsersIcon, title: 'Signed-in players', text: 'Anyone with a panel account' }, + { id: 'groups', icon: Lock, title: 'Specific groups', text: 'Only members of the groups below' }, + ] as v} + + {/each} +
+ {#if form.visibility === 'groups'} +
+ {#each groups as g} + {@const on = form.allowed_groups.includes(g.name)} + + {:else} +

No groups yet — create them on the Players page.

+ {/each} +
+ {/if} +
+
+

Listing

+ + + +
+
+ {/if} +
+ + {#if dirty} +
+ You have unsaved changes + + +
+ {/if} + + {#if importing} +
+
+ +

Importing {importing}

+

Downloading and unpacking the pack. Large packs can take a minute.

+
+
+ {/if} + + +

The instance disappears from every launcher and its hosted files are deleted. Players keep their local worlds.

+ {#snippet footer()} + + + {/snippet} +
+{:else} +

Loading…

+{/if} + + diff --git a/panel/web/src/pages/Instances.svelte b/panel/web/src/pages/Instances.svelte new file mode 100644 index 0000000..c46db82 --- /dev/null +++ b/panel/web/src/pages/Instances.svelte @@ -0,0 +1,147 @@ + + +
+
+
+

Instances

+

Each instance is a playable profile in the launcher: a Minecraft version or a modpack, plus an optional server.

+
+ +
+ + {#if instances === null} +
{#each [1, 2, 3] as _}
{/each}
+ {:else if instances.length === 0} +
+ +

No instances yet

+

Create one to give players something to launch.

+ +
+ {:else} + + {/if} +
+ + + + +
+ Start from +
+ {#each [ + { id: 'vanilla', title: 'Minecraft version', text: 'Vanilla or a bare mod loader' }, + { id: 'modpack', title: 'Modpack', text: 'Modrinth, CurseForge or a .zip' }, + ] as s} + + {/each} +
+
+ {#if draft.source === 'vanilla'} + + {:else} +

You'll pick the modpack on the next screen. Its Minecraft version and loader are set automatically.

+ {/if} + {#snippet footer()} + + + {/snippet} +
+ + diff --git a/panel/web/src/pages/Login.svelte b/panel/web/src/pages/Login.svelte new file mode 100644 index 0000000..5aebce4 --- /dev/null +++ b/panel/web/src/pages/Login.svelte @@ -0,0 +1,59 @@ + + +
+
+
+ +
+ + diff --git a/panel/web/src/pages/Settings.svelte b/panel/web/src/pages/Settings.svelte new file mode 100644 index 0000000..797e239 --- /dev/null +++ b/panel/web/src/pages/Settings.svelte @@ -0,0 +1,109 @@ + + +
+
+
+

Settings

+

How players sign in, and connections to other services.

+
+ +
+ + {#if s} +
+

Panel accounts

+ + {#if s.auth.panel_accounts} +
+ Sign-ups from the launcher +
+ + + +
+ + {s.auth.registration === 'closed' ? 'Only admins create accounts.' : s.auth.registration === 'approval' ? 'Anyone can sign up; you approve them on the Players page.' : 'Anyone can create an account and play immediately.'} + +
+ {/if} +
+ +
+

Offline play

+ +
+ +
+

Microsoft accounts

+ + {#if s.auth.microsoft} + + {/if} +
+ +
+

Integrations

+ + +
+ {/if} +
+ + diff --git a/panel/web/src/pages/Users.svelte b/panel/web/src/pages/Users.svelte new file mode 100644 index 0000000..a388157 --- /dev/null +++ b/panel/web/src/pages/Users.svelte @@ -0,0 +1,247 @@ + + +
+
+
+

Players

+

Accounts sign in from the launcher and play with their username in offline mode.

+
+ +
+ + {#if pending.length} +
+ {pending.length} player{pending.length > 1 ? 's' : ''} waiting for approval +
+ {#each pending as u} + {u.username} + + + {/each} +
+
+ {/if} + +
+
+
+ + {users.length} accounts +
+
+ + + + {#each shown as u (u.id)} + + + + + + + + {/each} + +
PlayerRoleGroupsLast seen
+
+ +
+ {u.username}{#if u.id === session.user?.id} (you){/if} + {u.uuid} +
+
+
+ {#if u.role === 'admin'} Admin{:else}Player{/if} + {#if u.status === 'pending'}Pending{:else if u.status === 'disabled'}Disabled{/if} +
{#each u.groups as g}{g}{/each}
{timeAgo(u.last_login)} + + {#if u.id !== session.user?.id} + + {/if} +
+
+
+ +
+

Groups

+

Use groups to give VIPs, staff or testers access to private instances.

+
+ {#each groups as g (g.id)} +
+ + {g.name} + {g.members} member{g.members === 1 ? '' : 's'} + + +
+ {/each} +
+
+ + + +
+
+
+
+ + + {#if editing} + {#if !editing.id} + + {/if} + + +
+ + +
+ {#if groups.length} +
+ Groups +
+ {#each groups as g} + {@const on = editing.groups?.includes(g.name)} + + {/each} +
+
+ {/if} + {/if} + {#snippet footer()} + + + {/snippet} +
+ + +

They won't be able to sign in any more. This can't be undone.

+ {#snippet footer()} + + + {/snippet} +
+ + diff --git a/panel/web/svelte.config.js b/panel/web/svelte.config.js new file mode 100644 index 0000000..36c98fd --- /dev/null +++ b/panel/web/svelte.config.js @@ -0,0 +1,3 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { preprocess: vitePreprocess() }; diff --git a/panel/web/tsconfig.json b/panel/web/tsconfig.json new file mode 100644 index 0000000..2c84844 --- /dev/null +++ b/panel/web/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "types": ["svelte", "vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.svelte"] +} diff --git a/panel/web/vite.config.ts b/panel/web/vite.config.ts new file mode 100644 index 0000000..482f0dc --- /dev/null +++ b/panel/web/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vite'; +import { svelte } from '@sveltejs/vite-plugin-svelte'; + +// In dev, API calls are proxied to a locally running panel (cargo run -p scopenet-panel). +const target = process.env.PANEL_URL ?? 'http://localhost:8080'; + +export default defineConfig({ + plugins: [svelte()], + server: { + port: 5173, + proxy: { + '/api': target, + '/files': target, + '/uploads': target, + }, + }, + build: { + target: 'es2022', + cssCodeSplit: false, + chunkSizeWarningLimit: 800, + }, +});