commit bc84c95a8934c2f550aaaef8f5b16e5dc754014e Author: bql Date: Tue Oct 27 22:46:06 2026 +0800 Add a scaffold script that replaces the bq prefix with the new project name. Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b1acad --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.react-router + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..6de4aea --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +node_modules +dist +.react-router +package-lock.json \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..631e6ae --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "always" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..06e8e25 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# bq_react + +React 应用脚手架。创建新项目时,包名、源码和 `src` 下文件名里的 `bq` 前缀会换成你传入的项目名。 + +例如项目名是 `shop`: + +- `bq_react` 变成 `shop_react` +- `bq-theme` 变成 `shop-theme` +- 主题变量 `bq` 变成 `shop` + +## 创建项目 + +需要 Node.js,以及已登录的 GitHub CLI(`gh auth login`)。 + +```bash +curl -fsSL https://raw.githubusercontent.com/bqbqbqbqbq-l/bq_react/v0.1.0/scripts/create.sh | bash -s -- shop +``` + +这会在当前目录生成 `shop`,并执行 `npm install`。 + +## 开发这个模板 + +```bash +npm install +npm run dev +``` diff --git a/doc/基础知识/DDD是什么.md b/doc/基础知识/DDD是什么.md new file mode 100644 index 0000000..c90f52f --- /dev/null +++ b/doc/基础知识/DDD是什么.md @@ -0,0 +1,72 @@ +# DDD是什么 + +这里的 DDD 指领域规则写在领域对象上,再用分层规定代码放在哪一层。本仓库用它来规定功能模块的写法。 + +## 目录 + +- [四层](#四层) +- [聚合](#聚合) +- [端口](#端口) +- [本仓库里怎么落](#本仓库里怎么落) + +## 四层 + +一个功能模块在 `src/features//` 下按层分目录。 + +| 层 | 放什么 | 不放什么 | +|---|---|---| +| `domain` | 聚合根、值对象、领域方法、端口 | React、`src/app`、本地或远程的说法 | +| `application` | 用例。一次业务动作,例如登录、按 `store` 保存项目 | 页面状态、axios、IndexedDB | +| `infra` | 端口的实现。访问网络或本地库,并把接口数据收成领域对象 | 页面和 ViewModel | +| `presentation` | 页面和 ViewModel | 领域规则,例如笔画长短、撤销栈 | + +没有内容的层不建空目录。首页只有 `presentation`,因为它不持有自己的用例,列表和新建都调用 `ProjectApplication`。 + +用例是模块的业务。`SecurityApplication.login` 负责登录,并把 `LoginResult` 里的访问凭证写入会话。ViewModel 是页面的业务,只保存当前页的表单、错误和加载状态,再调用用例。 + +## 聚合 + +`Project` 是项目聚合的根。文件:`src/features/project/domain/entities/project.ts`。外部通过它的方法改文档: + +| 方法 | 作用 | +|---|---| +| `Project.create(name)` | 新建一个项目 | +| `addShape(shape)` | 位移小于 2 时拒绝;接受后把当前 `Document` 快照记进聚合,再加入图形 | +| `undo()` / `redo()` | 在聚合内部的 `past`、`future` 之间恢复 `Document` | +| `touch()` | 更新 `updatedAt`。保存前由用例调用 | + +`Document.addShape` 只由 `Project` 调用。`past` 和 `future` 是聚合上的普通字段,不是 `tProp`,所以 `getSnapshot(project)` 不会把撤销栈写进本地库或接口。 + +安全模块的领域对象是 `AuthCredentials`、`RegisterCredentials`、`User` 和 `LoginResult`。用户名不少于 3 个字符,密码不少于 6 个字符,注册时两次密码相同。`User.hasPermission` 判断角色。`LoginResult` 持有 `User` 和 `accessToken`。 + +`AUTH_CHANGED` 由 `src/app/session` 在凭证写入或清除后通知界面。它属于会话,不属于领域对象。 + +## 端口 + +`domain` 里的端口是抽象类。方法名只表达业务,不写 `local` 或 `remote`。 + +`ISecurityRepository.login` 返回 `LoginResult`,`register` 接收 `RegisterCredentials`。`SecurityRepository` 用 `Http` 访问接口,再把响应收成 `User` 和 `LoginResult`。 + +`IProjectRepository` 声明 `list`、`get`、`save`。`LocalProjectRepository` 和 `RemoteProjectRepository` 都实现这一个端口。存在哪一侧由领域枚举 `ProjectStore` 决定,`Project` 上没有这个字段。 + +## 本仓库里怎么落 + +登录: + +```text +presentation/security_view_model.ts 校验表单,调用 login +application/security_application.ts 调用端口,把 accessToken 交给 TokenStore +domain/i_security_repository.ts 声明 login,返回 LoginResult +infra/security_repository.ts 用 Http 访问 /auth/login,收成 LoginResult +``` + +绘制: + +```text +presentation/editor_view_model.ts 记录草稿,调用 Project.addShape +domain/entities/project.ts 判定笔画,维护撤销栈 +application/project_application.ts 按 store 调用 save +infra/..._project_repository.ts 把项目快照写入 IndexedDB 或 Http +``` + +`TokenStore`、`Http`、`Database` 属于 `src/app`,是全库能力,不是功能模块的领域规则。 diff --git a/doc/基础知识/DI是什么.md b/doc/基础知识/DI是什么.md new file mode 100644 index 0000000..69b4e6e --- /dev/null +++ b/doc/基础知识/DI是什么.md @@ -0,0 +1,81 @@ +# DI是什么 + +依赖注入是指类不自己 `new` 依赖,而是由容器按构造函数的参数创建并传入。本仓库的容器是 Inversify 的 `rootContainer`。端口是依赖倒置里的抽象,分层见 [DDD是什么](./DDD是什么.md)。 + +## 目录 + +- [原则和做法](#原则和做法) +- [容器](#容器) +- [Singleton 与 Transient](#singleton-与-transient) +- [端口怎么绑到实现](#端口怎么绑到实现) +- [同一个端口的多个实现](#同一个端口的多个实现) +- [装饰器能生效的条件](#装饰器能生效的条件) + +## 原则和做法 + +依赖倒置、依赖注入、控制反转是同一条链上的三件事。 + +依赖倒置:用例和仓储实现都只对着端口。`SecurityApplication` 调用 `ISecurityRepository.login`,不引用 `Http`。`SecurityRepository` 负责请求。登录成功后,用例把 `accessToken` 写入会话。 + +依赖注入:仓储和 `TokenStore` 从构造函数进来。`SecurityApplication` 只声明参数类型。 + +控制反转:创建哪一个实现、创建几次,由 `rootContainer` 决定。页面用 `useViewModel` 取出 ViewModel,容器顺着构造函数继续创建用例和仓储。 + +## 容器 + +`src/app/di/root_container.ts` 创建唯一容器。`autobind` 打开后,标了 `@injectable` 的具体类可以按类本身取出。`defaultScope` 是 `Singleton`。 + +页面通过 `useViewModel(SecurityViewModel)` 取出 ViewModel。`useViewModel` 内部调用 `rootContainer.get`。ViewModel 的构造函数需要 `SecurityApplication` 时,容器继续创建或取出用例,再把仓储注入用例。 + +## Singleton 与 Transient + +`Singleton` 在整个应用里只有一个实例。用例、仓储、`TokenStore`、`Http`、`Database` 使用它,这样凭证和连接可以在页面之间共用。 + +`Transient` 每次 `get` 都新建一个实例。页面 ViewModel 使用它。`useViewModel` 在页面挂载时取一次,离开页面后不再使用这个实例,表单和草稿不会留到下一次进入。 + +无参的 `@injectable()` 等于 `Singleton`。新代码要写出 `'Singleton'` 或 `'Transient'`。 + +## 端口怎么绑到实现 + +用例依赖的是抽象类 `ISecurityRepository`,容器默认不知道用哪个实现。实现类同时标上: + +```ts +@bindAs(ISecurityRepository) +@injectable('Singleton') +export class SecurityRepository implements ISecurityRepository {} +``` + +`@bindAs` 写在 `@injectable` 上面。类装饰器从靠近类的那一个开始执行,`@injectable` 先记下构造函数参数的类型,`@bindAs` 再把端口绑到这个实现。这个类文件必须被执行到,绑定才会发生。执行方式见 [代码实践](./代码实践.md) 里的模块入口。 + +只在 `infra` 的端口实现上使用 `@bindAs`。用例和 ViewModel 按类本身注册。 + +## 同一个端口的多个实现 + +`IProjectRepository` 有本地和远程两个实现。绑定时带上名称,容器里才会同时留下两份: + +```ts +@bindAs(IProjectRepository, ProjectStore.LOCAL) +@injectable('Singleton') +export class LocalProjectRepository implements IProjectRepository {} +``` + +`RemoteProjectRepository` 使用 `ProjectStore.REMOTE`。两个实现留在同一个端口上,用名称区分。 + +用例的构造函数依赖抽象端口。两个参数类型都是 `IProjectRepository`,类型元数据区分不了本地和远程,所以用 `@inject` 指定端口,用 `@named` 指定实现: + +```ts +constructor( + @inject(IProjectRepository) @named(ProjectStore.LOCAL) private localRepository: IProjectRepository, + @inject(IProjectRepository) @named(ProjectStore.REMOTE) private remoteRepository: IProjectRepository, +) {} +``` + +页面只传入 `ProjectStore`,不自己决定用 IndexedDB 还是 `Http`。 + +## 装饰器能生效的条件 + +JavaScript 运行时不保留构造函数参数的类型。`tsconfig.app.json` 打开 `experimentalDecorators` 和 `emitDecoratorMetadata` 后,编译器把参数类型写成 `design:paramtypes`。容器读这个数组,按类型创建依赖。 + +`SecurityApplication` 的参数类型分别是 `ISecurityRepository` 和 `TokenStore`,数组里就能分开,构造函数只声明类型。`ProjectApplication` 的两个参数类型都是 `IProjectRepository`。上一节用 `@inject` 指定端口,用 `@named(ProjectStore.LOCAL)` 和 `@named(ProjectStore.REMOTE)` 指定实现,容器按名称选择。 + +跨目录引用写 `@/app/...` 或 `@/features/...`。只表示类型的导入写成 `import type`,避免把类型当成运行时代码。 diff --git a/doc/基础知识/mobx是什么.md b/doc/基础知识/mobx是什么.md new file mode 100644 index 0000000..fdc6676 --- /dev/null +++ b/doc/基础知识/mobx是什么.md @@ -0,0 +1,53 @@ +# mobx是什么 + +MobX 负责页面上会变化的状态。mobx-keystone 负责可以保存、比较和恢复的实体。页面组件只负责渲染,不存放业务状态。 + +## 目录 + +- [页面状态](#页面状态) +- [ViewModel 的生命周期](#viewmodel-的生命周期) +- [实体与快照](#实体与快照) + +## 页面状态 + +ViewModel 里需要触发重渲染的字段标成 `observable`,修改这些字段的方法标成 `action`。组件用 `mobx-react-lite` 的 `observer` 包起来,字段变化后才会重画。 + +`SecurityViewModel` 的 `errorMessage`、`isLoading` 就是页面状态。它们跟着这一次打开的登录页,所以这个类标 `@injectable('Transient')`。 + +普通字段不用 observable。不会显示在界面上的局部变量也不放进 ViewModel。 + +## ViewModel 的生命周期 + +ViewModel 继承 `PageViewModel`。页面不自己 `new`,而调用 `useViewModel`。 + +```ts +export function useViewModel(identifier: new (...args: any[]) => T): T { + const [viewModel] = useState(() => { + const vm = rootContainer.get(identifier); + vm.created(); + return vm; + }); + + useEffect(() => { + viewModel.ready(); + const onPageHide = () => viewModel.beforeRefresh(); + window.addEventListener('pagehide', onPageHide); + return () => { + window.removeEventListener('pagehide', onPageHide); + viewModel.dispose(); + }; + }, [viewModel]); + + return viewModel; +} +``` + +`created` 在实例取出时调用。`ready` 在页面挂上后调用,适合开始加载数据,例如 `HomeViewModel.ready` 去拉项目列表。`beforeRefresh` 在页面被浏览器收起时调用。离开页面时调用 `dispose`。 + +## 实体与快照 + +`Project`、`Document`、`LineShape` 使用 mobx-keystone 的 `@model`。要保存的字段用 `tProp` 声明,修改实体的方法标 `@modelAction`。对外改文档的方法在聚合根上:`Project.addShape`、`Project.undo`、`Project.redo`、`Project.touch`。 + +快照是实体在某一刻的普通对象。`getSnapshot` 取出它,`applySnapshot` 把实体恢复成那一份。`Project.addShape` 接受图形前,把 `Document` 的快照放进聚合自己的 `past`,`undo` 和 `redo` 再用 `applySnapshot` 换回来。`past` 和 `future` 在 `onInit` 里标成 `observable`,页面才能根据 `canUndo`、`canRedo` 重绘。它们不是 `tProp`,`getSnapshot(project)` 不包含撤销栈。 + +保存到本地库或提交给接口之前,仓储取出项目快照再写入。实体类仍然留在内存里给界面使用。编辑器 ViewModel 只持有当前的 `project`、`store`、工具和草稿。 diff --git a/doc/基础知识/代码实践.md b/doc/基础知识/代码实践.md new file mode 100644 index 0000000..9afcbb3 --- /dev/null +++ b/doc/基础知识/代码实践.md @@ -0,0 +1,50 @@ +# 代码实践 + +沿登录走一遍。目的是看见页面、入口文件、ViewModel、用例、端口和网络怎样接在一起。分层见 [DDD是什么](./DDD是什么.md),容器见 [DI是什么](./DI是什么.md)。 + +## 目录 + +- [入口文件先执行](#入口文件先执行) +- [页面取出 ViewModel](#页面取出-viewmodel) +- [用例只依赖端口](#用例只依赖端口) +- [实现负责请求](#实现负责请求) +- [少了入口 import 会怎样](#少了入口-import-会怎样) + +## 入口文件先执行 + +`src/features/security/security.ts` 没有导出,只有两行 import: + +```ts +import './i18n'; +import './infra/persistence/security_repository'; +``` + +第一行执行 `registerMessages`,登录页的错误文案才能按键取到。第二行执行 `SecurityRepository` 上的 `@bindAs(ISecurityRepository)`,容器才知道端口用哪个类。 + +登录页第一行是 `import '../../security'`。这行不使用任何导出,作用是让上面两个文件运行。 + +## 页面取出 ViewModel + +`security.tsx` 是默认导出的页面,由 `src/app/routes.ts` 挂在 `/security`,并放在 `guest` 布局里。已登录用户进不了这个路径。 + +页面是 `observer`,用 `useViewModel(SecurityViewModel)` 取出 ViewModel。用户提交时调用 `submit`。ViewModel 是 `Transient`,这次填写的账号和错误只留在这一次打开的页面里。 + +## 用例只依赖端口 + +`submit` 在登录页校验 `AuthCredentials`。通过后调用: + +```ts +await this.application.login(this.loginForm); +``` + +`SecurityApplication.login` 不写 axios。它调用 `ISecurityRepository.login`,拿到 `LoginResult` 后执行 `tokenStore.save(result.accessToken)`。凭证怎样通知界面,由 `src/app/session` 负责。 + +## 实现负责请求 + +`SecurityRepository.login` 把实体收成快照,再调用 `this.http.instance.post('/auth/login', plainDto)`。响应收成 `User` 和 `LoginResult` 后返回给用例。`Http` 会在请求上附带已有凭证,并处理 401。这些步骤不写进用例。 + +## 少了入口 import 会怎样 + +页面如果没有 `import '../../security'`,`security.ts` 不会执行。文案没有注册,`ISecurityRepository` 也没有绑定。`useViewModel` 创建 `SecurityViewModel` 时,容器无法提供 `SecurityApplication` 所依赖的仓储。 + +项目模块同样依赖 `src/features/project/project.ts`。它导入本地和远程两个仓储,两者分别用 `@bindAs(IProjectRepository, ProjectStore.LOCAL)` 和 `ProjectStore.REMOTE` 绑到同一个端口。用例用 `@inject` 和 `@named` 注入对应实现。首页和编辑器在文件顶部写 `import '@/features/project/project'`,本地列表和远程列表写 `import '../../project'`。新增仓储时,把实现文件加进对应的入口,并保证被路由加载到的页面引用了这个入口。 diff --git a/doc/基础知识/常见设计模式及使用.md b/doc/基础知识/常见设计模式及使用.md new file mode 100644 index 0000000..114241b --- /dev/null +++ b/doc/基础知识/常见设计模式及使用.md @@ -0,0 +1,33 @@ +# 常见设计模式及使用 + +本篇只写这套脚手架里会碰到的三种写法。名称用来对照代码,不展开其他模式。 + +## 目录 + +- [依赖注入](#依赖注入) +- [仓储](#仓储) +- [观察者](#观察者) + +## 依赖注入 + +调用方不创建依赖,容器按构造函数传入。`SecurityApplication` 需要仓储和 `TokenStore` 时,只声明参数类型,不写 `new SecurityRepository()`。 + +依赖倒置、注入方式和 `Singleton`、`Transient`、`@bindAs` 的用法见 [DI是什么](./DI是什么.md)。 + +## 仓储 + +仓储把「业务要的数据」和「数据放在哪」分开。 + +`ISecurityRepository` 声明 `login`,返回 `LoginResult`。用例只调用这个方法,再把 `accessToken` 写入 `TokenStore`。`SecurityRepository` 才知道要访问 `/auth/login`,并把响应收成 `User`。 + +项目模块只有一个端口 `IProjectRepository`,声明 `list`、`get`、`save`。本地实现走 IndexedDB,远程实现走 `Http`,都实现这个端口。`ProjectApplication` 按 `store` 选择其中一个。 + +因此 `domain` 不出现 axios 和 IndexedDB,`infra` 不决定登录成功后要做什么。 + +## 观察者 + +状态变化后,订阅者自己更新。本仓库有两处。 + +MobX 的 `observable` 字段变化时,包了 `observer` 的组件重绘。这是页面状态到界面的观察者。 + +`TokenStore.save` 和 `TokenStore.remove` 调用 `notifyAuthChanged`。`useAuthenticated` 通过 `subscribeAuth` 收到 `AUTH_CHANGED` 后再读一次 `hasSession`。登录态变化时,首页不必自己轮询。 diff --git a/doc/基础知识/当前框架技术栈说明.md b/doc/基础知识/当前框架技术栈说明.md new file mode 100644 index 0000000..09f8c72 --- /dev/null +++ b/doc/基础知识/当前框架技术栈说明.md @@ -0,0 +1,64 @@ +# 当前框架技术栈说明 + +本目录讲上手这套脚手架需要的概念。本仓库怎么分层、模块之间数据怎么走,见 [项目架构](../项目文档/项目架构.md)。新代码怎么命名、怎么接进容器,见 [代码撰写规范](../项目文档/代码撰写规范.md)。 + +## 目录 + +- [阅读顺序](#阅读顺序) +- [技术栈](#技术栈) +- [样式](#样式) +- [路由怎么挂页面](#路由怎么挂页面) + +## 阅读顺序 + +1. 本文。 +2. [DDD是什么](./DDD是什么.md)。知道四层、聚合根和端口各自放什么。 +3. [DI是什么](./DI是什么.md)。知道类怎样进容器,页面怎样拿到它们。 +4. [mobx是什么](./mobx是什么.md)。知道页面状态、实体和快照。 +5. [常见设计模式及使用](./常见设计模式及使用.md)。只看本仓库用到的几种。 +6. [代码实践](./代码实践.md)。沿登录走一遍。 + +国际化、数据库、Worker 的用法在 `doc/项目文档/基础应用/`。界面组件使用 Ant Design,不在本目录展开。 + +## 技术栈 + +| 作用 | 库 | +|---|---| +| 界面 | React | +| 路由 | React Router。路由表在 `src/app/routes.ts` | +| 构建 | Vite。环境文件在 `src/`,只暴露 `VITE_` 开头的变量 | +| 语言 | TypeScript。装饰器需要 `experimentalDecorators` 和 `emitDecoratorMetadata` | +| 依赖注入 | Inversify。容器在 `src/app/di/root_container.ts` | +| 页面状态 | MobX,配合 `mobx-react-lite` 的 `observer` | +| 实体 | mobx-keystone 的 `@model` | +| 请求 | axios。封装在 `src/app/network` | +| 文案 | i18next | +| 样式 | Sass | + +路径别名 `@/` 指向 `src/`。跨目录用它,同一功能模块内部用相对路径。 + +## 样式 + +组件样式用同名 `.scss`,在组件文件里 import。主题色由 `src/app/theme` 解析后交给 Ant Design,不在每个组件里各写一套亮色和暗色。 + +## 路由怎么挂页面 + +页面不自己注册路由。`src/app/routes.ts` 把 URL 指到 `presentation` 下的页面文件,页面使用默认导出。 + +```ts +export default [ + layout('routes/app_shell.tsx', [ + index('../features/home/presentation/home/home.tsx'), + layout('routes/layouts/guest.layout.tsx', [ + route('security', '../features/security/presentation/security/security.tsx'), + ]), + layout('routes/layouts/auth.layout.tsx', [ + route('editor/:projectId', '../features/editor/presentation/editor/editor.tsx'), + ]), + ]), +] satisfies RouteConfig; +``` + +`app_shell` 提供文案和主题。`guest` 只允许未登录进入,例如 `/security`。`auth` 只允许已登录进入,例如 `/editor/:projectId`。判断使用 `hasSession()`,规则在 `decideRoutePolicy`。 + +新增页面时:在功能模块的 `presentation` 写下默认导出的页面,再在这张表里登记路径和布局。 diff --git a/doc/项目文档/*撰写总则.md b/doc/项目文档/*撰写总则.md new file mode 100644 index 0000000..7ea05a1 --- /dev/null +++ b/doc/项目文档/*撰写总则.md @@ -0,0 +1,68 @@ +# 撰写总则 + +## 目录 + +- [文档分三类](#文档分三类) +- [每篇文档都写目录](#每篇文档都写目录) +- [图中只用项目词条](#图中只用项目词条) +- [图只画本层](#图只画本层) +- [功能模块何时拆开](#功能模块何时拆开) +- [文件名](#文件名) +- [示例与正式文档](#示例与正式文档) + +## 文档分三类 + + +| 文档 | 位置 | 正文 | +| ---- | ---------------------------------- | ---------------------------------------- | +| 项目架构 | `doc/项目架构.md`,全库一篇 | 目录结构(到 `features/[feature]`)、模块定义、功能模块之间的业务数据流 | +| 基础应用 | `doc/基础应用/`,`src/app` 下,一个目录一篇 | 功能定义、使用示例、该应用内部的数据流与时序 | +| 功能模块 | `doc/功能模块/`,`src/features` 下一个目录一组 | 模块内目录、功能定义、核心业务层与核心模型、核心业务时序图 | + + +`src/app` 下的每个目录是一个基础应用。`src/features` 下的每个目录是一个功能模块。 + +章节的写法分别见: + +- [项目架构文档撰写说明](./项目架构文档撰写说明.md) +- [基础应用文档撰写说明](./基础应用/基础应用文档撰写说明.md) +- [功能模块文档撰写说明](./功能模块/功能模块文档撰写说明.md) + +## 每篇文档都写目录 + +标题下第一段是目录,标题为「目录」。条目与正文标题一致,并链到对应标题。拆出去的业务文档写进父文档目录,链到文件。 + +撰写说明、示例、正式文档都要有目录。 + +## 图中只用项目词条 + +Mermaid 的节点、参与者、箭头文字只用仓库里的目录名、类名、方法名、字段名。中文说明写在图外的正文里。 + +## 图只画本层 + +项目架构只画 `home`、`security`、`project`、`editor` 之间的业务数据。不画 `session`、`network`、`db`、`routes` 这些基础应用,也不画时序。 + +基础应用只画该目录内部的类和函数。不画功能模块的调用链,也不把其他基础应用的内部步骤画进来。调用方可以出现在使用示例的代码里。时序图已经能说明这条行为时,不再画数据流图。 + +功能模块的时序写在功能模块文档。那里不展开基础应用内部步骤。 + +## 功能模块何时拆开 + +一条主业务时,时序图写在该模块主文档里。 + +同一模块有多条要分开讲述的事件业务时,为每条业务另写一篇,放在该模块目录下。主文档保留目录结构、功能定义、核心业务层与核心模型,并用目录链到各业务文档。编辑器的打开、绘制、撤销与重做按这个方式拆开。 + +方法定义写在主文档的核心业务层。业务文档写该业务的定义和时序图,并点名用到的方法。 + +## 文件名 + +- 基础应用:`中文名(目录名).md`,例如 `会话(session).md`。 +- 功能模块主文档:`模块名(目录名).md`,例如 `编辑器模块(editor).md`。 +- 拆出的业务文档:放在同一模块目录,用业务名作文件名,例如 `绘制.md`。 +- 说明与示例只放在 `doc/文档撰写说明与示例/`。 + + + +## 示例与正式文档 + +示例用来对齐章节和粒度,正式文档另写。类名、方法、目录以当时仓库为准;代码变了就改正式文档。 \ No newline at end of file diff --git a/doc/项目文档/*项目架构文档撰写说明.md b/doc/项目文档/*项目架构文档撰写说明.md new file mode 100644 index 0000000..5f38994 --- /dev/null +++ b/doc/项目文档/*项目架构文档撰写说明.md @@ -0,0 +1,33 @@ +# 项目架构文档撰写说明 + +适用文件:`doc/项目架构.md`。 + +## 目录 + +- [1. 项目目录结构](#1-项目目录结构) +- [2. 模块定义](#2-模块定义) +- [3. 模块间数据流](#3-模块间数据流) + +示例见 [项目架构(示例)](./项目架构(示例).md)。 + +## 1. 项目目录结构 + +用目录树写出 `src`。`src/app` 展开到每个基础应用目录。`src/features` 展开到 `features/[feature]`,不写模块内部。 + +模块内部目录由功能模块文档写。基础应用内部文件由使用示例和数据流体现。 + +## 2. 模块定义 + +先用一段话定义本仓库的模块:`src/app/<目录>` 是基础应用,`src/features/<目录>` 是功能模块。 + +然后分「基础应用」「功能模块」两张表。每行:名称、目录、这个模块做什么。表中的目录与上一节一致,不多出、不漏掉。 + +功能模块的定义写 application 层的业务,也就是该模块对外提供的用例。不写页面上的操作,不写 ViewModel。ViewModel 是页面业务,放在功能模块文档。模块没有自己的 application 时,写明它不持有独立用例,以及它调用哪个模块的用例。 + +## 3. 模块间数据流 + +只画功能模块之间的业务数据。节点是 `home`、`security`、`project`、`editor`。箭头文字使用在模块之间传递的方法名或字段名,例如 `list`、`projectId`。中文写在图下。 + +不画基础应用,例如 `session`、`network`、`db`、`routes`。不画时序,不写类内部的调用步骤。基础应用内部的数据流和时序写在基础应用文档。功能模块内部的时序写在功能模块文档。 + +一条业务一张图。图下用几句话说明业务数据从哪个功能模块到哪个功能模块。决定下游数据能否继续流动的状态必须画出来。例如 `home` 先取得 `security` 的 `authenticated`,才从 `project` 取得 `projectId`,再交给 `editor`。 \ No newline at end of file diff --git a/doc/项目文档/代码撰写规范.md b/doc/项目文档/代码撰写规范.md new file mode 100644 index 0000000..43ff8fa --- /dev/null +++ b/doc/项目文档/代码撰写规范.md @@ -0,0 +1,172 @@ +# 代码撰写规范 + +本规范规定脚手架里新代码怎么放、怎么命名、怎么接进容器。业务数据流见 [项目架构](./项目架构.md)。 + +## 目录 + +- [环境变量](#环境变量) +- [目录与分层](#目录与分层) +- [文件命名](#文件命名) +- [类与函数命名](#类与函数命名) +- [依赖注入](#依赖注入) + - [Singleton 与 Transient](#singleton-与-transient) +- [功能模块入口](#功能模块入口) +- [import](#import) +- [新增功能模块](#新增功能模块) + +## 环境变量 + +环境文件放在 `src/`。`vite.config.ts` 的 `envDir` 是 `src`。 + + +| 文件 | 脚本 | +| ---------------------- | ----------------------------------- | +| `src/.env.debug` | `npm run debug` | +| `src/.env.development` | `npm run dev`、`npm run build` | +| `src/.env.production` | `npm run prod`、`npm run build:prod` | + + +只有 `VITE_` 开头的变量会进入浏览器。命名是 `VITE_` 加大写蛇形,例如 `VITE_API_URL`。 + +这里只放会公开到前端的地址和开关。密钥不放进这些文件。业务代码用 `import.meta.env.VITE_*` 读取,并在使用处给出缺省值。`Http` 的基地址写在 `src/app/network/http.ts`: + +```ts +baseURL: import.meta.env.VITE_API_URL || 'https://mybackend.com' +``` + +## 目录与分层 + +`src/app` 放全库能力,不放业务用例。`src/features/` 放一个功能模块。 + + +| 层 | 放置 | 可以依赖 | +| -------------- | ----------------- | -------------------------------------- | +| `domain` | 聚合、值对象、领域事件、接口 | 本模块 `domain` | +| `application` | 用例 | 本模块 `domain`,以及 `src/app` 里该用例需要的入口 | +| `infra` | 接口的实现 | 本模块 `domain`,以及 `src/app` 的网络、数据库、依赖注入 | +| `presentation` | 页面、ViewModel、展示组件 | 本模块 `application`,以及标明可使用的其他模块类型 | + + +`domain` 不引用 React,也不引用 `src/app`。没有内容的层不建空目录。 + +聚合根是外部唯一可以修改的实体。`Project` 包含 `Document` 和图形,落笔是否成立、撤销和重做都写在 `Project` 上。页面和其他模块不调用 `Document.addShape`,也不替换文档快照。 + +仓储接口只表达取出和保存聚合,不表达本地或远程。`local` 与 `remote` 留在 `application`,用来选择哪个 `infra` 实现。接口的返回值是领域对象。登录返回 `User`,不返回未标明类型的 `user`。 + +登录成功先形成领域结果,再由用例调用 `TokenStore.save`。`AUTH_CHANGED` 只通知界面,不是领域事件。 + +路由只在 `src/app/routes.ts` 里把 URL 指到 `presentation` 下的页面。需要登录的页面放进 `auth` 布局,仅访客可进的页面放进 `guest` 布局。 + +## 文件命名 + +目录和文件用小写蛇形。 + + +| 种类 | 形式 | 例子 | +| --------- | ---------------------------------------- | --------------------------------- | +| 页面 | 与所在目录同名 | `home/home.tsx` | +| ViewModel | `*_view_model.ts` | `security_view_model.ts` | +| 样式 | 与组件同名的 `.scss` | `home.scss` | +| 实体 | 聚合或业务名文件 | `project.ts`、`document.ts`、`user.ts` | +| 抽象接口 | `i_` 前缀 | `i_security_repository.ts` | +| 仓储实现 | `*_repository.ts`,放在 `infra/persistence` | `security_repository.ts` | +| 布局 | 点号标出角色 | `auth.layout.tsx` | +| 文案 | 语言码 | `zh_cn.ts`、`en_us.ts` | +| 模块入口 | 与功能目录同名 | `security.ts`、`project.ts` | + + +## 类与函数命名 + +类用帕斯卡命名。函数和钩子用小驼峰,钩子以 `use` 开头。 + + +| 种类 | 形式 | 例子 | +| ---- | -------------------------------- | --------------------------- | +| 用例 | `名称Application` | `SecurityApplication` | +| 页面模型 | `名称ViewModel`,继承 `PageViewModel` | `HomeViewModel` | +| 仓储实现 | `名称Repository` | `LocalProjectRepository` | +| 接口 | 抽象类,`I` 前缀 | `ISecurityRepository` | +| 实体 | 业务名,不加 `Entity` 后缀 | `Project`、`User`、`AuthCredentials` | +| 枚举 | `名称Enum` | `AuthEnum` | + + +实体使用 `User`、`Project` 这样的业务名,类名不带 `Entity` 后缀。 + +路由页面使用默认导出。`src/app/routes.ts` 按文件加载页面。 + +## 依赖注入 + +全库只有 `src/app/di/root_container.ts` 里的 `rootContainer`。容器打开 `autobind`,默认 scope 是 `Singleton`。 + + +| 对象 | 写法 | +| --------------------------------- | ------------------------------------------- | +| 用例、`TokenStore`、`Http`、`Database` | `@injectable('Singleton')` | +| 页面 ViewModel | `@injectable('Transient')` | +| 接口实现 | `@injectable('Singleton')`,并加 `@bindAs(接口)` | + + +### Singleton 与 Transient + +`Singleton` 在整个应用里只创建一次。之后无论哪个页面、哪次 `rootContainer.get`,拿到的都是同一个实例。适合没有页面现场、需要一直留着的对象:用例、仓储、`TokenStore`、`Http`、`Database`。登录后的凭证、数据库连接和用例方法要在首页、编辑器之间共用,所以这些类用 `Singleton`。 + +`Transient` 每次 `rootContainer.get` 都新建一个实例。`useViewModel` 在页面挂载时取一次 ViewModel,离开页面后这次实例不再使用。适合只属于当前页的状态:表单、`errorMessage`、尚未落笔的草稿。撤销栈属于 `Project`,不放在 ViewModel 里。`SecurityViewModel`、`HomeViewModel`、`EditorViewModel`、`ProjectListViewModel` 都用 `Transient`。 + +ViewModel 若标成 `Singleton`,再次进入同一页会看到上次留下的表单、错误和草稿。用例若标成 `Transient`,每个页面都会各持有一份,凭证和仓储无法在页面之间共用。 + +无参的 `@injectable()` 会落到容器默认的单例,效果与 `Singleton` 相同。新代码写出 scope。现有仓储仍是无参写法。 + +`bindAs` 定义在 `src/app/di/bind_as.ts`。装饰器在类定义时绑定,并且写在 `@injectable` 上面。类装饰器从靠近类的那一个开始执行,`@injectable` 先写入构造参数类型,`bind(port).to(impl)` 才能读到。同一个实现再次执行时,先卸掉上一次的绑定再绑。同一个接口有多个实现时传入名称,例如 `@bindAs(IProjectRepository, ProjectStore.LOCAL)`。用例构造函数用 `@inject(IProjectRepository)` 和 `@named(ProjectStore.LOCAL)` 注入对应实现。 + +```ts +@bindAs(ISecurityRepository) +@injectable('Singleton') +export class SecurityRepository implements ISecurityRepository {} +``` + +只在 `infra` 的接口实现上使用 `@bindAs`。用例和 ViewModel 由 `autobind` 按类本身注册,不再包一层接口。 + +## 功能模块入口 + +`src/features//.ts` 只做副作用 import,不导出类,不写用例。它让文案注册和 `@bindAs` 在页面渲染前执行。 + +`src/features/security/security.ts`: + +```ts +import './i18n'; +import './infra/persistence/security_repository'; +``` + +第一行执行 `registerMessages`。第二行使 `SecurityRepository` 上的 `@bindAs(ISecurityRepository)` 跑起来。安全页在文件顶部引用它: + +```ts +import '../../security'; +``` + +`project.ts` 导入本地仓储和远程仓储。使用项目用例的页面同样要先引用它,例如 `import '@/features/project/project'` 或 `import '../../project'`。 + +新增仓储或文案包时,把对应文件 import 进这个入口,并保证至少有一个会被路由加载到的页面引用该入口。没有这行 import,容器里没有实现,文案也不会注册。 + +## import + +跨目录使用 `@/`,与 `tsconfig.app.json` 的 `paths` 一致。同一功能模块内部使用相对路径。 + +```ts +import { useViewModel } from '@/app/lifecycle/use_view_model'; +import { ProjectApplication } from '@/features/project/application/project_application'; +import { SecurityViewModel } from './security_view_model'; +``` + +副作用 import 放在文件最上方,单独成行,不写绑定。类型使用 `import type`。 + +其他模块可以引用对方的 application,以及标明可使用的 entity 和展示组件。不引用对方的 `infra`。 + +## 新增功能模块 + +1. 在 `src/features//domain` 写实体和接口。接口是 `I` 前缀的抽象类。 +2. 在 `application` 写用例,并标 `@injectable('Singleton')`。 +3. 在 `infra` 写实现,标 `@injectable('Singleton')` 和 `@bindAs(接口)`。 +4. 把实现 import 进 `src/features//.ts`。有文案时,同时 import 该模块的 `i18n`。 +5. 在 `presentation` 写页面和 ViewModel。ViewModel 继承 `PageViewModel`,标 `@injectable('Transient')`。页面先 import 模块入口,再用 `useViewModel` 取出 ViewModel。页面默认导出。 +6. 在 `src/app/routes.ts` 登记路径。需要登录的页面放进 `auth` 布局。 + diff --git a/doc/项目文档/功能模块/*功能模块文档撰写说明.md b/doc/项目文档/功能模块/*功能模块文档撰写说明.md new file mode 100644 index 0000000..7b3df33 --- /dev/null +++ b/doc/项目文档/功能模块/*功能模块文档撰写说明.md @@ -0,0 +1,75 @@ +# 功能模块文档撰写说明 + +适用文件:`doc/功能模块/<模块名>/<模块名>(目录名).md`。 + +## 目录 + +- [主文档写什么](#主文档写什么) +- [一条主业务](#一条主业务) +- [多条事件业务](#多条事件业务) +- [业务文档写什么](#业务文档写什么) + +单文档形态见本节「一条主业务」。拆分形态的示例是 [编辑器主文档](./功能模块(示例).md) 和 [绘制](./绘制(示例).md)。 + +## 主文档写什么 + +### 1. 模块目录结构 + +写出该 `src/features/[feature]` 内部的目录和承担业务的文件。空目录按仓库现状保留。 + +### 2. 功能定义 + +一段话定义这个模块做什么。下面列出它包含的业务。每条业务一句话。 + +### 3. 核心业务层与核心模型 + +分两节,不合并成「核心类」。 + +核心业务层是 ViewModel。每个 ViewModel 写定义、文件路径和方法。公开方法必写,承担业务的私有方法一并列出。方法写签名和这个方法做什么。页面组件不列入。 + +核心模型写聚合根、它包含的实体,以及不变条件。每个类型写定义、文件路径和领域方法。包含关系用类图。外部只能调用聚合根的方法。存储位置、接口报文字段不写进实体。 + +需要画出页面怎样向聚合提交动作时,另用用例图:参与者写 ViewModel,用例写聚合根的方法名。类图和用例图不要重复同一件事。 + +## 一条主业务 + +模块只有一条要讲述的主业务时,不另建文件。主文档增加第 4 节「核心业务」,放一张时序图。图中只用类名和方法名,图下用中文按调用顺序写清这条业务。 + +只有一条主业务的模块,目录是: + +```markdown +## 目录 + +- [模块目录结构](#模块目录结构) +- [功能定义](#功能定义) +- [核心业务层](#核心业务层) +- [核心模型](#核心模型) +- [核心业务](#核心业务) +``` + +「核心业务」里直接放时序图。例如项目模块的保存:调用方 → `ProjectApplication.save` → `IProjectRepository`。应用层先按 `store` 选定实现,领域端口本身不区分本地和远程。`list`、`get`、`save`、`create` 写在功能定义。落笔、撤销和重做写在聚合根。 + +## 多条事件业务 + +同一模块有多条事件业务时,主文档不放时序图。第 4 节改为「业务文档」,作为索引链到同目录下的各篇。 + +编辑器按事件拆开,例如: + +```text +doc/功能模块/编辑器模块/ +├── 编辑器模块(editor).md +├── 打开项目.md +├── 绘制.md +└── 撤销与重做.md +``` + +主文档目录里列出这些文件。 + +## 业务文档写什么 + +每篇业务文档包含目录,以及: + +1. 业务定义:这个事件做什么,从哪个操作开始。 +2. 时序图:参与者使用主文档里的类名,箭头文字使用方法名。图下用中文按顺序说明这次事件。 + +方法表留在主文档的核心业务层。业务文档点名本事件调用的方法。 diff --git a/doc/项目文档/功能模块/安全模块/安全模块(security).md b/doc/项目文档/功能模块/安全模块/安全模块(security).md new file mode 100644 index 0000000..a64ba14 --- /dev/null +++ b/doc/项目文档/功能模块/安全模块/安全模块(security).md @@ -0,0 +1,124 @@ +# 安全模块 + +## 目录 + +- [模块目录结构](#模块目录结构) +- [功能定义](#功能定义) +- [核心业务层](#核心业务层) + - [SecurityViewModel](#securityviewmodel) +- [核心模型](#核心模型) +- [核心业务](#核心业务) + - [登录](#登录) + +## 模块目录结构 + +```text +src/features/security/ +├── application/ +│ └── security_application.ts +├── domain/ +│ ├── entities/ +│ │ ├── auth_credentials.ts +│ │ ├── register_credentials.ts +│ │ ├── login_result.ts +│ │ └── user.ts +│ ├── enum/ +│ │ └── auth_enum.ts +│ └── repositories/ +│ └── i_security_repository.ts +├── i18n/ +├── infra/ +│ └── persistence/ +│ └── security_repository.ts +├── presentation/ +│ ├── profile/ +│ └── security/ +└── security.ts +``` + +`security.ts` 导入文案和 `SecurityRepository`。`profile` 页面没有用例。 + +## 功能定义 + +安全模块提供身份用例。 + +- `login(credentials)`:校验身份,得到 `LoginResult`。用例再把其中的访问凭证写入会话。 +- `register(credentials)`:注册一个用户。 +- `logout()`:清除会话中的凭证。 + +这些方法在 `SecurityApplication`。端口方法名不带 `remote`。 + +## 核心业务层 + +### SecurityViewModel + +登录与注册页的状态。文件:`src/features/security/presentation/security/security_view_model.ts`。继承 `PageViewModel`。 + +状态:`activeTab`、`isLoading`、`errorMessage`、`loginForm`、`registerForm`。 + +| 方法 | 说明 | +|---|---| +| `switchTab(tab: AuthEnum): void` | 在 `LOGIN` 与 `REGISTER` 之间切换,并清空 `errorMessage` | +| `submit(): Promise` | 表单已经由领域对象判定有效后,登录调用 `login`,注册调用 `register` | +| `dispose(): void` | 清空 `errorMessage` | + +`submit` 在 `isValid` 不为真时,用 `validationErrorKey` 取出文案并返回 `false`。登录成功返回 `true`。注册成功后切回 `LOGIN` 并返回 `false`。 + +## 核心模型 + +| 类型 | 文件 | 定义 | +|---|---|---| +| `AuthCredentials` | `src/features/security/domain/entities/auth_credentials.ts` | 登录凭证。用户名不少于 3 个字符,密码不少于 6 个字符 | +| `RegisterCredentials` | `src/features/security/domain/entities/register_credentials.ts` | 注册凭证。在登录凭证上增加一次确认密码,两次密码必须相同 | +| `User` | `src/features/security/domain/entities/user.ts` | 用户。持有 `id`、`username`、`roles` | +| `LoginResult` | `src/features/security/domain/entities/login_result.ts` | 一次成功的登录。持有 `User` 和 `accessToken` | +| `ISecurityRepository` | `src/features/security/domain/repositories/i_security_repository.ts` | `login` 返回 `LoginResult`,`register` 接收 `RegisterCredentials` | + +```mermaid +classDiagram + class AuthCredentials { + username + password + isValid + validationErrorKey + } + class RegisterCredentials { + confirmPassword + isValid + validationErrorKey + } + class User { + id + username + roles + hasPermission(permission) + } + class LoginResult { + user + accessToken + } + AuthCredentials <|-- RegisterCredentials + LoginResult --> User +``` + +`User.hasPermission` 在角色包含 `admin` 或指定权限时返回真。登录失败不产生 `LoginResult`。 + +## 核心业务 + +### 登录 + +```mermaid +sequenceDiagram + participant SecurityViewModel + participant SecurityApplication + participant ISecurityRepository + participant LoginResult + participant TokenStore + + SecurityViewModel->>SecurityApplication: login(credentials) + SecurityApplication->>ISecurityRepository: login(credentials) + ISecurityRepository-->>SecurityApplication: LoginResult + SecurityApplication->>TokenStore: save(accessToken) +``` + +`SecurityViewModel.submit` 在 `activeTab` 为 `LOGIN` 时调用 `login`。仓储返回的是 `User` 和 `accessToken`,不是未标明类型的用户字段。用例只把 `accessToken` 交给 `TokenStore.save`。`AUTH_CHANGED` 是会话发出的界面通知,不是领域事件。通知怎样到达页面,见 [会话](../../基础应用/会话(session).md)。 diff --git a/doc/项目文档/功能模块/编辑器模块/打开项目.md b/doc/项目文档/功能模块/编辑器模块/打开项目.md new file mode 100644 index 0000000..1e54a53 --- /dev/null +++ b/doc/项目文档/功能模块/编辑器模块/打开项目.md @@ -0,0 +1,28 @@ +# 打开项目 + +## 目录 + +- [业务定义](#业务定义) +- [时序](#时序) + +## 业务定义 + +编辑页根据路由中的 `projectId` 和 `store` 载入 `Project`,并清空尚未落笔的草稿。 + +本事件使用 `EditorViewModel.open`。参数 `store` 必须是枚举 `ProjectStore` 的取值。 + +## 时序 + +```mermaid +sequenceDiagram + participant EditorPage + participant EditorViewModel + participant ProjectApplication + participant Project + + EditorPage->>EditorViewModel: open(id, store) + EditorViewModel->>ProjectApplication: get(store, id) + ProjectApplication-->>EditorViewModel: Project +``` + +`id` 或 `store` 为空时,`errorMessage` 为缺少项目。`store` 不在 `ProjectStore` 的取值里时,`errorMessage` 为项目存储无效。这两种情况都不再请求。读到的聚合写入 `project`,并记下 `store`,同时清空 `draft`。没有这个项目时,`errorMessage` 为找不到项目。 diff --git a/doc/项目文档/功能模块/编辑器模块/撤销与重做.md b/doc/项目文档/功能模块/编辑器模块/撤销与重做.md new file mode 100644 index 0000000..61efe63 --- /dev/null +++ b/doc/项目文档/功能模块/编辑器模块/撤销与重做.md @@ -0,0 +1,31 @@ +# 撤销与重做 + +## 目录 + +- [业务定义](#业务定义) +- [时序](#时序) + +## 业务定义 + +编辑器请求 `Project` 后退或前进一份文档,并把聚合保存。快照由 `Project` 持有。 + +本事件使用 `EditorViewModel` 的 `undo`、`redo`。 + +## 时序 + +```mermaid +sequenceDiagram + participant EditorPage + participant EditorViewModel + participant Project + participant ProjectApplication + + EditorPage->>EditorViewModel: undo() + EditorViewModel->>Project: undo() + EditorViewModel->>ProjectApplication: save(store, project) + EditorPage->>EditorViewModel: redo() + EditorViewModel->>Project: redo() + EditorViewModel->>ProjectApplication: save(store, project) +``` + +`Project.undo` 没有更早的快照时返回失败,页面不调用 `save`。成功时聚合把当前文档换回上一份快照。`Project.redo` 没有更新的快照时同样不保存。成功时聚合恢复下一份快照。两次成功之后都按 `store` 调用 `ProjectApplication.save`。 diff --git a/doc/项目文档/功能模块/编辑器模块/绘制.md b/doc/项目文档/功能模块/编辑器模块/绘制.md new file mode 100644 index 0000000..af2bc3d --- /dev/null +++ b/doc/项目文档/功能模块/编辑器模块/绘制.md @@ -0,0 +1,32 @@ +# 绘制 + +## 目录 + +- [业务定义](#业务定义) +- [时序](#时序) + +## 业务定义 + +用户在画布上按下、移动、抬起指针。编辑器按当前工具形成草稿,再交给 `Project.addShape`。聚合接受这次落笔后,用例保存整个 `Project`。 + +本事件使用 `EditorViewModel` 的 `pointerDown`、`pointerMove`、`pointerUp`。是否落笔由 `Project.addShape` 决定。 + +## 时序 + +```mermaid +sequenceDiagram + participant EditorPage + participant EditorViewModel + participant Project + participant ProjectApplication + + EditorPage->>EditorViewModel: pointerDown(x, y) + EditorViewModel->>EditorViewModel: draft + EditorPage->>EditorViewModel: pointerMove(x, y) + EditorViewModel->>EditorViewModel: draft.x2 / draft.y2 + EditorPage->>EditorViewModel: pointerUp() + EditorViewModel->>Project: addShape(shape) + EditorViewModel->>ProjectApplication: save(store, project) +``` + +`pointerDown` 要求已经打开项目,并用当前 `EditorTool` 创建 `DraftShape`。`pointerMove` 只在存在草稿时调用 `DraftShape.move`。`pointerUp` 先清掉草稿,再按工具生成 `LineShape` 或 `RectShape` 并调用 `addShape`。位移小于 2 时 `addShape` 拒绝,不保存。接受时,聚合自己记下文档快照并写入图形,然后 `save` 按页面上的 `store` 写回仓储。 diff --git a/doc/项目文档/功能模块/编辑器模块/编辑器模块(editor).md b/doc/项目文档/功能模块/编辑器模块/编辑器模块(editor).md new file mode 100644 index 0000000..444ac26 --- /dev/null +++ b/doc/项目文档/功能模块/编辑器模块/编辑器模块(editor).md @@ -0,0 +1,92 @@ +# 编辑器模块 + +## 目录 + +- [模块目录结构](#模块目录结构) +- [功能定义](#功能定义) +- [核心业务层](#核心业务层) + - [EditorViewModel](#editorviewmodel) +- [核心模型](#核心模型) +- [业务文档](#业务文档) + +## 模块目录结构 + +```text +src/features/editor/ +├── domain/ +│ ├── entities/ +│ │ └── draft_shape.ts +│ └── enum/ +│ └── editor_tool.ts +└── presentation/ + └── editor/ + ├── editor.tsx + ├── editor.scss + └── editor_view_model.ts +``` + +编辑器不定义自己的聚合和用例。文档规则在项目模块的 `Project` 上。当前工具和未落笔的草稿是编辑器自己的领域对象。 + +## 功能定义 + +编辑器把页面动作提交给已经载入的 `Project`,再交给项目用例保存。 + +- 按 `projectId` 和 `store` 载入聚合。 +- 按当前工具收集指针草稿,落笔时调用 `Project.addShape`。 +- 调用 `Project.undo` 或 `Project.redo`,再保存。 + +## 核心业务层 + +### EditorViewModel + +编辑页的状态和操作。文件:`src/features/editor/presentation/editor/editor_view_model.ts`。继承 `PageViewModel`。 + +状态:`project`、`store`、`tool`、`draft`、`errorMessage`。撤销栈不在这里。 + +| 方法 | 说明 | +|---|---| +| `open(id: string, store: string \| null): Promise` | 按 `store` 读取 `Project`,并清空 `draft` | +| `setTool(tool: EditorTool): void` | 切换 `EditorTool.LINE` 或 `EditorTool.RECT` | +| `pointerDown(x: number, y: number): void` | 在已有项目时记下草稿起点 | +| `pointerMove(x: number, y: number): void` | 更新草稿终点 | +| `pointerUp(): void` | 用草稿调用 `Project.addShape`,接受后保存 | +| `undo(): void` | 调用 `Project.undo`,成功后保存 | +| `redo(): void` | 调用 `Project.redo`,成功后保存 | + +`store` 只记录这次页面从本地还是远程打开,供 `ProjectApplication.get` 和 `save` 选择仓储。 + +## 核心模型 + +编辑器定义当前工具和未落笔的草稿。已提交的图形和撤销规则仍在项目模块的 `Project` 上,见 [项目模块](../项目模块/项目模块(project).md)。 + +| 类型 | 文件 | 定义 | +|---|---|---| +| `EditorTool` | `src/features/editor/domain/enum/editor_tool.ts` | 当前工具。`LINE` 与 `RECT` | +| `DraftShape` | `src/features/editor/domain/entities/draft_shape.ts` | 一笔尚未提交的草稿。持有工具和起点、终点 | + +```mermaid +flowchart LR + EditorViewModel((EditorViewModel)) + open([open]) + addShape([addShape]) + undo([undo]) + redo([redo]) + Project + + EditorViewModel --> open + EditorViewModel --> addShape + EditorViewModel --> undo + EditorViewModel --> redo + open --> Project + addShape --> Project + undo --> Project + redo --> Project +``` + +`open` 载入 `Project`。`addShape`、`undo`、`redo` 都调用这个聚合根,不调用 `Document`。 + +## 业务文档 + +- [打开项目](./打开项目.md) +- [绘制](./绘制.md) +- [撤销与重做](./撤销与重做.md) diff --git a/doc/项目文档/功能模块/项目模块/项目模块(project).md b/doc/项目文档/功能模块/项目模块/项目模块(project).md new file mode 100644 index 0000000..e19882b --- /dev/null +++ b/doc/项目文档/功能模块/项目模块/项目模块(project).md @@ -0,0 +1,133 @@ +# 项目模块 + +## 目录 + +- [模块目录结构](#模块目录结构) +- [功能定义](#功能定义) +- [核心业务层](#核心业务层) + - [ProjectListViewModel](#projectlistviewmodel) +- [核心模型](#核心模型) +- [核心业务](#核心业务) + - [保存项目](#保存项目) + +## 模块目录结构 + +```text +src/features/project/ +├── application/ +│ └── project_application.ts +├── domain/ +│ ├── entities/ +│ │ ├── document.ts +│ │ └── project.ts +│ ├── enum/ +│ │ └── project_store.ts +│ └── repositories/ +│ └── i_project_repository.ts +├── infra/ +│ └── persistence/ +│ ├── local_project_repository.ts +│ └── remote_project_repository.ts +├── presentation/ +│ ├── local_list/ +│ ├── project_list/ +│ ├── project_row/ +│ └── remote_list/ +└── project.ts +``` + +`project.ts` 导入 `LocalProjectRepository` 和 `RemoteProjectRepository`。两者都实现 `IProjectRepository`,由应用层按 `store` 选择。`store` 是领域枚举 `ProjectStore`,取值 `local` 与 `remote`,不是 `Project` 的字段。 + +## 功能定义 + +项目模块维护 `Project` 聚合,并按 `store` 选择仓储。 + +- `list(store)`:列出该存储位置的项目。 +- `get(store, id)`:读取一个项目。 +- `save(store, project)`:调用 `Project.touch` 后保存整个聚合。 +- `create(store, name)`:通过 `Project.create` 新建项目并保存。`local` 写入本地仓储,`remote` 写入远程仓储。 + +这些方法在 `ProjectApplication`。页面和编辑器不直接选择仓储实现。 + +## 核心业务层 + +### ProjectListViewModel + +本地列表和远程列表页的状态。文件:`src/features/project/presentation/project_list/project_list_view_model.ts`。继承 `PageViewModel`。 + +状态:`projects`、`errorMessage`。 + +| 方法 | 说明 | +|---|---| +| `load(store: ProjectStore): Promise` | 调用 `ProjectApplication.list`,写入 `projects` | + +`ProjectStore` 定义在 `domain/enum/project_store.ts`。`ProjectListPage` 在进入页面时调用 `load`。本地页传入 `ProjectStore.LOCAL`,远程页传入 `ProjectStore.REMOTE`。 + +## 核心模型 + +`Project` 是聚合根。外部只通过它修改文档和图形。 + +| 类型 | 文件 | 定义 | +|---|---|---| +| `Project` | `src/features/project/domain/entities/project.ts` | 聚合根。持有 `id`、`name`、`updatedAt`、`document`,以及撤销栈 | +| `ProjectStore` | `src/features/project/domain/enum/project_store.ts` | 存储位置。`LOCAL` 与 `REMOTE` | +| `Document` | `src/features/project/domain/entities/document.ts` | 聚合内部的图形文档,持有 `shapes` | +| `LineShape` | 同上 | 一条线段 | +| `RectShape` | 同上 | 一个矩形 | +| `IProjectRepository` | `src/features/project/domain/repositories/i_project_repository.ts` | `list`、`get`、`save`。不区分存储位置 | + +```mermaid +classDiagram + class Project { + id + name + updatedAt + create(name) + touch() + addShape(shape) + undo() + redo() + } + class Document { + shapes + } + class LineShape { + id + x1 + y1 + x2 + y2 + } + class RectShape { + id + x + y + width + height + } + Project --> Document + Document --> LineShape + Document --> RectShape +``` + +`Project.create` 生成 `id`、名称和空文档。`touch` 把 `updatedAt` 写成当前时间。 + +`addShape` 是唯一写入图形的方法。线段或矩形的位移小于 2 时拒绝,文档保持不变。通过时,先记下当前文档快照,再把图形放入 `Document`。 + +`undo` 在没有更早的快照时不做。否则把当前文档换回上一份快照。`redo` 按相反方向恢复。快照存在聚合内部,不放在页面 ViewModel 里。 + +## 核心业务 + +### 保存项目 + +```mermaid +sequenceDiagram + participant ProjectApplication + participant Project + participant IProjectRepository + + ProjectApplication->>Project: touch() + ProjectApplication->>IProjectRepository: save(project) +``` + +`ProjectApplication` 按 `store` 把 `IProjectRepository` 指到 `LocalProjectRepository` 或 `RemoteProjectRepository`,再调用 `save`。`list`、`get`、`create` 使用同一次选择。领域端口上看不到 `local` 或 `remote`。 diff --git a/doc/项目文档/功能模块/首页/首页(home).md b/doc/项目文档/功能模块/首页/首页(home).md new file mode 100644 index 0000000..ad14030 --- /dev/null +++ b/doc/项目文档/功能模块/首页/首页(home).md @@ -0,0 +1,87 @@ +# 首页 + +## 目录 + +- [模块目录结构](#模块目录结构) +- [功能定义](#功能定义) +- [核心业务层](#核心业务层) + - [HomeViewModel](#homeviewmodel) +- [核心模型](#核心模型) +- [核心业务](#核心业务) + - [列出并新建](#列出并新建) + +## 模块目录结构 + +```text +src/features/home/ +└── presentation/ + └── home/ + ├── home.tsx + ├── home.scss + └── home_view_model.ts +``` + +首页没有 `application`、`domain`、`infra`。 + +## 功能定义 + +首页不持有独立用例。它调用项目模块的 `list` 和 `create`。 + +- 进入页面时按 `local` 与 `remote` 各列出一份项目。 +- 新建时调用 `ProjectApplication.create`。聚合由 `Project.create` 生成,再按 `store` 保存。 + +已登录状态来自安全模块一侧的 `authenticated`。未登录时,`projectId` 不交给编辑器。 + +## 核心业务层 + +### HomeViewModel + +首页的项目列表状态。文件:`src/features/home/presentation/home/home_view_model.ts`。继承 `PageViewModel`。 + +状态:`cloudProjects`、`localProjects`、`errorMessage`。 + +| 方法 | 说明 | +|---|---| +| `ready(): Promise` | 进入页面时调用 `reload` | +| `reload(): Promise` | 调用 `ProjectApplication.list(ProjectStore.REMOTE)` 和 `list(ProjectStore.LOCAL)` | +| `createLocal(): Promise` | 调用 `ProjectApplication.create(ProjectStore.LOCAL)` | + +`local` 和 `remote` 是传给用例的 `store`,用来选择仓储。它们不是 `Project` 的属性。 + +## 核心模型 + +首页不定义实体。它使用项目模块的聚合根 `Project`。 + +```mermaid +flowchart LR + HomeViewModel((HomeViewModel)) + list([list]) + create([create]) + Project + + HomeViewModel --> list + HomeViewModel --> create + list --> Project + create --> Project +``` + +`list` 的结果写入 `cloudProjects` 或 `localProjects`。`create` 返回新建的 `Project`。 + +## 核心业务 + +### 列出并新建 + +```mermaid +sequenceDiagram + participant HomeViewModel + participant ProjectApplication + participant Project + + HomeViewModel->>ProjectApplication: list(remote) + HomeViewModel->>ProjectApplication: list(local) + HomeViewModel->>ProjectApplication: create(local) + ProjectApplication->>Project: create(name) + ProjectApplication-->>HomeViewModel: Project +``` + +`ready` 触发两次 `list`。其中一次失败时,对应列表为空,错误写入 `errorMessage`。`create` 返回的 `Project.id` 与本次使用的 `store` 一起交给编辑器。 diff --git a/doc/项目文档/基础应用/*基础应用文档撰写说明.md b/doc/项目文档/基础应用/*基础应用文档撰写说明.md new file mode 100644 index 0000000..c869255 --- /dev/null +++ b/doc/项目文档/基础应用/*基础应用文档撰写说明.md @@ -0,0 +1,27 @@ +# 基础应用文档撰写说明 + +适用文件:`doc/基础应用/<中文名>(目录名).md`。一个 `src/app` 子目录一篇。 + +## 目录 + +- [1. 功能定义](#1-功能定义) +- [2. 使用示例](#2-使用示例) +- [3. 数据流与时序](#3-数据流与时序) + +示例见 [基础应用(示例)](./基础应用(示例).md)。 + +## 1. 功能定义 + +一段话说明这个基础应用做什么。下列出对外入口:类或函数、文件路径、各自负责的事。 + +## 2. 使用示例 + +给出调用这个入口的代码,并注明代码所在文件。示例表现功能模块或路由怎样使用它。一个入口一段示例;入口很小且调用方式相同时可以合在一段里。 + +## 3. 数据流与时序 + +一条行为优先用时序图。时序图已经能说明调用顺序时,不要再画数据流图。数据流图只在时序图表达不了数据经过哪些对象时再补。图中只用本目录的类名、函数名、方法名。图下用中文复述这条路径。 + +本节标题按实际采用的图来写。只有时序图时,标题为「时序」。 + +不画功能模块的调用链,例如 `SecurityViewModel` 到 `SecurityApplication`。不把其他基础应用的内部步骤画进本篇。功能模块何时调用这些入口,由功能模块文档的时序图说明。使用示例里的代码片段可以来自调用方。 diff --git a/doc/项目文档/基础应用/主题(theme).md b/doc/项目文档/基础应用/主题(theme).md new file mode 100644 index 0000000..531cd8e --- /dev/null +++ b/doc/项目文档/基础应用/主题(theme).md @@ -0,0 +1,49 @@ +# 主题 + +## 目录 + +- [功能定义](#功能定义) +- [使用示例](#使用示例) +- [时序](#时序) + - [切换主题](#切换主题) + +## 功能定义 + +主题保存 `light`、`dark` 或 `system`,解析成实际的 `light` 或 `dark`,并交给界面库。 + +| 入口 | 文件 | 定义 | +|---|---|---| +| `getThemeMode()` | `src/app/theme/theme.ts` | 读取保存的 `ThemeMode`,缺省为 `system` | +| `resolveTheme(mode)` | 同上 | 把 `ThemeMode` 解析为 `ResolvedTheme` | +| `setThemeMode(mode)` | 同上 | 保存模式、涂色,并通知订阅者 | +| `subscribeTheme(listener)` | 同上 | 订阅主题变化 | +| `useThemeMode()` | `src/app/theme/use_theme.ts` | 读取当前 `ThemeMode` | +| `useResolvedTheme()` | 同上 | 读取当前 `ResolvedTheme` | +| `antdTheme(resolved)` | `src/app/theme/antd_theme.ts` | 按解析结果选择界面库算法 | + +## 使用示例 + +应用外壳把解析结果交给界面库。文件:`src/app/routes/app_shell.tsx`。 + +```ts +const resolved = useResolvedTheme(); + +``` + +## 时序 + +### 切换主题 + +```mermaid +sequenceDiagram + participant setThemeMode + participant applyTheme + participant resolveTheme + participant subscribeTheme + + setThemeMode->>applyTheme: applyTheme(mode) + applyTheme->>resolveTheme: resolveTheme(mode) + setThemeMode->>subscribeTheme: emit() +``` + +`setThemeMode` 写入本地后调用 `applyTheme`。`resolveTheme` 在模式为 `system` 时读取系统配色,否则直接采用 `light` 或 `dark`。涂色写到 `documentElement.dataset.theme`,然后通知 `subscribeTheme` 的监听者。`useResolvedTheme` 通过这个订阅更新界面。首次涂色在根组件挂载之后执行,水合完成前不改 `` 上的属性。 diff --git a/doc/项目文档/基础应用/会话(session).md b/doc/项目文档/基础应用/会话(session).md new file mode 100644 index 0000000..9dfc953 --- /dev/null +++ b/doc/项目文档/基础应用/会话(session).md @@ -0,0 +1,67 @@ +# 会话 + +## 目录 + +- [功能定义](#功能定义) +- [使用示例](#使用示例) +- [时序](#时序) + - [凭证变化](#凭证变化) + +## 功能定义 + +会话负责登录凭证的保存、读取和清除,并在凭证变化后通知界面。`AUTH_CHANGED` 只表示凭证变了,不是领域事件。登录是否成功由安全模块的 `LoginResult` 决定。 + +| 入口 | 文件 | 定义 | +|---|---|---| +| `TokenStore.save(token)` | `src/app/session/token_store.ts` | 写入凭证并调用 `notifyAuthChanged` | +| `TokenStore.get()` | 同上 | 读出凭证;没有则返回 `null` | +| `TokenStore.remove()` | 同上 | 清除凭证并调用 `notifyAuthChanged` | +| `TokenStore.has()` | 同上 | 当前是否持有凭证 | +| `hasSession()` | `src/app/session/auth_session.ts` | 从根容器取出 `TokenStore` 并调用 `has()` | +| `useAuthenticated()` | `src/app/session/use_authenticated.ts` | 订阅 `AUTH_CHANGED`,得到当前是否已登录 | +| `notifyAuthChanged()` | `src/app/session/auth_event.ts` | 发出 `AUTH_CHANGED` | +| `subscribeAuth(listener)` | 同上 | 登记 `AUTH_CHANGED` 的监听,并返回取消函数 | + +## 使用示例 + +安全模块在登录成功后写入凭证,登出时清除。文件:`src/features/security/application/security_application.ts`。 + +```ts +async login(credentials: AuthCredentials): Promise { + const result = await this.repository.login(credentials); + this.tokenStore.save(result.accessToken); +} + +async logout(): Promise { + this.tokenStore.remove(); +} +``` + +路由用 `hasSession()` 判断能否进入。文件:`src/app/routes/layouts/auth.layout.tsx`。 + +```ts +const policy = decideRoutePolicy('require-auth', hasSession(), url.pathname, url.search); +``` + +## 时序 + +### 凭证变化 + +```mermaid +sequenceDiagram + participant TokenStore + participant notifyAuthChanged + participant subscribeAuth + participant useAuthenticated + participant hasSession + + useAuthenticated->>subscribeAuth: subscribeAuth(sync) + TokenStore->>TokenStore: save(token) + TokenStore->>notifyAuthChanged: notifyAuthChanged() + notifyAuthChanged->>subscribeAuth: AUTH_CHANGED + subscribeAuth->>useAuthenticated: sync() + useAuthenticated->>hasSession: hasSession() + hasSession->>TokenStore: has() +``` + +`TokenStore.save` 和 `TokenStore.remove` 改完本地凭证后调用 `notifyAuthChanged`。`notifyAuthChanged` 发出 `AUTH_CHANGED`。`useAuthenticated` 事先通过 `subscribeAuth` 登记 `sync`,收到事件后再次调用 `hasSession`。`hasSession` 只调用 `TokenStore.has`。 diff --git a/doc/项目文档/基础应用/依赖注入(DI).md b/doc/项目文档/基础应用/依赖注入(DI).md new file mode 100644 index 0000000..e554f6d --- /dev/null +++ b/doc/项目文档/基础应用/依赖注入(DI).md @@ -0,0 +1,66 @@ +# 依赖注入 + +## 目录 + +- [功能定义](#功能定义) +- [使用示例](#使用示例) +- [时序](#时序) + - [绑定端口](#绑定端口) + +## 功能定义 + +依赖注入提供全库共用的根容器,并把功能模块的端口绑定到具体实现。容器默认单例,且对带 `@injectable` 的类自动绑定。 + +| 入口 | 文件 | 定义 | +|---|---|---| +| `rootContainer` | `src/app/di/root_container.ts` | 根容器。`autobind` 打开,`defaultScope` 为 `Singleton` | +| `bindAs(port, name?)` | `src/app/di/bind_as.ts` | 把抽象端口绑定到实现类。同一端口有多个实现时传入 `name` | + +## 使用示例 + +安全模块把 `ISecurityRepository` 绑到 `SecurityRepository`。文件:`src/features/security/infra/persistence/security_repository.ts`。 + +```ts +@bindAs(ISecurityRepository) +@injectable('Singleton') +export class SecurityRepository implements ISecurityRepository {} +``` + +`@bindAs` 写在 `@injectable` 上面。类装饰器从靠近类的那一个开始执行,`@injectable` 先写入构造参数类型,`bind(port).to(impl)` 才能读到。 + +项目模块把同一个 `IProjectRepository` 绑两次,用名称区分: + +```ts +@bindAs(IProjectRepository, ProjectStore.LOCAL) +@injectable('Singleton') +export class LocalProjectRepository implements IProjectRepository {} +``` + +`RemoteProjectRepository` 使用 `ProjectStore.REMOTE`。用例按名称注入抽象端口: + +```ts +constructor( + @inject(IProjectRepository) @named(ProjectStore.LOCAL) private localRepository: IProjectRepository, + @inject(IProjectRepository) @named(ProjectStore.REMOTE) private remoteRepository: IProjectRepository, +) {} +``` + +模块入口负责导入这些实现,绑定才会执行。文件:`src/features/security/security.ts`。 + +```ts +import './infra/persistence/security_repository'; +``` + +## 时序 + +### 绑定端口 + +```mermaid +sequenceDiagram + participant bindAs + participant rootContainer + + bindAs->>rootContainer: bind(port).to(impl).whenNamed(name) +``` + +`bindAs` 返回的装饰器在类定义时把端口绑到实现。带名称时用 `whenNamed` 区分同一端口的多个实现。同一个实现再次执行时,先卸掉上一次的绑定再绑。用例构造函数用 `@inject` 指定端口,用 `@named` 指定实现,容器创建用例时传入对应的仓储。 diff --git a/doc/项目文档/基础应用/国际化(i18n).md b/doc/项目文档/基础应用/国际化(i18n).md new file mode 100644 index 0000000..3e8033a --- /dev/null +++ b/doc/项目文档/基础应用/国际化(i18n).md @@ -0,0 +1,54 @@ +# 国际化 + +## 目录 + +- [功能定义](#功能定义) +- [使用示例](#使用示例) +- [时序](#时序) + - [注册文案](#注册文案) + +## 功能定义 + +国际化保存各模块的文案,并按当前语言取出文本。默认回退语言是 `zh`。 + +| 入口 | 文件 | 定义 | +|---|---|---| +| `registerMessages(feature, locales)` | `src/app/i18n/catalog.ts` | 把一个功能模块的 `zh`、`en` 文案写入目录 | +| `buildResources()` | 同上 | 把已登记的文案交给 i18next | +| `I18n.translate(key)` | `src/app/i18n/i18n.ts` | 按键取出当前语言的文本 | +| `I18n.changeLanguage(lng)` | 同上 | 切换 `zh` 或 `en` | +| `I18n.currentLanguage` | 同上 | 当前语言 | + +功能模块自己的键缺失时,`parseMissingKeyHandler` 会改查 `common` 下的同名键。 + +## 使用示例 + +安全模块在加载时登记文案。文件:`src/features/security/i18n/index.ts`。 + +```ts +registerMessages('security', { zh: zhCN, en: enUS }); +``` + +页面 ViewModel 取文本。文件:`src/features/security/presentation/security/security_view_model.ts`。 + +```ts +this.errorMessage = this.i18n.translate(currentForm.validationErrorKey); +``` + +## 时序 + +### 注册文案 + +```mermaid +sequenceDiagram + participant I18n + participant buildResources + participant registerMessages + participant i18n + + I18n->>buildResources: buildResources() + registerMessages->>registerMessages: bundles + registerMessages->>i18n: addResourceBundle +``` + +`I18n` 构造时用 `buildResources` 初始化。此后 `registerMessages` 写入 `bundles`;i18next 已经初始化时,再调用 `addResourceBundle`。尚未初始化时只写入 `bundles`,等 `I18n` 构造时一并载入。 diff --git a/doc/项目文档/基础应用/数据库(Database).md b/doc/项目文档/基础应用/数据库(Database).md new file mode 100644 index 0000000..57a3d7b --- /dev/null +++ b/doc/项目文档/基础应用/数据库(Database).md @@ -0,0 +1,45 @@ +# 数据库 + +## 目录 + +- [功能定义](#功能定义) +- [使用示例](#使用示例) +- [时序](#时序) + - [打开并升级](#打开并升级) + +## 功能定义 + +数据库打开名为 `local_db` 的 IndexedDB,并按 `schemaVersions` 从旧版本逐级升级。 + +| 入口 | 文件 | 定义 | +|---|---|---| +| `Database.open()` | `src/app/db/database.ts` | 打开数据库。同一时刻复用同一次打开 | +| `Database.reset()` | 同上 | 关闭进行中的打开,并删除 `local_db` | +| `schemaVersions` | `src/app/db/schema/index.ts` | 按版本排列的升级函数。`DB_VERSION` 等于它的长度 | +| `upgradeV1` | `src/app/db/schema/v1.ts` | 第一版。创建 `projects` 对象仓库,`keyPath` 为 `id` | + +## 使用示例 + +本地项目仓储在读写前打开数据库。文件:`src/features/project/infra/persistence/local_project_repository.ts`。 + +```ts +const db = await this.database.open(); +``` + +对象仓库不存在时,仓储关闭连接并调用 `Database.reset()`,再打开一次。 + +## 时序 + +### 打开并升级 + +```mermaid +sequenceDiagram + participant Database + participant schemaVersions + + Database->>Database: open() + Database->>Database: connect() + Database->>schemaVersions: upgrade(oldVersion) +``` + +`open` 若已有进行中的连接,直接返回该连接。`onupgradeneeded` 时,`upgrade` 从 `oldVersion` 走到 `newVersion`,依次执行 `schemaVersions` 里对应的函数。版本变化导致连接关闭时,下一次 `open` 重新连接。 diff --git a/doc/项目文档/基础应用/生命周期(lifecycle).md b/doc/项目文档/基础应用/生命周期(lifecycle).md new file mode 100644 index 0000000..de70432 --- /dev/null +++ b/doc/项目文档/基础应用/生命周期(lifecycle).md @@ -0,0 +1,52 @@ +# 生命周期 + +## 目录 + +- [功能定义](#功能定义) +- [使用示例](#使用示例) +- [时序](#时序) + - [页面 ViewModel](#页面-viewmodel) + +## 功能定义 + +生命周期负责创建页面 ViewModel,并在进入、刷新、离开时回调。 + +| 入口 | 文件 | 定义 | +|---|---|---| +| `PageViewModel` | `src/app/lifecycle/page_view_model.ts` | 页面 ViewModel 的基类。回调为 `created`、`ready`、`beforeRefresh`、`dispose` | +| `useViewModel(identifier)` | `src/app/lifecycle/use_view_model.ts` | 从根容器取出 ViewModel,并接上上述回调 | + +## 使用示例 + +编辑器页面取出 `EditorViewModel`。文件:`src/features/editor/presentation/editor/editor.tsx`。 + +```ts +const vm = useViewModel(EditorViewModel); +``` + +ViewModel 继承 `PageViewModel`,需要在容器里可解析。文件:`src/features/editor/presentation/editor/editor_view_model.ts`。 + +```ts +@injectable('Transient') +export class EditorViewModel extends PageViewModel {} +``` + +## 时序 + +### 页面 ViewModel + +```mermaid +sequenceDiagram + participant useViewModel + participant rootContainer + participant PageViewModel + + useViewModel->>rootContainer: get(identifier) + rootContainer-->>useViewModel: PageViewModel + useViewModel->>PageViewModel: created() + useViewModel->>PageViewModel: ready() + useViewModel->>PageViewModel: beforeRefresh() + useViewModel->>PageViewModel: dispose() +``` + +`created` 在首次取出实例时调用。`ready` 在效果提交后调用。窗口触发 `pagehide` 时调用 `beforeRefresh`。离开页面时去掉监听并调用 `dispose`。 diff --git a/doc/项目文档/基础应用/线程脚本(WebWorker).md b/doc/项目文档/基础应用/线程脚本(WebWorker).md new file mode 100644 index 0000000..6e2f722 --- /dev/null +++ b/doc/项目文档/基础应用/线程脚本(WebWorker).md @@ -0,0 +1,51 @@ +# 线程脚本 + +## 目录 + +- [功能定义](#功能定义) +- [使用示例](#使用示例) +- [时序](#时序) + - [请求与回复](#请求与回复) + +## 功能定义 + +线程脚本管理一个 Worker 的创建、请求和崩溃。当前没有功能模块调用它。 + +| 入口 | 文件 | 定义 | +|---|---|---| +| `WorkerSession.request(message)` | `src/app/worker/worker_session.ts` | 保证 Worker 存在,发送 `WorkerMessage`,并等待对应 `WorkerReply` | +| `WorkerSession.dispose()` | 同上 | 拒绝尚未完成的请求,并终止 Worker | +| `WorkerRuntime.listen(handlers)` | `src/app/worker/worker_runtime.ts` | 在 Worker 线程按 `type` 分发,并用 `WorkerReply` 回复 | +| `WorkerMessage` | `src/app/worker/entities/worker_message.ts` | 请求体。含 `id`、`type`、`payload` | +| `WorkerReply` | `src/app/worker/entities/worker_reply.ts` | 回复体。按 `id` 交回等待中的请求 | +| `WorkerError` | `src/app/worker/entities/worker_error.ts` | Worker 失败时的错误 | + +连续崩溃超过一次后,`WorkerSession` 停止再创建,后续 `request` 直接拒绝。 + +## 使用示例 + +调用方传入创建函数,再发送一条消息。 + +```ts +const worker = new WorkerSession(() => new Worker('worker.js')); +const message = new WorkerMessage('type', payload); +const result = await worker.request(message); +``` + +## 时序 + +### 请求与回复 + +```mermaid +sequenceDiagram + participant WorkerSession + participant WorkerMessage + participant WorkerReply + + WorkerSession->>WorkerSession: ensure() + WorkerSession->>WorkerMessage: toJson() + WorkerSession->>WorkerReply: fromJson(data) + WorkerReply-->>WorkerSession: unwrap() +``` + +`ensure` 没有实例时调用构造函数传入的工厂。`request` 按 `WorkerMessage.id` 登记等待。Worker 线程里的 `WorkerRuntime.listen` 按 `type` 调用处理函数,并用 `WorkerReply` 回复。`onmessage` 用 `WorkerReply.fromJson` 找到同一 `id`,再 `unwrap` 完成该请求。`onerror` 或 `onmessageerror` 时终止当前 Worker,并拒绝尚未完成的请求。 diff --git a/doc/项目文档/基础应用/网络(network).md b/doc/项目文档/基础应用/网络(network).md new file mode 100644 index 0000000..6965318 --- /dev/null +++ b/doc/项目文档/基础应用/网络(network).md @@ -0,0 +1,42 @@ +# 网络 + +## 目录 + +- [功能定义](#功能定义) +- [使用示例](#使用示例) +- [时序](#时序) + - [本站请求](#本站请求) + +## 功能定义 + +网络提供两个 HTTP 客户端。`Http` 访问本站接口,自动附带凭证,并在 401 时清除凭证。`ExternalHttp` 访问第三方接口,响应体原样返回。 + +| 入口 | 文件 | 定义 | +|---|---|---| +| `Http.instance` | `src/app/network/http.ts` | 本站 Axios 实例。请求拦截器读取凭证,响应拦截器按 `code` 解包 | +| `ExternalHttp.instance` | `src/app/network/external_http.ts` | 第三方 Axios 实例。成功时返回 `response.data` | + +`Http` 在响应 `code` 不是 200 时拒绝。状态码为 401 时调用 `TokenStore.remove`。凭证如何保存见 [会话](./会话(session).md)。 + +## 使用示例 + +安全仓储通过 `Http.instance` 提交登录。文件:`src/features/security/infra/persistence/security_repository.ts`。 + +```ts +return await this.http.instance.post('/auth/login', plainDto); +``` + +## 时序 + +### 本站请求 + +```mermaid +sequenceDiagram + participant Http + + Http->>Http: interceptors.request + Http->>Http: Authorization + Http->>Http: interceptors.response +``` + +请求拦截器读取凭证,有凭证时写入 `Authorization`。响应拦截器在 `code` 为 200 时返回 `data`,否则拒绝。状态码为 401 时清除凭证并离开当前页。`ExternalHttp` 不写入 `Authorization`,也不读取 `code`。 diff --git a/doc/项目文档/基础应用/路由(routes).md b/doc/项目文档/基础应用/路由(routes).md new file mode 100644 index 0000000..88d884a --- /dev/null +++ b/doc/项目文档/基础应用/路由(routes).md @@ -0,0 +1,54 @@ +# 路由 + +## 目录 + +- [功能定义](#功能定义) +- [使用示例](#使用示例) +- [时序](#时序) + - [访问策略](#访问策略) + +## 功能定义 + +路由把 URL 指到功能模块页面,并用布局策略决定未登录或已登录时能否进入。 + +| 入口 | 文件 | 定义 | +|---|---|---| +| 路由表 | `src/app/routes.ts` | 声明页面路径和所属布局 | +| `decideRoutePolicy` | `src/app/routes/layouts/decide_route_policy.ts` | 根据 `require-auth` 或 `require-guest` 返回 `allowed` 或 `denied` | +| `AuthLayout` | `src/app/routes/layouts/auth.layout.tsx` | 已登录区域。未登录时拒绝 | +| `GuestLayout` | `src/app/routes/layouts/guest.layout.tsx` | 访客区域。已登录时拒绝 | + +路由表: + +| 路径 | 布局 | 页面 | +|---|---|---| +| `/` | `app_shell` | `home` | +| `/security` | `guest` | `security` | +| `/profile` | `auth` | `security` 的资料页 | +| `/projects/remote` | `auth` | `project` | +| `/projects/local` | `auth` | `project` | +| `/editor/:projectId` | `auth` | `editor` | + +## 使用示例 + +已登录布局把 `hasSession()` 交给策略。文件:`src/app/routes/layouts/auth.layout.tsx`。 + +```ts +const policy = decideRoutePolicy('require-auth', hasSession(), url.pathname, url.search); +if (policy.status === 'denied') throw redirect(policy.redirect.to); +``` + +## 时序 + +### 访问策略 + +```mermaid +sequenceDiagram + participant AuthLayout + participant decideRoutePolicy + + AuthLayout->>decideRoutePolicy: decideRoutePolicy(require-auth, authenticated, pathname, search) + decideRoutePolicy-->>AuthLayout: denied +``` + +`require-auth` 且未登录时,结果是 `denied`,`redirect.to` 指向 `/security`,并带上当前路径。`require-guest` 且已登录时,`redirect.to` 为 `/`。其余情况为 `allowed`。`GuestLayout` 使用 `require-guest`,步骤相同。 diff --git a/doc/项目文档/项目架构.md b/doc/项目文档/项目架构.md new file mode 100644 index 0000000..d1c6e45 --- /dev/null +++ b/doc/项目文档/项目架构.md @@ -0,0 +1,96 @@ +# 项目架构 + +## 目录 + +- [项目目录结构](#项目目录结构) +- [模块定义](#模块定义) + - [基础应用](#基础应用) + - [功能模块](#功能模块) +- [模块间数据流](#模块间数据流) + - [项目业务](#项目业务) + +## 项目目录结构 + +本仓库是脚手架。`src/app` 是每个项目保留的基础应用。`src/features` 下的四个目录是示例业务,用来示范功能模块如何接入;新项目可以替换它们。 + +```text +src/ +├── app/ +│ ├── db/ +│ ├── di/ +│ ├── i18n/ +│ ├── lifecycle/ +│ ├── network/ +│ ├── routes/ +│ ├── session/ +│ ├── theme/ +│ ├── worker/ +│ ├── entry.client.tsx +│ ├── root.tsx +│ └── routes.ts +├── assets/ +└── features/ + ├── editor/ + ├── home/ + ├── project/ + └── security/ +``` + +## 模块定义 + +`src/app` 下每个目录是一个基础应用,供功能模块和路由使用。`src/features` 下每个目录是一个功能模块,承载一块业务。功能模块的定义写它的 application 用例。 + +### 基础应用 + + +| 模块 | 目录 | 定义 | +| --------------------------------- | ------------------- | ---------------------------- | +| [数据库](./基础应用/数据库(Database).md) | `src/app/db` | 打开本地库并按版本升级表结构 | +| [依赖注入](./基础应用/依赖注入(DI).md) | `src/app/di` | 提供根容器,并把端口绑定到实现 | +| [国际化](./基础应用/国际化(i18n).md) | `src/app/i18n` | 注册文案并按当前语言取出文本 | +| [生命周期](./基础应用/生命周期(lifecycle).md) | `src/app/lifecycle` | 创建页面 ViewModel,并在进入、刷新、离开时回调 | +| [网络](./基础应用/网络(network).md) | `src/app/network` | 提供 HTTP 客户端,附带凭证并处理 401 | +| [路由](./基础应用/路由(routes).md) | `src/app/routes` | 把 URL 指到页面,并按会话决定能否进入 | +| [会话](./基础应用/会话(session).md) | `src/app/session` | 保存、读取、清除登录凭证,并通知登录态变化 | +| [主题](./基础应用/主题(theme).md) | `src/app/theme` | 解析当前主题,并交给界面库 | +| [线程脚本](./基础应用/线程脚本(WebWorker).md) | `src/app/worker` | 封装 Worker 的消息会话 | + + + + +### 功能模块 + + +| 模块 | 目录 | 定义 | +| ------------------------------------ | ----------------------- | ---------------------------- | +| [编辑器](./功能模块/编辑器模块/编辑器模块(editor).md) | `src/features/editor` | 把指针和当前工具提交给项目聚合,不持有文档规则 | +| [首页](./功能模块/首页/首页(home).md) | `src/features/home` | 不持有独立用例,调用项目模块按存储位置列出与新建 | +| [项目](./功能模块/项目模块/项目模块(project).md) | `src/features/project` | 维护项目聚合:创建、列出、读取、保存,以及落笔、撤销和重做 | +| [安全](./功能模块/安全模块/安全模块(security).md) | `src/features/security` | 提供身份用例:注册、登录和登出。登录得到 `User` | + + + + +## 模块间数据流 + + + +### 项目业务 + +```mermaid +flowchart LR + security + home + project + editor + + security -->|authenticated| home + home -->|list / create| project + project -->|projectId / store| home + home -->|projectId / store| editor + editor -->|get / save| project +``` + +`home` 先读取 `security` 的 `authenticated`。已登录时,`home` 通过 `list` 或 `create` 向 `project` 要项目,拿到 `projectId` 和 `store` 后再交给 `editor`。未登录时,`projectId` 无法流转至 `editor`。 + +`store` 只表示应用层选择本地仓储还是远程仓储,不是 `Project` 的字段。进入 `editor` 后,`get` 载入聚合。落笔、撤销和重做由 `Project` 执行,再经 `save` 写回。 \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..973e273 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import { defineConfig, globalIgnores } from 'eslint/config'; + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + }, +]); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..435ac65 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4897 @@ +{ + "name": "bq_react", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bq_react", + "version": "0.0.0", + "dependencies": { + "@ant-design/icons": "^6.3.4", + "antd": "^6.6.7", + "axios": "^1.20.0", + "i18next": "^26.5.10", + "i18next-browser-languagedetector": "^8.2.2", + "inversify": "^8.2.3", + "isbot": "^5", + "mobx": "^7.1.1", + "mobx-keystone": "^2.4.1", + "mobx-react-lite": "^5.1.0", + "react": "^19.3.1", + "react-dom": "^19.3.1", + "react-i18next": "^17.1.8", + "react-router": "^8.5.0", + "reflect-metadata": "^0.2.2", + "sass": "^1.106.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@react-router/dev": "^8.5.0", + "@types/node": "^24.19.2", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.2", + "eslint": "^10.13.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.7", + "globals": "^17.13.0", + "prettier": "^3.9.10", + "typescript": "~6.0.3", + "typescript-eslint": "^8.71.2", + "vite": "^8.3.4" + } + }, + "node_modules/@ant-design/colors": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-8.0.1.tgz", + "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", + "integrity": "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.2.tgz", + "integrity": "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^2.1.2", + "@babel/runtime": "^7.23.2", + "@rc-component/util": "^1.4.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-3.0.1.tgz", + "integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==", + "license": "MIT", + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "6.3.4", + "resolved": "https://registry.npmmirror.com/@ant-design/icons/-/icons-6.3.4.tgz", + "integrity": "sha512-kDoUlppczVyCUTFthF6cHmPzMBYqNZQhu9j6EyIX/YIdM9j9CZNZoJUjzVw7/1v1VO8WwVMmPp15r/uwIAS0AQ==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/icons-svg": "^4.6.0", + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.6.0.tgz", + "integrity": "sha512-PRomU725ABMf/lnQp5HiB7my1kjEbFY0D10N4lXYxK6TIB1gKjIVD5MRThpDaezLgw1D774J8eOeeDB0M6wHrQ==", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-2.0.0.tgz", + "integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "clsx": "^2.1.1", + "json2mq": "^0.2.0", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "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/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.11", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.11", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.11.tgz", + "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.10", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.10", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmmirror.com/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inversifyjs/common": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@inversifyjs/common/-/common-2.0.1.tgz", + "integrity": "sha512-pJAR4IAcT2jkYfZ9bD9XhtUDBLJRr8QOiSjb+2XyaHru6DLvu0VD2Id2iP7+tVRKkEe3XFUwDUEdKxcYlF699Q==", + "license": "MIT" + }, + "node_modules/@inversifyjs/container": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/@inversifyjs/container/-/container-3.1.3.tgz", + "integrity": "sha512-+CZLXKzU5uwDbkY1WamY1Gnbtz0sNl62cqfd1vvI13Ei8Fe29sCwgfyEWMOzYcJhs2y2wBmYGANPDH5yq4VdTg==", + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "2.0.1", + "@inversifyjs/core": "15.0.1", + "@inversifyjs/plugin": "0.3.1", + "@inversifyjs/reflect-metadata-utils": "1.5.0" + }, + "peerDependencies": { + "reflect-metadata": "~0.2.2" + } + }, + "node_modules/@inversifyjs/core": { + "version": "15.0.1", + "resolved": "https://registry.npmmirror.com/@inversifyjs/core/-/core-15.0.1.tgz", + "integrity": "sha512-4C/hLEdYo3xG0Ez61kTP3nikINmCQz1O3OHU7dfbi9UC6Of5Qly2CLd0OSoZd6M8fumk51A0tuBxPIqs28CpNg==", + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "2.0.1", + "@inversifyjs/prototype-utils": "0.2.1", + "@inversifyjs/reflect-metadata-utils": "1.5.0" + } + }, + "node_modules/@inversifyjs/plugin": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/@inversifyjs/plugin/-/plugin-0.3.1.tgz", + "integrity": "sha512-ByklTw731fydBCTMwMpkmwm+lv0U+JWm9NEqRsz3n5KzAC5Om2XtLjqzEC2w+8Ote3gVC3Qxsx6YmG9XLIZpvg==", + "license": "MIT" + }, + "node_modules/@inversifyjs/prototype-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/@inversifyjs/prototype-utils/-/prototype-utils-0.2.1.tgz", + "integrity": "sha512-53cVE3cw+RxnSkGlg+jOFNSox2owJF9Fv3HgFKe4f+4aPullscltIiio88QRkx2Sc5yo3VlqPsXQFGw2CVJZnw==", + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "2.0.1" + } + }, + "node_modules/@inversifyjs/reflect-metadata-utils": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-1.5.0.tgz", + "integrity": "sha512-NpJVbRbuQ6Ao2vO+aw96un3oHDFCwXI0+pplsFt0Jh0gyR8DWk4m7ml/GBNMjdbeKVW/QgJ2S6NGXjk042uwqg==", + "license": "MIT", + "peerDependencies": { + "reflect-metadata": "~0.2.2" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@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/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.155.0", + "resolved": "https://registry.npmmirror.com/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-6.0.0.tgz", + "integrity": "sha512-D3AGQwdyE58gmvx6waVSXJ80JGO+IY5L2O8HDnSOex7JNlzB3GuN/4hyHNTdhy2qtOhkpbIjmeAN3tL993wKbA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/cascader": { + "version": "1.22.0", + "resolved": "https://registry.npmmirror.com/@rc-component/cascader/-/cascader-1.22.0.tgz", + "integrity": "sha512-SffrA57aS9oub3VuI7ajPhJTPtaNxngSvtRhD40Rd8dwJ5vfWPSrVanWgeepdWFGBt7EHftIK5RUU0u3rCTwWw==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.10.0", + "@rc-component/tree": "~1.4.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/checkbox": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@rc-component/checkbox/-/checkbox-2.0.0.tgz", + "integrity": "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/collapse": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@rc-component/collapse/-/collapse-1.2.0.tgz", + "integrity": "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@rc-component/color-picker/-/color-picker-3.1.1.tgz", + "integrity": "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/@rc-component/context/-/context-2.0.2.tgz", + "integrity": "sha512-uiGpAlblCNlziHPwj4S4Iy/oemeuz/hR03mbiEjTCXwsqOIN3BOzsRMyDwpyO5Fm0vIEEJRUf9ZtbRLbhksuTA==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dialog": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/@rc-component/dialog/-/dialog-1.10.0.tgz", + "integrity": "sha512-eDukNlz9vNszAGv7i3zKXdxEd3wgVmNxuJijYt8zvTh17QwTu8KK/bdURRd/lU4qaMzhO1HKKmMrwOnkaw0BvQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.3.3", + "@rc-component/portal": "^2.1.0", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/drawer": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/@rc-component/drawer/-/drawer-1.4.2.tgz", + "integrity": "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.1.3", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dropdown": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rc-component/dropdown/-/dropdown-1.0.3.tgz", + "integrity": "sha512-YTST/N6kpqpDz3IMuM/PSSZnrDpSOA6dgHv12gPA90ZTSLv2CoqkZ0+9NtwTY6BeO7dstPblSic2QJg7dSFy/g==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/@rc-component/form": { + "version": "1.8.6", + "resolved": "https://registry.npmmirror.com/@rc-component/form/-/form-1.8.6.tgz", + "integrity": "sha512-1EXVsSKPZC6kGrIqxVck7kAWM6T65b6et/n5wIcyiaIa41VrDshD0nVLHoBDEIZr2ATQsOP6icb4XQkNFMLBAw==", + "license": "MIT", + "dependencies": { + "@rc-component/async-validator": "^6.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/image": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/@rc-component/image/-/image-1.10.0.tgz", + "integrity": "sha512-BjeZCRQ+hw+4WAhvrw8rJvy5fckA2xpf/X2XQEOABUHvLTNB9inB98X3Mp54jYQ7g10DfWERQWHXeC4ylxp1Uw==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/portal": "^2.1.2", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/input": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/@rc-component/input/-/input-1.3.1.tgz", + "integrity": "sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig==", + "license": "MIT", + "dependencies": { + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/input-number": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/@rc-component/input-number/-/input-number-1.6.2.tgz", + "integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==", + "license": "MIT", + "dependencies": { + "@rc-component/mini-decimal": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/listy": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@rc-component/listy/-/listy-1.2.3.tgz", + "integrity": "sha512-IXiMjV5s0rczLBlfh7G5nB4M3365mrEeedjwKtf5I+Ns3PqRUsebR2h5u8CeFarsVfLUPC2I5p0h09TNoOWyvQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.3.1", + "@rc-component/virtual-list": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/mentions": { + "version": "1.12.0", + "resolved": "https://registry.npmmirror.com/@rc-component/mentions/-/mentions-1.12.0.tgz", + "integrity": "sha512-v5MBx8zDcqCT+uybJgKpcx3Fgrvj0m3u+A/15RlSBaMx12lLoJZTTYggC3pbYYfohcKujWWUyBz6O6Gp2vGiNA==", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.3.1", + "@rc-component/menu": "~1.5.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/menu": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@rc-component/menu/-/menu-1.5.0.tgz", + "integrity": "sha512-RjvzPsZkwVEg3xu+/jDDqDeUCBJAnWylONFlGeQgg5enlNAnlpybqhaMGE/UPEIvr6hL90PGhjFBguJqj0Qb2Q==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@rc-component/mini-decimal/-/mini-decimal-1.1.4.tgz", + "integrity": "sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/motion": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@rc-component/motion/-/motion-1.3.3.tgz", + "integrity": "sha512-Xh3IszxvlSv3/PLYFyC2UZi9LNB83yOnkB/LNmRzaypZLvkhqUIPS7MQpGZcCMWrNsXV2p6YTSWbSGvFpEle9A==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz", + "integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/notification": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/@rc-component/notification/-/notification-2.1.1.tgz", + "integrity": "sha512-HD+Gx/HUjivBI6GZtfPVLjvOJZybSEIoE2upnmoVg4w9vZQHMaIPWP6B52cHWOGg+u48HBbO+HnyVNN8AKQxgQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.13.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/overflow": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rc-component/overflow/-/overflow-1.0.1.tgz", + "integrity": "sha512-syfmgAABaHCnCDzPwHZ/2tuvIcpOO3jefYZMmfkN+pmo8HKTzsfhS57vxo4ksPdN0By+uWVJhJWNFozNBxi2eA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/pagination": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@rc-component/pagination/-/pagination-1.4.0.tgz", + "integrity": "sha512-CW1g7P9V8u+e8JQdUsl2RWg+GCsoee0mtJjZUCCxn/vb3jzOwDKm6hAdwddHCVBfWJ58eGUBZz3IvnU8rRktjw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/picker": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/@rc-component/picker/-/picker-1.12.2.tgz", + "integrity": "sha512-ZB1A+tzTQkgtR4flPrAMvxIAOelIIOOOsp22fU2Nad6PbZcJacnyGJUJqUH7ZS9mwxeUwurf1pLMKXrVsK1tlA==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/trigger": "^3.6.15", + "@rc-component/util": "^1.13.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/@rc-component/portal": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/@rc-component/portal/-/portal-2.2.1.tgz", + "integrity": "sha512-ck+r1kW/JSv0wxPji3KN2ss9K6Z0qqwusw/mf/0JobXhZ8hC2ejZwCJObW/SvDi0uhA0VzmCnx0CaCci95tcmA==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/progress": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rc-component/progress/-/progress-1.0.3.tgz", + "integrity": "sha512-Q1nPLIvKr95el/q76yaBh5nv1hqCsWaHQb8U7vXEDMefEhHgM7n3CPoN+o4LHnTyANQHmMFgoS/naAT4Bhxuaw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@rc-component/qrcode/-/qrcode-2.0.0.tgz", + "integrity": "sha512-aAv3QhPP1xyafuTZOxub6a54pCeBnN3IwQkpETrBtthq4BL5IgxnCbuoBWPDpdLw1y1j6BgBUCAKV92+yX06Dw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/rate": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@rc-component/rate/-/rate-1.0.1.tgz", + "integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/resize-observer": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@rc-component/resize-observer/-/resize-observer-1.1.2.tgz", + "integrity": "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/segmented": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@rc-component/segmented/-/segmented-1.4.0.tgz", + "integrity": "sha512-NZlpKGSplOYlmF5ukUF1fCLr0ns5WfjNoqMlMkf4AHP0iu90wyiP7hn2qZKRHEuRlG4CvEEz/lF9qRiYnLTofQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^8.0.0", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/segmented/node_modules/@babel/runtime": { + "version": "8.0.8", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-8.0.8.tgz", + "integrity": "sha512-dEu8nGlRqjsJsvviUUoSMfcRHpwomCiRlXsc2mPfJ79+ffTpDm+JtfUtTMzUrqCApHQnCsVPD3gT1ZT8RgBRCQ==", + "license": "MIT" + }, + "node_modules/@rc-component/select": { + "version": "1.10.1", + "resolved": "https://registry.npmmirror.com/@rc-component/select/-/select-1.10.1.tgz", + "integrity": "sha512-H+yQsl+qED9NilQ3g6zdpsMwUgwVjrcMTkNHAWRVU/MoNCYgTbDgU+MIMgZDK+rVdd2JUfI/MkysMcZZ0cyQKw==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.2.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/slider": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@rc-component/slider/-/slider-1.1.1.tgz", + "integrity": "sha512-LSzgWGYDgeCDgR4r1XlU29gbYws6HpLnvJd/uMhLeW/vQgxldeR+Wb4uzHDCHiYEbr1bnEHWdjkPxjJRHxuiig==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/steps": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@rc-component/steps/-/steps-1.2.3.tgz", + "integrity": "sha512-/b9gHcvDGjTDWJZW7+1kKFvzs0jrEeQ8mFvvIgX+h7xNT0FsOYODJYKjyJR4fPbvMBCfCOEqaBK66HBxov5BQA==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/switch": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@rc-component/switch/-/switch-1.0.3.tgz", + "integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/table": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@rc-component/table/-/table-1.11.1.tgz", + "integrity": "sha512-OWdS6DMmeWb7bJBGqPxYZpQbzBlBiXZUu2sqo6Ii7Sjs9GeK1IsrXrWk26SL2c6KEseabswdxrRj7WUm9LdECw==", + "license": "MIT", + "dependencies": { + "@rc-component/context": "^2.0.1", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tabs": { + "version": "1.13.0", + "resolved": "https://registry.npmmirror.com/@rc-component/tabs/-/tabs-1.13.0.tgz", + "integrity": "sha512-3FXr+9ZII8tFLWuDWrgjRaxZd2mazkKJAGRFEfVmp4r1qnBglOlxQ78l54Td1aj6/iFaaEReKSAF2HToztU5uw==", + "license": "MIT", + "dependencies": { + "@rc-component/dropdown": "~1.0.0", + "@rc-component/menu": "~1.5.0", + "@rc-component/motion": "^1.1.3", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tooltip": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/@rc-component/tooltip/-/tooltip-1.5.2.tgz", + "integrity": "sha512-VLq4BclU3LgRe46g2hTr2bdsM8tFYs7dVwpuD88rPNNg4lCGe8mBSS4FiRzYzS+ffnZ46y4SI8KYKo9K7qBnhw==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.10.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/@rc-component/tour/-/tour-2.4.0.tgz", + "integrity": "sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg==", + "license": "MIT", + "dependencies": { + "@rc-component/portal": "^2.2.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.7.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tree": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@rc-component/tree/-/tree-1.4.0.tgz", + "integrity": "sha512-dGsJGDJQedA0BqqVgj3F8BvHXTSZijyhTXdbAdkcx8lynzZkty/CV3Z3LOm/fxz+BCfl3dfGiAQpb7Q5XNvl0Q==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.2.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/tree-select": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/@rc-component/tree-select/-/tree-select-1.16.1.tgz", + "integrity": "sha512-a1Oi6EJhqAhdOxxupdJi6fP0RPHMKn5TcfkX2+llaQ4lF4nwfH7b6SCHcnsybaa2s+pk1yZYwVyeOYkDnEBRdg==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.10.0", + "@rc-component/tree": "~1.4.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/trigger": { + "version": "3.10.1", + "resolved": "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-3.10.1.tgz", + "integrity": "sha512-mXlDN0IXdtV8Yqqm8195ECCyrbmfvvfKvwVvSlH0+qvKD6BUF8gRhEjSy0FOcD1+CcDRHgTiX99LoxfQrmh3Cw==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.3.3", + "@rc-component/portal": "^2.2.1", + "@rc-component/resize-observer": "^1.1.2", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/upload": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@rc-component/upload/-/upload-1.1.1.tgz", + "integrity": "sha512-GvYWSKeaJTOxxC5p6+nOSadzfvXA1h8C/iHFPFZX+szH3JUXrvs+DLiW8YUTBgvMh8m63mJeHrlYlJzAlg+pDA==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/util": { + "version": "1.14.0", + "resolved": "https://registry.npmmirror.com/@rc-component/util/-/util-1.14.0.tgz", + "integrity": "sha512-qFpkGBCiyN3NNAU+FOnZdcdz7A/rXCg3JdQk+bujUKAExZgRqzf2yy1f4YN63Lwctic5FcK9rgjQ6aWbykqwEw==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^19.2.7" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/virtual-list": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@rc-component/virtual-list/-/virtual-list-1.6.0.tgz", + "integrity": "sha512-aHPlTWVtfgDmzb4oJNrqBW1yeEdFeIS28EoJLQ/dGBgZPRlrQ84c2Rk/Il6JxDUV5P7FDtNFD0aXc7EB22kR3Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^8.0.0", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/virtual-list/node_modules/@babel/runtime": { + "version": "8.0.8", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-8.0.8.tgz", + "integrity": "sha512-dEu8nGlRqjsJsvviUUoSMfcRHpwomCiRlXsc2mPfJ79+ffTpDm+JtfUtTMzUrqCApHQnCsVPD3gT1ZT8RgBRCQ==", + "license": "MIT" + }, + "node_modules/@react-router/dev": { + "version": "8.5.0", + "resolved": "https://registry.npmmirror.com/@react-router/dev/-/dev-8.5.0.tgz", + "integrity": "sha512-SXmL0ym9Wp8Hi0LdXLG6yPaN8V1rUW4PuavvBS9eqpr0hM3PFjiCUT44pRzCj4G9lKEQBeNcj4beOtpN7IxuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@react-router/node": "8.5.0", + "@remix-run/node-fetch-server": "^0.14.1", + "babel-dead-code-elimination": "^1.0.12", + "chokidar": "^5.0.0", + "dedent": "^1.7.2", + "es-module-lexer": "^2.1.0", + "exit-hook": "5.1.0", + "isbot": "^5.1.40", + "jsesc": "3.1.0", + "lodash": "^4.18.1", + "p-map": "^7.0.4", + "pathe": "^2.0.3", + "picocolors": "^1.1.1", + "pkg-types": "^2.3.1", + "react-refresh": "^0.18.0", + "semver": "^7.8.1", + "tinyglobby": "^0.2.16", + "valibot": "^1.4.1" + }, + "bin": { + "react-router": "bin.cjs" + }, + "engines": { + "node": ">=22.22.0" + }, + "peerDependencies": { + "@react-router/serve": "^8.5.0", + "@vitejs/plugin-rsc": "~0.5.26", + "react-router": "^8.5.0", + "react-server-dom-webpack": "^19.2.7", + "typescript": "^5.1.0 || ^6.0.0 || ^7.0.0", + "vite": "^7.0.0 || ^8.0.0", + "wrangler": "^4.0.0" + }, + "peerDependenciesMeta": { + "@react-router/serve": { + "optional": true + }, + "@vitejs/plugin-rsc": { + "optional": true + }, + "react-server-dom-webpack": { + "optional": true + }, + "typescript": { + "optional": true + }, + "wrangler": { + "optional": true + } + } + }, + "node_modules/@react-router/dev/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-router/node": { + "version": "8.5.0", + "resolved": "https://registry.npmmirror.com/@react-router/node/-/node-8.5.0.tgz", + "integrity": "sha512-9a3a37TW5GAo/J336Uk1tca/X1OxjpseETz/zCHcojtL7oRlFrlFoDt6wSdCtFRCiwlLyHcWY+qwZ+xnb05kSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@remix-run/node-fetch-server": "^0.14.1" + }, + "engines": { + "node": ">=22.22.0" + }, + "peerDependencies": { + "react-router": "8.5.0", + "typescript": "^5.1.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@remix-run/node-fetch-server": { + "version": "0.14.2", + "resolved": "https://registry.npmmirror.com/@remix-run/node-fetch-server/-/node-fetch-server-0.14.2.tgz", + "integrity": "sha512-1cpM/f5i1ka/9CUMBwZXZp7Bn92x8VQjzg+F4eEe88DlVdNHkAKrf/vvbZoYgdkfy2bWHtGC6LQGLmojEqOF4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@remix-run/route-pattern": { + "version": "0.22.1", + "resolved": "https://registry.npmmirror.com/@remix-run/route-pattern/-/route-pattern-0.22.1.tgz", + "integrity": "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", + "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", + "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", + "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", + "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", + "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", + "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", + "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", + "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", + "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", + "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", + "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", + "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", + "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", + "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", + "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", + "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.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.2", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.72.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.0.tgz", + "integrity": "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/type-utils": "8.72.0", + "@typescript-eslint/utils": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.72.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.14", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.72.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.72.0.tgz", + "integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.72.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.72.0.tgz", + "integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.72.0", + "@typescript-eslint/types": "^8.72.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.72.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz", + "integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.72.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz", + "integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.72.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz", + "integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.72.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.72.0.tgz", + "integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.72.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz", + "integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.72.0", + "@typescript-eslint/tsconfig-utils": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.72.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.72.0.tgz", + "integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.72.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz", + "integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", + "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", + "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", + "oxc-transform-react": "^0.152.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/antd": { + "version": "6.6.7", + "resolved": "https://registry.npmmirror.com/antd/-/antd-6.6.7.tgz", + "integrity": "sha512-6D9oSAot26LIHYmR0ZyquOZY4WIcgRurZ/9//1GEJoo/vWvKV3E1PJPmB3orGkRlmGxArX3vSJFxQfRKr5qHZA==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/cssinjs": "^2.1.2", + "@ant-design/cssinjs-utils": "^2.1.2", + "@ant-design/fast-color": "^3.0.1", + "@ant-design/icons": "^6.3.4", + "@ant-design/react-slick": "~2.0.0", + "@babel/runtime": "^7.29.2", + "@rc-component/cascader": "~1.22.0", + "@rc-component/checkbox": "~2.0.0", + "@rc-component/collapse": "~1.2.0", + "@rc-component/color-picker": "~3.1.1", + "@rc-component/dialog": "~1.10.0", + "@rc-component/drawer": "~1.4.2", + "@rc-component/dropdown": "~1.0.3", + "@rc-component/form": "~1.8.6", + "@rc-component/image": "~1.10.0", + "@rc-component/input": "~1.3.1", + "@rc-component/input-number": "~1.6.2", + "@rc-component/listy": "~1.2.3", + "@rc-component/mentions": "~1.12.0", + "@rc-component/menu": "~1.5.0", + "@rc-component/motion": "^1.3.3", + "@rc-component/mutate-observer": "^2.0.1", + "@rc-component/notification": "~2.1.1", + "@rc-component/pagination": "~1.4.0", + "@rc-component/picker": "~1.12.2", + "@rc-component/progress": "~1.0.3", + "@rc-component/qrcode": "~2.0.0", + "@rc-component/rate": "~1.0.1", + "@rc-component/resize-observer": "^1.1.2", + "@rc-component/segmented": "~1.4.0", + "@rc-component/select": "~1.10.1", + "@rc-component/slider": "~1.1.1", + "@rc-component/steps": "~1.2.3", + "@rc-component/switch": "~1.0.3", + "@rc-component/table": "~1.11.1", + "@rc-component/tabs": "~1.13.0", + "@rc-component/tooltip": "~1.5.2", + "@rc-component/tour": "~2.4.0", + "@rc-component/tree": "~1.4.0", + "@rc-component/tree-select": "~1.16.1", + "@rc-component/trigger": "^3.10.1", + "@rc-component/upload": "~1.1.1", + "@rc-component/util": "^1.14.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.11", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/babel-dead-code-elimination": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.12.tgz", + "integrity": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.32", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.32.tgz", + "integrity": "sha512-UvuxFU9P3hM29LrQdwu0cD6w6/+SFr+UlB9s/knI7+O2rK9xFxKUwSDWxGdbbqQ9/V7BJ5s0NI5nQAE4uJp3gQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.29.4", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.29.4.tgz", + "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.29", + "caniuse-lite": "^1.0.30001816", + "electron-to-chromium": "^1.5.459", + "node-releases": "^2.0.59", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001819", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", + "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.3.1.tgz", + "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.464", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz", + "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.13.0", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-10.13.0.tgz", + "integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.7", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.7.tgz", + "integrity": "sha512-XhJSzLljuYD4UjNuFGJu2v7aD3sqNVK12w5/DCUfrNfHWegZo2+TsavNx8MuxMwEquuFAzNbNfXKU0WoZ+YUIg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exit-hook": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/exit-hook/-/exit-hook-5.1.0.tgz", + "integrity": "sha512-INjr2xyxHo7bhAqf5ong++GZPPnpcuBcaXUKt03yf7Fie9yWD7FapL4teOU0+awQazGs5ucBh7xWs/AD+6nhog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exsolve": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.1.3.tgz", + "integrity": "sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/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/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.1.tgz", + "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.13.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-17.13.0.tgz", + "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmmirror.com/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-parse-stringify": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-4.0.1.tgz", + "integrity": "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==", + "license": "MIT", + "funding": { + "url": "https://locize.com" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/i18next": { + "version": "26.5.10", + "resolved": "https://registry.npmmirror.com/i18next/-/i18next-26.5.10.tgz", + "integrity": "sha512-YS4EiLQhhk0wGThm4Zy+BL2c9rBNIK+kAbksi4l/B+D6kVRQLIFZgu2b/MU2LibJJHwWNTYoOYR52T+jfV8kPQ==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.2", + "resolved": "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.2.tgz", + "integrity": "sha512-99PKPDVeLj7s4nDJAz+z664/6IWEb2EyssTsknyzc1MY+mmtAXpx3BP862MrKLGRtmSJnlUioPuOE22F+RR+ng==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "license": "MIT" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inversify": { + "version": "8.2.3", + "resolved": "https://registry.npmmirror.com/inversify/-/inversify-8.2.3.tgz", + "integrity": "sha512-OmTj/HB/lEV6WFaeHXBDXdUYWwtadF8QTbyEKT7LUZMzOkw5xF+Nrm1GTjIhBOrAUN4K65vtqKgxRWlSHzQH7w==", + "license": "MIT", + "dependencies": { + "@inversifyjs/common": "2.0.1", + "@inversifyjs/container": "3.1.3", + "@inversifyjs/core": "15.0.1" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "license": "MIT" + }, + "node_modules/isbot": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/isbot/-/isbot-5.2.2.tgz", + "integrity": "sha512-iQcBXcd+Rv/pkubRyGh2utW2j1oPG5hZY6TUhVPpqK4G+o3IbxpJNx04hgksjc/N7GK5pEorUxDeg31cFgEk/w==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mobx": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/mobx/-/mobx-7.1.1.tgz", + "integrity": "sha512-IBnj1k7aoSlpNcuI5tr3/nykyDIUzm0+7Zu9IKBJn0IhJgeIKp26KLAsCFvCPpDmcjiszfg5XOeTJTQi/JoEOw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + } + }, + "node_modules/mobx-keystone": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/mobx-keystone/-/mobx-keystone-2.4.1.tgz", + "integrity": "sha512-jPK/sL16dvHcnLLr8rnNvku3ez+NIEMfPHmyH+8bs9hAuZoIk4DrPVpcyvrnyh+OUDqhTkkF7eeGPLi5cxzW3g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "ts-toolbelt": "^9.6.0", + "tslib": "^2.8.1" + }, + "peerDependencies": { + "mobx": "^7.0.0 || ^6.0.0 || ^5.0.0 || ^4.0.0" + } + }, + "node_modules/mobx-react-lite": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/mobx-react-lite/-/mobx-react-lite-5.1.0.tgz", + "integrity": "sha512-++LQTCRr4FJCWuaEvYl5gCtGBwOpOwrnWjm9uhCUDI5nmkzLR8eT1jgMeYfJ8wc88YOYyC/ygjw2KpYPBYGsww==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^7.0.0", + "react": "^18 || ^19" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.60", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.60.tgz", + "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/p-map/-/p-map-7.1.1.tgz", + "integrity": "sha512-6Lh3jQPLthYujSrlw6ccWXBGZTkoTCz27Hd92g79LH/kjAbgoEu0t442cCCNbegPSJsA0lwDX8z0bcKtkpA32g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.4.0.tgz", + "integrity": "sha512-07Fys7iVx0X6TcviMOgogM1Lk33ZyqOezMnU79S+kffWqwolG/5W9V8EgQSeYCIfFTYVgssRbnH630Oa2+soBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.3.1", + "exsolve": "^1.1.3", + "pathe": "^2.0.3" + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "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.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmmirror.com/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.3.1", + "resolved": "https://registry.npmmirror.com/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.1" + } + }, + "node_modules/react-i18next": { + "version": "17.1.8", + "resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-17.1.8.tgz", + "integrity": "sha512-vGH+Tbhqbv5LdN9KvC9GEmQ6ATxJAjyxPEK0zSCSRlkeDqCPNxVtEOvDNRAz+51HTxc6QPwhnHaFD7ICu6pD1A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "html-parse-stringify": "^4.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "19.3.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.3.1.tgz", + "integrity": "sha512-RRieF6IyxQBkcK6pP8xOHprLTPW6QjRkbGdImtqNme+/hYVwCZkLQLdlZGOhUqXwkS9IEynaGvPJtv6XB/cz0Q==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "8.5.0", + "resolved": "https://registry.npmmirror.com/react-router/-/react-router-8.5.0.tgz", + "integrity": "sha512-9nEd/deifcTJIIcUaNIyQGkyXD0D6QVB9XD2O5GgHpP3xu3U5RXm87YQ8++Wn8JQWgMBM/5+PjWPM/7n7O7WHA==", + "license": "MIT", + "dependencies": { + "@remix-run/route-pattern": "^0.22.1", + "cookie-es": "^3.1.1" + }, + "engines": { + "node": ">=22.22.0" + }, + "peerDependencies": { + "@types/react": ">=19.2.0", + "@types/react-dom": ">=19.2.0", + "react": ">=19.2.7", + "react-dom": ">=19.2.7" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/rolldown": { + "version": "1.2.15", + "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.2.15.tgz", + "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.155.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.15", + "@rolldown/binding-android-arm64": "1.2.15", + "@rolldown/binding-darwin-arm64": "1.2.15", + "@rolldown/binding-darwin-x64": "1.2.15", + "@rolldown/binding-freebsd-x64": "1.2.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", + "@rolldown/binding-linux-arm64-gnu": "1.2.15", + "@rolldown/binding-linux-arm64-musl": "1.2.15", + "@rolldown/binding-linux-ppc64-gnu": "1.2.15", + "@rolldown/binding-linux-s390x-gnu": "1.2.15", + "@rolldown/binding-linux-x64-gnu": "1.2.15", + "@rolldown/binding-linux-x64-musl": "1.2.15", + "@rolldown/binding-openharmony-arm64": "1.2.15", + "@rolldown/binding-win32-arm64-msvc": "1.2.15", + "@rolldown/binding-win32-x64-msvc": "1.2.15" + } + }, + "node_modules/sass": { + "version": "1.106.1", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.106.1.tgz", + "integrity": "sha512-kD+Gi66vycZcbJt+42NC4z4PXNMcCajVrJPhqoKKXy5cOBMmJQt052CxIBcRjwYXeImHYOpu/ydWWGZQIenl7A==", + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/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/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-toolbelt": { + "version": "9.6.0", + "resolved": "https://registry.npmmirror.com/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.72.0", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.72.0.tgz", + "integrity": "sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.72.0", + "@typescript-eslint/parser": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/valibot": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/valibot/-/valibot-1.5.0.tgz", + "integrity": "sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "8.3.5", + "resolved": "https://registry.npmmirror.com/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1 || ^0.8.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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmmirror.com/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9417034 --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "bq_react", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "debug": "react-router dev --mode debug", + "dev": "react-router dev --mode development", + "prod": "react-router dev --mode production", + "build": "react-router build --mode development", + "build:prod": "react-router build --mode production", + "lint": "eslint .", + "preview": "vite preview", + "format": "prettier --write .", + "format:check": "prettier --check ." + }, + "dependencies": { + "@ant-design/icons": "^6.3.4", + "antd": "^6.6.7", + "axios": "^1.20.0", + "i18next": "^26.5.10", + "i18next-browser-languagedetector": "^8.2.2", + "inversify": "^8.2.3", + "isbot": "^5", + "mobx": "^7.1.1", + "mobx-keystone": "^2.4.1", + "mobx-react-lite": "^5.1.0", + "react": "^19.3.1", + "react-dom": "^19.3.1", + "react-i18next": "^17.1.8", + "react-router": "^8.5.0", + "reflect-metadata": "^0.2.2", + "sass": "^1.106.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@react-router/dev": "^8.5.0", + "@types/node": "^24.19.2", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.2", + "eslint": "^10.13.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.7", + "globals": "^17.13.0", + "prettier": "^3.9.10", + "typescript": "~6.0.3", + "typescript-eslint": "^8.71.2", + "vite": "^8.3.4" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/react-router.config.ts b/react-router.config.ts new file mode 100644 index 0000000..bcb90c4 --- /dev/null +++ b/react-router.config.ts @@ -0,0 +1,6 @@ +import type { Config } from '@react-router/dev/config'; + +export default { + appDirectory: 'src/app', + ssr: false, +} satisfies Config; diff --git a/scripts/apply-name.mjs b/scripts/apply-name.mjs new file mode 100644 index 0000000..1678e74 --- /dev/null +++ b/scripts/apply-name.mjs @@ -0,0 +1,72 @@ +import { readdir, readFile, rename, writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +const projectName = process.argv[2]; +const root = path.resolve(process.argv[3] ?? process.cwd()); + +if (!projectName || !/^[a-z][a-z0-9-]*$/.test(projectName)) { + console.error('用法: node scripts/apply-name.mjs <项目名> [目录]'); + console.error('项目名需为小写字母开头,只含小写字母、数字和连字符'); + process.exit(1); +} + +const skipDirs = new Set(['node_modules', '.git', 'dist', '.react-router', 'scripts', 'doc']); +const textExtensions = new Set([ + '.ts', + '.tsx', + '.js', + '.mjs', + '.cjs', + '.scss', + '.css', + '.json', + '.html', + '.svg', +]); + +function replaceBqPrefix(text) { + return text.replace(/(^|[^A-Za-z0-9])bq/g, `$1${projectName}`); +} + +async function rewriteFile(file) { + const text = await readFile(file, 'utf8'); + const next = + path.basename(file) === 'package-lock.json' + ? text.replaceAll('"name": "bq_react"', `"name": "${replaceBqPrefix('bq_react')}"`) + : replaceBqPrefix(text); + if (next !== text) await writeFile(file, next); +} + +function shouldRewrite(file, inSrc) { + if (inSrc) return textExtensions.has(path.extname(file)); + const base = path.basename(file); + return base === 'package.json' || base === 'package-lock.json'; +} + +async function processDir(dir) { + const relativeDir = path.relative(root, dir); + const inSrc = relativeDir === 'src' || relativeDir.startsWith(`src${path.sep}`); + const entries = await readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + if (skipDirs.has(entry.name)) continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + await processDir(full); + } else if (entry.isFile() && shouldRewrite(full, inSrc)) { + await rewriteFile(full); + } + } + + if (!inSrc) return; + + const after = await readdir(dir, { withFileTypes: true }); + for (const entry of after) { + const nextName = replaceBqPrefix(entry.name); + if (nextName === entry.name) continue; + await rename(path.join(dir, entry.name), path.join(dir, nextName)); + } +} + +await processDir(root); +console.log(`已将 bq 前缀替换为 ${projectName}`); diff --git a/scripts/create.sh b/scripts/create.sh new file mode 100755 index 0000000..dcad0f9 --- /dev/null +++ b/scripts/create.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# 从公开 GitHub 仓库创建项目,并把源码中的 bq 前缀换成项目名。 +# 用法: +# curl -fsSL https://raw.githubusercontent.com/bqbqbqbqbq-l/bq_react/v0.1.0/scripts/create.sh | bash -s -- shop +set -euo pipefail + +OWNER="bqbqbqbqbq-l" +REPO="bq_react" +REF="${BQ_REF:-v0.1.0}" +NAME="${1:-}" + +if [[ ! "$NAME" =~ ^[a-z][a-z0-9-]*$ ]]; then + echo "用法: curl -fsSL https://raw.githubusercontent.com/${OWNER}/${REPO}/${REF}/scripts/create.sh | bash -s -- <项目名>" >&2 + echo "项目名需为小写字母开头,只含小写字母、数字和连字符" >&2 + exit 1 +fi + +if [[ -e "$NAME" ]]; then + echo "目录已存在: $NAME" >&2 + exit 1 +fi + +command -v gh >/dev/null 2>&1 || { + echo "需要 GitHub CLI。安装后执行: gh auth login" >&2 + exit 1 +} +command -v node >/dev/null 2>&1 || { + echo "需要 Node.js" >&2 + exit 1 +} +gh auth status >/dev/null 2>&1 || { + echo "请先登录 GitHub CLI: gh auth login" >&2 + exit 1 +} + +TMP="$(mktemp -d)" +cleanup() { rm -rf "$TMP"; } +trap cleanup EXIT + +ARCHIVE="$TMP/src.tar.gz" +if ! gh api "repos/${OWNER}/${REPO}/tarball/${REF}" >"$ARCHIVE"; then + echo "下载失败。请确认 ${OWNER}/${REPO} 上存在引用 ${REF}。" >&2 + exit 1 +fi +if ! tar -tzf "$ARCHIVE" >/dev/null 2>&1; then + echo "下载到的不是源码包。GitHub 返回:" >&2 + head -c 500 "$ARCHIVE" >&2 || true + echo >&2 + exit 1 +fi + +mkdir -p "$TMP/src" +tar -xzf "$ARCHIVE" -C "$TMP/src" +shopt -s nullglob +entries=("$TMP/src"/*) +if [[ ${#entries[@]} -ne 1 || ! -d "${entries[0]}" ]]; then + echo "压缩包结构异常" >&2 + exit 1 +fi + +mv "${entries[0]}" "$NAME" +node "$NAME/scripts/apply-name.mjs" "$NAME" "$NAME" +rm -rf "$NAME/scripts" +git init -b main "$NAME" >/dev/null +(cd "$NAME" && npm install) + +echo "项目已创建: $NAME" +echo "bq 前缀已替换为 ${NAME},例如包名 ${NAME}_react" +echo "下一步: cd $NAME && npm run dev" diff --git a/src/.env.debug b/src/.env.debug new file mode 100644 index 0000000..cd41370 --- /dev/null +++ b/src/.env.debug @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:3000 \ No newline at end of file diff --git a/src/.env.development b/src/.env.development new file mode 100644 index 0000000..3f7b8a0 --- /dev/null +++ b/src/.env.development @@ -0,0 +1 @@ +VITE_API_URL=http://api.dev.com \ No newline at end of file diff --git a/src/.env.production b/src/.env.production new file mode 100644 index 0000000..366b517 --- /dev/null +++ b/src/.env.production @@ -0,0 +1 @@ +VITE_API_URL=http://prod.dev.com \ No newline at end of file diff --git a/src/app/db/database.ts b/src/app/db/database.ts new file mode 100644 index 0000000..5b2dc26 --- /dev/null +++ b/src/app/db/database.ts @@ -0,0 +1,90 @@ +import { injectable } from 'inversify'; +import { DB_VERSION, schemaVersions } from './schema'; + +// 本地数据库名称 +const DB_NAME = 'local_db'; + +@injectable('Singleton') +export class Database { + private opening: Promise | null = null; + + reset(): Promise { + this.opening = null; + return new Promise((resolve, reject) => { + const request = indexedDB.deleteDatabase(DB_NAME); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error ?? new Error('Failed to delete IndexedDB')); + request.onblocked = () => reject(new Error('IndexedDB delete is blocked')); + }); + } + + open(): Promise { + this.opening ??= this.connect(); + return this.opening; + } + + private connect(): Promise { + return new Promise((resolve, reject) => { + if (typeof indexedDB === 'undefined') { + reject(new Error('IndexedDB is not available')); + return; + } + + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onupgradeneeded = (event) => { + const versionChange = event as IDBVersionChangeEvent; + const transaction = request.transaction; + if (!transaction) { + reject(new Error('IndexedDB upgrade transaction is missing')); + return; + } + try { + this.upgrade( + request.result, + transaction, + versionChange.oldVersion, + versionChange.newVersion ?? DB_VERSION, + ); + } catch (error) { + reject(error); + } + }; + + request.onsuccess = () => { + const db = request.result; + db.onversionchange = () => { + db.close(); + this.opening = null; + }; + resolve(db); + }; + + request.onerror = () => { + this.opening = null; + reject(request.error ?? new Error('Failed to open IndexedDB')); + }; + + request.onblocked = () => { + this.opening = null; + reject(new Error('IndexedDB open is blocked by another connection')); + }; + }); + } + + /** oldVersion 从 0 起。停在旧版本的浏览器会依次执行中间每一版。 */ + private upgrade( + db: IDBDatabase, + transaction: IDBTransaction, + oldVersion: number, + newVersion: number, + ) { + for (let version = oldVersion; version < newVersion; version += 1) { + const upgrade = schemaVersions[version]; + if (!upgrade) { + throw new Error(`Missing IndexedDB schema for version ${version + 1}`); + } + upgrade(db, transaction); + } + } +} diff --git a/src/app/db/schema/index.ts b/src/app/db/schema/index.ts new file mode 100644 index 0000000..28273e2 --- /dev/null +++ b/src/app/db/schema/index.ts @@ -0,0 +1,9 @@ +import type { SchemaUpgrade } from './schema_upgrade'; +import { upgradeV1 } from './v1'; + +export type { SchemaUpgrade }; + +/** 按版本顺序排列。已发布的版本只追加,不改旧文件。 */ +export const schemaVersions: SchemaUpgrade[] = [upgradeV1]; + +export const DB_VERSION = schemaVersions.length; diff --git a/src/app/db/schema/schema_upgrade.ts b/src/app/db/schema/schema_upgrade.ts new file mode 100644 index 0000000..1b58de7 --- /dev/null +++ b/src/app/db/schema/schema_upgrade.ts @@ -0,0 +1 @@ +export type SchemaUpgrade = (db: IDBDatabase, transaction: IDBTransaction) => void; diff --git a/src/app/db/schema/v1.ts b/src/app/db/schema/v1.ts new file mode 100644 index 0000000..6680f23 --- /dev/null +++ b/src/app/db/schema/v1.ts @@ -0,0 +1,10 @@ +import type { SchemaUpgrade } from './schema_upgrade'; + +export const PROJECTS_STORE = 'projects'; + +/** 初始版本:本地项目。 */ +export const upgradeV1: SchemaUpgrade = (db) => { + if (!db.objectStoreNames.contains(PROJECTS_STORE)) { + db.createObjectStore(PROJECTS_STORE, { keyPath: 'id' }); + } +}; diff --git a/src/app/di/bind_as.ts b/src/app/di/bind_as.ts new file mode 100644 index 0000000..6fda751 --- /dev/null +++ b/src/app/di/bind_as.ts @@ -0,0 +1,23 @@ +import type { BindingIdentifier } from 'inversify'; +import { rootContainer } from './root_container'; + +const bindings = new WeakMap>(); + +/** + * 把抽象端口绑到实现类。同一端口有多个实现时传入 name。 + * + * 写在 `@injectable` 上面。类装饰器自下而上执行,`@injectable` 先写入构造参数类型,这里的 `to(impl)` 才能读到。 + * 不要用 `isBound` 探测端口:`autobind` 会把抽象类自己绑上,且该绑定匹配任意 name。 + */ +export function bindAs(port: abstract new (...args: never) => unknown, name?: string) { + return (impl: new (...args: never) => unknown) => { + const key = name ?? ''; + const byName = bindings.get(port) ?? new Map(); + const previous = byName.get(key); + if (previous !== undefined) rootContainer.unbind(previous); + const bound = rootContainer.bind(port).to(impl); + const syntax = name === undefined ? bound : bound.whenNamed(name); + byName.set(key, syntax.getIdentifier()); + bindings.set(port, byName); + }; +} diff --git a/src/app/di/root_container.ts b/src/app/di/root_container.ts new file mode 100644 index 0000000..53f94a7 --- /dev/null +++ b/src/app/di/root_container.ts @@ -0,0 +1,7 @@ +import 'reflect-metadata'; +import { Container } from 'inversify'; +/** 根容器 DI自动绑定 */ +export const rootContainer = new Container({ + autobind: true, + defaultScope: 'Singleton', +}); diff --git a/src/app/entry.client.tsx b/src/app/entry.client.tsx new file mode 100644 index 0000000..607c4b3 --- /dev/null +++ b/src/app/entry.client.tsx @@ -0,0 +1,17 @@ +import 'reflect-metadata'; // 反射元数据 +import { startTransition, StrictMode } from 'react'; +import { hydrateRoot } from 'react-dom/client'; +import { HydratedRouter } from 'react-router/dom'; +import { rootContainer } from './di/root_container'; +import { I18n } from './i18n/i18n'; + +rootContainer.get(I18n); + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); diff --git a/src/app/i18n/catalog.ts b/src/app/i18n/catalog.ts new file mode 100644 index 0000000..76e7bfb --- /dev/null +++ b/src/app/i18n/catalog.ts @@ -0,0 +1,25 @@ +import i18n from 'i18next'; +import { zhCN as commonZh } from './locales/zh_cn'; +import { enUS as commonEn } from './locales/en_us'; + +type Messages = Record; + +const bundles: Record<'zh' | 'en', Record> = { + zh: { common: commonZh }, + en: { common: commonEn }, +}; + +/** 注册消息 */ +export const registerMessages = (feature: string, locales: { zh: Messages; en: Messages }) => { + bundles.zh[feature] = locales.zh; + bundles.en[feature] = locales.en; + if (!i18n.isInitialized) return; + i18n.addResourceBundle('zh', 'translation', { [feature]: locales.zh }, true, true); + i18n.addResourceBundle('en', 'translation', { [feature]: locales.en }, true, true); +}; + +/** 构建资源 */ +export const buildResources = () => ({ + zh: { translation: bundles.zh }, + en: { translation: bundles.en }, +}); diff --git a/src/app/i18n/i18n.ts b/src/app/i18n/i18n.ts new file mode 100644 index 0000000..89e3f64 --- /dev/null +++ b/src/app/i18n/i18n.ts @@ -0,0 +1,50 @@ +import { injectable } from 'inversify'; +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { buildResources } from './catalog'; + +export { i18n }; + +@injectable() +export class I18n { + constructor() { + i18n + .use(LanguageDetector) + .use(initReactI18next) + .init({ + // 设置翻译资源 + resources: buildResources(), + // 设置默认语言 + fallbackLng: 'zh', + // 设置插值方式 + interpolation: { escapeValue: false }, + // 处理缺失的翻译键 + parseMissingKeyHandler: (key: string) => { + const dot = key.indexOf('.'); + if (dot <= 0) return key; + if (key.slice(0, dot) === 'common') return key; + const commonKey = `common.${key.slice(dot + 1)}`; + return i18n.exists(commonKey) ? String(i18n.t(commonKey)) : key; + }, + }); + } + + /** + * 💡 通用翻译代理方法 + */ + public translate(key: string, options?: any): string { + return i18n.t(key, options) as string; + } + + /** + * 💡 动态切换语言方法 + */ + public changeLanguage(lng: 'zh' | 'en'): void { + i18n.changeLanguage(lng); + } + + public get currentLanguage(): string { + return i18n.language; + } +} diff --git a/src/app/i18n/locales/en_us.ts b/src/app/i18n/locales/en_us.ts new file mode 100644 index 0000000..1c46a31 --- /dev/null +++ b/src/app/i18n/locales/en_us.ts @@ -0,0 +1,3 @@ +export const enUS = { + save: 'Save', +}; diff --git a/src/app/i18n/locales/zh_cn.ts b/src/app/i18n/locales/zh_cn.ts new file mode 100644 index 0000000..4a6ae6d --- /dev/null +++ b/src/app/i18n/locales/zh_cn.ts @@ -0,0 +1,3 @@ +export const zhCN = { + save: '保存', +}; diff --git a/src/app/index.scss b/src/app/index.scss new file mode 100644 index 0000000..243a765 --- /dev/null +++ b/src/app/index.scss @@ -0,0 +1,93 @@ +@use './theme/tokens'; + +:root { + @include tokens.emit(tokens.$light); + @include tokens.emit(tokens.$fonts); + + font: 18px/145% var(--sans); + letter-spacing: 0.18px; + color-scheme: light; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + @media (max-width: 1024px) { + font-size: 16px; + } +} + +[data-theme='dark'] { + @include tokens.emit(tokens.$dark); + color-scheme: dark; + + #social .button-icon { + filter: invert(1) brightness(2); + } +} + +#root { + width: 1126px; + max-width: 100%; + margin: 0 auto; + text-align: center; + border-inline: 1px solid var(--border); + min-height: 100svh; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +body { + margin: 0; +} + +h1, +h2 { + font-family: var(--heading); + font-weight: 500; + color: var(--text-h); +} + +h1 { + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; + + @media (max-width: 1024px) { + font-size: 36px; + margin: 20px 0; + } +} + +h2 { + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + + @media (max-width: 1024px) { + font-size: 20px; + } +} + +p { + margin: 0; +} + +code, +.counter { + font-family: var(--mono); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); +} + +code { + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); +} diff --git a/src/app/lifecycle/page_view_model.ts b/src/app/lifecycle/page_view_model.ts new file mode 100644 index 0000000..70a5447 --- /dev/null +++ b/src/app/lifecycle/page_view_model.ts @@ -0,0 +1,6 @@ +export abstract class PageViewModel { + created(): void {} + ready(): void {} + dispose(): void {} + beforeRefresh(): void {} +} diff --git a/src/app/lifecycle/use_view_model.ts b/src/app/lifecycle/use_view_model.ts new file mode 100644 index 0000000..aaa9252 --- /dev/null +++ b/src/app/lifecycle/use_view_model.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from 'react'; +import type { PageViewModel } from './page_view_model'; +import { rootContainer } from '../di/root_container'; + +export function useViewModel(identifier: new (...args: any[]) => T): T { + const [viewModel] = useState(() => { + const vm = rootContainer.get(identifier); + vm.created(); + return vm; + }); + + useEffect(() => { + viewModel.ready(); + const onPageHide = () => viewModel.beforeRefresh(); + window.addEventListener('pagehide', onPageHide); + return () => { + window.removeEventListener('pagehide', onPageHide); + viewModel.dispose(); + }; + }, [viewModel]); + + return viewModel; +} diff --git a/src/app/network/external_http.ts b/src/app/network/external_http.ts new file mode 100644 index 0000000..f425a3b --- /dev/null +++ b/src/app/network/external_http.ts @@ -0,0 +1,33 @@ +// src/app/network/third_party_http_client.ts +import axios, { type AxiosInstance } from 'axios'; +import { injectable } from 'inversify'; + +@injectable() +export class ExternalHttp { + public readonly instance: AxiosInstance; + + constructor() { + this.instance = axios.create({ + timeout: 15000, + }); + + this.initializeTransformers(); + } + + private initializeTransformers() { + // 第三方请求拦截器:通常只做通用的日志或基础透传 + this.instance.interceptors.request.use( + (config) => config, + (error) => Promise.reject(error), + ); + + // 第三方响应拦截器:不对 response.data 进行解构,原样返回由业务层自己解析 + this.instance.interceptors.response.use( + (response) => response.data, + (error) => { + console.error('第三方接口调用失败:', error.message); + return Promise.reject(error); + }, + ); + } +} diff --git a/src/app/network/http.ts b/src/app/network/http.ts new file mode 100644 index 0000000..e3ef194 --- /dev/null +++ b/src/app/network/http.ts @@ -0,0 +1,76 @@ +// src/app/network/self_http_client.ts +import axios, { type AxiosInstance } from 'axios'; +import { injectable } from 'inversify'; +import { TokenStore } from '@/app/session/token_store'; + +@injectable('Singleton') +export class Http { + public readonly instance: AxiosInstance; + + constructor(private tokenStore: TokenStore) { + this.instance = axios.create({ + baseURL: import.meta.env.VITE_API_URL || 'https://mybackend.com', + timeout: 10000, + headers: { 'Content-Type': 'application/json' }, + }); + + this.initializeTransformers(); + } + + private initializeTransformers() { + // 💡 1. 请求拦截器:自动注入 Token + this.instance.interceptors.request.use( + (config) => { + const token = this.tokenStore.get(); + if (token && config.headers) { + config.headers['Authorization'] = `Bearer ${token}`; + } + if (config.data !== undefined) config.data = stripDollar(config.data); + return config; + }, + (error) => Promise.reject(error), + ); + + // 💡 2. 响应拦截器:统一业务异常、处理 401 鉴权失效 + this.instance.interceptors.response.use( + (response) => { + // 假设自己后端的标准结构为 { code: 200, data: ..., message: '' } + const res = response.data; + if (res.code !== 200) { + // 可以在此处结合领域抛出自定义业务异常 + return Promise.reject(new Error(res.message || '业务逻辑错误')); + } + return res.data; + }, + (error) => { + const status = error.response?.status as number | undefined; + const serverMessage = error.response?.data?.message; + if (status === 401) { + this.tokenStore.remove(); + const path = `${window.location.pathname}${window.location.search}`; + if (!path.startsWith('/security')) { + window.location.replace(`/security?${new URLSearchParams({ from: path })}`); + } + } + const message = + typeof serverMessage === 'string' && serverMessage ? serverMessage : error.message; + return Promise.reject(new Error(message)); + }, + ); + } +} + +function stripDollar(value: unknown): unknown { + if (Array.isArray(value)) return value.map(stripDollar); + if (!isPlainRecord(value)) return value; + const result: Record = {}; + for (const [key, item] of Object.entries(value)) { + if (key.startsWith('$')) continue; + result[key] = stripDollar(item); + } + return result; +} + +function isPlainRecord(value: unknown): value is Record { + return Object.prototype.toString.call(value) === '[object Object]'; +} diff --git a/src/app/root.tsx b/src/app/root.tsx new file mode 100644 index 0000000..0e384ef --- /dev/null +++ b/src/app/root.tsx @@ -0,0 +1,30 @@ +import { useEffect } from 'react'; +import { Outlet, Scripts, ScrollRestoration } from 'react-router'; +import { applyTheme } from './theme/theme'; +import './index.scss'; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + bq_react + + + + {children} + + + + + ); +} + +export default function App() { + useEffect(() => { + applyTheme(); + }, []); + + return ; +} diff --git a/src/app/routes.ts b/src/app/routes.ts new file mode 100644 index 0000000..5e4253f --- /dev/null +++ b/src/app/routes.ts @@ -0,0 +1,17 @@ +import { index, layout, route, type RouteConfig } from '@react-router/dev/routes'; + +export default [ + layout('routes/app_shell.tsx', [ + index('../features/home/presentation/home/home.tsx'), + layout('routes/layouts/guest.layout.tsx', [ + route('security', '../features/security/presentation/security/security.tsx'), + ]), + layout('routes/layouts/auth.layout.tsx', [ + route('profile', '../features/security/presentation/security/profile.tsx'), + route('projects/remote', '../features/project/presentation/remote_list/remote_project_list.tsx'), + route('projects/local', '../features/project/presentation/local_list/local_project_list.tsx'), + route('editor/:projectId', '../features/editor/presentation/editor/editor.tsx'), + ]), + route('*', 'routes/components/catchall.tsx'), + ]), +] satisfies RouteConfig; diff --git a/src/app/routes/app_shell.tsx b/src/app/routes/app_shell.tsx new file mode 100644 index 0000000..b5c7673 --- /dev/null +++ b/src/app/routes/app_shell.tsx @@ -0,0 +1,20 @@ +import { App, ConfigProvider } from 'antd'; +import { I18nextProvider } from 'react-i18next'; +import { Outlet } from 'react-router'; +import { i18n } from '@/app/i18n/i18n'; +import { antdTheme } from '@/app/theme/antd_theme'; +import { useResolvedTheme } from '@/app/theme/use_theme'; + +export default function AppShell() { + const resolved = useResolvedTheme(); + + return ( + + + + + + + + ); +} diff --git a/src/app/routes/components/404.tsx b/src/app/routes/components/404.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/routes/components/catchall.tsx b/src/app/routes/components/catchall.tsx new file mode 100644 index 0000000..192a620 --- /dev/null +++ b/src/app/routes/components/catchall.tsx @@ -0,0 +1,5 @@ +import { Navigate } from 'react-router'; + +export default function CatchallRedirect() { + return ; +} diff --git a/src/app/routes/components/route_loading/route_loading.scss b/src/app/routes/components/route_loading/route_loading.scss new file mode 100644 index 0000000..a80498d --- /dev/null +++ b/src/app/routes/components/route_loading/route_loading.scss @@ -0,0 +1,26 @@ +.loading-container { + height: 100vh; + width: 100vw; + background-color: rgba(0, 0, 0, 0.1); + display: flex; + justify-content: center; + align-items: center; +} + +.spin { + width: 40px; + height: 40px; + border: 4px solid #000; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/src/app/routes/components/route_loading/route_loading.tsx b/src/app/routes/components/route_loading/route_loading.tsx new file mode 100644 index 0000000..73fe8a4 --- /dev/null +++ b/src/app/routes/components/route_loading/route_loading.tsx @@ -0,0 +1,9 @@ +import './route_loading.scss'; + +export function RouteLoading() { + return ( +
+
+
+ ); +} diff --git a/src/app/routes/layouts/auth.layout.tsx b/src/app/routes/layouts/auth.layout.tsx new file mode 100644 index 0000000..1115d8e --- /dev/null +++ b/src/app/routes/layouts/auth.layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, redirect, type MiddlewareFunction } from 'react-router'; +import { hasSession } from '@/app/session/auth_session'; +import { decideRoutePolicy } from './decide_route_policy'; + +export const clientMiddleware: MiddlewareFunction[] = [ + async ({ request }, next) => { + const url = new URL(request.url); + const policy = decideRoutePolicy('require-auth', hasSession(), url.pathname, url.search); + if (policy.status === 'denied') throw redirect(policy.redirect.to); + await next(); + }, +]; + +export default function AuthLayout() { + return ; +} diff --git a/src/app/routes/layouts/decide_route_policy.ts b/src/app/routes/layouts/decide_route_policy.ts new file mode 100644 index 0000000..6eb82f2 --- /dev/null +++ b/src/app/routes/layouts/decide_route_policy.ts @@ -0,0 +1,20 @@ +import type { RouteAccess, RoutePolicyResult } from './route_policy.type'; + +export function decideRoutePolicy( + access: RouteAccess, + authenticated: boolean, + pathname: string, + search: string, +): RoutePolicyResult { + if (access === 'require-auth' && !authenticated) { + const from = `${pathname}${search}`; + return { + status: 'denied', + redirect: { to: `/security?${new URLSearchParams({ from })}` }, + }; + } + if (access === 'require-guest' && authenticated) { + return { status: 'denied', redirect: { to: '/' } }; + } + return { status: 'allowed' }; +} diff --git a/src/app/routes/layouts/guest.layout.tsx b/src/app/routes/layouts/guest.layout.tsx new file mode 100644 index 0000000..e38c236 --- /dev/null +++ b/src/app/routes/layouts/guest.layout.tsx @@ -0,0 +1,16 @@ +import { Outlet, redirect, type MiddlewareFunction } from 'react-router'; +import { hasSession } from '@/app/session/auth_session'; +import { decideRoutePolicy } from './decide_route_policy'; + +export const clientMiddleware: MiddlewareFunction[] = [ + async ({ request }, next) => { + const url = new URL(request.url); + const policy = decideRoutePolicy('require-guest', hasSession(), url.pathname, url.search); + if (policy.status === 'denied') throw redirect(policy.redirect.to); + await next(); + }, +]; + +export default function GuestLayout() { + return ; +} diff --git a/src/app/routes/layouts/route_policy.type.ts b/src/app/routes/layouts/route_policy.type.ts new file mode 100644 index 0000000..af6bb0f --- /dev/null +++ b/src/app/routes/layouts/route_policy.type.ts @@ -0,0 +1,4 @@ +export type RoutePolicyResult = + { status: 'allowed' } | { status: 'denied'; redirect: { to: string } }; + +export type RouteAccess = 'require-auth' | 'require-guest'; diff --git a/src/app/session/auth_event.ts b/src/app/session/auth_event.ts new file mode 100644 index 0000000..7fb85db --- /dev/null +++ b/src/app/session/auth_event.ts @@ -0,0 +1,10 @@ +export const AUTH_CHANGED = 'auth-changed'; + +export function notifyAuthChanged(): void { + window.dispatchEvent(new Event(AUTH_CHANGED)); +} + +export function subscribeAuth(listener: () => void): () => void { + window.addEventListener(AUTH_CHANGED, listener); + return () => window.removeEventListener(AUTH_CHANGED, listener); +} diff --git a/src/app/session/auth_session.ts b/src/app/session/auth_session.ts new file mode 100644 index 0000000..71f919b --- /dev/null +++ b/src/app/session/auth_session.ts @@ -0,0 +1,17 @@ +import { rootContainer } from '@/app/di/root_container'; +import { TokenStore } from './token_store'; + +export function hasSession(): boolean { + return rootContainer.get(TokenStore).has(); +} + +/** 只有同源路径才被接受作为登录后返回的目标 */ +export function safeInternalPath(value: string | null): string | null { + /** 如果值为空 或 不以 / 开头,则返回空 */ + if (!value || !value.startsWith('/')) return null; + /** 如果值以 // 或 /\\ 开头,则返回值 */ + if (value.startsWith('//') || value.startsWith('/\\')) return null; + /** 如果值包含 :// 或 \,则返回空 */ + if (value.includes('://') || value.includes('\\')) return null; + return null; +} diff --git a/src/app/session/token_store.ts b/src/app/session/token_store.ts new file mode 100644 index 0000000..c12263d --- /dev/null +++ b/src/app/session/token_store.ts @@ -0,0 +1,42 @@ +import { injectable } from 'inversify'; +import { notifyAuthChanged } from './auth_event'; + +@injectable('Singleton') +export class TokenStore { + private readonly tokenKey = '_sys_auth_token_'; + private readonly secretSalt = 'APP_PLATFORM_SALT_2026'; + + save(token: string): void { + localStorage.setItem(this.tokenKey, this.encrypt(token)); + notifyAuthChanged(); + } + + get(): string | null { + const encrypted = localStorage.getItem(this.tokenKey); + if (!encrypted) return null; + const token = this.decrypt(encrypted); + return token || null; + } + + remove(): void { + localStorage.removeItem(this.tokenKey); + notifyAuthChanged(); + } + + has(): boolean { + return this.get() !== null; + } + + private encrypt(value: string): string { + return btoa(encodeURIComponent(value) + this.secretSalt); + } + + private decrypt(ciphertext: string): string { + try { + const decoded = atob(ciphertext); + return decodeURIComponent(decoded.replace(this.secretSalt, '')); + } catch { + return ''; + } + } +} diff --git a/src/app/session/use_authenticated.ts b/src/app/session/use_authenticated.ts new file mode 100644 index 0000000..2c1ba98 --- /dev/null +++ b/src/app/session/use_authenticated.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from 'react'; +import { hasSession } from './auth_session'; +import { subscribeAuth } from './auth_event'; + +export function useAuthenticated(): boolean { + const [authenticated, setAuthenticated] = useState(false); + + useEffect(() => { + const sync = () => setAuthenticated(hasSession()); + sync(); + return subscribeAuth(sync); + }, []); + + return authenticated; +} diff --git a/src/app/theme/_tokens.scss b/src/app/theme/_tokens.scss new file mode 100644 index 0000000..e5f8687 --- /dev/null +++ b/src/app/theme/_tokens.scss @@ -0,0 +1,37 @@ +$light: ( + text: #6b6375, + text-h: #08060d, + bg: #fff, + border: #e5e4e7, + code-bg: #f4f3ec, + accent: #aa3bff, + accent-bg: rgba(170, 59, 255, 0.1), + accent-border: rgba(170, 59, 255, 0.5), + social-bg: rgba(244, 243, 236, 0.5), + shadow: 'rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px', +); + +$dark: ( + text: #9ca3af, + text-h: #f3f4f6, + bg: #16171d, + border: #2e303a, + code-bg: #1f2028, + accent: #c084fc, + accent-bg: rgba(192, 132, 252, 0.15), + accent-border: rgba(192, 132, 252, 0.5), + social-bg: rgba(47, 48, 58, 0.5), + shadow: 'rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px', +); + +$fonts: ( + sans: "system-ui, 'Segoe UI', Roboto, sans-serif", + heading: "system-ui, 'Segoe UI', Roboto, sans-serif", + mono: 'ui-monospace, Consolas, monospace', +); + +@mixin emit($tokens) { + @each $name, $value in $tokens { + --#{$name}: #{$value}; + } +} diff --git a/src/app/theme/antd_theme.ts b/src/app/theme/antd_theme.ts new file mode 100644 index 0000000..6535083 --- /dev/null +++ b/src/app/theme/antd_theme.ts @@ -0,0 +1,9 @@ +import { theme, type ThemeConfig } from 'antd'; +import type { ResolvedTheme } from './theme'; + +export function antdTheme(resolved: ResolvedTheme): ThemeConfig { + return { + algorithm: resolved === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm, + cssVar: { key: 'bq' }, + }; +} diff --git a/src/app/theme/theme.ts b/src/app/theme/theme.ts new file mode 100644 index 0000000..7d744aa --- /dev/null +++ b/src/app/theme/theme.ts @@ -0,0 +1,69 @@ +export type ThemeMode = 'light' | 'dark' | 'system'; +export type ResolvedTheme = 'light' | 'dark'; + +const STORAGE_KEY = 'bq-theme'; +const SYSTEM_QUERY = '(prefers-color-scheme: dark)'; + +const listeners = new Set<() => void>(); +let media: MediaQueryList | null = null; + +export function getThemeMode(): ThemeMode { + if (typeof localStorage === 'undefined') return 'system'; + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored === 'light' || stored === 'dark' || stored === 'system') return stored; + } catch { + return 'system'; + } + return 'system'; +} + +export function resolveTheme(mode: ThemeMode = getThemeMode()): ResolvedTheme { + if (mode === 'light' || mode === 'dark') return mode; + if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return 'light'; + return window.matchMedia(SYSTEM_QUERY).matches ? 'dark' : 'light'; +} + +export function applyTheme(mode: ThemeMode = getThemeMode()): ResolvedTheme { + const resolved = resolveTheme(mode); + paint(resolved); + watchSystemTheme(mode); + return resolved; +} + +export function setThemeMode(mode: ThemeMode): void { + try { + localStorage.setItem(STORAGE_KEY, mode); + } catch { + // 隐私模式写不进去时,本次会话仍然切换。 + } + applyTheme(mode); + emit(); +} + +export function subscribeTheme(listener: () => void): () => void { + listeners.add(listener); + return () => listeners.delete(listener); +} + +function paint(resolved: ResolvedTheme) { + if (typeof document === 'undefined') return; + document.documentElement.dataset.theme = resolved; + document.documentElement.style.colorScheme = resolved; +} + +function watchSystemTheme(mode: ThemeMode) { + if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return; + media ??= window.matchMedia(SYSTEM_QUERY); + media.removeEventListener('change', onSystemThemeChange); + if (mode === 'system') media.addEventListener('change', onSystemThemeChange); +} + +function onSystemThemeChange() { + paint(resolveTheme('system')); + emit(); +} + +function emit() { + listeners.forEach((listener) => listener()); +} diff --git a/src/app/theme/use_theme.ts b/src/app/theme/use_theme.ts new file mode 100644 index 0000000..749024a --- /dev/null +++ b/src/app/theme/use_theme.ts @@ -0,0 +1,20 @@ +import { useSyncExternalStore } from 'react'; +import { + getThemeMode, + resolveTheme, + subscribeTheme, + type ResolvedTheme, + type ThemeMode, +} from './theme'; + +export function useThemeMode(): ThemeMode { + return useSyncExternalStore(subscribeTheme, getThemeMode, () => 'system'); +} + +export function useResolvedTheme(): ResolvedTheme { + return useSyncExternalStore( + subscribeTheme, + () => resolveTheme(getThemeMode()), + () => 'light', + ); +} diff --git a/src/app/worker/entities/worker_error.ts b/src/app/worker/entities/worker_error.ts new file mode 100644 index 0000000..1f08e75 --- /dev/null +++ b/src/app/worker/entities/worker_error.ts @@ -0,0 +1,24 @@ +export class WorkerError extends Error { + override readonly name = 'WorkerError'; + + constructor(message: string) { + super(message); + } + + static from(error: unknown): WorkerError { + if (error instanceof WorkerError) return error; + if (error instanceof Error) return new WorkerError(error.message); + return new WorkerError(String(error)); + } + + static fromJson(value: unknown): WorkerError { + if (!value || typeof value !== 'object') throw new WorkerError('Invalid worker error'); + const body = value as { message?: unknown }; + if (typeof body.message !== 'string') throw new WorkerError('Invalid worker error'); + return new WorkerError(body.message); + } + + toJson(): { name: string; message: string } { + return { name: this.name, message: this.message }; + } +} diff --git a/src/app/worker/entities/worker_message.ts b/src/app/worker/entities/worker_message.ts new file mode 100644 index 0000000..f194d40 --- /dev/null +++ b/src/app/worker/entities/worker_message.ts @@ -0,0 +1,26 @@ +import { WorkerError } from './worker_error'; + +export class WorkerMessage { + readonly id: string; + readonly type: string; + readonly payload: unknown; + + constructor(type: string, payload: unknown, id: string = crypto.randomUUID()) { + this.id = id; + this.type = type; + this.payload = payload; + } + + static fromJson(value: unknown): WorkerMessage { + if (!value || typeof value !== 'object') throw new WorkerError('Invalid worker message'); + const body = value as { id?: unknown; type?: unknown; payload?: unknown }; + if (typeof body.id !== 'string' || typeof body.type !== 'string' || !('payload' in body)) { + throw new WorkerError('Invalid worker message'); + } + return new WorkerMessage(body.type, body.payload, body.id); + } + + toJson(): { id: string; type: string; payload: unknown } { + return { id: this.id, type: this.type, payload: this.payload }; + } +} diff --git a/src/app/worker/entities/worker_reply.ts b/src/app/worker/entities/worker_reply.ts new file mode 100644 index 0000000..73147fc --- /dev/null +++ b/src/app/worker/entities/worker_reply.ts @@ -0,0 +1,46 @@ +import { WorkerError } from './worker_error'; + +export class WorkerReply { + readonly id: string; + readonly payload: unknown; + readonly error: WorkerError | null; + + private constructor(id: string, payload: unknown, error: WorkerError | null) { + this.id = id; + this.payload = payload; + this.error = error; + } + + static ok(id: string, payload: unknown): WorkerReply { + return new WorkerReply(id, payload, null); + } + + static fail(id: string, error: WorkerError): WorkerReply { + return new WorkerReply(id, undefined, error); + } + + static fromJson(value: unknown): WorkerReply { + if (!value || typeof value !== 'object') throw new WorkerError('Invalid worker reply'); + const body = value as { id?: unknown; ok?: unknown; payload?: unknown; error?: unknown }; + if (typeof body.id !== 'string' || typeof body.ok !== 'boolean') { + throw new WorkerError('Invalid worker reply'); + } + if (body.ok) { + if (!('payload' in body)) throw new WorkerError('Invalid worker reply'); + return WorkerReply.ok(body.id, body.payload); + } + return WorkerReply.fail(body.id, WorkerError.fromJson(body.error)); + } + + toJson(): + | { id: string; ok: true; payload: unknown } + | { id: string; ok: false; error: { name: string; message: string } } { + if (this.error) return { id: this.id, ok: false, error: this.error.toJson() }; + return { id: this.id, ok: true, payload: this.payload }; + } + + unwrap(): unknown { + if (this.error) throw this.error; + return this.payload; + } +} diff --git a/src/app/worker/worker_runtime.ts b/src/app/worker/worker_runtime.ts new file mode 100644 index 0000000..bfdc1de --- /dev/null +++ b/src/app/worker/worker_runtime.ts @@ -0,0 +1,75 @@ +import { WorkerError } from './entities/worker_error'; +import { WorkerMessage } from './entities/worker_message'; +import { WorkerReply } from './entities/worker_reply'; + +export type WorkerHandler = (payload: unknown) => unknown | Promise; + +/** worker 作用域。 */ +interface WorkerScope { + /** 监听消息。 */ + onmessage: ((event: MessageEvent) => void) | null; + /** 发送消息。 */ + postMessage(message: unknown): void; +} + +/** worker 线程按消息类型分发。业务异常回传给调用方,不关闭 worker。 */ +export class WorkerRuntime { + /** 监听消息并分发。 */ + listen(handlers: Record): void { + // 获取当前 worker 实例 + const scope = self as unknown as WorkerScope; + // 监听消息 + scope.onmessage = (event: MessageEvent) => { + // 分发消息 + void this.dispatch(scope, handlers, event.data); + }; + } + + /** 分发消息。 */ + private async dispatch( + scope: WorkerScope, + handlers: Record, + data: unknown, + ): Promise { + let message: WorkerMessage; + // 解析消息 + try { + message = WorkerMessage.fromJson(data); + } catch (error) { + if (error instanceof WorkerError) return; + throw error; + } + + // 获取消息处理器 + const handler = handlers[message.type]; + // 如果消息处理器不存在,则发送失败回复 + if (!handler) { + // 发送失败回复 + this.send( + scope, + WorkerReply.fail(message.id, new WorkerError(`Unknown worker message: ${message.type}`)), + ); + return; + } + + try { + // 执行消息处理器 + const payload = await handler(message.payload); + // 发送成功回复 + this.send(scope, WorkerReply.ok(message.id, payload)); + } catch (error) { + // 发送失败回复 + this.send(scope, WorkerReply.fail(message.id, WorkerError.from(error))); + } + } + + /** 发送回复。 */ + private send(scope: WorkerScope, reply: WorkerReply): void { + // 发送回复 + try { + scope.postMessage(reply.toJson()); + } catch (error) { + scope.postMessage(WorkerReply.fail(reply.id, WorkerError.from(error)).toJson()); + } + } +} diff --git a/src/app/worker/worker_session.ts b/src/app/worker/worker_session.ts new file mode 100644 index 0000000..8e22e2e --- /dev/null +++ b/src/app/worker/worker_session.ts @@ -0,0 +1,184 @@ +import { WorkerError } from './entities/worker_error'; +import { WorkerMessage } from './entities/worker_message'; +import { WorkerReply } from './entities/worker_reply'; + +type Pending = { + resolve: (payload: unknown) => void; + reject: (error: WorkerError) => void; +}; + +/** 两次创建之间没有成功处理过消息,就停止再创建。 */ +const MAX_CONSECUTIVE_CRASHES = 1; + +export type WorkerFactory = () => Worker; + +/** 管理一个 worker 实例的生命周期。 + * Worker使用示例: + * ```ts + * const worker = new WorkerSession(() => new Worker('worker.js')); + * let workerMessage: WorkerMessage; + * workerMessage = new WorkerMessage('type', { payload: [Entity].toJson() }); + * worker.request(workerMessage).then((result: [Entity].fromJson(result)) => { + * console.log(result); + * }); + * ``` + */ +export class WorkerSession { + private worker: Worker | null = null; + private readonly pending = new Map(); + private consecutiveCrashes = 0; + private stopped = false; + + constructor(private readonly createWorker: WorkerFactory) {} + + /** 发送消息并等待回复。 */ + request(message: WorkerMessage): Promise { + return new Promise((resolve, reject) => { + if (this.stopped) { + reject(new WorkerError('Worker stopped after repeated crashes')); + return; + } + + let worker: Worker; + try { + // 确保 worker 实例存在 + worker = this.ensure(); + } catch (error) { + // 如果 worker 实例不存在,则拒绝请求 + // 并记录崩溃次数 + reject(WorkerError.from(error)); + return; + } + + // 将请求添加到 pending 队列 + this.pending.set(message.id, { + // 如果请求成功,则解析回复 + resolve: (value) => resolve(value as T), + // 如果请求失败,则拒绝请求 + reject, + }); + + try { + // 发送消息 + worker.postMessage(message.toJson()); + } catch (error) { + // 如果请求失败,则删除请求 + this.pending.delete(message.id); + // 并拒绝请求 + reject(WorkerError.from(error)); + } + }); + } + + /** 销毁 worker 实例。 */ + dispose(): void { + // 拒绝所有 pending 请求 + this.failPending(new WorkerError('Worker disposed')); + // 销毁 worker 实例 + this.worker?.terminate(); + // 清空 worker 实例 + this.worker = null; + } + + /** 确保 worker 实例存在。 */ + private ensure(): Worker { + // 如果 worker 实例存在,则返回 + if (this.worker) return this.worker; + + let worker: Worker; + // 创建 worker 实例 + try { + worker = this.createWorker(); + } catch (error) { + // 如果创建 worker 实例失败,则记录崩溃次数 + this.noteCrash(); + throw error; + } + + // 监听消息 + worker.onmessage = (event: MessageEvent) => { + this.onMessage(event.data); + }; + // 监听错误 + worker.onerror = (event) => { + event.preventDefault(); + // 如果监听错误,则记录崩溃次数 + this.crash(worker, new WorkerError(event.message || 'Worker crashed')); + }; + // 监听消息错误 + worker.onmessageerror = () => { + // 如果监听消息错误,则记录崩溃次数 + this.crash(worker, new WorkerError('Worker message could not be deserialized')); + }; + + // 设置 worker 实例 + this.worker = worker; + // 返回 worker 实例 + return worker; + } + + /** 处理消息。 */ + private onMessage(data: unknown): void { + let reply: WorkerReply; + // 解析回复 + try { + // 如果解析回复失败,则返回 + reply = WorkerReply.fromJson(data); + } catch (error) { + // 如果解析回复失败,则返回 + if (error instanceof WorkerError) return; + // 如果解析回复失败,则抛出错误 + throw error; + } + + // 获取 pending 请求 + const pending = this.pending.get(reply.id); + // 如果 pending 请求不存在,则返回 + if (!pending) return; + + // 删除 pending 请求 + this.pending.delete(reply.id); + // 重置崩溃次数 + this.consecutiveCrashes = 0; + + try { + // 如果解析回复成功,则解析回复 + pending.resolve(reply.unwrap()); + } catch (error) { + // 如果解析回复失败,则拒绝请求 + pending.reject(WorkerError.from(error)); + } + } + + /** 处理崩溃。 */ + private crash(worker: Worker, error: WorkerError): void { + // 如果 worker 实例不存在,则返回 + if (this.worker !== worker) return; + // 销毁 worker 实例 + this.worker = null; + // 销毁 worker 实例 + worker.terminate(); + // 记录崩溃次数 + this.noteCrash(); + // 拒绝所有 pending 请求 + this.failPending(error); + } + + /** 记录崩溃次数。 */ + private noteCrash(): void { + // 记录崩溃次数 + this.consecutiveCrashes += 1; + // 如果崩溃次数超过最大连续崩溃次数,则停止 worker + if (this.consecutiveCrashes > MAX_CONSECUTIVE_CRASHES) this.stopped = true; + } + + /** 拒绝所有 pending 请求。 */ + private failPending(error: WorkerError): void { + // 获取 pending 请求 + const pending = [...this.pending.values()]; + // 清空 pending 请求 + this.pending.clear(); + // 拒绝所有 pending 请求 + pending.forEach((item) => item.reject(error)); + } +} diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/src/assets/hero.png differ diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/features/editor/domain/entities/draft_shape.ts b/src/features/editor/domain/entities/draft_shape.ts new file mode 100644 index 0000000..9aed14e --- /dev/null +++ b/src/features/editor/domain/entities/draft_shape.ts @@ -0,0 +1,31 @@ +import { EditorTool } from '../enum/editor_tool'; + +export class DraftShape { + readonly kind: EditorTool; + readonly x1: number; + readonly y1: number; + x2: number; + y2: number; + + constructor(kind: EditorTool, x: number, y: number) { + this.kind = kind; + this.x1 = x; + this.y1 = y; + this.x2 = x; + this.y2 = y; + } + + move(x: number, y: number) { + this.x2 = x; + this.y2 = y; + } + + rect(): { x: number; y: number; width: number; height: number } { + return { + x: Math.min(this.x1, this.x2), + y: Math.min(this.y1, this.y2), + width: Math.abs(this.x2 - this.x1), + height: Math.abs(this.y2 - this.y1), + }; + } +} diff --git a/src/features/editor/domain/enum/editor_tool.ts b/src/features/editor/domain/enum/editor_tool.ts new file mode 100644 index 0000000..be8eb11 --- /dev/null +++ b/src/features/editor/domain/enum/editor_tool.ts @@ -0,0 +1,4 @@ +export enum EditorTool { + LINE = 'line', + RECT = 'rect', +} diff --git a/src/features/editor/presentation/editor/editor.scss b/src/features/editor/presentation/editor/editor.scss new file mode 100644 index 0000000..44cec91 --- /dev/null +++ b/src/features/editor/presentation/editor/editor.scss @@ -0,0 +1,26 @@ +.editor-page { + padding: 24px; + text-align: left; +} + +.editor-toolbar { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; +} + +.editor-canvas { + width: 100%; + height: 480px; + border: 1px solid var(--border); + background: var(--bg); + touch-action: none; + + line, + rect { + fill: transparent; + stroke: var(--text-h); + stroke-width: 2; + } +} diff --git a/src/features/editor/presentation/editor/editor.tsx b/src/features/editor/presentation/editor/editor.tsx new file mode 100644 index 0000000..318c645 --- /dev/null +++ b/src/features/editor/presentation/editor/editor.tsx @@ -0,0 +1,103 @@ +import '@/features/project/project'; + +import { useEffect, type PointerEvent } from 'react'; +import { observer } from 'mobx-react-lite'; +import { useParams, useSearchParams } from 'react-router'; +import { Button } from 'antd'; +import { useViewModel } from '@/app/lifecycle/use_view_model'; +import { LineShape, RectShape } from '@/features/project/domain/entities/document'; +import { DraftShape } from '../../domain/entities/draft_shape'; +import { EditorTool } from '../../domain/enum/editor_tool'; +import { EditorViewModel } from './editor_view_model'; +import './editor.scss'; + +export default observer(function EditorPage() { + const vm = useViewModel(EditorViewModel); + const { projectId } = useParams(); + const [searchParams] = useSearchParams(); + const store = searchParams.get('store'); + + useEffect(() => { + void vm.open(projectId ?? '', store); + }, [vm, projectId, store]); + + useEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + const target = event.target; + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) return; + if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'z') return; + event.preventDefault(); + if (event.shiftKey) vm.redo(); + else vm.undo(); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [vm]); + + return ( +
+
+ + {vm.project?.name ?? 'Editor'} + + + + +
+ {vm.errorMessage &&

{vm.errorMessage}

} + { + const point = svgPoint(event); + event.currentTarget.setPointerCapture(event.pointerId); + vm.pointerDown(point.x, point.y); + }} + onPointerMove={(event) => { + if (!vm.draft) return; + const point = svgPoint(event); + vm.pointerMove(point.x, point.y); + }} + onPointerUp={() => vm.pointerUp()} + > + {vm.project?.document.shapes.map((shape) => + shape instanceof LineShape ? ( + + ) : shape instanceof RectShape ? ( + + ) : null, + )} + {vm.draft && } + +
+ ); +}); + +function Draft({ draft }: { draft: DraftShape }) { + if (draft.kind === EditorTool.LINE) { + return ; + } + const bounds = draft.rect(); + return ; +} + +function svgPoint(event: PointerEvent) { + const svg = event.currentTarget; + const matrix = svg.getScreenCTM(); + if (!matrix) return { x: 0, y: 0 }; + const point = svg.createSVGPoint(); + point.x = event.clientX; + point.y = event.clientY; + const local = point.matrixTransform(matrix.inverse()); + return { x: local.x, y: local.y }; +} diff --git a/src/features/editor/presentation/editor/editor_view_model.ts b/src/features/editor/presentation/editor/editor_view_model.ts new file mode 100644 index 0000000..7110ded --- /dev/null +++ b/src/features/editor/presentation/editor/editor_view_model.ts @@ -0,0 +1,140 @@ +import { injectable } from 'inversify'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { PageViewModel } from '@/app/lifecycle/page_view_model'; +import { ProjectApplication } from '@/features/project/application/project_application'; +import { ProjectStore } from '@/features/project/domain/enum/project_store'; +import { LineShape, RectShape } from '@/features/project/domain/entities/document'; +import type { Project } from '@/features/project/domain/entities/project'; +import { DraftShape } from '../../domain/entities/draft_shape'; +import { EditorTool } from '../../domain/enum/editor_tool'; + +@injectable('Transient') +export class EditorViewModel extends PageViewModel { + project: Project | null = null; + store: ProjectStore | null = null; + tool: EditorTool = EditorTool.LINE; + draft: DraftShape | null = null; + errorMessage = ''; + + constructor(private application: ProjectApplication) { + super(); + makeObservable(this, { + project: observable, + store: observable, + tool: observable, + draft: observable, + errorMessage: observable, + canUndo: computed, + canRedo: computed, + setTool: action, + pointerDown: action, + pointerMove: action, + pointerUp: action, + undo: action, + redo: action, + }); + } + + get canUndo(): boolean { + return this.project?.canUndo ?? false; + } + + get canRedo(): boolean { + return this.project?.canRedo ?? false; + } + + async open(id: string, store: string | null): Promise { + if (!id || !store) { + runInAction(() => { + this.errorMessage = '缺少项目'; + }); + return; + } + const current = Object.values(ProjectStore).find((item) => item === store); + if (!current) { + runInAction(() => { + this.errorMessage = '项目存储无效'; + }); + return; + } + + try { + const project = await this.application.get(current, id); + runInAction(() => { + this.project = project; + this.store = current; + this.draft = null; + this.errorMessage = project ? '' : '找不到项目'; + }); + } catch (error) { + runInAction(() => { + this.errorMessage = error instanceof Error ? error.message : '加载失败'; + }); + } + } + + setTool(tool: EditorTool) { + this.tool = tool; + } + + pointerDown(x: number, y: number) { + if (!this.project) return; + this.draft = new DraftShape(this.tool, x, y); + } + + pointerMove(x: number, y: number) { + if (!this.draft) return; + this.draft.move(x, y); + } + + pointerUp() { + const draft = this.draft; + this.draft = null; + if (!draft || !this.project) return; + + const bounds = draft.rect(); + const shape = + draft.kind === EditorTool.LINE + ? new LineShape({ + id: crypto.randomUUID(), + x1: draft.x1, + y1: draft.y1, + x2: draft.x2, + y2: draft.y2, + }) + : new RectShape({ + id: crypto.randomUUID(), + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }); + + if (!this.project.addShape(shape)) return; + void this.persist(); + } + + undo() { + if (!this.project?.undo()) return; + void this.persist(); + } + + redo() { + if (!this.project?.redo()) return; + void this.persist(); + } + + private async persist() { + if (!this.project || !this.store) return; + try { + await this.application.save(this.store, this.project); + runInAction(() => { + this.errorMessage = ''; + }); + } catch (error) { + runInAction(() => { + this.errorMessage = error instanceof Error ? error.message : '保存失败'; + }); + } + } +} diff --git a/src/features/home/presentation/home/home.scss b/src/features/home/presentation/home/home.scss new file mode 100644 index 0000000..c7e66ca --- /dev/null +++ b/src/features/home/presentation/home/home.scss @@ -0,0 +1,93 @@ +.home-page { + min-height: 100vh; + display: flex; + flex-direction: column; + background: var(--bg); +} + +.home-main { + flex: 1; + display: flex; + flex-direction: column; + align-items: stretch; + padding: 24px; + text-align: left; +} + +.home-editor { + display: flex; + justify-content: flex-end; + margin-bottom: 8px; +} + +.home-projects { + margin-top: 8px; +} + +.home-projects-head { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--border); + + h2 { + font-size: 18px; + } +} + +.home-projects-content { + min-height: 64px; + display: flex; + align-items: center; + justify-content: center; +} + +.home-projects-list { + display: flex; + flex-direction: row; + width: 100%; + gap: 16px; + margin: 16px 0 0; + padding: 0; + list-style: none; + + a { + color: var(--text-h); + text-decoration: none; + } + + li { + min-width: 0; + padding: 8px 16px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text-h); + cursor: pointer; + } + + li:hover { + color: var(--text-h); + background: #efefef; + } + +} + +.home-header { + display: flex; + align-items: center; + justify-content: space-between; + height: 56px; + padding: 0 24px; + border-bottom: 1px solid var(--border); +} + +.home-brand { + font-weight: 600; + color: var(--text-h); +} + +.home-account a { + color: var(--text-h); + text-decoration: none; +} diff --git a/src/features/home/presentation/home/home.tsx b/src/features/home/presentation/home/home.tsx new file mode 100644 index 0000000..cd3f57d --- /dev/null +++ b/src/features/home/presentation/home/home.tsx @@ -0,0 +1,102 @@ +import '@/features/project/project'; + +import { Link, useNavigate } from 'react-router'; +import { observer } from 'mobx-react-lite'; +import { Avatar, Button } from 'antd'; +import { UserOutlined } from '@ant-design/icons'; +import { useAuthenticated } from '@/app/session/use_authenticated'; +import { useViewModel } from '@/app/lifecycle/use_view_model'; +import { ProjectStore } from '@/features/project/domain/enum/project_store'; +import type { Project } from '@/features/project/domain/entities/project'; +import { HomeViewModel } from './home_view_model'; +import './home.scss'; + +function ProjectSection({ + title, + store, + projects, + to, +}: { + title: string; + store: ProjectStore; + projects: Project[]; + to: string; +}) { + const visible = projects.slice(0, 5); + + return ( +
+
+

{title}

+ + + +
+
+ {visible.length === 0 ? ( +

还没有项目

+ ) : ( +
    + {visible.map((project) => ( +
  • { + window.location.href = `/editor/${project.id}?store=${store}`; + }}> + {project.name} +
  • + ))} +
+ )} +
+
+ ); +} + +export default observer(function HomePage() { + const authenticated = useAuthenticated(); + const navigate = useNavigate(); + const vm = useViewModel(HomeViewModel); + + return ( +
+
+ BQ React +
+ {authenticated ? ( + + } /> + + ) : ( + Login + )} +
+
+
+
+ +
+ {vm.errorMessage &&

{vm.errorMessage}

} + + +
+
+ ); +}); diff --git a/src/features/home/presentation/home/home_view_model.ts b/src/features/home/presentation/home/home_view_model.ts new file mode 100644 index 0000000..b7263b7 --- /dev/null +++ b/src/features/home/presentation/home/home_view_model.ts @@ -0,0 +1,45 @@ +import { injectable } from 'inversify'; +import { makeObservable, observable, runInAction } from 'mobx'; +import { PageViewModel } from '@/app/lifecycle/page_view_model'; +import { ProjectApplication } from '@/features/project/application/project_application'; +import { ProjectStore } from '@/features/project/domain/enum/project_store'; +import type { Project } from '@/features/project/domain/entities/project'; + +@injectable('Transient') +export class HomeViewModel extends PageViewModel { + cloudProjects: Project[] = []; + localProjects: Project[] = []; + errorMessage = ''; + + constructor(private application: ProjectApplication) { + super(); + makeObservable(this, { + cloudProjects: observable, + localProjects: observable, + errorMessage: observable, + }); + } + + async ready(): Promise { + await this.reload(); + } + + async reload(): Promise { + const [cloud, local] = await Promise.allSettled([ + this.application.list(ProjectStore.REMOTE), + this.application.list(ProjectStore.LOCAL), + ]); + runInAction(() => { + this.cloudProjects = cloud.status === 'fulfilled' ? cloud.value : []; + this.localProjects = local.status === 'fulfilled' ? local.value : []; + this.errorMessage = [cloud, local] + .filter((result) => result.status === 'rejected') + .map((result) => (result.reason instanceof Error ? result.reason.message : '加载失败')) + .join(' '); + }); + } + + createLocal(): Promise { + return this.application.create(ProjectStore.LOCAL); + } +} diff --git a/src/features/project/application/project_application.ts b/src/features/project/application/project_application.ts new file mode 100644 index 0000000..3bff6ad --- /dev/null +++ b/src/features/project/application/project_application.ts @@ -0,0 +1,35 @@ +import { inject, injectable, named } from 'inversify'; +import { Project } from '../domain/entities/project'; +import { ProjectStore } from '../domain/enum/project_store'; +import { IProjectRepository } from '../domain/repositories/i_project_repository'; + +@injectable('Singleton') +export class ProjectApplication { + constructor( + @inject(IProjectRepository) @named(ProjectStore.LOCAL) private localRepository: IProjectRepository, + @inject(IProjectRepository) @named(ProjectStore.REMOTE) private remoteRepository: IProjectRepository, + ) {} + + list(store: ProjectStore): Promise { + return this.repository(store).list(); + } + + get(store: ProjectStore, id: string): Promise { + return this.repository(store).get(id); + } + + async save(store: ProjectStore, project: Project): Promise { + project.touch(); + await this.repository(store).save(project); + } + + async create(store: ProjectStore, name = '未命名'): Promise { + const project = Project.create(name); + await this.repository(store).save(project); + return project; + } + + private repository(store: ProjectStore): IProjectRepository { + return store === ProjectStore.REMOTE ? this.remoteRepository : this.localRepository; + } +} diff --git a/src/features/project/domain/entities/document.ts b/src/features/project/domain/entities/document.ts new file mode 100644 index 0000000..493e555 --- /dev/null +++ b/src/features/project/domain/entities/document.ts @@ -0,0 +1,34 @@ +import { Model, model, modelAction, tProp, types } from 'mobx-keystone'; + +@model('project/LineShape') +export class LineShape extends Model({ + type: tProp(types.literal('project/LineShape'), 'project/LineShape'), + id: tProp(types.string), + x1: tProp(types.number), + y1: tProp(types.number), + x2: tProp(types.number), + y2: tProp(types.number), +}) {} + +@model('project/RectShape') +export class RectShape extends Model({ + type: tProp(types.literal('project/RectShape'), 'project/RectShape'), + id: tProp(types.string), + x: tProp(types.number), + y: tProp(types.number), + width: tProp(types.number), + height: tProp(types.number), +}) {} + +export type Shape = LineShape | RectShape; + +@model('project/Document') +export class Document extends Model({ + type: tProp(types.literal('project/Document'), 'project/Document'), + shapes: tProp(types.array(types.or(types.model(LineShape), types.model(RectShape))), () => []), +}) { + @modelAction + addShape(shape: Shape) { + this.shapes.push(shape); + } +} diff --git a/src/features/project/domain/entities/project.ts b/src/features/project/domain/entities/project.ts new file mode 100644 index 0000000..6966f02 --- /dev/null +++ b/src/features/project/domain/entities/project.ts @@ -0,0 +1,116 @@ +import { makeObservable, observable } from 'mobx'; +import { + applySnapshot, + fromSnapshot, + getSnapshot, + Model, + model, + modelAction, + tProp, + types, +} from 'mobx-keystone'; +import { Document, LineShape, type Shape } from './document'; + +const MIN_STROKE = 2; + +@model('project/Project') +export class Project extends Model({ + type: tProp(types.literal('project/Project'), 'project/Project'), + id: tProp(types.string), + name: tProp(types.string), + updatedAt: tProp(types.number), + document: tProp(types.model(Document), () => new Document({})), +}) { + past: object[] = []; + future: object[] = []; + + static create(name = '未命名'): Project { + return new Project({ + id: crypto.randomUUID(), + name, + updatedAt: Date.now(), + document: new Document({}), + }); + } + + static fromData(data: unknown): Project { + return fromSnapshot(Project, withModelType(data) as never); + } + + protected onInit(): void { + makeObservable(this, { + past: observable, + future: observable, + }); + } + + @modelAction + touch() { + this.updatedAt = Date.now(); + } + + get canUndo(): boolean { + return this.past.length > 0; + } + + get canRedo(): boolean { + return this.future.length > 0; + } + + @modelAction + addShape(shape: Shape): boolean { + if (strokeTooSmall(shape)) return false; + this.past.push(getSnapshot(this.document)); + this.future = []; + this.document.addShape(shape); + return true; + } + + @modelAction + undo(): boolean { + const previous = this.past.pop(); + if (!previous) return false; + this.future.push(getSnapshot(this.document)); + applySnapshot(this.document, previous as never); + return true; + } + + @modelAction + redo(): boolean { + const next = this.future.pop(); + if (!next) return false; + this.past.push(getSnapshot(this.document)); + applySnapshot(this.document, next as never); + return true; + } +} + +/** 报文 type 到 @model 名称。两边的字符串可以不同。 */ +const modelByType: Record = { + 'project/Project': 'project/Project', + 'project/Document': 'project/Document', + 'project/LineShape': 'project/LineShape', + 'project/RectShape': 'project/RectShape', +}; + +function withModelType(value: unknown): unknown { + if (Array.isArray(value)) return value.map(withModelType); + if (!value || typeof value !== 'object') return value; + const result: Record = {}; + for (const [key, item] of Object.entries(value)) { + if (key.startsWith('$')) continue; + result[key] = withModelType(item); + } + if (typeof result.type === 'string') { + const modelType = modelByType[result.type]; + if (modelType) result.$modelType = modelType; + } + return result; +} + +function strokeTooSmall(shape: Shape): boolean { + if (shape instanceof LineShape) { + return Math.hypot(shape.x2 - shape.x1, shape.y2 - shape.y1) < MIN_STROKE; + } + return Math.hypot(shape.width, shape.height) < MIN_STROKE; +} diff --git a/src/features/project/domain/enum/project_store.ts b/src/features/project/domain/enum/project_store.ts new file mode 100644 index 0000000..57a0b8c --- /dev/null +++ b/src/features/project/domain/enum/project_store.ts @@ -0,0 +1,4 @@ +export enum ProjectStore { + LOCAL = 'local', + REMOTE = 'remote', +} diff --git a/src/features/project/domain/repositories/i_project_repository.ts b/src/features/project/domain/repositories/i_project_repository.ts new file mode 100644 index 0000000..424269d --- /dev/null +++ b/src/features/project/domain/repositories/i_project_repository.ts @@ -0,0 +1,7 @@ +import type { Project } from '../entities/project'; + +export abstract class IProjectRepository { + abstract list(): Promise; + abstract get(id: string): Promise; + abstract save(project: Project): Promise; +} diff --git a/src/features/project/infra/persistence/local_project_repository.ts b/src/features/project/infra/persistence/local_project_repository.ts new file mode 100644 index 0000000..63eb542 --- /dev/null +++ b/src/features/project/infra/persistence/local_project_repository.ts @@ -0,0 +1,64 @@ +import { injectable } from 'inversify'; +import { fromSnapshot, getSnapshot } from 'mobx-keystone'; +import { Database } from '@/app/db/database'; +import { PROJECTS_STORE } from '@/app/db/schema/v1'; +import { bindAs } from '@/app/di/bind_as'; +import { ProjectStore } from '../../domain/enum/project_store'; +import { Project } from '../../domain/entities/project'; +import { IProjectRepository } from '../../domain/repositories/i_project_repository'; + +@bindAs(IProjectRepository, ProjectStore.LOCAL) +@injectable('Singleton') +export class LocalProjectRepository implements IProjectRepository { + constructor(private database: Database) {} + + async list(): Promise { + const records = await this.readAll(); + return records + .map((record) => fromSnapshot(Project, record as never)) + .sort((a, b) => b.updatedAt - a.updatedAt); + } + + async get(id: string): Promise { + const db = await this.ensure(); + const record = await requestToPromise( + db.transaction(PROJECTS_STORE, 'readonly').objectStore(PROJECTS_STORE).get(id), + ); + if (!record) return null; + return fromSnapshot(Project, record as never); + } + + async save(project: Project): Promise { + const db = await this.ensure(); + await requestToPromise( + db + .transaction(PROJECTS_STORE, 'readwrite') + .objectStore(PROJECTS_STORE) + .put(getSnapshot(project)), + ); + } + + private async readAll(): Promise { + const db = await this.ensure(); + const records = await requestToPromise( + db.transaction(PROJECTS_STORE, 'readonly').objectStore(PROJECTS_STORE).getAll(), + ); + return Array.isArray(records) ? records : []; + } + + private async ensure(allowReset = true): Promise { + const db = await this.database.open(); + if (db.objectStoreNames.contains(PROJECTS_STORE)) return db; + if (!allowReset) throw new Error('Missing projects store'); + db.close(); + await this.database.reset(); + return this.ensure(false); + } +} + +function requestToPromise(request: IDBRequest): Promise { + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed')); + }); +} diff --git a/src/features/project/infra/persistence/remote_project_repository.ts b/src/features/project/infra/persistence/remote_project_repository.ts new file mode 100644 index 0000000..2d5b544 --- /dev/null +++ b/src/features/project/infra/persistence/remote_project_repository.ts @@ -0,0 +1,34 @@ +import { injectable } from 'inversify'; +import { getSnapshot } from 'mobx-keystone'; +import { Http } from '@/app/network/http'; +import { bindAs } from '@/app/di/bind_as'; +import { ProjectStore } from '../../domain/enum/project_store'; +import { Project } from '../../domain/entities/project'; +import { IProjectRepository } from '../../domain/repositories/i_project_repository'; + +@bindAs(IProjectRepository, ProjectStore.REMOTE) +@injectable('Singleton') +export class RemoteProjectRepository implements IProjectRepository { + url = '/projects'; + constructor(private http: Http) {} + + async list(): Promise { + const data = (await this.http.instance.get(this.url)) as unknown; + if (!Array.isArray(data)) return []; + return data.map((item) => Project.fromData(item)); + } + + async get(id: string): Promise { + try { + const data = (await this.http.instance.get(`${this.url}/${id}`)) as unknown; + if (!data || typeof data !== 'object') return null; + return Project.fromData(data); + } catch { + return null; + } + } + + async save(project: Project): Promise { + await this.http.instance.put(`${this.url}/${project.id}`, getSnapshot(project)); + } +} diff --git a/src/features/project/presentation/local_list/local_project_list.tsx b/src/features/project/presentation/local_list/local_project_list.tsx new file mode 100644 index 0000000..5b82c4a --- /dev/null +++ b/src/features/project/presentation/local_list/local_project_list.tsx @@ -0,0 +1,7 @@ +import '../../project'; +import { ProjectStore } from '../../domain/enum/project_store'; +import { ProjectListPage } from '../project_list/project_list_page'; + +export default function LocalProjectList() { + return ; +} diff --git a/src/features/project/presentation/project_list/project_list.scss b/src/features/project/presentation/project_list/project_list.scss new file mode 100644 index 0000000..90892e6 --- /dev/null +++ b/src/features/project/presentation/project_list/project_list.scss @@ -0,0 +1,28 @@ +.project-list-page { + padding: 24px; + text-align: left; + + h1 { + margin-top: 0; + } +} + +.project-list { + display: flex; + flex-direction: column; + gap: 12px; + margin: 0; + padding: 0; + list-style: none; + + a { + color: var(--text-h); + text-decoration: none; + } + + li { + padding: 16px; + border: 1px solid var(--border); + border-radius: 8px; + } +} diff --git a/src/features/project/presentation/project_list/project_list_page.tsx b/src/features/project/presentation/project_list/project_list_page.tsx new file mode 100644 index 0000000..9e487c2 --- /dev/null +++ b/src/features/project/presentation/project_list/project_list_page.tsx @@ -0,0 +1,37 @@ +import { useEffect } from 'react'; +import { observer } from 'mobx-react-lite'; +import { useViewModel } from '@/app/lifecycle/use_view_model'; +import type { ProjectStore } from '../../domain/enum/project_store'; +import { ProjectRow } from '../project_row/project_row'; +import { ProjectListViewModel } from './project_list_view_model'; +import './project_list.scss'; + +export const ProjectListPage = observer(function ProjectListPage({ + title, + store, +}: { + title: string; + store: ProjectStore; +}) { + const vm = useViewModel(ProjectListViewModel); + + useEffect(() => { + void vm.load(store); + }, [vm, store]); + + return ( +
+

{title}

+ {vm.errorMessage &&

{vm.errorMessage}

} + {vm.projects.length === 0 ? ( +

还没有项目

+ ) : ( +
    + {vm.projects.map((project) => ( + + ))} +
+ )} +
+ ); +}); diff --git a/src/features/project/presentation/project_list/project_list_view_model.ts b/src/features/project/presentation/project_list/project_list_view_model.ts new file mode 100644 index 0000000..d3b168a --- /dev/null +++ b/src/features/project/presentation/project_list/project_list_view_model.ts @@ -0,0 +1,35 @@ +import { injectable } from 'inversify'; +import { makeObservable, observable, runInAction } from 'mobx'; +import { PageViewModel } from '@/app/lifecycle/page_view_model'; +import { ProjectApplication } from '../../application/project_application'; +import type { ProjectStore } from '../../domain/enum/project_store'; +import type { Project } from '../../domain/entities/project'; + +@injectable('Transient') +export class ProjectListViewModel extends PageViewModel { + projects: Project[] = []; + errorMessage = ''; + + constructor(private application: ProjectApplication) { + super(); + makeObservable(this, { + projects: observable, + errorMessage: observable, + }); + } + + async load(store: ProjectStore): Promise { + try { + const projects = await this.application.list(store); + runInAction(() => { + this.projects = projects; + this.errorMessage = ''; + }); + } catch (error) { + runInAction(() => { + this.projects = []; + this.errorMessage = error instanceof Error ? error.message : '加载失败'; + }); + } + } +} diff --git a/src/features/project/presentation/project_row/project_row.tsx b/src/features/project/presentation/project_row/project_row.tsx new file mode 100644 index 0000000..3ba0b31 --- /dev/null +++ b/src/features/project/presentation/project_row/project_row.tsx @@ -0,0 +1,11 @@ +import { Link } from 'react-router'; +import type { ProjectStore } from '../../domain/enum/project_store'; +import type { Project } from '../../domain/entities/project'; + +export function ProjectRow({ project, store }: { project: Project; store: ProjectStore }) { + return ( +
  • + {project.name} +
  • + ); +} diff --git a/src/features/project/presentation/remote_list/remote_project_list.tsx b/src/features/project/presentation/remote_list/remote_project_list.tsx new file mode 100644 index 0000000..4a73d4d --- /dev/null +++ b/src/features/project/presentation/remote_list/remote_project_list.tsx @@ -0,0 +1,7 @@ +import '../../project'; +import { ProjectStore } from '../../domain/enum/project_store'; +import { ProjectListPage } from '../project_list/project_list_page'; + +export default function RemoteProjectList() { + return ; +} diff --git a/src/features/project/project.ts b/src/features/project/project.ts new file mode 100644 index 0000000..ffed414 --- /dev/null +++ b/src/features/project/project.ts @@ -0,0 +1,2 @@ +import './infra/persistence/local_project_repository'; +import './infra/persistence/remote_project_repository'; diff --git a/src/features/security/application/security_application.ts b/src/features/security/application/security_application.ts new file mode 100644 index 0000000..eec37a4 --- /dev/null +++ b/src/features/security/application/security_application.ts @@ -0,0 +1,36 @@ +// src/features/security/application/security_application.ts +import { injectable } from 'inversify'; +import { TokenStore } from '@/app/session/token_store'; +import type { AuthCredentials } from '../domain/entities/auth_credentials'; +import type { RegisterCredentials } from '../domain/entities/register_credentials'; +import { ISecurityRepository } from '../domain/repositories/i_security_repository'; + +@injectable('Singleton') +export class SecurityApplication { + constructor( + private repository: ISecurityRepository, + private tokenStore: TokenStore, + ) {} + + /** + * 登录用例 + */ + async login(credentials: AuthCredentials): Promise { + const result = await this.repository.login(credentials); + this.tokenStore.save(result.accessToken); + } + + /** + * 登出用例:清理用户登录凭证 + */ + async logout(): Promise { + this.tokenStore.remove(); + } + + /** + * 注册用例 + */ + public async register(credentials: RegisterCredentials): Promise { + await this.repository.register(credentials); + } +} diff --git a/src/features/security/domain/entities/auth_credentials.ts b/src/features/security/domain/entities/auth_credentials.ts new file mode 100644 index 0000000..beceadd --- /dev/null +++ b/src/features/security/domain/entities/auth_credentials.ts @@ -0,0 +1,35 @@ +// src/features/security/domain/entities/auth_credentials.ts +import { Model, model, modelAction, tProp, types } from 'mobx-keystone'; + +@model('security/AuthCredentials') +export class AuthCredentials extends Model({ + username: tProp(types.string, ''), + password: tProp(types.string, ''), +}) { + @modelAction + setUsername(value: string) { + this.username = value; + } + + @modelAction + setPassword(value: string) { + this.password = value; + } + + public get isValid(): boolean { + return this.isUsernameValid && this.isPasswordValid; + } + + public get isUsernameValid(): boolean { + return this.username.trim().length >= 3; + } + public get isPasswordValid(): boolean { + return this.password.length >= 6; + } + + public get validationErrorKey(): string { + if (!this.isUsernameValid) return 'security.err_username_length'; + if (!this.isPasswordValid) return 'security.err_password_length'; + return ''; + } +} diff --git a/src/features/security/domain/entities/login_result.ts b/src/features/security/domain/entities/login_result.ts new file mode 100644 index 0000000..460908c --- /dev/null +++ b/src/features/security/domain/entities/login_result.ts @@ -0,0 +1,8 @@ +import type { User } from './user'; + +export class LoginResult { + constructor( + readonly user: User, + readonly accessToken: string, + ) {} +} diff --git a/src/features/security/domain/entities/register_credentials.ts b/src/features/security/domain/entities/register_credentials.ts new file mode 100644 index 0000000..1d49294 --- /dev/null +++ b/src/features/security/domain/entities/register_credentials.ts @@ -0,0 +1,23 @@ +import { ExtendedModel, model, modelAction, tProp, types } from 'mobx-keystone'; +import { AuthCredentials } from './auth_credentials'; + +@model('security/RegisterCredentials') +export class RegisterCredentials extends ExtendedModel(AuthCredentials, { + confirmPassword: tProp(types.string, ''), +}) { + @modelAction + setConfirmPassword(value: string) { + this.confirmPassword = value; + } + + public override get isValid(): boolean { + return super.isValid && this.password === this.confirmPassword; + } + + public override get validationErrorKey(): string { + const parentKey = super.validationErrorKey; + if (parentKey) return parentKey; + if (this.password !== this.confirmPassword) return 'security.err_password_mismatch'; + return ''; + } +} diff --git a/src/features/security/domain/entities/user.ts b/src/features/security/domain/entities/user.ts new file mode 100644 index 0000000..eef0899 --- /dev/null +++ b/src/features/security/domain/entities/user.ts @@ -0,0 +1,16 @@ +export class User { + public readonly id: string; + public readonly username: string; + public readonly roles: string[]; + + constructor(id: string, username: string, roles: string[]) { + this.id = id; + this.username = username; + this.roles = roles; + } + + public hasPermission(permission: string): boolean { + if (this.roles.includes('admin')) return true; + return this.roles.includes(permission); + } +} diff --git a/src/features/security/domain/enum/auth_enum.ts b/src/features/security/domain/enum/auth_enum.ts new file mode 100644 index 0000000..8168a28 --- /dev/null +++ b/src/features/security/domain/enum/auth_enum.ts @@ -0,0 +1,4 @@ +export enum AuthEnum { + LOGIN = 'login', + REGISTER = 'register', +} diff --git a/src/features/security/domain/repositories/i_security_repository.ts b/src/features/security/domain/repositories/i_security_repository.ts new file mode 100644 index 0000000..7cef482 --- /dev/null +++ b/src/features/security/domain/repositories/i_security_repository.ts @@ -0,0 +1,8 @@ +import type { AuthCredentials } from '../entities/auth_credentials'; +import type { LoginResult } from '../entities/login_result'; +import type { RegisterCredentials } from '../entities/register_credentials'; + +export abstract class ISecurityRepository { + abstract login(credentials: AuthCredentials): Promise; + abstract register(credentials: RegisterCredentials): Promise; +} diff --git a/src/features/security/i18n/en_us.ts b/src/features/security/i18n/en_us.ts new file mode 100644 index 0000000..27bd293 --- /dev/null +++ b/src/features/security/i18n/en_us.ts @@ -0,0 +1,14 @@ +export const enUS = { + title: 'Example Application', + login_tab: 'Sign In', + register_tab: 'Sign Up', + username_placeholder: 'Enter username', + password_placeholder: 'Enter password', + confirm_password_placeholder: 'Confirm password', + btn_login: 'Login Now', + btn_register: 'Register', + err_username_length: 'Username must be at least 3 characters', + err_password_length: 'Password must be at least 6 characters', + err_password_mismatch: 'Passwords do not match', + err_empty: 'Credentials cannot be empty', +}; diff --git a/src/features/security/i18n/index.ts b/src/features/security/i18n/index.ts new file mode 100644 index 0000000..ffba7aa --- /dev/null +++ b/src/features/security/i18n/index.ts @@ -0,0 +1,6 @@ +// features/security/i18n/index.ts +import { registerMessages } from '@/app/i18n/catalog'; +import { zhCN } from './zh_cn'; +import { enUS } from './en_us'; + +registerMessages('security', { zh: zhCN, en: enUS }); diff --git a/src/features/security/i18n/zh_cn.ts b/src/features/security/i18n/zh_cn.ts new file mode 100644 index 0000000..3340f98 --- /dev/null +++ b/src/features/security/i18n/zh_cn.ts @@ -0,0 +1,14 @@ +export const zhCN = { + title: '示例应用', + login_tab: '账户登录', + register_tab: '新用户注册', + username_placeholder: '请输入用户名', + password_placeholder: '请输入密码', + confirm_password_placeholder: '请再次确认登录密码', + btn_login: '立 即 登 录', + btn_register: '注 册 账 户', + err_username_length: '用户名长度不能少于 3 位', + err_password_length: '密码长度不能少于 6 位', + err_password_mismatch: '两次输入的密码不一致', + err_empty: '账户凭证输入不能为空', +}; diff --git a/src/features/security/infra/persistence/security_repository.ts b/src/features/security/infra/persistence/security_repository.ts new file mode 100644 index 0000000..86c178f --- /dev/null +++ b/src/features/security/infra/persistence/security_repository.ts @@ -0,0 +1,42 @@ +import { injectable } from 'inversify'; +import { getSnapshot } from 'mobx-keystone'; +import { Http } from '@/app/network/http'; +import { bindAs } from '@/app/di/bind_as'; +import { AuthCredentials } from '../../domain/entities/auth_credentials'; +import { LoginResult } from '../../domain/entities/login_result'; +import { RegisterCredentials } from '../../domain/entities/register_credentials'; +import { User } from '../../domain/entities/user'; +import { ISecurityRepository } from '../../domain/repositories/i_security_repository'; + +type LoginResponse = { + token?: unknown; + user?: { id?: unknown; username?: unknown; roles?: unknown }; +}; + +@bindAs(ISecurityRepository) +@injectable('Singleton') +export class SecurityRepository implements ISecurityRepository { + constructor(private http: Http) {} + + public async login(credentials: AuthCredentials): Promise { + const data = (await this.http.instance.post( + '/auth/login', + getSnapshot(credentials), + )) as LoginResponse; + const token = typeof data.token === 'string' ? data.token : ''; + const body = data.user; + const roles = Array.isArray(body?.roles) + ? body.roles.filter((role) => typeof role === 'string') + : []; + const user = new User( + typeof body?.id === 'string' ? body.id : '', + typeof body?.username === 'string' ? body.username : credentials.username, + roles, + ); + return new LoginResult(user, token); + } + + public async register(credentials: RegisterCredentials): Promise { + await this.http.instance.post('/auth/register', getSnapshot(credentials)); + } +} diff --git a/src/features/security/presentation/profile/profile.tsx b/src/features/security/presentation/profile/profile.tsx new file mode 100644 index 0000000..15c6ce7 --- /dev/null +++ b/src/features/security/presentation/profile/profile.tsx @@ -0,0 +1,7 @@ +export default function ProfilePage() { + return ( +
    +

    Profile

    +
    + ); +} diff --git a/src/features/security/presentation/security/security.scss b/src/features/security/presentation/security/security.scss new file mode 100644 index 0000000..aeb579b --- /dev/null +++ b/src/features/security/presentation/security/security.scss @@ -0,0 +1,31 @@ +.auth-wrapper { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background-color: #f5f7fa; + + .auth-card { + width: 100%; + max-width: 420px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + border-radius: 8px; + background: #fff; + + .undo-redo-bar { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-bottom: 12px; + } + + .error-alert { + margin-bottom: 16px; + } + + .submit-btn { + width: 100%; + margin-top: 16px; + } + } +} diff --git a/src/features/security/presentation/security/security.tsx b/src/features/security/presentation/security/security.tsx new file mode 100644 index 0000000..5ba5fe6 --- /dev/null +++ b/src/features/security/presentation/security/security.tsx @@ -0,0 +1,99 @@ +import '../../security'; + +import React from 'react'; +import { observer } from 'mobx-react-lite'; +import { Card, Tabs, Input, Button, Alert, Space } from 'antd'; +import { UserOutlined, LockOutlined } from '@ant-design/icons'; +import { useNavigate, useSearchParams } from 'react-router'; +import { useTranslation } from 'react-i18next'; // 💡 引入 react-i18next 的渲染钩子 +import { safeInternalPath } from '@/app/session/auth_session'; +import { useViewModel } from '@/app/lifecycle/use_view_model'; +import { SecurityViewModel } from './security_view_model'; +import { AuthEnum } from '../../domain/enum/auth_enum'; + +import './security.scss'; + +export const SecurityView: React.FC = observer(() => { + const vm = useViewModel(SecurityViewModel); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const { t } = useTranslation(); // 💡 消费全局注册的多语言包 + + const tabItems = [ + { key: AuthEnum.LOGIN, label: t('security.login_tab') }, + { key: AuthEnum.REGISTER, label: t('security.register_tab') }, + ]; + + return ( +
    + + {vm.errorMessage && ( + + )} + + vm.switchTab(key as AuthEnum)} + /> + + {vm.activeTab === AuthEnum.LOGIN && ( + + } + placeholder={t('security.username_placeholder')} + value={vm.loginForm.username} + // onChange={(e) => vm.loginForm.username = e.target.value} + onChange={(e) => vm.loginForm.setUsername(e.target.value)} + /> + } + placeholder={t('security.password_placeholder')} + value={vm.loginForm.password} + onChange={(e) => vm.loginForm.setPassword(e.target.value)} + /> + + )} + + {vm.activeTab === AuthEnum.REGISTER && ( + + } + placeholder={t('security.username_placeholder')} + value={vm.registerForm.username} + // onChange={(e) => vm.registerForm.username = e.target.value} + onChange={(e) => vm.registerForm.setUsername(e.target.value)} + /> + } + placeholder={t('security.password_placeholder')} + value={vm.registerForm.password} + onChange={(e) => vm.registerForm.setPassword(e.target.value)} + /> + } + placeholder={t('security.confirm_password_placeholder')} + value={vm.registerForm.confirmPassword} + onChange={(e) => vm.registerForm.setConfirmPassword(e.target.value)} + /> + + )} + + + +
    + ); +}); + +export default SecurityView; diff --git a/src/features/security/presentation/security/security_view_model.ts b/src/features/security/presentation/security/security_view_model.ts new file mode 100644 index 0000000..6589a28 --- /dev/null +++ b/src/features/security/presentation/security/security_view_model.ts @@ -0,0 +1,73 @@ +// src/features/security/presentation/security_view_model.ts +import { injectable } from 'inversify'; +import { makeObservable, observable, action, runInAction } from 'mobx'; +import { SecurityApplication } from '../../application/security_application'; +import { I18n } from '@/app/i18n/i18n'; +import { AuthEnum } from '../../domain/enum/auth_enum'; +import { AuthCredentials } from '../../domain/entities/auth_credentials'; +import { RegisterCredentials } from '../../domain/entities/register_credentials'; +import { PageViewModel } from '@/app/lifecycle/page_view_model'; + +@injectable('Transient') +export class SecurityViewModel extends PageViewModel { + public activeTab: AuthEnum = AuthEnum.LOGIN; + public isLoading = false; + public errorMessage = ''; + + public loginForm = new AuthCredentials({}); + public registerForm = new RegisterCredentials({}); + + constructor( + private application: SecurityApplication, + private i18n: I18n, + ) { + super(); + makeObservable(this, { + activeTab: observable, + isLoading: observable, + errorMessage: observable, + switchTab: action, + submit: action, + }); + } + + dispose(): void { + this.errorMessage = ''; + } + + public switchTab(tab: AuthEnum) { + this.activeTab = tab; + this.errorMessage = ''; + } + + public async submit(): Promise { + this.errorMessage = ''; + const currentForm = this.activeTab === AuthEnum.LOGIN ? this.loginForm : this.registerForm; + + if (!currentForm.isValid) { + this.errorMessage = this.i18n.translate(currentForm.validationErrorKey); + return false; + } + + this.isLoading = true; + + try { + if (this.activeTab === AuthEnum.LOGIN) { + await this.application.login(this.loginForm); + return true; + } + await this.application.register(this.registerForm); + runInAction(() => this.switchTab(AuthEnum.LOGIN)); + return false; + } catch (err: any) { + runInAction(() => { + this.errorMessage = err.message || 'Error'; + }); + return false; + } finally { + runInAction(() => { + this.isLoading = false; + }); + } + } +} diff --git a/src/features/security/security.ts b/src/features/security/security.ts new file mode 100644 index 0000000..830a4a8 --- /dev/null +++ b/src/features/security/security.ts @@ -0,0 +1,2 @@ +import './i18n'; +import './infra/persistence/security_repository'; diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..fd0d09c --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + // "erasableSyntaxOnly": false, + "noFallthroughCasesInSwitch": true, + /* 装饰器 */ + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "paths": { + "@/*": ["./src/*"] // 路径别名 + } + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d32ff68 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..042b652 --- /dev/null +++ b/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", "react-router.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..5a01044 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +import { reactRouter } from '@react-router/dev/vite'; +import path from 'path'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [reactRouter()], + envDir: 'src', + resolve: { + alias: { '@': path.resolve(__dirname, './src') }, + }, +});