From 94dd560331896f3c18accbc0e11ed8937bf20468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E5=AE=9D=E9=BE=99?= Date: Tue, 28 Jul 2026 12:52:36 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 18 +- README.md | 108 +- .../example/training/common/ApiResponse.java | 17 + .../training/config/MybatisPlusConfig.java | 1 + .../training/config/RequestIdFilter.java | 33 + .../example/training/config/WebConfig.java | 18 + .../example/training/constant/ErrorCode.java | 25 + .../training/controller/HealthController.java | 7 +- .../training/controller/UserController.java | 100 +- .../com/example/training/domain/User.java | 55 +- .../domain/dto/BulkStatusRequest.java | 13 + .../training/domain/dto/UserRequest.java | 13 + .../example/training/domain/entity/User.java | 77 + .../training/domain/vo/BulkStatusVo.java | 4 + .../training/domain/vo/UserPageVo.java | 6 + .../training/domain/vo/UserSummaryVo.java | 4 + .../example/training/domain/vo/UserVo.java | 14 + .../exception/GlobalExceptionHandler.java | 60 + .../example/training/mapper/UserMapper.java | 2 +- .../example/training/service/UserService.java | 2 +- .../service/impl/UserServiceImpl.java | 21 +- .../training/util/RequestIdContext.java | 24 + backend/src/main/resources/application.yml | 5 +- backend/src/main/resources/schema.sql | 13 - .../training/TrainingApplicationTests.java | 13 +- .../example/training/UserControllerTests.java | 38 +- frontend/.env.example | 2 +- frontend/package-lock.json | 3321 +++++++++++++++-- frontend/package.json | 17 +- frontend/postcss.config.js | 6 + frontend/src/App.tsx | 332 +- frontend/src/assets/README.md | 3 + frontend/src/components/README.md | 3 + frontend/src/constants/user.ts | 24 + frontend/src/hooks/README.md | 3 + frontend/src/http/client.test.ts | 38 + frontend/src/http/client.ts | 59 + frontend/src/http/userApi.ts | 47 + frontend/src/interfaces/user/api.ts | 24 + frontend/src/interfaces/user/model.ts | 29 + frontend/src/routes/index.tsx | 10 + .../src/routes/user-management/Loader.tsx | 11 + frontend/src/routes/user-management/Page.tsx | 113 + .../DirectoryToolbar/index.module.scss | 3 + .../components/DirectoryToolbar/index.tsx | 48 + .../components/SummaryCards/index.module.scss | 3 + .../components/SummaryCards/index.tsx | 41 + .../components/UserDrawer/index.module.scss | 3 + .../components/UserDrawer/index.tsx | 46 + .../components/UserTable/index.module.scss | 3 + .../components/UserTable/index.tsx | 40 + .../routes/user-management/index.module.scss | 3 + frontend/src/stores/userStore.ts | 142 + frontend/src/styles.css | 4 + frontend/src/test/setup.ts | 1 + frontend/src/utils/classNames.ts | 5 + frontend/src/utils/user.test.ts | 14 + frontend/src/utils/user.ts | 21 + frontend/tailwind.config.js | 8 + frontend/vitest.config.ts | 11 + sql/users.sql | 20 +- 61 files changed, 4243 insertions(+), 906 deletions(-) create mode 100644 backend/src/main/java/com/example/training/common/ApiResponse.java create mode 100644 backend/src/main/java/com/example/training/config/RequestIdFilter.java create mode 100644 backend/src/main/java/com/example/training/config/WebConfig.java create mode 100644 backend/src/main/java/com/example/training/constant/ErrorCode.java create mode 100644 backend/src/main/java/com/example/training/domain/dto/BulkStatusRequest.java create mode 100644 backend/src/main/java/com/example/training/domain/dto/UserRequest.java create mode 100644 backend/src/main/java/com/example/training/domain/entity/User.java create mode 100644 backend/src/main/java/com/example/training/domain/vo/BulkStatusVo.java create mode 100644 backend/src/main/java/com/example/training/domain/vo/UserPageVo.java create mode 100644 backend/src/main/java/com/example/training/domain/vo/UserSummaryVo.java create mode 100644 backend/src/main/java/com/example/training/domain/vo/UserVo.java create mode 100644 backend/src/main/java/com/example/training/exception/GlobalExceptionHandler.java create mode 100644 backend/src/main/java/com/example/training/util/RequestIdContext.java delete mode 100644 backend/src/main/resources/schema.sql create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/assets/README.md create mode 100644 frontend/src/components/README.md create mode 100644 frontend/src/constants/user.ts create mode 100644 frontend/src/hooks/README.md create mode 100644 frontend/src/http/client.test.ts create mode 100644 frontend/src/http/client.ts create mode 100644 frontend/src/http/userApi.ts create mode 100644 frontend/src/interfaces/user/api.ts create mode 100644 frontend/src/interfaces/user/model.ts create mode 100644 frontend/src/routes/index.tsx create mode 100644 frontend/src/routes/user-management/Loader.tsx create mode 100644 frontend/src/routes/user-management/Page.tsx create mode 100644 frontend/src/routes/user-management/components/DirectoryToolbar/index.module.scss create mode 100644 frontend/src/routes/user-management/components/DirectoryToolbar/index.tsx create mode 100644 frontend/src/routes/user-management/components/SummaryCards/index.module.scss create mode 100644 frontend/src/routes/user-management/components/SummaryCards/index.tsx create mode 100644 frontend/src/routes/user-management/components/UserDrawer/index.module.scss create mode 100644 frontend/src/routes/user-management/components/UserDrawer/index.tsx create mode 100644 frontend/src/routes/user-management/components/UserTable/index.module.scss create mode 100644 frontend/src/routes/user-management/components/UserTable/index.tsx create mode 100644 frontend/src/routes/user-management/index.module.scss create mode 100644 frontend/src/stores/userStore.ts create mode 100644 frontend/src/test/setup.ts create mode 100644 frontend/src/utils/classNames.ts create mode 100644 frontend/src/utils/user.test.ts create mode 100644 frontend/src/utils/user.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/vitest.config.ts diff --git a/.env.example b/.env.example index 32b0767..e05cebd 100644 --- a/.env.example +++ b/.env.example @@ -2,19 +2,19 @@ # 当前环境标识。项目连接外部 MySQL,不通过 Docker 创建数据库。 APP_ENV=development -# 外部 MySQL 连接配置。Spring Boot 会读取这一组变量。 -MYSQL_HOST=24.233.2.106 -MYSQL_PORT=13322 +# 外部 MySQL 连接配置。请在本地 .env 或部署 Secret 中填写真实值。 +MYSQL_HOST=127.0.0.1 +MYSQL_PORT=3306 MYSQL_DATABASE=training -MYSQL_USER=admin -MYSQL_PASSWORD=13518200336LsD@@ -MYSQL_ROOT_PASSWORD=root_password +MYSQL_USER=training +MYSQL_PASSWORD=change-this-password +MYSQL_ROOT_PASSWORD=change-this-root-password # Spring Boot 运行配置。 SERVER_PORT=8080 -SPRING_JPA_HIBERNATE_DDL_AUTO=update -SPRING_JPA_SHOW_SQL=true +SPRING_JPA_HIBERNATE_DDL_AUTO=none +SPRING_JPA_SHOW_SQL=false SPRING_JPA_OPEN_IN_VIEW=false -SPRING_JPA_PROPERTIES_HIBERNATE_FORMAT_SQL=true +SPRING_JPA_PROPERTIES_HIBERNATE_FORMAT_SQL=false LOGGING_LEVEL_COM_EXAMPLE_TRAINING=DEBUG LOGGING_LEVEL_ORG_HIBERNATE_SQL=DEBUG diff --git a/README.md b/README.md index 5b4ea2f..10193c0 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,114 @@ # Training Full-Stack Project -前后端分离的全栈项目基础工程: +前后端分离的培训项目: -- `frontend`: React 18 + TypeScript + Vite -- `backend`: Spring Boot + Java 21 + Maven + MyBatis-Plus -- `MySQL`: 外部 MySQL 8 数据库,由环境变量提供连接配置 +- `frontend`:React 18、TypeScript、Vite、React Router、Zustand、Tailwind CSS、SCSS Modules +- `backend`:Spring Boot 3.4.5、Java 21、Maven、MyBatis-Plus +- `MySQL`:外部 MySQL 8 数据库,应用不会自动创建或修改表结构 ## 环境要求 - Node.js 20+ - JDK 21+ -- Maven 3.9+(或使用项目中的 Maven Wrapper) +- Maven 3.9+ - 可访问目标 MySQL 的网络和账号 -## 配置数据库 +## 配置环境 -项目不会创建或启动 MySQL。请先确保 `.env.example` 中配置的 MySQL 服务已经运行并允许当前机器访问。 - -Spring Boot 启动时按以下顺序读取配置:根目录 `.env`,然后是根目录 `.env.example`。同名变量以 `.env` 为准;没有 `.env` 时直接使用已配置的 `.env.example`。 - -建议将 `.env.example` 复制为本地 `.env`,实际 `.env` 不提交到仓库: +使用 `.env.example` 作为本地配置参考,并将真实值写入未提交的根目录 `.env`: ```powershell -cp .env.example .env +Copy-Item .env.example .env ``` -生产环境使用 `.env.production.example` 作为 `.env` 模板,或由部署平台注入同名环境变量: +生产环境使用 `.env.production.example`,或由部署平台注入同名 Secret。不要将真实密码、令牌或数据库地址写入示例文件、日志或提交内容。 -```powershell -cp .env.production.example .env +## 数据库初始化与迁移 + +SQL 文件只保存在根目录 `sql/`: + +- `sql/USERS.sql`:新环境的最终建表语句,包含表和字段中文注释。 +- `sql/migrations/V1__rename_users_to_USERS.sql`:保留既有 `users` 数据的人工迁移脚本。 + +迁移前只读检查目标库和影响范围: + +```sql +SHOW TABLES LIKE 'users'; +SHOW TABLES LIKE 'USERS'; ``` -需要确认 `MYSQL_HOST`、`MYSQL_PORT`、`MYSQL_DATABASE`、`MYSQL_USER` 和 `MYSQL_PASSWORD` 均已配置。生产环境建议使用部署平台的 Secret 管理能力。 +确认备份、目标数据库和回滚方式后,人工执行迁移脚本。MySQL 的 `RENAME TABLE` 会隐式提交,回滚使用: + +```sql +RENAME TABLE USERS TO users; +``` + +应用启动不会自动执行 SQL;迁移和建表必须由数据库管理员按环境流程执行。 ## 启动后端 -```bash +```powershell mvn -f backend/pom.xml spring-boot:run ``` -后端默认运行在 `http://localhost:8080`,健康检查地址为 `GET /api/health`。 +后端默认运行在 `http://127.0.0.1:8080`,健康检查地址为 `GET /api/v1/health`。每次请求支持并返回 `X-Request-Id`。 ## 启动前端 -```bash -cd frontend -npm install -npm run dev +```powershell +Set-Location frontend +npm ci +npm run dev -- --host 127.0.0.1 ``` -前端默认运行在 `http://localhost:5173`,开发服务器会将 `/api` 请求代理到后端。 +前端默认运行在 `http://127.0.0.1:5173`,开发服务器将 `/api` 请求代理到后端。API 基础路径为 `/api/v1`。 -## 构建检查 +## API 示例 -```bash -cd frontend +用户列表使用 1-based 分页: + +```text +GET /api/v1/users?page=1&pageSize=50&keyword=&role=&status= +GET /api/v1/users/summary +``` + +成功响应统一为: + +```json +{ + "code": 200, + "message": "success", + "data": {} +} +``` + +## 构建与测试 + +```powershell +Set-Location frontend +npm ci +npm run test npm run build -cd ../backend -mvn clean verify +Set-Location .. +mvn -f backend/pom.xml test +mvn -f backend/pom.xml verify ``` + +## 全栈只读验证 + +从项目根目录执行: + +```powershell +python .codex/skills/fullstack-start-verify/scripts/start_and_verify.py ` + --backend-cmd "mvn -f backend/pom.xml spring-boot:run" ` + --backend-dir . ` + --backend-url http://127.0.0.1:8080/api/v1/health ` + --frontend-cmd "npm run dev -- --host 127.0.0.1" ` + --frontend-dir frontend ` + --frontend-url http://127.0.0.1:5173/ ` + --check-url summary=http://127.0.0.1:8080/api/v1/users/summary ` + --check-url "users=http://127.0.0.1:8080/api/v1/users?page=1& pageSize=50" +``` + +验证期间只执行健康检查和 GET 查询,不执行新增、修改、删除或数据库迁移。脚本默认清理本次启动的进程;排查启动失败时增加 `--keep-logs` 保留日志,并确认日志中没有敏感信息。 diff --git a/backend/src/main/java/com/example/training/common/ApiResponse.java b/backend/src/main/java/com/example/training/common/ApiResponse.java new file mode 100644 index 0000000..fcfca0a --- /dev/null +++ b/backend/src/main/java/com/example/training/common/ApiResponse.java @@ -0,0 +1,17 @@ +package com.example.training.common; + +/** + * 统一接口响应结构。 + * + * @param 响应数据类型 + */ +public record ApiResponse(int code, String message, T data) { + + public static ApiResponse success(T data) { + return new ApiResponse<>(200, "success", data); + } + + public static ApiResponse failure(int code, String message) { + return new ApiResponse<>(code, message, null); + } +} diff --git a/backend/src/main/java/com/example/training/config/MybatisPlusConfig.java b/backend/src/main/java/com/example/training/config/MybatisPlusConfig.java index 20c8117..e7aab4b 100644 --- a/backend/src/main/java/com/example/training/config/MybatisPlusConfig.java +++ b/backend/src/main/java/com/example/training/config/MybatisPlusConfig.java @@ -15,4 +15,5 @@ public class MybatisPlusConfig { interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } + } diff --git a/backend/src/main/java/com/example/training/config/RequestIdFilter.java b/backend/src/main/java/com/example/training/config/RequestIdFilter.java new file mode 100644 index 0000000..61df8cc --- /dev/null +++ b/backend/src/main/java/com/example/training/config/RequestIdFilter.java @@ -0,0 +1,33 @@ +package com.example.training.config; + +import java.io.IOException; +import java.util.UUID; +import java.util.regex.Pattern; + +import com.example.training.util.RequestIdContext; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.filter.OncePerRequestFilter; + +public class RequestIdFilter extends OncePerRequestFilter { + + private static final Pattern SAFE_REQUEST_ID = Pattern.compile("[A-Za-z0-9._-]{1,64}"); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String requestId = request.getHeader(RequestIdContext.HEADER_NAME); + if (requestId == null || !SAFE_REQUEST_ID.matcher(requestId).matches()) { + requestId = UUID.randomUUID().toString(); + } + RequestIdContext.set(requestId); + response.setHeader(RequestIdContext.HEADER_NAME, requestId); + try { + filterChain.doFilter(request, response); + } finally { + RequestIdContext.clear(); + } + } +} diff --git a/backend/src/main/java/com/example/training/config/WebConfig.java b/backend/src/main/java/com/example/training/config/WebConfig.java new file mode 100644 index 0000000..2995ee6 --- /dev/null +++ b/backend/src/main/java/com/example/training/config/WebConfig.java @@ -0,0 +1,18 @@ +package com.example.training.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class WebConfig { + + @Bean + public FilterRegistrationBean requestIdFilter() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new RequestIdFilter()); + registration.addUrlPatterns("/*"); + registration.setOrder(-100); + return registration; + } +} diff --git a/backend/src/main/java/com/example/training/constant/ErrorCode.java b/backend/src/main/java/com/example/training/constant/ErrorCode.java new file mode 100644 index 0000000..7083f88 --- /dev/null +++ b/backend/src/main/java/com/example/training/constant/ErrorCode.java @@ -0,0 +1,25 @@ +package com.example.training.constant; + +public enum ErrorCode { + + VALIDATION_FAILED(400001, "请求参数校验失败"), + USER_NOT_FOUND(404001, "用户不存在"), + DUPLICATE_EMAIL(409001, "该邮箱已被使用,请更换后重试"), + INTERNAL_ERROR(500001, "服务暂时不可用,请稍后重试"); + + private final int code; + private final String message; + + ErrorCode(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/backend/src/main/java/com/example/training/controller/HealthController.java b/backend/src/main/java/com/example/training/controller/HealthController.java index 76552fd..5dd4902 100644 --- a/backend/src/main/java/com/example/training/controller/HealthController.java +++ b/backend/src/main/java/com/example/training/controller/HealthController.java @@ -3,17 +3,18 @@ package com.example.training.controller; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import com.example.training.common.ApiResponse; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/health") +@RequestMapping("/api/v1/health") public class HealthController { @GetMapping - public HealthResponse health() { - return new HealthResponse("UP", "training-backend", OffsetDateTime.now(ZoneOffset.UTC)); + public ApiResponse health() { + return ApiResponse.success(new HealthResponse("UP", "training-backend", OffsetDateTime.now(ZoneOffset.UTC))); } public record HealthResponse(String status, String service, OffsetDateTime timestamp) { diff --git a/backend/src/main/java/com/example/training/controller/UserController.java b/backend/src/main/java/com/example/training/controller/UserController.java index f7a390a..b7690da 100644 --- a/backend/src/main/java/com/example/training/controller/UserController.java +++ b/backend/src/main/java/com/example/training/controller/UserController.java @@ -1,22 +1,22 @@ package com.example.training.controller; -import java.time.LocalDateTime; import java.util.List; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.example.training.domain.User; +import com.example.training.common.ApiResponse; +import com.example.training.constant.ErrorCode; +import com.example.training.domain.dto.BulkStatusRequest; +import com.example.training.domain.dto.UserRequest; +import com.example.training.domain.entity.User; +import com.example.training.domain.vo.BulkStatusVo; +import com.example.training.domain.vo.UserPageVo; +import com.example.training.domain.vo.UserSummaryVo; +import com.example.training.domain.vo.UserVo; import com.example.training.service.UserService; import jakarta.validation.Valid; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -29,7 +29,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; @RestController -@RequestMapping("/api/users") +@RequestMapping("/api/v1/users") public class UserController { private final UserService userService; @@ -39,91 +39,55 @@ public class UserController { } @GetMapping - public UserPage list( + public ApiResponse list( @RequestParam(defaultValue = "") String keyword, @RequestParam(defaultValue = "") String role, @RequestParam(defaultValue = "") String status, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "50") int size) { - int safePage = Math.max(page, 0); - int safeSize = Math.min(Math.max(size, 1), 100); - Page resultPage = userService.pageUsers(keyword, role, status, safePage, safeSize); - return new UserPage(resultPage.getRecords().stream().map(UserResponse::from).toList(), - resultPage.getTotal(), (int) resultPage.getCurrent() - 1, (int) resultPage.getSize()); + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "50") int pageSize) { + int safePage = Math.max(page, 1); + int safePageSize = Math.min(Math.max(pageSize, 1), 100); + Page resultPage = userService.pageUsers(keyword, role, status, safePage, safePageSize); + UserPageVo data = new UserPageVo(resultPage.getRecords().stream().map(UserVo::from).toList(), + resultPage.getTotal(), safePage, safePageSize); + return ApiResponse.success(data); } @GetMapping("/summary") - public UserSummary summary() { + public ApiResponse summary() { UserService.UserSummary summary = userService.summarizeUsers(); - return new UserSummary(summary.total(), summary.active(), summary.inactive(), summary.admins()); + return ApiResponse.success(new UserSummaryVo(summary.total(), summary.active(), + summary.inactive(), summary.admins())); } @PatchMapping("/status") - public BulkStatusResponse updateStatuses(@Valid @RequestBody BulkStatusRequest request) { + public ApiResponse updateStatuses(@Valid @RequestBody BulkStatusRequest request) { int updated = userService.updateStatuses(request.ids(), request.status()); - return new BulkStatusResponse(updated, request.status()); + return ApiResponse.success(new BulkStatusVo(updated, request.status())); } @PostMapping - public ResponseEntity create(@Valid @RequestBody UserRequest request) { + public ResponseEntity> create(@Valid @RequestBody UserRequest request) { User user = userService.createUser(request.fullName(), request.email(), request.role(), request.status()); - return ResponseEntity.status(HttpStatus.CREATED).body(UserResponse.from(user)); + return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(UserVo.from(user))); } @PutMapping("/{id}") - public UserResponse update(@PathVariable Long id, @Valid @RequestBody UserRequest request) { + public ApiResponse update(@PathVariable Long id, @Valid @RequestBody UserRequest request) { User user = userService.updateUser(id, request.fullName(), request.email(), request.role(), request.status()); if (user == null) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "用户不存在"); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, ErrorCode.USER_NOT_FOUND.getMessage()); } - return UserResponse.from(user); + return ApiResponse.success(UserVo.from(user)); } @DeleteMapping("/{id}") - public ResponseEntity delete(@PathVariable Long id) { + public ApiResponse delete(@PathVariable Long id) { if (!userService.deleteUser(id)) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "用户不存在"); + throw new ResponseStatusException(HttpStatus.NOT_FOUND, ErrorCode.USER_NOT_FOUND.getMessage()); } - return ResponseEntity.noContent().build(); - } - - @ExceptionHandler(DataIntegrityViolationException.class) - ResponseEntity handleDuplicateEmail() { - return ResponseEntity.status(HttpStatus.CONFLICT) - .body(new ApiError("该邮箱已被使用,请更换后重试")); - } - - public record UserRequest( - @NotBlank @Size(max = 80) String fullName, - @NotBlank @Email @Size(max = 160) String email, - @NotBlank @Size(max = 40) String role, - @NotBlank @Size(max = 20) String status) { - } - - public record BulkStatusRequest( - @NotEmpty @Size(max = 100) List ids, - @NotBlank @Pattern(regexp = "ACTIVE|INACTIVE") String status) { - } - - public record UserPage(List items, long total, int page, int size) { - } - - public record UserSummary(long total, long active, long inactive, long admins) { - } - - public record BulkStatusResponse(int updated, String status) { - } - - public record UserResponse(Long id, String fullName, String email, String role, - String status, LocalDateTime createdAt, LocalDateTime updatedAt) { - static UserResponse from(User user) { - return new UserResponse(user.getId(), user.getFullName(), user.getEmail(), - user.getRole(), user.getStatus(), user.getCreatedAt(), user.getUpdatedAt()); - } - } - - public record ApiError(String message) { + return ApiResponse.success(null); } } diff --git a/backend/src/main/java/com/example/training/domain/User.java b/backend/src/main/java/com/example/training/domain/User.java index db073f8..5fcde29 100644 --- a/backend/src/main/java/com/example/training/domain/User.java +++ b/backend/src/main/java/com/example/training/domain/User.java @@ -1,57 +1,16 @@ package com.example.training.domain; -import java.time.LocalDateTime; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; - -@TableName("users") -public class User { - - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - @TableField("full_name") - private String fullName; - - private String email; - private String role; - private String status; - - @TableField("created_at") - private LocalDateTime createdAt; - - @TableField("updated_at") - private LocalDateTime updatedAt; +/** + * 兼容旧包路径的过渡类型,业务代码统一使用 domain.entity.User。 + */ +@Deprecated +public class User extends com.example.training.domain.entity.User { public User() { + super(); } public User(String fullName, String email, String role, String status) { - LocalDateTime now = LocalDateTime.now(); - this.fullName = fullName; - this.email = email; - this.role = role; - this.status = status; - this.createdAt = now; - this.updatedAt = now; - } - - public Long getId() { return id; } - public String getFullName() { return fullName; } - public String getEmail() { return email; } - public String getRole() { return role; } - public String getStatus() { return status; } - public LocalDateTime getCreatedAt() { return createdAt; } - public LocalDateTime getUpdatedAt() { return updatedAt; } - - public void update(String fullName, String email, String role, String status) { - this.fullName = fullName; - this.email = email; - this.role = role; - this.status = status; - this.updatedAt = LocalDateTime.now(); + super(fullName, email, role, status); } } diff --git a/backend/src/main/java/com/example/training/domain/dto/BulkStatusRequest.java b/backend/src/main/java/com/example/training/domain/dto/BulkStatusRequest.java new file mode 100644 index 0000000..c5b5c68 --- /dev/null +++ b/backend/src/main/java/com/example/training/domain/dto/BulkStatusRequest.java @@ -0,0 +1,13 @@ +package com.example.training.domain.dto; + +import java.util.List; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public record BulkStatusRequest( + @NotEmpty(message = "用户 ID 不能为空") @Size(max = 100, message = "单次最多更新 100 个用户") List ids, + @NotBlank(message = "状态不能为空") @Pattern(regexp = "ACTIVE|INACTIVE", message = "状态不合法") String status) { +} diff --git a/backend/src/main/java/com/example/training/domain/dto/UserRequest.java b/backend/src/main/java/com/example/training/domain/dto/UserRequest.java new file mode 100644 index 0000000..25c2e49 --- /dev/null +++ b/backend/src/main/java/com/example/training/domain/dto/UserRequest.java @@ -0,0 +1,13 @@ +package com.example.training.domain.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public record UserRequest( + @NotBlank(message = "姓名不能为空") @Size(max = 80, message = "姓名长度不能超过 80 个字符") String fullName, + @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") @Size(max = 160, message = "邮箱长度不能超过 160 个字符") String email, + @NotBlank(message = "角色不能为空") @Pattern(regexp = "Admin|Manager|Member|Viewer", message = "角色不合法") String role, + @NotBlank(message = "状态不能为空") @Pattern(regexp = "ACTIVE|INACTIVE", message = "状态不合法") String status) { +} diff --git a/backend/src/main/java/com/example/training/domain/entity/User.java b/backend/src/main/java/com/example/training/domain/entity/User.java new file mode 100644 index 0000000..cf37791 --- /dev/null +++ b/backend/src/main/java/com/example/training/domain/entity/User.java @@ -0,0 +1,77 @@ +package com.example.training.domain.entity; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +@TableName("USERS") +public class User { + + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + @TableField("full_name") + private String fullName; + + private String email; + private String role; + private String status; + + @TableField("created_at") + private LocalDateTime createdAt; + + @TableField("updated_at") + private LocalDateTime updatedAt; + + public User() { + } + + public User(String fullName, String email, String role, String status) { + LocalDateTime now = LocalDateTime.now(); + this.fullName = fullName; + this.email = email; + this.role = role; + this.status = status; + this.createdAt = now; + this.updatedAt = now; + } + + public Long getId() { + return id; + } + + public String getFullName() { + return fullName; + } + + public String getEmail() { + return email; + } + + public String getRole() { + return role; + } + + public String getStatus() { + return status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void update(String fullName, String email, String role, String status) { + this.fullName = fullName; + this.email = email; + this.role = role; + this.status = status; + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/backend/src/main/java/com/example/training/domain/vo/BulkStatusVo.java b/backend/src/main/java/com/example/training/domain/vo/BulkStatusVo.java new file mode 100644 index 0000000..432a10e --- /dev/null +++ b/backend/src/main/java/com/example/training/domain/vo/BulkStatusVo.java @@ -0,0 +1,4 @@ +package com.example.training.domain.vo; + +public record BulkStatusVo(int updated, String status) { +} diff --git a/backend/src/main/java/com/example/training/domain/vo/UserPageVo.java b/backend/src/main/java/com/example/training/domain/vo/UserPageVo.java new file mode 100644 index 0000000..d18deb0 --- /dev/null +++ b/backend/src/main/java/com/example/training/domain/vo/UserPageVo.java @@ -0,0 +1,6 @@ +package com.example.training.domain.vo; + +import java.util.List; + +public record UserPageVo(List items, long total, int page, int pageSize) { +} diff --git a/backend/src/main/java/com/example/training/domain/vo/UserSummaryVo.java b/backend/src/main/java/com/example/training/domain/vo/UserSummaryVo.java new file mode 100644 index 0000000..f461a50 --- /dev/null +++ b/backend/src/main/java/com/example/training/domain/vo/UserSummaryVo.java @@ -0,0 +1,4 @@ +package com.example.training.domain.vo; + +public record UserSummaryVo(long total, long active, long inactive, long admins) { +} diff --git a/backend/src/main/java/com/example/training/domain/vo/UserVo.java b/backend/src/main/java/com/example/training/domain/vo/UserVo.java new file mode 100644 index 0000000..32dc04b --- /dev/null +++ b/backend/src/main/java/com/example/training/domain/vo/UserVo.java @@ -0,0 +1,14 @@ +package com.example.training.domain.vo; + +import java.time.LocalDateTime; + +import com.example.training.domain.entity.User; + +public record UserVo(Long id, String fullName, String email, String role, + String status, LocalDateTime createdAt, LocalDateTime updatedAt) { + + public static UserVo from(User user) { + return new UserVo(user.getId(), user.getFullName(), user.getEmail(), user.getRole(), + user.getStatus(), user.getCreatedAt(), user.getUpdatedAt()); + } +} diff --git a/backend/src/main/java/com/example/training/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/example/training/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..5c69efd --- /dev/null +++ b/backend/src/main/java/com/example/training/exception/GlobalExceptionHandler.java @@ -0,0 +1,60 @@ +package com.example.training.exception; + +import com.example.training.common.ApiResponse; +import com.example.training.constant.ErrorCode; +import com.example.training.util.RequestIdContext; +import jakarta.validation.ConstraintViolationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.server.ResponseStatusException; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation(MethodArgumentNotValidException exception) { + String message = exception.getBindingResult().getFieldErrors().stream() + .findFirst() + .map(error -> error.getDefaultMessage()) + .orElse(ErrorCode.VALIDATION_FAILED.getMessage()); + return ResponseEntity.badRequest().body(ApiResponse.failure( + ErrorCode.VALIDATION_FAILED.getCode(), message)); + } + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity> handleConstraintViolation() { + return ResponseEntity.badRequest().body(ApiResponse.failure( + ErrorCode.VALIDATION_FAILED.getCode(), ErrorCode.VALIDATION_FAILED.getMessage())); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity> handleDataIntegrityViolation() { + return ResponseEntity.status(409).body(ApiResponse.failure( + ErrorCode.DUPLICATE_EMAIL.getCode(), ErrorCode.DUPLICATE_EMAIL.getMessage())); + } + + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity> handleResponseStatus(ResponseStatusException exception) { + int code = exception.getStatusCode().value() == 404 + ? ErrorCode.USER_NOT_FOUND.getCode() + : ErrorCode.VALIDATION_FAILED.getCode(); + String message = exception.getStatusCode().value() == 404 + ? ErrorCode.USER_NOT_FOUND.getMessage() + : exception.getReason(); + return ResponseEntity.status(exception.getStatusCode()).body(ApiResponse.failure(code, message)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleUnexpectedException(Exception exception) { + log.error("请求处理失败,requestId={}", RequestIdContext.current(), exception); + return ResponseEntity.internalServerError().body(ApiResponse.failure( + ErrorCode.INTERNAL_ERROR.getCode(), ErrorCode.INTERNAL_ERROR.getMessage())); + } +} diff --git a/backend/src/main/java/com/example/training/mapper/UserMapper.java b/backend/src/main/java/com/example/training/mapper/UserMapper.java index 5581392..ecad4d5 100644 --- a/backend/src/main/java/com/example/training/mapper/UserMapper.java +++ b/backend/src/main/java/com/example/training/mapper/UserMapper.java @@ -1,7 +1,7 @@ package com.example.training.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.training.domain.User; +import com.example.training.domain.entity.User; import org.apache.ibatis.annotations.Mapper; @Mapper diff --git a/backend/src/main/java/com/example/training/service/UserService.java b/backend/src/main/java/com/example/training/service/UserService.java index 9ec96a8..e031ea5 100644 --- a/backend/src/main/java/com/example/training/service/UserService.java +++ b/backend/src/main/java/com/example/training/service/UserService.java @@ -1,7 +1,7 @@ package com.example.training.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.example.training.domain.User; +import com.example.training.domain.entity.User; import java.util.List; diff --git a/backend/src/main/java/com/example/training/service/impl/UserServiceImpl.java b/backend/src/main/java/com/example/training/service/impl/UserServiceImpl.java index 7a69335..c29021e 100644 --- a/backend/src/main/java/com/example/training/service/impl/UserServiceImpl.java +++ b/backend/src/main/java/com/example/training/service/impl/UserServiceImpl.java @@ -3,9 +3,12 @@ package com.example.training.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.example.training.domain.User; +import com.example.training.domain.entity.User; import com.example.training.mapper.UserMapper; import com.example.training.service.UserService; +import com.example.training.util.RequestIdContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,6 +18,8 @@ import java.util.List; @Service public class UserServiceImpl implements UserService { + private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class); + private final UserMapper userMapper; public UserServiceImpl(UserMapper userMapper) { @@ -24,7 +29,7 @@ public class UserServiceImpl implements UserService { @Override @Transactional(readOnly = true) public Page pageUsers(String keyword, String role, String status, int page, int size) { - Page requestPage = new Page<>(page + 1L, size); + Page requestPage = new Page<>(Math.max(page, 1), Math.min(Math.max(size, 1), 100)); LambdaQueryWrapper query = new LambdaQueryWrapper<>(); if (keyword != null && !keyword.isBlank()) { String trimmedKeyword = keyword.trim(); @@ -65,7 +70,10 @@ public class UserServiceImpl implements UserService { update.in(User::getId, ids) .set(User::getStatus, status) .set(User::getUpdatedAt, LocalDateTime.now()); - return userMapper.update(null, update); + int updated = userMapper.update(null, update); + log.info("批量更新用户状态,requestId={},updated={},status={}", + RequestIdContext.current(), updated, status); + return updated; } @Override @@ -73,6 +81,7 @@ public class UserServiceImpl implements UserService { public User createUser(String fullName, String email, String role, String status) { User user = new User(normalize(fullName), normalizeEmail(email), role, status); userMapper.insert(user); + log.info("创建用户,requestId={},userId={}", RequestIdContext.current(), user.getId()); return user; } @@ -85,6 +94,7 @@ public class UserServiceImpl implements UserService { } user.update(normalize(fullName), normalizeEmail(email), role, status); userMapper.updateById(user); + log.info("更新用户,requestId={},userId={}", RequestIdContext.current(), id); return user; } @@ -94,7 +104,10 @@ public class UserServiceImpl implements UserService { if (userMapper.selectById(id) == null) { return false; } - return userMapper.deleteById(id) > 0; + boolean deleted = userMapper.deleteById(id) > 0; + log.info("删除用户,requestId={},userId={},deleted={}", + RequestIdContext.current(), id, deleted); + return deleted; } private static String normalize(String value) { diff --git a/backend/src/main/java/com/example/training/util/RequestIdContext.java b/backend/src/main/java/com/example/training/util/RequestIdContext.java new file mode 100644 index 0000000..3704f7a --- /dev/null +++ b/backend/src/main/java/com/example/training/util/RequestIdContext.java @@ -0,0 +1,24 @@ +package com.example.training.util; + +import org.slf4j.MDC; + +public final class RequestIdContext { + + public static final String HEADER_NAME = "X-Request-Id"; + private static final String MDC_KEY = "requestId"; + + private RequestIdContext() { + } + + public static void set(String requestId) { + MDC.put(MDC_KEY, requestId); + } + + public static String current() { + return MDC.get(MDC_KEY); + } + + public static void clear() { + MDC.remove(MDC_KEY); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index bcc3c10..45297c5 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -15,8 +15,7 @@ spring: validation-timeout: ${MYSQL_POOL_VALIDATION_TIMEOUT_MS:5000} sql: init: - mode: always - continue-on-error: false + mode: never mybatis-plus: configuration: map-underscore-to-camel-case: true @@ -36,3 +35,5 @@ logging: level: com.example.training: ${LOGGING_LEVEL_COM_EXAMPLE_TRAINING:INFO} com.baomidou.mybatisplus: INFO + pattern: + level: "%5p [requestId:%X{requestId}]" diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql deleted file mode 100644 index c4dd8fc..0000000 --- a/backend/src/main/resources/schema.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TABLE IF NOT EXISTS users ( - id BIGINT NOT NULL AUTO_INCREMENT, - full_name VARCHAR(80) NOT NULL, - email VARCHAR(160) NOT NULL, - role VARCHAR(40) NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (id), - UNIQUE KEY uk_users_email (email), - KEY idx_users_full_name (full_name), - KEY idx_users_status (status) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/backend/src/test/java/com/example/training/TrainingApplicationTests.java b/backend/src/test/java/com/example/training/TrainingApplicationTests.java index c1f08bc..644860a 100644 --- a/backend/src/test/java/com/example/training/TrainingApplicationTests.java +++ b/backend/src/test/java/com/example/training/TrainingApplicationTests.java @@ -7,13 +7,17 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import com.example.training.controller.HealthController; +import com.example.training.config.WebConfig; +import org.springframework.context.annotation.Import; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @WebMvcTest(HealthController.class) @ActiveProfiles("test") +@Import(WebConfig.class) class TrainingApplicationTests { @Autowired @@ -21,9 +25,12 @@ class TrainingApplicationTests { @Test void healthEndpointReturnsUp() throws Exception { - mockMvc.perform(get("/api/health")) + mockMvc.perform(get("/api/v1/health").header("X-Request-Id", "health-test-1")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.status").value("UP")) - .andExpect(jsonPath("$.service").value("training-backend")); + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.message").value("success")) + .andExpect(jsonPath("$.data.status").value("UP")) + .andExpect(jsonPath("$.data.service").value("training-backend")) + .andExpect(header().string("X-Request-Id", "health-test-1")); } } diff --git a/backend/src/test/java/com/example/training/UserControllerTests.java b/backend/src/test/java/com/example/training/UserControllerTests.java index 82ab9a9..3e014c4 100644 --- a/backend/src/test/java/com/example/training/UserControllerTests.java +++ b/backend/src/test/java/com/example/training/UserControllerTests.java @@ -31,39 +31,53 @@ class UserControllerTests { @Test void listPassesRoleAndStatusFilters() throws Exception { - when(userService.pageUsers("晓", "Admin", "ACTIVE", 0, 50)).thenReturn(new Page<>(1, 50)); + when(userService.pageUsers("晓", "Admin", "ACTIVE", 1, 50)).thenReturn(new Page<>(1, 50)); - mockMvc.perform(get("/api/users") + mockMvc.perform(get("/api/v1/users") .param("keyword", "晓") .param("role", "Admin") .param("status", "ACTIVE")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.total").value(0)); + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.total").value(0)) + .andExpect(jsonPath("$.data.page").value(1)) + .andExpect(jsonPath("$.data.pageSize").value(50)); - verify(userService).pageUsers("晓", "Admin", "ACTIVE", 0, 50); + verify(userService).pageUsers("晓", "Admin", "ACTIVE", 1, 50); } @Test void summaryReturnsOverviewCounts() throws Exception { when(userService.summarizeUsers()).thenReturn(new UserService.UserSummary(12, 9, 3, 2)); - mockMvc.perform(get("/api/users/summary")) + mockMvc.perform(get("/api/v1/users/summary")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.total").value(12)) - .andExpect(jsonPath("$.active").value(9)) - .andExpect(jsonPath("$.inactive").value(3)) - .andExpect(jsonPath("$.admins").value(2)); + .andExpect(jsonPath("$.data.total").value(12)) + .andExpect(jsonPath("$.data.active").value(9)) + .andExpect(jsonPath("$.data.inactive").value(3)) + .andExpect(jsonPath("$.data.admins").value(2)); } @Test void bulkStatusUpdatesSelectedUsers() throws Exception { when(userService.updateStatuses(anyList(), eq("INACTIVE"))).thenReturn(2); - mockMvc.perform(patch("/api/users/status") + mockMvc.perform(patch("/api/v1/users/status") .contentType("application/json") .content("{\"ids\":[4,7],\"status\":\"INACTIVE\"}")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.updated").value(2)) - .andExpect(jsonPath("$.status").value("INACTIVE")); + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.updated").value(2)) + .andExpect(jsonPath("$.data.status").value("INACTIVE")); + } + + @Test + void invalidUserRequestReturnsStandardError() throws Exception { + mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/api/v1/users") + .contentType("application/json") + .content("{\"fullName\":\"\",\"email\":\"invalid\",\"role\":\"Unknown\",\"status\":\"BROKEN\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(400001)) + .andExpect(jsonPath("$.data").doesNotExist()); } } diff --git a/frontend/.env.example b/frontend/.env.example index 14ea4ad..2bf124d 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1 +1 @@ -VITE_API_BASE_URL=/api +VITE_API_BASE_URL=/api/v1 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c6f9b99..ba56845 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,17 +8,1868 @@ "name": "training-frontend", "version": "0.1.0", "dependencies": { + "classnames": "^2.5.1", + "lucide-react": "^0.468.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0", + "zustand": "^5.0.2" }, "devDependencies": { "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "jsdom": "^25.0.1", + "postcss": "^8.4.49", + "sass": "^1.83.4", + "tailwindcss": "^3.4.17", "typescript": "^5.6.3", - "vite": "^5.4.10" + "vite": "^5.4.10", + "vitest": "^2.1.8" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "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.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "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/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.4", + "resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.4", + "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.3", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.3.tgz", + "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.3", + "@rollup/rollup-android-arm64": "4.62.3", + "@rollup/rollup-darwin-arm64": "4.62.3", + "@rollup/rollup-darwin-x64": "4.62.3", + "@rollup/rollup-freebsd-arm64": "4.62.3", + "@rollup/rollup-freebsd-x64": "4.62.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", + "@rollup/rollup-linux-arm-musleabihf": "4.62.3", + "@rollup/rollup-linux-arm64-gnu": "4.62.3", + "@rollup/rollup-linux-arm64-musl": "4.62.3", + "@rollup/rollup-linux-loong64-gnu": "4.62.3", + "@rollup/rollup-linux-loong64-musl": "4.62.3", + "@rollup/rollup-linux-ppc64-gnu": "4.62.3", + "@rollup/rollup-linux-ppc64-musl": "4.62.3", + "@rollup/rollup-linux-riscv64-gnu": "4.62.3", + "@rollup/rollup-linux-riscv64-musl": "4.62.3", + "@rollup/rollup-linux-s390x-gnu": "4.62.3", + "@rollup/rollup-linux-x64-gnu": "4.62.3", + "@rollup/rollup-linux-x64-musl": "4.62.3", + "@rollup/rollup-openbsd-x64": "4.62.3", + "@rollup/rollup-openharmony-arm64": "4.62.3", + "@rollup/rollup-win32-arm64-msvc": "4.62.3", + "@rollup/rollup-win32-ia32-msvc": "4.62.3", + "@rollup/rollup-win32-x64-gnu": "4.62.3", + "@rollup/rollup-win32-x64-msvc": "4.62.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.102.0", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.102.0.tgz", + "integrity": "sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==", + "dev": true, + "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/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "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/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmmirror.com/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "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/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": 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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "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/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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "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/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -301,6 +2152,121 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -338,7 +2304,7 @@ "node_modules/@esbuild/android-arm64": { "version": "0.21.5", "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -742,6 +2708,284 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.3", + "resolved": "https://registry.npmmirror.com/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1003,7 +3247,7 @@ }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.62.3", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz", "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==", "cpu": [ "x64" @@ -1155,14 +3399,14 @@ "version": "15.7.15", "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.31", "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -1200,10 +3444,228 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.4", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/baseline-browser-mapping": { - "version": "2.11.4", - "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.4.tgz", - "integrity": "sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==", + "version": "2.11.5", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.5.tgz", + "integrity": "sha512-xJo6a6YZnwZfnyGmQKWMbVOcii7XRibjOskRh+WJ9UHQoX16xrQrcIgAMQOzfvs8XiLMx6ih/fsLPF73iY2D1A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1213,6 +3675,32 @@ "node": ">=6.0.0" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.7", "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.7.tgz", @@ -1247,6 +3735,40 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001806", "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", @@ -1268,6 +3790,78 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1275,13 +3869,61 @@ "dev": true, "license": "MIT" }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "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==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", @@ -1300,13 +3942,149 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "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==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "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.396", - "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", - "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", + "version": "1.5.397", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.397.tgz", + "integrity": "sha512-khGTy9U9x02KEtsKM8vx5A62BsRmcOsIgDpWr1ImE32Ax8GxHGPHZf+Eu9H8zOOyHJnB0jTbseyTHbq2XCT8yw==", "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "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==", + "dev": true, + "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==", + "dev": true, + "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/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", @@ -1356,377 +4134,174 @@ "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==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "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/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "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/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/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "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/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.51", - "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.51.tgz", - "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "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/postcss": { - "version": "8.5.23", - "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.23.tgz", - "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.16", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.62.3", - "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.3.tgz", - "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.3", - "@rollup/rollup-android-arm64": "4.62.3", - "@rollup/rollup-darwin-arm64": "4.62.3", - "@rollup/rollup-darwin-x64": "4.62.3", - "@rollup/rollup-freebsd-arm64": "4.62.3", - "@rollup/rollup-freebsd-x64": "4.62.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", - "@rollup/rollup-linux-arm-musleabihf": "4.62.3", - "@rollup/rollup-linux-arm64-gnu": "4.62.3", - "@rollup/rollup-linux-arm64-musl": "4.62.3", - "@rollup/rollup-linux-loong64-gnu": "4.62.3", - "@rollup/rollup-linux-loong64-musl": "4.62.3", - "@rollup/rollup-linux-ppc64-gnu": "4.62.3", - "@rollup/rollup-linux-ppc64-musl": "4.62.3", - "@rollup/rollup-linux-riscv64-gnu": "4.62.3", - "@rollup/rollup-linux-riscv64-musl": "4.62.3", - "@rollup/rollup-linux-s390x-gnu": "4.62.3", - "@rollup/rollup-linux-x64-gnu": "4.62.3", - "@rollup/rollup-linux-x64-musl": "4.62.3", - "@rollup/rollup-openbsd-x64": "4.62.3", - "@rollup/rollup-openharmony-arm64": "4.62.3", - "@rollup/rollup-win32-arm64-msvc": "4.62.3", - "@rollup/rollup-win32-ia32-msvc": "4.62.3", - "@rollup/rollup-win32-x64-gnu": "4.62.3", - "@rollup/rollup-win32-x64-msvc": "4.62.3", - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "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/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "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/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">= 10.0.0" }, "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } + "@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/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "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" + ], "dev": true, - "license": "ISC" - } - } -} + "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", diff --git a/frontend/package.json b/frontend/package.json index 1cfa5a3..5780b67 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,17 +6,28 @@ "scripts": { "dev": "vite", "build": "tsc --noEmit -p tsconfig.app.json && tsc --noEmit -p tsconfig.node.json && vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run" }, "dependencies": { + "classnames": "^2.5.1", + "lucide-react": "^0.468.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-router-dom": "^6.28.0", + "zustand": "^5.0.2" }, "devDependencies": { "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "jsdom": "^25.0.1", + "postcss": "^8.4.49", + "sass": "^1.83.4", + "tailwindcss": "^3.4.17", "typescript": "^5.6.3", - "vite": "^5.4.10" + "vite": "^5.4.10", + "vitest": "^2.1.8" } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e30847a..09b1fa7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,331 +1,7 @@ -import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react' +import { RouterProvider } from 'react-router-dom' -type Status = 'ACTIVE' | 'INACTIVE' +import { router } from './routes' -type User = { - id: number - fullName: string - email: string - role: string - status: Status - createdAt: string - updatedAt: string +export default function App() { + return } - -type UserForm = Omit - -type UserSummary = { - total: number - active: number - inactive: number - admins: number -} - -type UserPage = { - items: User[] - total: number -} - -const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? '/api' -const roleOptions = ['Admin', 'Manager', 'Member', 'Viewer'] -const emptyForm: UserForm = { fullName: '', email: '', role: 'Member', status: 'ACTIVE' } - -function roleLabel(role: string) { - return ({ Admin: '管理员', Manager: '经理', Member: '成员', Viewer: '只读成员' }[role] ?? role) -} - -function statusLabel(status: Status) { - return status === 'ACTIVE' ? '活跃' : '未激活' -} - -function formatDate(value: string) { - return new Date(value).toLocaleDateString('zh-CN', { year: 'numeric', month: 'short', day: 'numeric' }) -} - -function initials(name: string) { - const parts = name.trim().split(/\s+/).filter(Boolean) - if (parts.length > 1) return `${parts[0].charAt(0)}${parts[parts.length - 1].charAt(0)}`.toUpperCase() - return name.trim().slice(0, 2).toUpperCase() -} - -async function readError(response: Response, fallback: string) { - const body = await response.json().catch(() => null) as { message?: string } | null - return body?.message ?? fallback -} - -function App() { - const [users, setUsers] = useState([]) - const [total, setTotal] = useState(0) - const [summary, setSummary] = useState({ total: 0, active: 0, inactive: 0, admins: 0 }) - const [keyword, setKeyword] = useState('') - const [search, setSearch] = useState('') - const [roleFilter, setRoleFilter] = useState('') - const [statusFilter, setStatusFilter] = useState<'' | Status>('') - const [form, setForm] = useState(emptyForm) - const [editingId, setEditingId] = useState(null) - const [isFormOpen, setIsFormOpen] = useState(false) - const [isLoading, setIsLoading] = useState(true) - const [isSaving, setIsSaving] = useState(false) - const [isBulkUpdating, setIsBulkUpdating] = useState(false) - const [selectedIds, setSelectedIds] = useState>(new Set()) - const [error, setError] = useState('') - const [notice, setNotice] = useState('') - - const loadUsers = useCallback(async () => { - setIsLoading(true) - setError('') - try { - const params = new URLSearchParams({ keyword: search, role: roleFilter, status: statusFilter, page: '0', size: '50' }) - const response = await fetch(`${apiBaseUrl}/users?${params}`) - if (!response.ok) throw new Error(await readError(response, '无法加载用户列表')) - const data = await response.json() as UserPage - setUsers(data.items) - setTotal(data.total) - setSelectedIds(new Set()) - } catch (requestError) { - setError(requestError instanceof Error ? requestError.message : '请求失败,请稍后重试') - } finally { - setIsLoading(false) - } - }, [roleFilter, search, statusFilter]) - - const loadSummary = useCallback(async () => { - try { - const response = await fetch(`${apiBaseUrl}/users/summary`) - if (!response.ok) throw new Error(await readError(response, '无法加载用户概览')) - setSummary(await response.json() as UserSummary) - } catch (requestError) { - setError(requestError instanceof Error ? requestError.message : '无法加载用户概览') - } - }, []) - - const refreshData = useCallback(async () => { - await Promise.all([loadUsers(), loadSummary()]) - }, [loadSummary, loadUsers]) - - useEffect(() => { void refreshData() }, [refreshData]) - - useEffect(() => { - if (!notice) return undefined - const timer = window.setTimeout(() => setNotice(''), 3200) - return () => window.clearTimeout(timer) - }, [notice]) - - function openCreate() { - setEditingId(null) - setForm(emptyForm) - setError('') - setIsFormOpen(true) - } - - function openEdit(user: User) { - setEditingId(user.id) - setForm({ fullName: user.fullName, email: user.email, role: user.role, status: user.status }) - setError('') - setIsFormOpen(true) - } - - async function submitForm(event: FormEvent) { - event.preventDefault() - setIsSaving(true) - setError('') - try { - const response = await fetch(`${apiBaseUrl}/users${editingId ? `/${editingId}` : ''}`, { - method: editingId ? 'PUT' : 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(form), - }) - if (!response.ok) throw new Error(await readError(response, '保存失败,请检查输入')) - setIsFormOpen(false) - setNotice(editingId ? '用户信息已更新' : '用户已创建') - await refreshData() - } catch (requestError) { - setError(requestError instanceof Error ? requestError.message : '保存失败,请稍后重试') - } finally { - setIsSaving(false) - } - } - - async function removeUser(user: User) { - if (!window.confirm(`确定删除用户“${user.fullName}”吗?此操作不可撤销。`)) return - setError('') - try { - const response = await fetch(`${apiBaseUrl}/users/${user.id}`, { method: 'DELETE' }) - if (!response.ok) throw new Error(await readError(response, '删除失败,请稍后重试')) - setNotice('用户已删除') - await refreshData() - } catch (requestError) { - setError(requestError instanceof Error ? requestError.message : '删除失败,请稍后重试') - } - } - - async function updateSelectedStatus(status: Status) { - if (selectedIds.size === 0) return - const label = statusLabel(status) - if (!window.confirm(`确定将选中的 ${selectedIds.size} 位用户设为“${label}”吗?`)) return - setIsBulkUpdating(true) - setError('') - try { - const response = await fetch(`${apiBaseUrl}/users/status`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ids: [...selectedIds], status }), - }) - if (!response.ok) throw new Error(await readError(response, '批量更新失败,请稍后重试')) - setNotice(`已将选中的用户设为${label}`) - await refreshData() - } catch (requestError) { - setError(requestError instanceof Error ? requestError.message : '批量更新失败,请稍后重试') - } finally { - setIsBulkUpdating(false) - } - } - - function submitSearch(event: FormEvent) { - event.preventDefault() - setSearch(keyword.trim()) - } - - function clearFilters() { - setKeyword('') - setSearch('') - setRoleFilter('') - setStatusFilter('') - } - - function toggleSelected(id: number) { - setSelectedIds((current) => { - const next = new Set(current) - if (next.has(id)) next.delete(id) - else next.add(id) - return next - }) - } - - function toggleAll() { - setSelectedIds((current) => current.size === users.length ? new Set() : new Set(users.map((user) => user.id))) - } - - const allSelected = users.length > 0 && selectedIds.size === users.length - const activeFilters = Boolean(search || roleFilter || statusFilter) - const summaryCards = useMemo(() => [ - { label: '全部用户', value: summary.total, detail: '当前组织账号', icon: '◎', tone: 'teal' }, - { label: '活跃用户', value: summary.active, detail: '可以正常访问', icon: '✓', tone: 'green' }, - { label: '未激活', value: summary.inactive, detail: '等待重新启用', icon: '—', tone: 'amber' }, - { label: '管理员', value: summary.admins, detail: '拥有管理权限', icon: '◆', tone: 'navy' }, - ], [summary]) - - return ( -
-
-
-
-
U
-
-

云端工作台

-

组织管理中心

-
-
-
系统运行正常管理员视图
-
-
- -
-
工作台/用户管理
-
-
-

成员目录 / DIRECTORY

-

用户管理

-

集中管理组织成员、角色和访问状态。

-
- -
- -
- {summaryCards.map((card) => ( -
-
{card.label}
- {card.value} - {card.detail} -
- ))} -
- -

成员列表

查看并更新组织内的账号信息。

共 {total} 位用户
- -
-
-
- - - setKeyword(event.target.value)} placeholder="搜索姓名或邮箱" /> - -
-
- - - {activeFilters && } -
-
-
- {[['', '全部'], ['ACTIVE', '活跃'], ['INACTIVE', '未激活']].map(([value, label]) => ( - - ))} -
- - {selectedIds.size > 0 &&
已选择 {selectedIds.size} 位用户
} - - {error &&
{error}
} - -
- {isLoading ?
正在加载用户列表
: users.length === 0 ? ( -
没有找到匹配用户试试其他关键词,或直接新增一个用户。{activeFilters && }
- ) : ( -
- - - - {users.map((user) => ( - - - - - - - - - - ))} - -
用户角色状态加入时间最近更新操作
toggleSelected(user.id)} />
{initials(user.fullName)}{user.fullName}{user.email}
{roleLabel(user.role)}{statusLabel(user.status)}{formatDate(user.createdAt)}{formatDate(user.updatedAt)}
-
- )} -
-
-
- - {notice &&
{notice}
} - - {isFormOpen && ( -
{ if (event.target === event.currentTarget) setIsFormOpen(false) }}> - -
- )} -
- ) -} - -export default App diff --git a/frontend/src/assets/README.md b/frontend/src/assets/README.md new file mode 100644 index 0000000..271d533 --- /dev/null +++ b/frontend/src/assets/README.md @@ -0,0 +1,3 @@ +# 静态资源目录 + +图片、字体和其他前端静态资源放在此目录,并在确定资源来源后再接入页面。 diff --git a/frontend/src/components/README.md b/frontend/src/components/README.md new file mode 100644 index 0000000..a84da2d --- /dev/null +++ b/frontend/src/components/README.md @@ -0,0 +1,3 @@ +# 通用组件目录 + +跨路由复用的组件放在此目录;仅用户管理页面使用的组件保留在对应路由目录中。 diff --git a/frontend/src/constants/user.ts b/frontend/src/constants/user.ts new file mode 100644 index 0000000..7298729 --- /dev/null +++ b/frontend/src/constants/user.ts @@ -0,0 +1,24 @@ +import type { Role, Status, UserForm } from '../interfaces/user/model' + +export const ROLE_OPTIONS: Role[] = ['Admin', 'Manager', 'Member', 'Viewer'] + +export const STATUS_OPTIONS: Array<{ value: Status | ''; label: string }> = [ + { value: '', label: '全部' }, + { value: 'ACTIVE', label: '活跃' }, + { value: 'INACTIVE', label: '未激活' }, +] + +export const EMPTY_USER_FORM: UserForm = { + fullName: '', + email: '', + role: 'Member', + status: 'ACTIVE', +} + +export function roleLabel(role: string): string { + return ({ Admin: '管理员', Manager: '经理', Member: '成员', Viewer: '只读成员' })[role] ?? role +} + +export function statusLabel(status: Status): string { + return status === 'ACTIVE' ? '活跃' : '未激活' +} diff --git a/frontend/src/hooks/README.md b/frontend/src/hooks/README.md new file mode 100644 index 0000000..68f5b6b --- /dev/null +++ b/frontend/src/hooks/README.md @@ -0,0 +1,3 @@ +# 自定义 Hook 目录 + +跨组件复用的 React Hook 放在此目录,页面专属逻辑优先保留在对应路由模块中。 diff --git a/frontend/src/http/client.test.ts b/frontend/src/http/client.test.ts new file mode 100644 index 0000000..b5a8c4a --- /dev/null +++ b/frontend/src/http/client.test.ts @@ -0,0 +1,38 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { ApiRequestError, request } from './client' + +afterEach(() => vi.restoreAllMocks()) + +function response(body: unknown, status = 200, requestId = 'request-test') { + return new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json', 'X-Request-Id': requestId }, + }) +} + +describe('HTTP 请求封装', () => { + it('解析统一成功响应并传递请求 ID', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(response({ code: 200, message: 'success', data: { value: 1 } })) + + await expect(request<{ value: number }>('/health')).resolves.toEqual({ value: 1 }) + expect(fetchMock).toHaveBeenCalledOnce() + expect(new Headers(fetchMock.mock.calls[0][1]?.headers).get('X-Request-Id')).toBeTruthy() + }) + + it('GET 网络失败时只重试一次,写操作不重试', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network')) + + await expect(request('/health')).rejects.toThrow('network') + expect(fetchMock).toHaveBeenCalledTimes(2) + fetchMock.mockClear() + await expect(request('/users', { method: 'POST' })).rejects.toThrow('network') + expect(fetchMock).toHaveBeenCalledOnce() + }) + + it('将统一错误转换为 ApiRequestError', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValue(response({ code: 404001, message: '用户不存在', data: null }, 404)) + + await expect(request('/users/1')).rejects.toBeInstanceOf(ApiRequestError) + }) +}) diff --git a/frontend/src/http/client.ts b/frontend/src/http/client.ts new file mode 100644 index 0000000..e010291 --- /dev/null +++ b/frontend/src/http/client.ts @@ -0,0 +1,59 @@ +import type { ApiResponse } from '../interfaces/user/api' + +const apiBaseUrl = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? '/api/v1' + +export class ApiRequestError extends Error { + readonly code: number + readonly requestId: string | null + + constructor(message: string, code: number, requestId: string | null) { + super(message) + this.name = 'ApiRequestError' + this.code = code + this.requestId = requestId + } +} + +function requestId(): string { + if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) { + return crypto.randomUUID() + } + return `web-${Date.now()}` +} + +async function parseResponse(response: Response, currentRequestId: string): Promise { + const payload = await response.json().catch(() => null) as ApiResponse | null + const responseRequestId = response.headers.get('X-Request-Id') ?? currentRequestId + if (!response.ok || !payload || payload.code !== 200) { + throw new ApiRequestError(payload?.message ?? '请求失败,请稍后重试', payload?.code ?? response.status, responseRequestId) + } + return payload.data as T +} + +export async function request(path: string, init: RequestInit = {}): Promise { + const method = (init.method ?? 'GET').toUpperCase() + const retryable = method === 'GET' + let lastError: unknown + + for (let attempt = 0; attempt <= (retryable ? 1 : 0); attempt += 1) { + const currentRequestId = requestId() + try { + const response = await fetch(`${apiBaseUrl}${path}`, { + ...init, + headers: { + Accept: 'application/json', + 'X-Request-Id': currentRequestId, + ...init.headers, + }, + }) + return await parseResponse(response, currentRequestId) + } catch (error) { + lastError = error + if (error instanceof ApiRequestError || attempt >= (retryable ? 1 : 0)) { + throw error + } + } + } + + throw lastError instanceof Error ? lastError : new Error('请求失败,请稍后重试') +} diff --git a/frontend/src/http/userApi.ts b/frontend/src/http/userApi.ts new file mode 100644 index 0000000..e8863e1 --- /dev/null +++ b/frontend/src/http/userApi.ts @@ -0,0 +1,47 @@ +import type { BulkStatusRequest, UserQuery, UserRequest } from '../interfaces/user/api' +import type { User, UserPage, UserSummary } from '../interfaces/user/model' +import { request } from './client' + +function queryString(query: UserQuery): string { + const params = new URLSearchParams({ + keyword: query.keyword, + role: query.role, + status: query.status, + page: String(query.page), + pageSize: String(query.pageSize), + }) + return params.toString() +} + +export const userApi = { + list(query: UserQuery): Promise { + return request(`/users?${queryString(query)}`) + }, + summary(): Promise { + return request('/users/summary') + }, + create(payload: UserRequest): Promise { + return request('/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) + }, + update(id: number, payload: UserRequest): Promise { + return request(`/users/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) + }, + remove(id: number): Promise { + return request(`/users/${id}`, { method: 'DELETE' }) + }, + updateStatuses(payload: BulkStatusRequest): Promise<{ updated: number; status: string }> { + return request<{ updated: number; status: string }>('/users/status', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) + }, +} diff --git a/frontend/src/interfaces/user/api.ts b/frontend/src/interfaces/user/api.ts new file mode 100644 index 0000000..a3c5c10 --- /dev/null +++ b/frontend/src/interfaces/user/api.ts @@ -0,0 +1,24 @@ +import type { Status, UserForm, UserPage, UserSummary } from './model' + +export interface ApiResponse { + code: number + message: string + data: T | null +} + +export interface UserQuery { + keyword: string + role: string + status: Status | '' + page: number + pageSize: number +} + +export interface BulkStatusRequest { + ids: number[] + status: Status +} + +export type UserRequest = UserForm +export type UserListResponse = UserPage +export type UserSummaryResponse = UserSummary diff --git a/frontend/src/interfaces/user/model.ts b/frontend/src/interfaces/user/model.ts new file mode 100644 index 0000000..3c758a2 --- /dev/null +++ b/frontend/src/interfaces/user/model.ts @@ -0,0 +1,29 @@ +export type Status = 'ACTIVE' | 'INACTIVE' + +export type Role = 'Admin' | 'Manager' | 'Member' | 'Viewer' + +export interface User { + id: number + fullName: string + email: string + role: Role + status: Status + createdAt: string + updatedAt: string +} + +export type UserForm = Pick + +export interface UserSummary { + total: number + active: number + inactive: number + admins: number +} + +export interface UserPage { + items: User[] + total: number + page: number + pageSize: number +} diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx new file mode 100644 index 0000000..b3f5f96 --- /dev/null +++ b/frontend/src/routes/index.tsx @@ -0,0 +1,10 @@ +import { createBrowserRouter } from 'react-router-dom' + +import UserManagementLoader from './user-management/Loader' + +export const router = createBrowserRouter([ + { + path: '/', + element: , + }, +]) diff --git a/frontend/src/routes/user-management/Loader.tsx b/frontend/src/routes/user-management/Loader.tsx new file mode 100644 index 0000000..23712a5 --- /dev/null +++ b/frontend/src/routes/user-management/Loader.tsx @@ -0,0 +1,11 @@ +import { lazy, Suspense } from 'react' + +const UserManagementPage = lazy(() => import('./Page')) + +export default function UserManagementLoader() { + return ( +
正在加载页面
}> + +
+ ) +} diff --git a/frontend/src/routes/user-management/Page.tsx b/frontend/src/routes/user-management/Page.tsx new file mode 100644 index 0000000..23f70e6 --- /dev/null +++ b/frontend/src/routes/user-management/Page.tsx @@ -0,0 +1,113 @@ +import type { FormEvent } from 'react' +import { useCallback, useEffect, useState } from 'react' +import { Check, Info, Plus, X } from 'lucide-react' + +import { statusLabel } from '../../constants/user' +import type { User, UserForm, Status } from '../../interfaces/user/model' +import { useUserStore } from '../../stores/userStore' +import styles from './index.module.scss' +import DirectoryToolbar from './components/DirectoryToolbar' +import SummaryCards from './components/SummaryCards' +import UserDrawer from './components/UserDrawer' +import UserTable from './components/UserTable' + +export default function UserManagementPage() { + const [keyword, setKeyword] = useState('') + const [editingUser, setEditingUser] = useState(null) + const [isFormOpen, setIsFormOpen] = useState(false) + const [notice, setNotice] = useState('') + const { + users, total, summary, search, roleFilter, statusFilter, selectedIds, + isLoading, isSaving, isBulkUpdating, error, + setSearch, setRoleFilter, setStatusFilter, clearFilters, + loadUsers, loadSummary, createUser, updateUser, deleteUser, updateSelectedStatus, + toggleSelected, toggleAll, clearError, + } = useUserStore() + + const refreshData = useCallback(async () => { + await Promise.all([loadUsers(), loadSummary()]) + }, [loadSummary, loadUsers]) + + useEffect(() => { void refreshData() }, [refreshData, search, roleFilter, statusFilter]) + + useEffect(() => { + if (!notice) return undefined + const timer = window.setTimeout(() => setNotice(''), 3200) + return () => window.clearTimeout(timer) + }, [notice]) + + function openCreate() { + setEditingUser(null) + clearError() + setIsFormOpen(true) + } + + function openEdit(user: User) { + setEditingUser(user) + clearError() + setIsFormOpen(true) + } + + async function submitForm(form: UserForm) { + const success = editingUser ? await updateUser(editingUser.id, form) : await createUser(form) + if (!success) return + setIsFormOpen(false) + setNotice(editingUser ? '用户信息已更新' : '用户已创建') + await refreshData() + } + + async function removeUser(user: User) { + if (!window.confirm(`确定删除用户“${user.fullName}”吗?此操作不可撤销。`)) return + const success = await deleteUser(user.id) + if (success) { + setNotice('用户已删除') + await refreshData() + } + } + + async function updateStatus(status: Status) { + if (selectedIds.size === 0) return + if (!window.confirm(`确定将选中的 ${selectedIds.size} 位用户设为“${statusLabel(status)}”吗?`)) return + const success = await updateSelectedStatus(status) + if (success) { + setNotice(`已将选中的用户设为${statusLabel(status)}`) + await refreshData() + } + } + + function submitSearch(event: FormEvent) { + event.preventDefault() + setSearch(keyword.trim()) + } + + function clearAllFilters() { + setKeyword('') + clearFilters() + } + + const allSelected = users.length > 0 && selectedIds.size === users.length + const activeFilters = Boolean(search || roleFilter || statusFilter) + const combinedError = error + + return ( +
+
U

云端工作台

组织管理中心

系统运行正常管理员视图
+
+
工作台/用户管理
+

成员目录 / DIRECTORY

用户管理

集中管理组织成员、角色和访问状态。

+ +

成员列表

查看并更新组织内的账号信息。

共 {total} 位用户
+
+ + {selectedIds.size > 0 &&
已选择 {selectedIds.size} 位用户
} + {combinedError &&
{combinedError}
} +
+ {isLoading ?
正在加载用户列表
: users.length === 0 ?
没有找到匹配用户试试其他关键词,或直接新增一个用户。{activeFilters && }
: void removeUser(user)} />} +
+
+
+ {notice &&
{notice}
} + setIsFormOpen(false)} onSubmit={(form) => void submitForm(form)} /> +
+ ) +} diff --git a/frontend/src/routes/user-management/components/DirectoryToolbar/index.module.scss b/frontend/src/routes/user-management/components/DirectoryToolbar/index.module.scss new file mode 100644 index 0000000..287be9e --- /dev/null +++ b/frontend/src/routes/user-management/components/DirectoryToolbar/index.module.scss @@ -0,0 +1,3 @@ +.root { + min-width: 0; +} diff --git a/frontend/src/routes/user-management/components/DirectoryToolbar/index.tsx b/frontend/src/routes/user-management/components/DirectoryToolbar/index.tsx new file mode 100644 index 0000000..7350e77 --- /dev/null +++ b/frontend/src/routes/user-management/components/DirectoryToolbar/index.tsx @@ -0,0 +1,48 @@ +import type { FormEvent } from 'react' +import { Search } from 'lucide-react' + +import { ROLE_OPTIONS, roleLabel, STATUS_OPTIONS } from '../../../../constants/user' +import type { Role, Status } from '../../../../interfaces/user/model' +import styles from './index.module.scss' + +interface DirectoryToolbarProps { + keyword: string + roleFilter: Role | '' + statusFilter: Status | '' + hasFilters: boolean + onKeywordChange: (value: string) => void + onSearch: (event: FormEvent) => void + onRoleChange: (value: Role | '') => void + onStatusChange: (value: Status | '') => void + onClear: () => void +} + +export default function DirectoryToolbar(props: DirectoryToolbarProps) { + return ( + <> +
+
+ +
+
+ {STATUS_OPTIONS.map((option) => ( + + ))} +
+ + ) +} diff --git a/frontend/src/routes/user-management/components/SummaryCards/index.module.scss b/frontend/src/routes/user-management/components/SummaryCards/index.module.scss new file mode 100644 index 0000000..287be9e --- /dev/null +++ b/frontend/src/routes/user-management/components/SummaryCards/index.module.scss @@ -0,0 +1,3 @@ +.root { + min-width: 0; +} diff --git a/frontend/src/routes/user-management/components/SummaryCards/index.tsx b/frontend/src/routes/user-management/components/SummaryCards/index.tsx new file mode 100644 index 0000000..2674043 --- /dev/null +++ b/frontend/src/routes/user-management/components/SummaryCards/index.tsx @@ -0,0 +1,41 @@ +import { ShieldCheck, UserCheck, Users, UserX } from 'lucide-react' +import type { LucideIcon } from 'lucide-react' + +import type { UserSummary } from '../../../../interfaces/user/model' +import styles from './index.module.scss' + +interface SummaryCardsProps { + summary: UserSummary +} + +interface SummaryCard { + label: string + value: number + detail: string + icon: LucideIcon + tone: string +} + +export default function SummaryCards({ summary }: SummaryCardsProps) { + const cards: SummaryCard[] = [ + { label: '全部用户', value: summary.total, detail: '当前组织账号', icon: Users, tone: 'teal' }, + { label: '活跃用户', value: summary.active, detail: '可以正常访问', icon: UserCheck, tone: 'green' }, + { label: '未激活', value: summary.inactive, detail: '等待重新启用', icon: UserX, tone: 'amber' }, + { label: '管理员', value: summary.admins, detail: '拥有管理权限', icon: ShieldCheck, tone: 'navy' }, + ] + + return ( +
+ {cards.map((card) => { + const Icon = card.icon + return ( +
+
{card.label}
+ {card.value} + {card.detail} +
+ ) + })} +
+ ) +} diff --git a/frontend/src/routes/user-management/components/UserDrawer/index.module.scss b/frontend/src/routes/user-management/components/UserDrawer/index.module.scss new file mode 100644 index 0000000..287be9e --- /dev/null +++ b/frontend/src/routes/user-management/components/UserDrawer/index.module.scss @@ -0,0 +1,3 @@ +.root { + min-width: 0; +} diff --git a/frontend/src/routes/user-management/components/UserDrawer/index.tsx b/frontend/src/routes/user-management/components/UserDrawer/index.tsx new file mode 100644 index 0000000..d89b1aa --- /dev/null +++ b/frontend/src/routes/user-management/components/UserDrawer/index.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react' +import { Info, X } from 'lucide-react' + +import { EMPTY_USER_FORM, ROLE_OPTIONS, roleLabel } from '../../../../constants/user' +import type { User, UserForm, Status } from '../../../../interfaces/user/model' +import styles from './index.module.scss' + +interface UserDrawerProps { + user: User | null + isOpen: boolean + isSaving: boolean + error: string + onClose: () => void + onSubmit: (form: UserForm) => void +} + +export default function UserDrawer(props: UserDrawerProps) { + const [form, setForm] = useState(EMPTY_USER_FORM) + + useEffect(() => { + setForm(props.user ? { + fullName: props.user.fullName, + email: props.user.email, + role: props.user.role, + status: props.user.status, + } : EMPTY_USER_FORM) + }, [props.user, props.isOpen]) + + if (!props.isOpen) return null + + return ( +
{ if (event.target === event.currentTarget) props.onClose() }}> + +
+ ) +} diff --git a/frontend/src/routes/user-management/components/UserTable/index.module.scss b/frontend/src/routes/user-management/components/UserTable/index.module.scss new file mode 100644 index 0000000..287be9e --- /dev/null +++ b/frontend/src/routes/user-management/components/UserTable/index.module.scss @@ -0,0 +1,3 @@ +.root { + min-width: 0; +} diff --git a/frontend/src/routes/user-management/components/UserTable/index.tsx b/frontend/src/routes/user-management/components/UserTable/index.tsx new file mode 100644 index 0000000..b29e69e --- /dev/null +++ b/frontend/src/routes/user-management/components/UserTable/index.tsx @@ -0,0 +1,40 @@ +import classNames from 'classnames' +import { Pencil, Trash2 } from 'lucide-react' + +import { roleLabel, statusLabel } from '../../../../constants/user' +import type { User } from '../../../../interfaces/user/model' +import { formatDate, initials } from '../../../../utils/user' +import styles from './index.module.scss' + +interface UserTableProps { + users: User[] + selectedIds: Set + allSelected: boolean + onToggleAll: () => void + onToggleSelected: (id: number) => void + onEdit: (user: User) => void + onDelete: (user: User) => void +} + +export default function UserTable(props: UserTableProps) { + return ( +
+ + + + {props.users.map((user) => ( + + + + + + + + + + ))} + +
用户角色状态加入时间最近更新操作
props.onToggleSelected(user.id)} />
{initials(user.fullName)}{user.fullName}{user.email}
{roleLabel(user.role)}{statusLabel(user.status)}{formatDate(user.createdAt)}{formatDate(user.updatedAt)}
+
+ ) +} diff --git a/frontend/src/routes/user-management/index.module.scss b/frontend/src/routes/user-management/index.module.scss new file mode 100644 index 0000000..67a49e7 --- /dev/null +++ b/frontend/src/routes/user-management/index.module.scss @@ -0,0 +1,3 @@ +.page { + min-height: 100vh; +} diff --git a/frontend/src/stores/userStore.ts b/frontend/src/stores/userStore.ts new file mode 100644 index 0000000..14ebabc --- /dev/null +++ b/frontend/src/stores/userStore.ts @@ -0,0 +1,142 @@ +import { create } from 'zustand' + +import { userApi } from '../http/userApi' +import type { UserQuery, UserRequest } from '../interfaces/user/api' +import type { Role, Status, User, UserSummary } from '../interfaces/user/model' +import { errorMessage } from '../utils/user' + +interface UserStore { + users: User[] + total: number + summary: UserSummary + search: string + roleFilter: Role | '' + statusFilter: Status | '' + selectedIds: Set + isLoading: boolean + isSaving: boolean + isBulkUpdating: boolean + error: string + setSearch: (search: string) => void + setRoleFilter: (role: Role | '') => void + setStatusFilter: (status: Status | '') => void + clearFilters: () => void + loadUsers: () => Promise + loadSummary: () => Promise + createUser: (payload: UserRequest) => Promise + updateUser: (id: number, payload: UserRequest) => Promise + deleteUser: (id: number) => Promise + updateSelectedStatus: (status: Status) => Promise + toggleSelected: (id: number) => void + toggleAll: () => void + clearError: () => void +} + +const emptySummary: UserSummary = { total: 0, active: 0, inactive: 0, admins: 0 } + +function queryFromState(state: UserStore): UserQuery { + return { + keyword: state.search, + role: state.roleFilter, + status: state.statusFilter, + page: 1, + pageSize: 50, + } +} + +export const useUserStore = create((set, get) => ({ + users: [], + total: 0, + summary: emptySummary, + search: '', + roleFilter: '', + statusFilter: '', + selectedIds: new Set(), + isLoading: true, + isSaving: false, + isBulkUpdating: false, + error: '', + setSearch: (search) => set({ search }), + setRoleFilter: (roleFilter) => set({ roleFilter }), + setStatusFilter: (statusFilter) => set({ statusFilter }), + clearFilters: () => set({ search: '', roleFilter: '', statusFilter: '' }), + loadUsers: async () => { + set({ isLoading: true, error: '' }) + try { + const data = await userApi.list(queryFromState(get())) + set({ users: data.items, total: data.total, selectedIds: new Set() }) + } catch (error) { + set({ error: errorMessage(error, '无法加载用户列表') }) + } finally { + set({ isLoading: false }) + } + }, + loadSummary: async () => { + try { + const summary = await userApi.summary() + set({ summary }) + } catch (error) { + set({ error: errorMessage(error, '无法加载用户概览') }) + } + }, + createUser: async (payload) => { + set({ isSaving: true, error: '' }) + try { + await userApi.create(payload) + return true + } catch (error) { + set({ error: errorMessage(error, '保存失败,请检查输入') }) + return false + } finally { + set({ isSaving: false }) + } + }, + updateUser: async (id, payload) => { + set({ isSaving: true, error: '' }) + try { + await userApi.update(id, payload) + return true + } catch (error) { + set({ error: errorMessage(error, '保存失败,请检查输入') }) + return false + } finally { + set({ isSaving: false }) + } + }, + deleteUser: async (id) => { + set({ error: '' }) + try { + await userApi.remove(id) + return true + } catch (error) { + set({ error: errorMessage(error, '删除失败,请稍后重试') }) + return false + } + }, + updateSelectedStatus: async (status) => { + const ids = [...get().selectedIds] + if (ids.length === 0) return false + set({ isBulkUpdating: true, error: '' }) + try { + await userApi.updateStatuses({ ids, status }) + return true + } catch (error) { + set({ error: errorMessage(error, '批量更新失败,请稍后重试') }) + return false + } finally { + set({ isBulkUpdating: false }) + } + }, + toggleSelected: (id) => set((state) => { + const selectedIds = new Set(state.selectedIds) + if (selectedIds.has(id)) selectedIds.delete(id) + else selectedIds.add(id) + return { selectedIds } + }), + toggleAll: () => set((state) => ({ + selectedIds: state.users.length > 0 && state.selectedIds.size === state.users.length + ? new Set() + : new Set(state.users.map((user) => user.id)), + })), + clearError: () => set({ error: '' }), +})) diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 8a8cf98..d1bf30b 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1,3 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + :root { font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #172b36; diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts new file mode 100644 index 0000000..336ce12 --- /dev/null +++ b/frontend/src/test/setup.ts @@ -0,0 +1 @@ +export {} diff --git a/frontend/src/utils/classNames.ts b/frontend/src/utils/classNames.ts new file mode 100644 index 0000000..f56b4c6 --- /dev/null +++ b/frontend/src/utils/classNames.ts @@ -0,0 +1,5 @@ +import classNames from 'classnames' + +export function cn(...values: Parameters): string { + return classNames(...values) +} diff --git a/frontend/src/utils/user.test.ts b/frontend/src/utils/user.test.ts new file mode 100644 index 0000000..1e070c0 --- /dev/null +++ b/frontend/src/utils/user.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest' + +import { formatDate, initials } from './user' + +describe('用户展示工具', () => { + it('可以生成姓名缩写', () => { + expect(initials('Lin Chen')).toBe('LC') + expect(initials('晓晨')).toBe('晓晨') + }) + + it('可以格式化日期', () => { + expect(formatDate('2025-01-02T00:00:00Z')).toContain('2025') + }) +}) diff --git a/frontend/src/utils/user.ts b/frontend/src/utils/user.ts new file mode 100644 index 0000000..2cef01e --- /dev/null +++ b/frontend/src/utils/user.ts @@ -0,0 +1,21 @@ +export function formatDate(value: string): string { + return new Date(value).toLocaleDateString('zh-CN', { + year: 'numeric', + month: 'short', + day: 'numeric', + }) +} + +export function initials(name: string): string { + const parts = name.trim().split(/\s+/).filter(Boolean) + if (parts.length > 1) { + return `${parts[0].charAt(0)}${parts[parts.length - 1].charAt(0)}`.toUpperCase() + } + return name.trim().slice(0, 2).toUpperCase() +} + +export function errorMessage(error: unknown, fallback: string): string { + if (!(error instanceof Error)) return fallback + const requestError = error as Error & { requestId?: string | null } + return requestError.requestId ? `${error.message}(请求编号:${requestError.requestId})` : error.message +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..efc1901 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{ts,tsx}'], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..9f20b0c --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + globals: true, + }, +}) diff --git a/sql/users.sql b/sql/users.sql index c4dd8fc..48730b8 100644 --- a/sql/users.sql +++ b/sql/users.sql @@ -1,13 +1,15 @@ -CREATE TABLE IF NOT EXISTS users ( - id BIGINT NOT NULL AUTO_INCREMENT, - full_name VARCHAR(80) NOT NULL, - email VARCHAR(160) NOT NULL, - role VARCHAR(40) NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, +-- 用户表最终结构。 +-- 执行前请确认目标数据库、影响范围和备份策略;本文件不会由应用自动执行。 +CREATE TABLE IF NOT EXISTS USERS ( + id BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户主键', + full_name VARCHAR(80) NOT NULL COMMENT '用户姓名', + email VARCHAR(160) NOT NULL COMMENT '用户邮箱,必须唯一', + role VARCHAR(40) NOT NULL COMMENT '用户角色:Admin、Manager、Member、Viewer', + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '账号状态:ACTIVE 或 INACTIVE', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', PRIMARY KEY (id), UNIQUE KEY uk_users_email (email), KEY idx_users_full_name (full_name), KEY idx_users_status (status) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='培训项目用户表';