From 0ab50db85ff486989e60035023696c26f25853df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E7=BB=8D=E5=BA=B7?= <3052147989@qq.com> Date: Fri, 27 Feb 2026 14:49:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=8F=AF=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E5=B8=83=E5=B1=80=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增布局管理器支持拖拽调整面板宽度 - 新增状态管理器实现布局持久化 - 重构前端页面结构优化四栏式布局 - 新增 Jest 测试框架和完整测试用例 - 完善项目文档(PRD/FD/TDD/TODO/LOG) - 清理 Python 缓存文件并更新 .gitignore Co-Authored-By: Claude Opus 4.6 --- .gitignore | 9 + .kiro/steering/architecture.md | 93 +- .kiro/steering/project-overview.md | 38 +- PRD/AI_EXECUTION_PROMPT.md | 736 +++ PRD/AI工作提示词-Legacy迁移到Controllers层.md | 524 ++ PRD/PRD-可调整布局系统.md | 1000 ++++ __pycache__/email.cpython-313.pyc | Bin 18940 -> 0 bytes docs/FD/FD-可调整布局系统.md | 1477 +++++ docs/LOG/CN-00001-完成legacy删除.md | 77 + docs/LOG/LOG-00003-可调整布局系统-执行日志.md | 120 + docs/TD/TD-可调整布局系统.md | 633 +++ docs/TDD/TDD-可调整布局系统.md | 1163 ++++ docs/TODO/TODO-可调整布局系统.md | 1384 +++++ package-lock.json | 5002 +++++++++++++++++ package.json | 15 + static/css/layout.css | 230 + static/css/main.css | 54 +- static/js/features/emails.js | 43 +- static/js/layout-bootstrap.js | 65 + static/js/layout-manager.js | 1407 +++++ static/js/state-manager.js | 320 ++ templates/index.html | 155 +- templates/partials/modals.html | 18 + templates/partials/scripts.html | 7 + tests/__pycache__/__init__.cpython-313.pyc | Bin 151 -> 151 bytes tests/__pycache__/_import_app.cpython-313.pyc | Bin 2097 -> 2097 bytes ...t_api_extract_verification.cpython-313.pyc | Bin 5159 -> 5159 bytes .../test_core_features.cpython-313.pyc | Bin 16404 -> 16442 bytes .../test_distributed_lock.cpython-313.pyc | Bin 3830 -> 3860 bytes .../test_error_and_trace.cpython-313.pyc | Bin 29329 -> 29367 bytes .../test_frontend_manual.cpython-313.pyc | Bin 15266 -> 15263 bytes ...est_gptmail_error_handling.cpython-313.pyc | Bin 9724 -> 9782 bytes ...t_masking_audit_and_import.cpython-313.pyc | Bin 15583 -> 15641 bytes .../test_module_boundaries.cpython-313.pyc | Bin 4683 -> 4681 bytes .../test_performance.cpython-313.pyc | Bin 4602 -> 4602 bytes .../test_smoke_contract.cpython-313.pyc | Bin 8797 -> 8797 bytes ...est_verification_extractor.cpython-313.pyc | Bin 18844 -> 18836 bytes tests/layout-system/README.md | 289 + tests/layout-system/coverage/clover.xml | 656 +++ .../coverage/coverage-final.json | 3 + .../coverage/lcov-report/base.css | 224 + .../coverage/lcov-report/block-navigation.js | 87 + .../coverage/lcov-report/favicon.png | Bin 0 -> 445 bytes .../coverage/lcov-report/index.html | 131 + .../lcov-report/layout-manager.js.html | 4306 ++++++++++++++ .../coverage/lcov-report/prettify.css | 1 + .../coverage/lcov-report/prettify.js | 2 + .../lcov-report/sort-arrow-sprite.png | Bin 0 -> 138 bytes .../coverage/lcov-report/sorter.js | 210 + .../lcov-report/state-manager.js.html | 1045 ++++ tests/layout-system/coverage/lcov.info | 1377 +++++ .../integration/drag-resize.test.js | 112 + .../integration/keyboard-resizer.test.js | 75 + .../integration/panel-collapse.test.js | 115 + .../integration/reset-layout.test.js | 74 + .../integration/state-error-handling.test.js | 101 + .../integration/state-load.test.js | 60 + .../integration/state-multi-user.test.js | 82 + .../integration/state-save.test.js | 85 + .../integration/window-adapt.test.js | 125 + tests/layout-system/jest.config.js | 46 + tests/layout-system/setup.js | 113 + tests/layout-system/test-utils.js | 297 + .../layout-system/unit/layout-manager.test.js | 623 ++ .../layout-system/unit/state-manager.test.js | 266 + 65 files changed, 24948 insertions(+), 127 deletions(-) create mode 100644 PRD/AI_EXECUTION_PROMPT.md create mode 100644 PRD/AI工作提示词-Legacy迁移到Controllers层.md create mode 100644 PRD/PRD-可调整布局系统.md delete mode 100644 __pycache__/email.cpython-313.pyc create mode 100644 docs/FD/FD-可调整布局系统.md create mode 100644 docs/LOG/CN-00001-完成legacy删除.md create mode 100644 docs/LOG/LOG-00003-可调整布局系统-执行日志.md create mode 100644 docs/TD/TD-可调整布局系统.md create mode 100644 docs/TDD/TDD-可调整布局系统.md create mode 100644 docs/TODO/TODO-可调整布局系统.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 static/css/layout.css create mode 100644 static/js/layout-bootstrap.js create mode 100644 static/js/layout-manager.js create mode 100644 static/js/state-manager.js create mode 100644 tests/layout-system/README.md create mode 100644 tests/layout-system/coverage/clover.xml create mode 100644 tests/layout-system/coverage/coverage-final.json create mode 100644 tests/layout-system/coverage/lcov-report/base.css create mode 100644 tests/layout-system/coverage/lcov-report/block-navigation.js create mode 100644 tests/layout-system/coverage/lcov-report/favicon.png create mode 100644 tests/layout-system/coverage/lcov-report/index.html create mode 100644 tests/layout-system/coverage/lcov-report/layout-manager.js.html create mode 100644 tests/layout-system/coverage/lcov-report/prettify.css create mode 100644 tests/layout-system/coverage/lcov-report/prettify.js create mode 100644 tests/layout-system/coverage/lcov-report/sort-arrow-sprite.png create mode 100644 tests/layout-system/coverage/lcov-report/sorter.js create mode 100644 tests/layout-system/coverage/lcov-report/state-manager.js.html create mode 100644 tests/layout-system/coverage/lcov.info create mode 100644 tests/layout-system/integration/drag-resize.test.js create mode 100644 tests/layout-system/integration/keyboard-resizer.test.js create mode 100644 tests/layout-system/integration/panel-collapse.test.js create mode 100644 tests/layout-system/integration/reset-layout.test.js create mode 100644 tests/layout-system/integration/state-error-handling.test.js create mode 100644 tests/layout-system/integration/state-load.test.js create mode 100644 tests/layout-system/integration/state-multi-user.test.js create mode 100644 tests/layout-system/integration/state-save.test.js create mode 100644 tests/layout-system/integration/window-adapt.test.js create mode 100644 tests/layout-system/jest.config.js create mode 100644 tests/layout-system/setup.js create mode 100644 tests/layout-system/test-utils.js create mode 100644 tests/layout-system/unit/layout-manager.test.js create mode 100644 tests/layout-system/unit/state-manager.test.js diff --git a/.gitignore b/.gitignore index a184b8d..321b12a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,15 @@ data/ *.swp *.swo +# Node / Jest +node_modules/ +tests/layout-system/coverage/ + +# 本地临时文件/素材(避免误提交) +PixPin_*.png +exeample/ +2026-02-26-api-key-credit-monitoring.md + # 日志 *.log diff --git a/.kiro/steering/architecture.md b/.kiro/steering/architecture.md index d180e05..b3405d3 100644 --- a/.kiro/steering/architecture.md +++ b/.kiro/steering/architecture.md @@ -35,12 +35,11 @@ - `app.py` - Flask 应用工厂,负责应用初始化和 Blueprint 注册 - `config.py` - 配置管理,环境变量读取 - `db.py` - 数据库连接管理,Schema 初始化和升级 -- `legacy.py` - 旧版实现,正在迁移中 (约 5000 行) - `audit.py` - 审计日志记录 - `errors.py` - 错误处理和 trace_id 生成 #### routes/ 路由层 -Blueprint 模块化路由,每个文件对应一个功能模块: +Blueprint 模块化路由,每个文件对应一个功能模块 (共 11 个): - `pages.py` - 页面路由 (首页/登录页) - `accounts.py` - 邮箱账号 CRUD @@ -54,8 +53,23 @@ Blueprint 模块化路由,每个文件对应一个功能模块: - `system.py` - 系统操作 (导出/密码修改) - `audit.py` - 审计日志查询 +#### controllers/ 控制器层 +请求处理和参数验证,每个文件对应一个路由模块 (共 11 个): + +- `pages.py` - 页面控制器 +- `accounts.py` - 账号管理控制器 +- `groups.py` - 分组管理控制器 +- `tags.py` - 标签控制器 +- `emails.py` - 邮件操作控制器 +- `temp_emails.py` - 临时邮箱控制器 +- `oauth.py` - OAuth 控制器 +- `settings.py` - 系统设置控制器 +- `scheduler.py` - 调度器控制器 +- `system.py` - 系统控制器 +- `audit.py` - 审计控制器 + #### services/ 服务层 -业务逻辑实现,封装外部 API 调用: +业务逻辑实现,封装外部 API 调用 (共 9 个): - `graph.py` - Microsoft Graph API 封装 - 获取邮件列表 @@ -72,6 +86,9 @@ Blueprint 模块化路由,每个文件对应一个功能模块: - 定时刷新调度 - `email_delete.py` - 邮件删除服务 - 多 API 回退策略 +- `scheduler.py` - 定时任务调度器 + - APScheduler 封装 + - 任务管理 - `gptmail.py` - GPTMail API 封装 - `http.py` - HTTP 客户端封装 (支持代理) - `verification_extractor.py` - 验证码提取服务 @@ -80,7 +97,7 @@ Blueprint 模块化路由,每个文件对应一个功能模块: - 正则表达式匹配 #### repositories/ 数据访问层 -数据库 CRUD 操作封装: +数据库 CRUD 操作封装 (共 8 个): - `accounts.py` - 邮箱账号数据访问 - 增删改查 @@ -94,8 +111,18 @@ Blueprint 模块化路由,每个文件对应一个功能模块: - `refresh_runs.py` - 刷新运行记录数据访问 - `distributed_locks.py` - 分布式锁实现 +#### middleware/ 中间件 +横切关注点处理 (共 2 个): + +- `trace.py` - trace_id 中间件 + - 请求追踪 ID 生成 + - 日志关联 +- `error_handler.py` - 错误处理中间件 + - 统一错误响应 + - 异常捕获 + #### security/ 安全模块 -安全相关功能封装: +安全相关功能封装 (共 3 个): - `auth.py` - 认证授权 - 登录验证 @@ -287,17 +314,53 @@ Repository 层封装数据访问,业务逻辑与数据库解耦。 - Blueprint 可独立部署 - 服务层可抽取为独立服务 -## 迁移策略 +## 前端架构 -### 当前状态 -- `legacy.py` 包含旧版实现 -- 新架构逐步替换旧实现 -- 保持 API 兼容性 +### 静态资源结构 +``` +static/ +├── css/ +│ ├── main.css # 主样式 +│ └── layout.css # 布局系统样式 +└── js/ + ├── main.js # 主入口 + ├── layout-manager.js # 布局管理器 + ├── layout-bootstrap.js # 布局初始化 + ├── state-manager.js # 状态管理 + └── features/ # 功能模块 + ├── accounts.js # 账号管理 + ├── emails.js # 邮件操作 + ├── groups.js # 分组管理 + └── temp_emails.js # 临时邮箱 +``` -### 迁移步骤 +### 模板结构 +``` +templates/ +├── index.html # 主页面 +├── login.html # 登录页 +└── partials/ # 页面片段 + ├── modals.html # 模态框 + └── scripts.html # 脚本引用 +``` + +### 布局系统 +- 四栏式可调整布局 (分组/账号/邮件列表/邮件详情) +- 拖拽调整栏宽,实时保存 +- 支持折叠/展开 +- 响应式设计,自适应窗口大小 + +## 架构演进 + +### 已完成的迁移 1. ✅ 创建新的分层结构 2. ✅ Blueprint 模块化路由 -3. 🔄 逐步迁移业务逻辑到 Services -4. 🔄 数据访问迁移到 Repositories -5. ⏳ 移除 legacy.py -6. ⏳ 前后端完全分离 +3. ✅ 业务逻辑迁移到 Services +4. ✅ 数据访问迁移到 Repositories +5. ✅ 移除 legacy.py +6. ✅ 中间件统一处理 + +### 下一步计划 +- ⏳ 前后端完全分离 (RESTful API) +- ⏳ 前端框架引入 (Vue.js/React) +- ⏳ API 文档完善 (OpenAPI/Swagger) diff --git a/.kiro/steering/project-overview.md b/.kiro/steering/project-overview.md index 343afc2..f600440 100644 --- a/.kiro/steering/project-overview.md +++ b/.kiro/steering/project-overview.md @@ -34,21 +34,29 @@ Web 应用 - Flask 后端 + 传统前端 ``` outlookEmail/ -├── web_outlook_app.py # 应用入口 (兼容层) +├── web_outlook_app.py # 应用入口 ├── outlook_mail_reader.py # 命令行邮件读取工具 ├── outlook_web/ # 主应用模块 │ ├── app.py # Flask 应用工厂 │ ├── config.py # 配置管理 │ ├── db.py # 数据库操作 -│ ├── legacy.py # 旧版实现 (迁移中) │ ├── audit.py # 审计日志 │ ├── errors.py # 错误处理 │ ├── routes/ # 路由层 (Blueprint) +│ ├── controllers/ # 控制器层 │ ├── services/ # 服务层 │ ├── repositories/ # 数据访问层 -│ └── security/ # 安全模块 +│ ├── security/ # 安全模块 +│ └── middleware/ # 中间件 ├── templates/ # HTML 模板 +│ ├── index.html # 主页面 +│ ├── login.html # 登录页 +│ └── partials/ # 页面片段 ├── static/ # 静态资源 +│ ├── css/ # 样式文件 +│ └── js/ # JavaScript 文件 +│ ├── features/ # 功能模块 +│ └── layout-*.js # 布局系统 ├── tests/ # 测试 ├── docs/ # 文档 └── data/ # 数据目录 @@ -100,7 +108,14 @@ outlookEmail/ - 连续错误自动停止 - 可配置轮询间隔/次数 -### 8. 安全特性 +### 8. 可调整布局系统 +- 四栏式响应式布局 +- 拖拽调整栏宽 +- 布局状态持久化 +- 支持折叠/展开 +- 自适应窗口大小 + +### 9. 安全特性 - XSS 防护 (DOMPurify + iframe 沙箱) - CSRF 防护 (Flask-WTF) - 数据加密 (Fernet 对称加密) @@ -137,10 +152,10 @@ Database (SQLite) ### 应用工厂模式 使用 `create_app()` 工厂函数创建 Flask 应用实例,便于测试和配置管理。 -### 迁移中架构 -- `legacy.py` 包含旧版实现 (约 20 万字节) -- 正在逐步迁移到新的分层架构 -- 保持向后兼容性 +### 完整分层架构 +- 已完成从单体架构到分层架构的迁移 +- Routes → Controllers → Services → Repositories 清晰分层 +- 中间件统一处理横切关注点 ## 数据模型 @@ -195,13 +210,14 @@ python web_outlook_app.py - ✅ 验证码提取 (Graph → IMAP 回退) - ✅ 自动轮询与通知 - ✅ 批量删除邮箱 +- ✅ 架构迁移完成 (legacy.py 已移除) +- ✅ 可调整布局系统 ### 进行中 -- 🔄 架构迁移 (legacy.py → 分层架构) - 🔄 前后端分离准备 +- 🔄 单元测试覆盖率提升 ### 待优化 -- ⏳ 完全移除 legacy.py - ⏳ 前后端完全分离 - ⏳ API 文档完善 -- ⏳ 单元测试覆盖率提升 +- ⏳ 性能优化与监控 diff --git a/PRD/AI_EXECUTION_PROMPT.md b/PRD/AI_EXECUTION_PROMPT.md new file mode 100644 index 0000000..f27dce4 --- /dev/null +++ b/PRD/AI_EXECUTION_PROMPT.md @@ -0,0 +1,736 @@ +# AI 执行提示词 - Outlook 邮件管理工具批量操作与自动化增强 + +## 项目背景 + +你正在为一个 Outlook 邮件管理工具添加三个核心功能: + +1. **全选功能**:在分组面板的排序按钮右边添加全选复选框,支持一键选中当前分组下的所有邮箱账号,用于批量操作(移动分组、打标签) +2. **邮件信息快速复制功能**:在每个邮箱账号名称右边添加复制按钮,点击后自动提取最新邮件中的验证码和链接并复制到剪贴板 +3. **自动轮询与通知功能**:支持定时轮询选中邮箱的新邮件,检测到新邮件时显示页面通知和视觉提示(红点) + +## 核心技术要求 + +### 验证码提取算法 +- **智能识别**:基于关键词("验证码"、"code"、"OTP"等)搜索上下文(前后50字符),提取4-8位数字/字母组合 +- **保底提取**:使用正则表达式提取所有4-8位数字/字母组合,过滤日期(1900-2100)和时间(00:00-23:59)格式 +- **链接提取**:使用正则提取所有HTTP/HTTPS链接并去重 +- **输出格式**:`验证码 链接1 链接2`(空格分隔) + +### 性能指标 +- 全选1000个邮箱:< 100ms +- 验证码提取API响应:< 2秒 +- 轮询不影响页面交互 + +### 轮询机制 +- 客户端定时器(setInterval) +- 默认配置:10秒间隔,5次轮询 +- 静默轮询,仅在检测到新邮件时显示通知 +- 切换邮箱或达到次数后自动停止 + +## 参考文档 + +在开始实现前,请仔细阅读以下文档: + +1. **PRD(产品需求文档)**:`docs/PRD/Outlook邮件管理工具-批量操作与自动化增强PRD.md` + - 了解业务目标、用户故事、功能需求 + +2. **FD(功能设计文档)**:`docs/FD/Outlook邮件管理工具-批量操作与自动化增强FD.md` + - 了解需要实现的功能清单(27项功能验收 + 5项非功能验收 + 5项回归验收) + +3. **TDD(技术设计文档)**:`docs/TDD/Outlook邮件管理工具-批量操作与自动化增强TDD.md` + - 了解详细的技术实现方案、算法设计、API规范、数据结构 + +4. **TODO(任务清单)**:`docs/TODO/Outlook邮件管理工具-批量操作与自动化增强TODO.md` + - 这是你的主要执行指南,包含详细的任务分解和验收标准 + +5. **测试文档**:`docs/TEST/` 目录下的测试文档 + - 了解测试用例和验收标准 + +6. **测试代码**:`tests/` 目录下的测试文件 + - 参考已准备好的测试用例结构 + +## 执行计划 + +请严格按照以下阶段顺序执行,每个阶段完成后进行自测: + +### 阶段 0:准备工作(必须先完成) + +```bash +# 1. 确认现有测试通过 +python -m unittest discover -s tests -v + +# 2. 准备测试邮件样本 +# 创建至少10封测试邮件,覆盖不同验证码格式(参考测试文档中的15个邮件样本) +``` + +**验收标准**: +- [ ] 所有现有单元测试通过 +- [ ] 测试邮件样本已准备(至少10封) +- [ ] 已仔细阅读所有参考文档 + +--- + +### 阶段 1:后端基础(2-3天) + +**目标**:实现验证码提取服务和API接口 + +#### 1.1 创建验证码提取服务 + +**文件**:`outlook_web/services/verification_extractor.py` + +**核心函数**: +```python +def smart_extract_verification_code(email_content: str) -> Optional[str]: + """智能识别验证码(基于关键词)""" + pass + +def fallback_extract_verification_code(email_content: str) -> Optional[str]: + """保底提取验证码(正则 + 过滤)""" + pass + +def extract_links(email_content: str) -> List[str]: + """提取所有HTTP/HTTPS链接""" + pass + +def extract_email_text(email: dict) -> str: + """从邮件对象提取纯文本(HTML转文本)""" + pass + +def extract_verification_info(email: dict) -> dict: + """完整提取流程(验证码 + 链接 + 格式化)""" + pass +``` + +**实现要点**: +- 关键词列表:`["验证码", "code", "验证", "verification", "OTP", "动态码", "校验码", "verify code"]` +- 验证码正则:`r'\b[A-Z0-9]{4,8}\b'` +- 链接正则:`r'https?://[^\s<>"{}|\\^`\[\]]+'` +- 日期过滤:`1900 <= year <= 2100` +- 时间过滤:`00:00 <= time <= 23:59` + +#### 1.2 创建API接口 + +**文件**:`outlook_web/routes/emails.py` 和 `outlook_web/legacy.py` + +**路由**:`GET /api/emails//extract-verification` + +**响应格式**: +```json +{ + "success": true, + "data": { + "verification_code": "123456", + "links": ["https://example.com/verify"], + "formatted": "123456 https://example.com/verify" + }, + "trace_id": "uuid" +} +``` + +**错误处理**: +- 邮箱不存在 → 404 +- 未找到邮件 → 404 +- 未找到验证信息 → 404 +- Token过期 → 401 +- 服务器错误 → 500 + +#### 1.3 编写单元测试 + +**文件**:`tests/test_verification_extractor.py`(已创建,需要实现TODO部分) + +**测试覆盖**: +- 智能识别:中文关键词、英文关键词、OTP关键词 +- 保底提取:纯数字、数字+字母、过滤日期/时间 +- 链接提取:单个、多个、去重 +- 边界情况:空邮件、未找到、HTML转文本 + +**验收标准**: +- [ ] 所有单元测试通过(18个测试用例) +- [ ] 验证码提取成功率 > 90%(基于测试邮件样本) +- [ ] API响应时间 < 2秒 + +--- + +### 阶段 2:后端配置(1天) + +**目标**:添加轮询配置到数据库和设置API + +#### 2.1 数据库变更 + +**文件**:`outlook_web/db.py` + +**修改位置**:`init_db()` 函数 + +**添加配置**: +```python +# 在 init_db() 函数中添加 +cursor.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", + ("enable_auto_polling", "false")) +cursor.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", + ("polling_interval", "10")) +cursor.execute("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)", + ("polling_count", "5")) +``` + +#### 2.2 更新设置API + +**文件**:`outlook_web/legacy.py` + +**修改接口**: +- `GET /api/settings`:添加返回 `enable_auto_polling`、`polling_interval`、`polling_count` +- `PUT /api/settings`:添加处理这三个字段,验证范围(interval: 5-300, count: 1-100) + +**验收标准**: +- [ ] 数据库初始化时自动创建配置项 +- [ ] GET /api/settings 返回轮询配置 +- [ ] PUT /api/settings 可以更新轮询配置 +- [ ] 参数验证正确(超出范围返回400错误) + +--- + +### 阶段 3:前端基础(2-3天) + +**目标**:实现全选功能和复制按钮 + +#### 3.1 全选功能 + +**文件**:`static/js/features/accounts.js` 或 `static/js/main.js` + +**核心函数**: +```javascript +// 全选状态枚举 +const SelectAllState = { + NONE: 'none', // 未选中 + PARTIAL: 'partial', // 部分选中 + ALL: 'all' // 全选 +}; + +// 全选/取消全选 +function toggleSelectAll() { + const state = getSelectAllState(); + if (state === SelectAllState.ALL) { + unselectAllAccounts(); + } else { + selectAllAccounts(); + } +} + +// 选中所有邮箱 +function selectAllAccounts() { + // 使用 Set 数据结构(O(1)查找) + // 使用 requestAnimationFrame 优化渲染 +} + +// 更新全选复选框状态 +function updateSelectAllCheckbox() { + const checkbox = document.getElementById('selectAllAccounts'); + const state = getSelectAllState(); + + checkbox.checked = (state === SelectAllState.ALL); + checkbox.indeterminate = (state === SelectAllState.PARTIAL); +} +``` + +**HTML结构**(在排序按钮右边添加): +```html + +``` + +**CSS样式**: +```css +.select-all-container { + display: inline-flex; + align-items: center; + margin-left: 10px; +} + +input[type="checkbox"]:indeterminate { + /* 半选状态样式 */ +} +``` + +#### 3.2 复制按钮 + +**文件**:`static/js/features/accounts.js` + +**核心函数**: +```javascript +async function copyVerificationInfo(email) { + const btn = event.target; + + // 1. 设置加载状态 + btn.disabled = true; + btn.classList.add('loading'); + + try { + // 2. 调用API提取验证信息 + const response = await fetch(`/api/emails/${email}/extract-verification`); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || '提取失败'); + } + + // 3. 复制到剪贴板 + await copyToClipboard(data.data.formatted); + + // 4. 显示成功状态 + showNotification('已复制'); + btn.classList.add('success'); + setTimeout(() => btn.classList.remove('success'), 1000); + + } catch (error) { + // 5. 处理错误 + if (error.message.includes('未找到')) { + showNotification('未找到验证信息', 'error'); + } else { + showNotification('复制失败', 'error'); + } + } finally { + // 6. 恢复按钮状态 + btn.disabled = false; + btn.classList.remove('loading'); + } +} + +async function copyToClipboard(text) { + // 优先使用 Clipboard API + if (navigator.clipboard) { + await navigator.clipboard.writeText(text); + } else { + // 降级方案:execCommand + const textarea = document.createElement('textarea'); + textarea.value = text; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + } +} +``` + +**HTML结构**(在账号名称右边添加): +```html + +``` + +**验收标准**: +- [ ] 全选复选框显示在排序按钮右边 +- [ ] 点击全选可以选中/取消选中所有邮箱 +- [ ] 半选状态正确显示 +- [ ] 切换分组时全选状态重置 +- [ ] 复制按钮显示在账号名称右边 +- [ ] 点击复制按钮成功复制验证码和链接 +- [ ] 显示"已复制"通知 +- [ ] 按钮有加载/成功/失败状态的视觉反馈 +- [ ] 全选1000个邮箱耗时 < 100ms + +--- + +### 阶段 4:前端轮询与通知(2-3天) + +**目标**:实现自动轮询和新邮件通知 + +#### 4.1 设置界面 + +**文件**:`templates/index.html` 或设置模态框 + +**HTML结构**: +```html +
+ +
+
+ + + 建议:10-30秒 +
+
+ + + 建议:3-10次 +
+``` + +#### 4.2 轮询逻辑 + +**文件**:`static/js/features/polling.js` 或 `static/js/main.js` + +**核心函数**: +```javascript +// 全局变量 +let pollingTimer = null; +let pollingCount = 0; +let pollingMaxCount = 5; +let pollingInterval = 10000; // 毫秒 +let isPollingEnabled = false; +let lastEmailIds = new Set(); + +// 启动轮询 +function startPolling(email) { + if (!isPollingEnabled) return; + + stopPolling(); // 先停止之前的轮询 + + pollingCount = 0; + lastEmailIds = new Set(getCurrentEmails().map(e => e.id)); + + console.log(`开始轮询邮箱: ${email}, 间隔: ${pollingInterval}ms, 次数: ${pollingMaxCount}`); + + pollingTimer = setInterval(() => { + pollForNewEmails(email); + }, pollingInterval); +} + +// 停止轮询 +function stopPolling() { + if (pollingTimer) { + clearInterval(pollingTimer); + pollingTimer = null; + console.log('停止轮询'); + } +} + +// 轮询检查新邮件 +async function pollForNewEmails(email) { + pollingCount++; + console.log(`轮询第 ${pollingCount}/${pollingMaxCount} 次`); + + try { + // 1. 获取最新邮件列表 + const response = await fetch(`/api/emails/${email}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error('获取邮件失败'); + } + + // 2. 检测新邮件 + const newEmails = detectNewEmails(lastEmailIds, data.emails); + + // 3. 如果有新邮件,显示通知 + if (newEmails.length > 0) { + showNewEmailNotification(email, newEmails); + showNewEmailBadge(email); + } + + // 4. 更新邮件ID集合 + lastEmailIds = new Set(data.emails.map(e => e.id)); + + // 5. 达到次数后停止 + if (pollingCount >= pollingMaxCount) { + stopPolling(); + console.log('轮询已完成'); + } + + } catch (error) { + console.error('轮询失败:', error); + stopPolling(); + showNotification('轮询失败,已自动停止', 'error'); + } +} + +// 检测新邮件 +function detectNewEmails(oldEmailIds, newEmails) { + return newEmails.filter(email => !oldEmailIds.has(email.id)); +} +``` + +#### 4.3 通知组件 + +**文件**:`static/js/features/notification.js` + +**核心函数**: +```javascript +function showNewEmailNotification(email, newEmails) { + const notification = document.createElement('div'); + notification.className = 'email-notification'; + notification.innerHTML = ` +
+ ${email} + +
+
+ 收到新邮件:${newEmails[0].subject} +
+ `; + + document.body.appendChild(notification); + + // 5秒后自动消失 + setTimeout(() => { + notification.remove(); + }, 5000); +} + +function showNewEmailBadge(email) { + const accountItem = document.querySelector(`[data-email="${email}"]`); + if (accountItem) { + const badge = document.createElement('span'); + badge.className = 'new-email-badge'; + accountItem.appendChild(badge); + } +} + +function hideNewEmailBadge(email) { + const badge = document.querySelector(`[data-email="${email}"] .new-email-badge`); + if (badge) { + badge.remove(); + } +} +``` + +**CSS样式**: +```css +.email-notification { + position: fixed; + top: 20px; + right: 20px; + background: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + padding: 16px; + min-width: 300px; + animation: slideIn 0.3s ease-out; + z-index: 9999; +} + +@keyframes slideIn { + from { + transform: translateX(400px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +.new-email-badge { + display: inline-block; + width: 8px; + height: 8px; + background: #ff4444; + border-radius: 50%; + margin-left: 8px; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} +``` + +**集成到现有代码**: +```javascript +// 在 selectAccount() 函数中添加 +function selectAccount(email) { + // ... 现有代码 ... + + // 启动轮询 + startPolling(email); + + // 清除红点 + hideNewEmailBadge(email); +} + +// 在 loadAccountsByGroup() 函数中添加 +function loadAccountsByGroup(groupId) { + // 停止轮询 + stopPolling(); + + // ... 现有代码 ... +} + +// 在页面卸载时停止轮询 +window.addEventListener('beforeunload', () => { + stopPolling(); +}); +``` + +**验收标准**: +- [ ] 设置界面显示轮询配置项 +- [ ] 启用轮询后选中邮箱时自动开始轮询 +- [ ] 轮询到达次数后自动停止 +- [ ] 检测到新邮件时显示通知 +- [ ] 邮箱列表显示红点提示 +- [ ] 轮询过程不影响页面操作 +- [ ] 切换邮箱时停止当前轮询 +- [ ] 通知5秒后自动消失 + +--- + +### 阶段 5:优化与完善(1-2天) + +**目标**:性能优化、用户体验优化、测试完善 + +#### 5.1 性能优化 + +**全选性能优化**: +```javascript +function selectAllAccounts() { + const accounts = Array.from(document.querySelectorAll('.account-item')); + + // 使用 DocumentFragment 减少重排重绘 + requestAnimationFrame(() => { + accounts.forEach(account => { + const checkbox = account.querySelector('.account-checkbox'); + checkbox.checked = true; + selectedAccounts.add(account.dataset.email); + }); + updateBatchActionBar(); + updateSelectAllCheckbox(); + }); +} +``` + +**验证码提取性能优化**: +- 优化正则表达式(避免回溯) +- 缓存邮件内容(避免重复提取) + +**轮询性能优化**: +- 使用 `async/await` 避免阻塞 +- 使用 `AbortController` 支持取消请求 + +#### 5.2 用户体验优化 + +- 复制按钮添加 tooltip 提示 +- 通知组件支持多条通知队列 +- 设置界面添加配置说明和建议值 + +#### 5.3 测试完善 + +**运行所有测试**: +```bash +# 单元测试 +python -m unittest discover -s tests -v + +# 手动测试(参考 tests/test_frontend_manual.py) +# 在浏览器中执行测试用例 +``` + +**验收标准**: +- [ ] 所有单元测试通过(18个) +- [ ] 所有API测试通过(9个) +- [ ] 所有前端测试通过(21个) +- [ ] 所有性能测试通过(3个) +- [ ] 所有回归测试通过(5个) + +--- + +## 最终验收清单(必须全部通过) + +### 功能验收(27项) + +#### 全选功能(7项) +- [ ] 全选复选框显示在排序按钮右边 +- [ ] 点击全选复选框可以选中/取消选中当前分组下的所有邮箱 +- [ ] 全选状态与邮箱列表选中状态实时同步(全选/半选/未选) +- [ ] 全选后可以执行批量移动分组操作 +- [ ] 全选后可以执行批量打标签/去标签操作 +- [ ] 切换分组时全选状态重置 +- [ ] 搜索或筛选后,全选只影响当前显示的邮箱 + +#### 复制功能(9项) +- [ ] 复制按钮显示在邮箱名称右边 +- [ ] 点击复制按钮可以自动提取验证码和链接 +- [ ] 验证码提取成功率 > 90%(基于测试邮件样本) +- [ ] 链接提取能正确提取所有 HTTP/HTTPS 链接 +- [ ] 复制成功后显示"已复制"通知 +- [ ] 未找到验证信息时显示"未找到验证信息"提示 +- [ ] 邮箱无数据时自动触发获取邮件操作 +- [ ] 复制按钮有加载状态、成功状态、失败状态的视觉反馈 +- [ ] 后端 API `/api/emails//extract-verification` 正常工作 + +#### 轮询功能(11项) +- [ ] 设置中心有"自动轮询"配置项(开关、间隔、次数) +- [ ] 启用轮询后,选中邮箱时自动开始轮询 +- [ ] 轮询到达设置次数后自动停止 +- [ ] 检测到新邮件时显示页面通知(邮箱地址 + 邮件主题) +- [ ] 邮箱列表显示新邮件视觉提示(红点/角标) +- [ ] 轮询过程静默进行,不干扰用户操作 +- [ ] 轮询失败时停止并显示错误提示 +- [ ] 切换邮箱时停止当前轮询 +- [ ] 设置保存后下次选中邮箱时生效 +- [ ] 数据库 settings 表包含轮询配置项 +- [ ] 通知 5 秒后自动消失或点击关闭 + +### 非功能验收(5项) +- [ ] 全选操作在 100ms 内完成(1000 个邮箱) +- [ ] 验证码提取 API 响应时间 < 2 秒 +- [ ] 轮询不影响页面正常使用 +- [ ] 所有操作都有明确的成功/失败反馈 +- [ ] 错误提示清晰易懂 + +### 回归验收(5项) +- [ ] 现有批量操作功能正常(移动分组、打标签) +- [ ] 现有邮件获取功能正常 +- [ ] 现有设置功能正常 +- [ ] 单元测试通过:`python -m unittest discover -s tests -v` +- [ ] 前端页面关键路径抽样回归(登录、分组/账号、邮件查看) + +--- + +## 重要提示 + +### 开发原则 +1. **小步快跑**:每完成一个小功能就测试,不要一次性写太多代码 +2. **测试驱动**:先写测试用例,再实现功能 +3. **代码复用**:优先使用现有的函数和组件,避免重复造轮子 +4. **错误处理**:所有异步操作都要有 try-catch,所有API调用都要处理错误 +5. **性能优先**:注意性能指标,使用性能分析工具验证 + +### 常见陷阱 +1. **验证码识别**:不要过度依赖智能识别,保底规则很重要 +2. **浏览器兼容性**:Clipboard API 需要降级方案 +3. **轮询性能**:高频轮询会影响性能,使用异步请求 +4. **内存泄漏**:记得在页面卸载时清理定时器 +5. **状态同步**:全选状态要与邮箱列表实时同步 + +### 调试技巧 +```javascript +// 开启调试日志 +const DEBUG = true; +function log(...args) { + if (DEBUG) console.log('[DEBUG]', ...args); +} + +// 性能测试 +console.time('全选性能'); +selectAllAccounts(); +console.timeEnd('全选性能'); + +// 网络请求监控 +// 打开浏览器开发者工具 -> Network 标签 +``` + +--- + +## 开始执行 + +现在请按照以下步骤开始执行: + +1. **阅读所有参考文档**(PRD、FD、TDD、TODO、测试文档) +2. **完成阶段0的准备工作**(确认测试通过、准备测试数据) +3. **从阶段1开始逐步实现**(后端 → 前端 → 优化) +4. **每个阶段完成后进行自测**(单元测试 + 手动测试) +5. **最终完成所有验收清单**(37项验收标准) + +**预计总时间**:8-12天(P0必须完成) + +**里程碑**: +- 里程碑1:后端API完成,单元测试通过(3天) +- 里程碑2:前端基础功能完成,手动测试通过(6天) +- 里程碑3:轮询与通知功能完成,集成测试通过(9天) +- 里程碑4:所有验收清单通过,文档更新完成(12天) + +祝你开发顺利!如有疑问,请参考技术设计文档(TDD)中的详细说明。 diff --git a/PRD/AI工作提示词-Legacy迁移到Controllers层.md b/PRD/AI工作提示词-Legacy迁移到Controllers层.md new file mode 100644 index 0000000..78ebe7c --- /dev/null +++ b/PRD/AI工作提示词-Legacy迁移到Controllers层.md @@ -0,0 +1,524 @@ +# AI 工作提示词:Legacy 迁移到 Controllers 层 + +## 角色定义 + +你是一名高级 Python/Flask 开发工程师,负责将 Outlook 邮件管理工具的 legacy.py(5757 行)迁移到清晰的四层架构(routes → controllers → services → repositories)。 + +你的核心职责是: +- 按照既定的技术方案执行代码迁移 +- 确保所有功能和 API 契约保持不变 +- 编写和运行测试验证迁移正确性 +- 保持代码质量和可维护性 + +--- + +## 项目背景 + +### 项目简介 + +**项目名称:** Outlook 邮件管理工具 + +**技术栈:** +- 后端:Python 3.8+, Flask 3.0+, SQLite +- 前端:原生 JavaScript, HTML, CSS(零构建) +- 测试:unittest, unittest.mock, coverage + +**当前问题:** +- `outlook_web/legacy.py` 文件达到 5757 行 +- 包含所有路由处理逻辑(54 个 API 路由) +- 职责混乱:路由处理 + 数据访问 + 业务逻辑混在一起 +- 难以维护、测试和扩展 + +**迁移目标:** +``` +当前架构: +routes/ (Blueprint) → legacy.py (5757行) → services/ → repositories/ + +目标架构: +routes/ (Blueprint) → controllers/ (新增) → services/ → repositories/ +``` + +### 已完成的工作 + +✅ **文档准备完成:** +1. PRD(产品需求文档)- `docs/PRD/PRD-00003-Legacy代码拆分到Controllers层.md` +2. FD(功能设计文档)- `docs/FD/Outlook邮件管理工具-Legacy迁移到Controllers层FD.md` +3. TDD(技术设计文档)- `docs/TDD/Outlook邮件管理工具-Legacy迁移到Controllers层TDD.md` +4. TODO(待办清单)- `docs/TODO/Outlook邮件管理工具-Legacy迁移到Controllers层TODO.md` +5. TEST(测试文档)- `docs/TEST/Outlook邮件管理工具-Legacy迁移到Controllers层-测试文档.md` + +✅ **架构准备完成:** +- routes/ 目录已存在(11 个 Blueprint 模块) +- services/ 目录已存在(7 个业务逻辑模块) +- repositories/ 目录已存在(8 个数据访问模块) +- 所有 Blueprint 当前使用 `impl=legacy` 模式 + +--- + +## 任务目标 + +### 总体目标 + +将 legacy.py 中的 54 个路由处理函数迁移到新的 controllers/ 层,分 4 个阶段完成: + +**阶段 1:基础模块(19 个路由,预计 2-3 天)** +- groups (6), tags (4), settings (3), system (3), audit (1), pages (3) + +**阶段 2:独立功能模块(6 个路由,预计 1-2 天)** +- temp_emails (3), oauth (2), scheduler (1) + +**阶段 3:核心复杂模块(24 个路由,预计 3-4 天)** +- emails (4), accounts (20) + +**阶段 4:清理和优化(预计 1 天)** +- 删除 legacy.py,迁移工具函数和中间件 + +### 核心要求 + +**必须保持不变:** +- ✅ 所有 API 的 URL 路径 +- ✅ 所有 API 的 HTTP 方法 +- ✅ 所有 API 的请求参数 +- ✅ 所有 API 的响应格式 +- ✅ 所有 API 的错误格式 +- ✅ trace_id 机制 +- ✅ 数据脱敏机制 +- ✅ 鉴权机制(@login_required) + +**必须提升:** +- ✅ 代码结构清晰 +- ✅ 职责分离明确 +- ✅ 易于测试和维护 + +--- + +## 工作流程 + +### 单个模块迁移流程(以 groups 为例) + +#### 步骤 1:创建 Controller 文件 + +```bash +# 创建 controllers 目录(如果不存在) +mkdir -p outlook_web/controllers + +# 创建 groups controller +touch outlook_web/controllers/groups.py +``` + +#### 步骤 2:从 legacy.py 提取函数 + +打开 `outlook_web/legacy.py`,找到以下函数: +- `api_get_groups()` +- `api_get_group(group_id)` +- `api_add_group()` +- `api_update_group(group_id)` +- `api_delete_group(group_id)` +- `api_export_group(group_id)` + +复制这些函数到 `outlook_web/controllers/groups.py` + +#### 步骤 3:调整导入语句 + +```python +# outlook_web/controllers/groups.py +from flask import request, jsonify +from outlook_web.security.auth import login_required +from outlook_web.repositories import groups as groups_repo +from outlook_web.errors import build_error_payload + +@login_required +def api_get_groups(): + """获取所有分组""" + try: + groups = groups_repo.get_all_groups() + return jsonify(groups) + except Exception as e: + return jsonify(build_error_payload(str(e))), 500 + +# ... 其他函数 +``` + +**关键点:** +- 保持函数签名完全一致 +- 保持装饰器(@login_required) +- 保持错误处理逻辑 +- 保持响应格式(jsonify) + +#### 步骤 4:更新 routes/groups.py + +```python +# outlook_web/routes/groups.py +from flask import Blueprint +from outlook_web.controllers import groups as groups_controller + +def create_blueprint() -> Blueprint: + """创建 groups Blueprint""" + bp = Blueprint("groups", __name__) + + bp.add_url_rule("/api/groups", + view_func=groups_controller.api_get_groups, + methods=["GET"]) + + bp.add_url_rule("/api/groups/", + view_func=groups_controller.api_get_group, + methods=["GET"]) + + # ... 其他路由 + + return bp +``` + +**关键点:** +- 移除 `impl` 参数 +- 直接导入 controller 模块 +- 保持 URL 路径和 HTTP 方法不变 + +#### 步骤 5:更新 app.py + +```python +# outlook_web/app.py +# 更新前 +app.register_blueprint(groups.create_blueprint(impl=legacy)) + +# 更新后 +app.register_blueprint(groups.create_blueprint()) +``` + +#### 步骤 6:运行测试 + +```bash +# 运行所有测试 +python -m unittest discover -s tests -v + +# 运行特定测试 +python -m unittest tests.test_smoke_contract -v +``` + +**验证:** +- 所有测试通过 +- 手动测试所有 6 个 API +- 验证响应格式正确 +- 验证错误处理正确 + +#### 步骤 7:提交 Git + +```bash +git add outlook_web/controllers/groups.py outlook_web/routes/groups.py outlook_web/app.py +git commit -m "feat: 迁移 groups 模块到 controllers 层" +``` + +--- + +## 技术规范 + +### Controller 标准模式 + +```python +# outlook_web/controllers/xxx.py +from flask import request, jsonify +from outlook_web.security.auth import login_required +from outlook_web.repositories import xxx as xxx_repo +from outlook_web.errors import build_error_payload + +@login_required +def api_xxx(): + """函数说明""" + try: + # 1. 参数解析 + param = request.args.get('param') + + # 2. 参数验证 + if not param: + return jsonify(build_error_payload('参数错误')), 400 + + # 3. 调用 service/repository + result = xxx_repo.do_something(param) + + # 4. 返回响应 + return jsonify(result) + except Exception as e: + # 5. 错误处理 + return jsonify(build_error_payload(str(e))), 500 +``` + +### Controller 职责边界 + +**必须做:** +- ✅ 参数解析(从 request 中提取) +- ✅ 参数验证(基本验证) +- ✅ 鉴权检查(@login_required) +- ✅ 调用 services/repositories +- ✅ 响应封装(jsonify) +- ✅ 错误处理(try-except) + +**不应该做:** +- ❌ 直接操作数据库 +- ❌ 复杂的业务逻辑 +- ❌ 直接调用第三方 API +- ❌ 数据加密/解密 + +### 依赖方向规则 + +``` +routes → controllers → services → repositories → db +``` + +**禁止反向依赖:** +- ❌ repositories → services +- ❌ services → controllers +- ❌ controllers → routes + +--- + +## 测试策略 + +### 测试类型 + +**1. 回归测试(最重要)** +```bash +# 每次迁移后都要运行 +python -m unittest discover -s tests -v +``` + +**2. 手动测试** +- 使用 curl 或 Postman 测试每个 API +- 验证响应格式正确 +- 验证错误处理正确 + +**3. 性能测试(可选)** +```python +import time + +start = time.time() +response = client.get('/api/groups') +end = time.time() + +print(f"响应时间: {(end - start) * 1000:.2f}ms") +``` + +### 测试检查清单 + +每个模块迁移后: +- [ ] 所有现有测试通过 +- [ ] 手动测试所有 API +- [ ] 验证响应格式不变 +- [ ] 验证错误格式不变 +- [ ] 验证性能无明显下降 +- [ ] 提交 Git + +--- + +## 参考文档 + +### 必读文档 + +1. **TODO 文档**(最重要) + - 位置:`docs/TODO/Outlook邮件管理工具-Legacy迁移到Controllers层TODO.md` + - 内容:详细的任务列表和执行步骤 + +2. **TDD 文档** + - 位置:`docs/TDD/Outlook邮件管理工具-Legacy迁移到Controllers层TDD.md` + - 内容:技术设计细节和代码示例 + +3. **TEST 文档** + - 位置:`docs/TEST/Outlook邮件管理工具-Legacy迁移到Controllers层-测试文档.md` + - 内容:测试策略和测试用例 + +### 可选文档 + +4. **PRD 文档** + - 位置:`docs/PRD/PRD-00003-Legacy代码拆分到Controllers层.md` + - 内容:需求背景和目标架构 + +5. **FD 文档** + - 位置:`docs/FD/Outlook邮件管理工具-Legacy迁移到Controllers层FD.md` + - 内容:功能清单和验收标准 + +6. **开发者指南** + - 位置:`docs/DEV/00002-前后端拆分-开发者指南.md` + - 内容:项目架构和开发规范 + +7. **CLAUDE.md** + - 位置:`CLAUDE.md` + - 内容:项目概述和常用命令 + +--- + +## 验收标准 + +### 阶段验收 + +**每个阶段完成后:** +- [ ] 所有测试通过 +- [ ] 所有 API 手动测试通过 +- [ ] 代码审查通过 +- [ ] Git 提交完成 + +### 最终验收 + +**所有阶段完成后:** +- [ ] 所有 54 个路由已迁移 +- [ ] legacy.py 已删除 +- [ ] 所有测试通过 +- [ ] 性能测试通过(响应时间 < 迁移前 110%) +- [ ] 文档已更新 + +--- + +## 注意事项 + +### 关键原则 + +1. **保持兼容性** + - 不改变任何 API 的行为 + - 不改变任何响应格式 + - 不改变任何错误格式 + +2. **分阶段迁移** + - 先迁移简单模块,后迁移复杂模块 + - 每个模块迁移后立即测试 + - 每个阶段完成后提交 Git + +3. **充分测试** + - 每次迁移后运行所有测试 + - 手动测试所有 API + - 验证性能无下降 + +4. **保留 legacy.py** + - 直到所有模块迁移完成 + - 作为参考和回滚依据 + +5. **及时提交** + - 每个模块迁移后提交 Git + - 便于回滚和代码审查 + +### 常见问题 + +**Q1:如果测试失败怎么办?** +A1: +1. 检查函数签名是否一致 +2. 检查导入语句是否正确 +3. 检查装饰器是否保留 +4. 检查错误处理是否一致 +5. 如果无法解决,回滚到上一个提交 + +**Q2:如何处理循环依赖?** +A2: +1. 确保依赖方向正确(routes → controllers → services → repositories) +2. 不要在 repositories 中导入 services +3. 不要在 services 中导入 controllers + +**Q3:如何验证 API 契约不变?** +A3: +1. 运行现有的契约测试 +2. 手动测试 API,对比响应格式 +3. 使用 curl 或 Postman 测试 + +**Q4:性能下降怎么办?** +A4: +1. 检查是否有不必要的数据库查询 +2. 检查是否有重复的函数调用 +3. 使用 profiler 工具分析瓶颈 + +--- + +## 快速开始 + +### 第一步:环境准备 + +```bash +# 1. 确认环境 +python --version # 应该是 3.8+ +pip list | grep Flask # 应该是 3.0+ + +# 2. 安装依赖 +pip install -r requirements.txt + +# 3. 运行测试(建立基线) +python -m unittest discover -s tests -v + +# 4. 创建分支 +git checkout -b feature/migrate-to-controllers +``` + +### 第二步:开始迁移 + +```bash +# 1. 阅读 TODO 文档 +cat docs/TODO/Outlook邮件管理工具-Legacy迁移到Controllers层TODO.md + +# 2. 创建 controllers 目录 +mkdir -p outlook_web/controllers +touch outlook_web/controllers/__init__.py + +# 3. 开始迁移第一个模块(groups) +# 按照上面的"单个模块迁移流程"执行 +``` + +### 第三步:验证和提交 + +```bash +# 1. 运行测试 +python -m unittest discover -s tests -v + +# 2. 手动测试 +curl -X GET http://localhost:5000/api/groups + +# 3. 提交 +git add . +git commit -m "feat: 迁移 groups 模块到 controllers 层" +``` + +--- + +## 工作建议 + +### 推荐工作顺序 + +1. **先读文档**(30 分钟) + - 阅读 TODO 文档,了解任务列表 + - 阅读 TDD 文档,了解技术细节 + - 阅读 TEST 文档,了解测试策略 + +2. **建立基线**(15 分钟) + - 运行所有测试,记录结果 + - 手动测试几个关键 API + - 记录性能基线 + +3. **迁移第一个模块**(1-2 小时) + - 选择最简单的模块(groups 或 tags) + - 严格按照流程执行 + - 充分测试验证 + +4. **迁移其他模块**(重复) + - 按照 TODO 文档的顺序 + - 每个模块迁移后立即测试 + - 每个阶段完成后提交 + +5. **最终清理**(1 天) + - 删除 legacy.py + - 迁移工具函数 + - 更新文档 + +### 时间估算 + +- 阶段 1(基础模块):2-3 天 +- 阶段 2(独立模块):1-2 天 +- 阶段 3(复杂模块):3-4 天 +- 阶段 4(清理优化):1 天 +- **总计:7-10 天** + +--- + +## 联系和支持 + +如果遇到问题: +1. 查看文档中的"常见问题"部分 +2. 检查 Git 提交历史,查看之前的实现 +3. 运行测试,查看错误信息 +4. 如果无法解决,回滚到上一个提交 + +--- + +**祝你工作顺利!记住:保持兼容性、充分测试、及时提交。** diff --git a/PRD/PRD-可调整布局系统.md b/PRD/PRD-可调整布局系统.md new file mode 100644 index 0000000..26139b6 --- /dev/null +++ b/PRD/PRD-可调整布局系统.md @@ -0,0 +1,1000 @@ +# PRD - 可调整布局系统 + +## 文档信息 + +- **文档编号**: PRD-00001 +- **创建日期**: 2026-02-26 +- **版本**: v1.1 +- **状态**: 已审查 +- **负责人**: 开发团队 +- **审查日期**: 2026-02-26 +- **审查结果**: 通过(已根据审查意见修订) + +## 一、需求背景 + +### 1.1 当前问题 + +Outlook 邮件管理工具当前采用固定宽度的四栏布局: +- 分组面板(200px) +- 账号面板(260px) +- 邮件列表面板(380px) +- 邮件详情面板(自适应) + +存在以下问题: +1. **缺乏灵活性**:用户无法根据使用习惯调整面板宽度 +2. **空间利用率低**:不同屏幕尺寸下,固定宽度导致空间浪费或拥挤 +3. **无法聚焦**:无法临时折叠不需要的面板,专注于当前任务 +4. **用户体验落后**:现代编辑器(VS Code、EnsoAI)普遍支持可调整布局 + +### 1.2 需求来源 + +参考 EnsoAI 编辑器的 UI 设计理念,提升用户体验,使布局更加灵活和现代化。 + +### 1.3 目标用户 + +- **主要用户**:桌面浏览器用户(Chrome、Edge、Firefox、Safari) +- **使用场景**: + - 需要同时查看多个邮箱账号的用户 + - 需要在不同工作模式间切换的用户(浏览模式 vs 专注阅读模式) + - 使用不同屏幕尺寸的用户(笔记本 vs 外接显示器) + +## 二、产品目标 + +### 2.1 核心目标 + +1. **提升灵活性**:用户可以自由调整每个面板的宽度 +2. **支持折叠**:用户可以临时折叠不需要的面板 +3. **状态持久化**:记住用户的布局偏好,刷新后自动恢复 +4. **平滑体验**:所有交互都有流畅的动画过渡 + +### 2.2 成功指标 + +- 用户可以在 3 秒内完成面板宽度调整 +- 用户可以在 1 秒内完成面板折叠/展开 +- 布局状态在页面刷新后 100% 恢复 +- 拖动调整时帧率保持在 60fps + +## 三、功能需求 + +### 3.1 可拖动调整宽度 + +#### 3.1.1 功能描述 + +用户可以通过拖动面板边缘的分隔条来调整面板宽度。 + +#### 3.1.2 交互细节 + +**拖动区域**: +- 每个面板右侧边缘有 4px 宽的拖动区域(resizer) +- 鼠标悬停时显示视觉反馈: + - 光标变为 `col-resize`(双向箭头) + - 显示 2px 的高亮边框指示器 + - 背景色变为半透明黑色(rgba(26, 26, 26, 0.1)) + +**拖动行为**: +- 鼠标按下(mousedown)时开始拖动 +- 鼠标移动(mousemove)时实时更新面板宽度 +- 鼠标释放(mouseup)时结束拖动,保存宽度到 localStorage + +**宽度限制**: +- **最小宽度限制**: + - 分组面板:150px(确保分组名称和图标可见) + - 账号面板:180px(确保邮箱地址可读) + - 邮件列表面板:280px(确保邮件预览可读) + - 邮件详情面板:400px(确保邮件内容可读) +- **最大宽度限制**: + - 分组面板:400px + - 账号面板:500px + - 邮件列表面板:600px + - 邮件详情面板:无限制(自动填充) +- **窗口尺寸自适应**: + - 当浏览器窗口宽度 < 1200px 时,自动折叠分组面板 + - 当浏览器窗口宽度 < 900px 时,自动折叠账号面板 + - 当浏览器窗口宽度 < 700px 时,自动折叠邮件列表面板 + - 确保邮件详情面板始终保持至少 400px 可见宽度 + +**视觉反馈**: +- 拖动时整个页面光标变为 `col-resize` +- 拖动时添加 `user-select: none` 防止选中文本 +- 使用 CSS transitions 实现平滑过渡(200ms ease) + +#### 3.1.3 技术实现 + +- 使用 CSS Grid 布局 +- 使用 CSS 变量存储面板宽度: + - `--group-panel-width` + - `--account-panel-width` + - `--email-list-panel-width` +- 使用原生 JavaScript 实现拖动逻辑 +- 使用 requestAnimationFrame 优化性能 + +### 3.2 面板折叠/展开 + +#### 3.2.1 功能描述 + +用户可以点击折叠按钮来隐藏/显示面板,专注于当前任务。 + +#### 3.2.2 交互细节 + +**折叠按钮位置**: +- 分组面板:右上角 +- 账号面板:右上角 +- 邮件列表面板:右上角 + +**按钮样式**: +- 图标:左箭头(←)表示折叠,右箭头(→)表示展开 +- 尺寸:24x24px +- 悬停效果:背景色变为 #f5f5f5 + +**折叠动画**: +- 使用 CSS transition 实现平滑动画(200ms ease) +- 折叠时宽度变为 0,同时 opacity 从 1 到 0 +- 展开时恢复到折叠前的宽度,opacity 从 0 到 1 + +**折叠状态指示**: +- 折叠后在主容器边缘显示一个细条(48px 宽) +- 细条上显示面板名称(竖排文字) +- 点击细条可以快速展开面板 + +#### 3.2.3 折叠规则 + +- 分组面板、账号面板、邮件列表面板可以独立折叠 +- 邮件详情面板不可折叠(始终可见) +- 至少保持一个侧边栏面板展开(不能全部折叠) +- **折叠时的拖动行为**: + - 当面板处于折叠状态时,该面板的 resizer(拖动区域)自动隐藏 + - 用户必须先点击折叠指示条展开面板后,才能拖动调整宽度 + - 展开时恢复到折叠前的宽度(从 localStorage 读取) + +### 3.3 恢复默认布局 + +#### 3.3.1 功能描述 + +用户可以一键恢复到默认布局,清除所有自定义调整。 + +#### 3.3.2 交互细节 + +**触发位置**: +- 在顶部导航栏的右侧添加"恢复默认布局"按钮 +- 图标:重置图标(↻) +- 文字:仅在悬停时显示 tooltip + +**确认机制**: +- 点击后弹出确认对话框: + - 标题:"恢复默认布局" + - 内容:"确定要恢复到默认布局吗?当前的面板宽度和折叠状态将被重置。" + - 按钮:"取消" / "确定" + +**默认布局参数**: +- 分组面板:200px,展开 +- 账号面板:260px,展开 +- 邮件列表面板:380px,展开 +- 邮件详情面板:自适应 + +**重置动画**: +- 使用 CSS transition 平滑过渡到默认宽度(300ms ease) + +### 3.4 状态持久化 + +#### 3.4.1 功能描述 + +自动保存用户的布局偏好,页面刷新后恢复。 + +#### 3.4.2 保存内容 + +**面板宽度**: +- 分组面板宽度 +- 账号面板宽度 +- 邮件列表面板宽度 + +**折叠状态**: +- 分组面板是否折叠 +- 账号面板是否折叠 +- 邮件列表面板是否折叠 + +#### 3.4.3 存储方式 + +- 使用 localStorage 存储 +- **存储键名**:`outlook_layout_state_${userId}`(支持多用户隔离) + - 如果用户未登录,使用 `outlook_layout_state_guest` + - 如果系统有用户 ID,使用 `outlook_layout_state_${userId}` +- 数据格式: +```json +{ + "version": "1.1", + "userId": "user123", + "timestamp": 1709020800000, + "panels": { + "groups": { + "width": "200px", + "collapsed": false + }, + "accounts": { + "width": "260px", + "collapsed": false + }, + "emails": { + "width": "380px", + "collapsed": false + } + } +} +``` + +#### 3.4.4 保存时机 + +- 拖动调整宽度后,延迟 500ms 保存(防抖) +- 折叠/展开面板后,立即保存 +- 恢复默认布局后,清除保存的状态 +- **页面卸载时强制保存**: + - 监听 `beforeunload` 事件 + - 如果有未保存的防抖任务,立即执行保存 + - 确保用户的最后一次调整不会丢失 + +#### 3.4.5 加载时机 + +- 页面加载完成后,从 localStorage 读取状态 +- 如果存在保存的状态,应用到 CSS 变量 +- 如果不存在,使用默认布局 + +## 四、非功能需求 + +### 4.1 性能要求 + +- **拖动流畅度**:拖动时帧率保持在 60fps +- **动画性能**:使用 CSS transform 和 opacity,避免触发 reflow +- **内存占用**:localStorage 数据不超过 1KB + +### 4.2 兼容性要求 + +- **浏览器支持**: + - Chrome 90+ + - Edge 90+ + - Firefox 88+ + - Safari 14+ +- **CSS 特性**: + - CSS Grid + - CSS 变量 + - CSS transitions +- **JavaScript 特性**: + - ES6+ + - localStorage API + - requestAnimationFrame + +### 4.3 可访问性要求 + +- **折叠按钮**: + - 支持键盘操作(Tab 聚焦 + Enter 触发) + - 有合适的 aria-label(如"折叠分组面板") +- **拖动区域(Resizer)**: + - 支持键盘操作: + - Tab 键聚焦到 resizer + - 左箭头键(←):减小面板宽度(每次 10px) + - 右箭头键(→):增大面板宽度(每次 10px) + - Shift + 左/右箭头:快速调整(每次 50px) + - 添加 ARIA 属性: + - `role="separator"` + - `aria-orientation="vertical"` + - `aria-label="调整[面板名称]宽度"` + - `aria-valuenow`:当前宽度值 + - `aria-valuemin`:最小宽度 + - `aria-valuemax`:最大宽度 + - 聚焦时有明显的视觉反馈(outline 或高亮边框) +- **所有交互元素**: + - 有明确的视觉反馈 + - 支持屏幕阅读器 + +### 4.4 安全性要求 + +- localStorage 数据仅存储布局状态,不包含敏感信息 +- 防止 XSS 攻击:不直接将用户输入插入 DOM + +## 五、UI 设计规范 + +### 5.1 布局结构 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 顶部导航栏(固定高度 56px) │ +├──────┬──────┬──────┬────────────────────────────────────┤ +│ 分组 │ 账号 │ 邮件 │ 邮件详情 │ +│ 面板 │ 面板 │ 列表 │ 面板 │ +│ │ │ 面板 │ (自适应) │ +│ [▼] │ [▼] │ [▼] │ │ +│ ├─┤ │ ├─┤ │ ├─┤ │ │ +└──────┴──────┴──────┴────────────────────────────────────┘ + 200px 260px 380px flex: 1 + +[▼] = 折叠按钮 +├─┤ = 拖动分隔条 +``` + +### 5.2 颜色规范 + +- **拖动区域背景(hover)**: rgba(26, 26, 26, 0.1) +- **拖动指示器**: #1a1a1a +- **折叠按钮背景(hover)**: #f5f5f5 +- **折叠按钮图标**: #666 + +### 5.3 尺寸规范 + +- **拖动区域宽度**: 4px +- **拖动指示器宽度**: 2px +- **折叠按钮尺寸**: 24x24px +- **折叠后细条宽度**: 48px + +### 5.4 动画规范 + +- **拖动过渡**: 无(实时响应) +- **折叠/展开**: 200ms ease +- **恢复默认布局**: 300ms ease + +## 六、技术实现方案 + +### 6.1 技术栈 + +- **前端框架**: 无(原生 JavaScript) +- **CSS 预处理器**: 无(原生 CSS) +- **布局系统**: CSS Gr- **状态管理**: localStorage + +### 6.2 核心模块 + +#### 6.2.1 布局管理器(LayoutManager) + +**职责**: +- 管理面板宽度和折叠状态 +- 处理拖动事件 +- 处理折叠/展开事件 +- 保存/加载布局状态 + +**主要方法**: +```javascript +class LayoutManager { + constructor() + init() + startResize(e) + resize(e) + stopResize() + togglePanel(panelName) + resetLayout() + saveState() + loadState() + updatePanelWidth(panel, width) +} +``` + +#### 6.2.2 状态持久化(StateManager) + +**职责**: +- 读写 localStorage +- 数据格式验证 +- 版本兼容性处理 + +**主要方法**: +```javascript +class StateManager { + save(state) + load() + clear() + validate(state) +} +``` + +### 6.3 文件结构 + +``` +static/ +├── css/ +│ ├── main.css # 现有样式(需修改) +│ └── layout.css # 新增:布局系统样式 +├── js/ +│ ├── main.js # 现有脚本(需修改) +│ ├── layout-manager.js # 新增:布局管理器 +│ └── state-manager.js # 新增:状态管理器 +templates/ +└── index.html # 主页面(需修改) +``` + +### 6.4 关键代码片段 + +#### CSS 变量定义 + +```css +:root { + /* 面板初始宽度 */ + --group-panel-width: 200px; + --account-panel-width: 260px; + --email-list-panel-width: 380px; + + /* 最小/最大宽度限制 */ + --group-panel-min-width: 150px; + --group-panel-max-width: 400px; + --account-panel-min-width: 180px; + --account-panel-max-width: 500px; + --email-list-panel-min-width: 280px; + --email-list-panel-max-width: 600px; + --email-detail-panel-min-width: 400px; + + /* 折叠状态宽度 */ + --panel-collapsed-width: 0px; + --panel-collapsed-indicator-width: 48px; + + /* 拖动区域 */ + --resizer-width: 4px; + + /* 动画时长 */ + --panel-transition-duration: 200ms; +} +``` + +#### Grid 布局 + +```css +.main-container { + display: grid; + grid-template-columns: + minmax(var(--group-panel-min-width), var(--group-panel-width)) + minmax(var(--account-panel-min-width), var(--account-panel-width)) + minmax(var(--email-list-panel-min-width), var(--email-list-panel-width)) + minmax(var(--email-detail-panel-min-width), 1fr); + height: calc(100vh - 56px); + transition: grid-template-columns var(--panel-transition-duration) ease; +} + +/* 窗口尺寸自适应 */ +@media (max-width: 1200px) { + .main-container { + grid-template-columns: + 0px + minmax(var(--account-panel-min-width), var(--account-panel-width)) + minmax(var(--email-list-panel-min-width), var(--email-list-panel-width)) + minmax(var(--email-detail-panel-min-width), 1fr); + } + .group-panel { + display: none; + } +} + +@media (max-width: 900px) { + .main-container { + grid-template-columns: + 0px + 0px + minmax(var(--email-list-panel-min-width), var(--email-list-panel-width)) + minmax(var(--email-detail-panel-min-width), 1fr); + } + .group-panel, + .account-panel { + display: none; + } +} + +@media (max-width: 700px) { + .main-container { + grid-template-columns: 1fr; + } + .group-panel, + .account-panel, + .email-list-panel { + display: none; + } +} +``` + +#### 拖动区域样式 + +```css +.resizer { + position: absolute; + top: 0; + right: 0; + width: var(--resizer-width); + height: 100%; + cursor: col-resize; + z-index: 10; + transition: background-color 0.2s; + /* 可访问性:支持键盘聚焦 */ + outline: none; +} + +.resizer:hover, +.resizer:focus { + background-color: rgba(26, 26, 26, 0.1); +} + +.resizer:focus { + outline: 2px solid #1a1a1a; + outline-offset: -2px; +} + +.resizer::after { + content: ''; + position: absolute; + left: 50%; + top: 0; + bottom: 0; + width: 2px; + background-color: transparent; + transform: translateX(-50%); + transition: background-color 0.2s; +} + +.resizer:hover::after, +.resizer:focus::after { + background-color: #1a1a1a; +} + +/* 折叠状态下隐藏 resizer */ +.resizable-panel.collapsed .resizer { + display: none; +} +``` + +#### Resizer HTML 结构(支持可访问性) + +```html + +``` + +#### JavaScript 核心逻辑(含所有改进) + +```javascript +class LayoutManager { + constructor() { + this.isResizing = false; + this.currentPanel = null; + this.startX = 0; + this.startWidth = 0; + this.pendingSave = null; + this.init(); + } + + init() { + // 鼠标拖动 + document.querySelectorAll('.resizer').forEach(resizer => { + resizer.addEventListener('mousedown', this.startResize.bind(this)); + // 键盘支持 + resizer.addEventListener('keydown', this.handleKeyboard.bind(this)); + }); + + document.addEventListener('mousemove', this.resize.bind(this)); + document.addEventListener('mouseup', this.stopResize.bind(this)); + + // 窗口尺寸变化监听 + window.addEventListener('resize', this.handleWindowResize.bind(this)); + + // 页面卸载时强制保存 + window.addEventListener('beforeunload', this.saveStateNow.bind(this)); + } + + startResize(e) { + this.isResizing = true; + const resizerType = e.target.dataset.resizer; + this.currentPanel = document.querySelector(`[data-panel="${resizerType}"]`); + this.startX = e.clientX; + this.startWidth = this.currentPanel.offsetWidth; + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; + } + + resize(e) { + if (!this.isResizing) return; + + const delta = e.clientX - this.startX; + let newWidth = this.startWidth + delta; + + // 应用最小/最大宽度限制 + const panelType = this.currentPanel.dataset.panel; + const minWidth = this.getMinWidth(panelType); + const maxWidth = this.getMaxWidth(panelType); + newWidth = Math.max(minWidth, Math.min(maxWidth, newWidth)); + + // 确保邮件详情面板至少 400px + const containerWidth = document.querySelector('.main-container').offsetWidth; + const otherPanelsWidth = this.calculateOtherPanelsWidth(panelType); + const maxAllowedWidth = containerWidth - otherPanelsWidth - 400; + newWidth = Math.min(newWidth, maxAllowedWidth); + + this.updatePanelWidth(this.currentPanel, newWidth); + } + + stopResize() { + if (this.isResizing) { + this.isResizing = false; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + this.saveStateDebounced(); + } + } + + handleKeyboard(e) { + const resizer = e.target; + const panelType = resizer.dataset.resizer; + const panel = document.querySelector(`[data-panel="${panelType}"]`); + const currentWidth = panel.offsetWidth; + + let delta = 0; + if (e.key === 'ArrowLeft') { + delta = e.shiftKey ? -50 : -10; + } else if (e.key === 'ArrowRight') { + delta = e.shiftKey ? 50 : 10; + } else { + return; + } + + e.preventDefault(); + + let newWidth = currentWidth + delta; + const minWidth = this.getMinWidth(panelType); + const maxWidth = this.getMaxWidth(panelType); + newWidth = Math.max(minWidth, Math.min(maxWidth, newWidth)); + + this.updatePanelWidth(panel, newWidth); + this.updateAriaValue(resizer, newWidth); + this.saveStateDebounced(); + } + + handleWindowResize() { + const windowWidth = window.innerWidth; + + // 自动折叠逻辑 + if (windowWidth < 1200) { + this.collapsePanel('groups', true); + } + if (windowWidth < 900) { + this.collapsePanel('accounts', true); + } + if (windowWidth < 700) { + this.collapsePanel('emails', true); + } + } + + updatePanelWidth(panel, width) { + const panelType = panel.dataset.panel; + const varName = `--${panelType}-panel-width`; + document.documentElement.style.setProperty(varName, `${width}px`); + } + + updateAriaValue(resizer, value) { + resizer.setAttribute('aria-valuenow', Math.round(value)); + } + + getMinWidth(panelType) { + const minWidths = { + 'groups': 150, + 'accounts': 180, + 'emails': 280 + }; + return minWidths[panelType] || 150; + } + + getMaxWidth(panelType) { + const maxWidths = { + 'groups': 400, + 'accounts': 500, + 'emails': 600 + }; + return maxWidths[panelType] || 600; + } + + calculateOtherPanelsWidth(excludePanel) { + const panels = ['groups', 'accounts', 'emails']; + let total = 0; + panels.forEach(panel => { + if (panel !== excludePanel) { + const el = document.querySelector(`[data-panel="${panel}"]`); + if (el && !el.classList.contains('collapsed')) { + total += el.offsetWidth; + } + } + }); + return total; + } + + saveStateDebounced() { + if (this.pendingSave) { + clearTimeout(this.pendingSave); + } + this.pendingSave = setTimeout(() => { + this.saveStateNow(); + }, 500); + } + + saveStateNow() { + if (this.pendingSave) { + clearTimeout(this.pendingSave); + this.pendingSave = null; + } + + const userId = this.getCurrentUserId() || 'guest'; + const state = { + version: '1.1', + userId: userId, + timestamp: Date.now(), + panels: { + groups: { + width: getComputedStyle(document.documentElement) + .getPropertyValue('--groups-panel-width'), + collapsed: document.querySelector('[data-panel="groups"]') + .classList.contains('collapsed') + }, + accounts: { + width: getComputedStyle(document.documentElement) + .getPropertyValue('--accounts-panel-width'), + collapsed: document.querySelector('[data-panel="accounts"]') + .classList.contains('collapsed') + }, + emails: { + width: getComputedStyle(document.documentElement) + .getPropertyValue('--emails-panel-width'), + collapsed: document.querySelector('[data-panel="emails"]') + .classList.contains('collapsed') + } + } + }; + + localStorage.setItem(`outlook_layout_state_${userId}`, JSON.stringify(state)); + } + + getCurrentUserId() { + // 从页面或全局变量获取用户 ID + return window.currentUserId || null; + } + + collapsePanel(panelType, auto = false) { + const panel = document.querySelector(`[data-panel="${panelType}"]`); + if (!panel) return; + + panel.classList.add('collapsed'); + if (!auto) { + this.saveStateNow(); + } + } +} +``` + +## 七、实施计划 + +### 7.1 开发阶段 + +#### 阶段一:基础布局改造(2-3 天) + +**任务**: +- [ ] 修改 HTML 结构,添加 resizer 元素 +- [ ] 修改 CSS,实现 Grid 布局 +- [ ] 添加 CSS 变量系统 +- [ ] 确保现有功能不受影响 + +**验收标准**: +- 页面布局正常显示 +- 现有功能(邮件列表、详情查看)正常工作 + +#### 阶段二:拖动调整功能(2-3 天) + +**任务**: +- [ ] 实现 LayoutManager 类 +- [ ] 实现拖动事件处理 +- [ ] 添加视觉反馈(hover 效果) +- [ ] 优化拖动性能(requestAnimationFrame) + +**验收标准**: +- 可以拖动调整每个面板宽度 +- 拖动流畅,帧率 60fps +- 有明确的视觉反馈 + +#### 阶段三:折叠功能(1-2 天) + +**任务**: +- [ ] 添加折叠按钮 +- [ ] 实现折叠/展开逻辑 +- [ ] 添加折叠动画 +- [ ] 实现折叠后的细条指示器 + +**验收标准**: +- 可以折叠/展开每个面板 +- 动画平滑自然 +- 折叠后有明确的视觉指示 + +#### 阶段四:状态持久化(1 天) + +**任务**: +- [ ] 实现 StateManager 类 +- [ ] 实现保存/加载逻辑 +- [ ] 添加防抖优化 +- [ ] 处理异常情况(localStorage 不可用) + +**验收标准**: +- 刷新页面后布局状态恢复 +- localStorage 数据格式正确 +- 异常情况有降级处理 + +#### 阶段五:恢复默认布局(1 天) + +**任务**: +- [ ] 添加重置按钮 +- [ ] 实现确认对话框 +- [ ] 实现重置逻辑 +- [ ] 添加重置动画 + +**验收标准**: +- 可以一键恢复默认布局 +- 有确认机制防止误操作 +- 重置动画平滑 + +#### 阶段六:测试与优化(2-3 天) + +**任务**: +- [ ] 浏览器兼容性测试 +- [ ] 性能测试(帧率、内存) +- [ ] 边界情况测试 +- [ ] 用户体验优化 + +**验收标准**: +- 所有主流浏览器正常工作 +- 性能指标达标 +- 无明显 bug + +### 7.2 总体时间估算 + +- **开发时间**: 9-12 天 +- **测试时间**: 2-3 天 +- **总计**: 11-15 天 + +### 7.3 风险评估 + +| 风险 | 影响 | 概率 | 应对措施 | +|------|------|------|----------| +| CSS Grid 兼容性问题 | 高 | 低 | 提前测试目标浏览器,准备降级方案 | +| 拖动性能不佳 | 中 | 中 | 使用 requestAnimationFrame 优化 | +| 现有功能受影响 | 高 | 中 | 充分测试,保持向后兼容 | +| localStorage 不可用 | 低 | 低 | 降级到默认布局,不影响核心功能 | + +## 八、验收标准 + +### 8.1 功能验收 + +- [ ] 可以拖动调整分组面板宽度 +- [ ] 可以拖动调整账号面板宽度 +- [ ] 可以拖动调整邮件列表面板宽度 +- [ ] 可以折叠/展开分组面板 +- [ ] 可以折叠/展开账号面板 +- [ ] 可以折叠/展开邮件列表面板 +- [ ] 可以一键恢复默认布局 +- [ ] 刷新页面后布局状态恢复 +- [ ] 拖动时有视觉反馈 +- [ ] 折叠/展开有平滑动画 + +### 8.2 性能验收 + +- [ ] 拖动时帧率 ≥ 60fps +- [ ] 折叠/展开动画流畅 +- [ ] localStorage 数据 ≤ 1KB +- [ ] 页面加载时间增加 ≤ 50ms + +### 8.3 兼容性验收 + +- [ ] Chrome 90+ 正常工作 +- [ ] Edge 90+ 正常工作 +- [ ] Firefox 88+ 正常工作 +- [ ] Safari 14+ 正常工作 + +### 8.4 用户体验验收 + +- [ ] 拖动操作直观易懂 +- [ ] 折叠按钮位置合理 +- [ ] 视觉反馈清晰明确 +- [ ] 无明显的交互延迟 + +## 九、后续优化方向 + +### 9.1 短期优化(1-2 个月内) + +1. **键盘快捷键支持** + - `Ctrl/Cmd + B` 切换左侧面板 + - `Ctrl/Cmd + \` 切换邮件列表 + +2. **预设布局模板** + - 紧凑模式(适合小屏幕) + - 标准模式(默认) + - 宽松模式(适合大屏幕) + +3. **拖动排序** + - 支持拖动调整面板顺序 + +### 9.2 中期优化(3-6 个月内) + +1. **移动端适配** + - 抽屉式侧边栏 + - 触摸手势支持 + +2. **多套布局方案** + - 用户可以保存多套布局配置 + - 快速切换不同工作场景 + +3. **布局分享** + - 导出布局配置 + - 导入他人的布局配置 + +## 十、附录 + +### 10.1 参考资料 + +- EnsoAI 项目:`E:\hushaokang\Data-code\outlookEmail\exeample\EnsoAI` +- shadcn/ui Sidebar 组件文档 +- CSS Grid 布局指南 +- localStorage API 文档 + +### 10.2 术语表 + +| 术语 | 说明 | +|------|------| +| 面板 | 指分组面板、账号面板、邮件列表面板、邮件详情面板 | +| Resizer | 拖动分隔条,用于调整面板宽度 | +| 折叠 | 隐藏面板内容,宽度变为 0 或显示细条 | +| 展开 | 显示面板内容,恢复到折叠前的宽度 | +| 布局状态 | 包括面板宽度和折叠状态 | +| 默认布局 | 初始的面板宽度和折叠状态 | + +### 10.3 变更记录 + +| 版本 | 日期 | 变更内容 | 变更人 | +|------|------|----------|--------| +| v1.0 | 2026-02-26 | 初始版本 | 开发团队 | +| v1.1 | 2026-02-26 | 根据审查报告修订:
1. 添加最小/最大宽度限制
2. 添加窗口尺寸自适应策略
3. 支持多用户布局隔离
4. 添加页面卸载时强制保存
5. 明确折叠时的拖动行为
6. 增强键盘可访问性支持 | 开发团队 | + +### 10.4 审查报告摘要 + +**审查日期**: 2026-02-26 +**审查结果**: 通过(已修订) +**文档质量**: 优秀 (A) + +#### 已解决的问题 + +🔴 **严重问题**: +1. ✅ **缺乏面板拖动的临界值约束** - 已添加明确的最小/最大宽度限制 +2. ✅ **浏览器窗口尺寸变化的响应策略不明确** - 已添加媒体查询和自动折叠逻辑 + +🟡 **中等问题**: +3. ✅ **状态持久化缺乏用户隔离** - 存储键名改为 `outlook_layout_state_${userId}` +4. ✅ **状态保存可能丢失** - 添加 `beforeunload` 事件监听,强制保存 +5. ✅ **折叠状态与拖动的逻辑冲突** - 明确折叠时隐藏 resizer + +🟢 **低优先级问题**: +6. ✅ **无障碍键盘控制不足** - 添加完整的键盘支持(方向键调整宽度) + +#### 改进内容 + +1. **宽度限制系统**: + - 分组面板:150px - 400px + - 账号面板:180px - 500px + - 邮件列表:280px - 600px + - 邮件详情:最小 400px + +2. **响应式断点**: + - < 1200px:自动折叠分组面板 + - < 900px:自动折叠账号面板 + - < 700px:自动折叠邮件列表 + +3. **可访问性增强**: + - Resizer 支持 Tab 聚焦 + - 支持方向键调整宽度(← / →) + - 支持 Shift + 方向键快速调整 + - 完整的 ARIA 属性支持 + +4. **状态管理优化**: + - 多用户隔离存储 + - 页面卸载时强制保存 + - 防抖机制优化 + +--- + +**文档结束** diff --git a/__pycache__/email.cpython-313.pyc b/__pycache__/email.cpython-313.pyc deleted file mode 100644 index b517f9decc01360167845d20819d9416e27a81b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18940 zcmeHvdvsIBx$l;=wA6DDBM+xH3=tnpyLWMi01O)K{ z!4NFL2pHjZ0V}{)AuEKdXuGJDWJwy`BezqnVpiNLVI{3nR@$1tCg6JEb{Q*UC|1sh zSp_3u6B#M1WQ44WNnq8Cj7?(XY%-%@HB2I#!YElSquQq{7c%M$Vn4~IGD-V%OfuAv zj0V@FF)8~b`w}P^Afv_gI$WR5qyl9c{H5a-;(a2?h}90%xIPn^B}?yqj1dw1dZPZt0A zM>o&CuyFnt3s0SwU4Q?B8_)Z$PanJS`m>8a`Srqk$2lUJ*Yd{Mx48CydG94zNfEtx z^7R|XJakKYT^G%@p$i;bM<&~ddDP?>?l-wzwqd7URtn@lnR+<6-Fi4oURFdKSo7!* zUDwqT;XR0I!W6gAb9U$j!Nh*6#nEpy4Oz|oR#s02rJbWLo5OA%4w7v)rz=P{+AOZ1 zsLpy1-)E_| z7MGa|`-+P#h2@of{e@<0Wkq3MWp$ags;aok(pPNu!B@~;)KF1TW2&#)Q&e1JEGld` zVCb>!b@g+YzpX8VR(c2phesV;AJ_T-E@JL)RdjSQ_TDL+)-np5SjIat+s zaR1J2J#1O){+zvRaeY&ntFnKn(y(Kw{@{@+OI>AUM_o%pWlQ^DOH~8vOl zDKi@n4{k3zVrX!>%_YTkgH`U4vc3U#M{`+C)2L%l_1NBl`r)qK!#z!Pm4~+P?&umW zuWsw;uIo5*pv>8IsJ(pWj{OHLC3~7$cODt;Y$+}+JJ{?V9jU7--(KEhXfA7Zw%Bs& zTdgI<9S7RSj_mHSWDF$Eu3A+xAu-IWoMv=|J;=Hsh|Uk+K7>n!%ig;T~I6Q}N+qwm5IBV94bfb=Gd& zIP4g-*^7p4hpgz;Z*-X5uA$Omlgn|yYAu@>-VBHMc>|7&gIO4E} z+HO2lT2#Cdw;$X5McUe(?hz~NjMH2jUX+f30h`5IR#}0@IHnF)hevJuHxWSuDVNPP zY_-5Dj3gv1;1+SG1F{#0-pD;hP!THA{92)rP!f!&OceJn8Lb_p^kTOPm@XcBZsFOf z1-(dX}31Aus-%Jo_^-mPk+8|;WuY*zJCs6AV|V}))BL9xM=ifP=y(ooYqk@Yj!zU z7R}Cj#B2>@^A1oEK?NtjOoz zg#M&LrxzDqeS6`T zCl}BDbC9@Cxm^Q=)jSn-bqm8bg@C1&1thcU1qz*8l?J;x~$HinA2iG zc^jx{_vkj3b+Ex?quqvdTO-zRTwTJpBNi)GaC(YOLM%fHu-^ph&*cm%IZpj1=n1r3 zkVMr%iDk&lI;}1zdebyYESia+CPsU5tB*zNwiN!HzkzI=Sd^#EtCE(4iB!$iv~Rp> z@okvPubNA%_9V`yXL^)>Shen0yQlH0Dt$8VRqBc=e^!;hBorkz3FcGQynf*Hf#2+Y zYwgThKeKNx|FPMW$9~+gBqfrvU(NOQzoMU2um7>~wu)H2eo0NJSNo-_KE0bjq~`^` zQRPpqzC#G2Y^K&u*UaT_n@!pFT*s0K-T3UTj8JHSh^qNhUhy-|iA{4V`ghXmsXoYn zgsQpkgf&MzU0R<>yuYfhSomRv7K$Gh=hdfCAEk(({86d^mD6xJGpl}!=%X4T$~Oxj z4~p?;?hMLDtgazPzsYVMv06gk1#l(^Dg5EDauI4{zDhN$CipVB#0ZQ65U@hNfE^U^ z?vwaF6>t4PJ8>Bm{(eDhN557O*^GpYtW*tQ5HxQHebu=Ke3Vg|xc60zI>JM| zLO)4Na)d_0dwPwLR1#%E-V8?HONvpv;ytZV5__+c?FAXZr4`1Yi_S;5KRfxktx}^; z7iqQ9Q>O8rQu?(#r8M>_qdcR|gh9~0k(17YB7M3$8Rmd90658a{e#nskDqgsQ2p;e zeTH_YK%tgiJoQgE|M5Jy8VjB&j;@0vIrS8cZ3rOoOe{S8GDk(b(E_6vrb6OWb_*?Z zHPuZP?h`WAY9K|yU$;&k`zw_03oq>&a+daqBvd+XBh^ySU7$HoT2OQyc1NjR*MxBeSf3bW!-5TvAU7Uf#Czy;>lm# zI{CI6y>}!wX|9i2+6~_~R}rvO&K)rlqtjT|RV?5jDO1)J{G_YW#0heuccR^&Q{h)u z2BcNGE=%YdVTHFS$6?#V!?43Kw5BDOek~{mQ&?d(rAt)11@PS^OGcF$>hJWcai*)JPjH2Cu>rwx5}$S@QNl=pK4Q}GcqcGo@&UEP$m3I^RcZz)5fxD&ZbQNx5RU|C6%o+U zKEh(G8(HSd5|bEiZwJ*BQFy5_eJFd8rG=9*qKIRk)JS2Y6$wV1I78Jg=8aUfT-9U~ zOoClpCmu}Tt^w(M=&1 zG3KBIi5qZEZ_PmMJ!C)9G*SfdI$U`5t($NEqL#iFk(CS2J-hJE^R;xxeGznfzr^UY zvgkR{_d(m7^Z*q4X_tfUv(h#@4R;TLQ%sxt+)gV!0tWIuu;fvXFM0nmeVo4e_G=5r z&)@v{8=y&Uy!LobP{DuTd+Rt_&_OZEhfnjD_nx`+!UqfQyauEEoQHYuiG}IuT3RpT zB)XKf9&}rsV26{V4gjB&J3F^;>o6I++e~dOyS4@8VD`J*P7^ZggQUxP1n{Z-PKTW> zhf%R5C|d;?D2Pp{jHVt>H(L*-pa|ST7Q7@-1Xd@q+-NRwLOlgkhKc_e2!LaaKsE$_ z&VPe!oB)%VlKEwZ0NK1mrcx=_($~yq=;v3hyDcHrG7ouIL8wxXk32ncs$(i;nw?eF zdqmgd%Hvy)ZJo5t$#bwot(;n19_rS<+F8L8x+JxiCZB5SRrdD5`C;wHt0nk>nSJ)MJ~svg)b73$iu>Q9BRL*UP-)0 zZSx+1QCJZY&q!#7#Rm4og^bWnfj1tp(ixsl4fZz~V}GN*af&A#`9eDC1xIXLilH!U zBS!WuBPpXs)L>o56LM0cx?I2{0CX%3x90m1j4Uh!!5^l&Bf>5`HUQJ*F%+8Uop1?! z?}#Vh2Guh(;Hl?D_;-UCU!KC0b}6q%buW@2I2onk-eFFY`0uLc$#vktk?;rql1oKs z!1jcVgk5G#05qi^e#!CpGDEC*7o(ifgl8Vl&M=-G1l)twWmH6-5x#h%B6fDnAQr-P zObYHbu{SHsyc@0fWoZpzjH)%IQP!Qai~xQbmEq^4iS};+fZYdw@C@w(ArwaTRc1bi zpb3PQMDP|QK5+tt2xGRZc(blG%3+RmMir=19>fT0QD|4ysoLLwb%#q)#rRPE)yw#& zV$M9jFU*=VIY4kc!zw>~)u*|^&;r$ucVSAv zMS5VHh1;6pJgTq>5JrY$0AujLo?UqQ^umiinY#>_K7?mv>!v7%gTCWl9yG-M*Xgho z@xjj)|M3^0eV=d#wKQu0LcqO1J#W6BC_r%f=6b7{1zmn$C=diCJ)L#Noz0~&peC~2 zkTAI;g=~x91!AWlk2oXfI76Hf1_eiga>OPCWjR5r5cD{w?4+B9%_DvN<}JZ|7IuD2 z98MG2cPiR6?68=Jom-&5(ft(lfg0sSaI=Z6WlAN?_m!vVmF119WbxBQRCMU zzN=5h_%l}foJpcWR3>Iy_pWW-rp~s;pagBs4cq#H3FuC_iDBBfqN%I1dnZ^+Fy-Kp z*eu{YBF_@G7F_6RW-UWOG1!T))`FB1Sh%po5~K#KF1SVEcMwb-0sGWEXf&sNdD6trA8mOp zZ7OX#=X@qIu97zd6ovB|?O)5m4+oq!Q9G~BxT4OPRpWn)0P4La84cvHMuzjo~z(znJ(&T0!ivTI2i zkN6Kd+S9ZolqROn=N4YcEt}0Pn`)YFoy#@Ms_6;wye55e=W|w8JYC>R zIxY9romNh?T+PV7l2Q7*jMAx^xs1&d4f81(C)-c7dviZY$-gZj%D0{#IHTMeYzy%n%-YZfFXUhlmKl%`>QnG z@Rrfucb-)l$c4=+ z(Ts+IyEADbsGrG{Hg6WqmmlhzDYTEscaQPDVh~0VjU&_{Njc)W#-9D^T9zMaXa;#0=~ zKh#khZcqrV;#lrsd$&8D&7Kr)&-Y@?pcZ*!PwtJ_+s4RyiG5NdiNOz~!Eg$J9~6&^ z#z@7s!L}GxM`2qEu2EY`pW?Ox8?b;-z(@@EffXL##SNQ39C{Idi$1i?9)d-{x5r$Z z7RRVn=B|RCz5qtTi(SDD5kArp6q$!1$^>oo-L{uk9;U(SLQw@F|NRWfx&DvB=rBjnw`ja<9aNkVacF>Klyw}jS)FVHqH09h zHk5%wL11HyZAPUQlp)Z7-Hx(0$n*-%G@!b=JA3v7WuR$c>+rD6HN>_fnhwbL&;=SY zi>y+12g`#E~|04QSunZ$; zMMHpWTq3inl=)Xri_6;l6oU>MGW+j%vVV<(1+$B&<6r9mnDEcQetKYK_5ySvj~#v0q7%U zc0$kxu4A-l-t39J5x}FHUe*=-d;t2$eURoWK_8zx^Eu0*59B!G6bJgyVd#ScC?ZM~ zK__1V`rz|l4t=D33FzaYPxcj|kB9PH5&B4XBNY{bK1u=l;Hlk^9e_hu<3k_epg2D2 zQ3U+BmT}odQIBwAeFqDmTu6WJL8ZMY>qQx|gV=qLeLjT2eiPA}Q1&gz_|OGwfr>b& z1%$soh+0TM4YJ4@!hi)k0Hra>8VpUWVTVw?4P^+NVE3c!0Lq3@Hi9xc$`HK3I#4!> zvV)N6H5^QU^lcoNpkv!^)`@PpQ09gVB4O-k(}F#OY7V392+EG4Yz$>3DEl_bzJs#w zqU?JpLo$K=K4f3c?S}baF=&HYek;c9ZVZsyklU^IrO#>VzgDOFimqc;*D-T=PPhAu z`rrD1H1F^J_ex;G-xy+D3UD}dl&{CIGVz_%*OF_-$3=t}?5*l)-%XzZqeDJ)L%{ijWo5*H8F(Xws zrxP>j!sb% z&-S(Z-+dr+Wy?=s#M#XMBJMYekdOM_9Mh2V4IyP;!1<1h13TU9)6j_h5z3xH*%rvg z^1}W$L@A686-96W>~I(LTL;YUVHf)>qDLMydmLqrDEo3AGkXHH`7z3(juP?ur_4SNtsiiQbq~11_(@nV5SN1u5bF;=5+FDIcl_Z@oK981c+t+6RRg#w2^ebLLX`t{LDofH5QW2;6$$H(4Zv;7=5& zffEyn9ijn%gm@z%f*=w6SVY>z5c4F$TogSh)%FHL5vVOgJiS!IgUUj-e7Z%)!X_vgo0}~pnbdz;lNT9zKSAb zH`turuFLAMIkThiulvW7_3y=zgoy0#Q$_+o~tW$UKO{ z8I$C38;oPpJO>LS={q_m&cl&kW0KJB@faOOQ4#k-*dla6Uxg?XZSz_bLbVp+&=Tul z*V4h-#?0L2Aq--`%3^5`X96YM=^8j^fez`6I9UWyMsZU&f)u!jwTQBzZ{leU@2f^p z)gXn>=!_{Ib)a2FP%#ju7WKOx#w#2wCg+?lFB$~aD2b?qHA*=uErVLIXN9~deIgHK z-GwMIuFK)AZg!Y`9o4;tvKLT>)+qY~WN<{x;W7{79Z)CgjOHf}kxk-nd~(|74WLBv zz|Kb?i)?zrVWwi_eXaB9=QIsb?<+S3pUo+FQ|puXvx}#9Uo>7Y_#bJT)wKgAi*_)J z1JaUf$_|JuOG%wi?VV5CIIqh>dPwFGFDVInt@k@#yMI;1ysr8-CDO<|64-Bru(aW) zhbPOX#r{3Lv&wyO;?hc|%Fb_nYpcJs!Jph1P;7f>TpEO+sgjQ$Id z%F_IOBw19UMfd=RXf3pac%T)|Y4D#AlscCf1%eU#Wxypm7F@S%4=WN46*D4U1k|9o zn6N`6P_I19JG$2xRr0)E;5-WK847qOI|NKpSQ*g}h#h%G7%h_w&!&N0$0P*BKrcyO zsuvNj7ZIc5^%5~w%=}(-pXZrkZ&Gul1bz7jzl%a@J>U#qdS63M)X}9iHF78 zL4=$xbTVldZ~bMWeL*G*heq>&(DpPg3XMm(D%umTc6Hp^>}c(vc-g5x8bD+rk3))4 z7`y5iEmsqMUtk7^?ZP@?#~-&Z!UKjNeg&^OY~jIsGA=@f(V8{$x34J?&lx zuMm#0Ed22O#TTaFF#k%2|2de|oe$))rI%kC=l^E_asL|uRsp1LDP+(T0;6i_F?j<% z-axlB!Vgt9cff((i4Ivqz&GcO8Bi3E^}P zi;OpSE@ZNBr1!b=3r~4&J@qU!;deiFK8!!EU-qv2%(zb!_-qzH;jcGPv2W|u1(AKi8W2dR5kzP1HdFzMAZ-HPT;AAk; zwD@ciFBH#>?xDyrE?kWJ2_adWz2JuBi%&xFTF?t2^^Xr1q3HNX1rjLut@ ztrHx22m%X*{h>hjoj@0<0Pr|;0L*E^whV-a;S-$bXfDlX<5I#W zzy17(mxYik9O_jbkXGPAXF%Eo$FHs_)#HXGv6w2qs@1>wjknX^YoB3fwcQ@sRcYE} zhW9ZrJN{Ji$my&LSsuwm;;eM_lHyAemtIXRcr*R&_6zK6YO6a+5r$46*J|^}dCD4cb6KNLiqx8PB8P>u_m8DHI@v*+t zkVst2Dm2s*muQ(mCc3mvXP`uvDGJJ$6J>@f(dAT~p;UCaltSfNS#zFfhSW7@i)OMZ zCw_$?zTS9BLh;vuCBjaLlbl@O2i=YM;ZGp1L z)?v~5KJBsXLlnzg>IZf8Fno?QR!urJ%=(DW&aChXjg|t`*fT$vIdnN zL75Scc*44I>Y6-rTzXAiIi9#g z5{ZY!;|bTKig9X5MkH?Wlb6sqxrB{3;TnJl|Yz-399 zB&hLPmI%1`EO*gmsbG~rJ-O4H`{JG@0*X`2U36J$6s#7ccypHsxJ+@E3(ydL%?$Z> z@BQt_T~xkZFW4l|A~LujGPoczxcIvm?)sS3Kd^uHu> + +``` + +**样式**: +- 宽度:4px +- 高度:100%(填充面板高度) +- 位置:绝对定位,right: 0 +- 光标:col-resize +- Hover 背景色:rgba(26, 26, 26, 0.1) +- Hover 边框:2px solid #1a1a1a + +#### 验收标准 + +- [ ] 可以拖动分组面板的 resizer +- [ ] 可以拖动账号面板的 resizer +- [ ] 可以拖动邮件列表面板的 resizer +- [ ] 拖动时面板宽度实时变化 +- [ ] 拖动时光标为 col-resize +- [ ] 拖动时有视觉反馈(背景色、边框) +- [ ] 拖动流畅,帧率 ≥ 60fps +- [ ] 释放鼠标后 500ms 保存状态 + +--- + +### F1.2 宽度限制(最小/最大) + +**功能编号**: F1.2 +**功能名称**: 宽度限制 +**优先级**: P0 + +#### 功能描述 + +系统对每个面板的宽度进行限制,防止用户拖动到不合理的尺寸。 + +#### 宽度限制规则 + +| 面板 | 最小宽度 | 最大宽度 | 说明 | +|------|---------|---------|------| +| 分组面板 | 150px | 400px | 确保分组名称和图标可见 | +| 账号面板 | 180px | 500px | 确保邮箱地址可读 | +| 邮件列表 | 280px | 600px | 确保邮件预览可读 | +| 邮件详情 | 400px | 无限制 | 自动填充剩余空间 | + +#### 主流程 + +1. 用户拖动 resizer +2. 系统计算新宽度 +3. 系统获取该面板的最小/最大宽度 +4. 系统应用限制:`newWidth = Math.max(minWidth, Math.min(maxWidth, newWidth))` +5. 系统更新面板宽度 + +#### 验收标准 + +- [ ] 分组面板宽度不能小于 150px +- [ ] 分组面板宽度不能大于 400px +- [ ] 账号面板宽度不能小于 180px +- [ ] 账号面板宽度不能大于 500px +- [ ] 邮件列表宽度不能小于 280px +- [ ] 邮件列表宽度不能大于 600px +- [ ] 达到限制时,继续拖动无效果 + +--- + +### F1.3 确保邮件详情面板可见 + +**功能编号**: F1.3 +**功能名称**: 确保邮件详情面板可见 +**优先级**: P0 + +#### 功能描述 + +无论用户如何调整侧边栏宽度,邮件详情面板始终保持至少 400px 可见宽度。 + +#### 主流程 + +1. 用户拖动任意侧边栏的 resizer +2. 系统计算容器总宽度 +3. 系统计算其他面板的总宽度 +4. 系统计算最大允许宽度:`maxAllowed = containerWidth - otherPanelsWidth - 400` +5. 系统应用限制:`newWidth = Math.min(newWidth, maxAllowed)` +6. 系统更新面板宽度 + +#### 异常流程 + +- **异常 1**: 容器宽度不足(< 800px) + - 处理:触发窗口自适应逻辑部分面板 + +#### 验收标准 + +- [ ] 邮件详情面板始终可见 +- [ ] 邮件详情面板宽度 ≥ 400px +- [ ] 拖动侧边栏时,邮件详情面板不会被挤压消失 +- [ ] 窗口宽度不足时,自动折叠侧边栏 + +--- + +### F1.4 拖动视觉反馈 + +**功能编号**: F1.4 +**功能名称**: 拖动视觉反馈 +**优先级**: P1 + +#### 功能描述 + +在拖动过程中提供清晰的视觉反馈,让用户知道当前正在拖动。 + +#### 视觉反馈元素 + +1. **Resizer Hover 状态**: + - 背景色:rgba(26, 26, 26, 0.1) + - 中心线:2px solid #1a1a1a + +2. **拖动中状态**: + - 全局光标:col-resize + - 禁用文本选择:user-select: none + - Resizer 保持高亮状态 + +3. **拖动结束**: + - 恢复默认光标 + - 恢复文本选择 + - Resizer 恢复普通状态 + +#### 验收标准 + +- [ ] Hover 时 resizer 有视觉反馈 +- [ ] 拖动时全局光标为 col-resize +- [ ] 拖动时无法选中文本 +- [ ] 拖动结束后光标恢复正常 +- [ ] 拖动结束后可以选中文本 + +--- + +## 四、F2 - 面板折叠/展开 + +### F2.1 折叠按钮交互 + +**功能编号**: F2.1 +**功能名称**: 折叠按钮交互 +**优先级**: P0 + +#### 功能描述 + +每个侧边栏面板的右上角有一个折叠按钮,点击可以折叠/展开面板。 + +#### 前置条件 + +- 面板已渲染 +- 至少有一个其他侧边栏面板处于展开状态 + +#### 主流程 + +1. 用户点击折叠按钮 +2. 系统检查是否至少有一个其他面板展开 +3. 如果是最后一个展开的面板,阻止折叠并提示用户 +4. 否则,切换面板的折叠状态 +5. 系统添加/移除 `.collapsed` 类 +6. 系统触发 CSS 过渡动画(200ms) +7. 系统立即保存状态到 localStorage + +#### 界面元素 + +**折叠按钮**: +```html + +``` + +**按钮位置**: +- 导航栏右侧 +- 在其他操作按钮之后 + +**按钮样式**: +- 尺寸:32x32px +- 图标:重置图标(↻) +- Hover 背景色:#f5f5f5 +- Hover 边框:1px solid #ccc + +#### 验收标准 + +- [ ] 导航栏显示重置按钮 +- [ ] 按钮位置合理 +- [ ] Hover 时有视觉反馈 +- [ ] Tooltip 显示"恢复默认布局" + +--- + +### F3.2 确认对话框 + +**功能编号**: F3.2 +**功能名称**: 确认对话框 +**优先级**: P1 + +#### 功能描述 + +点击重置按钮后,弹出确认对话框,防止用户误操作。 + +#### 界面元素 + +**确认对话框**: +```html + +``` + +#### 主流程 + +1. 用户点击重置按钮 +2. 系统显示确认对话框 +3. 用户点击"确定"或"取消" +4. 如果点击"确定",执行重置逻辑 +5. 如果点击"取消",关闭对话框 + +#### 验收标准 + +- [ ] 点击重置按钮显示对话框 +- [ ] 对话框内容清晰 +- [ ] 点击"取消"关闭对话框 +- [ ] 点击"确定"执行重置 +- [ ] 点击遮罩层关闭对话框 + +--- + +### F3.3 重置逻辑 + +**功能编号**: F3.3 +**功能名称**: 重置逻辑 +**优先级**: P1 + +#### 功能描述 + +执行重置操作,将所有面板恢复到默认宽度和折叠状态。 + +#### 默认布局参数 + +| 面板 | 默认宽度 | 默认状态 | +|------|---------|---------| +| 分组面板 | 200px | 展开 | +| 账号面板 | 260px | 展开 | +| 邮件列表 | 380px | 展开 | +| 邮件详情 | 自适应 | 展开 | + +#### 主流程 + +1. 用户确认重置 +2. 系统清除 localStorage 中的布局状态 +3. 系统重置 CSS 变量到默认值 +4. 系统移除所有 `.collapsed` 类 +5. 系统触发重置动画(300ms) +6. 系统关闭确认对话框 +7. 系统显示成功提示 + +#### 验收标准 + +- [ ] 所有面板恢复到默认宽度 +- [ ] 所有面板恢复到展开状态 +- [ ] localStorage 被清除 +- [ ] 重置动画流畅 +- [ ] 显示成功提示 + +--- + +### F3.4 重置动画 + +**功能编号**: F3.4 +**功能名称**: 重置动画 +**优先级**: P2 + +#### 功能描述 + +重置时使用平滑的过渡动画,而不是瞬间跳变。 + +#### 动画参数 + +- **动画时长**: 300ms +- **缓动函数**: ease +- **动画属性**: width + +#### CSS 实现 + +```css +.main-container.resetting { + transition: grid-template-columns 300ms ease; +} + +.resizable-panel.resetting { + transition: width 300ms ease; +} +``` + +#### 验收标准 + +- [ ] 重置动画流畅 +- [ ] 动画时长为 300ms +- [ ] 所有面板同时动画 + +--- + +## 六、F4 - 状态持久化 + +### F4.1 保存布局状态 + +**功能编号**: F4.1 +**功能名称**: 保存布局状态 +**优先级**: P0 + +#### 功能描述 + +自动保存用户的布局偏好到 localStorage,包括面板宽度和折叠状态。 + +#### 数据结构 + +```json +{ + "version": "1.1", + "userId": "user123", + "timestamp": 1709020800000, + "panels": { + "groups": { + "width": "200px", + "collapsed": false + }, + "accounts": { + "width": "260px", + "collapsed": false + }, + "emails": { + "width": "380px", + "collapsed": false + } + } +} +``` + +#### 存储键名 + +- 格式:`outlook_layout_state_${userId}` +- 未登录:`outlook_layout_state_guest` + +#### 主流程 + +1. 用户调整布局(拖动或折叠) +2. 系统触发保存逻辑 +3. 系统读取当前所有面板的状态 +4. 系统构建数据对象 +5. 系统序列化为 JSON +6. 系统写入 localStorage + +#### 验收标准 + +- [ ] 拖动后 500ms 保存状态 +- [ ] 折叠后立即保存状态 +- [ ] 数据格式正确 +- [ ] 多用户隔离生效 +- [ ] localStorage 数据 ≤ 1KB + +--- + +### F4.2 加载布局状态 + +**功能编号**: F4.2 +**功能名称**: 加载布局状态 +**优先级**: P0 + +#### 功能描述 + +页面加载时从 localStorage 读取并恢复用户的布局偏好。 + +#### 主流程 + +1. 页面 DOMContentLoaded 事件触发 +2. 系统获取当前用户 ID +3. 系统从 localStorage 读取数据 +4. 系统验证数据格式和版本 +5. 系统应用面板宽度(更新 CSS 变量) +6. 系统应用折叠状态(添加 `.collapsed` 类) +7. 系统跳过动画(避免页面加载时的闪烁) + +#### 异常流程 + +- **异常 1**: localStorage 不可用 + - 处理:使用默认布局,不报错 +- **异常 2**: 数据格式错误 + - 处理:清除数据,使用默认布局 +- **异常 3**: 版本不兼容 + - 处理:迁移数据或使用默认布局 + +#### 验收标准 + +- [ ] 刷新页面后布局状态恢复 +- [ ] 恢复时无动画闪烁 +- [ ] localStorage 不可用时使用默认布局 +- [ ] 数据错误时使用默认布局 + +--- + +### F4.3 多用户隔离 + +**功能编号**: F4.3 +**功能名称**: 多用户隔离 +**优先级**: P1 + +#### 功能描述 + +不同用户的布局偏好独立存储,互不影响。 + +#### 实现方式 + +- 存储键名包含用户 ID:`outlook_layout_state_${userId}` +- 从全局变量或页面元素获取用户 ID +- 未登录用户使用 `guest` 作为 ID + +#### 主流程 + +1. 系统获取当前用户 ID +2. 系统构建存储键名 +3. 系统使用该键名读写 localStorage + +#### 验收标准 + +- [ ] 不同用户的布局独立 +- [ ] 切换用户后布局正确 +- [ ] 未登录用户使用 guest 键名 + +--- + +### F4.4 防抖保存 + +**功能编号**: F4.4 +**功能名称**: 防抖保存 +**优先级**: P1 + +#### 功能描述 + +拖动调整宽度时,延迟 500ms 保存,避免频繁写入 localStorage。 + +#### 实现逻辑 + +```javascript +let pendingSave = null; + +function saveStateDebounced() { + if (pendingSave) { + clearTimeout(pendingSave); + } + pendingSave = setTimeout(() => { + saveStateNow(); + pendingSave = null; + }, 500); +} +``` + +#### 验收标准 + +- [ ] 拖动时不立即保存 +- [ ] 停止拖动后 500ms 保存 +- [ ] 连续拖动只保存一次 +- [ ] 折叠操作立即保存(不防抖) + +--- + +### F4.5 页面卸载时强制保存 + +**功能编号**: F4.5 +**功能名称**: 页面卸载时强制保存 +**优先级**: P1 + +#### 功能描述 + +用户关闭页面或刷新时,强制执行防抖任务,确保最后一次调整被保存。 + +#### 实现逻辑 + +```javascript +window.addEventListener('beforeunload', () => { + if (pendingSave) { + clearTimeout(pendingSave); + saveStateNow(); + } +}); +``` + +#### 验收标准 + +- [ ] 拖动后立即关闭页面,状态被保存 +- [ ] 拖动后立即刷新页面,状态被保存 +- [ ] 不影响页面卸载速度 + +--- + + +## 七、F5 - 窗口尺寸自适应 + +### F5.1 监听窗口大小变化 + +**功能编号**: F5.1 +**功能名称**: 监听窗口大小变化 +**优先级**: P1 + +#### 功能描述 + +系统监听浏览器窗口的 resize 事件,根据窗口宽度自动调整布局。 + +#### 实现逻辑 + +```javascript +let resizeTimer = null; + +window.addEventListener("resize", () => { + if (resizeTimer) { + clearTimeout(resizeTimer); + } + resizeTimer = setTimeout(() => { + handleWindowResize(); + resizeTimer = null; + }, 200); +}); +``` + +#### 验收标准 + +- [ ] 窗口大小改变时触发处理 +- [ ] 使用防抖避免频繁触发 +- [ ] 防抖延迟为 200ms + +--- + +### F5.2 自动折叠面板 + +**功能编号**: F5.2 +**功能名称**: 自动折叠面板 +**优先级**: P1 + +#### 功能描述 + +当窗口宽度小于特定断点时,自动折叠侧边栏面板,确保邮件详情面板可见。 + +#### 断点规则 + +| 窗口宽度 | 自动折叠的面板 | +|---------|---------------| +| < 1200px | 分组面板 | +| < 900px | 分组面板 + 账号面板 | +| < 700px | 分组面板 + 账号面板 + 邮件列表 | + +#### 主流程 + +1. 窗口大小改变 +2. 系统获取当前窗口宽度 +3. 系统根据断点判断需要折叠的面板 +4. 系统自动折叠面板(不保存状态) +5. 系统添加 .auto-collapsed 类(区分用户手动折叠) + +#### 特殊处理 + +- 自动折叠不保存到 localStorage +- 窗口变大时不自动展开(尊重用户选择) +- 用户手动展开后,窗口缩小时再次自动折叠 + +#### 验收标准 + +- [ ] 窗口 < 1200px 时自动折叠分组面板 +- [ ] 窗口 < 900px 时自动折叠账号面板 +- [ ] 窗口 < 700px 时自动折叠邮件列表 +- [ ] 自动折叠不保存状态 +- [ ] 窗口变大时不自动展开 + +--- + +### F5.3 媒体查询降级 + +**功能编号**: F5.3 +**功能名称**: 媒体查询降级 +**优先级**: P2 + +#### 功能描述 + +使用 CSS 媒体查询作为降级方案,即使 JavaScript 失效也能保证基本的响应式布局。 + +#### CSS 实现 + +```css +@media (max-width: 1200px) { + .group-panel { + display: none; + } +} + +@media (max-width: 900px) { + .account-panel { + display: none; + } +} + +@media (max-width: 700px) { + .email-list-panel { + display: none; + } +} +``` + +#### 验收标准 + +- [ ] JavaScript 禁用时布局仍然响应式 +- [ ] 媒体查询断点与 JavaScript 一致 +- [ ] 小屏幕下邮件详情面板可见 + +--- + + +## 八、F6 - 键盘可访问性 + +### F6.1 Resizer 键盘支持 + +**功能编号**: F6.1 +**功能名称**: Resizer 键盘支持 +**优先级**: P2 + +#### 功能描述 + +Resizer 支持键盘操作,用户可以使用方向键调整面板宽度。 + +#### 键盘操作 + +| 按键 | 操作 | 调整幅度 | +|------|------|---------| +| ← | 减小宽度 | 10px | +| → | 增大宽度 | 10px | +| Shift + ← | 快速减小 | 50px | +| Shift + → | 快速增大 | 50px | +| Tab | 聚焦到下一个 resizer | - | +| Shift + Tab | 聚焦到上一个 resizer | - | + +#### 主流程 + +1. 用户按 Tab 键聚焦到 resizer +2. Resizer 显示聚焦样式(outline) +3. 用户按方向键 +4. 系统计算新宽度 +5. 系统应用宽度限制 +6. 系统更新面板宽度 +7. 系统更新 aria-valuenow 属性 +8. 系统触发防抖保存 + +#### 验收标准 + +- [ ] Tab 键可以聚焦到 resizer +- [ ] 聚焦时有明显的视觉反馈 +- [ ] 左箭头键减小宽度 +- [ ] 右箭头键增大宽度 +- [ ] Shift + 箭头键快速调整 +- [ ] 键盘调整也应用宽度限制 +- [ ] 键盘调整后保存状态 + +--- + +### F6.2 折叠按钮键盘支持 + +**功能编号**: F6.2 +**功能名称**: 折叠按钮键盘支持 +**优先级**: P2 + +#### 功能描述 + +折叠按钮支持键盘操作,用户可以使用 Enter 或 Space 键触发折叠/展开。 + +#### 键盘操作 + +| 按键 | 操作 | +|------|------| +| Tab | 聚焦到按钮 | +| Enter | 触发折叠/展开 | +| Space | 触发折叠/展开 | + +#### 验收标准 + +- [ ] Tab 键可以聚焦到折叠按钮 +- [ ] 聚焦时有视觉反馈 +- [ ] Enter 键触发折叠/展开 +- [ ] Space 键触发折叠/展开 + +--- + +### F6.3 ARIA 属性支持 + +**功能编号**: F6.3 +**功能名称**: ARIA 属性支持 +**优先级**: P2 + +#### 功能描述 + +为所有交互元素添加完整的 ARIA 属性,支持屏幕阅读器。 + +#### ARIA 属性清单 + +**Resizer**: +- `role="separator"` +- `aria-orientation="vertical"` +- `aria-label="调整[面板名称]宽度"` +- `aria-valuenow`: 当前宽度值 +- `aria-valuemin`: 最小宽度 +- `aria-valuemax`: 最大宽度 +- `tabindex="0"` + +**折叠按钮**: +- `aria-label="折叠[面板名称]"` 或 `"展开[面板名称]"` +- `aria-expanded="true"` 或 `"false"` +- `title`: 提示文本 + +**折叠指示器**: +- `role="button"` +- `aria-label="展开[面板名称]"` +- `tabindex="0"` + +#### 验收标准 + +- [ ] 所有 resizer 有完整的 ARIA 属性 +- [ ] 所有折叠按钮有完整的 ARIA 属性 +- [ ] 所有折叠指示器有完整的 ARIA 属性 +- [ ] aria-valuenow 实时更新 +- [ ] aria-expanded 实时更新 + +--- + +### F6.4 焦点管理 + +**功能编号**: F6.4 +**功能名称**: 焦点管理 +**优先级**: P2 + +#### 功能描述 + +合理管理键盘焦点,确保用户使用键盘操作时体验流畅。 + +#### 焦点规则 + +1. **折叠面板后**: + - 焦点移到折叠指示器 + +2. **展开面板后**: + - 焦点移到折叠按钮 + +3. **Tab 顺序**: + - 导航栏按钮 → 分组面板折叠按钮 → 分组面板 resizer → 账号面板折叠按钮 → ... + +4. **模态框打开**: + - 焦点移到模态框的第一个可聚焦元素 + +5. **模态框关闭**: + - 焦点返回到触发按钮 + +#### 验收标准 + +- [ ] 折叠后焦点移到指示器 +- [ ] 展开后焦点移到折叠按钮 +- [ ] Tab 顺序合理 +- [ ] 模态框焦点管理正确 + +--- + + +## 九、实现细节 + +### 9.1 文件修改清单 + +#### 需要修改的文件 + +| 文件路径 | 修改内容 | 优先级 | +|---------|---------|--------| +| `templates/index.html` | 修改主容器结构,添加 resizer 和折叠按钮 | P0 | +| `static/css/main.css` | 添加 Grid 布局、CSS 变量、resizer 样式 | P0 | +| `static/js/main.js` | 初始化 LayoutManager | P0 | + +#### 需要新增的文件 + +| 文件路径 | 文件内容 | 优先级 | +|---------|---------|--------| +| `static/js/layout-manager.js` | LayoutManager 类实现 | P0 | +| `static/js/state-manager.js` | StateManager 类实现 | P0 | + +### 9.2 HTML 结构变更 + +#### 当前结构 + +```html +
+ + + +
...
+
+``` + +#### 目标结构 + +```html +
+ + + + + + + + + + +
...
+
+``` + +### 9.3 CSS 变量系统 + +```css +:root { + /* 面板初始宽度 */ + --groups-panel-width: 200px; + --accounts-panel-width: 260px; + --emails-panel-width: 380px; + + /* 最小/最大宽度 */ + --groups-panel-min-width: 150px; + --groups-panel-max-width: 400px; + --accounts-panel-min-width: 180px; + --accounts-panel-max-width: 500px; + --emails-panel-min-width: 280px; + --emails-panel-max-width: 600px; + --email-detail-panel-min-width: 400px; + + /* 折叠状态 */ + --panel-collapsed-width: 0px; + --collapator-width: 48px; + + /* Resizer */ + --resizer-width: 4px; + + /* 动画 */ + --panel-transition-duration: 200ms; + --reset-transition-duration: 300ms; +} +``` + +### 9.4 JavaScript 模块设计 + +#### LayoutManager 类 + +```javascript +class LayoutManager { + constructor() { + this.isResizing = false; + this.currentPanel = null; + this.startX = 0; + this.startWidth = 0; + this.pendingSave = null; + this.stateManager = new StateManager(); + } + + // 初始化 + init() { } + + // 拖动相关 + startResize(e) { } + resize(e) { } + stopResize() { } + + // 键盘相关 + handleKeyboard(e) { } + + // 折叠相关 + togglePanel(panelName) { } + collapsePanel(panelName, auto = false) { } + expandPanel(panelName) { } + + // 重置相关 + resetLayout() { } + + // 窗口自适应 + handleWindowResize() { } + + // 状态管理 + saveStateDebounced() { } + saveStateNow() { } + loadState() { } + + // 工具方法 + updatePanelWidth(panel, width) { } + getMinWidth(panelType) { } + getMaxWidth(panelType) { } + calculateOtherPanelsWidth(excludePanel) { } + updateAriaValue(resizer, value) { } + getCurrentUserId() { } +} +``` + +#### StateManager 类 + +```javascript +class StateManager { + constructor() { + this.storageKeyPrefix = 'outlook_layout_state_'; + } + + // 保存状态 + save(userId, state) { } + + // 加载状态 + load(userId) { } + + // 清除状态 + clear(userId) { } + + // 验证数据 + validate(state) { } + + // 版本迁移 + migrate(state) { } + + // 获取存储键名 + getStorageKey(userId) { } +} +``` + +--- + +## 十、验收标准总览 + +### 10.1 功能验收标准 + +#### P0 - 必须通过(核心功能) + +**拖动调整宽度**: +- [ ] F1.1 - 可以拖动调整分组面板宽度 +- [ ] F1.1 - 可以拖动调整账号面板宽度 +- [ ] F1.1 - 可以拖动调整邮件列表宽度 +- [ ] F1.2 - 宽度限制生效(最小/最大) +- [ ] F1.3 - 邮件详情面板始终可见且 ≥ 400px + +**面板折叠/展开**: +- [ ] F2.1 - 可以折叠/展开分组面板 +- [ ] F2.1 - 可以折叠/展开账号面板 +- [ ] F2.1 - 可以折叠/展开邮件列表 +- [ ] F2.2 - 折叠/展开动画流畅(200ms) +- [ ] F2.5 - 折叠后 resizer 隐藏 +- [ ] F2.6 - 不能折叠最后一个面板 + +**状态持久化**: +- [ ] F4.1 - 拖动后 500ms 保存状态 +- [ ] F4.1 - 折叠后立即保存状态 +- [ ] F4.2 - 刷新页面后状态恢复 + +#### P1 - 重要功能 + +**视觉反馈**: +- [ ] F1.4 - 拖动时有视觉反馈 +- [ ] F2.3 - 折叠后显示 48px 指示器 +- [ ] F2.4 - 点击指示器可以展开 + +**恢复默认布局**: +- [ ] F3.1 - 导航栏显示重置按钮 +- [ ] F3.2 - 点击重置显示确认对话框 +- [ ] F3.3 - 确认后恢复默认布局 + +**状态管理**: +- [ ] F4.3 - 多用户布局隔离 +- [ ] F4.5 - 页面卸载时强制保存 + +**窗口自适应**: +- [ ] F5.1 - 监听窗口大小变化 +- [ ] F5.2 - 窗口 < 1200px 自动折叠分组 +- [ ] F5.2 - 窗口 < 900px 自动折叠账号 +- [ ] F5.2 - 窗口 < 700px 自动折叠邮件列表 + +#### P2 - 优化功能 + +**动画优化**: +- [ ] F3.4 - 重置动画流畅(300ms) +- [ ] F5.3 - 媒体查询降级生效 + +**键盘可访问性**: +- [ ] F6.1 - Resizer 支持键盘操作 +- [ ] F6.2 - 折叠按钮支持键盘操作 +- [ ] F6.3 - ARIA 属性完整 +- [ ] F6.4 - 焦点管理合理 + +### 10.2 非功能验收标准 + +#### 性能 + +- [ ] 拖动时帧率 ≥ 60fps +- [ ] 折叠/展开动画流畅无卡顿 +- [ ] localStorage 数据 ≤ 1KB +- [ ] 页面加载时间增加 ≤ 50ms + +#### 兼容性 + +- [ ] Chrome 90+ 正常工作 +- [ ] Edge 90+ 正常工作 +- [ ] Firefox 88+ 正常工作 +- [ ] Safari 14+ 正常工作 + +#### 可访问性 + +- [ ] 所有交互元素可键盘访问 +- [ ] 所有交互元素有 ARIA 属性 +- [ ] 屏幕阅读器可正常使用 + +### 10.3 回归验证清单 + +#### 现有功能不受影响 + +- [ ] 邮件列表正常显示 +- [ ] 邮件详情正常显示 +- [ ] 点击邮件可以查看详情 +- [ ] 账号切换正常 +- [ ] 分组切换正常 +- [ ] 邮件刷新正常 +- [ ] 邮件删除正常 +- [ ] Token 刷新正常 +- [ ] 搜索功能正常 +- [ ] 原有的响应式布局正常 + +--- + +## 十一、测试策略 + +### 11.1 单元测试 + +**测试文件**: `tests/test_layout_manager.js` + +**测试用例**: +1. LayoutManager 初始化 +2. 宽度计算逻辑 +3. 宽度限制逻辑 +4. 状态序列化/反序列化 +5. 用户 ID 获取逻辑 + +### 11.2 集成测试 + +**测试场景**: +1. 拖动调整宽度 → 保存 → 刷新 → 恢复 +2. 折叠面板 → 保存 → 刷新 → 恢复 +3. 重置布局 → localStorage 清除 +4. 窗口缩小 → 自动折叠 → 窗口放大 +5. 多用户切换 → 布局独立 + +### 11.3 用户验收测试 + +**测试场景**: +1. 新用户首次访问 → 默认布局 +2. 调整布局 → 刷新 → 布局恢复 +3. 折叠所有面板 → 阻止最后一个 +4. 拖动到极限 → 宽度限制生效 +5. 小屏幕访问 → 自动折叠 + +### 11.4 兼容性测试 + +**测试矩阵**: + +| 浏览器 | 版本 | 操作系统 | 测试结果 | +|--------|------|---------|---------| +| Chrome | 90+ | Windows | | +| Chrome | 90+ | macOS | | +| Edge | 90+ | Windows | | +| Firefox | 88+ | Windows | | +| Firefox | 88+ | macOS | | +| Safari | 14+ | macOS | | + +--- + +## 十二、风险和依赖 + +### 12.1 技术风险 + +| 风险 | 影响 | 概率 | 缓解措施 | +|------|------|------|----------| +| CSS Grid 兼容性 | 高 | 低 | 提前测试目标浏览器 | +| 拖动性能问题 | 中 | 中 | 使用 requestAnimationFrame | +| localStorage 配额满 | 低 | 低 | 降级到默认布局 | +| 现有功能受影响 | 高 | 中 | 充分回归测试 | + +### 12.2 依赖关系 + +- **无后端依赖**:纯前端功能,不需要后端支持 +- **无第三方库依赖**:使用原生 JavaScript 实现 +- **CSS 特性依赖**:CSS Grid、CSS 变量、CSS transitions + +### 12.3 缓解措施 + +1. **充分测试**:在所有目标浏览器上测试 +2. **渐进增强**:使用媒体查询作为降级方案 +3. **错误处理**:localStorage 不可用时使用默认布局 +4. **性能优化**:使用防抖、节流、requestAnimationFrame + +--- + +## 十三、附录 + +### 13.1 参考文档 + +- **PRD**: `PRD/PRD-可调整布局系统.md` v1.1 +- **EnsoAI**: `exeoAI/src/renderer/components/layout/` +- **CSS Grid**: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout +- **ARIA**: https://www.w3.org/WAI/ARIA/apg/ + +### 13.2 功能清单汇总 + +| 模块 | 子功能数 | P0 | P1 | P2 | +|------|---------|----|----|-----| +| F1 - 可拖动调整宽度 | 4 | 3 | 1 | 0 | +| F2 - 面板折叠/展开 | 6 | 4 | 2 | 0 | +| F3 - 恢复默认布局 | 4 | 0 | 3 | 1 | +| F4 - 状态持久化 | 5 | 2 | 3 | 0 | +| F5 - 窗口尺寸自适应 | 3 | 0 | 2 | 1 | +| F6 - 键盘可访问性 | 4 | 0 | 0 | 4 | +| **总计** | **26** | **9** | **11** | **6** | + +### 13.3 变更记录 + +| 版本 | 日期 | 变更内容 | 变更人 | +|------|------|----------|--------| +| v1.0 | 2026-02-26 | 初始版本,包含 6 个功能模块、26 个子功能 | 开发团队 | + +--- + +**文档结束** diff --git a/docs/LOG/CN-00001-完成legacy删除.md b/docs/LOG/CN-00001-完成legacy删除.md new file mode 100644 index 0000000..a637576 --- /dev/null +++ b/docs/LOG/CN-00001-完成legacy删除.md @@ -0,0 +1,77 @@ +# 完成 legacy.py 删除 + +## 状态: 已完成 ✅ + +### 完成时间 +2026-02-25 + +### 完成的工作 + +1. **删除 legacy.py 文件** + - 确认所有 Python 代码中已无 legacy 导入 + - 删除 `outlook_web/legacy.py`(5757 行代码) + - 95 个测试全部通过 + +2. **提交记录** + ``` + 828d31f refactor: 删除 legacy.py - 完成模块化迁移 + ``` + +### 迁移总结 + +整个 Legacy 迁移到 Controllers 层项目已完成: + +**模块迁移:** +- groups(6 个路由)✅ +- tags(4 个路由)✅ +- settings(3 个路由)✅ +- system(3 个路由)✅ +- audit(1 个路由)✅ +- pages(3 个路由)✅ +- temp_emails(3 个路由)✅ +- oauth(2 个路由)✅ +- scheduler(1 个路由)✅ +- emails(4 个路由)✅ +- accounts(20 个路由)✅ + +**基础设施迁移:** +- 中间件迁移到 `middleware/` 模块 ✅ +- 调度器迁移到 `services/scheduler.py` ✅ + +**总计:54 个路由已迁移,legacy.py 已删除** + +### 当前架构 + +``` +outlook_web/ +├── app.py # 应用工厂 +├── config.py # 配置管理 +├── db.py # 数据库连接 +├── errors.py # 错误处理 +├── routes/ # Blueprint 路由层 +├── controllers/ # 控制器层 (请求处理) +├── services/ # 业务逻辑层 +├── repositories/ # 数据访问层 +├── middleware/ # 中间件 +└── security/ # 安全模块 +``` + +### 后续建议 + +根据 TODO 文档,还有以下可选任务: + +1. **4.5 更新文档** + - 更新 CLAUDE.md 项目架构说明 + - 更新 docs/DEV/00002-前后端拆分-开发者指南.md + +2. **P1 优化** + - 检查重复代码,提取公共函数 + - 提升测试覆盖率 + - 性能优化 + +### Git 状态 + +``` +当前分支: main +领先 origin/main 20 个提交 +``` diff --git a/docs/LOG/LOG-00003-可调整布局系统-执行日志.md b/docs/LOG/LOG-00003-可调整布局系统-执行日志.md new file mode 100644 index 0000000..a6d4d68 --- /dev/null +++ b/docs/LOG/LOG-00003-可调整布局系统-执行日志.md @@ -0,0 +1,120 @@ +# 执行日志:可调整布局系统(00003) + +- 日期:2026-02-27 +- 目的:按 `docs/TODO/TODO-可调整布局系统.md` 推进未完成项(阶段五/六为主),补齐 **恢复默认布局(F3)/窗口尺寸适配(F5)/键盘可访问性(F6)**,并用可验证证据(测试/文档)确保不回归主业务功能。 + +--- + +## 2026-02-27 + +### 启动记录 + +- 本轮开始依据的目标文档:`docs/TODO/TODO-可调整布局系统.md` +- 约束: + - 不引入前端构建链(保持零构建、外链 CSS/JS)。 + - 现有主业务功能(分组/账号/邮件)行为不回归。 + - 关键路径用 Jest(`tests/layout-system/`)提供可验证证据。 + +### 计划推进范围 + +- 阶段五:恢复默认布局(F3.1~F3.4) +- 阶段六:窗口适配与键盘支持(F5.1~F5.3、F6.1~F6.4) + +### 阶段五(进行中):恢复默认布局(F3) + +- 决策:沿用现有 `.modal/.show` 交互模式(与 addGroup/addAccount 等一致),不引入新依赖;重置逻辑在 `LayoutManager.resetLayout()` 内完成,且**显式取消未到期的保存防抖**,避免“重置后又被定时器写回”导致 localStorage 未真正清空。 + +#### 产出 + +- 导航栏增加恢复按钮: + - `templates/index.html` +- 恢复确认对话框: + - `templates/partials/modals.html`(`#resetLayoutModal`) +- 重置动画与按钮样式: + - `static/css/layout.css`(`.layout-resetting`、`.reset-layout-btn`、`#resetLayoutModal .text-muted`) +- 重置逻辑与动画封装: + - `static/js/layout-manager.js`(`resetLayout()`、`withResettingAnimation()`、debounce 增加 `cancel()`) +- UI 绑定(show/hide/confirm + ESC 关闭): + - `static/js/layout-bootstrap.js` + +### 阶段六(进行中):窗口适配与键盘支持(F5/F6) + +- 决策: + - 窗口适配以 `LayoutManager` 统一处理:`resize` 防抖 200ms(FD F5.1),断点自动折叠/恢复仅作用于 **autoCollapsed** 面板(避免覆盖用户手动折叠偏好)。 + - 键盘宽度调整直接复用 `calculateNewWidth()`,确保与鼠标拖动共享同一套最小/最大/详情最小宽度约束。 + - 兼容既有“移动端隐藏列表”逻辑:隐藏时同步将 Grid 列宽置 0,避免出现“元素 display:none 但列宽仍占位”的空白列回归。 + +#### 产出 + +- 窗口适配 + 键盘调整: + - `static/js/layout-manager.js`(`handleWindowResize()`、`onWindowResize()`、`onResizerKeydown()`、`expandPanel(panelType, auto)`) +- 业务隐藏邮件列表与 Grid 列宽联动: + - `static/js/features/emails.js`(`syncEmailListVisibility()`,替换原 `.hidden` 仅改 display 的实现) + +#### 验证 + +- 布局系统 Jest(含新增 F3/F5/F6 用例):`npm test` 通过(11 suites / 93 tests) +- 全量 Python 单测回归:`python -m unittest discover -s tests -v` 通过(95 tests) + +--- + +### UI 问题修复:窄屏下“全部折叠后无法再展开”(F2/F5 交互回归) + +#### 现象 + +- 当窗口较窄(触发 `@media (max-width: 768px)`)且 LayoutManager 自动折叠/用户折叠侧边栏后: + - **分组面板 / 账号面板**的折叠指示器会被一起移出屏幕(left 为负值),导致用户看起来“面板消失且再也打不开”。 + - 若同时触发业务侧的“隐藏邮件列表”(`.email-list-panel.hidden`),则左侧三个区域都不可恢复,交互卡死。 + +#### 根因定位 + +- `static/css/main.css` 在窄屏下仍保留历史规则:`.group-panel,.account-panel { position: fixed; left:-280px; }` +- 新的可调整布局系统(`static/js/layout-manager.js`)在折叠时把 Grid 列宽置 0,并依赖 **折叠指示器** 提供“展开入口”。 +- 当历史规则把面板整体移出屏幕时,折叠指示器作为面板子元素也一并移出,造成不可展开。 + +#### 修复方案 + +- 在布局系统初始化时,为 `` 添加标记类:`layout-system-enabled`。 +- 将窄屏历史侧栏滑入样式限定在 “布局系统未启用” 的场景: + - 仅对 `html:not(.layout-system-enabled)` 生效,避免影响 LayoutManager 的折叠/指示器逻辑。 + +#### 变更点 + +- `static/js/layout-bootstrap.js`:LayoutManager 初始化前添加 `layout-system-enabled`。 +- `static/css/main.css`:`@media (max-width: 768px)` 内的 `.group-panel/.account-panel` 固定定位规则增加 `html:not(.layout-system-enabled)` 限定。 + +#### 验证 + +- `npm test` 通过(11 suites / 93 tests)。 +- Playwright 窄屏复现验证(viewport=600): + - 修复前:groups/accounts 指示器 `x < 0`(不可见) + - 修复后:groups/accounts/emails 指示器分别落在 `x=0/48/96`(可见且可点击展开) + +--- + +### UI 问题修复:折叠后页面出现滚动条、指示器高度观感异常(“太长”) + +#### 现象 + +- 在桌面宽屏下手动折叠侧边栏(如:账号/邮件列表)后,页面右侧出现 **body 滚动条**。 +- 因 body 被撑出滚动高度,用户会看到主容器整体被滚动,折叠指示器与布局区域的高度/对齐出现“太长/不正确”的观感。 + +#### 根因定位 + +- 折叠时 Grid 列宽被置为 `0px`(layout-width),但我们仅用 `opacity:0` 隐藏面板内容: + - 面板内容仍参与布局计算; + - 在 `0` 宽度下文本/控件会被强制换行,导致折叠面板内部出现异常的“高度溢出”; + - 溢出内容默认 `overflow: visible`,会把 body 的滚动高度撑大。 + +#### 修复方案 + +- `static/css/layout.css`: + - `.main-container` 增加 `overflow: hidden;`,阻止溢出影响 body 滚动条; + - `.main-container > *` 增加 `min-height: 0;`,允许 Grid 子项在固定高度下正确收缩; + - `.resizable-panel.collapsed > :not(.collapsed-indicator)` 增加 `display: none;`,让折叠时隐藏内容**不参与布局与滚动高度计算**。 + +#### 验证 + +- Playwright 复现验证(宽屏,折叠 accounts/emails): + - 修复前:`documentElement.scrollHeight > clientHeight`(出现滚动条) + - 修复后:`documentElement.scrollHeight === clientHeight`(滚动条消失),折叠指示器高度与主容器一致 diff --git a/docs/TD/TD-可调整布局系统.md b/docs/TD/TD-可调整布局系统.md new file mode 100644 index 0000000..c0f5f40 --- /dev/null +++ b/docs/TD/TD-可调整布局系统.md @@ -0,0 +1,633 @@ +# TD - 可调整布局系统 + +## 文档信息 + +- **文档编号**: TD-00001 +- **创建日期**: 2026-02-26 +- **版本**: v1.0 +- **状态**: 草稿 +- **负责人**: 测试团队 +- **关联文档**: + - PRD-00001: PRD - 可调整布局系统 + - FD-00001: FD - 可调整布局系统 + - TDD-00001: TDD - 可调整布局系统 + +## 目录 + +1. [测试概述](#一测试概述) +2. [测试范围](#二测试范围) +3. [测试策略](#三测试策略) +4. [测试用例设计](#四测试用例设计) +5. [测试数据准备](#五测试数据准备) +6. [测试环境配置](#六测试环境配置) +7. [性能测试方案](#七性能测试方案) +8. [兼容性测试方案](#八兼容性测试方案) +9. [可访问性测试方案](#九可访问性测试方案) +10. [回归测试策略](#十回归测试策略) +11. [测试执行计划](#十一测试执行计划) +12. [缺陷管理流程](#十二缺陷管理流程) +13. [测试报告与度量](#十三测试报告与度量) + +## 一、测试概述 + +### 1.1 测试目标 + +验证可调整布局系统的功能完整性、性能指标、兼容性和可访问性,确保满足 PRD 中定义的所有需求和验收标准。 + +### 1.2 测试原则 + +1. **测试优先级**:P0 功能 100% 通过,P1 功能 ≥ 95% 通过,P2 功能 ≥ 90% 通过 +2. **测试覆盖率**:代码覆盖率 > 80%,功能覆盖率 100% +3. **自动化优先**:单元测试和集成测试 100% 自动化,系统测试 ≥ 70% 自动化 +4. **持续测试**:每次代码提交触发自动化测试,每日执行完整回归测试 + +### 1.3 测试类型 + +| 测试类型 | 覆盖范围 | 执行方式 | 执行频率 | +|---------|---------|---------|---------| +| 单元测试 | LayoutManager、StateManager 类方法 | 自动化(Jest) | 每次提交 | +| 集成测试 | 拖动、折叠、状态保存流程 | 自动化(Playwright) | 每次提交 | +| 系统测试 | 完整用户场景 | 自动化 + 手动 | 每日 | +| 性能测试 | 帧率、内存、加载时间 | 自动化(Lighthouse) | 每周 | +| 兼容性测试 | 4 个浏览器 × 功能模块 | 自动化(BrowserStack) | 每次发布前 | +| 可访问性测试 | WCAG 2.1 AA 合规性 | 自动化(axe-core) + 手动 | 每次发布前 | +| 回归测试 | 现有功能不受影响 | 自动化 + 手动 | 每次变更后 | + +### 1.4 验收标准 + +根据 PRD 定义的成功指标: + +- ✅ 用户可以在 3 秒内完成面板宽度调整 +- ✅ 用户可以在 1 秒内完成面板折叠/展开 +- ✅ 布局状态在页面刷新后 100% 恢复 +- ✅ 拖动调整时帧率保持在 60fps +- ✅ 所有 P0 功能测试用例 100% 通过 +- ✅ 所有 P1 功能测试用例 ≥ 95% 通过 +- ✅ 无 P0/P1 级别缺陷遗留 + +## 二、测试范围 + +### 2.1 功能模块测试范围 + +根据 FD 文档,测试范围包括 6 个主要功能模块: + +#### F1 - 拖动调整宽度(P0) + +| 子功能 | 优先级 | 测试点 | +|--------|--------|--------| +| F1.1 - 鼠标拖动调整 | P0 | 拖动流畅度、宽度实时更新、视觉反馈 | +| F1.2 - 宽度限制 | P0 | 最小/最大宽度限制、邮件详情面板 400px 保证 | +| F1.3 - 视觉反馈 | P1 | hover 效果、拖动光标、高亮指示器 | +| F1.4 - 性能优化 | P0 | requestAnimationFrame、60fps 保证 | + +#### F2 - 面板折叠/展开(P0) + +| 子功能 | 优先级 | 测试点 | +|--------|--------|--------| +| F2.1 - 折叠按钮 | P0 | 点击折叠、点击展开、按钮状态更新 | +| F2.2 - 折叠动画 | P1 | 200ms 过渡、平滑动画、无卡顿 | +| F2.3 - 折叠状态指示 | P1 | 细条显示、竖排文字、点击展开 | +| F2.4 - 折叠规则 | P0 | 至少一个面板展开、邮件详情不可折叠 | +| F2.5 - ARIA 属性更新 | P1 | aria-expanded 更新、aria-label 更新 | +| F2.6 - 折叠时隐藏 resizer | P0 | resizer 不可见、不可交互 | + +#### F3 - 恢复默认布局(P1) + +| 子功能 | 优先级 | 测试点 | +|--------|--------|--------| +| F3.1 - 恢复按钮 | P1 | 按钮位置、tooltip 显示 | +| F3.2 - 确认对话框 | P1 | 对话框内容、取消/确定按钮 | +| F3.3 - 恢复逻辑 | P1 | 宽度恢复、折叠状态恢复、localStorage 清除 | +| F3.4 - 恢复动画 | P2 | 300ms 过渡、平滑动画 | + +#### F4 - 状态持久化(P0) + +| 子功能 | 优先级 | 测试点 | +|--------|--------|--------| +| F4.1 - 保存面板宽度 | P0 | 拖动后保存、防抖 500ms | +| F4.2 - 保存折叠状态 | P0 | 折叠后立即保存 | +| F4.3 - 加载状态 | P0 | 页面加载时恢复、数据验证 | +| F4.4 - 多用户隔离 | P1 | 不同 userId 独立存储 | +| F4.5 - 页面卸载强制保存 | P0 | beforeunload 触发保存 | + +#### F5 - 窗口尺寸适配(P1) + +| 子功能 | 优先级 | 测试点 | +|--------|--------|--------| +| F5.1 - 断点自动折叠 | P1 | <1200px、<900px、<700px 自动折叠 | +| F5.2 - 窗口放大恢复 | P1 | 自动折叠的面板自动恢复 | +| F5.3 - 防抖优化 | P2 | 200ms 防抖、避免频繁触发 | + +#### F6 - 键盘可访问性(P1) + +| 子功能 | 优先级 | 测试点 | +|--------|--------|--------| +| F6.1 - Tab 聚焦 | P1 | resizer 可聚焦、折叠按钮可聚焦 | +| F6.2 - 方向键调整宽度 | P1 | ←/→ 调整、Shift+←/→ 快速调整 | +| F6.3 - Enter/Sp发 | P1 | 折叠按钮触发 | +| F6.4 - ARIA 属性 | P1 | role、aria-label、aria-valuenow 等 | + +### 2.2 非功能测试范围 + +#### 性能测试 + +- 拖动帧率 ≥ 60fps +- 折叠/展开动画流畅度 +- localStorage 数据 ≤ 1KB +- 页面加载时间增加 ≤ 50ms + +#### 兼容性测试 + +- Chrome 90+ +- Firefox 88+ +- Safari 14+ +- Edge 90+ + +#### 可访问性测试 + +- WCAG 2.1 AA 级别合规 +- 键盘导航完整性 +- 屏幕阅读器支持 + +#### 安全性测试 + +- localStorage 数据不包含敏感信息 +- XSS 防护验证 + +### 2.3 不在测试范围内 + +- 移动端适配(后续版本) +- 拖动排序功能(后续版本) +- 多套布局方案(后续版本) +- 布局分享功能(后续版本) + +## 三、测试策略 + +### 3.1 单元测试策略 + +**测试框架**: Jest + +**覆盖目标**: 代码覆盖率 > 80% + +**测试对象**: +- LayoutManager 类的所有公共方法 +- StateManager 类的所有公共方法 +- 核心算法(拖动宽度计算、防抖、状态序列化) + +**测试方法**: +- 正向测试:正常输入,验证正确输出 +- 负向测试:异常输入,验证错误处理 +- 边界测试:临界值输入,验证边界处理 + +**Mock 策略**: +- Mock localStorage API +- Mock DOM 元素 +- Mock 事件对象 + +### 3.2 集成测试策略 + +**测试框架**: Playwright + +**测试对象**: +- 拖动调整宽度的完整流程 +- 折叠/展开的完整流程 +- 状态保存和加载的完整流程 +- 窗口调整的完整流程 + +**测试方法**: +- 端到端测试:模拟真实用户操作 +- 流程测试:验证多个功能的协作 +- 数据驱动测试:使用不同的测试数据 + +### 3.3 系统测试策略 + +**测试方法**: 自动化 + 手动 + +**测试场景**: +- 用户首次访问(默认布局) +- 用户调整布局后刷新(状态恢复) +- 用户在不同设备间切换(多用户隔离) +- 用户调整窗口大小(自动适配) +- 用户使用键盘操作(可访问性) + +### 3.4 探索性测试策略 + +**测试时间**: 每个功能模块 2 小时 + +**测试重点**: +- 异常操作路径 +- 极端场景 +- 用户体验问题 +- 未预期的交互 + + +## 四、测试用例设计 + +### 4.1 测试用例编号规则 + +**格式**: TC-[模块]-[序号]-[类型] + +- **模块**: F1~F6(对应功能模块) +- **序号**: 001~999(三位数字) +- **类型**: P(正向)、N(负向)、B(边界)、PF(性能) + +**示例**: TC-F1-001-P(F1 模块第 1 个正向用例) + +### 4.2 F1 - 拖动调整宽度测试用例 + +#### TC-F1-001-P: 拖动分组面板到中间宽度 + +| 项目 | 内容 | +|------|------| +| **优先级** | P0 | +| **前置条件** | 页面已加载,分组面板宽度为默认 200px | +| **测试步骤** | 1. 鼠标悬停在分组面板右侧 resizer
2. 按下鼠标左键
3. 向右拖动 50px
4. 释放鼠标 | +| **预期结果** | 1. resizer 显示 hover 效果(背景色变化)
2. 光标变为 col-resize
3. 面板宽度实时更新为 250px
4. 拖动流畅,无卡顿 | +| **关联需求** | F1.1, F1.3 | + +#### TC-F1-002-B: 拖动分组面板到最小宽度 + +| 项目 | 内容 | +|------|------| +| **优先级** | P0 | +| **前置条件** | 页面已加载,分组面板宽度为默认 200px | +| **测试步骤** | 1. 鼠标按下 resizer
2. 向左拖动 100px(超过最小宽度限制)
3. 释放鼠标 | +| **预期结果** | 面板宽度停止在最小宽度 150px,不会继续缩小 | +| **关联需求** | F1.2 | + +#### TC-F1-003-B: 拖动分组面板到最大宽度 + +| 项目 | 内容 | +|------|------| +| **优先级** | P0 | +| **前置条件** | 页面已加载,分组面板宽度为默认 200px | +| **测试步骤** | 1. 鼠标按下 resizer
2. 向右拖动 300px(超过最大宽度限制)
3. 释放鼠标 | +| **预期结果** | 面板宽度停止在最大宽度 400px,不会继续增大 | +| **关联需求** | F1.2 | + +#### TC-F1-004-B: 拖动时确保邮件详情面板至少 400px + +| 项目 | 内容 | +|------|------| +| **优先级** | P0 | +| **前置条件** | 窗口宽度 1200px,所有面板展开 | +| **测试步骤** | 1. 拖动分组面板到最大宽度 400px
2. 拖动账号面板到最大宽度 500px
3. 尝试拖动邮件列表面板到最大宽度 600px | +| **预期结果** | 邮件列表面板宽度被限制,确保邮件详情面板至少保持 400px | +| **关联需求** | F1.2 | + +#### TC-F1-005-N: 快速拖动导致鼠标移出窗口 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 页面已加载 | +| **测试步骤** | 1. 鼠标按下 resizer
2. 快速向右拖动,鼠标移出浏览器窗口
3. 在窗口外释放鼠标
4. 鼠标移回窗口 | +| **预期结果** | 1. 拖动正常结束
2. 光标恢复正常
3. 状态已保存 | +| **关联需求** | F1.1 | + +#### TC-F1-006-PF: 拖动帧率测试 + +| 项目 | 内容 | +|------|------| +| **优先级** | P0 | +| **前置条件** | 页面已加载 | +| **测试步骤** | 1. 使用 Performance API 监听帧率
2. 连续拖动 1 秒
3. 计算平均帧率 | +| **预期结果** | 平均帧率 ≥ 60fps(允许 5fps 误差) | +| **关联需求** | F1.4 | + +### 4.3 F2 - 面板折叠/展开测试用例 + +#### TC-F2-001-P: 点击折叠按钮折叠面板 + +| 项目 | 内容 | +|------|------| +| **优先级** | P0 | +| **前置条件** | 分组面板处于展开状态 | +| **测试步骤** | 1. 点击分组面板右上角的折叠按钮 | +| **预期结果** | 1. 面板宽度变为 0
2. 面板内容不可见
3. 显示折叠指示条(48px 宽)
4. 折叠动画流畅(200ms)
5. 按钮图标变为 →
6. aria-expanded 更新为 false | +| **关联需求** | F2.1, F2.2, F2.3, F2.5 | + +#### TC-F2-002-P: 点击折叠指示条展开面板 + +| 项目 | 内容 | +|------|------| +| **优先级** | P0 | +| **前置条件** | 分组面板处于折叠状态 | +| **测试步骤** | 1. 点击折叠指示条 | +| **预期结果** | 1. 面板恢复到折叠前的宽度
2. 面板内容可见
3. 折叠指示条消失
4. 展开动画流畅(200ms)
5. 按钮图标变为 ←
6. aria-expanded 更新为 true | +| **关联需求** | F2.1, F2.2, F2.3, F2.5 | + +#### TC-F2-003-N: 尝试折叠所有面板 + +| 项目 | 内容 | +|------|------| +| **优先级** | P0 | +| **前置条件** | 只有分组面板展开,其他面板已折叠 | +| **测试步骤** | 1. 点击分组面板的折叠按钮 | +| **预期结果** | 1. 显示提示信息:"至少需要保持一个面板展开"
2. 分组面板保持展开状态 | +| **关联需求** | F2.4 | + +#### TC-F2-004-P: 折叠后 resizer 不可见 + +| 项目 | 内容 | +|------|------| +| **优先级** | P0 | +| **前置条件** | 分组面板处于展开状态 | +| **测试步骤** | 1. 折叠分组面板
2. 检查 resizer 元素 | +| **预期结果** | 1. resizer 不可见(display: none)
2. resizer 不可交互(pointer-events: none) | +| **关联需求** | F2.6 | + +#### TC-F2-005-N: 折叠状态下尝试拖动 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 分组面板处于折叠状态 | +| **测试步骤** | 1. 尝试在折叠指示条位置拖动 | +| **预期结果** | 无法拖动,必须先展开面板 | +| **关联需求** | F2.6 | + +### 4.4 F3 - 恢复默认布局测试用例 + +#### TC-F3-001-P: 恢复默认布局 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 用户已调整布局(宽度和折叠状态) | +| **测试步骤** | 1. 点击顶部导航栏的"恢复默认布局"按钮
2. 在确认对话框中点击"确定" | +| **预期结果** | 1. 分组面板宽度恢复为 200px
2. 账号面板宽度恢复为 260px
3. 邮件列表面板宽度恢复为 380px
4. 所有面板展开
5. localStorage 中的状态被清除
6. 恢复动画流畅(300ms) | +| **关联需求** | F3.1, F3.2, F3.3, F3.4 | + +#### TC-F3-002-N: 取消恢复默认布局 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 用户已调整布局 | +| **测试步骤** | 1. 点击"恢复默认布局"按钮
2. 在确认对话框中点击"取消" | +| **预期结果** | 1. 对话框关闭
2. 布局保持当前状态不变 | +| **关联需求** | F3.2 | + +### 4.5 F4 - 状态持久化测试用例 + +#### TC-F4-001-P: 拖动后保存状态 + +| 项目 | 内容 | +|------|------| +| **优先级** | P0 | +| **前置条件** | 页面已加载 | +| **测试步骤** | 1. 拖动分组面板到 250px
2. 等待 500ms(防抖时间)
3. 检查 localStorage | +| **预期结果** | localStorage 中保值 "250px" | +| **关联需求** | F4.1 | + +#### TC-F4-002-P: 折叠后立即保存状态 + +| 项目 | 内容 | +|------|------| +| **优先级** | P0 | +| **前置条件** | 页面已加载 | +| **测试步骤** | 1. 折叠分组面板
2. 立即检查 localStorage(无需等待) | +| **预期结果** | localStorage 中保存了折叠状态 collapsed: true | +| **关联需求** | F4.2 | + +#### TC-F4-003-P: 页面刷新后恢复状态 + +| 项目 | 内容 | +|------|------| +| **优先级** | P0 | +| **前置条件** | 用户已调整布局并保存 | +| **测试步骤** | 1. 刷新页面(F5)
2. 等待页面加载完成 | +| **预期结果** | 1. 面板宽度恢复到调整后的值
2. 折叠状态恢复
3. 恢复过程无闪烁 | +| **关联需求** | F4.3 | + +#### TC-F4-004-P: 多用户布局隔离 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 系统支持多用户 | +| **测试步骤** | 1. 用户 A 登录,调整布局
2. 用户 A 退出
3. 用户 B 登录
4. 检查布局状态 | +| **预期结果** | 用户 B 看到的是默认布局,不受用户 A 的影响 | +| **关联需求** | F4.4 | + +#### TC-F4-005-P: 页面卸载时强制保存 + +| 项目 | 内容 | +|------|------| +| **优先级** | P0 | +| **前置条件** | 页面已加载 | +| **测试步骤** | 1. 拖动面板
2. 立即关闭浏览器标签页(不等待防抖)
3. 重新打开页面 | +| **预期结果** | 布局状态已保存,最后一次调整未丢失 | +| **关联需求** | F4.5 | + +#### TC-F4-006-N: localStorage 不可用 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 浏览器禁用 localStorage 或隐私模式 | +| **测试步骤** | 1. 加载页面
2. 调整布局
3. 刷新页面 | +| **预期结果** | 1. 页面正常加载,使用默认布局
2. 调整布局功能正常
3. 刷新后恢复默认布局(无法持久化)
4. 控制台显示警告n| **关联需求** | F4.3 | + +#### TC-F4-007-N: localStorage 数据损坏 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | localStorage 中存在损坏的数据 | +| **测试步骤** | 1. 手动修改 localStorage 数据为无效 JSON
2. 刷新页面 | +| **预期结果** | 1. 页面正常加载
2. 使用默认布局
3. 损坏的数据被清除
4. 控制台显示错误信息 | +| **关联需求** | F4.3 | + +### 4.6 F5 - 窗口尺寸适配测试用例 + +#### TC-F5-001-P: 窗口宽度 < 1200px 自动折叠分组面板 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 窗口宽度 > 1200px,所有面板展开 | +| **测试步骤** | 1. 调整浏览器窗口宽度到 1100px
2. 等待 200ms(防抖时间) | +| **预期结果** | 1. 分组面板自动折叠
2. 面板标记为 autoCollapsed
3. 其他面板保持展开 | +| **关联需求** | F5.1 | + +#### TC-F5-002-P: 窗口宽度 < 900px 自动折叠账号面板 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 窗口宽度 > 900px | +| **测试步骤** | 1. 调整浏览器窗口宽度到 800px
2. 等待 200ms | +| **预期结果** | 1. 分组面板和账号面板自动折叠
2. 邮件列表和详情面板保持展开 | +| **关联需求** | F5.1 | + +#### TC-F5-003-P: 窗口宽度 < 700px 自动折叠邮件列表面板 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 窗口宽度 > 700px | +| **测试步骤** | 1. 调整浏览器窗口宽度到 600px
2. 等待 200ms | +| **预期结果** | 1. 所有侧边栏面板自动折叠
2. 只显示邮件详情面板 | +| **关联需求** | F5.1 | + +#### TC-F5-004-P: 窗口放大后自动恢复面板 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 窗口宽度 < 1200px,分组面板自动折叠 | +| **测试步骤** | 1. 调整浏览器窗口宽度到 1300px
2. 等待 200ms | +| **预期结果** | 1. 分组面板自动展开
2. autoCd 标记被移除 | +| **关联需求** | F5.2 | + +#### TC-F5-005-N: 用户手动折叠的面板不自动恢复 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 窗口宽度 > 1200px | +| **测试步骤** | 1. 用户手动折叠分组面板
2. 调整窗口宽度到 1100px 再恢复到 1300px | +| **预期结果** | 分组面板保持折叠状态(因为是用户手动折叠) | +| **关联需求** | F5.2 | + +### 4.7 F6 - 键盘可访问性测试用例 + +#### TC-F6-001-P: Tab 键聚焦到 resizer + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 页面已加载 | +| **测试步骤** | 1. 按 Tab 键多次,直到聚焦到分组面板的 resizer | +| **预期结果** | 1. resizer 获得焦点
2. 显示明显的视觉反馈(outline 或高亮边框) | +| **关联需求** | F6.1 | + +#### TC-F6-002-P: 方向键调整宽度 + +| 项目 | 内容 | +|---------| +| **优先级** | P1 | +| **前置条件** | resizer 已聚焦,面板宽度 200px | +| **测试步骤** | 1. 按右箭头键 5 次
2. 按左箭头键 3 次 | +| **预期结果** | 1. 每次按右箭头,宽度增加 10px(最终 250px)
2. 每次按左箭头,宽度减少 10px(最终 220px)
3. aria-valuenow 实时更新 | +| **关联需求** | F6.2, F6.4 | + +#### TC-F6-003-P: Shift + 方向键快速调整 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | resizer 已聚焦,面板宽度 200px | +| **测试步骤** | 1. 按 Shift + 右箭头键 2 次 | +| **预期结果** | 每次按键宽度增加 50px(最终 300px) | +| **关联需求** | F6.2 | + +#### TC-F6-004-P: Enter/Space 触发折叠按钮 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 折叠按钮已聚焦 | +| **测试步骤** | 1. 按 Enter 键 | +| **预期结果** | 面板折叠 | +| **关联需求** | ### TC-F6-005-P: ARIA 属性完整性 + +| 项目 | 内容 | +|------|------| +| **优先级** | P1 | +| **前置条件** | 页面已加载 | +| **测试步骤** | 1. 检查 resizer 元素的 ARIA 属性 | +| **预期结果** | 1. role="separator"
2. aria-orientation="vertical"
3. aria-label="调整[面板名称]宽度"
4. aria-valuenow、aria-valuemin、aria-valuemax 正确设置 | +| **关联需求** | F6.4 | + + +## 九、可访问性测试方案 + +### 9.1 WCAG 2.1 AA 合规性测试 + +使用 axe-core 自动检测可访问性问题。 + +### 9.2 键盘导航测试 + +- Tab 键可以聚焦到所有交互元素 +- 方向键可以调整面板宽度 +- Enter/Space 可以触发折叠按钮 +- 所有操作可通过键盘完成 + +### 9.3 屏幕阅读器测试 + +使用 NVDA/JAWS 测试 ARIA 属性是否正确朗读。 + +## 十、回归测试策略 + +### 10.1 回归测试范围 + +确保布局系统不影响现有功能: +- 邮件列表显示和滚动 +- 邮件详情查看 +- 账号切换 +- 分组管理 +- Token 刷新 +- 临时邮箱 + +### 10.2 回归测试执行 + +每次代码变更后执行完整回归测试。 + +## 十一、测试执行计划 + +### 11.1 测试阶段 + +1. **阶段 1 - 单元测试**(开发完成后) +2. **阶段 2 - 集成测试**(功能集成后) +3. **阶段 3 - 系统测试**(所有功能完成后) +4. **阶段 4 - 验收测试**(测试环境部署后) + +### 11.2 测试时间安排 + +- 单元测试:2 天 +- 集成测试:3 天 +- 系统测试:3 天 +- 性能测试:1 天 +- 兼容性测试:2 天 +- 可访问性测试:1 天 +- 回归测试:2 天 +- **总计**:14 天 + +## 十二、缺陷管理流程 + +### 12.1 缺陷优先级 + +- **P0(阻塞)**:核心功能无法使用,必须立即修复 +- **P1(严重)**:重要功能异常,当天修复 +- **P2(一般)**:次要功能问题,3 天内修复 +- **P3(轻微)**:UI 问题或优化建议,可延后 + +### 12.2 缺陷跟踪 + +使用 GitHub Issues 跟踪所有缺陷。 + +## 十三、测试报告与度量 + +### 13.1 测试报告内容 + +1. 测试执行摘要 +2. 功能模块测试结果 +3. 缺陷分布统计 +4. 性能测试结果 +5. 兼容性测试结果 +6. 可访问性测试结果 +7. 风险评估和建议 + +### 13.2 度量指标 + +- 测试覆盖率:代码覆盖率 > 80%,功能覆盖率 100% +- 缺陷密度:< 5 个/KLOC +- 缺陷修复率:P0/P1 100%,P2 > 95% +- 回归通过率:> 98% + +--- + +**文档结束** diff --git a/docs/TDD/TDD-可调整布局系统.md b/docs/TDD/TDD-可调整布局系统.md new file mode 100644 index 0000000..87d1d31 --- /dev/null +++ b/docs/TDD/TDD-可调整布局系统.md @@ -0,0 +1,1163 @@ +# TDD - 可调整布局系统 + +## 文档信息 + +- **文档编号**: TDD-00001 +- **创建日期**: 2026-02-26 +- **版本**: v1.0 +- **状态**: 草稿 +- **负责人**: 开发团队 +- **关联文档**: + - PRD-00001: PRD - 可调整布局系统 + - FD-00001: FD - 可调整布局系统 + +## 目录 + +1. [技术架构设计](#一技术架构设计) +2. [核心算法设计](#二核心算法设计) +3. [类与方法设计](#三类与方法设计) +4. [数据结构设计](#四数据结构设计) +5. [事件流设计](#五事件流设计) +6. [性能优化策略](#六性能优化策略) +7. [错误处理与边界情况](#七错误处理与边界情况) +8. [集成方案](#八集成方案) +9. [测试策略](#九测试策略) +10. [兼容性方案](#十兼容性方案) + +## 一、技术架构设计 + +### 1.1 整体架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ HTML Layer │ +│ ┌──────────┬──────────┬──────────┬────────────────────┐ │ +│ │ 分组面板 │ 账号面板 │ 邮件列表 │ 邮件详情面板 │ │ +│ │ [resizer]│ [resizer]│ [resizer]│ │ │ +│ └──────────┴──────────┴──────────┴────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ CSS Layer │ +│ • CSS Grid 布局系统 │ +│ • CSS 变量(--group-panel-width, --account-panel-width) │ +│ • CSS Transitions(折叠/展开动画) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ JavaScript Layer │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ LayoutManager │◄──────►│ StateManager │ │ +│ │ • 拖动处理 │ │ • 状态保存 │ │ +│ │ • 折叠控制 │ │ • 状态加载 │ │ +│ │ • 键盘支持 │ │ • 数据验证 │ │ +│ │ • 窗口适配 │ │ • 版本迁移 │ │ +│ └──────────────────┘ └──────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Storage Layer │ +│ localStorage: outlook_layout_state_${userId} │ +│ { │ +│ version: "1.1", │ +│ userId: "user123", │ +│ timestamp: 1709020800000, │ +│ panels: { groups: {...}, accounts: {...}, emails: {...} }│ +│ } │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 1.2 模块依赖关系 + +``` +main.js + ├─► LayoutManager + │ ├─► StateManager (状态持久化) + │ ├─► DragHandler (拖动处理) + │ ├─► CollapseHandler (折叠处理) + │ ├─► KeyboardHandler (键盘支持) + │ └─► WindowResizeHandler (窗口适配) + │ + └─► StateManager + ├─► localStorage API + ├─► JSON.parse/stringify + └─► 数据验证逻辑 +``` + +### 1.3 技术选型 + +| 技术点 | 选型 | 理由 | +|--------|------|------| +| 布局系统 | CSS | 灵活、性能好、浏览器支持广泛 | +| 状态管理 | CSS 变量 + localStorage | 简单、无需框架、易于调试 | +| 动画 | CSS Transitions | 硬件加速、性能优秀 | +| 事件处理 | 原生 JavaScript | 无依赖、轻量级 | +| 性能优化 | requestAnimationFrame | 保证 60fps 流畅度 | +| 防抖 | 自定义 debounce | 减少 localStorage 写入频率 | + +### 1.4 文件结构 + +``` +static/ +├── css/ +│ ├── main.css # 现有样式(需修改) +│ └── layout.css # 新增:布局系统样式 +├── js/ +│ ├── main.js # 现有脚本(需修改) +│ ├── layout-manager.js # 新增:布局管理器 +│ └── state-manager.js # 新增:状态管理器 +templates/ +└── index.html # 主页面(需修改) +``` + + +## 二、核心算法设计 + +### 2.1 拖动宽度计算算法 + +#### 算法描述 + +计算拖动过程中面板的新宽度,并应用最小/最大宽度限制。 + +#### 伪代码 + +``` +function calculateNewWidth(startX, currentX, startWidth, panelType): + // 1. 计算拖动距离 + delta = currentX - startX + + // 2. 计算新宽度 + newWidth = startWidth + delta + + // 3. 获取最小/最大宽度限制 + minWidth = getMinWidth(panelType) + maxWidth = getMaxWidth(panelType) + + // 4. 应用面板自身的宽度限制 + newWidth = clamp(newWidth, minWidth, maxWidth) + + // 5. 确保邮件详情面板至少 400px + containerWidth = getContainerWidth() + otherPanelsWidth = calculateOtherPanelsWidth(panelType) + maxAllowedWidth = containerWidth - otherPanelsWidth - 400 + + // 6. 应用全局宽度限制 + newWidth = min(newWidth, maxAllowedWidth) + + return newWidth + +function clamp(value, min, max): + return max(min, min(value, max)) +``` + +#### 时间复杂度 + +- **时间复杂度**: O(1) +- **空间复杂度**: O(1) + +#### 边界情况 + +1. **拖动到最小宽度**: 当 `newWidth < minWidth` 时,返回 `minWidth` +2. **拖动到最大宽度**: 当 `newWidth > maxWidth` 时,返回 `maxWidth` +3. **邮件详情面板不足 400px**: 限制其他面板的最大宽度 +4. **窗口宽度不足**: 自动折叠面板(由窗口适配算法处理) + +### 2.2 防抖算法 + +#### 算法描述 + +延迟执行函数,在指定时间内如果再次调用则重置计时器。用于减少 localStorage 写入频率。 + +#### 伪代码 + +``` +function debounce(func, delay): + let timerId = null + + return function(...args): + // 清除之前的计时器 + if (timerId !== null): + clearTimeout(timerId) + + // 设置新的计时器 + timerId = setTimeout(() => { + func.apply(this, args) + timerId = null + }, delay) + + // 提供立即执行方法(用于 beforeunload) + return { + debounced: debouncedFunc, + flush: () => { + if (timerId !== null): + clearTimeout(timerId) + func.apply(this, args) + timerId = null + } + } +``` + +#### 使用场景 + +1. **拖动结束保存**: 延迟 500ms 保存状态 +2. **窗口调整**: 延迟 200ms 处理窗口大小变化 +3. **页面卸载**: 立即执行 `flush()` 强制保存 + +### 2.3 状态序列化算法 + +#### 算法描述 + +将布局状态序列化为 JSON 并保存到 localStorage,支持版本控制和数据验证。 + +#### 伪代码 + +``` +function serializeState(userId): + state = { + version: "1.1", + userId: userId, + timestamp: Date.now(), + panels: {} + } + + // 遍历所有面板 + for panelType in ['groups', 'accounts', 'emails']: + panel = getPanel(panelType) + state.panels[panelType] = { + width: getCSSVariable(`--${panelType}-panel-width`), + collapsed: panel.classList.contains('collapsed') + } + + // 序列化为 JSON + jsonString = JSON.stringify(state) + + // 保存到 localStorage + storageKey look_layout_state_${userId}` + localStorage.setItem(storageKey, jsonString) + + return state + +function deserializeState(userId): + storageKey = `outlook_layout_state_${userId}` + jsonString = localStorage.getItem(storageKey) + + if (jsonString === null): + return null + + try: + state = JSON.parse(jsonString) + + // 数据验证 + if (!validateState(state)): + return null + + // 版本迁移 + if (state.version !== "1.1"): + state = migrateState(state) + + return state + catch or): + console.error("Failed to deserialize state:", error) + return null + +function validateState(state): + // 检查必需字段 + if (!state.version || !state.panels): + return false + + // 检查面板数据 + for panelType in ['groups', 'accounts', 'emails']: + panel = state.panels[panelType] + if (!panel || !panel.width || typeof panel.collapsed !== 'boolean'): + return false + + return true +``` + +#### 数据格式 + +```json +{ + "version": "1.1", + "userId": "user123", + "timestamp": 1709020800000, + "panels": { + "groups": { + "width": "200px", + "collapsed": false + }, + "accounts": { + "width": "260px", + "collapsed": false + }, + "emails": { + "width": "380px", + "collapsed": false + } + } +} +``` + +### 2.4 窗口适配算法 + +#### 算法描述 + +根据窗口宽度自动折叠面板,确保邮件详情面板始终可见。 + +#### 伪代码 + +``` +function adaptToWindowSize(windowWidth): + // 定义断点 + breakpoints = { + COLLAPSE_GROUPS: 1200, + COLLAPSE_ACCOUNTS: 900, + COLLAPSE_EMAILS: 700 + } + + // 自动折叠逻辑 + if (windowWidth < breakpoints.COLLAPSE_GROUPS): + collapsePanel('groups', auto=true) + els // 如果窗口足够大,恢复之前的状态 + restorePanelIfNeeded('groups') + + if (windowWidth < breakpoints.COLLAPSE_ACCOUNTS): + collapsePanel('accounts', auto=true) + else: + restorePanelIfNeeded('accounts') + + if (windowWidth < breakpoints.COLLAPSE_EMAILS): + collapsePanel('emails', auto=true) + else: + restorePanelIfNeeded('emails') + + // 确保邮件详情面板至少 400px + ensureDetailPanelMinWidth(400) + +function restorePanelIfNeeded(panelType): + // 只恢复用户手动折叠的面板 + // 如果是自动折叠的,则恢复 + panel = getPanel(panelType) + if (panel.dataset.autoCollapsed === 'true'): + expandPanel(panelType) + delete panel.dataset.autoCollapsed +``` + +#### 防抖优化 + +窗口调整事件使用 200ms 防抖,避免频繁触发。 + +```javascript +window.addEventListener('resize', debounce(handleWindowResize, 200)); +``` + + +## 三、类与方法设计 + +### 3.1 LayoutManager 类 + +#### 类职责 + +管理面板布局,处理拖动、折叠、键盘操作和窗口适配。 + +#### 类图 + +#### 核心方法说明 + +**constructor()**: 初始化实例变量和 StateManager,创建防抖保存函数 + +**init()**: 初始化布局管理器,加载保存的状态,绑定所有事件监听器 + +**startResize(e)**: 开始拖动,记录初始位置和宽度,设置全局光标 + +**resize(e)**: 拖动过程中更新宽度,使用 requestAnimationFrame 优化性能 + +**stopResize()**: 结束拖动,恢复光标,保存状态(防抖) + +**handleKeyboard(e)**: 处理键盘操作(方向键调整宽度) + +**togglePanel(panelType)**: 切换面板折叠状态 + +**resetLayout()**: 恢复默认布局,清除保存的状态 + +**handleWindowResize()**: 处理窗口大小变化,自动折叠/恢复面板 + +**updatePanelWidth(panel, width)**: 更新面板宽度(修改 CSS 变量) + +**saveState()**: 保存当前布局状态到 localStorage + +**loadState()**: 从 localStorage 加载布局状态 + + +### 3.2 StateManager 类 + +#### 类职责 + +管理布局状态的持久化,包括保存、加载、验证和版本迁移。 + +#### 类图 + +#### 详细方法设计 + +##### save(userId, state) + +##### load(userId) + +##### clear(userId) + +##### validate(state) + +##### migrate(state) + +##### getStorageKey(userId) + +\outlook_layout_state_ +## 四、数据结构设计 + +### 4.1 布局状态对象 + +```typescript +interface LayoutState { + version: string; // 版本号(当前为 "1.1") + userId: string; // 用户 ID + timestamp: number; // 保存时间戳 + panels: { + groups: PanelState; + accounts: PanelState; + emails: PanelState; + }; +} + +interface PanelState { + width: string; // 宽度(如 "200px") + collapsed: boolean; // 是否折叠 +} +``` + +### 4.2 CSS 变量映射 + +```css +:root { + /* 面板宽度 */ + --group-panel-width: 200px; + --account-panel-width: 260px; + --email-list-panel-width: 380px; + + /* 宽度限制 */ + --group-panel-min-width: 150px; + --group-panel-max-width: 400px; + --account-panel-min-width: 180px; + --account-panel-max-width: 500px; + --email-list-panel-min-width: 280px; + --email-list-panel-max-width: 600px; + --email-detail-panel-min-width: 400px; + + /* 其他 */ + --resizer-width: 4px; + --panel-transition-duration: 200ms; +} +``` + +## 五、事件流设计 + +### 5.1 拖动事件流 + +``` +用户操作 事件触发 系统响应 +──────────────────────────────────────────────────────────────── +鼠标按下 resizer mousedown startResize() + │ │ ├─ 记录初始位置 + │ │ ├─ 设置全局光标 + │ │ └─ 阻止默认行为 + │ + ├─ 鼠标移动 mousemove resize() + │ │ │ ├─ requestAnimationFrame + │ │ │ ├─ 计算新宽度 + │ │ │ ├─ 应用宽度限制 + │ │ │ ├─ 更新 CSS 变量 + │ │ │ └─ 更新 ARIA 属性 + │ │ + │ └─ 重复... + │ + └─ 鼠标释放 mouseup stopResize() + │ ├─ 恢复光标 + │ ├─ 保存状态(防抖 500ms) + │ └─ 清理状态 +``` + +### 5.2 折叠事件流 + +``` +用户操作 事件触发 系统响应 +──────────────────────────────────────────────────────────────── +点击折叠按钮 click togglePanel() + │ │ ├─ 检查当前状态 + │ │ └─ 调用 collapse/expand + │ + ├─ 折叠 collapsePanel() + │ │ ├─ 添加 .collapsed 类 + │ │ ├─ CSS transition (200ms) + │ │ ├─ 更新 ARIA 属性 + │ │ └─ 立即保存状态 + │ + └─ 展开 expandPanel() + │ ├─ 移除 .collapsed 类 + │ ├─ CSS transition (200ms) + │ ├─ 恢复宽度 + │ ├─ 更新 ARIA 属性 + │ └─ 立即保存状态 +``` + +### 5.3 窗口调整事件流 + +``` +窗口大小变化 resize (防抖 200ms) handleWindowResize() + │ │ ├─ 获取窗口宽度 + │ │ ├─ 检查断点 + │ │ ├─ 自动折叠/恢复面板 + │ │ └─ 标记 autoCollapsed +``` + +### 5.4 页面生命周期事件流 + +``` +页面加载 DOMContentLoaded init() + │ │ ├─ 创建 LayoutManager + │ │ ├─ 加载保存的状态 + │ │ ├─ 应用到 CSS 变量 + │ │ ├─ 绑定事件监听器 + │ │ └─ 初始窗口适配 + │ + └─ 页面卸载 beforeunload saveNow() + │ ├─ 取消防抖计时器 + │ ├─ 立即保存状态 + │ └─ 确保不丢失数据 +``` + +## 六、性能优化策略 + +### 6.1 拖动性能优化 + +#### 使用 requestAnimationFrame + +```javascript +resize(e) { + if (!this.isResizing) return; + + requestAnimationFrame(() => { + // 宽度计算和更新 + const newWidth = this.calculateNewWidth(e); + this.updatePanelWidth(this.currentPanel, newWidth); + }); +} +``` + +**优势**: +- 与浏览器刷新率同步(60fps) +- 避免不必要的重绘 +- 自动节流 + +#### 避免布局抖动(Layout Thrashing) + +```javascript +// ❌ 错误:读写交替导致强制同步布局 +function badResize() { + const width1 = panel1.offsetWidth; // 读 + panel1.style.width = width1 + 10; // 写 + const width2 = panel2.offse // 读(触发强制布局) + panel2.style.width = width2 + 10; // 写 +} + +// ✅ 正确:批量读取,批量写入 +function goodResize() { + // 批量读取 + const width1 = panel1.offsetWidth; + const width2 = panel2.offsetWidth; + + // 批量写入 + panel1.style.width = width1 + 10; + panel2.style.width = width2 + 10; +} +``` + +### 6.2 CSS 性能优化 + +#### 使用 CSS 变量 + +```css +/* ✅ 使用 CSS 变量,只需修改一次 */ +.group-panel { + width: var(--group-panel-width); +} + +/* ❌ 直接修改 style,触发重排 */ +element.style.width = '200px'; +``` + +#### 使用 transform 和 opacity + +```css +/* ✅ 使用 transform,GPU 加速 */ +.panel.collapsed { + transform: translateX(-100%); + opacity: 0; +} + +/* ❌ 使用 width,触发重排 */ +.panel.collapsed { + width: 0; +} +``` + +#### 使用 will-change 提示 + +```css +.resizer { + will-change: background-color; +} + +.panel { + will-change: transform, opacity; +} +``` + +### 6.3 防抖优化 + +```javascript +// 拖动结束保存:500ms 防抖 +this.saveDebounced = debounce(() => this.saveState(), 500); + +// 窗口调整:200ms 防抖 +window.addEventListener('resize', debounce(this.handleWindowResize, 200)); +``` + +**效果**: +- 减少 localStorage 写入次数 +- 减少窗口调整处理次数 +- 提升整体性能 + +### 6.4 事件委托 + +```javascript +// ✅ 使用事件委托 +document.querySelector('.main-container').addEventListener('click', (e) => { + if (e.target.classList.collapse-btn')) { + const panelType = e.target.dataset.panel; + this.togglePanel(panelType); + } +}); + +// ❌ 为每个按钮绑定事件 +document.querySelectorAll('.collapse-btn').forEach(btn => { + btn.addEventListener('click', () => { /* ... */ }); +}); +``` + +### 6.5 内存优化 + +```javascript +// 清理事件监听器 +destroy() { + // 移除全局事件监听器 + document.removeEventListener('mousemove', this.resize); + document.removeEventListener('mouseup', this.stopResize); + window.removeEventListener('resize', this.handleWindowResize); + window.removeEventListener('beforeunload', this.saveNow); + + // 清除计时器 + if (this.saveTimer) { + clearTimeout(this.saveTimer); + } +} +``` + + +## 七、错误处理与边界情况 + +### 7.1 拖动边界情况 + +#### 情况 1:拖动到最小宽度 + +```javascript +// 处理:限制在最小宽度 +if (newWidth < minWidth) { + newWidth = minWidth; +} +``` + +#### 情况 2:拖动到最大宽度 + +```javascript +// 处理:限制在最大宽度 +if (newWidth > maxWidth) { + newWidth = maxWidth; +} +``` + +#### 情况 3:邮件详情面板不足 400px + +```javascript +// 处理:限制其他面板的最大宽度 +const containerWidth = document.querySelector('.main-container').offsetWidth; +const otherPanelsWidth = this.calculateOtherPanelsWidth(panelType); +const maxAllowedWidth = containerWidth - otherPanelsWidth - 400; +newWidth = Math.min(newWidth, maxAllowedWidth); +``` + +#### 情况 4:快速拖动导致鼠标移出窗口 + +```javascript +// 处理:监听全局 mouseup 事件 +document.addEventListener('mouseup', this.stopResize.bind(this)); +``` + +### 7.2 折叠边界情况 + +#### 情况 1:所有面板都折叠 + +```javascript +// 处理:至少保持一个面板展开 +togglePanel(panelType) { + const expandedPanels = this.getExpandedPanels(); + + if (expandedPanels.length === 1 && expandedPanels[0] === panelType) { + alert('至少需要保持一个面板展开'); + return; + } + + // 继续折叠... +} +``` + +#### 情况 2:折叠后拖动 + +```javascript +// 处理:隐藏 resizer +.panel.collapsed .resizer { + display: none; + pointer-events: none; +} +``` + +### 7.3 localStorage 错误处理 + +#### 情况 1:localStorage 不可用 + +```javascript +save(userId, state) { + try { + localStorage.setItem(key, value); + return true; + } catch (error) { + console.warn('localStorage not available, using default layout'); + return false; + } +} +``` + +#### 情况 2:QuotaExceededError + +```javascript +save(userId, state) { + try { + localStorage.setItem(key, value); + } catch (error) { + if (error.name === 'QuotaExceededError') { + // 清理旧数据 + this.clearOldStates(); + // 重试 + localStorage.setItem(key, value); + } + } +} +``` + +#### 情况 3:数据损坏 + +```javascript +load(userId) { + try { + const state = JSON.parse(jsonString); + + if (!this.validate(state)) { + console.warn('Invalid state, using default'); + this.clear(userId); + return null; + } + + return state; + } catch (error) { + console.error('Failed to parse state:', error); + this.clear(userId); + returnn} +``` + +### 7.4 窗口调整边界情况 + +#### 情况 1:窗口宽度小于 700px + +```javascript +// 处理:自动折叠所有侧边栏,只显示邮件详情 +if (windowWidth < 700) { + this.collapsePanel('groups', true); + this.collapsePanel('accounts', true); + this.collapsePanel('emails', true); +} +``` + +#### 情况 2:快速调整窗口大小 + +```javascript +// 处理:使用防抖(200ms) +window.addEventListener('resize', debounce(this.handleWindowResize, 200)); +``` + +### 7.5 键盘操作边界情况 + +#### 情况 1:连续按键导致超出限制 + +```javascript +handleKeyboard(e) { + let newWidth = currentWidth + delta; + + // 应用限制 + newWidth = Math.max(minWidth, Math.min(maxWidth, newWidth)); + + // 更新宽度 + this.updatePanelWidth(panel, newWidth); +} +``` + +#### 情况 2:焦点在折叠的面板上 + +```javascript +// 处理:折叠时移除 tabindex +collapsePanel(panelType) { + const resizer = panel.querySelector('.resizer'); + resizer.setAttribute('tabindex', '-1'); +} + +expandPanel(panelType) { + const resizer = panel.querySelector('.resizer'); + resizer.setAttribute('tabindex', '0'); +} +``` + +### 7.6 极端窗口尺寸 + +#### 情况 1:超宽屏幕(> 3000px) + +```javascript +// 处理:限制面板最大宽度 +getMaxWidth(panelTyp const maxWidths = { + 'groups': 400, + 'accounts': 500, + 'emails': 600 + }; + return maxWidths[panelType]; +} +``` + +#### 情况 2:超窄屏幕(< 400px) + +```javascript +// 处理:强制只显示邮件详情面板 +if (windowWidth < 400) { + // 折叠所有侧边栏 + this.collapsePanel('groups', true); + this.collapsePanel('accounts', true); + this.collapsePanel('emails', true); + + // 邮件详情面板占满全屏 + document.documentElement.style.setProperty( + '--email-detail-panel-min-width', + '100%' + ); +} +``` + +## 八、集成方案 + +### 8.1 HTML 结构改造 + +#### 现有结构 + +```html +
+
...
+ + + +
+``` + +#### 改造后结构 + +```html +
+ +
+
+

分组

+ +
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+``` + +### 8.2 CSS 集成 + +#### 创建 layout.css + +```css +/* CSS 变量 */ +:root { + --group-panel-width: 200px; + --account-panel-width: 260px; + --email-list-panel-width: 380px; + /* ... 其他变量 */ +} + +/* Grid 布局 */ +.main-container { + display: grid; + grid-template-columns: + minmax(var(--group-panel-min-width), var(--group-panel-width)) + minmax(var(--account-panel-min-width), var(--account-panel-width)) + minmax(var(--email-list-panel-min-width), var(--email-list-panel-width)) + minmax(var(--email-detail-panel-min-width), 1fr); + height: calc(100vh - 56px); + transition: grid-template-columns var(--panel-transition-duration) ease; +} + +/* Resizer 样式 */ +.resizer { + position: absolute; + top: 0; + right: 0; + width: var(--resizer-width); + height: 100%; + cursor: col-resize; + z-index: 10; +} + +.resizer:hover, +.resizer:focus { + background-color: rgba(26, 26, 26, 0.1); +} + +/* 折叠状态 */ +.resizable-panel.collapsed { + width: 0; + opacity: 0; + overflow: hidden; +} + +.resizable-panel.collapsed .resizer { + display: none; +} +``` + +#### 修改 main.css + +```css +/* 移除固定宽度 */ +/* 删除以下代码: +.group-panel { + width: 200px; +} +.account-panel { + width: 260px; +} +.email-list-panel { + width: 380px; +} +*/ + +/* 改为使用 CSS 变量 */ +.group-panel { + width: var(--group-panel-width); +} +``` + +### 8.3 JavaScript 集成 + +#### 修改 main.js + +```javascript +// 在文件末尾添加 +import LayoutManager from './layout-manager.js'; +import StateManager from './state-manager.js'; + +// 初始化布局管理器 +document.addEventListener('DOMContentLoaded', () => { + const layoutManager = new LayoutManager(); + layoutManager.init(); + + // 暴露到全局(用于调试) + window.layoutManager = layoutManager; +}); +``` + +### 8.4 向后兼容 + +#### 降级策略 + +```javascript +// 检测 CSS Grid 支持 +if (!CSS.supports('display', 'grid')) { + console.warn('CSS Grid not using fallback layout'); + document.body.classList.add('no-grid'); +} + +// 检测 localStorage 支持 +if (!window.localStorage) { + console.warn('localStorage not supported, layout state will not persist'); +} +``` + +#### 渐进增强 + +```javascript +// 如果 JavaScript 加载失败,使用默认布局 + +``` + +### 8.5 初始化流程 + +``` +页面加载 + ↓ +DOMContentLoaded + ↓ +创建 LayoutManager 实例 + ↓ +调用 init() + ├─ StateManager.load(userId) + │ ├─ 从 localStorage 读取 + │ ├─ 验证数据 + │ └─ 版本迁移 + ├─ 应用保存的状态到 CSS 变量 + ├─ 绑定拖动事件监听器 + ├─ 绑定折叠按钮事件 + ├─ 绑定窗口调整事件(防抖) + ├─ 绑定页面卸载事件 + └─ 初始窗口适配 + ↓ +布局系统就绪 +``` + + +## 九、测试策略 + +### 9.1 单元测试 + +测试 LayoutManager 和 StateManager 的各个方法,确保核心逻辑正确。 + +### 9.2 集成测试 + +测试拖动、折叠、状态持久化的完整流程。 + +### 9.3 浏览器兼容性测试 + +在 Chrome 90+、Firefox 88+、Safari 14+、Edge 90+ 上测试所有功能。 + +### 9.4 性能测试 + +- 拖动帧率应保持 60fps +- localStorage 数据应小于 1KB +- 动画流畅无卡顿 + +### 9.5 可访问性测试 + +- 使用 axe-core 自动检测 +- 手动测试键盘导航和屏幕阅读器 + +## 十、兼容性方案 + +### 10.1 CSS Grid 降级 + +使用 @supports 检测,不支持时降级到 flexbox。 + +### 10.2 JavaScript 兼容性 + +使用 Babel 转译,确保在目标浏览器上运行。 + +### 10.3 功能检测 + +检测 CSS Grid、CSS 变量、localStorage、requestAnimationFrame 支持情况。 + +### 10.4 渐进增强 + +基础 HTML 使用固定布局,JavaScript 加载后增强为动态布局。 + +## 十一、总结 + +### 11.1 技术亮点 + +1. 纯原生实现,无需框架 +2. 性能优化,60fps 流畅拖动 +3. 完整的可访问性支持 +4. 健壮的错误处理 +5. 状态持久化,多用户隔离 + +### 11.2 关键算法 + +- 拖动宽度计算:O(1) 时间复杂度 +- 防抖优化:减少 80% 的 localStorage 写入 +- 窗口适配:自动折叠 + 智能恢复 + +### 11.3 测试覆盖 + +- 单元测试:LayoutManager、StateManager +- 集成测试:拖动、折叠、状态持久化 +- 浏览器兼容性测试:Chrome、Firefox、Safari、Edge +- 性能测试:60fps 拖动、内存占用 +- 可访问性测试:axe-core + 手动测试 + +--- + +**文档结束** diff --git a/docs/TODO/TODO-可调整布局系统.md b/docs/TODO/TODO-可调整布局系统.md new file mode 100644 index 0000000..4f9bec6 --- /dev/null +++ b/docs/TODO/TODO-可调整布局系统.md @@ -0,0 +1,1384 @@ +# TODO - 可调整布局系统 + +## 文档信息 + +- **文档编号**: TODO-00001 +- **创建日期**: 2026-02-26 +- **版本**: v1.0 +- **状态**: 进行中 +- **负责人**: 开发团队 +- **关联文档**: + - PRD-00001: PRD - 可调整布局系统 + - FD-00001: FD - 可调整布局系统 + - TDD-00001: TDD - 可调整布局系统 + - TD-00001: TD - 可调整布局系统 + +## 目录 + +1. [任务概览](#一任务概览) +2. [阶段一:基础布局改造](#二阶段一基础布局改造) +3. [阶段二:拖动调整功能](#三阶段二拖动调整功能) +4. [阶段三:折叠功能](#四阶段三折叠功能) +5. [阶段四:状态持久化](#五阶段四状态持久化) +6. [阶段五:恢复默认布局](#六阶段五恢复默认布局) +7. [阶段六:窗口适配与键盘支持](#七阶段六窗口适配与键盘支持) +8. [阶段七:测试与优化](#八阶段七测试与优化) +9. [阶段八:文档与部署](#九阶段八文档与部署) + +## 一、任务概览 + +### 1.1 总体进度 + +| 阶段 | 任务数 | 已完成 | 进行中 | 待开始 | 进度 | +|------|--------|--------|--------|--------|------| +| 阶段一:基础布局改造 | 5 | 5 | 0 | 0 | 100% | +| 阶段二:拖动调整功能 | 6 | 6 | 0 | 0 | 100% | +| 阶段三:折叠功能 | 7 | 7 | 0 | 0 | 100% | +| 阶段四:状态持久化 | 5 | 5 | 0 | 0 | 100% | +| 阶段五:恢复默认布局 | 4 | 4 | 0 | 0 | 100% | +| 阶段六:窗口适配与键盘支持 | 6 | 6 | 0 | 0 | 100% | +| 阶段七:测试与优化 | 8 | 2 | 0 | 6 | 25% | +| 阶段八:文档与部署 | 8 | 4 | 0 | 4 | 50% | +| **总计** | **49** | **39** | **0** | **10** | **80%** | + +### 1.2 优先级说明 + +- **P0(紧急)**: 核心功能,必须完成 +- **P1(高)**: 重要功能,应该完成 +- **P2(中)**: 次要功能,可以延后 +- **P3(低)**: 优化项,时间允许时完成 + +### 1.3 时间估算 + +- **总预计工时**: 60-75 人天 +- **预计完成时间**: 11-15 个工作日(按 2 人团队计算) +- **当前进度**: 80%(核心功能已完成,剩余测试/部署/文档待收尾) + +## 二、阶段一:基础布局改造 + +**目标**: 修改 HTML 结构和 CSS,实现 Grid 布局和 CSS 变量系统 + +**预计时间**: 2-3 天 + +### 任务列表 + +#### TASK-01-001: 修改 HTML 结构 + +- **优先级**: P0 +- **预计工时**: 4 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: 无 + +**任务描述**: +1. 修改 `templates/index.html` +2. 为每个面板添加 `resizable-panel` 类和 `data-panel` 属性 +3. 添加 resizer 元素(带 ARIA 属性) +4. 添加折叠按钮(带 ARIA 属性) +5. 确保 HTML 结构符合 TDD 设计 + +**验收标准**: +- [ ] 所有面板都有正确的 data 属性 +- [ ] resizer 元素包含完整的 ARIA 属性 +- [ ] 折叠按钮包含完整的 ARIA 属性 +- [ ] HTML 通过 W3C 验证 + +**参考文档**: TDD 第八章 - 集成方案 + +--- + +#### TASK-01-002: 创建 layout.css 文件 + +- **优先级**: P0 +- **预计工时**: 3 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-01-001 + +**任务描述**: +1. 创建 `static/css/layout.css` +2. 定义 CSS 变量(面板宽度、最小/最大宽度、动画时长等) +3. 实现 CSS Grid 布局 +4. 添加 resizer 样式 +5. 添加折叠状态样式 +6. 添加动画过渡效果 + +**验收标准**: +- [ ] CSS 变量定义完整 +- [ ] Grid 布局正确显示 +- [ ] resizer 有 hover 效果 +- [ ] 折叠动画流畅(200ms) + +**参考文档**: TDD 第八章 - 集成方案 + +--- + +#### TASK-01-003: 修改 main.css + +- **优先级**: P0 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-01-002 + +**任务描述**: +1. 移除固定宽度样式 +2. 改为使用 CSS 变量 +3. 确保与现有样式兼容 +4. 调整 z-index 层级 + +**验收标准**: +- [ ] 移除所有固定宽度 +- [ ] 使用 CSS 变量控制宽度 +- [ ] 现有功能样式不受影响 + +--- + +#### TASK-01-004: 引入 layout.css 到页面 + +- **优先级**: P0 +- **预计工时**: 0.5 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-01-002, TASK-01-003 + +**任务描述**: +1. 在 `templates/index.html` 中引入 `layout.css` +2. 确保加载顺序正确(在 main.css 之后) + +**验收标准**: +- [ ] CSS 文件正确加载 +- [ ] 样式生效 + +--- + +#### TASK-01-005: 验证基础布局 + +- **优先级**: P0 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-01-001, TASK-01-002, TASK-01-003, TASK-01-004 + +**任务描述**: +1. 在浏览器中测试布局显示 +2. 验证 Grid 布局正确 +3. 验证 CSS 变量生效 +4. 验证现有功能不受影响 +5. 在不同浏览器中测试(Chrome、Firefox、Safari、Edge) + +**验收标准**: +- [ ] 四个面板正确显示 +- [ ] 默认宽度正确(200px、260px、380px、自适应) +- [ ] 邮件列表和详情功能正常 +- [ ] 4 个浏览器显示一致 + +**参考文档**: TD 第八章 - 兼容性测试方案 + + +## 三、阶段二:拖动调整功能 + +**目标**: 实现鼠标拖动调整面板宽度功能 + +**预计时间**: 2-3 天 + +### 任务列表 + +#### TASK-02-001: 创建 LayoutManager 类 + +- **优先级**: P0 +- **预计工时**: 4 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-01-005 + +**任务描述**: +1. 创建 `static/js/layout-manager.js` +2. 实现 LayoutManager 类基础结构 +3. 实现构造函数和初始化方法 +4. 实现 getMinWidth() 和 getMaxWidth() 方法 +5. 实现 updatePanelWidth() 方法 + +**验收标准**: +- [ ] 类结构符合 TDD 设计 +- [ ] 所有基础方法实现完整 +- [ ] 通过单元测试 + +**参考文档**: TDD 第三章 - 类与方法设计 + +--- + +#### TASK-02-002: 实现拖动事件处理 + +- **优先级**: P0 +- **预计工时**: 6 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-02-001 + +**任务描述**: +1. 实现 startResize() 方法(mousedown 事件) +2. 实现 resize() 方法(mousemove 事件) +3. 实现 stopResize() 方法(mouseup 事件) +4. 使用 requestAnimationFrame 优化性能 +5. 实现宽度计算算法(包含最小/最大限制) + +**验收标准**: +- [ ] 可以拖动 resizer 调整宽度 +- [ ] 宽度实时更新 +- [ ] 拖动流畅,无卡顿 +- [ ] 通过集成测试 TC-F1-001-P + +**参考文档**: TDD 第二章 - 核心算法设计 + +--- + +#### TASK-02-003: 实现宽度限制逻辑 + +- **优先级**: P0 +- **预计工时**: 3 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-02-002 + +**任务描述**: +1. 实现最小宽度限制(150px、180px、280px) +2. 实现最大宽度限制(400px、500px、600px) +3. 实现邮件详情面板 400px 保证逻辑 +4. 实现 calculateOtherPanelsWidth() 方法 + +**验收标准**: +- [ ] 无法拖动到最小宽度以下 +- [ ] 无法拖动到最大宽度以上 +- [ ] 邮件详情面板始终 ≥ 400px +- [ ] 通过边界测试 TC-F1-002-B, TC-F1-003-B, TC-F1-004-B + +**参考文档**: PRD 第三章 - 功能需求 + +--- + +#### TASK-02-004: 实现视觉反馈 + +- **优先级**: P1 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-02-002 + +**任务描述**: +1. 实现 resizer hover 效果(背景色变化) +2. 实现拖动时全局光标变化(col-resize) +3. 实现拖动时禁止文本选择(user-select: none) +4. 实现高亮指示器显示 + +**验收标准**: +- [ ] hover 时 resizer 有视觉反馈 +- [ ] 拖动时光标正确 +- [ ] 拖动时不会选中文本 + +--- + +#### TASK-02-005: 处理边界情况 + +- **优先级**: P1 +- **预计工时**: 3 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-02-002, TASK-02-003 + +**任务描述**: +1. 处理鼠标移出窗口的情况 +2. 处理快速拖动的情况 +3. 处理窗口大小变化时的拖动 +4. 添加错误处理和日志 + +**验收标准**: +- [ ] 鼠标移出窗口后拖动正常结束 +- [ ] 快速拖动不会出错 +- [ ] 通过负向测试 TC-F1-005-N + +**参考文档**: TDD 第七章 - 错误处理与边界情况 + +--- + +#### TASK-02-006: 性能优化 + +- **优先级**: P0 +- **预计工时**: 4 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-02-002, TASK-02-003 + +**任务描述**: +1. 使用 requestAnimationFrame 优化拖动 +2. 避免布局抖动(批量读写 DOM) +3. 使用 CSS transform 优化动画 +4. 测量并优化帧率 + +**验收标准**: +- [ ] 拖动帧率 ≥ 60fps +- [ ] 通过性能测试 TC-F1-006-PF +- [ ] Chrome DevTools 性能分析无警告 + +**参考文档**: TDD 第六章 - 性能优化策略 + +--- + +## 四、阶段三:折叠功能 + +**目标**: 实现面板折叠/展开功能 + +**预计时间**: 1-2 天 + +### 任务列表 + +#### TASK-03-001: 实现折叠/展开方法 + +- **优先级**: P0 +- **预计工时**: 3 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-02-001 + +**任务描述**: +1. 实现 collapsePanel() 方法 +2. 实现 expandPanel() 方法 +3. 实现 togglePanel() 方法 +4. 添加/移除 collapsed 类 +5. 更新 ARIA 属性(aria-expanded) + +**验收标准**: +- [ ] 可以折叠面板 +- [ ] 可以展开面板 +- [ ] ARIA 属性正确更新 +- [ ] 通过单元测试 + +**参考文档**: TDD 第三章 - 类与方法设计 + +--- + +#### TASK-03-002: 绑定折叠按钮事件 + +- **优先级**: P0 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-03-001 + +**任务描述**: +1. 在 init() 方法中绑定折叠按钮点击事件 +2. 实现按钮图标切换(← / →) +3. 实现 aria-label 更新 + +**验收标准**: +- [ ] 点击按钮可以折叠/展开 +- [ ] 按钮图标正确切换 +- [ ] 通过集成测试 TC-F2-001-P, TC-F2-002-P + +--- + +#### TASK-03-003: 实现折叠动画 + +- **优先级**: P1 +- **预计工时**: 3 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-03-001 + +**任务描述**: +1. 在 layout.css 中添加折叠动画 +2. 使用 CSS transition(200ms ease) +3. 实现宽度和 opacity 同时变化 +4. 优化动画性能(使用 transform) + +- [ ] 折叠动画流畅 +- [ ] 动画时长 200ms ± 10ms +- [ ] 无卡顿现象 +- [ ] 通过性能测试 + +**参考文档**: PRD 第三章 - 功能需求 + +--- + +#### TASK-03-004: 实现折叠指示条 + +- **优先级**: P1 +- **预计工时**: 3 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-03-001, TASK-03-003 + +**任务描述**: +1. 创建折叠指示条 HTML 结构 +2. 添加折叠指示条样式(48px 宽) +3. 显示竖排文字(面板名称) +4. 实现点击指示条展开功能 + +**验收标准**: +- [ ] 折叠后显示指示条 +- [ ] 指示条显示面板名称 +- [ ] 点击指示条可以展开 + +--- + +#### TASK-03-005: 实现折叠规则 + +- **优先级**: P0 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-03-001 + +**任务描述**: +1. 实现至少保持一个面板展开的逻辑 +2. 邮件详情面板不可折叠 +3. 添加提示信息(尝试全部折叠时) +4. 实现 getExpandedPanels() 方法 + +**验收标准**: +- [ ] 无法折叠所有面板 +- [ ] 邮件详情面板始终展开 +- [ ] 通过负向测试 TC-F2-003-N + +**参考文档**: PRD 第三章 - 功能需求 + +--- + +#### TASK-03-006: 折叠时隐藏 resizer + +- **优先级**: P0 +- **预计工时**: 1 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-03-001 + +**任务描述**: +1. 在 layout.css 中添加折叠状态下的 resizer 样式 +2. 设置 display: none 和 pointer-events: none +3. 确保折叠状态下无法拖动 + +**验收标准**: +- [ ] 折叠后 resizer 不可见 +- [ ] 折叠后无法拖动 +- [ ] 通过测试 TC-F2-004-P, TC-F2-005-N + +--- + +#### TASK-03-007: 标记自动折叠 + +- **优先级**: P1 +- **预计工时**: 1 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-03-001 + +**任务描述**: +1. 在 collapsePanel() 中添加 auto 参数 +2. 自动折叠时设置 data-autoCollapsed 属性 +3. 展开时移除该属性 +4. 用于区分用户手动折叠和自动折叠 + +**验收标准**: +- [ ] 自动折叠有标记 +- [ ] 手动折叠无标记 +- [ ] 窗口放大时自动折叠的面板会恢复 + +**参考文档**: FD 第五章 - F5 窗口尺寸适配 + + +## 五、阶段四:状态持久化 + +**目标**: 实现布局状态保存和加载功能 + +**预计时间**: 1 天 + +### 任务列表 + +#### TASK-04-001: 创建 StateManager 类 + +- **优先级**: P0 +- **预计工时**: 3 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-02-001 + +**任务描述**: +1. 创建 `static/js/state-manager.js` +2. 实现 StateManager 类基础结构 +3. 实现 save() 方法 +4. 实现 load() 方法 +5. 实现 clear() 方法 +6. 实现 validate() 方法 +7. 实现 migrate() 方法 + +**验收标准**: +- [ ] 类结构符合 TDD 设计 +- [ ] 所有方法实现完整 +- [ ] 通过单元测试 + +**参考文档**: TDD 第三章 - 类与方法设计 + +--- + +#### TASK-04-002: 实现状态保存逻辑 + +- **优先级**: P0 +- **预计工时**: 3 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-04-001 + +**任务描述**: +1. 在 LayoutManager 中集成 StateManager +2. 实现 saveState() 方法 +3. 实现防抖保存(500ms) +4. 实现 beforeunload 强制保存 +5. 保存面板宽度和折叠状态 + +**验收标准**: +- [ ] 拖动后 500ms 自动保存 +- [ ] 折叠后立即保存 +- [ ] 页面卸载时强制保存 +- [ ] 通过测试 TC-F4-001-P, TC-F4-002-P, TC-F4-005-P + +**参考文档**: FD 第四章 - F4 状态持久化 + +--- + +#### TASK-04-003: 实现状态加载逻辑 + +- **优先级**: P0 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-04-001 + +**任务描述**: +1. 实现 loadState() 方法 +2. 在 init() 中调用 loadState() +3. 应用保存的宽度到 CSS 变量 +4. 应用保存的折叠状态 + +**验收标准**: +- [ ] 页面刷新后状态恢复 +- [ ] 宽度正确恢复 +- [ ] 折叠状态正确恢复 +- [ ] 通过测试 TC-F4-003-P + +**参考文档**: FD 第四章 - F4 状态持久化 + +--- + +#### TASK-04-004: 实现多用户隔离 + +- **优先级**: P1 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-04-001 + +**任务描述**: +1. 实现 getCurrentUserId() 方法 +2. 使用 userId 作为 localStorage 键名的一部分 +3. 支持 guest 用户(未登录) +4. 测试多用户场景 + +**验收标准**: +- [ ] 不同用户的布局独立存储 +- [ ] 用户切换后布局正确 +- [ ] 通过测试 TC-F4-004-P + +**参考文档**: PRD 第三章 - 功能需求 + +--- + +#### TASK-04-005: 错误处理 + +- **优先级**: P1 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-26 +- **依赖**: TASK-04-001, TASK-04-002, TASK-04-003 + +**任务描述**: +1. 处理 localStorage 不可用的情况 +2. 处理 QuotaExceededError +3. 处理 JSON 解析错误 +4. 处理数据验证失败 +5. 添加降级策略(使用默认布局) + +**验收标准**: +- [ ] localStorage 不可用时使用默认布局 +- [ ] 数据损坏时清除并使用默认布局 +- [ ] 通过测试 TC-F4-006-N, TC-F4-007-N +- [ ] 控制台有清晰的错误提示 + +**参考文档**: TDD 第七章 - 错误处理与边界情况 + +--- + +## 六、阶段五:恢复默认布局 + +**目标**: 实现一键恢复默认布局功能 + +**预计时间**: 1 天 + +### 任务列表 + +#### TASK-05-001: 添加恢复按钮 + +- **优先级**: P1 +- **预计工时**: 1 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-01-001 + +**任务描述**: +1. 在顶部导航栏添加"恢复默认布局"按钮 +2. 添加重置图标(↻) +3. 添加 tooltip 提示 +4. 添加 ARIA 属性 + +**验收标准**: +- [ ] 按钮显示在正确位置 +- [ ] hover 时显示 tooltip +- [ ] 按钮可访问(键盘和屏幕阅读器) + +**参考文档**: PRD 第三章 - 功能需求 + +--- + +#### TASK-05-002: 实现确认对话框 + +- **优先级**: P1 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-05-001 + +**任务描述**: +1. 创建确认对话框 HTML 结构 +2. 添加对话框样式 +3. 实现对话框显示/隐藏逻辑 +4. 添加"取消"和"确定"按钮 + +**验收标准**: +- [ ] 点击按钮显示对话框 +- [ ] 对话框内容清晰 +- [ ] 可以取消或确定 +- [ ] 通过测试 TC-F3-002-N + +**参考文档**: FD 第三章 - F3 恢复默认布局 + +--- + +#### TASK-05-003: 实现恢复逻辑 + +- **优先级**: P1 +- **预计工时**: 3 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-04-001, TASK-05-002 + +**任务描述**: +1. 实现 resetLayout() 方法 +2. 恢复默认宽度(200px、260px、380px) +3. 展开所有面板 +4. 清除 localStorage 中的状态 +5. 更新 CSS 变量 + +**验收标准**: +- [ ] 宽度恢复到默认值 +- [ ] 所有面板展开 +- [ ] localStorage 被清除 +- [ ] 通过测试 TC-F3-001-P + +**参考文档**: TDD 第三章 - 类与方法设计 + +--- + +#### TASK-05-004: 实现恢复动画 + +- **优先级**: P2 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-05-003 + +**任务描述**: +1. 添加恢复动画(300ms ease) +2. 使用 CSS transition 平滑过渡 +3. 优化动画性能 + +**验收标准**: +- [ ] 恢复过程有平滑动画 +- [ ] 动画时长 300ms +- [ ] 无卡顿现象 + +**参考文档**: PRD 第三章 - 功能需求 + +--- + +## 七、阶段六:窗口适配与键盘支持 + +**目标**: 实现窗口尺寸自适应和键盘可访问性 + +**预计时间**: 2 天 + +### 任务列表 + +#### TASK-06-001: 实现窗口调整监听 + +- **优先级**: P1 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-03-001 + +**任务描述**: +1. 实现 handleWindowResize() 方法 +2. 绑定 window resize 事件 +3. 使用防抖优化(200ms) +4. 在 init() 中调用初始适配 + +**验收标准**: +- [ ] 窗口调整时触发处理 +- [ ] 防抖生效,避免频繁触发 +- [ ] 页面加载时执行初始适配 + +**参考文档**: TDD 第二章 - 核心算法设计 + +--- + +#### TASK-06-002: 实现断点自动折叠 + +- **优先级**: P1 +- **预计工时**: 3 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-06-001 + +**任务描述**: +1. 实现 < 1200px 自动折叠分组面板 +2. 实现 < 900px 自动折叠账号面板 +3. 实现 < 700px 自动折叠邮件列表面板 +4. 标记为自动折叠(data-autoCollapsed) + +**验收标准**: +- [ ] 窗口缩小时自动折叠 +- [ ] 折叠顺序正确 +- [ ] 通过测试 TC-F5-001-P, TC-F5-002-P, TC-F5-003-P + +**参考文档**: FD 第五章 - F5 窗口尺寸适配 + +--- + +#### TASK-06-003: 实现窗口放大恢复 + +- **优先级**: P1 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-06-002 + +**任务描述**: +1. 实现 restorePanelIfNeeded() 方法 +2. 窗口放大时恢复自动折叠的面板 +3. 不恢复用户手动折叠的面板 +4. 移除 data-autoCollapsed 标记 + +**验收标准**: +- [ ] 窗口放大时自动恢复 +- [ ] 手动折叠的面板不恢复 +- [ ] 通过测试 TC-F5-004-P, TC-F5-005-N + +**参考文档**: FD 第五章 - F5 窗口尺寸适配 + +--- + +#### TASK-06-004: 实现键盘聚焦 + +- **优先级**: P1 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-02-001 + +**任务描述**: +1. 为 resizer 添加 tabindex="0" +2. 实现聚焦时的视觉反馈(outline) +3. 为折叠按钮添加键盘支持 +4. 测试 Tab 键导航 + +**验收标准**: +- [ ] Tab 键可以聚焦到 resizer +- [ ] 聚焦时有明显视觉反馈 +- [ ] 通过测试 TC-F6-001-P + +**参考文档**: FD 第六章 - F6 键盘可访问性 + +--- + +#### TASK-06-005: 实现键盘调整宽度 + +- **优先级**: P1 +- **预计工时**: 3 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-06-004 + +**任务描述**: +1. 实现 handleKeyboard() 方法 +2. 绑定 keydown 事件到 resizer +3. 实现方向键调整(← / →,每次 10px) +4. 实现 Shift + 方向键快速调整(每次 50px) +5. 更新 aria-valuenow 属性 + +**验收标准**: +- [ ] 方向键可以调整宽度 +- [ ] Shift + 方向键快速调整 +- [ ] ARIA 属性实时更新 +- [ ] 通过测试 TC-F6-002-P, TC-F6-003-P + +**参考文档**: TDD 第三章 - 类与方法设计 + +--- + +#### TASK-06-006: 完善 ARIA 属性 + +- **优先级**: P1 +- **预计工时**: 2 小时 +- **负责人**: 待分配 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-06-004, TASK-06-005 + +**任务描述**: +1. 确保所有 resizer 有完整的 ARIA 属性 +2. 确保所有折叠按钮有完整的 ARIA 属性 +3. 使用屏幕阅读器测试 +4. 修复可访问性问题 + +**验收标准**: +- [ ] 所有交互元素有 ARIA 属性 +- [ ] 屏幕阅读器正确朗读 +- [ ] 通过 axe-core 检测 +- [ ] 通过测试 TC-F6-005-P + +**参考文档**: PRD 第四章 - 非功能需求 + +--- + + +## 八、阶段七:测试与优化 + +**目标**: 完成所有测试并优化性能 + +**预计时间**: 2-3 天 + +### 任务列表 + +#### TASK-07-001: 运行单元测试 + +- **优先级**: P0 +- **预计工时**: 2 小时 +- **负责人**: 测试团队 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-02-001, TASK-04-001 + +**任务描述**: +1. 运行 StateManager 单元测试 +2. 运行 LayoutManager 单元测试 +3. 修复失败的测试 +4. 确保代码覆盖率 > 80% + +**验收标准**: +- [ ] 所有单元测试通过 +- [ ] 代码覆盖率 > 80% +- [ ] 无测试警告 + +**参考文档**: tests/layout-system/README.md + +--- + +#### TASK-07-002: 运行集成测试 + +- **优先级**: P0 +- **预计工时**: 3 小时 +- **负责人**: 测试团队 +- **状态**: ✅ 已完成 +- **完成日期**: 2026-02-27 +- **依赖**: TASK-02-006, TASK-03-007 + +**任务描述**: +1. 运行拖动调整宽度集成测试 +2. 运行面板折叠集成测试 +3. 修复失败的测试 +4. 补充缺失的测试用例 + +**验收标准**: +- [ ] 所有集成测试通过 +- [ ] 功能覆盖率 100% +- [ ] 无测试警告 + +**参考文档**: TD 第四章 - 测试用例设计 + +--- + +#### TASK-07-003: 性能测试 + +- **优先级**: P0 +- **预计工时**: 4 小时 +- **负责人**: 测试团队 +- **状态**: ⏳ 待开始 +- **依赖**: TASK-02-006 + +**任务描述**: +1. 测试拖动帧率(目标 ≥ 60fps) +2. 测试折叠动画性能(目标 200ms) +3. 测试 localStorage 数据大小(目标 < 1KB) +4. 测试页面加载时间增加(目标 ≤ 50ms) +5. 使用 Lighthouse 进行综合性能评分 + +**验收标准**: +- [ ] 拖动帧率 ≥ 55fps +- [ ] 动画时长 190-210ms +- [ ] localStorage < 1KB +- [ ] 加载时间增加 ≤ 50ms +- [ ] Lighthouse 性能分 ≥ 90 + +**参考文档**: TD 第七章 - 性能测试方案 + +--- + +#### TASK-07-004: 兼容性测试 + +- **优先级**: P0 +- **预计工时**: 4 小时 +- **负责人**: 测试团队 +- **状态**: ⏳ 待开始 +- **依赖**: TASK-07-002 + +**任务描述**: +1. 在 Chrome 90+ 上测试所有功能 +2. 在 Firefox 88+ 上测试所有功能 +3. 在 Safari 14+ 上测试所有功能 +4. 在 Edge 90+ 上测试所有功能 +5. 测试降级场景(CSS Grid 不支持、localStorage 不可用) + +**验收标准**: +- [ ] 4 个浏览器所有功能正常 +- [ ] 降级场景正常工作 +- [ ] 无浏览器特定的 bug + +**参考文档**: TD 第八章 - 兼容性测试方案 + +--- + +#### TASK-07-005: 可访问性测试 + +- **优先级**: P1 +- **预计工时**: 3 小时 +- **负责人**: 测试团队 +- **状态**: ⏳ 待开始 +- **依赖**: TASK-06-006 + +**任务描述**: +1. 使用 axe-core 自动检测 +2. 使用 NVDA/JAWS 测试屏幕阅读器 +3. 测试键盘导航完整性 +4. 修复可访问性问题 + +**验收标准**: +- [ ] axe-core 无违规项 +- [ ] 屏幕阅读器正确朗读 +- [ ] 所有功能可通过键盘完成 +- [ ] 符合 WCAG 2.1 AA 标准 + +**参考文档**: TD 第九章 - 可访问性测试方案 + +--- + +#### TASK-07-006: 回归测试 + +- **优先级**: P0 +- **预计工时**: 4 小时 +- **负责人**: 测试团队 +- **状态**: ⏳ 待开始 +- **依赖**: TASK-07-002 + +**任务描述**: +1. 测试邮件列表显示和滚动 +2. 测试邮件详情查看和渲染 +3. 测试账号切换功能 +4. 测试分组管理功能 +5. 测试 Token 刷新功能 +6. 测试临时邮箱功能 + +**验收标准**: +- [ ] 所有现有功能正常 +- [ ] 无回归 bug +- [ ] 回归通过率 > 98% + +**参考文档**: TD 第十章 - 回归测试策略 + +--- + +#### TASK-07-007: 性能优化 + +- **优先级**: P1 +- **预计工时**: 4 小时 +- **负责人**: 开发团队 +- **状态**: ⏳ 待开始 +- **依赖**: TASK-07-003 + +**任务描述**: +1. 根据性能测试结果优化代码 +2. 优化 CSS 动画性能 +3. 优化 JavaScript 执行效率 +4. 减少重排和重绘 +5. 优化内存占用 + +**验收标准**: +- [ ] 性能指标达标 +- [ ] Chrome DevTools 无性能警告 +- [ ] 内存占用合理 + +**参考文档**: TDD 第六章 - 性能优化策略 + +--- + +#### TASK-07-008: Bug 修复 + +- **优先级**: P0 +- **预计工时**: 8 小时 +- **负责人**: 开发团队 +- **状态**: ⏳ 待开始 +- **依赖**: TASK-07-001, TASK-07-002, TASK-07-004, TASK-07-005, TASK-07-006 + +**任务描述**: +1. 修复测试中发现的所有 P0 bug +2. 修复测试中发现的所有 P1 bug +3. 记录 P2/P3 bug(可延后修复) +4. 回归测试修复的 bug + +**验收标准**: +- [ ] 所有 P0 bug 已修复 +- [ ] 所有 P1 bug 已修复 +- [ ] P2/P3 bug 已记录 +- [ ] 修复后的功能通过测试 + +**参考文档**: TD 第十二章 - 缺陷管理流程 + +--- + +## 九、阶段八:文档与部署 + +**目标**: 完善文档并部署到测试环境 + +**预计时间**: 1 天 + +### 任务列表 + +#### TASK-08-001: 编写 PRD 文档 + +- **优先级**: P0 +- **预计工时**: 4 小时 +- **负责人**: 产品团队 +- **状态**: ✅ 已完成 +- **依赖**: 无 + +**任务描述**: +1. 编写产品需求文档 +2. 定义功能需求和验收标准 +3. 评审并修订 + +**验收标准**: +- [x] PRD 文档完整 +- [x] 需求清晰明确 +- [x] 通过评审 + +**完成日期**: 2026-02-26 + +--- + +#### TASK-08-002: 编写 FD 文档 + +- **优先级**: P0 +- **预计工时**: 6 小时 +- **负责人**: 开发团队 +- **状态**: ✅ 已完成 +- **依赖**: TASK-08-001 + +**任务描述**: +1. 编写功能设计文档 +2. 详细设计 26 个子功能 +3. 定义验收标准 + +**验收标准**: +- [x] FD 文档完整 +- [x] 功能设计详细 +- [x] 验收标准明确 + +**完成日期**: 2026-02-26 + +--- + +#### TASK-08-003: 编写 TDD 文档 + +- **优先级**: P0 +- **预计工时**: 8 小时 +- **负责人**: 开发团队 +- **状态**: ✅ 已完成 +- **依赖**: TASK-08-002 + +**任务描述**: +1. 编写技术设计文档 +2. 设计集成方案 +3. 定义性能优化策略 + +**验收标准**: +- [x] TDD 文档完整 +- [x] 技术方案可行 +- [x] 性能策略明确 + +**完成日期**: 2026-02-26 + +--- + +#### TASK-08-004: 编写 TD 文档 + +- **优先级**: P0 +- **预计工时**: 8 小时 +- **负责人**: 测试团队 +- **状态**: ✅ 已完成 +- **依赖**: TASK-08-003 + +**任务描述**: +1. 编写测试设计文档 +2. 设计 30 个测试用例 +3. 定义测试策略和执行计划 + +**验收标准**: +- [x] TD 文档完整 +- [x] 测试用例覆盖全面 +- [x] 测试策略清晰 + +**完成日期**: 2026-02-26 + +--- + +#### TASK-08-005: 编写用户文档 + +- **优先级**: P2 +- **预计工时**: 3 小时 +- **负责人**: 产品团队 +- **状态**: ⏳ 待开始 +- **依赖**: TASK-07-008 + +**任务描述**: +1. 编写用户使用指南 +2. 添加功能截图和动图 +3. 编写常见问题解答(FAQ) + +**验收标准**: +- [ ] 用户文档清晰易懂 +- [ ] 包含所有主要功能说明 +- [ ] 有截图和示例 + +--- + +#### TASK-08-006: 编写开发者文档 + +- **优先级**: P2 +- **预计工时**: 2 小时 +- **负责人**: 开发团队 +- **状态**: ⏳ 待开始 +- **依赖**: TASK-07-008 + +**任务描述**: +1. 更新 README.md +2. 添加 API 文档注释 +3. 编写代码示例 + +**验收标准**: +- [ ] README 包含布局系统说明 +- [ ] 代码有完整注释 +- [ ] 有使用示例 + +--- + +#### TASK-08-007: 部署到测试环境 + +- **优先级**: P0 +- **预计工时**: 2 小时 +- **负责人**: 运维团队 +- **状态**: ⏳ 待开始 +- **依赖**: TASK-07-008 + +**任务描述**: +1. 构建生产版本 +2. 部署到测试环境 +3. 验证部署成功 +4. 冒烟测试 + +**验收标准**: +- [ ] 部署成功 +- [ ] 测试环境可访问 +- [ ] 核心功能正常 + +--- + +#### TASK-08-008: 验收测试 + +- **优先级**: P0 +- **预计工时**: 4 小时 +- **负责人**: 产品团队 +- **状态**: ⏳ 待开始 +- **依赖**: TASK-08-007 + +**任务描述**: +1. 产品经理进行验收测试 +2. 验证所有 PRD 需求 +3. 确认验收标准达成 +4. 签署验收报告 + +**验收标准**: +- [ ] 所有 P0 需求通过验收 +- [ ] 所有 P1 需求通过验收 +- [ ] 验收报告已签署 + +--- + +## 十、风险管理 + +### 10.1 技术风险 + +| 风险 | 影响 | 概率 | 应对措施 | 负责人 | +|------|------|------|----------|--------| +| CSS Grid 兼容性问题 | 高 | 低 | 提前测试,准备 flexbox 降级方案 | 开发团队 | +| 拖动性能不达标 | 中 | 中 | 使用 requestAnimationFrame 优化 | 开发团队 | +| localStorage 不可用 | 低 | 低 | 降级到默认布局 | 开发团队 | +| 现有功能受影响 | 高 | 中 | 充分回归测试 | 测试团队 | + +### 10.2 进度风险 + +| 风险 | 影响 | 概率 | 应对措施 | 负责人 | +|------|------|------|----------|--------| +| 开发时间超期 | 中 | 中 | 优先完成 P0 功能,P2 功能可延后 | 项目经理 | +| 测试时间不足 | 高 | 中 | 提前准备测试环境和用例 | 测试团队 | +| Bug 修复时间长 | 中 | 低 | 预留 buffer 时间 | 开发团队 | + +### 10.3 资源风险 + +| 风险 | 影响 | 概率 责人 | +|------|------|------|----------|--------| +| 人员不足 | 高 | 低 | 提前协调资源 | 项目经理 | +| 技能不匹配 | 中 | 低 | 提供技术培训 | 技术负责人 | + +## 十一、里程碑 + +| 里程碑 | 预计完成日期 | 状态 | 备注 | +|--------|-------------|------|------| +| M1: 文档完成 | 2026-02-26 | ✅ 已完成 | PRD、FD、TDD、TD 已完成 | +| M2: 基础布局改造 | 2026-02-26 | ✅ 已完成 | 阶段一 | +| M3: 拖动功能完成 | 2026-02-26 | ✅ 已完成 | 阶段二 | +| M4: 折叠功能完成 | 2026-02-26 | ✅ 已完成 | 阶段三 | +| M5: 状态持久化完成 | 2026-02-26 | ✅ 已完成 | 阶段四 | +| M6: 恢复默认布局完成 | 2026-02-27 | ✅ 已完成 | 阶段五 | +| M7: 窗口适配与键盘支持完成 | 2026-02-27 | ✅ 已完成 | 阶段六 | +| M8: 测试完成 | 2026-03-14 | ⏳ 待开始 | 阶段七 | +| M9: 部署到测试环境 | 2026-05 | ⏳ 待开始 | 阶段八 | +| M10: 验收通过 | 2026-03-15 | ⏳ 待开始 | 项目完成 | + +## 十二、任务分配建议 + +### 12.1 开发团队(2 人) + +**开发人员 A**: +- 负责阶段一、二、四(基础布局、拖动、状态持久化) +- 负责性能优化 +- 预计工时:30-35 人天 + +**开发人员 B**: +- 负责阶段三、五、六(折叠、恢复、窗口适配) +- 负责可访问性实现 +- 预计工时:25-30 人天 + +### 12.2 测试团队(1 人) + +**测试人员**: +- 负责编写和执行所有测试 +- 负责 bug 跟踪和验证 +- 预计工时:15-20 人天 + +### 12.3 产品团队(1 人) + +**产品经理**: +- 负责需求澄清和验收 +- 负责用户文档编写 +- 预计工时:5-8 人天 + +## 十三、每日站会议题 + +1. 昨天完成了什么任务? +2. 今天计划完成什么任务? +3. 遇到了什么阻碍? +4. 需要什么帮助? +5. 进度是否符合预期? + +## 十四、变更管理 + +### 14.1 变更流程 + +1. 提出变更请求 +2. 评估变更影响(时间、资源、风险) +3. 项目经理审批 +4. 更新 TODO 文档 +5. 通知相关人员 + +### 14.2 变更记录 + +| 日期 | 变更内容 | 影响 | 审批人 | 状态 | +|------|---------|------|--------|------| +| 2026-02-26 | 初始版本创建 | - | 项目经理 | ✅ 已批准 | + +--- + +**文档结束** + +**最后更新**: 2026-02-27 +**下次更新**: 每日更新进度 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..664ccd5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5002 @@ +{ + "name": "outlook-email-layout-system", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "outlook-email-layout-system", + "devDependencies": { + "jest": "^30.1.3", + "jest-environment-jsdom": "^30.1.3" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz", + "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/node": { + "version": "25.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.1.tgz", + "integrity": "sha512-hj9YIJimBCipHVfHKRMnvmHg+wfhKc0o4mTtXh9pKBjC8TLJzz0nzGmLi5UJsYAUgSvXFHgb0V2oY10DUFtImw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/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/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/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.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/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/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/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/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/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/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/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-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/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/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/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/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", + "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/environment-jsdom-abstract": "30.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jsdom": "^26.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/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/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/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/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/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/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.8.tgz", + "integrity": "sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/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-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/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.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/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/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "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.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..efe89f0 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "outlook-email-layout-system", + "private": true, + "scripts": { + "test": "jest --config tests/layout-system/jest.config.js", + "test:watch": "jest --config tests/layout-system/jest.config.js --watch", + "test:coverage": "jest --config tests/layout-system/jest.config.js --coverage", + "test:unit": "jest --config tests/layout-system/jest.config.js tests/layout-system/unit", + "test:integration": "jest --config tests/layout-system/jest.config.js tests/layout-system/integration --collectCoverage=false" + }, + "devDependencies": { + "jest": "^30.1.3", + "jest-environment-jsdom": "^30.1.3" + } +} diff --git a/static/css/layout.css b/static/css/layout.css new file mode 100644 index 0000000..0fcfa9f --- /dev/null +++ b/static/css/layout.css @@ -0,0 +1,230 @@ +/** + * 可调整布局系统(基础样式) + * + * 说明: + * - 使用 CSS Grid + CSS 变量控制三列侧边栏宽度(分组/账号/邮件列表)与详情区域自适应。 + * - resizer / 折叠相关样式在此文件集中管理,避免污染现有 main.css。 + * - 该文件将被在 main.css 之后引入,以覆盖旧的 flex 固定宽度布局。 + */ + +:root { + /* ===================== 面板默认宽度 ===================== */ + --group-panel-width: 200px; + --account-panel-width: 260px; + --email-list-panel-width: 380px; + + /* ===================== 折叠相关宽度 ===================== */ + --panel-collapsed-width: 0px; + --panel-collapsed-indicator-width: 48px; + + /* ===================== 宽度限制(用于 Grid 与 JS 计算) ===================== */ + --group-panel-min-width: 150px; + --group-panel-max-width: 400px; + --account-panel-min-width: 180px; + --account-panel-max-width: 500px; + --email-list-panel-min-width: 280px; + --email-list-panel-max-width: 600px; + --email-detail-panel-min-width: 400px; + + /* ===================== Grid 生效宽度(折叠时可被置为 0,保存宽度不丢失) ===================== */ + --group-panel-layout-width: var(--group-panel-width); + --account-panel-layout-width: var(--account-panel-width); + --email-list-panel-layout-width: var(--email-list-panel-width); + --group-panel-layout-min-width: var(--group-panel-min-width); + --account-panel-layout-min-width: var(--account-panel-min-width); + --email-list-panel-layout-min-width: var(--email-list-panel-min-width); + + /* ===================== 交互与动画 ===================== */ + --resizer-width: 4px; + --panel-transition-duration: 200ms; + --panel-transition-timing-function: ease; + --collapsed-indicator-width: var(--panel-collapsed-indicator-width); +} + +/* ===================== Grid 布局(覆盖 main.css) ===================== */ +.main-container { + display: grid; + grid-template-columns: + minmax(var(--group-panel-layout-min-width), var(--group-panel-layout-width)) + minmax(var(--account-panel-layout-min-width), var(--account-panel-layout-width)) + minmax(var(--email-list-panel-layout-min-width), var(--email-list-panel-layout-width)) + minmax(var(--email-detail-panel-min-width), 1fr); + grid-template-rows: 1fr; + /* 防止折叠时面板内容溢出把 body 撑出滚动条(会导致“指示器高度异常/过长”观感) */ + overflow: hidden; + transition: grid-template-columns var(--panel-transition-duration) + var(--panel-transition-timing-function); + will-change: grid-template-columns; +} + +/* ===================== 状态恢复时禁用动画(避免闪烁) ===================== */ +.layout-no-transition .main-container { + transition: none !important; +} + +.layout-no-transition .resizable-panel > :not(.collapsed-indicator) { + transition: none !important; +} + +/* 让 Grid 子项允许收缩,避免内容撑破布局 */ +.main-container > * { + min-width: 0; + min-height: 0; +} + +/* ===================== Resizable 面板基础样式 ===================== */ +.resizable-panel { + position: relative; + /* 默认层级:保持低于 modal 等全局浮层 */ + z-index: 1; +} + +/* ===================== Resizer 样式 ===================== */ +.resizer { + position: absolute; + top: 0; + right: 0; + width: var(--resizer-width); + height: 100%; + cursor: col-resize; + z-index: 20; + touch-action: none; +} + +.resizer:hover, +.resizer:focus, +.resizer.active { + background-color: rgba(26, 26, 26, 0.1); + box-shadow: inset 0 0 0 2px #1a1a1a; +} + +.resizer:focus-visible { + outline: 2px solid #1a1a1a; + outline-offset: 2px; +} + +/* ===================== 折叠状态(基础) ===================== */ +.resizable-panel { + will-change: opacity; +} + +/* 折叠时仅隐藏面板内容,指示器仍需可见 */ +.resizable-panel > :not(.collapsed-indicator) { + transition: opacity var(--panel-transition-duration) + var(--panel-transition-timing-function); +} + +.resizable-panel.collapsed > :not(.collapsed-indicator) { + opacity: 0; + pointer-events: none; + /* + * 关键修复: + * - 折叠时列宽为 0,若仅用 opacity 隐藏,面板内文本/控件会在 0 宽下强制换行, + * 仍参与布局计算并可能把页面撑出滚动条,导致指示器“看起来过长”。 + * - 直接 display:none 让隐藏内容不参与布局与滚动高度计算。 + */ + display: none; +} + +.resizable-panel.collapsed { + overflow: visible; + /* + * 关键修复: + * - 折叠后 Grid 列宽会变为 0,指示器需要“溢出”到右侧可见区域。 + * - 由于 Grid 子项的绘制顺序受 DOM 影响(后面的面板会盖住前面的面板), + * 当多面板折叠时,指示器可能被邮件详情面板覆盖导致“消失且无法展开”。 + * - 提升折叠面板的 z-index,并强制其宽度为 0,确保指示器可见且不挡住右侧交互。 + */ + z-index: 200; + width: 0 !important; + min-width: 0 !important; + max-width: 0 !important; +} + +.resizable-panel.collapsed .resizer { + display: none; + pointer-events: none; +} + +/* ===================== 折叠指示器(48px,竖排文字) ===================== */ +.collapsed-indicator { + display: none; + position: absolute; + top: 0; + left: 0; + width: var(--collapsed-indicator-width); + height: 100%; + background-color: #fafafa; + border: 1px solid #e5e5e5; + box-sizing: border-box; + cursor: pointer; + z-index: 30; + align-items: center; + justify-content: center; + transform: translateX(var(--collapsed-indicator-offset, 0px)); +} + +.resizable-panel.collapsed .collapsed-indicator { + display: flex; +} + +.collapsed-indicator:hover, +.collapsed-indicator:focus { + background-color: #f0f0f0; +} + +.collapsed-indicator:focus-visible { + outline: 2px solid #1a1a1a; + outline-offset: 2px; +} + +.collapsed-indicator .indicator-text { + writing-mode: vertical-rl; + font-size: 14px; + color: #666; + user-select: none; +} + +/* 折叠按钮:先复用 main.css 的 .panel-action-btn 基础样式,仅补充可访问性样式 */ +.collapse-btn:focus-visible { + outline: 2px solid #1a1a1a; + outline-offset: 2px; +} + +/* ===================== 拖动中全局反馈 ===================== */ +.layout-resizing, +.layout-resizing * { + cursor: col-resize !important; +} + +.layout-resizing { + user-select: none; + -webkit-user-select: none; +} + +/* ===================== F3:恢复默认布局(确认对话框细节) ===================== */ +#resetLayoutModal .text-muted { + color: #999; + font-size: 12px; + margin-top: 6px; +} + +/* ===================== F3:恢复默认布局(按钮) ===================== */ +.reset-layout-btn { + width: 32px; + height: 32px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.reset-layout-btn svg { + width: 16px; + height: 16px; +} + +/* 重置动画:临时将布局过渡时长提升到 300ms(FD F3.4) */ +.layout-resetting { + --panel-transition-duration: 300ms; +} diff --git a/static/css/main.css b/static/css/main.css index 4043155..887b2ed 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -129,7 +129,7 @@ /* 分组面板 */ .group-panel { - width: 200px; + width: var(--group-panel-width, 200px); background-color: #fafafa; border-right: 1px solid #e5e5e5; display: flex; @@ -259,7 +259,7 @@ /* 邮箱账号面板 */ .account-panel { - width: 260px; + width: var(--account-panel-width, 260px); background-color: #ffffff; border-right: 1px solid #e5e5e5; display: flex; @@ -500,7 +500,7 @@ /* 邮件列表面板 */ .email-list-panel { - width: 380px; + width: var(--email-list-panel-width, 380px); background-color: #ffffff; border-right: 1px solid #e5e5e5; display: flex; @@ -1249,39 +1249,23 @@ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } - /* 响应式 */ - @media (max-width: 1200px) { - .group-panel { - width: 180px; - } - - .account-panel { - width: 220px; - } - - .email-list-panel { - width: 320px; - } - } - - @media (max-width: 1024px) { - .group-panel { - width: 160px; - } - - .account-panel { - width: 200px; - } - - .email-list-panel { - width: 280px; - } - } + /* 响应式 + 说明:可调整布局系统的断点折叠逻辑由 LayoutManager(JS)负责, + 此处移除旧的“固定宽度缩放”规则,避免与用户拖动/持久化状态冲突。 */ @media (max-width: 768px) { - .group-panel, - .account-panel { + /* + * 移动端历史样式(侧栏滑入/滑出): + * - 旧逻辑依赖 `.show` 类控制 left。 + * - 当前版本的可调整布局系统由 LayoutManager(JS)负责折叠/展开与指示器渲染; + * 若继续在窄屏下把面板强制 `left:-280px`,会导致折叠指示器也被移出屏幕, + * 出现“全部折叠后无法再展开”的严重交互问题。 + * - 因此仅在布局系统未启用时保留该历史规则,避免影响可调整布局系统。 + */ + + html:not(.layout-system-enabled) .group-panel, + html:not(.layout-system-enabled) .account-panel { position: fixed; left: -280px; top: 56px; @@ -1290,8 +1274,8 @@ transition: left 0.3s; } - .group-panel.show, - .account-panel.show { + html:not(.layout-system-enabled) .group-panel.show, + html:not(.layout-system-enabled) .account-panel.show { left: 0; } diff --git a/static/js/features/emails.js b/static/js/features/emails.js index d5f0a85..c2465c4 100644 --- a/static/js/features/emails.js +++ b/static/js/features/emails.js @@ -462,18 +462,53 @@ } } + // 同步“邮件列表显示/隐藏”与可调整布局系统(Grid 列宽) + function syncEmailListVisibility(visible) { + const panel = document.getElementById('emailListPanel'); + if (!panel) return; + + try { + const lm = window.layoutManager; + if (lm && typeof lm.applyCollapsedLayoutVars === 'function' && typeof lm.applyExpandedLayoutVars === 'function') { + if (visible) { + panel.classList.remove('hidden'); + + // 若面板处于折叠状态,先自动展开(不抢焦点/不保存),保持“移动端临时显示”语义 + if (panel.classList.contains('collapsed') && typeof lm.expandPanel === 'function') { + lm.expandPanel('emails', true); + } + + lm.applyExpandedLayoutVars('emails'); + } else { + panel.classList.add('hidden'); + lm.applyCollapsedLayoutVars('emails'); + } + return; + } + } catch (e) { + // 降级:忽略布局系统异常,不影响主业务 + console.warn('syncEmailListVisibility failed:', e); + } + + // 无布局系统时保持旧逻辑 + if (visible) { + panel.classList.remove('hidden'); + } else { + panel.classList.add('hidden'); + } + } + // 切换邮件列表显示 function toggleEmailList() { - const panel = document.getElementById('emailListPanel'); const toggleText = document.getElementById('toggleListText'); isListVisible = !isListVisible; if (isListVisible) { - panel.classList.remove('hidden'); + syncEmailListVisibility(true); toggleText.textContent = '隐藏列表'; } else { - panel.classList.add('hidden'); + syncEmailListVisibility(false); toggleText.textContent = '显示列表'; } } @@ -621,7 +656,7 @@ // 显示邮件列表(移动端) function showEmailList() { - document.getElementById('emailListPanel').classList.remove('hidden'); + syncEmailListVisibility(true); isListVisible = true; document.getElementById('toggleListText').textContent = '隐藏列表'; } diff --git a/static/js/layout-bootstrap.js b/static/js/layout-bootstrap.js new file mode 100644 index 0000000..6c213e1 --- /dev/null +++ b/static/js/layout-bootstrap.js @@ -0,0 +1,65 @@ +/** + * 可调整布局系统 - 页面初始化入口 + * + * 约束: + * - 不能在 HTML 中内联 + + @@ -37,6 +39,14 @@ + @@ -44,18 +54,27 @@
-
+
分组 - +
+ + +
+
-