From a7c013575a0d64f5b2bf6d0ad98edae1b7e967f4 Mon Sep 17 00:00:00 2001 From: huyan <2971427998@qq.com> Date: Mon, 27 Jul 2026 15:02:47 +0800 Subject: [PATCH 01/21] build(frontend): scaffold vite react typescript workspace The repository has no frontend workspace yet, so Issue #58 needs a buildable base before any module code can land. Add the Vite + React + TypeScript project with Tailwind, oxlint and Vitest configuration, the @/ path alias, and a dev proxy forwarding /api to the local backend on port 8000. The workspace now builds, type-checks and runs tests, giving later commits somewhere to put shared, entities and page code. --- .gitignore | 4 + frontend/.gitignore | 24 + frontend/.oxlintrc.json | 8 + frontend/index.html | 13 + frontend/package-lock.json | 3136 +++++++++++++++++++++++++++++++++++ frontend/package.json | 33 + frontend/public/favicon.svg | 1 + frontend/public/icons.svg | 24 + frontend/src/index.css | 8 + frontend/tsconfig.app.json | 31 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 23 + frontend/vite.config.ts | 26 + frontend/vitest.config.ts | 16 + 14 files changed, 3354 insertions(+) create mode 100644 .gitignore create mode 100644 frontend/.gitignore create mode 100644 frontend/.oxlintrc.json create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/icons.svg create mode 100644 frontend/src/index.css create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 frontend/vitest.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2603d1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# macOS +.DS_Store + +# 依赖与构建产物由各子项目的 .gitignore 负责 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/frontend/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..04807c1 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Windup · 2D 角色资产生成 + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..57c690e --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3136 @@ +{ + "name": "windup-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "windup-frontend", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.3.3", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router": "^8.3.0", + "tailwindcss": "^4.3.3" + }, + "devDependencies": { + "@testing-library/react": "^16.3.2", + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "jsdom": "^29.1.1", + "oxlint": "^1.71.0", + "typescript": "~6.0.2", + "vite": "^8.1.1", + "vitest": "^4.1.10" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.75.0.tgz", + "integrity": "sha512-lutovtFzJqlRaqpZrCqSSGaHZzl9nIxxpjLzhSRLunN6dCLylj0uzlCyQGaQDIys7rrv8kVXiFO+R4Zpn0bX7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.75.0.tgz", + "integrity": "sha512-hXI0hDgHkw4w5nfru72aG7y+2iQJmC4waH/KV6H/hbgA6yAP5jYNx0P9yug15Hs0tWl/+mda3Jjn/2gmDT48tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.75.0.tgz", + "integrity": "sha512-D91BWbK/dMYfCcrghspPIuKs2D9LF4Z/OabVSQjw1AO6PWxArD7teDA48bm0ySFqWDaPVqmQRl5GMWNglTXyrQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.75.0.tgz", + "integrity": "sha512-02mpwzf12BonZ6PT0TuQoomvEh2kVl2WGBIKWezCyToIS+rYkQZ6GXnARBAl9A4Ovm2V+Xe7M4KretyqmmcnJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.75.0.tgz", + "integrity": "sha512-qZJgLnDaBsiL5YESx2t/TZ8eXkL9fEkKoXEdzegROhlz9A0lgyGnZ0dAzJrh7LJAHQl2K9RdRueN2s/9N7+odg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.75.0.tgz", + "integrity": "sha512-7XlaWA5BJD3XpCfrEqjEe6Zseeb14S7QGa304XfwKignRaKQ+eIj775BQ7nIslggWickl4IsPUFqJ+/gAyNHVg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.75.0.tgz", + "integrity": "sha512-av6Tpv8yrcMMMOadOqENBhlsLRcGFXXwoQ0hzHhsmS9FJ4Wioy8we427GbcMe2XTxmL2e60T67H1Dyr3up+tAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.75.0.tgz", + "integrity": "sha512-WcUhd8fHT5plrA14lANevl+hOl815mVI5t2hU21oFWrZKFXIVV/Sr4rWQV0NzSvzBupbMLNc5ErEA6Ehxh5jMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.75.0.tgz", + "integrity": "sha512-UWzp5wRHFe/ESO3+eEaxXsTkYTGLYjnTsi/I5neEacXSItQ6WNleapfOAeA4x2b8nyhJ4uQxqvtv9pHv8kWJtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.75.0.tgz", + "integrity": "sha512-XEVRwGMLKCUKrvhLAz4F6AIh8MJrQVdSZtAmPpRZt9tGPsUnamPOcl3dS/ZQzJnar/Ymgc//+xho0L60Emzuxg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.75.0.tgz", + "integrity": "sha512-mAG4DUXqfLC8cTjMD2kt3jDmVzFREYtDyeLNdLdsCcBc4Zbl2EMuiFektGBilQwkNjYnMvCqJs55U+Hyb+b+jw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.75.0.tgz", + "integrity": "sha512-95hrAvriAlI+pekSomTFIn0+bawMDlDwTNVmdjsFusTHyL2JWh7TWvRNG/Lkim72uN8OiCcO9wcaC6omLP5E3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.75.0.tgz", + "integrity": "sha512-4b6f2+FrtruAESrCqIKcrarzfrSx+wk2QNcp+RT91/Prc+pMQMAfyZ1rG1c3tFQNl8Bc616tx40uNXyxNBRPbQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.75.0.tgz", + "integrity": "sha512-nshAhrUvXFUWOvqQ2soIw7HFNWvpvEV4o0cYSqPtzLiPF5gKyYTDOOTJ6Rn8g8K/iGvPIrbDA4v8+5MvnjJrrg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.75.0.tgz", + "integrity": "sha512-e4jNxLKnxLC6sYBQRxrI2pgIIxnmMtF8U/VwNYcjTT/CLS+spH624cYVnj07bTKwaEWT37/e025isOs6j/0xqA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.75.0.tgz", + "integrity": "sha512-hZ2lH+1qLf/DiEP9UWuQTK2JWj/BgvMB4jhIV4SmNU1wfEiYYX4TynQyAZXx0j9X4qRYizAL042SKaV+8ynh4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.75.0.tgz", + "integrity": "sha512-Ilj6PNzGDS3bCU0MSJH7Msh0NhH+T/mRp2shwg+q+GHeVlPwP5LEboW96aW+3kVKFk6zYZy1Xi5pZkqZh6X8KQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.75.0.tgz", + "integrity": "sha512-QVit2nOEOiPhkmsrksPSkoGCdnZRNkspt8fwoYyP09te1VEbnSj4LAxua4rc8FKTmWkySVe05j8iz9GXYfF1AQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.75.0.tgz", + "integrity": "sha512-DSxnNkBUAYARPwJtR12Ig3deWr8w0H997xP6jy33i+e0SyYJw8FKuz4+cZtpmPEhQmvlPJE3X/2vNxDmLkd/rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "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==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "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/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", + "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/enhanced-resolve": { + "version": "5.24.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "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==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "musl" + ], + "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" + ], + "libc": [ + "glibc" + ], + "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" + ], + "libc": [ + "musl" + ], + "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" + ], + "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" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "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.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/oxlint": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.75.0.tgz", + "integrity": "sha512-m9WzjRcRYA/uqIZDa9tclrieoPJ/ln1QYTKdFx6NUOs8uY5DiHlIwRQoCrHT6OM6O3ww3l2skY5gO7G7ZphE7g==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.75.0", + "@oxlint/binding-android-arm64": "1.75.0", + "@oxlint/binding-darwin-arm64": "1.75.0", + "@oxlint/binding-darwin-x64": "1.75.0", + "@oxlint/binding-freebsd-x64": "1.75.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.75.0", + "@oxlint/binding-linux-arm-musleabihf": "1.75.0", + "@oxlint/binding-linux-arm64-gnu": "1.75.0", + "@oxlint/binding-linux-arm64-musl": "1.75.0", + "@oxlint/binding-linux-ppc64-gnu": "1.75.0", + "@oxlint/binding-linux-riscv64-gnu": "1.75.0", + "@oxlint/binding-linux-riscv64-musl": "1.75.0", + "@oxlint/binding-linux-s390x-gnu": "1.75.0", + "@oxlint/binding-linux-x64-gnu": "1.75.0", + "@oxlint/binding-linux-x64-musl": "1.75.0", + "@oxlint/binding-openharmony-arm64": "1.75.0", + "@oxlint/binding-win32-arm64-msvc": "1.75.0", + "@oxlint/binding-win32-ia32-msvc": "1.75.0", + "@oxlint/binding-win32-x64-msvc": "1.75.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "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.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-router": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.3.0.tgz", + "integrity": "sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==", + "license": "MIT", + "dependencies": { + "cookie-es": "^3.1.1" + }, + "engines": { + "node": ">=22.22.0" + }, + "peerDependencies": { + "react": ">=19.2.7", + "react-dom": ">=19.2.7" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "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/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", + "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.9" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", + "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "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", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "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.3.0", + "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/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..3fb90bb --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "windup-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview", + "test": "vitest run", + "typecheck": "tsc -b" + }, + "dependencies": { + "@tailwindcss/vite": "^4.3.3", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router": "^8.3.0", + "tailwindcss": "^4.3.3" + }, + "devDependencies": { + "@testing-library/react": "^16.3.2", + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "jsdom": "^29.1.1", + "oxlint": "^1.71.0", + "typescript": "~6.0.2", + "vite": "^8.1.1", + "vitest": "^4.1.10" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..f80172a --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,8 @@ +@import 'tailwindcss'; + +/* 全局只放这一点点:其余样式一律走 Tailwind 工具类,避免样式散落各处。 */ +html, +body, +#root { + height: 100%; +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..474b9e6 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* 路径别名:@/xxx -> src/xxx,需与 vite.config.ts 的 resolve.alias 保持一致 */ + "paths": { + "@/*": ["./src/*"] + }, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..ce82f7a --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,26 @@ +import { fileURLToPath, URL } from 'node:url' + +import tailwindcss from '@tailwindcss/vite' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + // 全项目统一用 @/xxx 引用 src 下的模块,避免 ../../.. 相对路径 + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + server: { + proxy: { + // 开发时把 /api 转给本地后端(backend/ 默认跑在 8000) + '/api': { + target: 'http://127.0.0.1:8000', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), + }, + }, + }, +}) diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..34561a2 --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,16 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + test: { + // 纯逻辑测试跑 node;需要 DOM 的用文件顶部 @vitest-environment jsdom 单独声明 + environment: 'node', + include: ['src/**/*.test.ts', 'src/**/*.test.tsx', '../tests/**/*.test.ts', '../tests/**/*.test.tsx'], + }, +}) From 487dc178f7cd7faf9157ad07ec5d9100b1d22dc0 Mon Sep 17 00:00:00 2001 From: huyan <2971427998@qq.com> Date: Mon, 27 Jul 2026 15:03:29 +0800 Subject: [PATCH 02/21] feat(shared): add transport layer with real and mock clients Business code needs one way to reach the backend that works both before and after the API exists, without touching call sites when switching. Add shared/api with real fetch client, mock handlers and response mappers behind a single request surface, plus the async-state hook, PageHeader component and test helpers. Entities can now read and write data through one entry point, and the mock and real implementations stay interchangeable via VITE_USE_MOCK. --- .../src/shared/api/client/mappers/index.ts | 61 ++++++++ frontend/src/shared/api/client/mock/index.ts | 26 ++++ .../api/client/mock/project-handlers.ts | 130 ++++++++++++++++++ frontend/src/shared/api/client/mock/types.ts | 10 ++ frontend/src/shared/api/client/real/index.ts | 29 ++++ frontend/src/shared/api/client/types.ts | 8 ++ frontend/src/shared/api/index.ts | 32 +++++ frontend/src/shared/lib/async-state.ts | 44 ++++++ frontend/src/shared/lib/index.ts | 3 + frontend/src/shared/testing/index.ts | 6 + frontend/src/shared/ui/index.ts | 3 + frontend/src/shared/ui/page-header.tsx | 34 +++++ 12 files changed, 386 insertions(+) create mode 100644 frontend/src/shared/api/client/mappers/index.ts create mode 100644 frontend/src/shared/api/client/mock/index.ts create mode 100644 frontend/src/shared/api/client/mock/project-handlers.ts create mode 100644 frontend/src/shared/api/client/mock/types.ts create mode 100644 frontend/src/shared/api/client/real/index.ts create mode 100644 frontend/src/shared/api/client/types.ts create mode 100644 frontend/src/shared/api/index.ts create mode 100644 frontend/src/shared/lib/async-state.ts create mode 100644 frontend/src/shared/lib/index.ts create mode 100644 frontend/src/shared/testing/index.ts create mode 100644 frontend/src/shared/ui/index.ts create mode 100644 frontend/src/shared/ui/page-header.tsx diff --git a/frontend/src/shared/api/client/mappers/index.ts b/frontend/src/shared/api/client/mappers/index.ts new file mode 100644 index 0000000..a97ed46 --- /dev/null +++ b/frontend/src/shared/api/client/mappers/index.ts @@ -0,0 +1,61 @@ +/** 传输格式的通用转换与错误映射。不认识任何业务概念。 */ + +/** 后端统一响应,见 windup_common/result/response.py(PR #57)。成功 code = 200。 */ +export interface ApiResponse { + code: number + message: string + data: T | null + timestamp?: string +} + +/** 列表响应,多三个分页字段,data 恒为数组。 */ +export interface ApiListResponse { + code: number + message: string + data: T[] + total: number + page: number + page_size: number + timestamp?: string +} + +/** 分页入参,各处列表统一用这份。 */ +export interface PageQuery { + page?: number + pageSize?: number +} + +export interface Paged { + items: T[] + total: number + page: number + pageSize: number +} + +/** code !== 200 时抛出,用于区分业务失败与网络失败。 */ +export class ApiError extends Error { + readonly code: number + + constructor(message: string, code: number) { + super(message) + this.name = 'ApiError' + this.code = code + } +} + +/** 拆掉响应壳,业务失败翻成 ApiError。 */ +export function unwrap(payload: ApiResponse): T | null { + if (payload.code !== 200) throw new ApiError(payload.message, payload.code) + return payload.data +} + +/** 拆列表响应,顺带把 page_size 翻成前端命名。 */ +export function unwrapList(payload: ApiListResponse): Paged { + if (payload.code !== 200) throw new ApiError(payload.message, payload.code) + return { + items: payload.data, + total: payload.total, + page: payload.page, + pageSize: payload.page_size, + } +} diff --git a/frontend/src/shared/api/client/mock/index.ts b/frontend/src/shared/api/client/mock/index.ts new file mode 100644 index 0000000..876bc3c --- /dev/null +++ b/frontend/src/shared/api/client/mock/index.ts @@ -0,0 +1,26 @@ +import type { RequestOptions } from '../types' +import { projectMockHandlers } from './project-handlers' +import type { MockRoute } from './types' + +export type { MockHandler, MockRoute } from './types' + +/** + * mock 分发。与真实现形状一致,业务代码换实现不用改。 + * 后端接口到位后删掉对应 handler 即可,不必整体切换。 + */ +const routes: MockRoute[] = [...projectMockHandlers] + +/** 让加载态在开发时看得见。 */ +const MOCK_LATENCY_MS = 200 + +export async function mockRequest(path: string, options: RequestOptions): Promise { + const method = options.method ?? 'GET' + await new Promise((resolve) => setTimeout(resolve, MOCK_LATENCY_MS)) + + for (const route of routes) { + if (route.method !== method) continue + const matched = route.pattern.exec(path) + if (matched) return route.handler(options, matched.slice(1)) as R + } + throw new Error(`[mock] 尚未实现的接口:${method} ${path}`) +} diff --git a/frontend/src/shared/api/client/mock/project-handlers.ts b/frontend/src/shared/api/client/mock/project-handlers.ts new file mode 100644 index 0000000..190edb4 --- /dev/null +++ b/frontend/src/shared/api/client/mock/project-handlers.ts @@ -0,0 +1,130 @@ +import type { MockRoute } from './types' + +/** + * mock 扮演后端,字段与 ProjectOut 严格一致(含 snake_case),不写成 camelCase。 + * 对照 web/api/project.py(PR #57)。 + */ +interface ProjectDto { + id: number + user_id: number + workflow_id: number | null + project_name: string + character_perspective: number + directional_movement: number + sprite_width: number + sprite_height: number + game_style: string | null + sprite_sample_url: string | null + create_at: string + update_at: string +} + +let nextId = 3 +const store: ProjectDto[] = [ + { + id: 1, + user_id: 1, + workflow_id: null, + project_name: '像素小骑士', + character_perspective: 1, + directional_movement: 2, + sprite_width: 64, + sprite_height: 64, + game_style: '16-bit 像素、暖色调', + sprite_sample_url: null, + create_at: '2026-07-25T10:00:00Z', + update_at: '2026-07-25T10:00:00Z', + }, + { + id: 2, + user_id: 1, + workflow_id: null, + project_name: '森林精灵', + character_perspective: 2, + directional_movement: 1, + sprite_width: 64, + sprite_height: 64, + game_style: null, + sprite_sample_url: null, + create_at: '2026-07-26T09:30:00Z', + update_at: '2026-07-26T09:30:00Z', + }, +] + +export const projectMockHandlers: MockRoute[] = [ + { + method: 'GET', + pattern: /^\/projects$/, + handler: (options) => { + const page = Number(options.query?.page ?? 1) + const pageSize = Number(options.query?.page_size ?? 20) + const rawUserId = options.query?.user_id + const userId = rawUserId === undefined ? null : Number(rawUserId) + const visible = (userId === null ? store : store.filter((item) => item.user_id === userId)) + // 接口文档:列表按项目 ID 倒序,最新创建的在前 + .slice() + .sort((a, b) => b.id - a.id) + const start = (page - 1) * pageSize + return { + code: 200, + message: 'success', + data: visible.slice(start, start + pageSize), + total: visible.length, + page, + page_size: pageSize, + } + }, + }, + { + method: 'GET', + pattern: /^\/projects\/(\d+)$/, + handler: (_options, params) => { + const found = store.find((item) => item.id === Number(params[0])) + return found + ? { code: 200, message: 'success', data: found } + : { code: 404, message: '项目不存在', data: null } + }, + }, + { + method: 'POST', + pattern: /^\/projects$/, + handler: (options) => { + const body = options.body as Partial + const userId = body.user_id ?? 1 + if ( + store.some( + (item) => item.user_id === userId && item.project_name === body.project_name, + ) + ) { + return { code: 400, message: '项目名称已存在', data: null } + } + const now = new Date().toISOString() + const created: ProjectDto = { + id: nextId++, + user_id: userId, + workflow_id: body.workflow_id ?? null, + project_name: body.project_name ?? '未命名项目', + character_perspective: body.character_perspective ?? 1, + directional_movement: body.directional_movement ?? 1, + sprite_width: body.sprite_width ?? 64, + sprite_height: body.sprite_height ?? 64, + game_style: body.game_style ?? null, + sprite_sample_url: body.sprite_sample_url ?? null, + create_at: now, + update_at: now, + } + store.push(created) + return { code: 200, message: '创建成功', data: created } + }, + }, + { + method: 'DELETE', + pattern: /^\/projects\/(\d+)$/, + handler: (_options, params) => { + const index = store.findIndex((item) => item.id === Number(params[0])) + if (index === -1) return { code: 404, message: '项目不存在', data: null } + store.splice(index, 1) + return { code: 200, message: '删除成功', data: null } + }, + }, +] diff --git a/frontend/src/shared/api/client/mock/types.ts b/frontend/src/shared/api/client/mock/types.ts new file mode 100644 index 0000000..11eea56 --- /dev/null +++ b/frontend/src/shared/api/client/mock/types.ts @@ -0,0 +1,10 @@ +import type { RequestOptions } from '../types' + +export type MockHandler = (options: RequestOptions, params: string[]) => unknown + +export interface MockRoute { + method: string + /** 捕获组作为 params 传给 handler。 */ + pattern: RegExp + handler: MockHandler +} diff --git a/frontend/src/shared/api/client/real/index.ts b/frontend/src/shared/api/client/real/index.ts new file mode 100644 index 0000000..42ae013 --- /dev/null +++ b/frontend/src/shared/api/client/real/index.ts @@ -0,0 +1,29 @@ +import { ApiError } from '../mappers' +import type { RequestOptions } from '../types' + +/** 开发时经 vite proxy 转发到 8000。 */ +const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? '/api' + +function buildUrl(path: string, query?: RequestOptions['query']): string { + if (!query) return `${BASE_URL}${path}` + const search = new URLSearchParams() + for (const [key, value] of Object.entries(query)) { + if (value !== undefined) search.set(key, String(value)) + } + const qs = search.toString() + return qs ? `${BASE_URL}${path}?${qs}` : `${BASE_URL}${path}` +} + +/** 发一次真实请求,返回后端原始响应体(含 code/message/data 壳)。 */ +export async function realRequest(path: string, options: RequestOptions): Promise { + const { method = 'GET', body, query } = options + const response = await fetch(buildUrl(path, query), { + method, + headers: body === undefined ? undefined : { 'Content-Type': 'application/json' }, + body: body === undefined ? undefined : JSON.stringify(body), + }) + if (!response.ok) { + throw new ApiError(`请求失败:HTTP ${response.status}`, response.status) + } + return (await response.json()) as R +} diff --git a/frontend/src/shared/api/client/types.ts b/frontend/src/shared/api/client/types.ts new file mode 100644 index 0000000..fa20490 --- /dev/null +++ b/frontend/src/shared/api/client/types.ts @@ -0,0 +1,8 @@ +/** 真实现与 mock 共同遵守的请求形状。 */ +export interface RequestOptions { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' + /** 自动序列化并带 Content-Type。 */ + body?: unknown + /** 值为 undefined 的键会被丢掉。 */ + query?: Record +} diff --git a/frontend/src/shared/api/index.ts b/frontend/src/shared/api/index.ts new file mode 100644 index 0000000..ca650c3 --- /dev/null +++ b/frontend/src/shared/api/index.ts @@ -0,0 +1,32 @@ +/** + * 只管传输:拼 URL、发请求、拆响应壳、切换 mock,不认识业务概念。 + * 唯一消费者是 entities,pages / features 不应直接用。 + */ +import { mockRequest } from './client/mock' +import { unwrap, unwrapList } from './client/mappers' +import type { ApiListResponse, ApiResponse, Paged } from './client/mappers' +import { realRequest } from './client/real' +import type { RequestOptions } from './client/types' + +/** 两种实现对外一致,业务代码不感知。.env.local 里 VITE_USE_MOCK=false 切真后端。 */ +const USE_MOCK = import.meta.env.VITE_USE_MOCK !== 'false' + +const send = (path: string, options: RequestOptions): Promise => + USE_MOCK ? mockRequest(path, options) : realRequest(path, options) + +/** 取单个对象。code !== 200 时抛 ApiError,调用方不必判断 code。 */ +export async function request(path: string, options: RequestOptions = {}): Promise { + return unwrap(await send>(path, options)) +} + +/** 取列表。 */ +export async function requestList( + path: string, + options: RequestOptions = {}, +): Promise> { + return unwrapList(await send>(path, options)) +} + +export { ApiError } from './client/mappers' +export type { ApiListResponse, ApiResponse, Paged, PageQuery } from './client/mappers' +export type { RequestOptions } from './client/types' diff --git a/frontend/src/shared/lib/async-state.ts b/frontend/src/shared/lib/async-state.ts new file mode 100644 index 0000000..fa6be66 --- /dev/null +++ b/frontend/src/shared/lib/async-state.ts @@ -0,0 +1,44 @@ +import { useEffect, useState } from 'react' + +/** + * entities 层所有 useXxx 的返回形状。 + * 自己定义而不直接暴露某个请求库的类型,是为了以后换实现时页面不用改。 + */ +export interface AsyncState { + data: T | null + loading: boolean + error: Error | null + /** 失败重试或外部操作后刷新。 */ + refresh: () => void +} + +/** deps 变化时重拉;旧请求结果会被丢弃,避免慢请求覆盖新数据。 */ +export function useAsync(factory: () => Promise, deps: unknown[]): AsyncState { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [tick, setTick] = useState(0) + + useEffect(() => { + let stale = false + setLoading(true) + setError(null) + factory() + .then((result) => { + if (!stale) setData(result) + }) + .catch((cause: unknown) => { + if (!stale) setError(cause instanceof Error ? cause : new Error(String(cause))) + }) + .finally(() => { + if (!stale) setLoading(false) + }) + return () => { + stale = true + } + // factory 每次渲染都是新函数,依赖由调用方经 deps 声明 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [...deps, tick]) + + return { data, loading, error, refresh: () => setTick((n) => n + 1) } +} diff --git a/frontend/src/shared/lib/index.ts b/frontend/src/shared/lib/index.ts new file mode 100644 index 0000000..4f3f642 --- /dev/null +++ b/frontend/src/shared/lib/index.ts @@ -0,0 +1,3 @@ +/** 单一职责的纯工具。 */ +export { useAsync } from './async-state' +export type { AsyncState } from './async-state' diff --git a/frontend/src/shared/testing/index.ts b/frontend/src/shared/testing/index.ts new file mode 100644 index 0000000..9163d12 --- /dev/null +++ b/frontend/src/shared/testing/index.ts @@ -0,0 +1,6 @@ +/** 通用测试辅助工具。业务无关,各层测试共用。 */ + +/** 等一个 tick,让 mock 的延迟与 React 状态更新落地。 */ +export function flush(ms = 0): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/frontend/src/shared/ui/index.ts b/frontend/src/shared/ui/index.ts new file mode 100644 index 0000000..1dc5067 --- /dev/null +++ b/frontend/src/shared/ui/index.ts @@ -0,0 +1,3 @@ +/** 业务无关的 UI 基础件。 */ +export { PageHeader } from './page-header' +export type { PageHeaderProps } from './page-header' diff --git a/frontend/src/shared/ui/page-header.tsx b/frontend/src/shared/ui/page-header.tsx new file mode 100644 index 0000000..dcc57cf --- /dev/null +++ b/frontend/src/shared/ui/page-header.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from 'react' + +export interface PageHeaderProps { + /** 页面标题。 */ + title: string + /** 补充说明,如「项目 3 · 生成动作」。 */ + subtitle?: string + /** 不传则不显示返回。 */ + onBack?: () => void + /** 本页特有操作。 */ + actions?: ReactNode +} + +/** 页内头部。 */ +export function PageHeader({ title, subtitle, onBack, actions }: PageHeaderProps) { + return ( +
+
+ {onBack ? ( + + ) : null} +

{title}

+ {subtitle ?

{subtitle}

: null} +
+ {actions ?
{actions}
: null} +
+ ) +} From 8536854adf8471774da12bda1f074edc6e2efdb6 Mon Sep 17 00:00:00 2001 From: huyan <2971427998@qq.com> Date: Mon, 27 Jul 2026 15:05:51 +0800 Subject: [PATCH 03/21] feat(entities): add data module public interface Every business module depends on the data layer, so its exports have to be fixed before page or feature code can be written against them. Add the entities module with project, character, workflow-run, action-template and wearable slices behind a single index, including workflow selectors and local persistence while the backend does not store workflows yet. The backend can now read one file to see which endpoints and fields are expected, and callers keep the same signatures once the API takes over. --- .../src/entities/action-template/index.ts | 27 ++++ frontend/src/entities/character/index.ts | 58 +++++++++ frontend/src/entities/character/types.ts | 68 +++++++++++ frontend/src/entities/index.ts | 79 ++++++++++++ frontend/src/entities/project/api.ts | 115 ++++++++++++++++++ frontend/src/entities/project/index.ts | 21 ++++ frontend/src/entities/project/types.ts | 57 +++++++++ frontend/src/entities/wearable/index.ts | 20 +++ .../workflow-run/api/create-workflow-run.ts | 21 ++++ .../workflow-run/api/get-workflow-run.ts | 14 +++ .../entities/workflow-run/api/local-store.ts | 69 +++++++++++ .../workflow-run/api/submit-workflow-step.ts | 31 +++++ frontend/src/entities/workflow-run/index.ts | 57 +++++++++ .../entities/workflow-run/model/queries.ts | 5 + .../entities/workflow-run/model/selectors.ts | 92 ++++++++++++++ .../src/entities/workflow-run/model/task.ts | 26 ++++ .../src/entities/workflow-run/model/types.ts | 66 ++++++++++ 17 files changed, 826 insertions(+) create mode 100644 frontend/src/entities/action-template/index.ts create mode 100644 frontend/src/entities/character/index.ts create mode 100644 frontend/src/entities/character/types.ts create mode 100644 frontend/src/entities/index.ts create mode 100644 frontend/src/entities/project/api.ts create mode 100644 frontend/src/entities/project/index.ts create mode 100644 frontend/src/entities/project/types.ts create mode 100644 frontend/src/entities/wearable/index.ts create mode 100644 frontend/src/entities/workflow-run/api/create-workflow-run.ts create mode 100644 frontend/src/entities/workflow-run/api/get-workflow-run.ts create mode 100644 frontend/src/entities/workflow-run/api/local-store.ts create mode 100644 frontend/src/entities/workflow-run/api/submit-workflow-step.ts create mode 100644 frontend/src/entities/workflow-run/index.ts create mode 100644 frontend/src/entities/workflow-run/model/queries.ts create mode 100644 frontend/src/entities/workflow-run/model/selectors.ts create mode 100644 frontend/src/entities/workflow-run/model/task.ts create mode 100644 frontend/src/entities/workflow-run/model/types.ts diff --git a/frontend/src/entities/action-template/index.ts b/frontend/src/entities/action-template/index.ts new file mode 100644 index 0000000..5b34111 --- /dev/null +++ b/frontend/src/entities/action-template/index.ts @@ -0,0 +1,27 @@ +import { useAsync } from '@/shared/lib' +import type { AsyncState } from '@/shared/lib' + +/** + * 可复用的动作模板,项目下属而非全站总库。 + * 复用能力一期之后做,先占好数据入口。 + */ + +export interface ActionTemplate { + id: number + projectId: number + name: string + /** 通常取第一帧。 */ + previewImageUrl: string | null + /** 由后端给出,前端不写死。 */ + frameCount: number + hasDisplacement: boolean +} + +export async function fetchActionTemplates(_projectId: number): Promise { + throw new Error('not implemented:等待后端 GET /projects/{id}/action-templates') +} + +/** 订阅动作模板列表。 */ +export function useActionTemplates(projectId: number): AsyncState { + return useAsync(() => fetchActionTemplates(projectId), [projectId]) +} diff --git a/frontend/src/entities/character/index.ts b/frontend/src/entities/character/index.ts new file mode 100644 index 0000000..2117711 --- /dev/null +++ b/frontend/src/entities/character/index.ts @@ -0,0 +1,58 @@ +import { useAsync } from '@/shared/lib' +import type { AsyncState } from '@/shared/lib' +import type { Action, Character, CreateCharacterInput } from './types' + +/** 角色、动作、帧。后端接口未提供,下面的签名即我们提给后端的需求。 */ + +export type { + Action, + ActionKind, + ActionStatus, + Character, + CreateCharacterInput, + Frame, + FrameQcResult, +} from './types' + +/** 带全部动作与帧。审核台进入只需这一个调用。 */ +export async function fetchCharacter(_id: number): Promise { + throw new Error('not implemented:等待后端 GET /characters/{id}') +} + +/** 资产库与项目详情页用。 */ +export async function fetchCharactersByProject(_projectId: number): Promise { + throw new Error('not implemented:等待后端 GET /projects/{id}/characters') +} + +/** 建角色并生成母版。异步任务,返回时 templateImageUrl 可能仍为 null。 */ +export async function createCharacter(_input: CreateCharacterInput): Promise { + throw new Error('not implemented:等待后端 POST /characters') +} + +/** 确认母版后才能加动作。 */ +export async function confirmTemplate(_characterId: number): Promise { + throw new Error('not implemented:等待后端 POST /characters/{id}/template/confirm') +} + +/** 加一个动作,此时还未生成。 */ +export async function addAction( + _characterId: number, + _input: { name: string; kind: 'preset' | 'custom'; templateId?: number }, +): Promise { + throw new Error('not implemented:等待后端 POST /characters/{id}/actions') +} + +/** 候选转正式。新候选不覆盖正式资产。 */ +export async function confirmAction(_actionId: number): Promise { + throw new Error('not implemented:等待后端 POST /actions/{id}/confirm') +} + +/** 订阅一个角色。 */ +export function useCharacter(id: number): AsyncState { + return useAsync(() => fetchCharacter(id), [id]) +} + +/** 订阅项目下的角色列表。 */ +export function useCharacters(projectId: number): AsyncState { + return useAsync(() => fetchCharactersByProject(projectId), [projectId]) +} diff --git a/frontend/src/entities/character/types.ts b/frontend/src/entities/character/types.ts new file mode 100644 index 0000000..0e6fa73 --- /dev/null +++ b/frontend/src/entities/character/types.ts @@ -0,0 +1,68 @@ +/** 角色、动作、帧。后端接口尚未提供,形状按界面需要先定,待与后端对齐。 */ + +/** 预设动作(行走/奔跑/跳跃/待机等 6–10 个)或自定义。 */ +export type ActionKind = 'preset' | 'custom' + +/** 动作在生成流水线上的位置。 */ +export type ActionStatus = + /** 已加入工作流但还没开始生成 */ + | 'planned' + /** 后端任务进行中 */ + | 'generating' + /** 生成完成,是候选,还没被用户确认 */ + | 'candidate' + /** 用户确认后成为正式资产 */ + | 'confirmed' + /** 生成失败 */ + | 'failed' + +/** 系统质检结论。系统通过不等于人工通过,分开记。 */ +export type FrameQcResult = 'pending' | 'passed' | 'failed' + +export interface Frame { + /** 动作内序号。一个动作几帧由后端决定,前端不写死。 */ + index: number + imageUrl: string + + qc: FrameQcResult + /** 人工退回。不设「通过此帧」,故是布尔量而非三态。 */ + rejected: boolean +} + +export interface Action { + id: number + characterId: number + + name: string + kind: ActionKind + status: ActionStatus + /** 播放时是否位移:跳跃有,待机、蹲下没有。由后端按动作标签给出。 */ + hasDisplacement: boolean + frames: Frame[] + /** + * 生成它的工作流 id,审核台退回单帧后据此跳回编辑器定位。 + * 类型必须与 WorkflowRun.id 一致,否则这条恢复链在类型上就是断的。 + */ + sourceWorkflowId: string | null +} + +/** 项目下的核心资产,带一整套动作。 */ +export interface Character { + id: number + projectId: number + name: string + /** 母版图,确认后作为后续动作的一致性基准。 */ + templateImageUrl: string | null + actions: Action[] + createdAt: string + updatedAt: string +} + +export interface CreateCharacterInput { + projectId: number + name: string + /** 交给模型生成母版。 */ + description: string + + referenceImageUrl?: string | null +} diff --git a/frontend/src/entities/index.ts b/frontend/src/entities/index.ts new file mode 100644 index 0000000..ee69534 --- /dev/null +++ b/frontend/src/entities/index.ts @@ -0,0 +1,79 @@ +/** + * 模块一:数据层。本模块唯一对外入口,外部不得 import 内部文件。 + * + * 这份清单是前后端契约的落点,但目前只有一部分算「已确认」: + * + * - 【已对接】Project —— 字段与路径对照后端 PR #57 的真实实现。 + * - 【提案·待双方 review】Character / WorkflowRun / ActionTemplate / Wearable —— + * 后端接口尚未提供,签名是前端按界面需要拟的,等同于向后端提的需求; + * 未经前后端一起走查前不得当作已定契约。阻断项见 frontend/README.md + * 「给后端的问题清单」,代码里对应 TODO(对后端)。 + */ + +/* 项目 —— 已对接后端 PR #57 */ +export { + CHARACTER_PERSPECTIVE, + DIRECTIONAL_MOVEMENT, + SPRITE_SIZES, + createProject, + deleteProject, + fetchProject, + fetchProjects, + uploadImage, + useProject, + useProjects, +} from './project' +export type { CreateProjectInput, Project } from './project' + +/* 角色 / 动作 / 帧 —— 提案,待与后端 review */ +export { + addAction, + confirmAction, + confirmTemplate, + createCharacter, + fetchCharacter, + fetchCharactersByProject, + useCharacter, + useCharacters, +} from './character' +export type { + Action, + ActionKind, + ActionStatus, + Character, + CreateCharacterInput, + Frame, + FrameQcResult, +} from './character' + +/* 工作流数据 —— 提案,待与后端 review */ +export { + availableCommands, + createWorkflowRun, + currentStep, + fetchWorkflowRun, + locateFrame, + subscribeTask, + submitWorkflowStep, + suggestNextCommand, + useWorkflowRun, + workflowRunKeys, +} from './workflow-run' +export type { + CreateWorkflowRunInput, + WorkflowCommand, + WorkflowCommandKind, + WorkflowDriver, + WorkflowLocation, + WorkflowRun, + WorkflowRunStatus, + WorkflowStep, + WorkflowStepStatus, + WorkflowStepType, +} from './workflow-run' + +/* 资产库 —— 提案,待与后端 review */ +export { fetchActionTemplates, useActionTemplates } from './action-template' +export type { ActionTemplate } from './action-template' +export { fetchWearables, useWearables } from './wearable' +export type { Wearable } from './wearable' diff --git a/frontend/src/entities/project/api.ts b/frontend/src/entities/project/api.ts new file mode 100644 index 0000000..62eb7cf --- /dev/null +++ b/frontend/src/entities/project/api.ts @@ -0,0 +1,115 @@ +import { ApiError, request, requestList } from '@/shared/api' +import type { Paged, PageQuery } from '@/shared/api' +import type { CreateProjectInput, Project } from './types' + +const SUPPORTED_IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']) + +/** 后端原始形状,只在本文件出现;出了这里全项目只认 Project。 */ +interface ProjectDto { + id: number + user_id: number + workflow_id: number | null + project_name: string + character_perspective: number + directional_movement: number + sprite_width: number + sprite_height: number + game_style: string | null + sprite_sample_url: string | null + create_at: string + update_at: string +} + +/** TODO(对后端):user_id 应由后端从 token 推出,不该前端传。暂时写死。 */ +const CURRENT_USER_ID = 1 + +/** 后端形状 → 业务对象。 */ +function toProject(dto: ProjectDto): Project { + return { + id: dto.id, + ownerId: dto.user_id, + workflowId: dto.workflow_id, + name: dto.project_name, + perspective: dto.character_perspective, + directionalMovement: dto.directional_movement, + spriteSize: { width: dto.sprite_width, height: dto.sprite_height }, + gameStyle: dto.game_style, + sampleImageUrl: dto.sprite_sample_url, + createdAt: dto.create_at, + updatedAt: dto.update_at, + } +} + +/** GET /projects */ +export async function fetchProjects(query: PageQuery = {}): Promise> { + const paged = await requestList('/projects', { + query: { + page: query.page ?? 1, + page_size: query.pageSize ?? 20, + user_id: CURRENT_USER_ID, + }, + }) + return { ...paged, items: paged.items.map(toProject) } +} + +/** GET /projects/{id},不存在时抛 ApiError(404)。 */ +export async function fetchProject(id: number): Promise { + const dto = await request(`/projects/${id}`) + if (!dto) throw new Error(`项目 ${id} 返回为空`) + return toProject(dto) +} + +/** POST /projects,重名时抛 ApiError(400)。 */ +export async function createProject(input: CreateProjectInput): Promise { + const dto = await request('/projects', { + method: 'POST', + body: { + user_id: CURRENT_USER_ID, + workflow_id: null, + project_name: input.name, + character_perspective: input.perspective, + directional_movement: input.directionalMovement, + sprite_width: input.spriteSize.width, + sprite_height: input.spriteSize.height, + game_style: input.gameStyle ?? null, + sprite_sample_url: input.sampleImageUrl ?? null, + }, + }) + if (!dto) throw new Error('创建项目未返回数据') + return toProject(dto) +} + +/** DELETE /projects/{id} */ +export async function deleteProject(id: number): Promise { + await request(`/projects/${id}`, { method: 'DELETE' }) +} + +/** + * POST /upload/image —— 上传参考图,返回可直接填进 sampleImageUrl 的 URL。 + * + * 走 multipart 而不是 JSON,所以不经 shared/api 的 request():那条通道只处理 + * JSON 请求体。不要手动设 Content-Type,浏览器会自动补 multipart boundary。 + * 后端白名单为 jpeg/png/webp/gif;前端先做同一检查,最终仍以后端校验为准。 + */ +export async function uploadImage(file: File): Promise { + if (!SUPPORTED_IMAGE_TYPES.has(file.type)) { + throw new ApiError('仅支持 jpg/png/webp/gif 图片', 400) + } + + const form = new FormData() + form.append('file', file) + + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL ?? '/api'}/upload/image`, { + method: 'POST', + body: form, + }) + const payload = (await response.json()) as { + code: number + message: string + data: { url: string } | null + } + if (payload.code !== 200 || !payload.data) { + throw new ApiError(payload.message, payload.code) + } + return payload.data.url +} diff --git a/frontend/src/entities/project/index.ts b/frontend/src/entities/project/index.ts new file mode 100644 index 0000000..f18a8c4 --- /dev/null +++ b/frontend/src/entities/project/index.ts @@ -0,0 +1,21 @@ +import { useAsync } from '@/shared/lib' +import type { AsyncState } from '@/shared/lib' +import type { Paged, PageQuery } from '@/shared/api' +import { fetchProject, fetchProjects } from './api' +import type { Project } from './types' + +/** 项目。后端 GET/POST /projects、GET/DELETE /projects/{id} 已实现(PR #57)。 */ + +export { createProject, deleteProject, fetchProject, fetchProjects, uploadImage } from './api' +export { CHARACTER_PERSPECTIVE, DIRECTIONAL_MOVEMENT, SPRITE_SIZES } from './types' +export type { CreateProjectInput, Project } from './types' + +/** 订阅项目列表。 */ +export function useProjects(query: PageQuery = {}): AsyncState> { + return useAsync(() => fetchProjects(query), [query.page, query.pageSize]) +} + +/** 订阅单个项目。 */ +export function useProject(id: number): AsyncState { + return useAsync(() => fetchProject(id), [id]) +} diff --git a/frontend/src/entities/project/types.ts b/frontend/src/entities/project/types.ts new file mode 100644 index 0000000..f4be1d7 --- /dev/null +++ b/frontend/src/entities/project/types.ts @@ -0,0 +1,57 @@ +/** + * 项目:角色生成的容器,保存全局约束(视角、尺寸、画风)。 + * 字段对照后端 ProjectOut(PR #57),命名转换在 ./api.ts。 + */ +export interface Project { + id: number + /** 后端目前要求前端显式传 user_id,无登录态前写死。 */ + ownerId: number + /** 未开始生成时为 null。 */ + workflowId: number | null + /** 后端限制 1–20 字符,同一用户下不可重名。 */ + name: string + /** 游戏视角,见 CHARACTER_PERSPECTIVE。 */ + perspective: number + /** 移动方向,见 DIRECTIONAL_MOVEMENT。 */ + directionalMovement: number + /** 后端校验 32–2048,实际取 SPRITE_SIZES 里的档位。 */ + spriteSize: { width: number; height: number } + /** 会进生成提示词。 */ + gameStyle: string | null + /** 由 POST /upload/image 上传后拿到。 */ + sampleImageUrl: string | null + createdAt: string + updatedAt: string +} + +/** 新建项目的入参。 */ +export interface CreateProjectInput { + name: string + perspective: number + directionalMovement: number + spriteSize: { width: number; height: number } + gameStyle?: string | null + sampleImageUrl?: string | null +} + +/** 游戏视角。取值与含义来自 windup_project 表注释(2026-07-27 后端接口文档)。 */ +export const CHARACTER_PERSPECTIVE: Record = { + 1: '横版视角', + 2: '俯视', + 3: '2.5D', +} + +/** 移动方向,决定一个动作要生成几套朝向的帧。 */ +export const DIRECTIONAL_MOVEMENT: Record = { + 1: '单向', + 2: '四向', + 3: '八向', +} + +/** + * 精灵图尺寸档位,来自 windup_project 表注释。 + * + * 注意:接口文档与 pydantic 校验写的是「32~2048 任意整数」,表注释列的却是这七个档。 + * 前端按档位做选择器,避免用户填出后端不打算支持的尺寸。TODO(对后端):以哪个为准。 + */ +export const SPRITE_SIZES = [32, 64, 128, 256, 512, 1024, 2048] as const diff --git a/frontend/src/entities/wearable/index.ts b/frontend/src/entities/wearable/index.ts new file mode 100644 index 0000000..ca6e5a7 --- /dev/null +++ b/frontend/src/entities/wearable/index.ts @@ -0,0 +1,20 @@ +import { useAsync } from '@/shared/lib' +import type { AsyncState } from '@/shared/lib' + +/** 可复用的穿戴资产,与 action-template 同属项目资产库,先占好入口。 */ + +export interface Wearable { + id: number + projectId: number + name: string + previewImageUrl: string | null +} + +export async function fetchWearables(_projectId: number): Promise { + throw new Error('not implemented:等待后端 GET /projects/{id}/wearables') +} + +/** 订阅穿戴资产列表。 */ +export function useWearables(projectId: number): AsyncState { + return useAsync(() => fetchWearables(projectId), [projectId]) +} diff --git a/frontend/src/entities/workflow-run/api/create-workflow-run.ts b/frontend/src/entities/workflow-run/api/create-workflow-run.ts new file mode 100644 index 0000000..9f61a72 --- /dev/null +++ b/frontend/src/entities/workflow-run/api/create-workflow-run.ts @@ -0,0 +1,21 @@ +import type { CreateWorkflowRunInput, WorkflowRun } from '../model/types' +import { initialSteps, newRunId, saveRun } from './local-store' + +/** + * 新建一次运行,两个入口都调它。 + * + * 后端暂不存 workflow,先落本地。未来改成 POST /workflows 时以保持调用方稳定为目标, + * 最终签名仍需前后端共同 Review。 + */ +export async function createWorkflowRun(input: CreateWorkflowRunInput): Promise { + const steps = initialSteps() + return saveRun({ + id: newRunId(), + projectId: input.projectId, + characterId: null, + driver: input.driver, + status: 'running', + currentStepId: steps[0].id, + steps, + }) +} diff --git a/frontend/src/entities/workflow-run/api/get-workflow-run.ts b/frontend/src/entities/workflow-run/api/get-workflow-run.ts new file mode 100644 index 0000000..cdc16a1 --- /dev/null +++ b/frontend/src/entities/workflow-run/api/get-workflow-run.ts @@ -0,0 +1,14 @@ +import type { WorkflowRun } from '../model/types' +import { loadRun } from './local-store' + +/** + * 按 id 取回一次运行,刷新页面与从审核台跳回时靠它恢复现场。 + * + * 后端暂不存 workflow。未来改成 GET /workflows/{id} 时以保持调用方稳定为目标, + * 最终签名仍需前后端共同 Review。 + */ +export async function fetchWorkflowRun(runId: string): Promise { + const run = loadRun(runId) + if (!run) throw new Error(`工作流 ${runId} 不存在`) + return run +} diff --git a/frontend/src/entities/workflow-run/api/local-store.ts b/frontend/src/entities/workflow-run/api/local-store.ts new file mode 100644 index 0000000..b9c3c19 --- /dev/null +++ b/frontend/src/entities/workflow-run/api/local-store.ts @@ -0,0 +1,69 @@ +import type { WorkflowRun, WorkflowStep } from '../model/types' + +/** + * 工作流的本地存储。 + * + * 后端当前不存 workflow(2026-07-27 确认),只提供图片与资源接口,所以这一层 + * 暂时由浏览器自己存。未来后端接管时,目标是优先替换 api/ 实现并删掉本文件; + * 但 entities 对外签名尚未经前后端 Review,不在此预先保证零改动。 + * + * 用 localStorage 而不是内存:刷新页面要能恢复现场,单帧退回也要找回当初那条工作流。 + * 拿不到 localStorage 时(隐私模式、测试环境)退回内存,此时数据只在本次运行内有效。 + */ +const STORAGE_KEY = 'windup.workflow-runs' + +type RunMap = Record + +let memory: RunMap = {} + +function storage(): Storage | null { + try { + return globalThis.localStorage ?? null + } catch { + return null + } +} + +function readAll(): RunMap { + const store = storage() + if (!store) return memory + try { + const raw = store.getItem(STORAGE_KEY) + return raw ? (JSON.parse(raw) as RunMap) : memory + } catch { + // 数据损坏时不让它把整个应用带崩 + return memory + } +} + +function writeAll(runs: RunMap): void { + memory = runs + try { + storage()?.setItem(STORAGE_KEY, JSON.stringify(runs)) + } catch { + // 配额满或隐私模式:已经写进 memory,本次运行内仍可用 + } +} + +export function saveRun(run: WorkflowRun): WorkflowRun { + const runs = readAll() + runs[run.id] = run + writeAll(runs) + return run +} + +export function loadRun(runId: string): WorkflowRun | null { + return readAll()[runId] ?? null +} + +export function newRunId(): string { + return `run-${globalThis.crypto.randomUUID().slice(0, 8)}` +} + +/** 一条新工作流的起始步骤:先生成母版,再确认。后续动作由用户/AI 逐个追加。 */ +export function initialSteps(): WorkflowStep[] { + return [ + { id: 'step-1', type: 'generate-template', status: 'pending', actionId: null, taskId: null }, + { id: 'step-2', type: 'confirm-template', status: 'pending', actionId: null, taskId: null }, + ] +} diff --git a/frontend/src/entities/workflow-run/api/submit-workflow-step.ts b/frontend/src/entities/workflow-run/api/submit-workflow-step.ts new file mode 100644 index 0000000..982df80 --- /dev/null +++ b/frontend/src/entities/workflow-run/api/submit-workflow-step.ts @@ -0,0 +1,31 @@ +import type { WorkflowCommand, WorkflowRun } from '../model/types' +import { loadRun, saveRun } from './local-store' + +/** + * 提交一步的结果并推进状态。 + * + * 后端暂时不存 workflow,所以推进由前端完成:把这一步标成 done,把下一个待办步骤 + * 设为当前步;没有待办了就整条完成。以后后端接管时改成 + * POST /workflows/{id}/steps/{stepId},由服务端返回推进后的数据。保持调用方稳定是目标, + * 最终签名仍需前后端共同 Review。 + */ +export async function submitWorkflowStep( + runId: string, + stepId: string, + _command: WorkflowCommand, +): Promise { + const run = loadRun(runId) + if (!run) throw new Error(`工作流 ${runId} 不存在`) + + const steps = run.steps.map((step) => + step.id === stepId ? { ...step, status: 'done' as const } : step, + ) + const next = steps.find((step) => step.status === 'pending') + + return saveRun({ + ...run, + steps, + currentStepId: next?.id ?? null, + status: next ? run.status : 'completed', + }) +} diff --git a/frontend/src/entities/workflow-run/index.ts b/frontend/src/entities/workflow-run/index.ts new file mode 100644 index 0000000..9bafa5d --- /dev/null +++ b/frontend/src/entities/workflow-run/index.ts @@ -0,0 +1,57 @@ +import { useAsync } from '@/shared/lib' +import type { AsyncState } from '@/shared/lib' +import { fetchWorkflowRun } from './api/get-workflow-run' +import type { TaskEvent } from './model/task' +import type { WorkflowRun } from './model/types' + +/** + * 两个入口共用的运行数据与命令。 + * 后端接口尚未提供,下面的签名即我们提给后端的需求。 + */ + +export { createWorkflowRun } from './api/create-workflow-run' +export { fetchWorkflowRun } from './api/get-workflow-run' +export { submitWorkflowStep } from './api/submit-workflow-step' +export { workflowRunKeys } from './model/queries' +export { + availableCommands, + currentStep, + locateFrame, + suggestNextCommand, +} from './model/selectors' +export type { TaskEvent, TaskStatus } from './model/task' +export type { + CreateWorkflowRunInput, + WorkflowCommand, + WorkflowCommandKind, + WorkflowDriver, + WorkflowLocation, + WorkflowRun, + WorkflowRunStatus, + WorkflowStep, + WorkflowStepStatus, + WorkflowStepType, +} from './model/types' + +/** + * 订阅一个异步任务的进度,返回取消订阅的函数。 + * + * 给的是 TaskEvent 而不是 WorkflowStep:任务与工作流步骤不是一回事, + * 拿到终态后由调用方决定要不要重新拉 WorkflowRun。 + * + * 当前只是向后端提出的能力需求,传输协议未定(端点、事件名、result 结构、 + * 重连策略四项都缺,见 model/task.ts)。它是同步函数,一旦在 useEffect 里 + * 调用会直接抛到错误边界,因此协议确定前**不要接调用点**;届时补的是实现, + * 签名不变。 + */ +export function subscribeTask( + _taskId: string, + _onEvent: (event: TaskEvent) => void, +): () => void { + throw new Error('subscribeTask 的 SSE 协议尚未与后端确定,暂不可调用') +} + +/** 订阅一次运行。 */ +export function useWorkflowRun(runId: string): AsyncState { + return useAsync(() => fetchWorkflowRun(runId), [runId]) +} diff --git a/frontend/src/entities/workflow-run/model/queries.ts b/frontend/src/entities/workflow-run/model/queries.ts new file mode 100644 index 0000000..3288f4c --- /dev/null +++ b/frontend/src/entities/workflow-run/model/queries.ts @@ -0,0 +1,5 @@ +/** 查询缓存键。同一个 runId 只能对应同一份 WorkflowRun 查询键。 */ +export const workflowRunKeys = { + all: ['workflow-run'] as const, + detail: (runId: string) => ['workflow-run', runId] as const, +} diff --git a/frontend/src/entities/workflow-run/model/selectors.ts b/frontend/src/entities/workflow-run/model/selectors.ts new file mode 100644 index 0000000..4ef8f00 --- /dev/null +++ b/frontend/src/entities/workflow-run/model/selectors.ts @@ -0,0 +1,92 @@ +import type { + WorkflowCommand, + WorkflowCommandKind, + WorkflowLocation, + WorkflowRun, + WorkflowStep, + WorkflowStepType, +} from './types' + +/** + * 当前步骤等纯计算。AI 自动与用户手动两个入口读的是同一份规则。 + * 纯函数,不碰网络也不碰界面。 + */ + +function stepsOfType(run: WorkflowRun, type: WorkflowStepType): WorkflowStep[] { + return run.steps.filter((step) => step.type === type) +} + +function isDone(steps: WorkflowStep[]): boolean { + return steps.length > 0 && steps.every((step) => step.status === 'done') +} + +/** 当前停在哪一步。 */ +export function currentStep(run: WorkflowRun): WorkflowStep | null { + return run.steps.find((step) => step.id === run.currentStepId) ?? null +} + +/** 此刻允许执行哪些命令。界面用它决定按钮状态,AI 用它约束自己不越步。 */ +export function availableCommands(run: WorkflowRun): WorkflowCommandKind[] { + if (run.status === 'completed' || run.status === 'failed') return [] + // 有任务在跑时不接新命令,避免连点产生重复任务 + if (run.steps.some((step) => step.status === 'running')) return [] + + const templateDone = isDone(stepsOfType(run, 'generate-template')) + const templateConfirmed = isDone(stepsOfType(run, 'confirm-template')) + const added = stepsOfType(run, 'add-action') + const generated = stepsOfType(run, 'generate-action') + + if (!templateDone) return ['generate-template'] + if (!templateConfirmed) return ['confirm-template', 'generate-template'] + + const commands: WorkflowCommandKind[] = ['add-action'] + // 有已加未生成的动作才能发起生成 + if (added.length > generated.length) commands.push('generate-action') + // 生成过动作才谈得上退回与收尾 + if (generated.length > 0) commands.push('reject-frame', 'finish') + return commands +} + +/** + * AI 自动驱动时下一步做什么。手动驱动时由用户选。 + * @param plan AI 解析那句话得到的计划:母版描述 + 要哪些动作。 + */ +export function suggestNextCommand( + run: WorkflowRun, + plan: { description: string; actionNames: string[] }, +): WorkflowCommand | null { + const allowed = availableCommands(run) + if (allowed.length === 0) return null + + if (allowed.includes('generate-template') && !isDone(stepsOfType(run, 'generate-template'))) { + return { kind: 'generate-template', description: plan.description } + } + if (allowed.includes('confirm-template')) return { kind: 'confirm-template' } + + const added = stepsOfType(run, 'add-action') + if (added.length < plan.actionNames.length) { + return { kind: 'add-action', name: plan.actionNames[added.length], source: 'preset' } + } + + const pending = run.steps.find( + (step) => step.type === 'generate-action' && step.status === 'pending', + ) + if (pending && pending.actionId !== null) { + return { kind: 'generate-action', actionId: pending.actionId } + } + + return allowed.includes('finish') ? { kind: 'finish' } : null +} + +/** 审核台点「重新生成此帧」时用,拿返回值跳回编辑器定位。 */ +export function locateFrame( + run: WorkflowRun, + actionId: number, + frameIndex: number, +): WorkflowLocation | null { + const step = run.steps.find( + (item) => item.type === 'generate-action' && item.actionId === actionId, + ) + if (!step) return null + return { runId: run.id, stepId: step.id, actionId, frameIndex } +} diff --git a/frontend/src/entities/workflow-run/model/task.ts b/frontend/src/entities/workflow-run/model/task.ts new file mode 100644 index 0000000..6ff7d2f --- /dev/null +++ b/frontend/src/entities/workflow-run/model/task.ts @@ -0,0 +1,26 @@ +/** + * 异步任务契约,与 WorkflowStep 是两回事。 + * + * 任务粒度 = 一次耗时较久的大模型调用:生成母版、生成一个动作(含 8 帧,算 1 个 + * 任务而非 1+8)、重生成某一帧,各是一个任务。一个 workflow 关联多个任务, + * 任务进度不等于工作流步骤本身——所以进度回调给的是 TaskEvent,不是 WorkflowStep。 + * + * 下面只是事件形状的候选,**传输协议尚未与后端确定**,四项缺口: + * SSE 端点路径、事件名与分帧格式、`result` 的具体结构、断线重连与补发策略。 + * 协议定下来之前 subscribeTask 不可调用,见其函数注释。 + */ +export type TaskStatus = 'queued' | 'running' | 'succeeded' | 'failed' + +export interface TaskEvent { + taskId: string + status: TaskStatus + /** 0–100。后端给不出就是 null,界面显示不确定进度。 */ + progress: number | null + /** 失败原因,status 为 failed 时有值。 */ + error: string | null + /** + * 任务产出(如生成出的图 URL)。 + * 保持 unknown 而不是先猜一个形状:猜错会让调用方写出依赖假结构的代码。 + */ + result: unknown +} diff --git a/frontend/src/entities/workflow-run/model/types.ts b/frontend/src/entities/workflow-run/model/types.ts new file mode 100644 index 0000000..bf7f81e --- /dev/null +++ b/frontend/src/entities/workflow-run/model/types.ts @@ -0,0 +1,66 @@ +/** + * 一次工作流的运行数据,后端持久化,这份是投影。 + * 两个入口共用同一份数据,只差一个 driver 字段。 + */ + +/** UI 文案叫「AI 快捷创作」「节点工作流」,代码里用这两个词。 */ +export type WorkflowDriver = 'ai' | 'manual' + +/** 会调生成能力的步骤在后端对应一个 generation 任务。 */ +export type WorkflowStepType = + | 'generate-template' + | 'confirm-template' + | 'add-action' + | 'generate-action' + | 'review' + +export type WorkflowStepStatus = 'pending' | 'running' | 'done' | 'failed' + +export interface WorkflowStep { + id: string + type: WorkflowStepType + status: WorkflowStepStatus + /** 添加/生成动作时有值。 */ + actionId: number | null + /** 异步任务 id,前端用 SSE 取结果。 */ + taskId: string | null +} + +export type WorkflowRunStatus = 'draft' | 'running' | 'paused' | 'completed' | 'failed' + +export interface WorkflowRun { + id: string + projectId: number + /** 建出角色前为 null。 */ + characterId: number | null + driver: WorkflowDriver + status: WorkflowRunStatus + currentStepId: string | null + steps: WorkflowStep[] +} + +export interface CreateWorkflowRunInput { + projectId: number + driver: WorkflowDriver + /** AI 驱动时用户输入的那句话。 */ + prompt?: string +} + +/** 工作流上可以发生的事,不区分是用户点的还是 AI 推的。 */ +export type WorkflowCommand = + | { kind: 'generate-template'; description: string; referenceImageUrl?: string } + | { kind: 'confirm-template' } + | { kind: 'add-action'; name: string; source: 'preset' | 'custom' | 'template'; templateId?: number } + | { kind: 'generate-action'; actionId: number } + | { kind: 'reject-frame'; actionId: number; frameIndex: number } + | { kind: 'finish' } + +export type WorkflowCommandKind = WorkflowCommand['kind'] + +/** 退回一帧后要跳回编辑器的哪个位置。 */ +export interface WorkflowLocation { + runId: string + stepId: string + actionId: number + frameIndex: number +} From 14baa3acf8d76729b844da4ba8fd412503558ca2 Mon Sep 17 00:00:00 2001 From: huyan <2971427998@qq.com> Date: Mon, 27 Jul 2026 15:06:04 +0800 Subject: [PATCH 04/21] feat(pages): add page modules and routing shell Issue #58 requires the module boundaries named in MODULES.md to exist as real code with declared interfaces, not just as directories. Add the workflow editor and inspection preview page modules with explicit props and no Router dependency, four feature slices, the routing shell, a global error boundary and a not-found route. Quick start now creates a run and the editor loads it by the same runId, and each page module can be rendered and tested outside the router. --- frontend/src/app/error-boundary.tsx | 53 ++++++++ frontend/src/app/index.tsx | 32 +++++ frontend/src/app/layout/index.tsx | 27 ++++ .../src/features/character-setup/index.tsx | 22 ++++ frontend/src/features/export/index.tsx | 15 +++ frontend/src/features/generation/index.tsx | 19 +++ frontend/src/features/review/index.tsx | 25 ++++ frontend/src/main.tsx | 11 ++ frontend/src/pages/asset-library/index.tsx | 34 +++++ frontend/src/pages/not-found/index.tsx | 21 +++ frontend/src/pages/playtest/index.tsx | 35 +++++ .../playtest/inspection-preview/index.tsx | 28 ++++ frontend/src/pages/project-detail/index.tsx | 68 ++++++++++ frontend/src/pages/projects/index.tsx | 36 ++++++ frontend/src/pages/quick-start/index.tsx | 63 +++++++++ .../pages/workflow-editor/editor/index.tsx | 122 ++++++++++++++++++ frontend/src/pages/workflow-editor/index.tsx | 46 +++++++ 17 files changed, 657 insertions(+) create mode 100644 frontend/src/app/error-boundary.tsx create mode 100644 frontend/src/app/index.tsx create mode 100644 frontend/src/app/layout/index.tsx create mode 100644 frontend/src/features/character-setup/index.tsx create mode 100644 frontend/src/features/export/index.tsx create mode 100644 frontend/src/features/generation/index.tsx create mode 100644 frontend/src/features/review/index.tsx create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/asset-library/index.tsx create mode 100644 frontend/src/pages/not-found/index.tsx create mode 100644 frontend/src/pages/playtest/index.tsx create mode 100644 frontend/src/pages/playtest/inspection-preview/index.tsx create mode 100644 frontend/src/pages/project-detail/index.tsx create mode 100644 frontend/src/pages/projects/index.tsx create mode 100644 frontend/src/pages/quick-start/index.tsx create mode 100644 frontend/src/pages/workflow-editor/editor/index.tsx create mode 100644 frontend/src/pages/workflow-editor/index.tsx diff --git a/frontend/src/app/error-boundary.tsx b/frontend/src/app/error-boundary.tsx new file mode 100644 index 0000000..9499ab5 --- /dev/null +++ b/frontend/src/app/error-boundary.tsx @@ -0,0 +1,53 @@ +import { Component } from 'react' +import type { ErrorInfo, ReactNode } from 'react' + +/** + * 全局错误边界。任何一页渲染时抛出的异常都在这里兜住, + * 不让整个应用白屏。放在 AppShell 内部包住路由,出错时顶部导航仍可用, + * 用户能自己走开,不必刷新。 + * + * 只能用 class 写:React 至今没有等价的函数组件写法。 + */ +interface ErrorBoundaryProps { + children: ReactNode +} + +interface ErrorBoundaryState { + error: Error | null +} + +export class ErrorBoundary extends Component { + state: ErrorBoundaryState = { error: null } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { error } + } + + componentDidCatch(error: Error, info: ErrorInfo): void { + // 骨架阶段只打控制台;接监控是生产阶段的事,不在本次范围 + console.error('[Windup] 未捕获的渲染错误', error, info.componentStack) + } + + reset = (): void => { + this.setState({ error: null }) + } + + render(): ReactNode { + const { error } = this.state + if (!error) return this.props.children + + return ( +
+

这个页面出错了

+

{error.message}

+ +
+ ) + } +} diff --git a/frontend/src/app/index.tsx b/frontend/src/app/index.tsx new file mode 100644 index 0000000..9846988 --- /dev/null +++ b/frontend/src/app/index.tsx @@ -0,0 +1,32 @@ +import { BrowserRouter, Route, Routes } from 'react-router' + +import { AssetLibraryPage } from '@/pages/asset-library' +import { NotFoundPage } from '@/pages/not-found' +import { PlaytestPage } from '@/pages/playtest' +import { ProjectDetailPage } from '@/pages/project-detail' +import { ProjectsPage } from '@/pages/projects' +import { QuickStartPage } from '@/pages/quick-start' +import { WorkflowEditorPage } from '@/pages/workflow-editor' +import { ErrorBoundary } from './error-boundary' +import { AppShell } from './layout' + +/** 路由表与全局外壳。app 层只做启动与装配。 */ +export function App() { + return ( + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ) +} diff --git a/frontend/src/app/layout/index.tsx b/frontend/src/app/layout/index.tsx new file mode 100644 index 0000000..6fa2f41 --- /dev/null +++ b/frontend/src/app/layout/index.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from 'react' +import { Link } from 'react-router' + +/** + * 模块五:跨页面常驻导航。不属于任何单个页面,故上提到 app 层。 + * 页内头部 PageHeader 在 shared/ui —— pages 不能 import app 层。 + */ + +export interface AppShellProps { + children: ReactNode +} + +/** 全站外壳,全局导航常驻。 */ +export function AppShell({ children }: AppShellProps) { + return ( +
+ +
{children}
+
+ ) +} diff --git a/frontend/src/features/character-setup/index.tsx b/frontend/src/features/character-setup/index.tsx new file mode 100644 index 0000000..f875b58 --- /dev/null +++ b/frontend/src/features/character-setup/index.tsx @@ -0,0 +1,22 @@ +import type { Character } from '@/entities' + +/** + * 创建/确认角色与母版,选择动作模板应用到角色。 + * 宿主:quick-start、workflow-editor、projects、asset-library 的「继续补充动作」。 + */ +export interface CharacterSetupProps { + projectId: number + /** 已有角色时传入,用于补充动作;不传表示新建。 */ + characterId?: number + /** 角色建好或母版确认后通知宿主。 */ + onCharacterReady?: (character: Character) => void +} + +export function CharacterSetup({ projectId, characterId }: CharacterSetupProps) { + return ( +
+ 角色与母版设置待实现(项目 {projectId} + {characterId ? ` · 角色 ${characterId}` : ' · 新建'})。 +
+ ) +} diff --git a/frontend/src/features/export/index.tsx b/frontend/src/features/export/index.tsx new file mode 100644 index 0000000..3da4b92 --- /dev/null +++ b/frontend/src/features/export/index.tsx @@ -0,0 +1,15 @@ +/** + * 选择内容、发起导出和下载。下载属于本 feature,不放 shared/lib。 + */ +export interface ExportProps { + characterId: number + onExported?: (downloadUrl: string) => void +} + +export function Export({ characterId }: ExportProps) { + return ( +
+ 导出待实现(角色 {characterId})。 +
+ ) +} diff --git a/frontend/src/features/generation/index.tsx b/frontend/src/features/generation/index.tsx new file mode 100644 index 0000000..b8cb579 --- /dev/null +++ b/frontend/src/features/generation/index.tsx @@ -0,0 +1,19 @@ +/** + * 发起生成、重试、确认候选。 + * 宿主:quick-start、workflow-editor。生成是异步任务,进度由 entities 的 SSE 给。 + */ +export interface GenerationProps { + runId: string + /** 生成哪个动作;不传表示生成母版。 */ + actionId?: number + onGenerated?: (actionId: number) => void +} + +export function Generation({ runId, actionId }: GenerationProps) { + return ( +
+ 生成待实现(工作流 {runId} + {actionId ? ` · 动作 ${actionId}` : ' · 母版'})。 +
+ ) +} diff --git a/frontend/src/features/review/index.tsx b/frontend/src/features/review/index.tsx new file mode 100644 index 0000000..89f46c6 --- /dev/null +++ b/frontend/src/features/review/index.tsx @@ -0,0 +1,25 @@ +/** + * 人工审核、查看自动质检结果与退回修复。 + * 逐帧审核的 Canvas 与 Worker 归本 feature。 + * 不提供「通过此帧」:用户只在有问题时点退回。 + */ +export interface ReviewProps { + /** 进来只需要一个角色。 */ + characterId: number + /** 受控选择:当前动作与帧由宿主持有,Review 不再保存第二份状态。 */ + actionId?: number + frameIndex?: number + onSelectFrame?: (actionId: number, frameIndex: number) => void + /** 退回某帧。只报告是哪一帧,跳去哪由宿主决定。 */ + onRejectFrame?: (actionId: number, frameIndex: number) => void +} + +export function Review({ characterId, actionId, frameIndex }: ReviewProps) { + return ( +
+ 逐帧审核待实现(角色 {characterId} + {actionId ? ` · 动作 ${actionId}` : ''} + {frameIndex !== undefined ? ` · 第 ${frameIndex + 1} 帧` : ''})。 +
+ ) +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..ff3b910 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' + +import { App } from '@/app' +import './index.css' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/pages/asset-library/index.tsx b/frontend/src/pages/asset-library/index.tsx new file mode 100644 index 0000000..d9eef87 --- /dev/null +++ b/frontend/src/pages/asset-library/index.tsx @@ -0,0 +1,34 @@ +import { useSearchParams } from 'react-router' + +import { CharacterSetup } from '@/features/character-setup' +import { PageHeader } from '@/shared/ui' + +/** + * 资产库是项目下属的,不是全站总库,所以必须带 projectId 进来。 + * 按角色/视角/动作浏览,「继续补充动作」复用 CharacterSetup。 + */ +export function AssetLibraryPage() { + const [search] = useSearchParams() + const projectId = Number(search.get('projectId') ?? 0) + + if (!projectId) { + return ( + <> + +

+ 资产库属于某个项目,请先从项目页进入(/asset-library?projectId=…)。 +

+ + ) + } + + return ( + <> + +

+ 待实现:读 Character / ActionTemplate / Wearable 三个列表并筛选。 +

+ + + ) +} diff --git a/frontend/src/pages/not-found/index.tsx b/frontend/src/pages/not-found/index.tsx new file mode 100644 index 0000000..0ea96a2 --- /dev/null +++ b/frontend/src/pages/not-found/index.tsx @@ -0,0 +1,21 @@ +import { useNavigate } from 'react-router' + +import { PageHeader } from '@/shared/ui' + +/** 路由兜底:地址不存在时不留白屏,给一条回去的路。 */ +export function NotFoundPage() { + const navigate = useNavigate() + + return ( + <> + + + + ) +} diff --git a/frontend/src/pages/playtest/index.tsx b/frontend/src/pages/playtest/index.tsx new file mode 100644 index 0000000..2ba11d1 --- /dev/null +++ b/frontend/src/pages/playtest/index.tsx @@ -0,0 +1,35 @@ +import { useNavigate, useParams } from 'react-router' + +import { PageHeader } from '@/shared/ui' +import { InspectionPreview } from './inspection-preview' + +/** + * 检查/预览台的路由适配器。 + * + * 只读 characterId、渲染页头和执行跨页跳转;播放与检查交互属于 + * ./inspection-preview 模块。 + */ +export function PlaytestPage() { + const navigate = useNavigate() + const { characterId: rawCharacterId } = useParams() + const characterId = Number(rawCharacterId) + + return ( + <> + navigate(-1)} /> + {Number.isInteger(characterId) && characterId > 0 ? ( + { + navigate( + `/workflow-editor/${location.runId}?stepId=${location.stepId}` + + `&actionId=${location.actionId}&frameIndex=${location.frameIndex}`, + ) + }} + /> + ) : ( +

无效的角色 ID。

+ )} + + ) +} diff --git a/frontend/src/pages/playtest/inspection-preview/index.tsx b/frontend/src/pages/playtest/inspection-preview/index.tsx new file mode 100644 index 0000000..3cfa19d --- /dev/null +++ b/frontend/src/pages/playtest/inspection-preview/index.tsx @@ -0,0 +1,28 @@ +import type { Character, WorkflowLocation } from '@/entities' + +/** + * 检查/预览台模块的全部公开接口。 + * + * 模块内部负责动作切换、播放、按键绑定和当前帧;它不操作 Router, + * 只把已解析的工作流位置交给页面跳转。 + */ +export interface InspectionPreviewProps { + characterId: Character['id'] + onOpenWorkflowAtFrame: (location: WorkflowLocation) => void +} + +/** + * 以一个角色为输入的独立检查/预览台。 + * + * 本 Issue 只落模块边界;PixiJS、按键绑定与单帧退回链路属后续实现。 + * 逐帧审核的写入能力仍由 Review Feature / Entities 提供,本模块不复制一套。 + */ +// onOpenWorkflowAtFrame 已在契约中定义,调用点随真实播放与逐帧退回一起补, +// 当前刻意不使用,避免造出一个没有触发路径的假实现。 +export function InspectionPreview({ characterId }: InspectionPreviewProps) { + return ( +
+ 检查/预览台待实现(角色 {characterId}):动作切换、按键绑定、播放与逐帧定格。 +
+ ) +} diff --git a/frontend/src/pages/project-detail/index.tsx b/frontend/src/pages/project-detail/index.tsx new file mode 100644 index 0000000..c889ffa --- /dev/null +++ b/frontend/src/pages/project-detail/index.tsx @@ -0,0 +1,68 @@ +import { useNavigate, useParams } from 'react-router' + +import { CHARACTER_PERSPECTIVE, DIRECTIONAL_MOVEMENT, createWorkflowRun, useProject } from '@/entities' +import { PageHeader } from '@/shared/ui' + +/** + * 单个项目的内容浏览:项目约束 + 项目下的全部内容(一期只角色,后续加动作模板、穿戴)。 + * 与项目列表是两页,07-22 会议要求两页都要有。 + */ +export function ProjectDetailPage() { + const navigate = useNavigate() + const { projectId = '' } = useParams() + const { data: project, loading, error } = useProject(Number(projectId)) + + async function start() { + const run = await createWorkflowRun({ projectId: Number(projectId), driver: 'manual' }) + navigate(`/workflow-editor/${run.id}`) + } + + return ( + <> + navigate('/projects')} + actions={ + project ? ( + + ) : null + } + /> + {loading ?

加载中…

: null} + {error ?

加载失败:{error.message}

: null} + {project ? ( + <> +
+
精灵尺寸
+
+ {project.spriteSize.width}×{project.spriteSize.height} +
+
视角
+
{CHARACTER_PERSPECTIVE[project.perspective]}
+
移动方向
+
{DIRECTIONAL_MOVEMENT[project.directionalMovement]}
+
画风
+
{project.gameStyle ?? '未设置'}
+
+

+ 待实现:本项目下的角色列表,以及跳转到项目资产库。 +

+ + + ) : null} + + ) +} diff --git a/frontend/src/pages/projects/index.tsx b/frontend/src/pages/projects/index.tsx new file mode 100644 index 0000000..def9504 --- /dev/null +++ b/frontend/src/pages/projects/index.tsx @@ -0,0 +1,36 @@ +import { useNavigate } from 'react-router' + +import { CHARACTER_PERSPECTIVE, useProjects } from '@/entities' +import { PageHeader } from '@/shared/ui' + +/** 项目列表。点进去是项目内容页,「开始工作流」在那一页上。 */ +export function ProjectsPage() { + const navigate = useNavigate() + const { data, loading, error } = useProjects({ page: 1, pageSize: 20 }) + + return ( + <> + + {loading ?

加载中…

: null} + {error ?

加载失败:{error.message}

: null} +
    + {data?.items.map((project) => ( +
  • + +
  • + ))} +
+ {data ?

共 {data.total} 个项目

: null} + + ) +} diff --git a/frontend/src/pages/quick-start/index.tsx b/frontend/src/pages/quick-start/index.tsx new file mode 100644 index 0000000..82c09ae --- /dev/null +++ b/frontend/src/pages/quick-start/index.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router' + +import { createWorkflowRun } from '@/entities' +import { CharacterSetup } from '@/features/character-setup' +import { PageHeader } from '@/shared/ui' + +/** + * 首页。解析一句话描述后,调 CharacterSetup、Generation,创建/恢复 WorkflowRun, + * 跳转 workflow-editor/${runId}。后台自动建项目,跳过创建步骤。 + */ +export function QuickStartPage() { + const navigate = useNavigate() + const [description, setDescription] = useState('') + const [submitting, setSubmitting] = useState(false) + const [error, setError] = useState(null) + + async function start() { + setSubmitting(true) + setError(null) + try { + // 待实现:先解析描述得到计划;项目 id 暂用快速开始的默认项目 + const run = await createWorkflowRun({ projectId: 1, driver: 'ai', prompt: description }) + navigate(`/workflow-editor/${run.id}`) + } catch (cause) { + setError(cause instanceof Error ? cause.message : String(cause)) + } finally { + setSubmitting(false) + } + } + + return ( + <> + +
{ + event.preventDefault() + void start() + }} + > +