diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml
new file mode 100644
index 0000000..fa48364
--- /dev/null
+++ b/.github/workflows/frontend-ci.yml
@@ -0,0 +1,56 @@
+name: Frontend CI
+
+on:
+ pull_request:
+ branches: [main]
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: frontend-ci-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ frontend-checks:
+ name: Frontend checks
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ env:
+ CI: 'true'
+
+ defaults:
+ run:
+ working-directory: frontend
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v7
+ with:
+ node-version: 24
+ cache: npm
+ cache-dependency-path: frontend/package-lock.json
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Check formatting
+ run: npm run format:check
+
+ - name: Lint
+ run: npm run lint
+
+ - name: Typecheck
+ run: npm run typecheck
+
+ - name: Test
+ run: npm run test
+
+ - name: Build
+ run: npm run build
diff --git a/frontend-architecture-v3.md b/frontend-architecture-v3.md
new file mode 100644
index 0000000..0c326e6
--- /dev/null
+++ b/frontend-architecture-v3.md
@@ -0,0 +1,115 @@
+# Windup 前端架构
+
+本文记录当前前端的模块划分、依赖规则和已经落地的首个工作流纵切。
+
+---
+
+## 1. 模块划分
+
+业务模块都在 `src/entities/` 下:
+
+| 模块 | 职责 |
+|---|---|
+| `project` | 项目级全局约束:视角、朝向数、精灵尺寸、画风 |
+| `character` | 角色资产。造型、动作、帧是它内部的一棵树 |
+| `action-template` | 能跨角色复用的动作配方 |
+| `generation` | 一次生成任务这份业务数据 |
+| `media` | 已上传媒体的不透明引用 |
+| `task` | 后端异步步骤的状态 |
+| `workflow-run` | 制作流程的运行记录 |
+
+**模块判据:这个东西能不能被单独取到。**
+
+能单独取,说明它需要自己的一套取数逻辑,才值得一个模块;取不到的,它只是别人身上的一个字段。
+
+按这条判据,`Outfit`、`Action`、`Frame` 没有独立模块——它们不能脱离 `Character` 被取到,所以是 `character` 内部的类型。`ActionTemplate` 有独立模块,因为它能被不同角色复用。
+
+---
+
+## 2. 层次
+
+```text
+pages -> features -> entities -> shared
+```
+
+| 层 | 内容 |
+|---|---|
+| `pages` | 八个路由页面 |
+| `features` | 用户操作:角色设置、生成、审核、导出;以及流程推进 `workflow-controller` |
+| `entities` | 上表业务模块 |
+| `shared` | 无业务语义的形状,目前只有分页 |
+
+`app` 只做启动和路由,不构造服务、不向下注入。
+
+### 依赖规则
+
+1. 只能向下依赖,不允许反向。
+2. 同层模块之间不互相导入。要共用就往下沉。
+3. 跨模块只从模块目录的 `index.ts` 进入;`entities` 统一从 `@/entities` 使用。
+4. `entities` 内部模块之间可以互相导入,对外仍是一个门。
+
+---
+
+## 3. 接口命名
+
+需要访问后端资源的模块暴露一组接口,统一叫 `XxxApis`:
+
+```text
+ProjectApis CharacterApis ActionTemplateApis GenerationApis
+TaskApis
+```
+
+**不使用 `Repository` / `Port` / `Adapter` 这些叫法**,也不做接口与实现的分离——实现跟着接口放在同一个模块里。
+
+`WorkflowRun` 是前端运行态,不声明后端接口。后端不读取、不推进、也不持久化它。
+
+---
+
+## 4. 流程推进
+
+`features/workflow-controller` 是快速开始与手动工作流共用的推进边界,不含界面。
+
+Controller 围绕同一份 WorkflowRun 提供创建、读取、订阅、当前步骤更新、推进、
+任务恢复、结果写回和中断。这些操作依赖同一份步骤数据,不拆成互不共享状态的独立模块。
+
+步骤顺序固定八步:
+
+```text
+角色资料 → 角色图 → 候选选择 → 动作资料 → 首帧 → 完整动画 → 审核 → 导出
+```
+
+**步骤怎么走、运行状态如何保存都由前端决定。** 后端不参与 WorkflowRun,
+只接收各节点发起的生成请求,并在最终确认时持久化角色与动作资产。固定八步是当前
+产品流程,不是为了通用编排而写的可配置工作流。
+
+当前存储版本只支持一个 Revision。从历史步骤重开尚未进入产品定义,Controller
+不提前暴露该操作;实现时必须同步升级本地存储版本和迁移规则。
+
+快速开始与手动模式将共用同一份推进逻辑,但连续自动推进属于 Quick Start 页面接入范围,
+当前 Controller 只实现一次推进一个步骤。
+
+Controller 的提交锁和任务订阅属于实例状态。页面接入时必须复用同一个 Feature 实例,
+不能在组件渲染或路由切换时重复创建。
+
+---
+
+## 5. 当前实现范围
+
+- `WorkflowRun` 的内存状态、版本化 localStorage 镜像和刷新校验
+- `角色资料 → 角色图生成 → 候选选择` 的 Controller 纵切
+- Store、Controller 和纵向流程测试
+
+页面、Workflow Editor、Quick Start 自动推进、后五步和真实后端适配器仍未实现。
+
+### 恢复边界
+
+- 已取得 `taskId`:刷新后先查询任务当前状态,未结束才重新订阅。
+- 请求已经发出但尚未取得 `taskId`:后端没有幂等键或按请求标识查询的能力,
+ 前端将本地 Run 标为失败,不自动重提,避免静默创建重复任务。
+- localStorage 写入失败时当前会话继续使用内存快照;页面提示与重新持久化策略在 UI 接入时补充。
+
+---
+
+## 6. 未与后端对齐的部分
+
+明细见 `frontend/API_CONTRACT.md`。
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..00baad0
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Vercel 本地项目链接元数据,不提交账号和项目 ID
+.vercel
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+.env*
diff --git a/frontend/.oxfmtrc.json b/frontend/.oxfmtrc.json
new file mode 100644
index 0000000..293ef6a
--- /dev/null
+++ b/frontend/.oxfmtrc.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
+ "endOfLine": "lf",
+ "printWidth": 100,
+ "semi": false,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "ignorePatterns": ["**/*.md"]
+}
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/API_CONTRACT.md b/frontend/API_CONTRACT.md
new file mode 100644
index 0000000..427b6eb
--- /dev/null
+++ b/frontend/API_CONTRACT.md
@@ -0,0 +1,119 @@
+# 前后端接口对齐清单
+
+前端各模块的 `XxxApis` 与后端 2026-07-30 接口文档逐条比对结果。
+
+后端现有四个相关模块:`project`、`character`、`generation`、`media`。`asset` 与 `wearable` 已按 07-30 评审要求删除。
+
+---
+
+## 一、已经确认的边界
+
+- `WorkflowRun` 是前端固定工作流的运行态。后端不读取、不推进、也不持久化,前端不声明 `WorkflowRunApis`。
+- `Character` 不使用独立 `name` 字段;前端已删除。
+- 前端保留 `jump` 动作类型,由后端补充对应枚举。
+- 查询生成任务统一携带 `projectId + taskId`。
+- 前端工作流节点不与后端 `GenerationType` 一一对应,按下表调用:
+
+| 前端工作流节点 | 后端接口 | 后端任务类型 |
+|---|---|---|
+| `character_template` | `POST /generation/image` | `character_image` |
+| `first_frame` | `POST /generation/image`,以上一步角色图作为参考图 | `character_image` |
+| `complete_animation` | `POST /generation/action`,以已确认动作首帧作为参考图 | `character_action` |
+
+图片生成和动作生成只返回任务及结果,不自动修改 WorkflowRun 或角色资产。用户最终确认后,前端再通过角色更新接口保存角色图和完整动作数据。
+
+---
+
+## 二、前端预期有、后端目前没有
+
+**这些接口仍需要确定由后端提供,还是改为前端本地能力。**
+
+| 前端接口 | 后端情况 |
+|---|---|
+| `ActionTemplateApis.listAvailable` | 没有 action template 模块 |
+| `ProjectApis.update` | 没有 `PATCH /projects/{project_id}` |
+
+前端已按服务端现状去掉 `TaskApis.cancel`——后端没有取消能力,不声明前端用不到的接口。
+
+---
+
+## 三、形状不一致
+
+这些差异可以在前端接口层转换,不要求领域类型与后端 DTO 使用相同命名。
+
+| 项 | 后端 | 前端 |
+|---|---|---|
+| 角色列表 | `list_characters` 分页,返回 `(list, total)` | `listByProject` 无分页 |
+| 更新角色 | `update_character(character_id, **fields)` 部分更新 | `update(character)` 整棵树替换 |
+| 等待任务完成 | 提供 `GET /generation/tasks/{task_id}` 轮询 | `TaskApis.subscribe`;适配器先立即回放当前快照,再继续轮询 |
+| 图片生成数量 | 入参有 `num_images`,结果只有一个 `image_url` | 角色图候选结果是 `images[]` |
+| 动作类型 | `walk` `idle` `attack` `custom`;待增加 `jump` | `walk` `idle` `attack` `jump` `custom` |
+| 角色视角 | `character_perspective` 为 `1~3`,文档中 2、3 都写成“正面” | `side` `top-down` `isometric` |
+
+ID 类型后端为 `int`、前端为 `string`,由前端转换层处理,不需要后端改动。
+
+---
+
+## 四、后端有、前端没接
+
+| 后端 | 说明 |
+|---|---|
+| `delete_character` | 前端 `CharacterApis` 没有删除 |
+| `Character.description` | 后端存在实体上;前端只在创建入参里,创建完查不到 |
+| `Character.reference_image_url` | 后端存在实体上;前端 `Character` 类型没有这个字段 |
+| `MediaService.upload` | 前端本次未提交上传模块 |
+
+---
+
+## 五、前端资产字段在后端没有落点
+
+后端 `character_data` 的嵌套结构(见 `character/model.py`):
+
+```text
+outfits[] → id / name / preview_url / actions[]
+actions[] → id / type / name / loop / fps / frame_count / frames[]
+frames[] → index / image_url / duration_ms
+```
+
+前端以下字段在后端结构里没有落点:
+
+- `Action.kind`(preset / custom 来源)
+- `Action.keyFrameIndex`
+- `Frame.rootMotion`
+- `Outfit.candidateCharacterTemplates`(母版候选列表)
+- `Outfit.characterTemplateUrl`(每套造型的已确认角色图)
+- `Outfit.baseFrames`
+
+`candidateCharacterTemplates` 属于生成过程数据;若只在当前 WorkflowRun 中使用,可以留在前端。其余字段若要随最终资产恢复,需要后端增加字段,或者前端在 MVP 中删除。
+
+---
+
+## 六、概念不一致
+
+后端 `character/model.py` 字段说明:
+
+> `reference_image_url`: 角色参考图,即旧概念中的 Character Template
+
+前端把这两者当成不同的东西:
+
+- 用户上传的参考图 —— 创建角色时的输入
+- AI 生成后用户选定的角色图(母版)—— `Outfit.characterTemplateUrl`
+
+**后端合成了一个字段。** 07-30 评审也提到「模板」这个叫法容易与 action template 混淆,暂改称「角色图」。三方对这里是几个概念的理解需要统一。
+
+---
+
+## 待确认
+
+- [ ] `ActionTemplateApis` 由后端提供还是前端内置
+- [ ] 母版候选几张
+- [ ] 参考图与角色图是一个字段还是两个
+- [ ] `Character.description` 前端要不要跟着存
+- [ ] `Action.kind` / `Action.keyFrameIndex` / `Frame.rootMotion` 是否进入最终资产
+- [ ] 上传模块何时提交
+
+## 已分工
+
+- [x] 前端删除 `WorkflowRunApis`,WorkflowRun 全程由前端管理
+- [x] 前端删除 `Character.name`
+- [ ] 后端增加 `jump` 动作类型
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..a7878ed
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,32 @@
+# Windup 前端
+
+React + Vite + TypeScript。
+
+## 开发
+
+```bash
+npm ci
+npm run dev
+```
+
+## 检查
+
+```bash
+npm run format:check # 格式
+npm run lint # 静态检查
+npm run typecheck # 类型
+npm run test # 单元与纵向集成测试
+npm run build # 构建
+```
+
+CI 按上面顺序全跑一遍。
+
+## 结构
+
+模块划分、依赖规则与命名约定见仓库根目录 `frontend-architecture-v3.md`。
+
+当前已实现纯前端 `WorkflowRun` 存储,以及
+`角色资料 → 角色图生成 → 候选选择` 的首个 Controller 纵切。页面仍是占位外壳,
+后五步和真实 `XxxApis` 实现按模块拆成后续 PR。
+
+与后端尚未对齐的接口见 `API_CONTRACT.md`。
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..cd1df0c
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Windup · 2D 角色资产生成
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..d513d30
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,3630 @@
+{
+ "name": "windup-frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "windup-frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7",
+ "react-router": "^8.3.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.3.3",
+ "@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",
+ "oxfmt": "^0.61.0",
+ "oxlint": "^1.71.0",
+ "tailwindcss": "^4.3.3",
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@oxfmt/binding-android-arm-eabi": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.61.0.tgz",
+ "integrity": "sha512-BaS+1OVvg9sr+Xav0+KdWedQRcAzrdoEcwMZeqoc2F6ieC1s/t5eM35YQoRPQ7vAqkZ+p3tbQb1r9I9mrV5oGA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-android-arm64": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.61.0.tgz",
+ "integrity": "sha512-of8atAV0M1egGcVOMbgZCvc10sFOP3ayQBNQV5h5G3fNq8gACdEswfFk9bzGrdbM23rtg0Coxi7np7oPLcueNw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-darwin-arm64": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.61.0.tgz",
+ "integrity": "sha512-7l8+5ov4BGwtAcmpzvEik/TG3bciwyw/S3e6j5GKH7pcQqcgCVxD3AuJeP6upto+SOTBKQ4wrrdbMt0gq8fHSQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-darwin-x64": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.61.0.tgz",
+ "integrity": "sha512-Fnz4dDDXBb7udk+DmwelNjxbD6yptyxwCqwCH2ebo4RVLxVsRfFsn/AHJC49KIltPrVokamGv4SSOsiV50DTxQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-freebsd-x64": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.61.0.tgz",
+ "integrity": "sha512-mddOebKNCP+AucmzfNsk3jgbr681qAUvgMqi865GW5gWLJ/AnzXbvjQRrny0e++NAN8aphav/aRSrfFxNsNjpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-arm-gnueabihf": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.61.0.tgz",
+ "integrity": "sha512-svx59iYL+DbaZGZUIoice4W0CjRXGExnbz7Re+awIb60rVxBS2KrU7Hnlx+nZYanLGLpjneUEgo/VFEKkSZAyQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-arm-musleabihf": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.61.0.tgz",
+ "integrity": "sha512-BYK9MPJPCf6d+fLKMTruThmEyCtHzQ1zLcsrTlUVkmnoXIaHAbfpeLYQwX1tkjs7W11dyzoi6HFvKcdnvX1zNg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-arm64-gnu": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.61.0.tgz",
+ "integrity": "sha512-QUaCNLq2/EC6G5ljOuFanl9Lgw6ZWp4co7rs4+KOMUzbGfA4Lq58FHRjjF9sVIG+93XSbo343MxFATrOU1qctA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-arm64-musl": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.61.0.tgz",
+ "integrity": "sha512-S6uvJ6MXnRXl+zTs0CARNDvkE+cymj0EVWEKKsyKnlLlqTyQJBjw5s4D2pSIOZc+S46cy4STefzcr/sm0VzVPA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-ppc64-gnu": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.61.0.tgz",
+ "integrity": "sha512-6VDlRcytvZG6UlSIdAFKDLbppo9tvPxrWzle6vHldYFMeuDPQEfMKrkwezp7FaBq1wik9ra554ZZeRPsyIkFpg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-riscv64-gnu": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.61.0.tgz",
+ "integrity": "sha512-KkBTYbzExpbmn15XjKPLu2fRV2PVlq+KWt+brad5rwIa03vdYoaDRWiS7raHII/dCTR6Ro4UpYUCH4t6lif4WQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-riscv64-musl": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.61.0.tgz",
+ "integrity": "sha512-69tzIq7sJLVB9dxYYtvMzcSSsnZHSO+U2U19O2RqDqgj6+Q4O7HjSXdaszbcgqzhsUwzSH7z5kWvk8nmf6BHTg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-s390x-gnu": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.61.0.tgz",
+ "integrity": "sha512-Oqi/N0OvtOVXsPKAOOhKgGH3msRYF8BLJaNBbWiupRiKoKVyc8JRKPCfarkQJC+RgP9U8raUKLe+bNwd0HUMiA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-x64-gnu": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.61.0.tgz",
+ "integrity": "sha512-3TKwv/ed4uwJSemAA8P9XcoqETpjQI4waquF9UilhA9Mn/dhr1PdUEXWlL74mtc6ZNfmKPA9+NEJm01nRF8CVA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-linux-x64-musl": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.61.0.tgz",
+ "integrity": "sha512-uFso4u4nLkVSlMCpgjyvWV60Gt7GvDQHnk1mmRxHIkZTMB0ljpUKwCD9FYGgN9H97x2wYl0UwEjgRZaPIuhEhw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-openharmony-arm64": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.61.0.tgz",
+ "integrity": "sha512-keGLkzeOvkMpNmPp4hffXWpfoSsY6e1K8++KXD4mSSfxdvM8q9QUDsYY689TB1k6Co832DZn1MnaaVx6cIBMWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-win32-arm64-msvc": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.61.0.tgz",
+ "integrity": "sha512-VzsAISkFxmNhJ5LBDEL9VuH6tJsVJMtqYit2LyIUf/HLnsCe4Pg9SMOjjVQzGWt0bnpyfJ94CrqTqcpNZzK+ug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-win32-ia32-msvc": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.61.0.tgz",
+ "integrity": "sha512-xv4t7yzwJoYaLB6Zv28B3W+j7brEjsyv50rLTAQgmxJzddce9fAMCxed8dSAkbWES0zz2J29nYK5FaTuD2YBHg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxfmt/binding-win32-x64-msvc": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.61.0.tgz",
+ "integrity": "sha512-6EZXFkqOwxdDYjIn3TSNnPk3ST5E5GiYd4FiM6UF/mCL/LZSfr6D6UygTfW3R1PCQP2quCKpCEGRlij8E3VYbg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.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"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.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"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.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"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@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"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@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"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@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"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@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"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@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"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz",
+ "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==",
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": 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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "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"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.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/oxfmt": {
+ "version": "0.61.0",
+ "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.61.0.tgz",
+ "integrity": "sha512-DxdHBEMYpcEnHoUHjjOigUqV2TYKsvxLwUPXnVYBjgFdqrcQ/91OtwubtZ2PUodCs3sStI8R5Qw3fKNGK4e8wQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinypool": "2.1.0"
+ },
+ "bin": {
+ "oxfmt": "bin/oxfmt"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ },
+ "optionalDependencies": {
+ "@oxfmt/binding-android-arm-eabi": "0.61.0",
+ "@oxfmt/binding-android-arm64": "0.61.0",
+ "@oxfmt/binding-darwin-arm64": "0.61.0",
+ "@oxfmt/binding-darwin-x64": "0.61.0",
+ "@oxfmt/binding-freebsd-x64": "0.61.0",
+ "@oxfmt/binding-linux-arm-gnueabihf": "0.61.0",
+ "@oxfmt/binding-linux-arm-musleabihf": "0.61.0",
+ "@oxfmt/binding-linux-arm64-gnu": "0.61.0",
+ "@oxfmt/binding-linux-arm64-musl": "0.61.0",
+ "@oxfmt/binding-linux-ppc64-gnu": "0.61.0",
+ "@oxfmt/binding-linux-riscv64-gnu": "0.61.0",
+ "@oxfmt/binding-linux-riscv64-musl": "0.61.0",
+ "@oxfmt/binding-linux-s390x-gnu": "0.61.0",
+ "@oxfmt/binding-linux-x64-gnu": "0.61.0",
+ "@oxfmt/binding-linux-x64-musl": "0.61.0",
+ "@oxfmt/binding-openharmony-arm64": "0.61.0",
+ "@oxfmt/binding-win32-arm64-msvc": "0.61.0",
+ "@oxfmt/binding-win32-ia32-msvc": "0.61.0",
+ "@oxfmt/binding-win32-x64-msvc": "0.61.0"
+ },
+ "peerDependencies": {
+ "svelte": "^5.0.0",
+ "vite-plus": "*"
+ },
+ "peerDependenciesMeta": {
+ "svelte": {
+ "optional": true
+ },
+ "vite-plus": {
+ "optional": true
+ }
+ }
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinypool": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz",
+ "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.0.0 || >=22.0.0"
+ }
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": 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==",
+ "dev": true,
+ "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..0a845f7
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "windup-frontend",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "oxlint",
+ "format": "oxfmt",
+ "format:check": "oxfmt --check",
+ "preview": "vite preview",
+ "test": "vitest run --passWithNoTests",
+ "typecheck": "tsc -b"
+ },
+ "dependencies": {
+ "react": "^19.2.7",
+ "react-dom": "^19.2.7",
+ "react-router": "^8.3.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.3.3",
+ "@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",
+ "oxfmt": "^0.61.0",
+ "oxlint": "^1.71.0",
+ "tailwindcss": "^4.3.3",
+ "typescript": "~6.0.2",
+ "vite": "^8.1.1",
+ "vitest": "^4.1.10"
+ }
+}
diff --git a/frontend/src/app/app.tsx b/frontend/src/app/app.tsx
new file mode 100644
index 0000000..b46ae87
--- /dev/null
+++ b/frontend/src/app/app.tsx
@@ -0,0 +1,36 @@
+import { BrowserRouter, Route, Routes } from 'react-router'
+
+import { AssetLibraryPage } from '@/pages/asset-library'
+import { HomePage } from '@/pages/home'
+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 { AppShell } from './layout'
+
+/**
+ * 路由表与全局外壳。
+ * 页面自己获取所需数据,不再由 app 层构造服务后逐层传入。
+ */
+export function App() {
+ return (
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+ )
+}
diff --git a/frontend/src/app/index.ts b/frontend/src/app/index.ts
new file mode 100644
index 0000000..f1c81c7
--- /dev/null
+++ b/frontend/src/app/index.ts
@@ -0,0 +1 @@
+export { App } from './app'
diff --git a/frontend/src/app/layout/index.tsx b/frontend/src/app/layout/index.tsx
new file mode 100644
index 0000000..aa21b82
--- /dev/null
+++ b/frontend/src/app/layout/index.tsx
@@ -0,0 +1,29 @@
+import type { ReactNode } from 'react'
+import { Link } from 'react-router'
+
+/** 跨页面常驻导航属于应用外壳,由 app 层统一承载。 */
+
+export interface AppShellProps {
+ /** 渲染在全局导航下方的当前路由页面。 */
+ children: ReactNode
+}
+
+/** 全站外壳,全局导航常驻。 */
+export function AppShell({ children }: AppShellProps) {
+ return (
+
+
+
{children}
+
+ )
+}
diff --git a/frontend/src/entities/action-template/index.ts b/frontend/src/entities/action-template/index.ts
new file mode 100644
index 0000000..2dafa39
--- /dev/null
+++ b/frontend/src/entities/action-template/index.ts
@@ -0,0 +1,14 @@
+interface ActionTemplateBase {
+ id: string
+ name: string
+ prompt: string
+}
+
+/** 系统内置模板没有项目归属;项目自定义模板必须携带所属 Project ID。 */
+export type ActionTemplate = ActionTemplateBase &
+ ({ scope: 'system'; projectId: null } | { scope: 'project'; projectId: string })
+
+/** ActionTemplate 对应的一组后端接口。 */
+export interface ActionTemplateApis {
+ listAvailable(projectId: string): Promise
+}
diff --git a/frontend/src/entities/character/index.ts b/frontend/src/entities/character/index.ts
new file mode 100644
index 0000000..616b5db
--- /dev/null
+++ b/frontend/src/entities/character/index.ts
@@ -0,0 +1,137 @@
+/**
+ * 动作「如何被定义」的来源维度:preset 复用预设定义,custom 由用户自定义。
+ * 它与动作做什么的 ActionType 相互独立,例如 custom + walk 和 preset + custom 都是合法组合。
+ */
+export type ActionKind = 'preset' | 'custom'
+
+/**
+ * 动作「做什么」的业务语义维度;custom 表示不属于当前内置语义枚举。
+ * 它不表示定义来源:custom 来源仍可描述 walk,preset 来源也可承载 custom 业务语义。
+ */
+export type ActionType = 'walk' | 'idle' | 'attack' | 'jump' | 'custom'
+
+/** 单帧相对动作首帧的根位移,单位为像素。 */
+export interface FrameRootMotion {
+ dx: number
+ /** 正值表示向上。 */
+ dy: number
+}
+
+/**
+ * 动作序列中的一张有序画面;帧序号由其在 Action.frames 中的位置决定。
+ *
+ * 不带任何审核字段:服务端只交付生成好的帧,不返回质检结论;用户侧的审核也只是查看,
+ * 没有打回。前端若要做自动质检,那是读取帧之后在本地算出来的临时结论,不属于资产数据。
+ */
+export interface Frame {
+ imageUrl: string
+ /**
+ * 此帧的显示时长,单位毫秒。
+ * null 表示该帧没有独立时长,读取方才使用所属 Action.fps 计算等时长回退值。
+ */
+ durationMs: number | null
+ /** null 表示不提供根位移,Playtest 与 Export 不应据此施加任何位移。 */
+ rootMotion: FrameRootMotion | null
+}
+
+/** 一张母版候选;attemptId 用于区分候选所属的生成尝试。 */
+export interface CharacterTemplateCandidate {
+ id: string
+ imageUrl: string
+ attemptId: string
+}
+
+/** 确认母版候选时同时命名造型与候选,避免两个字符串 ID 在调用处颠倒。 */
+export interface ConfirmCharacterTemplateInput {
+ outfitId: string
+ candidateId: string
+}
+
+/** 造型的基础参考帧;方向与质检结构等待真实资产契约后再扩展。 */
+export interface BaseFrame {
+ readonly imageUrl: string
+}
+
+/** 某个角色造型下的一段动画动作。 */
+export interface Action {
+ /**
+ * 仅在所属 Outfit 内唯一。动作没有自己的表,整棵树存在 character 记录里,
+ * 因此不存在全局唯一的动作 ID:任何按 ID 定位动作的地方都必须同时带上造型。
+ */
+ id: string
+ outfitId: Outfit['id']
+ name: string
+ /** 定义来源方式;与 type 正交,不用于推断动作业务语义。 */
+ kind: ActionKind
+ /** 动作业务语义;与 kind 的 preset/custom 来源维度相互独立。 */
+ type: ActionType
+ /**
+ * 每秒播放帧数。仅当某帧 durationMs 为 null 时用于等时长回退;
+ * Playtest 与 Export 不得用前端全局常量替代,也不得覆盖帧自己的 durationMs。
+ */
+ fps: number
+ /**
+ * 攻击触点、跳跃顶点等关键时刻在 frames 中的零基下标;null 表示没有明确关键帧。
+ * 非 null 值必须指向当前 frames 数组内的成员。
+ */
+ keyFrameIndex: number | null
+ /**
+ * 按播放顺序排列的帧;数组下标就是零基帧序号。
+ * 当前只表达单朝向。Project.directionalMovement 的四向/八向要如何落到这里
+ * (本层再分组,还是一个朝向一条 Action)尚未有产品定义,不要凭猜先定结构。
+ */
+ frames: Frame[]
+}
+
+/** 同一角色的一套独立造型;MVP UI 只展示第一套,但数据结构不折叠该层。 */
+export interface Outfit {
+ /**
+ * 仅在所属 Character 内唯一。造型没有自己的表,与动作一起存在 character 记录里,
+ * 因此不存在全局唯一的造型 ID:任何按 ID 定位造型的地方都必须同时带上角色。
+ */
+ id: string
+ characterId: string
+ name: string
+ /** 母版生成阶段返回的候选;生成完成前可以为空数组。 */
+ candidateCharacterTemplates: CharacterTemplateCandidate[]
+ /** 用户从候选图中选定的角色母版 URL;尚未选定时为 null。 */
+ characterTemplateUrl: string | null
+ /** 供后续动作生成使用的只读基础帧入口。 */
+ readonly baseFrames: readonly BaseFrame[]
+ /** 每个 Action.outfitId 必须等于本造型 ID。 */
+ actions: Action[]
+}
+
+/**
+ * 项目下的角色资产;造型拥有各自的母版和动作帧。
+ *
+ * 这棵树只承载已导出到资产库的内容,因此其中的动作一律是已确认的,不带生成过程状态。
+ * 工作流运行期间的造型、动作和帧活在 WorkflowRun 的步骤里,直到用户确认导出才整体写入。
+ */
+export interface Character {
+ id: string
+ projectId: string
+ /** 角色的全部独立造型;MVP 页面至少保留这一层,即使当前只有一个成员。 */
+ outfits: Outfit[]
+ createdAt: string
+ updatedAt: string
+}
+
+/** 创建角色并发起母版生成所需的入参。 */
+export interface CreateCharacterInput {
+ projectId: string
+ /** 交给模型生成母版。 */
+ description: string
+ referenceImageUrl?: string | null
+}
+
+/**
+ * Character 对应的一组后端接口。
+ * 造型、动作和帧是 Character 内的完整树,不通过独立粒度方法写入;每次确认后整棵更新。
+ */
+export interface CharacterApis {
+ get(id: Character['id']): Promise
+ listByProject(projectId: string): Promise
+ create(input: CreateCharacterInput): Promise
+ update(character: Character): Promise
+}
diff --git a/frontend/src/entities/generation/index.ts b/frontend/src/entities/generation/index.ts
new file mode 100644
index 0000000..7ca1889
--- /dev/null
+++ b/frontend/src/entities/generation/index.ts
@@ -0,0 +1,128 @@
+import type { ActionType } from '../character'
+import type { MediaReference } from '../media'
+import type { Task, TaskStatus } from '../task'
+
+/**
+ * Generation 是业务数据,不是「调用图片生成能力」。
+ * 前端只创建 generation 并订阅它的状态;真正调用模型的是后端,前端不接触那一层。
+ */
+
+/** 生成对应的三个前端可见异步步骤。 */
+export type GenerationType = 'character_template' | 'first_frame' | 'complete_animation'
+
+interface GenerationInputBase {
+ projectId: string
+ /** 可选参考媒体;没有参考图时传空数组。 */
+ referenceMedia: readonly MediaReference[]
+}
+
+/** 角色母版候选生成。 */
+export interface CharacterTemplateGenerationInput extends GenerationInputBase {
+ type: 'character_template'
+ /** 已由手动输入或 Quick Start 整理好的角色提示词。 */
+ prompt: string
+}
+
+/** 指定角色造型下的动作首帧生成;不能只绑定 Character。 */
+export interface FirstFrameGenerationInput extends GenerationInputBase {
+ type: 'first_frame'
+ characterId: string
+ outfitId: string
+ actionType: ActionType
+ /** 自定义动作或额外动作要求;没有时为 null。 */
+ prompt: string | null
+}
+
+/** 以已确认首帧为起点生成完整动画。 */
+export interface CompleteAnimationGenerationInput extends GenerationInputBase {
+ type: 'complete_animation'
+ characterId: string
+ outfitId: string
+ actionType: ActionType
+ /** 已确认的生成首帧 URL。 */
+ firstFrameUrl: string
+ prompt: string | null
+}
+
+export type GenerationInput =
+ | CharacterTemplateGenerationInput
+ | FirstFrameGenerationInput
+ | CompleteAnimationGenerationInput
+
+/** 后端当前能交付给前端的最小图片结果。 */
+export interface GeneratedImage {
+ url: string
+}
+
+/** 结果按 type 分别定义,不共用一个 urls 数组。 */
+export interface CharacterTemplateGenerationResult {
+ type: 'character_template'
+ images: readonly GeneratedImage[]
+}
+
+/** Task.result 来自运行时边界,写回 WorkflowRun 前必须按生成类型收窄。 */
+export function parseCharacterTemplateGenerationResult(
+ value: unknown,
+): CharacterTemplateGenerationResult | null {
+ if (!isRecord(value) || value.type !== 'character_template' || !Array.isArray(value.images)) {
+ return null
+ }
+ const images = value.images.filter(
+ (image): image is GeneratedImage =>
+ isRecord(image) && typeof image.url === 'string' && image.url.length > 0,
+ )
+ if (images.length === 0 || images.length !== value.images.length) return null
+ return { type: 'character_template', images }
+}
+
+export interface FirstFrameGenerationResult {
+ type: 'first_frame'
+ image: GeneratedImage
+}
+
+/** 帧顺序由数组位置表达。 */
+export interface CompleteAnimationGenerationResult {
+ type: 'complete_animation'
+ frames: readonly GeneratedImage[]
+}
+
+export type GenerationResult =
+ | CharacterTemplateGenerationResult
+ | FirstFrameGenerationResult
+ | CompleteAnimationGenerationResult
+
+export type GenerationResultFor =
+ T extends CharacterTemplateGenerationInput
+ ? CharacterTemplateGenerationResult
+ : T extends FirstFrameGenerationInput
+ ? FirstFrameGenerationResult
+ : CompleteAnimationGenerationResult
+
+/**
+ * 一次生成任务的完整快照。
+ * 它是服务端的资源,不是一次「调用能力」——前端创建它,然后订阅或轮询它的状态。
+ */
+export interface Generation {
+ /** 创建接口返回的后端 Task ID;后续查询与订阅必须把它交给 TaskApis。 */
+ id: Task['id']
+ projectId: string
+ /** 与创建时的输入判别字段保持同一字面量类型。 */
+ type: TType
+ status: TaskStatus
+ /** 完成前为 null;完成后形状由 type 决定。 */
+ result: GenerationResult | null
+ /** status 为 failed 时有值。 */
+ error: string | null
+}
+
+/** Generation 对应的一组后端接口。 */
+export interface GenerationApis {
+ /** 创建一次生成任务。 */
+ create(input: T): Promise>
+ /** 按所属项目和任务 ID 读取生成任务的最新快照。 */
+ get(projectId: Generation['projectId'], id: Generation['id']): Promise
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
+}
diff --git a/frontend/src/entities/index.ts b/frontend/src/entities/index.ts
new file mode 100644
index 0000000..33b1ff8
--- /dev/null
+++ b/frontend/src/entities/index.ts
@@ -0,0 +1,80 @@
+/**
+ * entities 唯一公开入口。外部不得绕过本文件访问内部文件。
+ * 外部只从这里使用实体契约与已经落地的实体能力。
+ */
+
+/* 项目 —— 全局约束:视角、朝向、精灵尺寸、画风 */
+export { CHARACTER_PERSPECTIVE, DIRECTIONAL_MOVEMENT, SPRITE_SIZES } from './project'
+export type {
+ CharacterPerspective,
+ CreateProjectInput,
+ DirectionalMovement,
+ Project,
+ ProjectApis,
+ UpdateProjectInput,
+} from './project'
+
+/* 角色 —— 资产本体;造型、动作、帧都在这棵树里 */
+export type {
+ Action,
+ ActionKind,
+ ActionType,
+ BaseFrame,
+ Character,
+ CharacterApis,
+ CharacterTemplateCandidate,
+ ConfirmCharacterTemplateInput,
+ CreateCharacterInput,
+ Frame,
+ FrameRootMotion,
+ Outfit,
+} from './character'
+
+/* 动作模板 —— 能跨角色复用的配方 */
+export type { ActionTemplate, ActionTemplateApis } from './action-template'
+
+/* 生成 —— 业务数据,不是「调用生成能力」 */
+export { parseCharacterTemplateGenerationResult } from './generation'
+export type {
+ CharacterTemplateGenerationInput,
+ CharacterTemplateGenerationResult,
+ CompleteAnimationGenerationInput,
+ CompleteAnimationGenerationResult,
+ FirstFrameGenerationInput,
+ FirstFrameGenerationResult,
+ GeneratedImage,
+ Generation,
+ GenerationApis,
+ GenerationInput,
+ GenerationResult,
+ GenerationResultFor,
+ GenerationType,
+} from './generation'
+
+/* 媒体引用 —— 不承诺 URL 或后端 Media ID 的具体表示 */
+export type { MediaReference } from './media'
+
+/* 后端异步任务 —— 与工作流节点是两回事 */
+export type { Task, TaskApis, TaskEvent, TaskStatus, TaskType } from './task'
+
+/* 工作流 —— 节点与运行状态都由前端管理 */
+export { createWorkflowRunStore, WORKFLOW_STEP_ORDER } from './workflow-run'
+export type {
+ CharacterSetupStepInput,
+ CharacterSetupWorkflowStep,
+ CharacterTemplateWorkflowStep,
+ CreateWorkflowRunStoreOptions,
+ CreateWorkflowRunInput,
+ ExportStatus,
+ GenerationStatus,
+ WorkflowDriver,
+ WorkflowStep,
+ WorkflowStepStatus,
+ WorkflowStepType,
+ WorkflowRevision,
+ WorkflowRevisionStatus,
+ WorkflowRun,
+ WorkflowRunStore,
+ WorkflowRunPurpose,
+ WorkflowRunStatus,
+} from './workflow-run'
diff --git a/frontend/src/entities/media/index.ts b/frontend/src/entities/media/index.ts
new file mode 100644
index 0000000..347e626
--- /dev/null
+++ b/frontend/src/entities/media/index.ts
@@ -0,0 +1,9 @@
+declare const mediaReferenceBrand: unique symbol
+
+/**
+ * 已上传媒体的不透明引用。
+ * 当前不承诺运行时字符串代表 URL、media_id 或其他后端标识。
+ */
+export type MediaReference = string & {
+ readonly [mediaReferenceBrand]: 'MediaReference'
+}
diff --git a/frontend/src/entities/project/index.ts b/frontend/src/entities/project/index.ts
new file mode 100644
index 0000000..4c71164
--- /dev/null
+++ b/frontend/src/entities/project/index.ts
@@ -0,0 +1,82 @@
+import type { Paged, PageQuery } from '@/shared/pagination'
+
+/** Project 前端领域形状;字段只表达当前页面需要,不对应任何已确认后端 DTO。 */
+export interface Project {
+ id: string
+ /** Project 所属用户 ID;认证来源尚未冻结。 */
+ ownerId: string
+ name: string
+ /** 游戏视角,见 CHARACTER_PERSPECTIVE。 */
+ perspective: CharacterPerspective
+ /** 移动方向,见 DIRECTIONAL_MOVEMENT。 */
+ directionalMovement: DirectionalMovement
+ /** 当前页面使用建议档位,后端范围尚未确认。 */
+ spriteSize: { width: number; height: number }
+ /** 项目级画风描述,作为本项目所有角色和动作生成的视觉约束。 */
+ gameStyle: string | null
+ /**
+ * 项目级画风参考图,本项目所有角色和动作都遵循它的视觉风格。
+ * 它不决定某个角色具体长什么样;角色自身参考图由 CreateCharacterInput.referenceImageUrl 表达。
+ */
+ sampleImageUrl: string | null
+ /** ISO 8601 字符串。 */
+ createdAt: string
+ /** ISO 8601 字符串。 */
+ updatedAt: string
+}
+
+/** 新建项目的入参。 */
+export interface CreateProjectInput {
+ name: string
+ perspective: CharacterPerspective
+ directionalMovement: DirectionalMovement
+ spriteSize: { width: number; height: number }
+ gameStyle?: string | null
+ sampleImageUrl?: string | null
+}
+
+/** 更新项目设置的入参;未提供的字段保持不变。 */
+export interface UpdateProjectInput {
+ name?: string
+ perspective?: CharacterPerspective
+ directionalMovement?: DirectionalMovement
+ spriteSize?: { width: number; height: number }
+ gameStyle?: string | null
+ sampleImageUrl?: string | null
+}
+
+/** 前端使用的游戏视角枚举;后端映射尚未冻结。 */
+export type CharacterPerspective = 'side' | 'top-down' | 'isometric'
+
+/** 前端使用的移动方向枚举;后端映射尚未冻结。 */
+export type DirectionalMovement = 'single' | 'four-way' | 'eight-way'
+
+/** 游戏视角的页面文案。 */
+export const CHARACTER_PERSPECTIVE: Record = {
+ side: '横版视角',
+ 'top-down': '俯视',
+ isometric: '2.5D',
+}
+
+/**
+ * 移动方向,决定一个动作要生成几套朝向的帧。
+ * 多朝向在 Action 上如何存放尚未定义,当前 Action.frames 只表达单朝向;
+ * 选了四向/八向的项目,生成侧还接不上,见 Action.frames 的说明。
+ */
+export const DIRECTIONAL_MOVEMENT: Record = {
+ single: '单向',
+ 'four-way': '四向',
+ 'eight-way': '八向',
+}
+
+/** UI 使用的建议尺寸档位;不代表后端约束。 */
+export const SPRITE_SIZES = [32, 64, 128, 256, 512, 1024, 2048] as const
+
+/** Project 对应的一组后端接口。 */
+export interface ProjectApis {
+ list(query?: PageQuery): Promise>
+ get(id: Project['id']): Promise
+ create(input: CreateProjectInput): Promise
+ update(id: Project['id'], input: UpdateProjectInput): Promise
+ remove(id: Project['id']): Promise
+}
diff --git a/frontend/src/entities/task/index.ts b/frontend/src/entities/task/index.ts
new file mode 100644
index 0000000..37e0950
--- /dev/null
+++ b/frontend/src/entities/task/index.ts
@@ -0,0 +1,52 @@
+/**
+ * 异步任务实体,与 WorkflowRun 的页面节点是两回事。
+ *
+ * 任务粒度 = 前端可见的一个异步步骤,而不是内部某次模型调用。
+ * 任务生命周期不等于工作流节点状态,两者只由 WorkflowStep.taskId 单向关联:
+ * 步骤记得自己发起过哪个任务,任务不认识步骤。
+ */
+
+/** 后端异步任务状态;pending 表示已提交但尚未执行。 */
+export type TaskStatus = 'pending' | 'running' | 'completed' | 'failed'
+
+/**
+ * MS2 前端需要展示和恢复的三类异步步骤。
+ * 完整动画内部可包含视频生成、截帧和多次图像处理,但对前端仍是一个 Task。
+ */
+export type TaskType = 'character_template' | 'first_frame' | 'complete_animation'
+
+/**
+ * 创建、查询和断线恢复都使用的完整任务快照。
+ * TType 在调用边界已知时保留精确任务类型;按 ID 恢复时使用默认值,等待运行时解析后再收窄。
+ */
+export interface Task {
+ /** 后端生成的任务 ID,是查询状态和订阅事件的唯一标识。 */
+ id: string
+ /** 后端 task_type 对应的领域判别字段,不接受任意字符串。 */
+ type: TType
+ /** 后端任务当前状态;它不会直接改变工作流节点状态。 */
+ status: TaskStatus
+ /** 失败原因,status 为 failed 时有值。completed 不表示工作流节点已通过。 */
+ error: string | null
+ /** 任务产出;契约冻结前保持 unknown,避免调用方依赖猜测结构。 */
+ result: unknown
+}
+
+/** 每条事件携带同类型 Task 的完整状态,taskId 对应 Task.id。 */
+export interface TaskEvent extends Omit, 'id'> {
+ taskId: Task['id']
+}
+
+/** Task 对应的一组后端接口。服务端没有取消能力,因此这里不声明 cancel。 */
+export interface TaskApis {
+ /**
+ * 按所属项目和后端 Task ID 读取最新快照。
+ * projectId 不能从 taskId 推导;后端查询接口要求两者同时传入。
+ */
+ get(projectId: string, taskId: Task['id']): Promise
+ /**
+ * 订阅后必须立即发送一次最新完整快照,随后再发送状态变化;任务已经终止也必须发送。
+ * 该语义关闭 get/create 与开始监听之间的终态竞态。
+ */
+ subscribe(projectId: string, taskId: Task['id'], onEvent: (event: TaskEvent) => void): () => void
+}
diff --git a/frontend/src/entities/workflow-run/constants.ts b/frontend/src/entities/workflow-run/constants.ts
new file mode 100644
index 0000000..16714ea
--- /dev/null
+++ b/frontend/src/entities/workflow-run/constants.ts
@@ -0,0 +1,19 @@
+export const WORKFLOW_DRIVERS = ['ai', 'manual'] as const
+export const WORKFLOW_PURPOSES = ['create_character', 'add_action'] as const
+export const WORKFLOW_RUN_STATUSES = ['active', 'interrupted', 'completed', 'failed'] as const
+export const WORKFLOW_REVISION_STATUSES = ['active', 'completed', 'failed', 'abandoned'] as const
+export const GENERATION_STATUSES = ['not_started', 'in_progress', 'completed', 'failed'] as const
+export const EXPORT_STATUSES = ['not_exported', 'exporting', 'exported', 'failed'] as const
+export const WORKFLOW_STEP_STATUSES = ['locked', 'available', 'active', 'passed', 'failed'] as const
+
+/** 当前产品工作流的固定八步,也是存储校验与进度 UI 的唯一顺序来源。 */
+export const WORKFLOW_STEP_ORDER = [
+ 'character-setup',
+ 'character-template',
+ 'template-candidate',
+ 'action-setup',
+ 'first-frame',
+ 'complete-animation',
+ 'review',
+ 'export',
+] as const
diff --git a/frontend/src/entities/workflow-run/index.ts b/frontend/src/entities/workflow-run/index.ts
new file mode 100644
index 0000000..f6bd2a1
--- /dev/null
+++ b/frontend/src/entities/workflow-run/index.ts
@@ -0,0 +1,197 @@
+import type {
+ CharacterTemplateGenerationInput,
+ CharacterTemplateGenerationResult,
+} from '../generation'
+import type { MediaReference } from '../media'
+import type { Task } from '../task'
+import {
+ EXPORT_STATUSES,
+ GENERATION_STATUSES,
+ WORKFLOW_DRIVERS,
+ WORKFLOW_PURPOSES,
+ WORKFLOW_REVISION_STATUSES,
+ WORKFLOW_RUN_STATUSES,
+ WORKFLOW_STEP_ORDER,
+ WORKFLOW_STEP_STATUSES,
+} from './constants'
+
+export { WORKFLOW_STEP_ORDER } from './constants'
+
+/** Quick Start 与手动工作流只改变输入方式,共用同一种运行模型。 */
+export type WorkflowDriver = (typeof WORKFLOW_DRIVERS)[number]
+
+/** 创建 WorkflowRun 时要完成的用户意图。 */
+export type WorkflowRunPurpose = (typeof WORKFLOW_PURPOSES)[number]
+
+/**
+ * 流程步骤类型的唯一标准顺序;它不是后端 Workflow 或 Execution 定义。
+ * 某个 Revision 已进入执行线的步骤顺序,由 WorkflowRevision.steps 的数组位置表达。
+ */
+/** 前端流程步骤类型,与 WORKFLOW_STEP_ORDER 的成员保持一致。 */
+export type WorkflowStepType = (typeof WORKFLOW_STEP_ORDER)[number]
+
+/**
+ * 步骤的可用性和执行结果;不直接复用后端任务状态。
+ * locked/available 表示尚未执行,active 表示当前页面阶段,passed/failed 表示结果。
+ */
+export type WorkflowStepStatus = (typeof WORKFLOW_STEP_STATUSES)[number]
+
+/**
+ * 单个版本的生命周期。
+ * abandoned 表示停止沿用但仍保留为历史。
+ */
+export type WorkflowRevisionStatus = (typeof WORKFLOW_REVISION_STATUSES)[number]
+
+/**
+ * 整次流程的汇总状态。
+ * interrupted 只表示用户主动停止自动推进:历史仍保留且可只读查看,它不等于 failed 或 completed。
+ * 后端 Task 是否真正停止是独立问题;当前纵切不提供重启操作。
+ */
+export type WorkflowRunStatus = (typeof WORKFLOW_RUN_STATUSES)[number]
+
+/** 当前版本在生成阶段的汇总状态;素材准备期间为 not_started。 */
+export type GenerationStatus = (typeof GENERATION_STATUSES)[number]
+
+/** 当前版本在导出阶段的汇总状态。 */
+export type ExportStatus = (typeof EXPORT_STATUSES)[number]
+
+interface WorkflowStepBase {
+ /** 只用于编排和页面定位,不作为业务 ID 发送给后端。 */
+ id: string
+ status: WorkflowStepStatus
+ /**
+ * 本步骤已提交、结果尚未写回 output 的生成任务 ID;没有在途任务时为 null。
+ * 它由前端随 WorkflowRun 一起维护,据此查回在途任务的状态,因而不会在同一次
+ * 前端运行中重复发起生成。是否写入浏览器存储属于前端实现,不形成后端契约。
+ * 任务本身不认识步骤,反向关联不存在。
+ */
+ taskId: Task['id'] | null
+ /**
+ * 前端开始提交、但后端 taskId 尚未返回时的本地尝试标识。
+ * 它非 null 而 taskId 为 null 时不能重复提交;若页面在这个窗口刷新,
+ * Controller 会把本地 Run 标为失败。它不是后端字段,也不冒充幂等键。
+ */
+ submissionId: string | null
+ /** 步骤失败后供页面解释原因;未失败时必须为 null。 */
+ error: string | null
+ /** 该步骤沿用或依赖的步骤 ID,用于版本来源追踪,不代表后端执行依赖。 */
+ referenceStepIds: string[]
+}
+
+/** 角色资料步骤保存的输入;参考媒体为空表示仅使用文字描述。 */
+export interface CharacterSetupStepInput {
+ description: string
+ referenceMedia: readonly MediaReference[]
+}
+
+export interface CharacterSetupWorkflowStep extends WorkflowStepBase {
+ type: 'character-setup'
+ input: CharacterSetupStepInput | null
+ output: null
+}
+
+export interface CharacterTemplateWorkflowStep extends WorkflowStepBase {
+ type: 'character-template'
+ /** 发起任务前为 null;提交时保存实际发送给 GenerationApis 的输入快照。 */
+ input: CharacterTemplateGenerationInput | null
+ output: CharacterTemplateGenerationResult | null
+}
+
+type RemainingWorkflowStepType = Exclude
+
+interface RemainingWorkflowStep extends WorkflowStepBase {
+ type: RemainingWorkflowStepType
+ /** 候选选择及后五步尚未实现,输入输出等对应纵切开始时再收窄。 */
+ input: unknown
+ output: unknown
+}
+
+/**
+ * 一个 Revision 中已经进入执行线的流程步骤。
+ * 前两个执行步骤已冻结输入输出;候选选择及后五步进入对应纵切时再收窄,
+ * 不提前猜页面尚未产生的数据形状。
+ */
+export type WorkflowStep =
+ | CharacterSetupWorkflowStep
+ | CharacterTemplateWorkflowStep
+ | RemainingWorkflowStep
+
+/**
+ * 一次页面执行版本;当前版本会推进,从旧步骤重开则追加新版本。
+ *
+ * 当前本地存储版本只接受单条执行线:revisions 恒为一个成员,
+ * basedOnRevisionId 与 restartStepId 恒为 null。「从历史步骤重开并保留旧版本」
+ * 尚未进入产品定义;实现时需要同步升级存储版本和迁移规则。
+ */
+export interface WorkflowRevision {
+ id: string
+ /** 首次创建的版本没有来源,因此为 null。 */
+ basedOnRevisionId: string | null
+ /** 在来源版本中选择的重启步骤 ID;非重启创建的版本为 null。 */
+ restartStepId: string | null
+ status: WorkflowRevisionStatus
+ /**
+ * 当前版本固定保存全部八步;数组位置是步骤顺序的唯一来源。
+ * 完整步骤类型顺序以 WORKFLOW_STEP_ORDER 为准。
+ */
+ steps: WorkflowStep[]
+ generationStatus: GenerationStatus
+ exportStatus: ExportStatus
+ createdAt: string
+}
+
+/**
+ * 一次由前端推进的页面流程。
+ * 步骤推进和运行状态都由前端管理;后端不读取、不推进、也不持久化 WorkflowRun。
+ * 后端只处理生成任务,并在用户最终确认时持久化角色与动作资产。
+ */
+export interface WorkflowRun {
+ id: string
+ projectId: string
+ /** 已关联的 Character ID;角色尚未创建或确认时为 null。 */
+ characterId: string | null
+ /** 已有角色加动作时的目标造型;新建角色时为 null。 */
+ outfitId: string | null
+ purpose: WorkflowRunPurpose
+ driver: WorkflowDriver
+ status: WorkflowRunStatus
+ /** 当前可编辑版本 ID;必须能在 revisions 中找到。 */
+ currentRevisionId: string
+ /** 按创建顺序保存的全部版本;历史版本保留用于只读查看和重启。 */
+ revisions: WorkflowRevision[]
+ /** Quick Start 的规范化提示词;空白输入或手动模式无提示词时为 null。 */
+ prompt: string | null
+}
+
+/** 两种入口共享的创建字段。 */
+interface CreateWorkflowRunInputBase {
+ projectId: string
+ driver: WorkflowDriver
+ /** Quick Start 的自然语言需求;提交时去除首尾空白,空字符串按 null 保存。 */
+ prompt?: string
+}
+
+/**
+ * 创建 WorkflowRun 的输入。
+ * add_action 分支把已有角色、造型、母版和基准帧设为必填,避免创建无法恢复的半成品运行。
+ */
+export type CreateWorkflowRunInput = CreateWorkflowRunInputBase &
+ (
+ | {
+ purpose: 'create_character'
+ characterId?: never
+ outfitId?: never
+ characterTemplateUrl?: never
+ baseFrameUrls?: never
+ }
+ | {
+ purpose: 'add_action'
+ characterId: string
+ outfitId: string
+ characterTemplateUrl: string
+ baseFrameUrls: readonly string[]
+ }
+ )
+
+export { createWorkflowRunStore } from './store'
+export type { CreateWorkflowRunStoreOptions, WorkflowRunStore } from './store'
diff --git a/frontend/src/entities/workflow-run/store.test.ts b/frontend/src/entities/workflow-run/store.test.ts
new file mode 100644
index 0000000..c165a4e
--- /dev/null
+++ b/frontend/src/entities/workflow-run/store.test.ts
@@ -0,0 +1,238 @@
+import { describe, expect, it, vi } from 'vitest'
+
+import { WORKFLOW_STEP_ORDER } from './constants'
+import type { WorkflowRun, WorkflowStep } from './index'
+import {
+ createWorkflowRunStore,
+ WORKFLOW_RUN_STORAGE_KEY,
+ WORKFLOW_RUN_STORAGE_VERSION,
+} from './store'
+
+class TestStorage {
+ value: string | null
+ failOnSet = false
+
+ constructor(value: string | null = null) {
+ this.value = value
+ }
+
+ getItem(): string | null {
+ return this.value
+ }
+
+ setItem(_key: string, value: string): void {
+ if (this.failOnSet) throw new Error('storage unavailable')
+ this.value = value
+ }
+}
+
+function createSteps(): WorkflowStep[] {
+ return WORKFLOW_STEP_ORDER.map((type, index) => {
+ const common = {
+ id: `revision-1:${type}`,
+ status: index === 0 ? ('active' as const) : ('locked' as const),
+ taskId: null,
+ submissionId: null,
+ error: null,
+ referenceStepIds: [],
+ }
+ if (type === 'character-setup') {
+ return {
+ ...common,
+ type,
+ input: { description: 'slime', referenceMedia: [] },
+ output: null,
+ }
+ }
+ if (type === 'character-template') {
+ return { ...common, type, input: null, output: null }
+ }
+ return { ...common, type, input: null, output: null } as WorkflowStep
+ })
+}
+
+function createRun(id = 'run-1'): WorkflowRun {
+ return {
+ id,
+ projectId: 'project-1',
+ characterId: null,
+ outfitId: null,
+ purpose: 'create_character',
+ driver: 'ai',
+ status: 'active',
+ currentRevisionId: 'revision-1',
+ revisions: [
+ {
+ id: 'revision-1',
+ basedOnRevisionId: null,
+ restartStepId: null,
+ status: 'active',
+ steps: createSteps(),
+ generationStatus: 'not_started',
+ exportStatus: 'not_exported',
+ createdAt: '2026-07-30T12:00:00.000Z',
+ },
+ ],
+ prompt: 'Create a slime',
+ }
+}
+
+describe('createWorkflowRunStore', () => {
+ it('stores a versioned snapshot and returns defensive clones', () => {
+ const storage = new TestStorage()
+ const store = createWorkflowRunStore({ storage })
+ const source = createRun()
+
+ store.save(source)
+ source.prompt = 'mutated outside'
+
+ const firstRead = store.get(source.id)
+ expect(firstRead?.prompt).toBe('Create a slime')
+
+ firstRead!.revisions[0].steps[0].status = 'failed'
+ expect(store.get(source.id)?.revisions[0].steps[0].status).toBe('active')
+
+ expect(JSON.parse(storage.value!)).toEqual({
+ version: WORKFLOW_RUN_STORAGE_VERSION,
+ runs: [createRun()],
+ })
+ })
+
+ it('hydrates valid runs from localStorage', () => {
+ const run = createRun()
+ const storage = new TestStorage(
+ JSON.stringify({
+ version: WORKFLOW_RUN_STORAGE_VERSION,
+ runs: [run],
+ }),
+ )
+
+ const store = createWorkflowRunStore({ storage })
+
+ expect(store.get(run.id)).toEqual(run)
+ })
+
+ it.each([
+ ['invalid JSON', '{'],
+ ['unknown version', JSON.stringify({ version: 2, runs: [createRun()] })],
+ ['invalid payload', JSON.stringify({ version: WORKFLOW_RUN_STORAGE_VERSION, runs: {} })],
+ [
+ 'invalid run',
+ JSON.stringify({
+ version: WORKFLOW_RUN_STORAGE_VERSION,
+ runs: [{ ...createRun(), currentRevisionId: 'missing-revision' }],
+ }),
+ ],
+ [
+ 'inconsistent run status',
+ JSON.stringify({
+ version: WORKFLOW_RUN_STORAGE_VERSION,
+ runs: [{ ...createRun(), status: 'failed' }],
+ }),
+ ],
+ [
+ 'multiple revisions in storage version 1',
+ JSON.stringify({
+ version: WORKFLOW_RUN_STORAGE_VERSION,
+ runs: [
+ {
+ ...createRun(),
+ revisions: [
+ ...createRun().revisions,
+ {
+ ...createRun().revisions[0],
+ id: 'revision-2',
+ status: 'abandoned',
+ },
+ ],
+ },
+ ],
+ }),
+ ],
+ [
+ 'restart metadata in storage version 1',
+ JSON.stringify({
+ version: WORKFLOW_RUN_STORAGE_VERSION,
+ runs: [
+ {
+ ...createRun(),
+ revisions: [
+ {
+ ...createRun().revisions[0],
+ restartStepId: 'revision-1:character-setup',
+ },
+ ],
+ },
+ ],
+ }),
+ ],
+ ])('ignores %s in localStorage', (_label, serialized) => {
+ const store = createWorkflowRunStore({ storage: new TestStorage(serialized) })
+
+ expect(store.get('run-1')).toBeNull()
+ })
+
+ it('keeps the memory snapshot and notifies subscribers when persistence fails', () => {
+ const storage = new TestStorage()
+ storage.failOnSet = true
+ const store = createWorkflowRunStore({ storage })
+ const listener = vi.fn()
+ const run = createRun()
+
+ store.subscribe(run.id, listener)
+
+ expect(() => store.save(run)).not.toThrow()
+ expect(store.get(run.id)).toEqual(run)
+ expect(listener).toHaveBeenCalledWith(run)
+ })
+
+ it('isolates subscriber values and stops notifications after unsubscribe', () => {
+ const store = createWorkflowRunStore({ storage: null })
+ const run = createRun()
+ const secondListener = vi.fn()
+ const unsubscribeFirst = store.subscribe(run.id, (savedRun) => {
+ savedRun.prompt = 'mutated by first listener'
+ })
+ const unsubscribeSecond = store.subscribe(run.id, secondListener)
+
+ store.save(run)
+
+ expect(secondListener).toHaveBeenLastCalledWith(run)
+ expect(store.get(run.id)).toEqual(run)
+
+ unsubscribeFirst()
+ unsubscribeSecond()
+ store.save({ ...run, prompt: 'new prompt' })
+
+ expect(secondListener).toHaveBeenCalledTimes(1)
+ })
+
+ it('does not let one failing subscriber block the saved state or other subscribers', () => {
+ const store = createWorkflowRunStore({ storage: null })
+ const run = createRun()
+ const secondListener = vi.fn()
+
+ store.subscribe(run.id, () => {
+ throw new Error('render failed')
+ })
+ store.subscribe(run.id, secondListener)
+
+ expect(() => store.save(run)).not.toThrow()
+ expect(store.get(run.id)).toEqual(run)
+ expect(secondListener).toHaveBeenCalledWith(run)
+ })
+
+ it('uses the stable storage key by default', () => {
+ const setItem = vi.fn()
+ const store = createWorkflowRunStore({
+ storage: {
+ getItem: vi.fn(() => null),
+ setItem,
+ },
+ })
+
+ store.save(createRun())
+
+ expect(setItem).toHaveBeenCalledWith(WORKFLOW_RUN_STORAGE_KEY, expect.any(String))
+ })
+})
diff --git a/frontend/src/entities/workflow-run/store.ts b/frontend/src/entities/workflow-run/store.ts
new file mode 100644
index 0000000..1a2f94f
--- /dev/null
+++ b/frontend/src/entities/workflow-run/store.ts
@@ -0,0 +1,265 @@
+import type { WorkflowRun } from './index'
+import { parseCharacterTemplateGenerationResult } from '../generation'
+import {
+ EXPORT_STATUSES,
+ GENERATION_STATUSES,
+ WORKFLOW_DRIVERS,
+ WORKFLOW_PURPOSES,
+ WORKFLOW_REVISION_STATUSES,
+ WORKFLOW_RUN_STATUSES,
+ WORKFLOW_STEP_ORDER,
+ WORKFLOW_STEP_STATUSES,
+} from './constants'
+
+export const WORKFLOW_RUN_STORAGE_KEY = 'windup.workflow-runs'
+export const WORKFLOW_RUN_STORAGE_VERSION = 1
+
+type WorkflowRunListener = (run: WorkflowRun) => void
+
+interface WorkflowRunStorage {
+ getItem(key: string): string | null
+ setItem(key: string, value: string): void
+}
+
+export interface WorkflowRunStore {
+ get(runId: WorkflowRun['id']): WorkflowRun | null
+ save(run: WorkflowRun): void
+ subscribe(runId: WorkflowRun['id'], listener: WorkflowRunListener): () => void
+}
+
+export interface CreateWorkflowRunStoreOptions {
+ /**
+ * 传 null 可显式创建仅内存存储;不传时在浏览器中使用 localStorage。
+ * 该入口也让纯逻辑测试无需模拟完整 DOM。
+ */
+ storage?: WorkflowRunStorage | null
+}
+
+interface PersistedWorkflowRuns {
+ version: typeof WORKFLOW_RUN_STORAGE_VERSION
+ runs: WorkflowRun[]
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
+}
+
+function isNullableString(value: unknown): value is string | null {
+ return typeof value === 'string' || value === null
+}
+
+function isStringArray(value: unknown): value is string[] {
+ return Array.isArray(value) && value.every((item) => typeof item === 'string')
+}
+
+function isMember(value: unknown, members: readonly T[]): value is T {
+ return typeof value === 'string' && members.includes(value as T)
+}
+
+function isWorkflowStep(value: unknown): boolean {
+ if (!isRecord(value)) return false
+
+ const commonFieldsAreValid =
+ typeof value.id === 'string' &&
+ isMember(value.type, WORKFLOW_STEP_ORDER) &&
+ isMember(value.status, WORKFLOW_STEP_STATUSES) &&
+ isNullableString(value.taskId) &&
+ isNullableString(value.submissionId) &&
+ isStringArray(value.referenceStepIds) &&
+ 'input' in value &&
+ 'output' in value
+ if (!commonFieldsAreValid) return false
+ const error = value.error
+ if (!isNullableString(error)) return false
+ if (
+ (value.status === 'failed' && (error === null || error.trim().length === 0)) ||
+ (value.status !== 'failed' && error !== null)
+ ) {
+ return false
+ }
+
+ if (value.type === 'character-setup') {
+ return (
+ value.output === null &&
+ (value.input === null ||
+ (isRecord(value.input) &&
+ typeof value.input.description === 'string' &&
+ isStringArray(value.input.referenceMedia)))
+ )
+ }
+ if (value.type === 'character-template') {
+ return (
+ (value.input === null ||
+ (isRecord(value.input) &&
+ value.input.type === 'character_template' &&
+ typeof value.input.projectId === 'string' &&
+ typeof value.input.prompt === 'string' &&
+ isStringArray(value.input.referenceMedia))) &&
+ (value.output === null || parseCharacterTemplateGenerationResult(value.output) !== null)
+ )
+ }
+ return true
+}
+
+function isWorkflowRevision(value: unknown): boolean {
+ if (!isRecord(value)) return false
+
+ return (
+ typeof value.id === 'string' &&
+ isNullableString(value.basedOnRevisionId) &&
+ isNullableString(value.restartStepId) &&
+ isMember(value.status, WORKFLOW_REVISION_STATUSES) &&
+ Array.isArray(value.steps) &&
+ value.steps.length === WORKFLOW_STEP_ORDER.length &&
+ value.steps.every(
+ (step, index) =>
+ isWorkflowStep(step) && isRecord(step) && step.type === WORKFLOW_STEP_ORDER[index],
+ ) &&
+ isMember(value.generationStatus, GENERATION_STATUSES) &&
+ isMember(value.exportStatus, EXPORT_STATUSES) &&
+ typeof value.createdAt === 'string'
+ )
+}
+
+function isWorkflowRun(value: unknown): value is WorkflowRun {
+ if (!isRecord(value) || !Array.isArray(value.revisions)) return false
+
+ const fieldsAreValid =
+ typeof value.id === 'string' &&
+ typeof value.projectId === 'string' &&
+ isNullableString(value.characterId) &&
+ isNullableString(value.outfitId) &&
+ isMember(value.purpose, WORKFLOW_PURPOSES) &&
+ isMember(value.driver, WORKFLOW_DRIVERS) &&
+ isMember(value.status, WORKFLOW_RUN_STATUSES) &&
+ typeof value.currentRevisionId === 'string' &&
+ value.revisions.length === 1 &&
+ value.revisions.every(isWorkflowRevision) &&
+ value.revisions.some(
+ (revision) => isRecord(revision) && revision.id === value.currentRevisionId,
+ ) &&
+ isNullableString(value.prompt)
+ if (!fieldsAreValid) return false
+
+ const currentRevision = value.revisions.find(
+ (revision) => isRecord(revision) && revision.id === value.currentRevisionId,
+ )
+ if (!isRecord(currentRevision) || !Array.isArray(currentRevision.steps)) return false
+ if (currentRevision.basedOnRevisionId !== null || currentRevision.restartStepId !== null) {
+ return false
+ }
+
+ const expectedRevisionStatus =
+ value.status === 'failed' ? 'failed' : value.status === 'completed' ? 'completed' : 'active'
+ if (currentRevision.status !== expectedRevisionStatus) return false
+
+ const activeStepCount = currentRevision.steps.filter(
+ (step) => isRecord(step) && step.status === 'active',
+ ).length
+ if (
+ ((value.status === 'active' || value.status === 'interrupted') && activeStepCount !== 1) ||
+ ((value.status === 'failed' || value.status === 'completed') && activeStepCount !== 0)
+ ) {
+ return false
+ }
+
+ return value.revisions.every(
+ (revision) =>
+ isRecord(revision) &&
+ Array.isArray(revision.steps) &&
+ revision.steps.every((step) => {
+ if (!isRecord(step)) return false
+ const taskId = step.taskId
+ const submissionId = step.submissionId
+ if (taskId !== null && submissionId !== null) return false
+ if (taskId === null && submissionId === null) return true
+ return step.type === 'character-template' && step.status === 'active'
+ }),
+ )
+}
+
+function readPersistedRuns(storage: WorkflowRunStorage | null): WorkflowRun[] {
+ if (storage === null) return []
+
+ try {
+ const serialized = storage.getItem(WORKFLOW_RUN_STORAGE_KEY)
+ if (serialized === null) return []
+
+ const persisted: unknown = JSON.parse(serialized)
+ if (
+ !isRecord(persisted) ||
+ persisted.version !== WORKFLOW_RUN_STORAGE_VERSION ||
+ !Array.isArray(persisted.runs)
+ ) {
+ return []
+ }
+
+ return persisted.runs.filter(isWorkflowRun).map((run) => structuredClone(run))
+ } catch {
+ return []
+ }
+}
+
+function resolveBrowserStorage(): WorkflowRunStorage | null {
+ if (typeof window === 'undefined') return null
+
+ try {
+ return window.localStorage
+ } catch {
+ return null
+ }
+}
+
+/**
+ * WorkflowRun 的内存快照是当前会话的权威状态,localStorage 只负责刷新恢复。
+ * 因此 save 先更新内存;浏览器拒绝写入时,本次运行仍能继续读取和订阅。
+ */
+export function createWorkflowRunStore(
+ options: CreateWorkflowRunStoreOptions = {},
+): WorkflowRunStore {
+ const storage = options.storage === undefined ? resolveBrowserStorage() : options.storage
+ const runs = new Map(readPersistedRuns(storage).map((run) => [run.id, run] as const))
+ const listeners = new Map>()
+
+ return {
+ get(runId) {
+ const run = runs.get(runId)
+ return run === undefined ? null : structuredClone(run)
+ },
+
+ save(run) {
+ const savedRun = structuredClone(run)
+ runs.set(savedRun.id, savedRun)
+
+ const persisted: PersistedWorkflowRuns = {
+ version: WORKFLOW_RUN_STORAGE_VERSION,
+ runs: [...runs.values()],
+ }
+
+ try {
+ storage?.setItem(WORKFLOW_RUN_STORAGE_KEY, JSON.stringify(persisted))
+ } catch {
+ // 内存已成功更新;持久化失败不能中断当前会话中的工作流。
+ }
+
+ for (const listener of listeners.get(savedRun.id) ?? []) {
+ try {
+ listener(structuredClone(savedRun))
+ } catch {
+ // 订阅方渲染失败不能撤销已经保存的运行状态,也不能阻断其他订阅方。
+ }
+ }
+ },
+
+ subscribe(runId, listener) {
+ const runListeners = listeners.get(runId) ?? new Set()
+ runListeners.add(listener)
+ listeners.set(runId, runListeners)
+
+ return () => {
+ runListeners.delete(listener)
+ if (runListeners.size === 0) listeners.delete(runId)
+ }
+ },
+ }
+}
diff --git a/frontend/src/features/character-setup/index.test.ts b/frontend/src/features/character-setup/index.test.ts
new file mode 100644
index 0000000..60a05ec
--- /dev/null
+++ b/frontend/src/features/character-setup/index.test.ts
@@ -0,0 +1,10 @@
+import { expectTypeOf, it } from 'vitest'
+
+import type { CharacterSetupStepInput } from '@/entities'
+import type { CharacterSetupProps } from '.'
+
+it('submits WorkflowRun character setup input', () => {
+ expectTypeOf()
+ .parameter(0)
+ .toEqualTypeOf()
+})
diff --git a/frontend/src/features/character-setup/index.ts b/frontend/src/features/character-setup/index.ts
new file mode 100644
index 0000000..13827b1
--- /dev/null
+++ b/frontend/src/features/character-setup/index.ts
@@ -0,0 +1,7 @@
+import type { CharacterSetupStepInput } from '@/entities'
+
+/** 填写角色资料并提交母版生成。 */
+export interface CharacterSetupProps {
+ projectId: string
+ onSubmit(input: CharacterSetupStepInput): void
+}
diff --git a/frontend/src/features/export/index.ts b/frontend/src/features/export/index.ts
new file mode 100644
index 0000000..adfc5aa
--- /dev/null
+++ b/frontend/src/features/export/index.ts
@@ -0,0 +1,6 @@
+/** 把确认后的造型与动作导出到资产库。 */
+export interface ExportProps {
+ runId: string
+ characterId: string
+ outfitId: string
+}
diff --git a/frontend/src/features/generation/index.ts b/frontend/src/features/generation/index.ts
new file mode 100644
index 0000000..2015752
--- /dev/null
+++ b/frontend/src/features/generation/index.ts
@@ -0,0 +1,10 @@
+/** 触发并展示一次生成;不感知后端调用的是哪个模型。 */
+export interface GenerationProps {
+ runId: string
+ /**
+ * 目标动作,生成母版等非动作任务可以省略。
+ * 动作 ID 只在造型内唯一,所以造型由 runId 对应的 WorkflowRun.outfitId 决定,
+ * 不能脱离 run 单独使用这个字段。
+ */
+ actionId?: string
+}
diff --git a/frontend/src/features/review/index.ts b/frontend/src/features/review/index.ts
new file mode 100644
index 0000000..3a48b66
--- /dev/null
+++ b/frontend/src/features/review/index.ts
@@ -0,0 +1,12 @@
+/**
+ * 逐帧查看已生成的动作,供用户在导出前过一遍。
+ *
+ * 只看不改:服务端不返回质检结论,产品上也不设「打回此帧」,
+ * 所以这里没有任何写操作,帧数据不会因为查看而改变。
+ */
+export interface ReviewProps {
+ /** 动作 ID 只在造型内唯一,调用方须自行持有所属造型,不能拿它跨角色定位。 */
+ actionId: string
+ frameIndex: number
+ onSelectFrame(index: number): void
+}
diff --git a/frontend/src/features/workflow-controller/character-template-task.ts b/frontend/src/features/workflow-controller/character-template-task.ts
new file mode 100644
index 0000000..d0656b7
--- /dev/null
+++ b/frontend/src/features/workflow-controller/character-template-task.ts
@@ -0,0 +1,457 @@
+import {
+ parseCharacterTemplateGenerationResult,
+ type GenerationApis,
+ type Task,
+ type TaskApis,
+ type TaskEvent,
+ type WorkflowRevision,
+ type WorkflowRun,
+ type WorkflowRunStore,
+ type WorkflowStep,
+} from '@/entities'
+import {
+ getActiveStep,
+ getCurrentRevision,
+ replaceWorkflowStep,
+ type WorkflowStepTarget,
+} from './workflow-state'
+
+interface ApplyServerResultInput extends WorkflowStepTarget {
+ /** 结果必须仍属于步骤当前记录的任务;重试前的旧结果会被忽略。 */
+ taskId: string
+ result: unknown
+}
+
+interface ActiveSubscription {
+ runId: WorkflowRun['id']
+ stop: () => void
+}
+
+export interface CharacterTemplateTask {
+ /** 启动或继续目标角色图步骤;同一实例内的重复调用共享一次提交。 */
+ start(runId: WorkflowRun['id'], target: WorkflowStepTarget): Promise
+
+ /** 页面恢复时先读取任务终态;仍在运行时再恢复订阅。 */
+ resume(runId: WorkflowRun['id']): Promise
+
+ /** 停止指定运行记录的前端任务订阅,不改变 WorkflowRun 状态。 */
+ stop(runId: WorkflowRun['id']): void
+}
+
+interface CreateCharacterTemplateTaskOptions {
+ store: WorkflowRunStore
+ generationApis: Pick
+ taskApis: TaskApis
+ createSubmissionId: () => string
+}
+
+/**
+ * 角色图异步任务的生命周期。
+ *
+ * 它只处理当前角色图步骤与后端 Task 的关联,不决定整个工作流下一步走什么。
+ * submissions 与 subscriptions 属于实例锁;生产环境必须复用同一个实例。
+ */
+export function createCharacterTemplateTask({
+ store,
+ generationApis,
+ taskApis,
+ createSubmissionId,
+}: CreateCharacterTemplateTaskOptions): CharacterTemplateTask {
+ const submissions = new Map>()
+ const subscriptions = new Map()
+
+ function getWorkflow(runId: WorkflowRun['id']) {
+ return store.get(runId)
+ }
+
+ function requireWorkflow(runId: WorkflowRun['id']) {
+ const run = getWorkflow(runId)
+ if (!run) throw new Error(`WorkflowRun 不存在:${runId}`)
+ return run
+ }
+
+ function save(run: WorkflowRun) {
+ store.save(run)
+ return run
+ }
+
+ function start(runId: WorkflowRun['id'], target: WorkflowStepTarget): Promise {
+ const run = requireWorkflow(runId)
+ const revision = getCurrentRevision(run)
+ const step = revision.steps.find((item) => item.id === target.stepId)
+ if (
+ revision.id !== target.revisionId ||
+ !step ||
+ step.type !== 'character-template' ||
+ step.status !== 'active'
+ ) {
+ return Promise.resolve(run)
+ }
+ if (step.taskId) {
+ ensureTaskSubscription(run, target.revisionId, target.stepId, step.taskId)
+ return Promise.resolve(requireWorkflow(runId))
+ }
+ if (!step.input) throw new Error('角色图生成步骤缺少输入快照')
+ return submit(runId, target)
+ }
+
+ function submit(runId: WorkflowRun['id'], target: WorkflowStepTarget) {
+ const key = submissionKey(runId, target.revisionId, target.stepId)
+ const pending = submissions.get(key)
+ if (pending) return pending
+
+ const submission = performSubmission(runId, target).finally(() => {
+ submissions.delete(key)
+ })
+ submissions.set(key, submission)
+ return submission
+ }
+
+ async function performSubmission(
+ runId: WorkflowRun['id'],
+ target: WorkflowStepTarget,
+ ): Promise {
+ const before = requireWorkflow(runId)
+ const beforeRevision = getCurrentRevision(before)
+ const beforeStep = beforeRevision.steps.find((step) => step.id === target.stepId)
+ if (
+ before.status !== 'active' ||
+ beforeRevision.id !== target.revisionId ||
+ !beforeStep ||
+ beforeStep.type !== 'character-template' ||
+ beforeStep.status !== 'active' ||
+ !beforeStep.input
+ ) {
+ return before
+ }
+ if (beforeStep.taskId) {
+ ensureTaskSubscription(before, target.revisionId, target.stepId, beforeStep.taskId)
+ return before
+ }
+ if (beforeStep.submissionId) {
+ throw new Error('角色图生成请求仍在等待后端确认,不能重复提交')
+ }
+
+ const submissionId = createSubmissionId()
+ const submitting = replaceWorkflowStep(before, target.revisionId, target.stepId, (current) => {
+ if (current.type !== 'character-template') return current
+ return { ...current, submissionId }
+ })
+ save(submitting)
+
+ try {
+ const generation = await generationApis.create(beforeStep.input)
+ const latest = requireWorkflow(runId)
+ const latestRevision = getCurrentRevision(latest)
+ const latestStep = latestRevision.steps.find((step) => step.id === target.stepId)
+ if (
+ (latest.status !== 'active' && latest.status !== 'interrupted') ||
+ latestRevision.id !== target.revisionId ||
+ !latestStep ||
+ latestStep.type !== 'character-template' ||
+ latestStep.status !== 'active' ||
+ latestStep.taskId ||
+ latestStep.submissionId !== submissionId
+ ) {
+ return latest
+ }
+ if (generation.type !== 'character_template' || generation.projectId !== latest.projectId) {
+ throw new Error('生成任务返回的类型或项目与当前 WorkflowRun 不匹配')
+ }
+
+ const withTask = replaceWorkflowStep(latest, target.revisionId, target.stepId, (current) => {
+ if (current.type !== 'character-template') return current
+ return { ...current, taskId: generation.id, submissionId: null }
+ })
+ save(withTask)
+
+ if (latest.status === 'interrupted') return withTask
+ if (generation.status === 'failed') {
+ return markFailed(
+ runId,
+ target,
+ generation.id,
+ null,
+ generation.error?.trim() || '角色图生成任务失败',
+ )
+ }
+ if (generation.status === 'completed') {
+ return applyServerResult(runId, {
+ ...target,
+ taskId: generation.id,
+ result: generation.result,
+ })
+ }
+
+ ensureTaskSubscription(withTask, target.revisionId, target.stepId, generation.id)
+ return requireWorkflow(runId)
+ } catch (cause) {
+ markFailed(runId, target, null, submissionId, errorMessage(cause, '角色图生成请求失败'))
+ throw cause instanceof Error ? cause : new Error(String(cause))
+ }
+ }
+
+ function ensureTaskSubscription(
+ run: WorkflowRun,
+ revisionId: WorkflowRevision['id'],
+ stepId: WorkflowStep['id'],
+ taskId: string,
+ ) {
+ const key = subscriptionKey(run.id, revisionId, stepId, taskId)
+ if (subscriptions.has(key)) return
+
+ subscriptions.set(key, { runId: run.id, stop: () => undefined })
+ try {
+ const stop = taskApis.subscribe(run.projectId, taskId, (event) => {
+ handleTaskEvent(run.id, { revisionId, stepId }, taskId, event)
+ })
+ const active = subscriptions.get(key)
+ if (active) subscriptions.set(key, { ...active, stop })
+ else stop()
+ } catch (cause) {
+ subscriptions.delete(key)
+ throw cause
+ }
+ }
+
+ function handleTaskEvent(
+ runId: WorkflowRun['id'],
+ target: WorkflowStepTarget,
+ taskId: string,
+ event: TaskEvent,
+ ) {
+ if (event.taskId !== taskId) return
+ if (event.status === 'pending' || event.status === 'running') return
+ if (event.status === 'failed') {
+ markFailed(runId, target, taskId, null, event.error?.trim() || '角色图生成任务失败')
+ return
+ }
+ if (event.type !== 'character_template') {
+ markFailed(runId, target, taskId, null, '任务结果类型与角色图生成步骤不匹配')
+ return
+ }
+ applyServerResult(runId, {
+ ...target,
+ taskId,
+ result: event.result,
+ })
+ }
+
+ async function resume(runId: WorkflowRun['id']): Promise {
+ const run = getWorkflow(runId)
+ if (!run || run.status !== 'active') return run
+ const revision = getCurrentRevision(run)
+ const activeStep = getActiveStep(revision)
+ if (activeStep?.type !== 'character-template' || activeStep.status !== 'active') {
+ return run
+ }
+ const target = { revisionId: revision.id, stepId: activeStep.id }
+
+ if (activeStep.submissionId && !activeStep.taskId) {
+ if (submissions.has(submissionKey(run.id, revision.id, activeStep.id))) {
+ return run
+ }
+ return markFailed(
+ run.id,
+ target,
+ null,
+ activeStep.submissionId,
+ '页面刷新时生成请求尚未返回任务 ID,已停止恢复以避免重复提交',
+ )
+ }
+ if (activeStep.taskId) {
+ const task = await taskApis.get(run.projectId, activeStep.taskId)
+ const latest = getWorkflow(run.id)
+ if (!latest || latest.status !== 'active' || latest.currentRevisionId !== revision.id) {
+ return latest
+ }
+ const latestRevision = getCurrentRevision(latest)
+ const latestStep = latestRevision.steps.find((step) => step.id === activeStep.id)
+ if (
+ latestStep?.type !== 'character-template' ||
+ latestStep.status !== 'active' ||
+ latestStep.taskId !== activeStep.taskId
+ ) {
+ return latest
+ }
+ if (task.id !== latestStep.taskId) {
+ throw new Error('任务查询结果与 WorkflowRun 记录的 taskId 不匹配')
+ }
+ if (task.type !== 'character_template') {
+ return markFailed(
+ latest.id,
+ { revisionId: latestRevision.id, stepId: latestStep.id },
+ latestStep.taskId,
+ null,
+ '任务查询结果类型与角色图生成步骤不匹配',
+ )
+ }
+ if (task.status === 'pending' || task.status === 'running') {
+ ensureTaskSubscription(latest, latestRevision.id, latestStep.id, latestStep.taskId)
+ } else {
+ handleTaskEvent(
+ latest.id,
+ { revisionId: latestRevision.id, stepId: latestStep.id },
+ latestStep.taskId,
+ taskEvent(task),
+ )
+ }
+ }
+ return getWorkflow(runId)
+ }
+
+ function applyServerResult(runId: WorkflowRun['id'], input: ApplyServerResultInput): WorkflowRun {
+ const run = requireWorkflow(runId)
+ if (run.status !== 'active' || run.currentRevisionId !== input.revisionId) {
+ return run
+ }
+
+ const revision = getCurrentRevision(run)
+ const step = revision.steps.find((item) => item.id === input.stepId)
+ if (
+ !step ||
+ step.type !== 'character-template' ||
+ step.status !== 'active' ||
+ step.taskId !== input.taskId
+ ) {
+ return run
+ }
+
+ const result = parseCharacterTemplateGenerationResult(input.result)
+ if (!result) {
+ return markFailed(
+ runId,
+ { revisionId: revision.id, stepId: step.id },
+ input.taskId,
+ null,
+ '角色图生成任务返回了无法识别的结果',
+ )
+ }
+ const candidateStep = revision.steps.find((item) => item.type === 'template-candidate')
+ if (!candidateStep) throw new Error('WorkflowRun 缺少 template-candidate 步骤')
+
+ const updated: WorkflowRun = {
+ ...run,
+ revisions: run.revisions.map((item) => {
+ if (item.id !== revision.id) return item
+ return {
+ ...item,
+ steps: item.steps.map((current) => {
+ if (current.id === step.id && current.type === 'character-template') {
+ return {
+ ...current,
+ status: 'passed' as const,
+ output: result,
+ taskId: null,
+ submissionId: null,
+ }
+ }
+ if (current.id === candidateStep.id && current.type === 'template-candidate') {
+ return { ...current, status: 'active' as const }
+ }
+ return current
+ }),
+ }
+ }),
+ }
+ stopSubscription(subscriptionKey(run.id, revision.id, step.id, input.taskId))
+ return save(updated)
+ }
+
+ function markFailed(
+ runId: WorkflowRun['id'],
+ target: WorkflowStepTarget,
+ expectedTaskId: string | null,
+ expectedSubmissionId: string | null,
+ error: string,
+ ) {
+ const run = requireWorkflow(runId)
+ if (run.status !== 'active' || run.currentRevisionId !== target.revisionId) return run
+ const revision = getCurrentRevision(run)
+ const step = revision.steps.find((item) => item.id === target.stepId)
+ if (
+ !step ||
+ step.type !== 'character-template' ||
+ step.status !== 'active' ||
+ (expectedTaskId !== null && step.taskId !== expectedTaskId) ||
+ (expectedSubmissionId !== null && step.submissionId !== expectedSubmissionId)
+ ) {
+ return run
+ }
+
+ const failureMessage = error.trim() || '角色图生成失败'
+ const failed: WorkflowRun = {
+ ...replaceWorkflowStep(
+ run,
+ target.revisionId,
+ target.stepId,
+ (current) => ({
+ ...current,
+ status: 'failed',
+ taskId: null,
+ submissionId: null,
+ error: failureMessage,
+ }),
+ (current) => ({
+ ...current,
+ status: 'failed',
+ generationStatus: 'failed',
+ }),
+ ),
+ status: 'failed',
+ }
+ if (step.taskId) {
+ stopSubscription(subscriptionKey(run.id, revision.id, step.id, step.taskId))
+ }
+ return save(failed)
+ }
+
+ function stopSubscription(key: string) {
+ const subscription = subscriptions.get(key)
+ subscriptions.delete(key)
+ try {
+ subscription?.stop()
+ } catch {
+ // 取消轮询失败不能反向破坏已经落盘的 WorkflowRun 状态。
+ }
+ }
+
+ function stop(runId: WorkflowRun['id']) {
+ for (const [key, subscription] of subscriptions) {
+ if (subscription.runId === runId) stopSubscription(key)
+ }
+ }
+
+ return { start, resume, stop }
+}
+
+function taskEvent(task: Task): TaskEvent {
+ return {
+ taskId: task.id,
+ type: task.type,
+ status: task.status,
+ error: task.error,
+ result: task.result,
+ }
+}
+
+function errorMessage(cause: unknown, fallback: string) {
+ return cause instanceof Error && cause.message.trim() ? cause.message.trim() : fallback
+}
+
+function subscriptionKey(
+ runId: WorkflowRun['id'],
+ revisionId: WorkflowRevision['id'],
+ stepId: WorkflowStep['id'],
+ taskId: string,
+) {
+ return `${runId}:${revisionId}:${stepId}:${taskId}`
+}
+
+function submissionKey(
+ runId: WorkflowRun['id'],
+ revisionId: WorkflowRevision['id'],
+ stepId: WorkflowStep['id'],
+) {
+ return `${runId}:${revisionId}:${stepId}`
+}
diff --git a/frontend/src/features/workflow-controller/controller.test.ts b/frontend/src/features/workflow-controller/controller.test.ts
new file mode 100644
index 0000000..b2acd99
--- /dev/null
+++ b/frontend/src/features/workflow-controller/controller.test.ts
@@ -0,0 +1,607 @@
+import { describe, expect, it, vi } from 'vitest'
+
+import {
+ WORKFLOW_STEP_ORDER,
+ type Generation,
+ type GenerationApis,
+ type GenerationInput,
+ type Task,
+ type TaskApis,
+ type TaskEvent,
+ type WorkflowRevision,
+ type WorkflowRun,
+ type WorkflowStep,
+ type WorkflowStepType,
+} from '@/entities'
+import { createWorkflowController } from '.'
+
+const NOW = '2026-07-30T12:00:00.000Z'
+
+type RunListener = (run: WorkflowRun) => void
+
+function cloneRun(run: WorkflowRun): WorkflowRun {
+ return structuredClone(run)
+}
+
+function createMemoryStore() {
+ const runs = new Map()
+ const listeners = new Map>()
+
+ const get = vi.fn((runId: string): WorkflowRun | null => {
+ const run = runs.get(runId)
+ return run ? cloneRun(run) : null
+ })
+
+ const save = vi.fn((run: WorkflowRun): void => {
+ const snapshot = cloneRun(run)
+ runs.set(run.id, snapshot)
+
+ for (const listener of listeners.get(run.id) ?? []) {
+ listener(cloneRun(snapshot))
+ }
+ })
+
+ const subscribe = vi.fn((runId: string, listener: RunListener): (() => void) => {
+ const runListeners = listeners.get(runId) ?? new Set()
+ runListeners.add(listener)
+ listeners.set(runId, runListeners)
+
+ return () => {
+ runListeners.delete(listener)
+ }
+ })
+
+ return { get, save, subscribe }
+}
+
+function createIdFactory() {
+ let nextId = 0
+ return vi.fn(() => `id-${++nextId}`)
+}
+
+function deferNextGeneration(harness: ReturnType) {
+ let resolve!: (generation: Generation<'character_template'>) => void
+ const promise = new Promise>((resolvePromise) => {
+ resolve = resolvePromise
+ })
+ vi.mocked(harness.generationApis.create).mockImplementationOnce(
+ async () => (await promise) as Generation,
+ )
+ return { resolve }
+}
+
+function pendingCharacterTemplateGeneration(): Generation<'character_template'> {
+ return {
+ id: 'task-1',
+ projectId: 'project-1',
+ type: 'character_template',
+ status: 'pending',
+ result: null,
+ error: null,
+ }
+}
+
+function createHarness() {
+ const store = createMemoryStore()
+ const taskListeners = new Map void>()
+
+ const createGeneration: GenerationApis['create'] = async (input: T) =>
+ ({
+ id: 'task-1',
+ projectId: input.projectId,
+ type: input.type,
+ status: 'pending',
+ result: null,
+ error: null,
+ }) as Generation
+
+ const generationApis: Pick = {
+ create: vi.fn(createGeneration),
+ }
+
+ const subscribeTask = vi.fn(
+ (projectId: string, taskId: string, onEvent: (event: TaskEvent) => void) => {
+ taskListeners.set(`${projectId}:${taskId}`, onEvent)
+ onEvent({
+ taskId,
+ type: 'character_template',
+ status: 'pending',
+ error: null,
+ result: null,
+ })
+ return () => {
+ taskListeners.delete(`${projectId}:${taskId}`)
+ }
+ },
+ )
+
+ const taskApis: TaskApis = {
+ get: vi.fn(async () => {
+ throw new Error('TaskApis.get is not used until a run is resumed')
+ }),
+ subscribe: subscribeTask,
+ }
+
+ const controller = createWorkflowController({
+ store,
+ generationApis,
+ taskApis,
+ createId: createIdFactory(),
+ now: () => NOW,
+ })
+
+ return {
+ controller,
+ generationApis,
+ subscribeTask,
+ store,
+ taskApis,
+ getTaskListener(projectId: string, taskId: string) {
+ return taskListeners.get(`${projectId}:${taskId}`) ?? null
+ },
+ emitTask(projectId: string, taskId: string, event: TaskEvent) {
+ const listener = taskListeners.get(`${projectId}:${taskId}`)
+ expect(listener, `missing task subscription for ${projectId}:${taskId}`).toBeTypeOf(
+ 'function',
+ )
+ listener?.(event)
+ },
+ }
+}
+
+function currentRevision(run: WorkflowRun): WorkflowRevision {
+ const revision = run.revisions.find(({ id }) => id === run.currentRevisionId)
+ if (!revision) {
+ throw new Error(`Current revision ${run.currentRevisionId} is missing`)
+ }
+ return revision
+}
+
+function step(run: WorkflowRun, type: WorkflowStepType): WorkflowStep {
+ const workflowStep = currentRevision(run).steps.find((item) => item.type === type)
+ if (!workflowStep) {
+ throw new Error(`Workflow step ${type} is missing`)
+ }
+ return workflowStep
+}
+
+async function createAiRun(harness: ReturnType) {
+ return harness.controller.create({
+ projectId: 'project-1',
+ purpose: 'create_character',
+ driver: 'ai',
+ prompt: ' pixel knight ',
+ })
+}
+
+async function startCharacterTemplate(harness: ReturnType) {
+ const run = await createAiRun(harness)
+ await harness.controller.nextStep(run.id)
+ return run
+}
+
+describe('createWorkflowController', () => {
+ it('creates one revision with the fixed eight steps and seeds AI input from the prompt', async () => {
+ const harness = createHarness()
+
+ expect(harness.controller).toEqual(
+ expect.objectContaining({
+ create: expect.any(Function),
+ getWorkflow: expect.any(Function),
+ subscribe: expect.any(Function),
+ updateCharacterSetup: expect.any(Function),
+ nextStep: expect.any(Function),
+ resume: expect.any(Function),
+ interrupt: expect.any(Function),
+ }),
+ )
+
+ const run = await createAiRun(harness)
+ const revision = currentRevision(run)
+
+ expect(run.prompt).toBe('pixel knight')
+ expect(run.projectId).toBe('project-1')
+ expect(run.revisions).toHaveLength(1)
+ expect(run.currentRevisionId).toBe(revision.id)
+ expect(revision.basedOnRevisionId).toBeNull()
+ expect(revision.restartStepId).toBeNull()
+ expect(revision.createdAt).toBe(NOW)
+ expect(revision.steps.map(({ type }) => type)).toEqual(WORKFLOW_STEP_ORDER)
+ expect(revision.steps.map(({ status }) => status)).toEqual([
+ 'active',
+ 'locked',
+ 'locked',
+ 'locked',
+ 'locked',
+ 'locked',
+ 'locked',
+ 'locked',
+ ])
+ expect(step(run, 'character-setup').input).toEqual({
+ description: 'pixel knight',
+ referenceMedia: [],
+ })
+
+ const allIds = [run.id, revision.id, ...revision.steps.map(({ id }) => id)]
+ expect(new Set(allIds).size).toBe(allIds.length)
+ expect(harness.store.save).toHaveBeenCalledWith(run)
+ })
+
+ it('persists the task id before subscribing and never submits the active generation twice', async () => {
+ const harness = createHarness()
+
+ await startCharacterTemplate(harness)
+
+ expect(harness.generationApis.create).toHaveBeenCalledTimes(1)
+ expect(harness.generationApis.create).toHaveBeenCalledWith({
+ type: 'character_template',
+ projectId: 'project-1',
+ prompt: 'pixel knight',
+ referenceMedia: [],
+ })
+ expect(harness.subscribeTask).toHaveBeenCalledWith('project-1', 'task-1', expect.any(Function))
+
+ const taskSaveIndex = harness.store.save.mock.calls.findIndex(([savedRun]) => {
+ return step(savedRun, 'character-template').taskId === 'task-1'
+ })
+ expect(taskSaveIndex).toBeGreaterThanOrEqual(0)
+ expect(harness.store.save.mock.invocationCallOrder[taskSaveIndex]).toBeLessThan(
+ harness.subscribeTask.mock.invocationCallOrder[0],
+ )
+
+ const createdRun = harness.store.save.mock.calls[0]?.[0]
+ if (!createdRun) throw new Error('Expected the created WorkflowRun to be saved')
+ const activeRun = harness.controller.getWorkflow(createdRun.id)
+ if (!activeRun) throw new Error('Expected the WorkflowRun to remain available')
+ expect(step(activeRun, 'character-setup').status).toBe('passed')
+ expect(step(activeRun, 'character-template')).toMatchObject({
+ status: 'active',
+ taskId: 'task-1',
+ })
+
+ await harness.controller.nextStep(activeRun.id)
+ expect(harness.generationApis.create).toHaveBeenCalledTimes(1)
+ })
+
+ it('shares one submission when nextStep is called concurrently', async () => {
+ const harness = createHarness()
+ const run = await createAiRun(harness)
+ const deferred = deferNextGeneration(harness)
+
+ const first = harness.controller.nextStep(run.id)
+ const second = harness.controller.nextStep(run.id)
+
+ expect(harness.generationApis.create).toHaveBeenCalledTimes(1)
+ deferred.resolve(pendingCharacterTemplateGeneration())
+ await Promise.all([first, second])
+
+ expect(harness.generationApis.create).toHaveBeenCalledTimes(1)
+ expect(step(harness.controller.getWorkflow(run.id)!, 'character-template').taskId).toBe(
+ 'task-1',
+ )
+ })
+
+ it('keeps an active submission alive when resume uses the same controller', async () => {
+ const harness = createHarness()
+ const run = await createAiRun(harness)
+ const deferred = deferNextGeneration(harness)
+
+ const submission = harness.controller.nextStep(run.id)
+ const resumed = await harness.controller.resume(run.id)
+
+ expect(resumed?.status).toBe('active')
+ expect(step(resumed!, 'character-template')).toMatchObject({
+ status: 'active',
+ taskId: null,
+ submissionId: expect.any(String),
+ })
+
+ deferred.resolve(pendingCharacterTemplateGeneration())
+ await submission
+
+ expect(harness.generationApis.create).toHaveBeenCalledTimes(1)
+ expect(step(harness.controller.getWorkflow(run.id)!, 'character-template').taskId).toBe(
+ 'task-1',
+ )
+ })
+
+ it('handles a terminal snapshot emitted synchronously when subscribing', async () => {
+ const harness = createHarness()
+ const stop = vi.fn()
+ harness.subscribeTask.mockImplementationOnce((_projectId, _taskId, onEvent) => {
+ onEvent({
+ taskId: 'task-1',
+ type: 'character_template',
+ status: 'completed',
+ error: null,
+ result: {
+ type: 'character_template',
+ images: [{ url: 'https://example.com/synchronous.png' }],
+ },
+ })
+ return stop
+ })
+
+ const run = await createAiRun(harness)
+ await harness.controller.nextStep(run.id)
+
+ expect(step(harness.controller.getWorkflow(run.id)!, 'character-template')).toMatchObject({
+ status: 'passed',
+ taskId: null,
+ })
+ expect(step(harness.controller.getWorkflow(run.id)!, 'template-candidate').status).toBe(
+ 'active',
+ )
+ expect(stop).toHaveBeenCalledOnce()
+ })
+
+ it('ignores another task result and advances only when the matching task completes', async () => {
+ const harness = createHarness()
+
+ const run = await startCharacterTemplate(harness)
+
+ harness.emitTask('project-1', 'task-1', {
+ taskId: 'another-task',
+ type: 'character_template',
+ status: 'completed',
+ error: null,
+ result: {
+ type: 'character_template',
+ images: [{ url: 'https://example.com/wrong.png' }],
+ },
+ })
+
+ const unchangedRun = harness.controller.getWorkflow(run.id)
+ if (!unchangedRun) throw new Error('Expected the WorkflowRun to remain available')
+ expect(step(unchangedRun, 'character-template')).toMatchObject({
+ status: 'active',
+ output: null,
+ taskId: 'task-1',
+ })
+ expect(step(unchangedRun, 'template-candidate').status).toBe('locked')
+
+ const result = {
+ type: 'character_template' as const,
+ images: [{ url: 'https://example.com/knight.png' }],
+ }
+ harness.emitTask('project-1', 'task-1', {
+ taskId: 'task-1',
+ type: 'character_template',
+ status: 'completed',
+ error: null,
+ result,
+ })
+
+ const completedRun = harness.controller.getWorkflow(run.id)
+ if (!completedRun) throw new Error('Expected the WorkflowRun to remain available')
+ expect(step(completedRun, 'character-template')).toMatchObject({
+ status: 'passed',
+ output: result,
+ taskId: null,
+ })
+ expect(step(completedRun, 'template-candidate').status).toBe('active')
+ })
+
+ it('marks the step, revision, generation, and run as failed when the task fails', async () => {
+ const harness = createHarness()
+
+ const run = await startCharacterTemplate(harness)
+
+ harness.emitTask('project-1', 'task-1', {
+ taskId: 'task-1',
+ type: 'character_template',
+ status: 'failed',
+ error: 'model unavailable',
+ result: null,
+ })
+
+ const failedRun = harness.controller.getWorkflow(run.id)
+ if (!failedRun) throw new Error('Expected the WorkflowRun to remain available')
+ const revision = currentRevision(failedRun)
+ expect(step(failedRun, 'character-template')).toMatchObject({
+ status: 'failed',
+ taskId: null,
+ submissionId: null,
+ error: 'model unavailable',
+ })
+ expect(revision.status).toBe('failed')
+ expect(revision.generationStatus).toBe('failed')
+ expect(failedRun.status).toBe('failed')
+ })
+
+ it('resumes a persisted in-flight task without creating another generation', async () => {
+ const harness = createHarness()
+ const run = await startCharacterTemplate(harness)
+ vi.mocked(harness.taskApis.get).mockResolvedValueOnce({
+ id: 'task-1',
+ type: 'character_template',
+ status: 'running',
+ error: null,
+ result: null,
+ })
+ const resumeSubscribe = vi.fn(
+ (_projectId: string, _taskId: string, _onEvent: (event: TaskEvent) => void) => () =>
+ undefined,
+ )
+ const resumedController = createWorkflowController({
+ store: harness.store,
+ generationApis: harness.generationApis,
+ taskApis: {
+ get: harness.taskApis.get,
+ subscribe: resumeSubscribe,
+ },
+ })
+
+ const resumed = await resumedController.resume(run.id)
+
+ expect(resumed?.id).toBe(run.id)
+ expect(harness.taskApis.get).toHaveBeenCalledWith('project-1', 'task-1')
+ expect(resumeSubscribe).toHaveBeenCalledWith('project-1', 'task-1', expect.any(Function))
+ expect(harness.generationApis.create).toHaveBeenCalledTimes(1)
+ })
+
+ it('applies a completed task found during refresh before subscribing again', async () => {
+ const harness = createHarness()
+ const run = await startCharacterTemplate(harness)
+ vi.mocked(harness.taskApis.get).mockResolvedValueOnce({
+ id: 'task-1',
+ type: 'character_template',
+ status: 'completed',
+ error: null,
+ result: {
+ type: 'character_template',
+ images: [{ url: 'https://example.com/recovered.png' }],
+ },
+ })
+ const resumeSubscribe = vi.fn(
+ (_projectId: string, _taskId: string, _onEvent: (event: TaskEvent) => void) => () =>
+ undefined,
+ )
+ const resumedController = createWorkflowController({
+ store: harness.store,
+ generationApis: harness.generationApis,
+ taskApis: {
+ get: harness.taskApis.get,
+ subscribe: resumeSubscribe,
+ },
+ })
+
+ const resumed = await resumedController.resume(run.id)
+
+ expect(step(resumed!, 'character-template')).toMatchObject({
+ status: 'passed',
+ taskId: null,
+ })
+ expect(step(resumed!, 'template-candidate').status).toBe('active')
+ expect(resumeSubscribe).not.toHaveBeenCalled()
+ })
+
+ it('does not subscribe after interrupting while resume waits for the task query', async () => {
+ const harness = createHarness()
+ const run = await startCharacterTemplate(harness)
+ let resolveTask!: (task: Task) => void
+ const pendingTask = new Promise((resolve) => {
+ resolveTask = resolve
+ })
+ const resumeSubscribe = vi.fn(
+ (_projectId: string, _taskId: string, _onEvent: (event: TaskEvent) => void) => () =>
+ undefined,
+ )
+ const resumedController = createWorkflowController({
+ store: harness.store,
+ generationApis: harness.generationApis,
+ taskApis: {
+ get: vi.fn(() => pendingTask),
+ subscribe: resumeSubscribe,
+ },
+ })
+
+ const resuming = resumedController.resume(run.id)
+ resumedController.interrupt(run.id)
+ resolveTask({
+ id: 'task-1',
+ type: 'character_template',
+ status: 'running',
+ error: null,
+ result: null,
+ })
+ const resumed = await resuming
+
+ expect(resumed?.status).toBe('interrupted')
+ expect(resumeSubscribe).not.toHaveBeenCalled()
+ })
+
+ it('fails safely after refresh when the request was sent before taskId arrived', async () => {
+ const harness = createHarness()
+ const run = await startCharacterTemplate(harness)
+ const uncertainSnapshot = harness.store.save.mock.calls
+ .map(([savedRun]) => savedRun)
+ .find((savedRun) => {
+ const template = step(savedRun, 'character-template')
+ return template.submissionId !== null && template.taskId === null
+ })
+ if (!uncertainSnapshot) throw new Error('expected the submitting snapshot to be persisted')
+ harness.store.save(uncertainSnapshot)
+ vi.mocked(harness.generationApis.create).mockClear()
+
+ const restoredController = createWorkflowController({
+ store: harness.store,
+ generationApis: harness.generationApis,
+ taskApis: harness.taskApis,
+ })
+
+ const restored = await restoredController.resume(run.id)
+
+ expect(restored?.status).toBe('failed')
+ expect(step(restored!, 'character-template')).toMatchObject({
+ status: 'failed',
+ taskId: null,
+ submissionId: null,
+ error: '页面刷新时生成请求尚未返回任务 ID,已停止恢复以避免重复提交',
+ })
+ expect(harness.generationApis.create).not.toHaveBeenCalled()
+ })
+
+ it('records a task id that returns after the run was interrupted', async () => {
+ const harness = createHarness()
+ const run = await createAiRun(harness)
+ const deferred = deferNextGeneration(harness)
+
+ const submission = harness.controller.nextStep(run.id)
+ await harness.controller.interrupt(run.id)
+ deferred.resolve(pendingCharacterTemplateGeneration())
+ await submission
+
+ const interrupted = harness.controller.getWorkflow(run.id)
+ expect(interrupted?.status).toBe('interrupted')
+ expect(step(interrupted!, 'character-template')).toMatchObject({
+ status: 'active',
+ taskId: 'task-1',
+ submissionId: null,
+ })
+ expect(harness.subscribeTask).not.toHaveBeenCalled()
+ })
+
+ it('keeps an interrupted run interrupted when a queued failure arrives late', async () => {
+ const harness = createHarness()
+ const run = await startCharacterTemplate(harness)
+ const queuedListener = harness.getTaskListener('project-1', 'task-1')
+ if (!queuedListener) throw new Error('expected an active task subscription')
+
+ await harness.controller.interrupt(run.id)
+ queuedListener({
+ taskId: 'task-1',
+ type: 'character_template',
+ status: 'failed',
+ error: 'late failure',
+ result: null,
+ })
+
+ expect(harness.controller.getWorkflow(run.id)?.status).toBe('interrupted')
+ })
+
+ it('publishes saved updates and can interrupt the current run', async () => {
+ const harness = createHarness()
+ const run = await createAiRun(harness)
+ const listener = vi.fn()
+ const unsubscribe = harness.controller.subscribe(run.id, listener)
+
+ const updated = await harness.controller.updateCharacterSetup(run.id, {
+ description: 'revised knight',
+ referenceMedia: [],
+ })
+ expect(step(updated, 'character-setup').input).toEqual({
+ description: 'revised knight',
+ referenceMedia: [],
+ })
+
+ const interrupted = await harness.controller.interrupt(run.id)
+
+ expect(interrupted.status).toBe('interrupted')
+ expect(listener).toHaveBeenLastCalledWith(
+ expect.objectContaining({ id: run.id, status: 'interrupted' }),
+ )
+
+ unsubscribe()
+ })
+})
diff --git a/frontend/src/features/workflow-controller/controller.ts b/frontend/src/features/workflow-controller/controller.ts
new file mode 100644
index 0000000..58194dc
--- /dev/null
+++ b/frontend/src/features/workflow-controller/controller.ts
@@ -0,0 +1,168 @@
+import type {
+ CharacterSetupStepInput,
+ GenerationApis,
+ TaskApis,
+ WorkflowRun,
+ WorkflowRunStore,
+} from '@/entities'
+import { createCharacterTemplateTask } from './character-template-task'
+import {
+ advanceCharacterSetupState,
+ createWorkflowRunState,
+ getActiveStep,
+ getCurrentRevision,
+ interruptWorkflowRunState,
+ requireActiveWorkflow,
+ updateCharacterSetupState,
+ type CreateWorkflowRunStateInput,
+} from './workflow-state'
+
+/** 首个纵切只开放创建角色;增加动作进入对应步骤实现时再加入 Controller。 */
+export type CreateWorkflowControllerInput = CreateWorkflowRunStateInput
+
+export interface WorkflowController {
+ /** 创建并保存一条纯前端运行记录。 */
+ create(input: CreateWorkflowControllerInput): WorkflowRun
+
+ /** 按路由中的 runId 读取快照;不存在时返回 null。 */
+ getWorkflow(runId: WorkflowRun['id']): WorkflowRun | null
+
+ /** 订阅指定运行记录的本地变化。 */
+ subscribe(runId: WorkflowRun['id'], listener: (run: WorkflowRun) => void): () => void
+
+ /** 修改当前角色资料步骤,页面无需知道步骤内部 ID。 */
+ updateCharacterSetup(runId: WorkflowRun['id'], input: CharacterSetupStepInput): WorkflowRun
+
+ /**
+ * 推进一个步骤。当前纵切只实现角色资料到角色图生成;
+ * 后续步骤进入各自实现 PR 后再扩展,不在这里伪造完成。
+ */
+ nextStep(runId: WorkflowRun['id']): Promise
+
+ /** 页面恢复时先读取任务终态;仍在运行时再恢复订阅。 */
+ resume(runId: WorkflowRun['id']): Promise
+
+ /** 只停止前端自动推进和任务订阅;后端当前没有取消任务能力。 */
+ interrupt(runId: WorkflowRun['id']): WorkflowRun
+}
+
+export interface CreateWorkflowControllerOptions {
+ store: WorkflowRunStore
+ generationApis: Pick
+ taskApis: TaskApis
+ /** 测试可注入确定性 ID;生产默认使用浏览器随机 UUID。 */
+ createId?: (scope: 'run' | 'revision' | 'submission') => string
+ /** 测试可注入确定性时间。 */
+ now?: () => string
+}
+
+/**
+ * Quick Start 与手动工作流共用的流程协调器。
+ *
+ * Controller 只负责读取当前步骤、保存状态并委派角色图任务;纯状态转换和异步任务
+ * 生命周期分别留在本 Feature 的内部模块。生产接入必须复用同一个 Controller 实例,
+ * 不能在组件渲染期间重复创建。
+ */
+export function createWorkflowController({
+ store,
+ generationApis,
+ taskApis,
+ createId = createRuntimeId,
+ now = () => new Date().toISOString(),
+}: CreateWorkflowControllerOptions): WorkflowController {
+ const characterTemplateTask = createCharacterTemplateTask({
+ store,
+ generationApis,
+ taskApis,
+ createSubmissionId: () => createId('submission'),
+ })
+
+ function getWorkflow(runId: WorkflowRun['id']) {
+ return store.get(runId)
+ }
+
+ function requireWorkflow(runId: WorkflowRun['id']) {
+ const run = getWorkflow(runId)
+ if (!run) throw new Error(`WorkflowRun 不存在:${runId}`)
+ return run
+ }
+
+ function save(run: WorkflowRun) {
+ store.save(run)
+ return run
+ }
+
+ function create(input: CreateWorkflowControllerInput): WorkflowRun {
+ return save(
+ createWorkflowRunState(input, {
+ runId: createId('run'),
+ revisionId: createId('revision'),
+ createdAt: now(),
+ }),
+ )
+ }
+
+ function subscribe(runId: WorkflowRun['id'], listener: (run: WorkflowRun) => void) {
+ return store.subscribe(runId, listener)
+ }
+
+ function updateCharacterSetup(
+ runId: WorkflowRun['id'],
+ input: CharacterSetupStepInput,
+ ): WorkflowRun {
+ return save(updateCharacterSetupState(requireWorkflow(runId), input))
+ }
+
+ async function nextStep(runId: WorkflowRun['id']): Promise {
+ const run = requireActiveWorkflow(requireWorkflow(runId))
+ const revision = getCurrentRevision(run)
+ const activeStep = getActiveStep(revision)
+ if (!activeStep) throw new Error('当前 WorkflowRun 没有 active 步骤')
+
+ if (activeStep.type === 'character-template') {
+ return characterTemplateTask.start(runId, {
+ revisionId: revision.id,
+ stepId: activeStep.id,
+ })
+ }
+ if (activeStep.type !== 'character-setup') {
+ throw new Error(`步骤 ${activeStep.type} 尚未进入本轮实现`)
+ }
+
+ const transitioned = advanceCharacterSetupState(run)
+ save(transitioned.run)
+ return characterTemplateTask.start(runId, transitioned.target)
+ }
+
+ function resume(runId: WorkflowRun['id']) {
+ return characterTemplateTask.resume(runId)
+ }
+
+ function interrupt(runId: WorkflowRun['id']): WorkflowRun {
+ const run = requireWorkflow(runId)
+ if (run.status !== 'active') return run
+
+ characterTemplateTask.stop(runId)
+ const latest = requireWorkflow(runId)
+ if (latest.status !== 'active') return latest
+ return save(interruptWorkflowRunState(latest))
+ }
+
+ return {
+ create,
+ getWorkflow,
+ subscribe,
+ updateCharacterSetup,
+ nextStep,
+ resume,
+ interrupt,
+ }
+}
+
+function createRuntimeId(scope: 'run' | 'revision' | 'submission') {
+ const suffix =
+ typeof globalThis.crypto?.randomUUID === 'function'
+ ? globalThis.crypto.randomUUID()
+ : `${Date.now()}-${Math.random().toString(36).slice(2)}`
+ return `${scope}-${suffix}`
+}
diff --git a/frontend/src/features/workflow-controller/index.ts b/frontend/src/features/workflow-controller/index.ts
new file mode 100644
index 0000000..fcb6978
--- /dev/null
+++ b/frontend/src/features/workflow-controller/index.ts
@@ -0,0 +1,6 @@
+export { createWorkflowController } from './controller'
+export type {
+ CreateWorkflowControllerInput,
+ CreateWorkflowControllerOptions,
+ WorkflowController,
+} from './controller'
diff --git a/frontend/src/features/workflow-controller/workflow-run.integration.test.ts b/frontend/src/features/workflow-controller/workflow-run.integration.test.ts
new file mode 100644
index 0000000..ca4cc9a
--- /dev/null
+++ b/frontend/src/features/workflow-controller/workflow-run.integration.test.ts
@@ -0,0 +1,106 @@
+import { describe, expect, it, vi } from 'vitest'
+
+import {
+ createWorkflowRunStore,
+ type Generation,
+ type GenerationApis,
+ type GenerationInput,
+ type TaskApis,
+ type TaskEvent,
+} from '@/entities'
+import { createWorkflowController } from '.'
+
+describe('WorkflowRun first vertical slice', () => {
+ it('runs character setup through a completed character-template task', async () => {
+ const store = createWorkflowRunStore({ storage: null })
+ const taskChannel: { listener?: (event: TaskEvent) => void } = {}
+
+ const createGeneration: GenerationApis['create'] = async (
+ input: T,
+ ) =>
+ ({
+ id: 'task-character-template-1',
+ projectId: input.projectId,
+ type: input.type,
+ status: 'pending',
+ result: null,
+ error: null,
+ }) as Generation
+
+ const generationApis: Pick = {
+ create: vi.fn(createGeneration),
+ }
+ const taskApis: TaskApis = {
+ get: vi.fn(async () => {
+ throw new Error('not used in this slice')
+ }),
+ subscribe: vi.fn((_projectId, taskId, onEvent) => {
+ taskChannel.listener = onEvent
+ onEvent({
+ taskId,
+ type: 'character_template',
+ status: 'pending',
+ error: null,
+ result: null,
+ })
+ return () => {
+ delete taskChannel.listener
+ }
+ }),
+ }
+ const ids = ['run-1', 'revision-1']
+ const controller = createWorkflowController({
+ store,
+ generationApis,
+ taskApis,
+ createId: () => ids.shift() ?? 'unexpected-id',
+ now: () => '2026-07-30T12:00:00.000Z',
+ })
+
+ const created = await controller.create({
+ projectId: 'project-1',
+ purpose: 'create_character',
+ driver: 'ai',
+ prompt: '像素骑士',
+ })
+
+ await controller.nextStep(created.id)
+
+ const inFlight = store.get(created.id)
+ expect(
+ inFlight?.revisions[0].steps.find((step) => step.type === 'character-template'),
+ ).toMatchObject({
+ status: 'active',
+ taskId: 'task-character-template-1',
+ })
+
+ const taskListener = taskChannel.listener
+ if (!taskListener) throw new Error('expected the task subscription to be active')
+ taskListener({
+ taskId: 'task-character-template-1',
+ type: 'character_template',
+ status: 'completed',
+ error: null,
+ result: {
+ type: 'character_template',
+ images: [{ url: 'https://example.com/knight.png' }],
+ },
+ })
+ await Promise.resolve()
+
+ const completed = store.get(created.id)
+ expect(
+ completed?.revisions[0].steps.find((step) => step.type === 'character-template'),
+ ).toMatchObject({
+ status: 'passed',
+ taskId: null,
+ output: {
+ type: 'character_template',
+ images: [{ url: 'https://example.com/knight.png' }],
+ },
+ })
+ expect(
+ completed?.revisions[0].steps.find((step) => step.type === 'template-candidate'),
+ ).toMatchObject({ status: 'active' })
+ })
+})
diff --git a/frontend/src/features/workflow-controller/workflow-state.test.ts b/frontend/src/features/workflow-controller/workflow-state.test.ts
new file mode 100644
index 0000000..fc68371
--- /dev/null
+++ b/frontend/src/features/workflow-controller/workflow-state.test.ts
@@ -0,0 +1,94 @@
+import { describe, expect, it } from 'vitest'
+
+import {
+ advanceCharacterSetupState,
+ createWorkflowRunState,
+ updateCharacterSetupState,
+} from './workflow-state'
+
+const CREATED_AT = '2026-07-31T02:40:00.000Z'
+
+function createRun() {
+ return createWorkflowRunState(
+ {
+ projectId: 'project-1',
+ purpose: 'create_character',
+ driver: 'ai',
+ prompt: ' pixel knight ',
+ },
+ {
+ runId: 'run-1',
+ revisionId: 'revision-1',
+ createdAt: CREATED_AT,
+ },
+ )
+}
+
+describe('workflow state transitions', () => {
+ it('creates the fixed workflow with normalized Quick Start input', () => {
+ const run = createRun()
+
+ expect(run).toMatchObject({
+ id: 'run-1',
+ projectId: 'project-1',
+ status: 'active',
+ prompt: 'pixel knight',
+ currentRevisionId: 'revision-1',
+ })
+ expect(run.revisions[0]?.createdAt).toBe(CREATED_AT)
+ expect(run.revisions[0]?.steps.map(({ type, status }) => ({ type, status }))).toEqual([
+ { type: 'character-setup', status: 'active' },
+ { type: 'character-template', status: 'locked' },
+ { type: 'template-candidate', status: 'locked' },
+ { type: 'action-setup', status: 'locked' },
+ { type: 'first-frame', status: 'locked' },
+ { type: 'complete-animation', status: 'locked' },
+ { type: 'review', status: 'locked' },
+ { type: 'export', status: 'locked' },
+ ])
+ expect(run.revisions[0]?.steps[0]?.input).toEqual({
+ description: 'pixel knight',
+ referenceMedia: [],
+ })
+ })
+
+ it('normalizes character setup input before storing it', () => {
+ const updated = updateCharacterSetupState(createRun(), {
+ description: ' revised knight ',
+ referenceMedia: [],
+ })
+
+ expect(updated.revisions[0]?.steps[0]?.input).toEqual({
+ description: 'revised knight',
+ referenceMedia: [],
+ })
+ })
+
+ it('activates character-template with its generation input snapshot', () => {
+ const run = updateCharacterSetupState(createRun(), {
+ description: 'revised knight',
+ referenceMedia: [],
+ })
+
+ const transitioned = advanceCharacterSetupState(run)
+
+ expect(transitioned.target).toEqual({
+ revisionId: 'revision-1',
+ stepId: 'revision-1:character-template',
+ })
+ expect(transitioned.run.revisions[0]?.steps.slice(0, 3)).toMatchObject([
+ { type: 'character-setup', status: 'passed' },
+ {
+ type: 'character-template',
+ status: 'active',
+ input: {
+ type: 'character_template',
+ projectId: 'project-1',
+ prompt: 'revised knight',
+ referenceMedia: [],
+ },
+ },
+ { type: 'template-candidate', status: 'locked' },
+ ])
+ })
+})
diff --git a/frontend/src/features/workflow-controller/workflow-state.ts b/frontend/src/features/workflow-controller/workflow-state.ts
new file mode 100644
index 0000000..47c608a
--- /dev/null
+++ b/frontend/src/features/workflow-controller/workflow-state.ts
@@ -0,0 +1,217 @@
+import {
+ WORKFLOW_STEP_ORDER,
+ type CharacterSetupStepInput,
+ type CharacterTemplateGenerationInput,
+ type CreateWorkflowRunInput,
+ type WorkflowRevision,
+ type WorkflowRun,
+ type WorkflowStep,
+ type WorkflowStepStatus,
+ type WorkflowStepType,
+} from '@/entities'
+
+export type CreateWorkflowRunStateInput = Extract<
+ CreateWorkflowRunInput,
+ { purpose: 'create_character' }
+>
+
+export interface CreateWorkflowRunStateOptions {
+ runId: WorkflowRun['id']
+ revisionId: WorkflowRevision['id']
+ createdAt: string
+}
+
+export interface WorkflowStepTarget {
+ revisionId: WorkflowRevision['id']
+ stepId: WorkflowStep['id']
+}
+
+export function createWorkflowRunState(
+ input: CreateWorkflowRunStateInput,
+ { runId, revisionId, createdAt }: CreateWorkflowRunStateOptions,
+): WorkflowRun {
+ const prompt = input.prompt?.trim() || null
+
+ return {
+ id: runId,
+ projectId: input.projectId,
+ characterId: null,
+ outfitId: null,
+ purpose: input.purpose,
+ driver: input.driver,
+ status: 'active',
+ currentRevisionId: revisionId,
+ revisions: [
+ {
+ id: revisionId,
+ basedOnRevisionId: null,
+ restartStepId: null,
+ status: 'active',
+ steps: WORKFLOW_STEP_ORDER.map((type, index) =>
+ createInitialStep(type, revisionId, index, prompt),
+ ),
+ generationStatus: 'not_started',
+ exportStatus: 'not_exported',
+ createdAt,
+ },
+ ],
+ prompt,
+ }
+}
+
+export function getCurrentRevision(run: WorkflowRun): WorkflowRevision {
+ const revision = run.revisions.find((item) => item.id === run.currentRevisionId)
+ if (!revision) throw new Error(`WorkflowRun ${run.id} 的 currentRevisionId 无效`)
+ return revision
+}
+
+export function getActiveStep(revision: WorkflowRevision): WorkflowStep | null {
+ return revision.steps.find((step) => step.status === 'active') ?? null
+}
+
+export function requireActiveWorkflow(run: WorkflowRun): WorkflowRun {
+ if (run.status !== 'active') throw new Error(`WorkflowRun 当前不可推进:${run.status}`)
+ return run
+}
+
+export function replaceWorkflowStep(
+ run: WorkflowRun,
+ revisionId: WorkflowRevision['id'],
+ stepId: WorkflowStep['id'],
+ update: (step: WorkflowStep) => WorkflowStep,
+ revisionUpdate?: (revision: WorkflowRevision) => WorkflowRevision,
+): WorkflowRun {
+ return {
+ ...run,
+ revisions: run.revisions.map((revision) => {
+ if (revision.id !== revisionId) return revision
+ const nextRevision = {
+ ...revision,
+ steps: revision.steps.map((step) => (step.id === stepId ? update(step) : step)),
+ }
+ return revisionUpdate ? revisionUpdate(nextRevision) : nextRevision
+ }),
+ }
+}
+
+export function updateCharacterSetupState(
+ workflow: WorkflowRun,
+ input: CharacterSetupStepInput,
+): WorkflowRun {
+ const run = requireActiveWorkflow(workflow)
+ const revision = getCurrentRevision(run)
+ const step = revision.steps.find((item) => item.type === 'character-setup')
+ if (!step || step.type !== 'character-setup' || step.status !== 'active') {
+ throw new Error('当前只能更新处于 active 状态的角色资料步骤')
+ }
+
+ const description = input.description.trim()
+ if (!description) throw new Error('角色描述不能为空')
+
+ return replaceWorkflowStep(run, revision.id, step.id, (current) => {
+ if (current.type !== 'character-setup') return current
+ return {
+ ...current,
+ input: {
+ description,
+ referenceMedia: [...input.referenceMedia],
+ },
+ }
+ })
+}
+
+export function advanceCharacterSetupState(workflow: WorkflowRun): {
+ run: WorkflowRun
+ target: WorkflowStepTarget
+} {
+ const run = requireActiveWorkflow(workflow)
+ const revision = getCurrentRevision(run)
+ const activeStep = getActiveStep(revision)
+ if (!activeStep) throw new Error('当前 WorkflowRun 没有 active 步骤')
+ if (activeStep.type !== 'character-setup') {
+ throw new Error(`当前步骤不是角色资料:${activeStep.type}`)
+ }
+ if (!activeStep.input) throw new Error('请先填写角色资料')
+
+ const templateStep = revision.steps.find((step) => step.type === 'character-template')
+ if (!templateStep) throw new Error('WorkflowRun 缺少 character-template 步骤')
+
+ const generationInput: CharacterTemplateGenerationInput = {
+ type: 'character_template',
+ projectId: run.projectId,
+ prompt: activeStep.input.description,
+ referenceMedia: activeStep.input.referenceMedia,
+ }
+
+ return {
+ run: {
+ ...run,
+ revisions: run.revisions.map((item) => {
+ if (item.id !== revision.id) return item
+ return {
+ ...item,
+ generationStatus: 'in_progress' as const,
+ steps: item.steps.map((step) => {
+ if (step.id === activeStep.id) return { ...step, status: 'passed' as const }
+ if (step.id !== templateStep.id || step.type !== 'character-template') return step
+ return {
+ ...step,
+ status: 'active' as const,
+ input: generationInput,
+ }
+ }),
+ }
+ }),
+ },
+ target: {
+ revisionId: revision.id,
+ stepId: templateStep.id,
+ },
+ }
+}
+
+export function interruptWorkflowRunState(run: WorkflowRun): WorkflowRun {
+ return run.status === 'active' ? { ...run, status: 'interrupted' } : run
+}
+
+function createInitialStep(
+ type: WorkflowStepType,
+ revisionId: string,
+ index: number,
+ prompt: string | null,
+): WorkflowStep {
+ const status: WorkflowStepStatus = index === 0 ? 'active' : 'locked'
+ const base: {
+ id: string
+ status: WorkflowStepStatus
+ taskId: null
+ submissionId: null
+ error: null
+ referenceStepIds: string[]
+ } = {
+ id: `${revisionId}:${type}`,
+ status,
+ taskId: null,
+ submissionId: null,
+ error: null,
+ referenceStepIds: [],
+ }
+
+ if (type === 'character-setup') {
+ return {
+ ...base,
+ type,
+ input: prompt ? { description: prompt, referenceMedia: [] } : null,
+ output: null,
+ }
+ }
+ if (type === 'character-template') {
+ return {
+ ...base,
+ type,
+ input: null,
+ output: null,
+ }
+ }
+ return { ...base, type, input: null, output: null } as WorkflowStep
+}
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/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..016328b
--- /dev/null
+++ b/frontend/src/pages/asset-library/index.tsx
@@ -0,0 +1,9 @@
+/** 资产库。 */
+export function AssetLibraryPage() {
+ return (
+
+ 资产库
+ 本次只提交模块划分与接口,页面实现进后续 PR。
+
+ )
+}
diff --git a/frontend/src/pages/home/index.tsx b/frontend/src/pages/home/index.tsx
new file mode 100644
index 0000000..7a153ad
--- /dev/null
+++ b/frontend/src/pages/home/index.tsx
@@ -0,0 +1,13 @@
+/** 首页:入口与项目概览。 */
+export function HomePage() {
+ return
+}
+
+function PagePlaceholder({ title }: { title: string }) {
+ return (
+
+ {title}
+ 本次只提交模块划分与接口,页面实现进后续 PR。
+
+ )
+}
diff --git a/frontend/src/pages/not-found/index.tsx b/frontend/src/pages/not-found/index.tsx
new file mode 100644
index 0000000..d35181b
--- /dev/null
+++ b/frontend/src/pages/not-found/index.tsx
@@ -0,0 +1,9 @@
+/** 页面不存在。 */
+export function NotFoundPage() {
+ return (
+
+ 页面不存在
+ 本次只提交模块划分与接口,页面实现进后续 PR。
+
+ )
+}
diff --git a/frontend/src/pages/playtest/index.tsx b/frontend/src/pages/playtest/index.tsx
new file mode 100644
index 0000000..9796e86
--- /dev/null
+++ b/frontend/src/pages/playtest/index.tsx
@@ -0,0 +1,9 @@
+/** 核验台。 */
+export function PlaytestPage() {
+ return (
+
+ 核验台
+ 本次只提交模块划分与接口,页面实现进后续 PR。
+
+ )
+}
diff --git a/frontend/src/pages/project-detail/index.tsx b/frontend/src/pages/project-detail/index.tsx
new file mode 100644
index 0000000..cc6ff5c
--- /dev/null
+++ b/frontend/src/pages/project-detail/index.tsx
@@ -0,0 +1,9 @@
+/** 项目详情。 */
+export function ProjectDetailPage() {
+ return (
+
+ 项目详情
+ 本次只提交模块划分与接口,页面实现进后续 PR。
+
+ )
+}
diff --git a/frontend/src/pages/projects/index.tsx b/frontend/src/pages/projects/index.tsx
new file mode 100644
index 0000000..8f807f7
--- /dev/null
+++ b/frontend/src/pages/projects/index.tsx
@@ -0,0 +1,9 @@
+/** 项目列表。 */
+export function ProjectsPage() {
+ return (
+
+ 项目列表
+ 本次只提交模块划分与接口,页面实现进后续 PR。
+
+ )
+}
diff --git a/frontend/src/pages/quick-start/index.tsx b/frontend/src/pages/quick-start/index.tsx
new file mode 100644
index 0000000..1ffe9fe
--- /dev/null
+++ b/frontend/src/pages/quick-start/index.tsx
@@ -0,0 +1,9 @@
+/** 快速开始。 */
+export function QuickStartPage() {
+ return (
+
+ 快速开始
+ 本次只提交模块划分与接口,页面实现进后续 PR。
+
+ )
+}
diff --git a/frontend/src/pages/workflow-editor/index.tsx b/frontend/src/pages/workflow-editor/index.tsx
new file mode 100644
index 0000000..59df779
--- /dev/null
+++ b/frontend/src/pages/workflow-editor/index.tsx
@@ -0,0 +1,9 @@
+/** 工作流画布。 */
+export function WorkflowEditorPage() {
+ return (
+
+ 工作流画布
+ 本次只提交模块划分与接口,页面实现进后续 PR。
+
+ )
+}
diff --git a/frontend/src/shared/README.md b/frontend/src/shared/README.md
new file mode 100644
index 0000000..b35fb60
--- /dev/null
+++ b/frontend/src/shared/README.md
@@ -0,0 +1,28 @@
+# shared
+
+与 Windup 业务无关、可被任意上层模块复用的基础代码。它是依赖方向的最底层,**不能反向依赖 `entities`、`features`、`pages` 或 `app`**。
+
+## 判断标准
+
+**如果一段代码需要理解 Windup 的业务词汇,它就不属于 shared。**
+
+## 现有内容
+
+- `pagination/` —— 与传输协议无关的分页请求与结果形状。
+
+## 后续允许放入
+
+- `ui/` —— 按钮、弹窗、加载状态等不含业务含义的展示组件
+- `hooks/` —— 通用浏览器或 React 行为,例如媒体查询、键盘快捷键
+- `utils/` —— 纯函数工具,例如日期格式化、文件大小显示
+- `config/` —— 前端通用常量与运行时配置读取
+
+**这些目录只在出现真实代码时创建,不为占位提前建空文件。**
+
+## 不允许放入
+
+- Project、Character、Generation、Task、WorkflowRun 等业务数据
+- `ProjectApis`、`CharacterApis` 这类业务接口集合
+- 流程的推进、重启、中断和 Revision 规则
+- 为开发与生产各维护一套实现的切换机制
+- 只被单个页面或模块使用、却以「复用」名义提前抽出的代码
diff --git a/frontend/src/shared/pagination/index.ts b/frontend/src/shared/pagination/index.ts
new file mode 100644
index 0000000..d5e2c27
--- /dev/null
+++ b/frontend/src/shared/pagination/index.ts
@@ -0,0 +1,13 @@
+/** 与传输协议无关的分页请求形状。 */
+export interface PageQuery {
+ page?: number
+ pageSize?: number
+}
+
+/** 与传输协议无关的分页结果形状。 */
+export interface Paged {
+ items: T[]
+ total: number
+ page: number
+ pageSize: number
+}
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..d32ff68
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "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..d3daab6
--- /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", "vitest.config.ts"]
+}
diff --git a/frontend/vercel.json b/frontend/vercel.json
new file mode 100644
index 0000000..c99f398
--- /dev/null
+++ b/frontend/vercel.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://openapi.vercel.sh/vercel.json",
+ "framework": "vite",
+ "installCommand": "npm ci",
+ "buildCommand": "npm run build",
+ "outputDirectory": "dist",
+ "rewrites": [
+ {
+ "source": "/(.*)",
+ "destination": "/index.html"
+ }
+ ]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..b928223
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,16 @@
+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)),
+ },
+ },
+})
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
new file mode 100644
index 0000000..3a40db6
--- /dev/null
+++ b/frontend/vitest.config.ts
@@ -0,0 +1,21 @@
+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',
+ ],
+ },
+})