From c7c3db42f61af2d44cb99dcc6cba113aba6c9e5d Mon Sep 17 00:00:00 2001 From: eddy Date: Sat, 1 Aug 2026 02:11:12 +0800 Subject: [PATCH] init --- .editorconfig | 9 + .gitignore | 199 ++ .prettierrc | 1 + CHANGELOG.md | 8 + LICENSE | 21 + README.md | 214 ++- deploy.sh | 173 ++ eslint.config.js | 14 + nodebook/2025.06-19.md | 71 + package-lock.json | 3655 +++++++++++++++++++++++++++++++++++++ package.json | 22 + src/css/styles.css | 242 +++ src/index.html | 261 +++ src/js/config.js | 31 + src/js/i18n/en.js | 123 ++ src/js/i18n/index.js | 29 + src/js/i18n/zh.js | 122 ++ src/js/import-export.js | 115 ++ src/js/main.js | 266 +++ src/js/modal.js | 82 + src/js/notify.js | 30 + src/js/render.js | 192 ++ src/js/seed-data.js | 94 + src/js/sort.js | 36 + src/js/storage.js | 63 + src/js/store.js | 107 ++ src/js/task-model.js | 56 + src/js/timeline.js | 63 + src/js/utils.js | 43 + task.md | 41 + test/backup-tasks-v1.json | 57 + test/core.test.js | 505 +++++ todo.md | 191 ++ 33 files changed, 7135 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 deploy.sh create mode 100644 eslint.config.js create mode 100644 nodebook/2025.06-19.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/css/styles.css create mode 100644 src/index.html create mode 100644 src/js/config.js create mode 100644 src/js/i18n/en.js create mode 100644 src/js/i18n/index.js create mode 100644 src/js/i18n/zh.js create mode 100644 src/js/import-export.js create mode 100644 src/js/main.js create mode 100644 src/js/modal.js create mode 100644 src/js/notify.js create mode 100644 src/js/render.js create mode 100644 src/js/seed-data.js create mode 100644 src/js/sort.js create mode 100644 src/js/storage.js create mode 100644 src/js/store.js create mode 100644 src/js/task-model.js create mode 100644 src/js/timeline.js create mode 100644 src/js/utils.js create mode 100644 task.md create mode 100644 test/backup-tasks-v1.json create mode 100644 test/core.test.js create mode 100644 todo.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..410601c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e82fbce --- /dev/null +++ b/.gitignore @@ -0,0 +1,199 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the enitre vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Node.js tooling +node_modules/ +.vite/ +coverage/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..4926aa8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{ "singleQuote": true, "trailingComma": "all", "printWidth": 100 } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6e8441f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## 1.0.0 - 2026-07-19 + +- Refactored the single-file application into native ES Modules. +- Preserved existing storage keys, seed tasks, and version 1.0 backup format. +- Replaced inline handlers with static listeners and delegated actions. +- Added resilient normalization, scoped rendering, i18n interpolation, and automated tests. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f80cf64 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 ghuang.top + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index fb3aab0..a033268 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,214 @@ -# Tasks +# 任务管理系统 +一个基于浏览器运行的现代化任务管理系统,支持任务生命周期管理、团队协作、进度跟踪、数据备份和中英文界面。项目已从单体脚本重构为原生 ES Modules,并兼容已有任务数据、偏好设置和 1.0 版 JSON 备份。 + +## 核心特性 + +### 任务管理 + +- **三状态管理**:待办事项、进行中、已完成 +- **双重描述系统**:任务描述(目标要求)和任务进展(实时状态) +- **可视化进度**:使用 0–100% 进度条跟踪任务 +- **优先级管理**:高、中、低三级优先级及颜色标识 +- **截止日期**:支持到期提醒和过期任务标识 +- **任务置顶**:重要任务在所有排序方式中优先显示 + +### 协作与记录 + +- **人员管理**:支持负责人和协作者 +- **时间轴记录**: + - 自动记录状态、进度、负责人、截止日期、优先级和进展内容变更 + - 支持手动添加、编辑和删除记录 + - 使用不同类型区分系统记录和手动记录;编辑系统记录会将其转为手动记录,并记在当前负责人名下 + - 编辑已有的手动记录只更新内容,原记录人保持不变 + - 记录按时间倒序展示,同一天内新增的记录也排在前面 + +### 搜索、排序与显示 + +- **13 种排序模式**:支持标题、数字、日期、优先级、进度和创建时间排序 +- **实时搜索**:搜索标题、描述、进展、负责人、协作者、到期日期、优先级和时间轴内容,界面固定文字不参与匹配;无匹配结果时栏内给出提示 +- **任务隐藏**:待办和已完成任务可单独隐藏,并可批量显示或隐藏 +- **国际化**:支持中英文界面切换 + +### 数据与兼容性 + +- **本地存储**:基于 `localStorage`,无需后端服务 +- **实时保存**:每次操作后自动持久化 +- **导入导出**:支持 JSON 覆盖导入和合并导入 +- **向后兼容**:保留原有存储键和 1.0 版备份格式 +- **容错处理**:损坏的任务原始数据会备份;存储不可用时当前页面会继续以内存模式运行 + +## 快速开始 + +### 安装与运行 + +```bash +npm install +npm start +``` + +打开 `serve` 输出的地址即可使用。由于应用采用原生 ES Modules,必须通过 HTTP 服务器访问,不支持使用 `file://` 直接打开 `src/index.html`。 + +也可以使用其他静态服务器: + +```bash +python -m http.server 8000 --directory src +# 或 +npx http-server src +``` + +### 质量检查 + +```bash +npm run lint +npm run format:check +npm test +``` + +如需自动格式化: + +```bash +npm run format +``` + +### 系统要求 + +- 支持原生 ES Modules 的现代浏览器 +- 支持 `localStorage`(通常约 5–10 MB) +- 可访问 Bootstrap 和 Font Awesome 的 CDN 资源 + +## 使用指南 + +### 基本操作 + +1. **添加任务**:填写标题、描述、进展、优先级等信息。 +2. **编辑任务**:点击任务卡片上的编辑按钮。 +3. **状态管理**:编辑任务并更改状态。 +4. **进度跟踪**:更新任务进度和进展内容,系统会自动生成时间轴记录。 +5. **任务置顶**:点击图钉按钮,使重要任务优先显示。 + +### 任务隐藏 + +- **适用范围**:隐藏仅对待办和已完成任务生效;进行中任务即使被标记隐藏也会正常显示、正常排序。 +- **单个隐藏**:编辑任务并勾选“隐藏此任务”,或点击卡片上的隐藏按钮。 +- **批量查看**:待办栏和已完成栏的眼睛按钮可显示或隐藏被隐藏的任务。 +- **排序规则**:被隐藏的待办和已完成任务在排序结果中自动排到非隐藏任务之后。 +- **视觉区分**:显示隐藏任务时,卡片以半透明样式呈现。 +- **数量统计**:栏目标题的计数与当前实际显示的卡片一致,会随搜索结果变化。 + +### 数据备份 + +- 导出的文件名格式为 `backup-tasks-yyyy-mm-dd.json`。 +- 覆盖导入会替换任务及兼容的排序偏好。 +- 合并导入会保留现有任务,并按 `title|status|createdDate` 跳过重复项;新导入任务会分配唯一 ID。 + +## 技术架构 + +### 技术栈 + +- **前端**:HTML5、CSS3、JavaScript(ES Modules) +- **UI 框架**:Bootstrap 5.3.3 +- **图标**:Font Awesome 6.7.2 +- **存储**:`localStorage` +- **测试**:Vitest + jsdom +- **代码质量**:ESLint + Prettier +- **部署方式**:纯静态部署 + +### 项目结构 + +- `src/index.html`:应用页面和模态框结构 +- `src/css/styles.css`:应用样式和响应式布局 +- `src/js/main.js`:初始化和委托事件处理 +- `src/js/config.js`:状态、排序选项、存储键和共享常量 +- `src/js/utils.js`:ID、日期、到期状态和 HTML 转义工具 +- `src/js/task-model.js`:任务和偏好数据标准化 +- `src/js/store.js`:应用状态及持久化变更 +- `src/js/storage.js`:唯一直接访问 `localStorage` 的应用模块 +- `src/js/sort.js`:任务比较器和 13 种排序模式 +- `src/js/render.js`:任务、时间轴、计数和排序选项渲染 +- `src/js/modal.js`:任务表单填充和保存 +- `src/js/timeline.js`:手动及系统时间轴记录 +- `src/js/import-export.js`:1.0 版备份验证、导入和导出 +- `src/js/notify.js`:通知和截止日期提醒 +- `src/js/seed-data.js`:首次运行时的示例任务 +- `src/js/i18n/`:中英文词典及插值逻辑 +- `test/`:单元测试和旧版数据兼容性测试 + +## 本地存储 + +应用保留以下六个原有存储键: + +- `tasks` +- `taskSortOrders` +- `showHiddenCompletedTasks` +- `showHiddenTodoTasks` +- `tasksInitialized` +- `tasksCorruptedBackup` + +任务、排序和显示偏好使用 JSON 编码。若任务列表格式损坏,原始文本会保存到 `tasksCorruptedBackup`。当浏览器存储不可用或空间不足时,应用会在当前页面会话中继续运行,但刷新后内存中的改动会丢失。 + +## 备份格式 + +```json +{ + "version": "1.0", + "exportDate": "2026-07-20T00:00:00.000Z", + "localDate": "2026-07-20", + "taskCount": 1, + "data": { + "tasks": [], + "sortOrders": { + "todo": "default", + "inProgress": "default", + "completed": "default" + } + } +} +``` + +## 任务数据结构 + +```javascript +{ + id: Number, // 唯一标识 + title: String, // 任务标题 + description: String, // 任务描述 + progressNotes: String, // 任务进展 + status: String, // todo | inProgress | completed + progress: Number, // 0–100 + assignee: String, // 负责人 + collaborators: Array, // 协作者 + dueDate: String, // 截止日期 + priority: String, // low | medium | high + language: String, // zh | en + isPinned: Boolean, // 是否置顶 + isHidden: Boolean, // 是否隐藏 + createdDate: String, // 创建时间 + timeline: Array // 时间轴记录 +} +``` + +## 故障排除 + +- **页面无法直接打开**:请使用 HTTP 服务器运行,原生 ES Modules 不支持 `file://` 加载。 +- **数据丢失**:检查浏览器是否允许 `localStorage`,并定期导出备份。 +- **导入失败**:确认文件为 1.0 版 JSON 备份,并检查任务字段和日期格式。 +- **界面异常**:检查 Bootstrap 和 Font Awesome CDN 是否可访问,并尝试清空缓存。 +- **性能问题**:任务数量较多时,建议归档已完成任务并清理不必要的时间轴记录。 + +## 贡献指南 + +欢迎提交 Issue 和 Pull Request。 + +- **问题反馈**:请详细描述问题场景和复现步骤。 +- **功能建议**:请说明需求和使用场景。 +- **代码贡献**:遵循现有代码风格,并运行 lint、格式检查和测试。 + +## 许可证 + +MIT License + +--- + +- **版本**:v1.0.0 +- **重构日期**:2026 年 7 月 diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..bd49b7f --- /dev/null +++ b/deploy.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +REPO_URL="https://github.com/ghuang-top/Tasks" +REPO_ARCHIVE_URL="${REPO_URL}/archive/refs/heads/main.tar.gz" +TARGET_DIR="/root/data/docker_data/Nginx/html/tasks" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +check_root() { + if [[ ${EUID} -ne 0 ]]; then + print_error "此脚本需要 root 权限运行" + print_info "请切换到 root 用户,或使用 sudo bash 执行脚本" + exit 1 + fi +} + +check_dependencies() { + local command_name + for command_name in curl tar find; do + if ! command -v "$command_name" >/dev/null 2>&1; then + print_error "缺少必要命令: $command_name" + exit 1 + fi + done +} + +prepare_target() { + if [[ -e "$TARGET_DIR" && ! -d "$TARGET_DIR" ]]; then + print_error "目标路径存在,但不是文件夹: $TARGET_DIR" + exit 1 + fi + + mkdir -p "$TARGET_DIR" +} + +clear_target() { + prepare_target + print_info "正在清空目标目录 $TARGET_DIR ..." + + find "$TARGET_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + + + print_success "目标目录已清空" +} + +deploy() ( + local temp_dir archive_file source_dir + temp_dir=$(mktemp -d) + archive_file="$temp_dir/repository.tar.gz" + trap 'rm -rf -- "$temp_dir"' EXIT + + print_info "正在从 GitHub 下载最新版本..." + print_info "仓库地址: $REPO_URL" + if ! curl -fL --retry 3 --connect-timeout 15 "$REPO_ARCHIVE_URL" -o "$archive_file"; then + print_error "仓库下载失败,请检查网络连接及 main 分支是否存在" + return 1 + fi + + print_info "正在解压并验证部署文件..." + if ! tar -xzf "$archive_file" -C "$temp_dir"; then + print_error "仓库压缩包解压失败" + return 1 + fi + + source_dir=$(find "$temp_dir" -mindepth 2 -maxdepth 2 -type d -name src -print -quit) + if [[ -z "$source_dir" || ! -f "$source_dir/index.html" || ! -d "$source_dir/js" || ! -d "$source_dir/css" ]]; then + print_error "仓库中缺少 src/index.html、src/js 或 src/css,已取消部署" + return 1 + fi + + clear_target + + print_info "正在拷贝网站文件到 $TARGET_DIR ..." + cp -a "$source_dir/." "$TARGET_DIR/" + + print_success "Tasks 部署完成" + print_info "部署目录: $TARGET_DIR" +) + +confirm_clear() { + local answer + print_warning "将清空 $TARGET_DIR 中的所有内容,此操作不可恢复" + read -r -p "确认继续?[y/N]: " answer + if [[ "$answer" =~ ^[Yy]$ ]]; then + clear_target + else + print_info "已取消清空" + fi +} + +show_menu() { + clear 2>/dev/null || true + echo -e "${CYAN}============================================${NC}" + echo " Tasks 一键安装脚本" + echo -e "${CYAN}============================================${NC}" + echo -e "${GREEN}1.${NC} 下载并安装最新版本" + echo -e "${GREEN}2.${NC} 清空网站文件" + echo -e "${GREEN}0.${NC} 退出" + echo "" +} + +run_menu() { + local choice + while true; do + show_menu + read -r -p "请选择操作 [0-2]: " choice + case "$choice" in + 1) + # 部署失败时回到菜单,而不是因 set -e 直接退出脚本 + deploy || print_warning "部署失败,可稍后重试" + read -r -p "按 Enter 键继续..." + ;; + 2) + confirm_clear + read -r -p "按 Enter 键继续..." + ;; + 0) + print_info "退出脚本" + return 0 + ;; + *) + print_error "无效选择,请重新输入" + sleep 1 + ;; + esac + done +} + +main() { + check_root + check_dependencies + + case "${1:-menu}" in + deploy) + deploy + ;; + clear) + confirm_clear + ;; + menu) + run_menu + ;; + *) + print_error "未知参数: $1" + echo "用法: $0 [deploy|clear|menu]" >&2 + exit 1 + ;; + esac +} + +main "$@" diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..0a57a9a --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,14 @@ +import js from '@eslint/js'; +import globals from 'globals'; +export default [ + { ignores: ['node_modules/**'] }, + js.configs.recommended, + { + files: ['src/**/*.js'], + languageOptions: { globals: { ...globals.browser, bootstrap: 'readonly' } }, + }, + { + files: ['test/**/*.js'], + languageOptions: { globals: { ...globals.node, ...globals.browser } }, + }, +]; diff --git a/nodebook/2025.06-19.md b/nodebook/2025.06-19.md new file mode 100644 index 0000000..4143eb6 --- /dev/null +++ b/nodebook/2025.06-19.md @@ -0,0 +1,71 @@ +## 中文 + +```yaml +## 新完成 + +## 进行中 + - 1. 所有设备相机参数标准化 + 进展:仅剩DieBond贴片相机待确认。 + + - 2. DieBond设备照明优化 + - I. 定制组合光源:同轴光+环形光 + - II. 采购更窄、更亮的侧面光源,以解决支架白边问题 + 进展:配置已与供应商确认,预计2025年7月4日发货。 + + - 3. DieBond和ClipBond软件界面中英文翻译更新(SG-TATA) + 进展:SG-TATA GUI 的中英文翻译已完成。未来更新杨杰的代码后,需要重新进行翻译(任务量很少)。 + +## 下一步计划 + - 1. 所有设备相机手册(校准、位置引导、检测) + - 2. 开发 TaTa SECS/GEM & Map 软件。 + - 3. 跟进 TaTa 软件授权: + - I. Halcon + - II. CIMConnect RT + - III. CIMConnect CIM142 + - IV. Visual Studio 2019 + - V. Microsoft .NET Runtime(免费) + - 4. 完成 TaTa SECS/GEM 通讯手册,用于支持客户集成机器的 SECS/GEM 功能。 + + - 5. 扫码枪是否需要添加: 用于获取 Wafer 和 Strip 的序列号(涉及 SECS/GEM 相关内容)。 + - 6. 日志文件优化:: 为 LogFile 添加翻译(当前日志文件为中文),格式如下:日期:中文+英文 + - 7. 光源优化(ClipBond 点胶相机): 添加侧灯,以进一步优化芯片上胶点的检测图像效果。 + - 8. 光源优化(ClipBond 向下看相机): 添加侧灯。 + - 9. 光源优化(DieBond Pick Camera): 添加侧灯。当前使用的光源为手工焊接的环光。需要找灵猴定制标准化侧灯(非紧急事项)。 + +``` + +## 英文 + +```yaml +## Newly Completed + +## Work In Progress + - 1. All Machines Camera parameter standardization + Progress: Only DieBond Place camera remains to be confirmed. + + - 2. DieBond machine lighting optimization (DieBond Place Camera) + - I. Custom combination light source: coaxial light + ring light + - II. Purchase narrower and brighter side lighting to solve the white edge problem of the frame + Progress: Configuration has been confirmed with the supplier, expected delivery on July 7, 2025. + + - 3. DieBond and ClipBond Software GUI Chinese-English translation update (SG-TATA) + Progress: The Chinese and English translations for the SG-TATA GUI have been completed. After updating to Yang Jie's code in the future, the translations will need to be redone (little workload). + +## Next To Do + - 1. All Machines Camera Manual (Calibration, Position Guidance, Inspection) + - 2. Develop TaTa SECS/GEM & Map software. + - 3. Follow up on TaTa software licensing: + - I. Halcon + - II. CIMConnect RT + - III. CIMConnect CIM142 + - IV. Visual Studio 2019 + - V. Microsoft .NET Runtime (free) + - 4. Complete TaTa SECS/GEM communication manual to support customers in integrating machine SECS/GEM functionality. + + - 5. Whether to add barcode scanner: for obtaining Wafer and Strip serial numbers (related to SECS/GEM content). + - 6. Log file optimization: Add translation for LogFile (current log files are in Chinese), format as follows: Date: Chinese + English + - 7. Lighting optimization (ClipBond dispensing camera): Add side lighting to further optimize chip adhesive point detection image quality. + - 8. Lighting optimization (ClipBond downward-looking camera): Add side lighting. + - 9. Lighting optimization (DieBond Pick Camera): Add side lighting. Currently using manually welded ring light. Need to find Linghou to customize standardized side lighting (non-urgent task). + +``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f8bce3d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3655 @@ +{ + "name": "task-manager", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "task-manager", + "version": "1.0.0", + "devDependencies": { + "@eslint/js": "latest", + "eslint": "latest", + "globals": "latest", + "jsdom": "latest", + "prettier": "latest", + "serve": "latest", + "vitest": "latest" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@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/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@zeit/schemas": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/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/ansi-align/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/ansi-align/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/ansi-align/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/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": "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/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "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/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/boxen": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", + "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.0", + "chalk": "^5.0.1", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/chalk-template/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/chalk-template/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/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", + "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^2.2.0", + "execa": "^5.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "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/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/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/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/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": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", + "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "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/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "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/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-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/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/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-port-reachable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", + "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/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/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.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": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/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/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.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": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/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-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "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-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?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/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/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/serve": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.6.tgz", + "integrity": "sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zeit/schemas": "2.36.0", + "ajv": "8.18.0", + "arg": "5.0.2", + "boxen": "7.0.0", + "chalk": "5.0.1", + "chalk-template": "0.4.0", + "clipboardy": "3.0.0", + "compression": "1.8.1", + "is-port-reachable": "4.0.0", + "serve-handler": "6.1.7", + "update-check": "1.5.4" + }, + "bin": { + "serve": "build/main.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/serve-handler": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.5", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/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/serve-handler/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/serve-handler/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-handler/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/serve/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/serve/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/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/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "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": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", + "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.9" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", + "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/update-check": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", + "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/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/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..693fd0e --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "task-manager", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "serve src", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check .", + "test": "vitest run --environment jsdom" + }, + "devDependencies": { + "@eslint/js": "latest", + "eslint": "latest", + "globals": "latest", + "jsdom": "latest", + "prettier": "latest", + "serve": "latest", + "vitest": "latest" + } +} diff --git a/src/css/styles.css b/src/css/styles.css new file mode 100644 index 0000000..7a2dd30 --- /dev/null +++ b/src/css/styles.css @@ -0,0 +1,242 @@ +/* Base */ +:root { + --primary: #0d6efd; + --todo: #6c757d; + --progress: #ffc107; + --complete: #198754; + --priority-low: #198754; + --priority-medium: #fd7e14; + --priority-high: #dc3545; + --danger: #dc3545; + --warning: #fd7e14; + --surface: #fff; + --surface-raised: #fffffff0; + --background: #f4f6fa; + --overdue-background: #fff5f5; + --due-soon-background: #fff8f0; + --muted: #6c757d; + --timeline: #dee2e6; + --header-start: #667eea; + --header-end: #764ba2; + --shadow-subtle: #00000012; + --shadow-hover: #0002; + --shadow-pinned: #ffc10744; +} +body { + background: var(--background); + font-family: 'Microsoft YaHei', system-ui, sans-serif; +} +.task-header { + background: linear-gradient(135deg, var(--header-start), var(--header-end)); + color: var(--surface); + padding: 1.25rem; + border-radius: 1rem; + margin-bottom: 1.5rem; +} +.task-header h1 { + font-size: 1.8rem; + margin: 0; +} +.status-column { + background: var(--surface); + border-radius: 1rem; + padding: 1rem; + box-shadow: 0 2px 12px var(--shadow-subtle); + min-height: 12rem; +} +.status-column-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + margin-bottom: 1rem; +} +.status-column-header h4 { + font-size: 1rem; + margin: 0; +} +.sort-selector { + font-size: 0.72rem; + padding: 0.25rem; + border: 1px solid var(--timeline); + border-radius: 0.35rem; + max-width: 45%; +} + +/* Task cards */ +.task-card { + position: relative; + border-left: 4px solid var(--primary); + margin-bottom: 1rem; + transition: 0.2s; +} +.task-card:hover { + transform: translateY(-2px); + box-shadow: 0 5px 14px var(--shadow-hover); +} +.task-card.todo { + border-left-color: var(--todo); +} +.task-card.inProgress { + border-left-color: var(--progress); +} +.task-card.completed { + border-left-color: var(--complete); +} +.task-card.overdue { + border-left-color: var(--danger) !important; + background: var(--overdue-background); +} +.task-card.due-soon { + border-left-color: var(--warning) !important; + background: var(--due-soon-background); +} +.task-card.hidden-task { + opacity: 0.6; +} +.task-card.pinned { + border-top: 3px solid var(--progress); + box-shadow: 0 4px 12px var(--shadow-pinned); +} +.task-actions { + position: absolute; + right: 0.35rem; + top: 0.35rem; + z-index: 2; + background: var(--surface-raised); + border-radius: 0.4rem; + opacity: 0; +} +.task-card:hover .task-actions { + opacity: 1; +} +.task-actions .btn { + padding: 0.25rem 0.4rem; +} +.priority-badge { + font-size: 0.7rem; + border-radius: 1rem; + padding: 0.2rem 0.5rem; + color: var(--surface); +} +.priority-low-badge { + background: var(--priority-low); +} +.priority-medium-badge { + background: var(--priority-medium); +} +.priority-high-badge { + background: var(--priority-high); +} +.progress-custom { + height: 0.45rem; +} +.person-badge { + font-size: 0.75rem; + margin: 0.1rem; +} +.task-description-display { + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +/* Timeline */ +.timeline-toggle { + cursor: pointer; +} +.timeline { + position: relative; + padding-left: 1.25rem; + margin-top: 0.75rem; +} +.timeline:before { + content: ''; + position: absolute; + left: 0.5rem; + inset-block: 0; + width: 2px; + background: var(--timeline); +} +.timeline-item { + position: relative; + padding-bottom: 0.75rem; + margin-bottom: 0.5rem; +} +.timeline-item:before { + content: ''; + position: absolute; + left: -0.72rem; + top: 0.35rem; + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: var(--primary); +} +.timeline-content, +.timeline-form, +.timeline-edit-form { + background: var(--background); + padding: 0.65rem; + border-radius: 0.5rem; + font-size: 0.85rem; +} +.timeline-date { + font-size: 0.75rem; + color: var(--muted); +} +.timeline-item-actions { + float: right; + opacity: 0; +} +.timeline-item:hover .timeline-item-actions { + opacity: 1; +} + +/* Overlays and controls */ +.notification { + position: fixed; + right: 1rem; + top: 1rem; + z-index: 2000; + max-width: min(28rem, 90vw); +} +.visibility-toggle { + padding: 0.1rem; +} +.empty-state { + color: var(--muted); + text-align: center; + padding: 1rem; +} + +/* Responsive */ +@media (max-width: 768px) { + .task-actions, + .timeline-item-actions { + opacity: 0.9; + } + .toolbar .btn { + flex: 1; + } + .status-column { + margin-bottom: 0.5rem; + } +} +@media (max-width: 576px) { + .task-header h1 { + font-size: 1.35rem; + } + .status-column-header { + align-items: flex-start; + } + .sort-selector { + max-width: 50%; + } + .task-actions .btn { + min-width: 2.25rem; + min-height: 2.25rem; + } + .toolbar .btn span:not(#langBtn) { + display: none; + } +} diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..6c70252 --- /dev/null +++ b/src/index.html @@ -0,0 +1,261 @@ + + + + + + 任务管理系统 + + + + + + +
+
+

任务管理系统

+
+
+
+ +
+
+
+ + +
+
+
+ +
+
+
+
+

+ (0) + +

+ +
+
+
+
+
+
+
+

+ (0) +

+ +
+
+
+
+
+
+
+

+ (0) + +

+ +
+
+
+
+
+
+ + +
+ + + + diff --git a/src/js/config.js b/src/js/config.js new file mode 100644 index 0000000..a7c7a4f --- /dev/null +++ b/src/js/config.js @@ -0,0 +1,31 @@ +export const STATUSES = ['todo', 'inProgress', 'completed']; +export const HIDEABLE_STATUSES = ['todo', 'completed']; +export const PRIORITIES = ['low', 'medium', 'high']; +export const PRIORITY_ORDER = { low: 1, medium: 2, high: 3 }; +export const LANGUAGES = ['zh', 'en']; +export const SORT_OPTIONS = [ + ['default', 'defaultSort'], + ['title-asc', 'titleAsc'], + ['title-desc', 'titleDesc'], + ['number-asc', 'numberAsc'], + ['number-desc', 'numberDesc'], + ['dueDate-asc', 'dueDateAsc'], + ['dueDate-desc', 'dueDateDesc'], + ['priority-asc', 'priorityAsc'], + ['priority-desc', 'priorityDesc'], + ['progress-asc', 'progressAsc'], + ['progress-desc', 'progressDesc'], + ['created-desc', 'createdDesc'], + ['created-asc', 'createdAsc'], +]; +export const SORT_VALUES = new Set(SORT_OPTIONS.map(([value]) => value)); +export const DEFAULT_SORT_ORDERS = { todo: 'default', inProgress: 'default', completed: 'default' }; +export const STORAGE_KEYS = { + tasks: 'tasks', + sortOrders: 'taskSortOrders', + initialized: 'tasksInitialized', + corruptedBackup: 'tasksCorruptedBackup', + showHidden: { completed: 'showHiddenCompletedTasks', todo: 'showHiddenTodoTasks' }, +}; +export const EXPORT_VERSION = '1.0'; +export const DUE_SOON_DAYS = 3; diff --git a/src/js/i18n/en.js b/src/js/i18n/en.js new file mode 100644 index 0000000..44075df --- /dev/null +++ b/src/js/i18n/en.js @@ -0,0 +1,123 @@ +export default { + appTitle: 'Task Management System', + todo: 'To Do', + inProgress: 'In Progress', + completed: 'Completed', + addTask: 'Add Task', + editTask: 'Edit Task', + exportData: 'Export Data', + importData: 'Import Data', + searchPlaceholder: 'Search tasks...', + taskTitleLabel: 'Task Title *', + statusLabel: 'Status', + progressNotesLabel: 'Progress Notes', + descriptionLabel: 'Task Description', + priorityLabel: 'Priority', + languageLabel: 'Language', + assigneeLabel: 'Assignee', + dueDateLabel: 'Due Date', + collaboratorsLabel: 'Collaborators', + progressLabel: 'Progress (%)', + hiddenTaskLabel: 'Hide this task (applies to completed and to-do tasks)', + saveTask: 'Save Task', + cancel: 'Cancel', + progressNotesPlaceholder: 'Record the latest progress and status...', + descriptionPlaceholder: 'Describe the task goals and requirements...', + assigneePlaceholder: 'Enter assignee name', + collaboratorsPlaceholder: 'Separate multiple people with commas', + defaultSort: 'Default Sort', + titleAsc: 'Title A-Z', + titleDesc: 'Title Z-A', + numberAsc: 'Number ↑', + numberDesc: 'Number ↓', + dueDateAsc: 'Due Date ↑', + dueDateDesc: 'Due Date ↓', + priorityAsc: 'Priority ↑ (Low→High)', + priorityDesc: 'Priority ↓ (High→Low)', + progressAsc: 'Progress ↑ (Low→High)', + progressDesc: 'Progress ↓ (High→Low)', + createdDesc: 'Newest First', + createdAsc: 'Oldest First', + lowPriority: 'Low Priority', + mediumPriority: 'Medium Priority', + highPriority: 'High Priority', + low: 'Low', + medium: 'Medium', + high: 'High', + assignee: 'Assignee: ', + collaborators: 'Collaborators: ', + dueDate: 'Due: ', + progress: 'Progress: ', + overdue: 'Overdue', + dueSoon: 'Due Soon', + noTasks: 'No tasks', + noSearchResults: 'No matching tasks', + timeline: 'Timeline', + noTimelineEntries: 'No timeline entries', + addTimelinePlaceholder: 'Add timeline entry...', + add: 'Add', + addEntry: 'Add Entry', + edit: 'Edit', + delete: 'Delete', + save: 'Save', + editContentPlaceholder: 'Edit content...', + keyboardHint: 'Ctrl+Enter to save | Esc to cancel', + systemUser: 'System', + defaultUser: 'User', + taskAdded: 'Task added', + taskUpdated: 'Task updated', + taskDeleted: 'Task deleted', + taskPinned: 'Task pinned', + taskUnpinned: 'Task unpinned', + taskHidden: 'Task hidden', + taskShown: 'Task shown', + pinTask: 'Pin task', + unpinTask: 'Unpin task', + showTask: 'Show task', + hideTask: 'Hide task', + taskTitleRequired: 'Task title cannot be empty', + taskNotFound: 'The task no longer exists', + confirmDelete: 'Are you sure you want to delete this task?', + confirmDeleteTimeline: 'Delete this timeline entry?', + contentRequired: 'Content cannot be empty', + timelineEntryAdded: 'Timeline entry added', + timelineEntryUpdated: 'Timeline entry updated', + timelineEntryDeleted: 'Timeline entry deleted', + statusChanged: 'Status Changed', + progressUpdated: 'Progress Updated', + assigneeChanged: 'Assignee Changed', + dueDateChanged: 'Due Date Changed', + priorityChanged: 'Priority Changed', + progressNotesUpdated: 'Progress Notes Updated', + progressCleared: 'Progress notes cleared', + storageTasksReset: 'Local task data is corrupted; the original data was backed up', + storageWriteFailed: + 'Could not save locally; this change is only in memory and will be lost on refresh', + overdueTasks: '{count} task(s) overdue!', + dueSoonTasks: '{count} task(s) due soon!', + allCompletedHidden: 'All completed tasks are hidden ({count})', + allTodoHidden: 'All to-do tasks are hidden ({count})', + showHiddenTasks: 'Show hidden tasks ({count})', + hideHiddenTasks: 'Hide hidden tasks', + showHiddenTitle: 'Show hidden tasks', + hideHiddenTitle: 'Hide hidden tasks', + exportSuccess: 'Data exported successfully', + exportError: 'Export failed', + importSuccess: 'Data imported successfully', + importError: 'Import failed', + invalidFileFormat: 'Invalid file format', + fileReadError: 'File read error', + confirmImport: 'Confirm Import Data', + importMode: 'Import Mode', + replaceMode: 'Replace Existing Data', + replaceModeDesc: 'Will delete all current tasks', + mergeMode: 'Merge Data', + mergeModeDesc: 'Keep existing tasks and add new tasks', + confirmImportBtn: 'Confirm Import', + importFileInfo: 'Import File Information', + fileVersion: 'File Version', + taskCount: 'Task Count', + exportDate: 'Export Date', + currentTaskCount: 'There are currently {count} tasks', + duplicateTasksSkipped: 'Skipped {count} duplicate tasks', +}; diff --git a/src/js/i18n/index.js b/src/js/i18n/index.js new file mode 100644 index 0000000..8a3b565 --- /dev/null +++ b/src/js/i18n/index.js @@ -0,0 +1,29 @@ +import zh from './zh.js'; +import en from './en.js'; +const dictionaries = { zh, en }; +let language = 'zh'; +export const getLanguage = () => language; +export function t(key, params = {}) { + const template = dictionaries[language][key] ?? key; + return Object.entries(params).reduce( + (text, [name, value]) => text.replaceAll(`{${name}}`, String(value)), + template, + ); +} +export function applyTranslations(root = document) { + document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en'; + document.title = t('appTitle'); + root.querySelectorAll('[data-key]').forEach((element) => { + element.textContent = t(element.dataset.key); + }); + root.querySelectorAll('[data-key-placeholder]').forEach((element) => { + element.placeholder = t(element.dataset.keyPlaceholder); + }); + const button = document.querySelector('#langBtn'); + if (button) button.textContent = language === 'zh' ? 'EN' : '中文'; +} +export function toggleLanguage() { + language = language === 'zh' ? 'en' : 'zh'; + applyTranslations(); + return language; +} diff --git a/src/js/i18n/zh.js b/src/js/i18n/zh.js new file mode 100644 index 0000000..8187f02 --- /dev/null +++ b/src/js/i18n/zh.js @@ -0,0 +1,122 @@ +export default { + appTitle: '任务管理系统', + todo: '待办事项', + inProgress: '进行中', + completed: '已完成', + addTask: '添加任务', + editTask: '编辑任务', + exportData: '导出数据', + importData: '导入数据', + searchPlaceholder: '搜索任务...', + taskTitleLabel: '任务标题 *', + statusLabel: '状态', + progressNotesLabel: '任务进展', + descriptionLabel: '任务描述', + priorityLabel: '优先级', + languageLabel: '语言', + assigneeLabel: '负责人', + dueDateLabel: '到期日期', + collaboratorsLabel: '协助人员', + progressLabel: '进度 (%)', + hiddenTaskLabel: '隐藏此任务(对已完成和待办事项生效)', + saveTask: '保存任务', + cancel: '取消', + progressNotesPlaceholder: '记录任务的最新进展、当前状态等...', + descriptionPlaceholder: '描述任务的目标、要求和基本信息...', + assigneePlaceholder: '输入负责人姓名', + collaboratorsPlaceholder: '用逗号分隔多个人员', + defaultSort: '默认排序', + titleAsc: '标题 A-Z', + titleDesc: '标题 Z-A', + numberAsc: '数字排序 ↑', + numberDesc: '数字排序 ↓', + dueDateAsc: '到期日期 ↑', + dueDateDesc: '到期日期 ↓', + priorityAsc: '优先级 ↑ (低→高)', + priorityDesc: '优先级 ↓ (高→低)', + progressAsc: '进度 ↑ (低→高)', + progressDesc: '进度 ↓ (高→低)', + createdDesc: '最新创建', + createdAsc: '最早创建', + lowPriority: '低优先级', + mediumPriority: '中优先级', + highPriority: '高优先级', + low: '低', + medium: '中', + high: '高', + assignee: '负责人:', + collaborators: '协助:', + dueDate: '到期:', + progress: '进度:', + overdue: '已过期', + dueSoon: '即将到期', + noTasks: '暂无任务', + noSearchResults: '没有匹配的任务', + timeline: '时间轴', + noTimelineEntries: '暂无时间轴记录', + addTimelinePlaceholder: '添加时间轴条目...', + add: '添加', + addEntry: '添加条目', + edit: '编辑', + delete: '删除', + save: '保存', + editContentPlaceholder: '编辑内容...', + keyboardHint: 'Ctrl+Enter保存 | Esc取消', + systemUser: '系统', + defaultUser: '用户', + taskAdded: '任务已添加', + taskUpdated: '任务已更新', + taskDeleted: '任务已删除', + taskPinned: '任务置顶', + taskUnpinned: '取消置顶', + taskHidden: '任务已隐藏', + taskShown: '任务已显示', + pinTask: '置顶任务', + unpinTask: '取消置顶', + showTask: '显示任务', + hideTask: '隐藏任务', + taskTitleRequired: '任务标题不能为空', + taskNotFound: '任务不存在或已被删除', + confirmDelete: '确定要删除这个任务吗?', + confirmDeleteTimeline: '确定要删除这个时间轴条目吗?', + contentRequired: '内容不能为空', + timelineEntryAdded: '时间轴条目已添加', + timelineEntryUpdated: '时间轴条目已更新', + timelineEntryDeleted: '时间轴条目已删除', + statusChanged: '状态变更', + progressUpdated: '进度更新', + assigneeChanged: '负责人变更', + dueDateChanged: '到期日期变更', + priorityChanged: '优先级变更', + progressNotesUpdated: '任务进展更新', + progressCleared: '进展内容已清空', + storageTasksReset: '本地任务数据损坏,无法加载;原始数据已备份', + storageWriteFailed: '无法保存到本地存储;当前修改仅保留在内存中,刷新后会丢失', + overdueTasks: '{count} 个任务已过期!', + dueSoonTasks: '{count} 个任务即将到期!', + allCompletedHidden: '所有已完成任务都已隐藏 ({count} 个)', + allTodoHidden: '所有待办事项都已隐藏 ({count} 个)', + showHiddenTasks: '显示隐藏任务 ({count}个)', + hideHiddenTasks: '隐藏已被隐藏的任务', + showHiddenTitle: '显示被隐藏的任务', + hideHiddenTitle: '隐藏被隐藏的任务', + exportSuccess: '数据导出成功', + exportError: '导出失败', + importSuccess: '数据导入成功', + importError: '导入失败', + invalidFileFormat: '无效的文件格式', + fileReadError: '文件读取失败', + confirmImport: '确认导入数据', + importMode: '导入模式', + replaceMode: '覆盖现有数据', + replaceModeDesc: '将删除当前所有任务', + mergeMode: '合并数据', + mergeModeDesc: '保留现有任务,添加新任务', + confirmImportBtn: '确认导入', + importFileInfo: '导入文件信息', + fileVersion: '文件版本', + taskCount: '任务数量', + exportDate: '导出时间', + currentTaskCount: '当前系统中有 {count} 个任务', + duplicateTasksSkipped: '已跳过 {count} 个重复任务', +}; diff --git a/src/js/import-export.js b/src/js/import-export.js new file mode 100644 index 0000000..9ee5127 --- /dev/null +++ b/src/js/import-export.js @@ -0,0 +1,115 @@ +import { EXPORT_VERSION, STATUSES } from './config.js'; +import { t } from './i18n/index.js'; +import { normalizeSortOrders, normalizeTask, normalizeTasks, isValidId } from './task-model.js'; +import { getSortOrders, getTasks, replaceSortOrders, replaceTasks } from './store.js'; +import { escapeHtml, formatLocalDate, generateUniqueId, parseDueDateEnd } from './utils.js'; + +const REVOKE_DELAY_MS = 10000; +let pending = null; +export function validateImportData(data) { + if ( + !data || + typeof data !== 'object' || + data.version !== EXPORT_VERSION || + !data.data || + !Array.isArray(data.data.tasks) + ) + return false; + return data.data.tasks.every( + (task) => + task && + typeof task === 'object' && + isValidId(task.id) && + typeof task.title === 'string' && + task.title.trim() && + STATUSES.includes(task.status) && + (task.dueDate == null || task.dueDate === '' || parseDueDateEnd(task.dueDate)), + ); +} +export function mergeTasks(existing, incoming) { + const keys = new Set(existing.map((task) => `${task.title}|${task.status}|${task.createdDate}`)); + const ids = new Set(existing.map((task) => task.id)); + const added = []; + let skipped = 0; + incoming.forEach((raw) => { + const created = typeof raw.createdDate === 'string' ? raw.createdDate : ''; + const task = normalizeTask({ ...raw, id: generateUniqueId(ids) }); + const key = created ? `${task.title}|${task.status}|${created}` : null; + if (key && keys.has(key)) { + skipped += 1; + return; + } + if (key) keys.add(key); + added.push(task); + }); + return { tasks: [...existing, ...added], skipped }; +} +export function createExportData(now = new Date()) { + return { + version: EXPORT_VERSION, + exportDate: now.toISOString(), + localDate: formatLocalDate(now), + taskCount: getTasks().length, + data: { tasks: getTasks(), sortOrders: getSortOrders() }, + }; +} +export function exportData() { + const data = createExportData(); + const filename = `backup-tasks-${data.localDate}.json`; + const url = URL.createObjectURL( + new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), + ); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + // Firefox only downloads from an anchor in the document, and the blob must outlive the click + // because browsers start the transfer asynchronously. + document.body.append(link); + link.click(); + link.remove(); + setTimeout(() => URL.revokeObjectURL(url), REVOKE_DELAY_MS); + return filename; +} +export function readImportFile(file, onReady, onInvalid, onReadError) { + pending = null; + const reader = new FileReader(); + reader.onload = () => { + try { + const data = JSON.parse(reader.result); + if (!validateImportData(data)) { + onInvalid(); + return; + } + pending = data; + onReady(data); + } catch { + onInvalid(); + } + }; + reader.onerror = onReadError; + reader.readAsText(file); +} +export function showImportDialog(data) { + const date = + data.exportDate && !Number.isNaN(Date.parse(data.exportDate)) + ? new Date(data.exportDate).toLocaleString() + : '-'; + document.querySelector('#importInfo').innerHTML = + `
${escapeHtml(t('importFileInfo'))}
${escapeHtml(t('currentTaskCount', { count: getTasks().length }))}
`; + bootstrap.Modal.getOrCreateInstance(document.querySelector('#importModal')).show(); +} +export function executeImport(mode) { + if (!pending) throw new Error('No import pending'); + if (!['replace', 'merge'].includes(mode)) throw new TypeError('Invalid import mode'); + let skipped = 0; + if (mode === 'replace') replaceTasks(normalizeTasks(pending.data.tasks)); + else { + const merged = mergeTasks(getTasks(), pending.data.tasks); + replaceTasks(merged.tasks); + skipped = merged.skipped; + } + replaceSortOrders(normalizeSortOrders(pending.data.sortOrders)); + pending = null; + bootstrap.Modal.getOrCreateInstance(document.querySelector('#importModal')).hide(); + return skipped; +} diff --git a/src/js/main.js b/src/js/main.js new file mode 100644 index 0000000..136c5e8 --- /dev/null +++ b/src/js/main.js @@ -0,0 +1,266 @@ +import { SORT_VALUES, STATUSES } from './config.js'; +import { applyTranslations, t, toggleLanguage } from './i18n/index.js'; +import { executeImport, exportData, readImportFile, showImportDialog } from './import-export.js'; +import { openTaskModal, saveTask, updateProgressDisplay } from './modal.js'; +import { checkDueDates, showNotification } from './notify.js'; +import { + applySearchFilter, + isTimelineExpanded, + pruneTaskCaches, + renderAllTasks, + renderColumn, + setTimelineExpanded, + updateTaskCounts, +} from './render.js'; +import { createSeedTasks } from './seed-data.js'; +import { + getShowHidden, + getTasks, + initializeStore, + removeTask, + setShowHidden, + setSortOrder, + toggleHidden, + togglePin, + wasCorrupted, +} from './store.js'; +import { consumeStorageWriteFailure, isInitialized } from './storage.js'; +import { addTimelineEntry, deleteTimelineEntry, updateTimelineEntry } from './timeline.js'; +import { isHiddenTask } from './utils.js'; + +function renderAffected(...statuses) { + [...new Set(statuses.filter(Boolean))].forEach(renderColumn); +} +function notifyPersisted(message, type = 'info') { + if (consumeStorageWriteFailure()) { + showNotification(t('storageWriteFailed'), 'danger'); + return false; + } + if (message) showNotification(message, type); + return true; +} +function taskAction(event) { + const action = event.target.closest('[data-action]'); + if (!action) return; + const card = action.closest('[data-task-id]'); + const id = Number(card?.dataset.taskId); + const status = card?.closest('.task-list')?.dataset.status; + if (action.dataset.action === 'toggle-show-hidden') { + const value = !getShowHidden(action.dataset.status); + setShowHidden(action.dataset.status, value); + renderColumn(action.dataset.status); + notifyPersisted( + t(value ? 'showHiddenTasks' : 'hideHiddenTasks', { + count: getTasks().filter( + (task) => task.status === action.dataset.status && isHiddenTask(task), + ).length, + }), + ); + return; + } + if (!card) return; + const timelineItem = action.closest('[data-timeline-id]'); + const timelineId = Number(timelineItem?.dataset.timelineId); + switch (action.dataset.action) { + case 'pin': { + const task = togglePin(id); + if (!task) { + showNotification(t('taskNotFound'), 'warning'); + break; + } + renderColumn(status); + notifyPersisted(t(task.isPinned ? 'taskPinned' : 'taskUnpinned')); + break; + } + case 'hide': { + const task = toggleHidden(id); + if (!task) { + showNotification(t('taskNotFound'), 'warning'); + break; + } + renderColumn(status); + notifyPersisted(t(task.isHidden ? 'taskHidden' : 'taskShown')); + break; + } + case 'edit': + openTaskModal(id); + break; + case 'delete': + if (confirm(t('confirmDelete'))) { + if (!removeTask(id)) { + showNotification(t('taskNotFound'), 'warning'); + break; + } + pruneTaskCaches(); + renderColumn(status); + notifyPersisted(t('taskDeleted'), 'warning'); + } + break; + case 'toggle-timeline': + setTimelineExpanded(id, !isTimelineExpanded(id)); + renderColumn(status); + break; + case 'show-add-timeline': { + const form = action.closest('.timeline').querySelector('.timeline-form'); + form.classList.remove('d-none'); + form.querySelector('input').focus(); + break; + } + case 'cancel-add-timeline': + action.closest('.timeline-form').classList.add('d-none'); + break; + case 'add-timeline': { + const input = action.closest('.timeline-form').querySelector('input'); + if (addTimelineEntry(id, input.value)) { + setTimelineExpanded(id, true); + renderColumn(status); + notifyPersisted(t('timelineEntryAdded'), 'success'); + } else showNotification(t('contentRequired'), 'warning'); + break; + } + case 'edit-timeline': + timelineItem.querySelector('.timeline-content').classList.add('d-none'); + timelineItem.querySelector('.timeline-edit-form').classList.remove('d-none'); + timelineItem.querySelector('textarea').focus(); + break; + case 'cancel-timeline': + timelineItem.querySelector('.timeline-content').classList.remove('d-none'); + timelineItem.querySelector('.timeline-edit-form').classList.add('d-none'); + break; + case 'save-timeline': { + const value = timelineItem.querySelector('textarea').value; + if (updateTimelineEntry(id, timelineId, value)) { + setTimelineExpanded(id, true); + renderColumn(status); + notifyPersisted(t('timelineEntryUpdated'), 'success'); + } else showNotification(t('contentRequired'), 'warning'); + break; + } + case 'delete-timeline': + if (confirm(t('confirmDeleteTimeline')) && deleteTimelineEntry(id, timelineId)) { + setTimelineExpanded(id, true); + renderColumn(status); + notifyPersisted(t('timelineEntryDeleted'), 'warning'); + } + break; + } +} +function bind() { + document.querySelector('#addTaskBtn').addEventListener('click', () => openTaskModal()); + document.querySelector('#saveTaskBtn').addEventListener('click', () => { + const result = saveTask(); + if (result?.error) { + showNotification(t(result.error), 'warning'); + return; + } + if (result) { + renderAffected(result.oldStatus, result.task.status); + notifyPersisted(t(result.oldStatus ? 'taskUpdated' : 'taskAdded'), 'success'); + } + }); + document.querySelector('#taskProgress').addEventListener('input', updateProgressDisplay); + const runSearch = () => { + applySearchFilter(); + updateTaskCounts(); + }; + document.querySelector('#searchInput').addEventListener('input', runSearch); + document.querySelector('#searchBtn').addEventListener('click', runSearch); + document.querySelector('#languageBtn').addEventListener('click', () => { + toggleLanguage(); + renderAllTasks(); + }); + document.querySelector('#exportBtn').addEventListener('click', () => { + try { + showNotification(`${t('exportSuccess')} (${exportData()})`, 'success'); + } catch { + showNotification(t('exportError'), 'danger'); + } + }); + document + .querySelector('#importBtn') + .addEventListener('click', () => document.querySelector('#fileInput').click()); + document.querySelector('#fileInput').addEventListener('change', (event) => { + const file = event.target.files[0]; + if (file) + readImportFile( + file, + showImportDialog, + () => showNotification(t('invalidFileFormat'), 'danger'), + () => showNotification(t('fileReadError'), 'danger'), + ); + event.target.value = ''; + }); + document.querySelector('#confirmImportBtn').addEventListener('click', () => { + try { + const mode = document.querySelector('[name="importMode"]:checked').value; + const skipped = executeImport(mode); + pruneTaskCaches(); + renderAllTasks(); + if (notifyPersisted(t('importSuccess'), 'success') && skipped) + showNotification(t('duplicateTasksSkipped', { count: skipped })); + } catch { + showNotification(t('importError'), 'danger'); + } + }); + document.querySelector('#taskBoard').addEventListener('click', taskAction); + document.querySelector('#taskBoard').addEventListener('change', (event) => { + if ( + event.target.matches('.sort-selector') && + STATUSES.includes(event.target.dataset.status) && + SORT_VALUES.has(event.target.value) + ) { + setSortOrder(event.target.dataset.status, event.target.value); + renderColumn(event.target.dataset.status); + notifyPersisted(); + } + }); + document.querySelector('#taskBoard').addEventListener('keydown', (event) => { + const card = event.target.closest('[data-task-id]'); + if (!card) return; + const status = card.closest('.task-list').dataset.status; + if (event.key === 'Enter' && event.target.matches('[data-role="timeline-input"]')) { + event.preventDefault(); + if (addTimelineEntry(Number(card.dataset.taskId), event.target.value)) { + setTimelineExpanded(Number(card.dataset.taskId), true); + renderColumn(status); + notifyPersisted(t('timelineEntryAdded'), 'success'); + } else showNotification(t('contentRequired'), 'warning'); + } + if (event.target.matches('[data-role="timeline-edit"]') && event.key === 'Escape') { + event.preventDefault(); + event.target.closest('.timeline-edit-form').classList.add('d-none'); + event.target + .closest('.timeline-item') + .querySelector('.timeline-content') + .classList.remove('d-none'); + } + if ( + event.target.matches('[data-role="timeline-edit"]') && + event.key === 'Enter' && + event.ctrlKey + ) { + event.preventDefault(); + const item = event.target.closest('[data-timeline-id]'); + if ( + updateTimelineEntry( + Number(card.dataset.taskId), + Number(item.dataset.timelineId), + event.target.value, + ) + ) { + setTimelineExpanded(Number(card.dataset.taskId), true); + renderColumn(status); + notifyPersisted(t('timelineEntryUpdated'), 'success'); + } else showNotification(t('contentRequired'), 'warning'); + } + }); +} +document.addEventListener('DOMContentLoaded', () => { + initializeStore(createSeedTasks(), isInitialized()); + applyTranslations(); + renderAllTasks(); + bind(); + if (wasCorrupted()) showNotification(t('storageTasksReset'), 'danger'); + notifyPersisted(); + checkDueDates(); +}); diff --git a/src/js/modal.js b/src/js/modal.js new file mode 100644 index 0000000..502c633 --- /dev/null +++ b/src/js/modal.js @@ -0,0 +1,82 @@ +import { t } from './i18n/index.js'; +import { addTask, getTask, getTasks, updateTask } from './store.js'; +import { addSystemEntries } from './timeline.js'; +import { generateUniqueId } from './utils.js'; + +const byId = (id) => document.querySelector(`#${id}`); +export function updateProgressDisplay() { + byId('progressDisplay').textContent = `${byId('taskProgress').value}%`; +} +export function openTaskModal(taskId = null) { + const form = byId('taskForm'); + form.reset(); + byId('taskId').value = taskId ?? ''; + const task = taskId ? getTask(taskId) : null; + byId('modalTitle').dataset.key = task ? 'editTask' : 'addTask'; + byId('modalTitle').textContent = t(task ? 'editTask' : 'addTask'); + if (task) { + byId('taskTitle').value = task.title; + byId('taskDescription').value = task.description; + byId('taskProgressNotes').value = task.progressNotes; + byId('taskStatus').value = task.status; + byId('taskProgress').value = task.progress; + byId('taskAssignee').value = task.assignee; + byId('taskDueDate').value = task.dueDate; + byId('taskCollaborators').value = task.collaborators.join(', '); + byId('taskLanguage').value = task.language; + byId('taskPriority').value = task.priority; + byId('taskHidden').checked = task.isHidden; + } else { + byId('taskPriority').value = 'low'; + byId('taskLanguage').value = 'zh'; + } + updateProgressDisplay(); + bootstrap.Modal.getOrCreateInstance(byId('taskModal')).show(); +} +function values() { + return { + title: byId('taskTitle').value.trim(), + description: byId('taskDescription').value, + progressNotes: byId('taskProgressNotes').value, + status: byId('taskStatus').value, + progress: Number(byId('taskProgress').value), + assignee: byId('taskAssignee').value, + dueDate: byId('taskDueDate').value, + collaborators: byId('taskCollaborators') + .value.split(',') + .map((v) => v.trim()) + .filter(Boolean), + language: byId('taskLanguage').value, + priority: byId('taskPriority').value, + isHidden: byId('taskHidden').checked, + }; +} +export function saveTask() { + const data = values(); + const title = byId('taskTitle'); + title.setCustomValidity(data.title ? '' : t('taskTitleRequired')); + if (!byId('taskForm').reportValidity()) return null; + const id = Number(byId('taskId').value); + let oldStatus = null; + let task; + if (id) { + const old = getTask(id); + if (!old) return { error: 'taskNotFound' }; + oldStatus = old.status; + task = updateTask(id, { ...data, timeline: addSystemEntries(old, data) }); + } else { + task = addTask({ + id: generateUniqueId(new Set(getTasks().map((item) => item.id))), + ...data, + isPinned: false, + createdDate: new Date().toISOString(), + timeline: [], + }); + if (data.progressNotes) + task = updateTask(task.id, { + timeline: addSystemEntries({ ...task, progressNotes: '' }, data), + }); + } + bootstrap.Modal.getOrCreateInstance(byId('taskModal')).hide(); + return { task, oldStatus }; +} diff --git a/src/js/notify.js b/src/js/notify.js new file mode 100644 index 0000000..ec6453c --- /dev/null +++ b/src/js/notify.js @@ -0,0 +1,30 @@ +import { t } from './i18n/index.js'; +import { getTasks } from './store.js'; +import { getDueDateState } from './utils.js'; +export function showNotification(message, type = 'info') { + const alert = document.createElement('div'); + alert.className = `alert alert-${type} alert-dismissible fade show`; + const text = document.createElement('span'); + text.textContent = String(message); + const close = document.createElement('button'); + close.className = 'btn-close'; + close.dataset.bsDismiss = 'alert'; + alert.append(text, close); + document.querySelector('#notifications').append(alert); + setTimeout(() => alert.remove(), 5000); +} +export function checkDueDates(now = new Date()) { + let overdue = 0, + soon = 0; + getTasks().forEach((task) => { + if (!task.dueDate || task.status === 'completed') return; + const state = getDueDateState(task.dueDate, now); + if (state.isOverdue) overdue += 1; + else if (state.isDueSoon) soon += 1; + }); + const parts = []; + if (overdue) parts.push(t('overdueTasks', { count: overdue })); + if (soon) parts.push(t('dueSoonTasks', { count: soon })); + if (parts.length) showNotification(parts.join(' '), overdue ? 'danger' : 'warning'); + return { overdue, soon }; +} diff --git a/src/js/render.js b/src/js/render.js new file mode 100644 index 0000000..1bec4b5 --- /dev/null +++ b/src/js/render.js @@ -0,0 +1,192 @@ +import { HIDEABLE_STATUSES, SORT_OPTIONS, STATUSES } from './config.js'; +import { t } from './i18n/index.js'; +import { getShowHidden, getSortOrders, getTasks } from './store.js'; +import { sortTasks } from './sort.js'; +import { escapeHtml, getDueDateState, isHiddenTask } from './utils.js'; + +const expanded = new Set(); +const searchIndexes = new Map(); +export const setTimelineExpanded = (id, value) => (value ? expanded.add(id) : expanded.delete(id)); +export const isTimelineExpanded = (id) => expanded.has(id); +export function pruneTaskCaches(taskIds = getTasks().map((task) => task.id)) { + const validIds = new Set(taskIds); + [expanded, searchIndexes].forEach((cache) => { + [...cache.keys()].forEach((id) => { + if (!validIds.has(id)) cache.delete(id); + }); + }); +} +export function renderSortOptions() { + const orders = getSortOrders(); + document.querySelectorAll('.sort-selector').forEach((select) => { + select.innerHTML = SORT_OPTIONS.map( + ([value, key]) => ``, + ).join(''); + select.value = orders[select.dataset.status]; + }); +} +function timelineContent(item) { + if (!item.event) return String(item.content ?? ''); + const values = item.event.values ?? {}; + const from = values.from || '-'; + const to = values.to || '-'; + if (item.event.key === 'progressNotesUpdated') { + const value = String(values.value ?? '').trim(); + return value ? `${value.slice(0, 50)}${value.length > 50 ? '...' : ''}` : t('progressCleared'); + } + if (item.event.key === 'progressUpdated') + return `${t('progressUpdated')}: ${values.from ?? '-'}% → ${values.to ?? '-'}%`; + if (['statusChanged', 'priorityChanged'].includes(item.event.key)) + return `${t(item.event.key)}: ${t(from)} → ${t(to)}`; + return `${t(item.event.key)}: ${from} → ${to}`; +} +function entryTime(item) { + // Imported entries can carry an unparsable date; treat those as the oldest instead of comparing NaN. + const time = Date.parse(item.date); + return Number.isNaN(time) ? -Infinity : time; +} +function timelineOrder(a, b) { + // Entry dates have day granularity, so ids (allocated in increasing order) break same-day ties. + const left = entryTime(a); + const right = entryTime(b); + return left === right ? b.id - a.id : right - left; +} +function createTimeline(task) { + const items = [...task.timeline].sort(timelineOrder); + const rows = items.length + ? items + .map( + (item) => + `
${escapeHtml(item.date)} - ${escapeHtml(item.userType === 'system' ? t('systemUser') : item.user)}
${escapeHtml(timelineContent(item))}
${t('keyboardHint')}
`, + ) + .join('') + : `

${t('noTimelineEntries')}

`; + return `${rows}
`; +} +function createTaskActions(task) { + const hiddenAction = HIDEABLE_STATUSES.includes(task.status) + ? `` + : ''; + return `
${hiddenAction}
`; +} +function createPriorityBadge(task) { + return `${t(task.priority)}`; +} +function createPeople(task) { + const assignee = task.assignee + ? `
${t('assignee')}${escapeHtml(task.assignee)}
` + : ''; + const collaborators = task.collaborators + .map((name) => `${escapeHtml(name)}`) + .join(''); + return `${assignee}${collaborators ? `
${t('collaborators')}${collaborators}
` : ''}`; +} +function createProgress(task) { + const notes = task.progressNotes + ? `
${t('progressNotesLabel')}:
${escapeHtml(task.progressNotes)}
` + : ''; + const bar = + task.progress > 0 + ? `
${t('progress')}${task.progress}%` + : ''; + return `${notes}${bar}`; +} +function createDueDateBlock(task, due) { + if (!task.dueDate) return ''; + const dateClass = due.isOverdue ? 'text-danger' : due.isDueSoon ? 'text-warning' : ''; + const badge = due.isOverdue + ? `${t('overdue')}` + : due.isDueSoon + ? `${t('dueSoon')}` + : ''; + return `
${t('dueDate')}${escapeHtml(task.dueDate)} ${badge}
`; +} +function buildSearchIndex(task) { + return [ + task.title, + task.description, + task.progressNotes, + task.assignee, + ...task.collaborators, + task.dueDate, + t(task.priority), + ...task.timeline.map(timelineContent), + ] + .join(' ') + .toLowerCase(); +} +function createCard(task) { + // Indexed here so that a rendered card always has a matching entry for applySearchFilter. + searchIndexes.set(task.id, buildSearchIndex(task)); + const due = getDueDateState(task.dueDate); + const open = expanded.has(task.id); + const classes = [ + 'task-card', + 'card', + task.status, + `priority-${task.priority}`, + task.isPinned ? 'pinned' : '', + isHiddenTask(task) ? 'hidden-task' : '', + due.isOverdue ? 'overdue' : due.isDueSoon ? 'due-soon' : '', + ] + .filter(Boolean) + .join(' '); + const description = task.description + ? `

${escapeHtml(task.description)}

` + : ''; + return `
${createTaskActions(task)}
${escapeHtml(task.title)}
${createPriorityBadge(task)}
${description}${createProgress(task)}${createPeople(task)}${createDueDateBlock(task, due)}
${t('timeline')} (${task.timeline.length})
${createTimeline(task)}
`; +} +export function updateTaskCounts() { + const tasks = getTasks(); + STATUSES.forEach((status) => { + const cards = [...document.querySelectorAll(`#${status}Tasks .task-card`)]; + document.querySelector(`#${status}Count`).textContent = cards.filter( + (card) => !card.hidden, + ).length; + }); + HIDEABLE_STATUSES.forEach((status) => { + const button = document.querySelector( + `[data-action="toggle-show-hidden"][data-status="${status}"]`, + ); + const count = tasks.filter((task) => task.status === status && isHiddenTask(task)).length; + button.hidden = count === 0; + button.title = t(getShowHidden(status) ? 'hideHiddenTitle' : 'showHiddenTitle'); + button.querySelector('i').className = `fas fa-${getShowHidden(status) ? 'eye-slash' : 'eye'}`; + }); +} +function updateSearchEmptyState(list) { + const cards = [...list.querySelectorAll('.task-card')]; + const notice = list.querySelector('.search-empty-state'); + if (cards.length && cards.every((card) => card.hidden)) { + const node = notice ?? document.createElement('p'); + node.className = 'empty-state search-empty-state'; + node.textContent = t('noSearchResults'); + if (!notice) list.append(node); + } else notice?.remove(); +} +export function applySearchFilter(root = document) { + const term = document.querySelector('#searchInput').value.trim().toLowerCase(); + root.querySelectorAll('.task-card').forEach((card) => { + const index = searchIndexes.get(Number(card.dataset.taskId)) ?? ''; + card.hidden = Boolean(term && !index.includes(term)); + }); + const lists = root.matches?.('.task-list') ? [root] : [...root.querySelectorAll('.task-list')]; + lists.forEach(updateSearchEmptyState); +} +export function renderColumn(status, updateCounts = true) { + const all = getTasks().filter((task) => task.status === status); + const hidden = all.filter(isHiddenTask).length; + const shown = getShowHidden(status) ? all : all.filter((task) => !isHiddenTask(task)); + const ordered = sortTasks(shown, getSortOrders()[status]); + const key = status === 'completed' ? 'allCompletedHidden' : 'allTodoHidden'; + document.querySelector(`#${status}Tasks`).innerHTML = ordered.length + ? ordered.map(createCard).join('') + : `

${hidden ? t(key, { count: hidden }) : t('noTasks')}

`; + applySearchFilter(document.querySelector(`#${status}Tasks`)); + if (updateCounts) updateTaskCounts(); +} +export function renderAllTasks() { + STATUSES.forEach((status) => renderColumn(status, false)); + updateTaskCounts(); + renderSortOptions(); +} diff --git a/src/js/seed-data.js b/src/js/seed-data.js new file mode 100644 index 0000000..436923f --- /dev/null +++ b/src/js/seed-data.js @@ -0,0 +1,94 @@ +export function createSeedTasks(now = new Date()) { + const createdDate = now.toISOString(); + return [ + { + id: 1, + title: '所有设备相机参数标准化', + description: '完成所有生产设备的相机参数标准化工作,确保图像采集的一致性和可靠性。', + progressNotes: '目前已完成大部分设备的参数标准化工作,仅剩DieBond贴片相机待确认参数配置。', + status: 'inProgress', + progress: 90, + assignee: '技术团队', + collaborators: [], + dueDate: '2025-02-15', + createdDate, + language: 'zh', + priority: 'high', + isPinned: false, + timeline: [], + isHidden: false, + }, + { + id: 2, + title: 'DieBond设备照明优化', + description: + '定制组合光源:同轴光+环形光,采购更窄、更亮的侧面光源,以解决支架白边问题,提升图像质量。', + progressNotes: + '配置方案已与供应商确认,技术参数符合要求。供应商承诺2025年7月4日发货,目前正在生产中。', + status: 'inProgress', + progress: 60, + assignee: '硬件团队', + collaborators: ['供应商'], + dueDate: '2025-07-04', + createdDate, + language: 'zh', + priority: 'medium', + isPinned: true, + timeline: [], + isHidden: false, + }, + { + id: 3, + title: 'DieBond和ClipBond软件界面中英文翻译更新', + description: '完成DieBond和ClipBond设备软件界面的中英文翻译工作,支持多语言操作环境。', + progressNotes: + 'SG-TATA GUI 的中英文翻译已完成。未来更新杨杰的代码后,需要重新进行翻译(任务量很少)。', + status: 'inProgress', + progress: 80, + assignee: '杨杰', + collaborators: ['翻译团队'], + dueDate: '2025-02-28', + createdDate, + language: 'zh', + priority: 'medium', + isPinned: false, + timeline: [], + isHidden: false, + }, + { + id: 4, + title: '所有设备相机手册', + description: '编写所有设备相机的操作手册,包括校准流程、位置引导方法、检测标准等技术文档。', + progressNotes: '任务刚启动,正在收集各设备的相机技术规格和操作要求。', + status: 'todo', + progress: 0, + assignee: '技术文档组', + collaborators: [], + dueDate: '2025-03-31', + createdDate, + language: 'zh', + priority: 'low', + isPinned: false, + timeline: [], + isHidden: false, + }, + { + id: 5, + title: '开发 TaTa SECS/GEM & Map 软件', + description: + '开发符合SEMI标准的SECS/GEM通信协议软件,实现设备与MES系统的标准化通信,包括Map数据管理功能。', + progressNotes: '需求分析阶段,正在梳理SECS/GEM协议规范和技术架构设计。', + status: 'todo', + progress: 0, + assignee: '软件开发组', + collaborators: ['系统架构师'], + dueDate: '2025-06-30', + createdDate, + language: 'zh', + priority: 'high', + isPinned: false, + timeline: [], + isHidden: false, + }, + ]; +} diff --git a/src/js/sort.js b/src/js/sort.js new file mode 100644 index 0000000..470ff20 --- /dev/null +++ b/src/js/sort.js @@ -0,0 +1,36 @@ +import { PRIORITY_ORDER, SORT_VALUES } from './config.js'; +import { isHiddenTask } from './utils.js'; + +const number = (task) => Number.parseInt(task.title.match(/\d+/)?.[0], 10) || 0; +const date = (value) => new Date(value).getTime() || 0; +const title = (a, b) => a.title.localeCompare(b.title); +const due = (a, b) => { + if (!a.dueDate && !b.dueDate) return 0; + if (!a.dueDate) return 1; + if (!b.dueDate) return -1; + return date(a.dueDate) - date(b.dueDate); +}; +const comparators = { + title, + number: (a, b) => number(a) - number(b) || title(a, b), + dueDate: due, + priority: (a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority], + progress: (a, b) => a.progress - b.progress, + created: (a, b) => date(a.createdDate) - date(b.createdDate), +}; +export function sortTasks(taskList, sortOrder = 'default') { + const result = [...taskList]; + const valid = SORT_VALUES.has(sortOrder) ? sortOrder : 'default'; + return result.sort((a, b) => { + if (a.isPinned !== b.isPinned) return a.isPinned ? -1 : 1; + if (isHiddenTask(a) !== isHiddenTask(b)) return isHiddenTask(a) ? 1 : -1; + if (valid === 'default') + return ( + PRIORITY_ORDER[b.priority] - PRIORITY_ORDER[a.priority] || + date(b.createdDate) - date(a.createdDate) + ); + const [key, direction] = valid.split('-'); + if (key === 'dueDate' && (!a.dueDate || !b.dueDate)) return due(a, b); + return comparators[key](a, b) * (direction === 'desc' ? -1 : 1); + }); +} diff --git a/src/js/storage.js b/src/js/storage.js new file mode 100644 index 0000000..c1eca5a --- /dev/null +++ b/src/js/storage.js @@ -0,0 +1,63 @@ +import { STORAGE_KEYS } from './config.js'; + +let writeFailed = false; +export function consumeStorageWriteFailure() { + const failed = writeFailed; + writeFailed = false; + return failed; +} +function readRaw(key) { + try { + return { value: localStorage.getItem(key), available: true }; + } catch { + return { value: null, available: false }; + } +} +function writeRaw(key, value) { + try { + localStorage.setItem(key, value); + return true; + } catch { + writeFailed = true; + return false; + } +} +function readJson(key, fallback) { + const { value, available } = readRaw(key); + if (!available || value === null) return fallback; + try { + return JSON.parse(value); + } catch { + return fallback; + } +} +function writeJson(key, value) { + return writeRaw(key, JSON.stringify(value)); +} +export function loadTasksData() { + const { value: raw, available } = readRaw(STORAGE_KEYS.tasks); + if (!available) return { tasks: [], corrupted: false, exists: false, available: false }; + if (raw === null) return { tasks: [], corrupted: false, exists: false, available: true }; + writeRaw(STORAGE_KEYS.initialized, 'true'); + try { + const tasks = JSON.parse(raw); + if (!Array.isArray(tasks)) throw new TypeError('Tasks must be an array'); + return { tasks, corrupted: false, exists: true, available: true }; + } catch { + writeRaw(STORAGE_KEYS.corruptedBackup, raw); + return { tasks: [], corrupted: true, exists: true, available: true }; + } +} +export const saveTasksData = (tasks) => writeJson(STORAGE_KEYS.tasks, tasks); +export const loadSortOrdersData = () => readJson(STORAGE_KEYS.sortOrders, {}); +export const saveSortOrdersData = (orders) => writeJson(STORAGE_KEYS.sortOrders, orders); +export const loadShowHiddenData = (status) => { + const key = STORAGE_KEYS.showHidden[status]; + return key ? readJson(key, false) === true : false; +}; +export const saveShowHiddenData = (status, value) => { + const key = STORAGE_KEYS.showHidden[status]; + return key ? writeJson(key, value) : false; +}; +export const isInitialized = () => readRaw(STORAGE_KEYS.initialized).value !== null; +export const markInitialized = () => writeRaw(STORAGE_KEYS.initialized, 'true'); diff --git a/src/js/store.js b/src/js/store.js new file mode 100644 index 0000000..7c7f4f7 --- /dev/null +++ b/src/js/store.js @@ -0,0 +1,107 @@ +import { DEFAULT_SORT_ORDERS, HIDEABLE_STATUSES, SORT_VALUES, STATUSES } from './config.js'; +import { normalizeSortOrders, normalizeTask, normalizeTasks } from './task-model.js'; +import { generateUniqueId } from './utils.js'; +import { + loadShowHiddenData, + loadSortOrdersData, + loadTasksData, + markInitialized, + saveShowHiddenData, + saveSortOrdersData, + saveTasksData, +} from './storage.js'; + +let tasks = []; +let sortOrders = { ...DEFAULT_SORT_ORDERS }; +let showHidden = Object.fromEntries(HIDEABLE_STATUSES.map((status) => [status, false])); +let corrupted = false; +const persistTasks = () => saveTasksData(tasks); +export function initializeStore(seedTasks, initialized) { + const loaded = loadTasksData(); + corrupted = loaded.corrupted; + tasks = normalizeTasks(loaded.tasks); + if (loaded.exists && !loaded.corrupted) persistTasks(); + if (!initialized && !loaded.exists) { + tasks = normalizeTasks(seedTasks); + persistTasks(); + markInitialized(); + } + sortOrders = { ...DEFAULT_SORT_ORDERS, ...normalizeSortOrders(loadSortOrdersData()) }; + showHidden = Object.fromEntries( + HIDEABLE_STATUSES.map((status) => [status, loadShowHiddenData(status)]), + ); +} +export const wasCorrupted = () => corrupted; +export const getTasks = () => tasks.map((task) => structuredClone(task)); +export const getTask = (id) => { + const task = tasks.find((item) => item.id === id); + return task ? structuredClone(task) : null; +}; +export const getSortOrders = () => ({ ...sortOrders }); +export const getShowHidden = (status) => showHidden[status] === true; +export function addTask(task) { + const normalized = normalizeTask(task); + const ids = new Set(tasks.map((item) => item.id)); + if (ids.has(normalized.id)) normalized.id = generateUniqueId(ids); + tasks.push(normalized); + persistTasks(); + return structuredClone(normalized); +} +export function updateTask(id, changes) { + const index = tasks.findIndex((task) => task.id === id); + if (index < 0) return null; + tasks[index] = normalizeTask({ ...tasks[index], ...changes, id }); + persistTasks(); + return structuredClone(tasks[index]); +} +export function removeTask(id) { + const before = tasks.length; + tasks = tasks.filter((task) => task.id !== id); + if (tasks.length !== before) persistTasks(); + return tasks.length !== before; +} +export function togglePin(id) { + const task = tasks.find((item) => item.id === id); + if (!task) return null; + task.isPinned = !task.isPinned; + persistTasks(); + return structuredClone(task); +} +export function toggleHidden(id) { + const task = tasks.find((item) => item.id === id); + if (!task) return null; + task.isHidden = !task.isHidden; + persistTasks(); + return structuredClone(task); +} +export function setSortOrder(status, value) { + if (!STATUSES.includes(status) || !SORT_VALUES.has(value)) return false; + sortOrders[status] = value; + saveSortOrdersData(sortOrders); + return true; +} +export function setShowHidden(status, value) { + if (!HIDEABLE_STATUSES.includes(status) || typeof value !== 'boolean') return false; + showHidden[status] = value; + saveShowHiddenData(status, value); + return true; +} +export function replaceTasks(value) { + tasks = normalizeTasks(value); + persistTasks(); +} +export function appendTasks(value) { + const ids = new Set(tasks.map((task) => task.id)); + const additions = normalizeTasks(value).map((task) => { + if (ids.has(task.id)) task.id = generateUniqueId(ids); + else ids.add(task.id); + return task; + }); + tasks = [...tasks, ...additions]; + persistTasks(); + return additions.map((task) => structuredClone(task)); +} +export function replaceSortOrders(value) { + sortOrders = { ...sortOrders, ...normalizeSortOrders(value) }; + saveSortOrdersData(sortOrders); +} diff --git a/src/js/task-model.js b/src/js/task-model.js new file mode 100644 index 0000000..770f76d --- /dev/null +++ b/src/js/task-model.js @@ -0,0 +1,56 @@ +import { LANGUAGES, PRIORITIES, SORT_VALUES, STATUSES } from './config.js'; +import { generateId, generateUniqueId, isValidId } from './utils.js'; + +const TIMELINE_TYPES = ['manual-entry', 'status-change', 'progress-update', 'progressNotesUpdated']; +export function normalizeTask(input) { + const task = input && typeof input === 'object' ? { ...input } : {}; + task.title = String(task.title ?? '').trim(); + task.description = String(task.description ?? ''); + task.progressNotes = String(task.progressNotes ?? ''); + task.assignee = String(task.assignee ?? ''); + task.dueDate = typeof task.dueDate === 'string' ? task.dueDate : ''; + task.createdDate = + task.createdDate && !Number.isNaN(Date.parse(task.createdDate)) + ? task.createdDate + : new Date().toISOString(); + task.status = STATUSES.includes(task.status) ? task.status : 'todo'; + task.priority = PRIORITIES.includes(task.priority) ? task.priority : 'medium'; + task.language = LANGUAGES.includes(task.language) ? task.language : 'zh'; + task.progress = Math.min(100, Math.max(0, Number.parseInt(task.progress, 10) || 0)); + task.collaborators = Array.isArray(task.collaborators) ? task.collaborators.map(String) : []; + task.isPinned = task.isPinned === true; + task.isHidden = task.isHidden === true; + task.id = isValidId(task.id) ? task.id : generateId(); + const timelineIds = new Set(); + task.timeline = (Array.isArray(task.timeline) ? task.timeline : []) + .filter((item) => item && typeof item === 'object' && !Array.isArray(item)) + .map((item) => { + let id = isValidId(item.id) ? item.id : generateId(); + while (timelineIds.has(id)) id = generateId(); + timelineIds.add(id); + return { ...item, id, type: TIMELINE_TYPES.includes(item.type) ? item.type : 'manual-entry' }; + }); + return task; +} +export function normalizeTasks(items) { + if (!Array.isArray(items)) return []; + const ids = new Set(); + return items + .filter((item) => item && typeof item === 'object') + .map((item) => { + const task = normalizeTask(item); + if (ids.has(task.id)) task.id = generateUniqueId(ids); + else ids.add(task.id); + return task; + }); +} +export function normalizeSortOrders(value) { + if (!value || typeof value !== 'object' || Array.isArray(value)) return {}; + return Object.fromEntries( + STATUSES.filter((status) => SORT_VALUES.has(value[status])).map((status) => [ + status, + value[status], + ]), + ); +} +export { isValidId } from './utils.js'; diff --git a/src/js/timeline.js b/src/js/timeline.js new file mode 100644 index 0000000..b6640d7 --- /dev/null +++ b/src/js/timeline.js @@ -0,0 +1,63 @@ +import { t } from './i18n/index.js'; +import { getTask, updateTask } from './store.js'; +import { formatLocalDate, generateUniqueId } from './utils.js'; + +export function addSystemEntries(task, changes) { + const timeline = [...task.timeline]; + const fields = [ + ['progressNotes', 'progressNotesUpdated', 'progressNotesUpdated', (a, b) => ({ value: b })], + ['status', 'status-change', 'statusChanged', (a, b) => ({ from: a, to: b })], + ['progress', 'progress-update', 'progressUpdated', (a, b) => ({ from: a, to: b })], + ['assignee', 'status-change', 'assigneeChanged', (a, b) => ({ from: a, to: b })], + ['dueDate', 'status-change', 'dueDateChanged', (a, b) => ({ from: a, to: b })], + ['priority', 'status-change', 'priorityChanged', (a, b) => ({ from: a, to: b })], + ]; + const used = new Set(timeline.map((item) => item.id)); + fields.forEach(([field, type, key, values]) => { + if (Object.hasOwn(changes, field) && task[field] !== changes[field]) + timeline.push({ + id: generateUniqueId(used), + date: formatLocalDate(), + type, + content: '', + event: { key, values: values(task[field], changes[field]) }, + userType: 'system', + }); + }); + return timeline; +} +export function addTimelineEntry(taskId, content) { + const task = getTask(taskId); + if (!task || !content.trim()) return false; + const id = generateUniqueId(new Set(task.timeline.map((item) => item.id))); + task.timeline.push({ + id, + date: formatLocalDate(), + type: 'manual-entry', + content: content.trim(), + user: task.assignee || t('defaultUser'), + }); + updateTask(taskId, { timeline: task.timeline }); + return true; +} +export function updateTimelineEntry(taskId, timelineId, content) { + const task = getTask(taskId); + const item = task?.timeline.find((entry) => entry.id === timelineId); + if (!item || !content.trim()) return false; + const wasSystem = item.userType === 'system'; + item.content = content.trim(); + item.type = 'manual-entry'; + if (wasSystem || !item.user) item.user = task.assignee || t('defaultUser'); + delete item.event; + delete item.userType; + updateTask(taskId, { timeline: task.timeline }); + return true; +} +export function deleteTimelineEntry(taskId, timelineId) { + const task = getTask(taskId); + if (!task) return false; + const timeline = task.timeline.filter((item) => item.id !== timelineId); + if (timeline.length === task.timeline.length) return false; + updateTask(taskId, { timeline }); + return true; +} diff --git a/src/js/utils.js b/src/js/utils.js new file mode 100644 index 0000000..9b6449a --- /dev/null +++ b/src/js/utils.js @@ -0,0 +1,43 @@ +import { DUE_SOON_DAYS, HIDEABLE_STATUSES } from './config.js'; + +// Only todo and completed tasks can be hidden, so the flag is inert in any other column. +export const isHiddenTask = (task) => + task.isHidden === true && HIDEABLE_STATUSES.includes(task.status); + +let idCounter = 0; +export const generateId = () => Date.now() * 1000 + (idCounter++ % 1000); +export function generateUniqueId(usedIds) { + let id = generateId(); + while (usedIds.has(id)) id = generateId(); + usedIds.add(id); + return id; +} +export const isValidId = (value) => Number.isSafeInteger(value) && value > 0; +export function escapeHtml(value) { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} +export function formatLocalDate(date = new Date()) { + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; +} +export function parseDueDateEnd(value) { + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value ?? ''); + if (!match) return null; + const [, y, m, d] = match.map(Number); + const date = new Date(y, m - 1, d, 23, 59, 59, 999); + return date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d + ? date + : null; +} +export function getDueDateState(value, now = new Date()) { + const dueEnd = parseDueDateEnd(value); + if (!dueEnd) return { isOverdue: false, isDueSoon: false }; + const soonEnd = new Date(now); + soonEnd.setDate(soonEnd.getDate() + DUE_SOON_DAYS); + soonEnd.setHours(23, 59, 59, 999); + return { isOverdue: dueEnd < now, isDueSoon: dueEnd >= now && dueEnd <= soonEnd }; +} diff --git a/task.md b/task.md new file mode 100644 index 0000000..8b3c6ad --- /dev/null +++ b/task.md @@ -0,0 +1,41 @@ + +### 待办 +- 1、打印任务功能 +- 2、任务中英文切换 +- 3、记录创建日期,用于 Completed 分类中的排序 + + +### 进行中 + +- V1.0 #1、拆分成 html、cs、js +- V1.0 #2、将Json文件 改名为backup-tasks-yyyy-mm-dd.json + + +### 已完成 + +### V1.0 + +```yaml + +``` + +### V0.3 + +```yaml +- 1、【待办事项】添加隐藏任务功能 +``` + +### V0.2 + +```yaml +- 1、数据保存到本地。Json 文件格式 +- 2、去除按钮 【检测到期按钮提醒】,保留刷新时自动检测 +- 3、已完成栏,可以隐藏任务功能 +``` + +### V0.1 + +```yaml +- 1、时间轴的内容可以编辑,修改,添加 +- 2、添加软件图标 +``` diff --git a/test/backup-tasks-v1.json b/test/backup-tasks-v1.json new file mode 100644 index 0000000..e3eb483 --- /dev/null +++ b/test/backup-tasks-v1.json @@ -0,0 +1,57 @@ +{ + "version": "1.0", + "exportDate": "2025-01-10T08:00:00.000Z", + "localDate": "2025-01-10", + "taskCount": 2, + "data": { + "tasks": [ + { + "id": 101, + "title": "Legacy todo", + "description": "Created by the pre-refactor app", + "progressNotes": "Existing progress", + "status": "todo", + "progress": 25, + "assignee": "Alice", + "collaborators": ["Bob"], + "dueDate": "2025-02-01", + "createdDate": "2025-01-01T08:00:00.000Z", + "language": "en", + "priority": "high", + "isPinned": true, + "isHidden": false, + "timeline": [ + { + "id": 1001, + "date": "2025-01-01", + "type": "manual-entry", + "content": "Legacy timeline entry", + "user": "Alice" + } + ] + }, + { + "id": 102, + "title": "Legacy completed", + "description": "", + "progressNotes": "", + "status": "completed", + "progress": 100, + "assignee": "", + "collaborators": [], + "dueDate": "", + "createdDate": "2025-01-02T08:00:00.000Z", + "language": "zh", + "priority": "low", + "isPinned": false, + "isHidden": true, + "timeline": [] + } + ], + "sortOrders": { + "todo": "priority-desc", + "inProgress": "created-asc", + "completed": "title-asc" + } + } +} diff --git a/test/core.test.js b/test/core.test.js new file mode 100644 index 0000000..bff40f0 --- /dev/null +++ b/test/core.test.js @@ -0,0 +1,505 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import legacyBackup from './backup-tasks-v1.json' with { type: 'json' }; +import { HIDEABLE_STATUSES, STATUSES } from '../src/js/config.js'; +import { t } from '../src/js/i18n/index.js'; +import { normalizeTask, normalizeTasks } from '../src/js/task-model.js'; +import { sortTasks } from '../src/js/sort.js'; +import { getDueDateState } from '../src/js/utils.js'; +import { applySearchFilter, renderColumn, updateTaskCounts } from '../src/js/render.js'; +import { addSystemEntries, addTimelineEntry, updateTimelineEntry } from '../src/js/timeline.js'; +import { + executeImport, + exportData, + mergeTasks, + readImportFile, + validateImportData, +} from '../src/js/import-export.js'; +import { + consumeStorageWriteFailure, + isInitialized, + loadShowHiddenData, + loadSortOrdersData, + loadTasksData, +} from '../src/js/storage.js'; +import { + addTask, + appendTasks, + getShowHidden, + getSortOrders, + getTasks, + initializeStore, + setShowHidden, + setSortOrder, + updateTask, +} from '../src/js/store.js'; +import { createSeedTasks } from '../src/js/seed-data.js'; + +const base = (id, title, extra = {}) => ({ + id, + title, + status: 'todo', + priority: 'medium', + progress: 20, + createdDate: `2025-01-${String(id).padStart(2, '0')}T00:00:00Z`, + dueDate: `2025-01-${String(id).padStart(2, '0')}`, + timeline: [], + ...extra, +}); + +beforeEach(() => localStorage.clear()); +afterEach(() => vi.restoreAllMocks()); + +describe('task model', () => { + it('normalizes fields and language whitelist', () => { + const task = normalizeTask({ + id: 1, + title: ' x ', + language: 'fr', + progress: 999, + status: 'bad', + }); + expect(task).toMatchObject({ title: 'x', language: 'zh', progress: 100, status: 'todo' }); + expect(normalizeTask({ id: 2, title: 'English', language: 'en' }).language).toBe('en'); + }); + it('deduplicates task and timeline ids', () => { + const tasks = normalizeTasks([ + base(1, 'a', { timeline: [{ id: 5 }, { id: 5 }] }), + base(1, 'b'), + ]); + expect(new Set(tasks.map((task) => task.id)).size).toBe(2); + expect(new Set(tasks[0].timeline.map((item) => item.id)).size).toBe(2); + }); + it('drops non-object timeline entries from legacy data', () => { + const task = normalizeTask({ + ...base(1, 'dirty timeline'), + timeline: ['oops', 42, true, null, [], { id: 7, content: 'valid' }], + }); + expect(task.timeline).toEqual([{ id: 7, content: 'valid', type: 'manual-entry' }]); + }); +}); + +describe('all 13 sorts', () => { + const tasks = [ + base(1, 'Beta 10', { + priority: 'medium', + progress: 50, + dueDate: '2025-03-02', + createdDate: '2025-01-02T00:00:00Z', + }), + base(2, 'Alpha 2', { + priority: 'high', + progress: 90, + dueDate: '2025-03-03', + createdDate: '2025-01-03T00:00:00Z', + }), + base(3, 'Gamma 1', { + priority: 'low', + progress: 10, + dueDate: '2025-03-01', + createdDate: '2025-01-01T00:00:00Z', + }), + ]; + const expected = { + default: [2, 1, 3], + 'title-asc': [2, 1, 3], + 'title-desc': [3, 1, 2], + 'number-asc': [3, 2, 1], + 'number-desc': [1, 2, 3], + 'dueDate-asc': [3, 1, 2], + 'dueDate-desc': [2, 1, 3], + 'priority-asc': [3, 1, 2], + 'priority-desc': [2, 1, 3], + 'progress-asc': [3, 1, 2], + 'progress-desc': [2, 1, 3], + 'created-asc': [3, 1, 2], + 'created-desc': [2, 1, 3], + }; + it.each(Object.entries(expected))('sorts %s in the expected order', (order, ids) => { + const copy = structuredClone(tasks); + expect(sortTasks(tasks, order).map((task) => task.id)).toEqual(ids); + expect(tasks).toEqual(copy); + }); + it('keeps pinned first for every sort', () => { + const pinned = { ...tasks[2], isPinned: true }; + Object.keys(expected).forEach((order) => + expect(sortTasks([tasks[0], tasks[1], pinned], order)[0].id).toBe(3), + ); + }); + it('keeps hidden todo tasks last but leaves hidden in-progress tasks in place', () => { + const hiddenTodo = { ...tasks[1], isHidden: true }; + expect(sortTasks([hiddenTodo, tasks[0]], 'title-asc').map((task) => task.id)).toEqual([1, 2]); + const hiddenWork = { ...tasks[1], status: 'inProgress', isHidden: true }; + expect(sortTasks([hiddenWork, tasks[0]], 'title-asc').map((task) => task.id)).toEqual([2, 1]); + }); + it('keeps tasks without due dates last in both directions', () => { + const dated = base(1, 'Dated'); + const undated = { ...base(2, 'Undated'), dueDate: '' }; + expect(sortTasks([undated, dated], 'dueDate-asc').map((task) => task.id)).toEqual([1, 2]); + expect(sortTasks([undated, dated], 'dueDate-desc').map((task) => task.id)).toEqual([1, 2]); + }); +}); + +describe('due dates', () => { + const now = new Date(2025, 0, 10, 12); + it('detects overdue and due-soon boundaries', () => { + expect(getDueDateState('2025-01-09', now)).toEqual({ isOverdue: true, isDueSoon: false }); + expect(getDueDateState('2025-01-10', now)).toEqual({ isOverdue: false, isDueSoon: true }); + expect(getDueDateState('2025-01-13', now).isDueSoon).toBe(true); + expect(getDueDateState('2025-01-14', now).isDueSoon).toBe(false); + }); + it('ignores empty and invalid dates', () => { + expect(getDueDateState('', now)).toEqual({ isOverdue: false, isDueSoon: false }); + expect(getDueDateState('2025-02-30', now)).toEqual({ isOverdue: false, isDueSoon: false }); + }); +}); + +describe('seed data', () => { + it('creates five unique tasks at the initialization time', () => { + const now = new Date('2026-07-19T02:54:00.000Z'); + const tasks = createSeedTasks(now); + expect(tasks).toHaveLength(5); + expect(new Set(tasks.map((task) => task.id)).size).toBe(5); + expect(tasks.every((task) => task.createdDate === now.toISOString())).toBe(true); + }); +}); + +describe('storage and store compatibility', () => { + it('distinguishes a missing task key from an intentionally empty task list', () => { + expect(loadTasksData()).toMatchObject({ tasks: [], corrupted: false, exists: false }); + localStorage.setItem('tasks', '[]'); + expect(loadTasksData()).toMatchObject({ tasks: [], corrupted: false, exists: true }); + }); + it('preserves corrupted raw data without replacing it with seed tasks', () => { + localStorage.setItem('tasks', '{broken'); + initializeStore([base(1, 'Seed')], false); + expect(getTasks()).toEqual([]); + expect(localStorage.getItem('tasks')).toBe('{broken'); + expect(localStorage.getItem('tasksCorruptedBackup')).toBe('{broken'); + }); + it('restores all legacy preferences and task fields', () => { + localStorage.setItem('tasks', JSON.stringify(legacyBackup.data.tasks)); + localStorage.setItem('taskSortOrders', JSON.stringify(legacyBackup.data.sortOrders)); + localStorage.setItem('showHiddenCompletedTasks', 'true'); + localStorage.setItem('showHiddenTodoTasks', 'false'); + localStorage.setItem('tasksInitialized', 'true'); + localStorage.setItem('tasksCorruptedBackup', '{historical backup'); + initializeStore([], isInitialized()); + expect(getTasks()).toMatchObject(legacyBackup.data.tasks); + expect(getSortOrders()).toEqual(legacyBackup.data.sortOrders); + expect(getShowHidden('completed')).toBe(true); + expect(getShowHidden('todo')).toBe(false); + expect(loadSortOrdersData()).toEqual(legacyBackup.data.sortOrders); + expect(loadShowHiddenData('completed')).toBe(true); + expect(localStorage.getItem('tasksCorruptedBackup')).toBe('{historical backup'); + }); + it('maintains store ids and rejects invalid preference mutations', () => { + initializeStore([], true); + const first = addTask(base(1, 'First')); + const duplicate = addTask(base(1, 'Second')); + const [appended] = appendTasks([base(first.id, 'Third')]); + expect(new Set([first.id, duplicate.id, appended.id]).size).toBe(3); + expect(setSortOrder('invalid', 'title-asc')).toBe(false); + expect(setSortOrder('todo', 'invalid')).toBe(false); + expect(setShowHidden('inProgress', true)).toBe(false); + expect(localStorage.getItem('undefined')).toBeNull(); + }); + it('degrades safely when storage access throws', () => { + const getItem = vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { + throw new DOMException('blocked', 'SecurityError'); + }); + expect(loadTasksData()).toMatchObject({ tasks: [], exists: false, available: false }); + expect(isInitialized()).toBe(false); + getItem.mockRestore(); + vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { + throw new DOMException('full', 'QuotaExceededError'); + }); + expect(() => initializeStore([base(1, 'Seed')], false)).not.toThrow(); + expect(consumeStorageWriteFailure()).toBe(true); + expect(consumeStorageWriteFailure()).toBe(false); + }); +}); + +describe('imports', () => { + function loadPending(data) { + const original = globalThis.FileReader; + class FakeReader { + readAsText() { + this.result = JSON.stringify(data); + this.onload(); + } + } + globalThis.FileReader = FakeReader; + readImportFile({}, vi.fn(), vi.fn(), vi.fn()); + globalThis.FileReader = original; + } + beforeEach(() => { + globalThis.bootstrap = { + Modal: { getOrCreateInstance: () => ({ hide: vi.fn(), show: vi.fn() }) }, + }; + document.body.innerHTML = '
'; + initializeStore([], true); + }); + it('validates the legacy export fixture', () => { + expect(validateImportData(legacyBackup)).toBe(true); + expect(validateImportData({ version: '1.0', data: { tasks: [] } })).toBe(true); + }); + it('rejects unsupported versions, invalid task shapes, and dates', () => { + expect(validateImportData(null)).toBe(false); + expect(validateImportData({ version: '2.0', data: { tasks: [] } })).toBe(false); + expect(validateImportData({ version: '1.0', data: { tasks: 'bad' } })).toBe(false); + expect( + validateImportData({ + version: '1.0', + data: { tasks: [{ id: 1, title: '', status: 'todo' }] }, + }), + ).toBe(false); + expect( + validateImportData({ + version: '1.0', + data: { tasks: [{ ...base(1, 'Bad date'), dueDate: '2025-02-30' }] }, + }), + ).toBe(false); + }); + it('deduplicates merge key and preserves missing dates', () => { + const existing = [base(1, 'Same')]; + const result = mergeTasks(existing, [ + base(2, 'Same', { createdDate: existing[0].createdDate }), + { ...base(3, 'Same'), createdDate: undefined }, + ]); + expect(result.skipped).toBe(1); + expect(result.tasks).toHaveLength(2); + expect(new Set(result.tasks.map((task) => task.id)).size).toBe(2); + }); + it('executes replacement import with legacy tasks and sort orders', () => { + addTask(base(1, 'Existing')); + loadPending(legacyBackup); + expect(executeImport('replace')).toBe(0); + expect(getTasks()).toMatchObject(legacyBackup.data.tasks); + expect(getSortOrders()).toEqual(legacyBackup.data.sortOrders); + }); + it('executes merge import and reports duplicates', () => { + const existing = legacyBackup.data.tasks[0]; + addTask(existing); + loadPending(legacyBackup); + expect(executeImport('merge')).toBe(1); + expect(getTasks()).toHaveLength(2); + }); + it('rejects an invalid import mode', () => { + loadPending(legacyBackup); + expect(() => executeImport('unexpected')).toThrow(TypeError); + }); + it('clears a previous pending import when a newly selected file is invalid', () => { + loadPending(legacyBackup); + loadPending({ version: '1.0', data: { tasks: 'invalid' } }); + expect(() => executeImport('replace')).toThrow('No import pending'); + }); +}); + +describe('timeline entries', () => { + beforeEach(() => initializeStore([], true)); + + it('ignores fields that the change set omits', () => { + const task = addTask(base(1, 'Untouched', { assignee: 'Ada', dueDate: '2025-01-01' })); + expect(addSystemEntries(task, { progress: task.progress })).toEqual([]); + }); + + it('records only the fields that actually changed', () => { + const task = addTask(base(1, 'Changed')); + const timeline = addSystemEntries(task, { progress: 80, status: task.status }); + expect(timeline).toHaveLength(1); + expect(timeline[0].event).toEqual({ key: 'progressUpdated', values: { from: 20, to: 80 } }); + }); + + it('turns an edited system entry into a manual entry owned by the assignee', () => { + const task = addTask(base(1, 'Edited', { assignee: 'Ada' })); + const timeline = addSystemEntries(task, { progress: 80 }); + updateTask(task.id, { timeline }); + expect(updateTimelineEntry(task.id, timeline[0].id, ' my own words ')).toBe(true); + const [stored] = getTasks(); + expect(stored.timeline[0]).toMatchObject({ + type: 'manual-entry', + content: 'my own words', + user: 'Ada', + }); + expect(stored.timeline[0].userType).toBeUndefined(); + expect(stored.timeline[0].event).toBeUndefined(); + }); + + it('keeps the original author when an existing manual entry is edited', () => { + const task = addTask(base(1, 'Reassigned', { assignee: 'Ada' })); + updateTask(task.id, { + timeline: [ + { id: 11, date: '2025-01-01', content: 'by bob', type: 'manual-entry', user: 'Bob' }, + ], + }); + expect(updateTimelineEntry(task.id, 11, ' typo fixed ')).toBe(true); + expect(getTasks()[0].timeline[0]).toMatchObject({ user: 'Bob', content: 'typo fixed' }); + }); +}); + +describe('board rendering', () => { + const fixture = () => + '
' + + STATUSES.map( + (status) => + `` + + `` + + `
`, + ).join('') + + HIDEABLE_STATUSES.map( + (status) => + ``, + ).join('') + + '
'; + const visibleTitles = (status) => + [...document.querySelectorAll(`#${status}Tasks .task-card`)] + .filter((card) => !card.hidden) + .map((card) => card.querySelector('h6').textContent); + const entryTexts = (status) => + [...document.querySelectorAll(`#${status}Tasks .timeline-content`)].map((node) => + node.lastElementChild.textContent.trim(), + ); + const search = (term) => { + document.querySelector('#searchInput').value = term; + applySearchFilter(); + updateTaskCounts(); + }; + + beforeEach(() => { + document.body.innerHTML = fixture(); + initializeStore([], true); + }); + + it('lists timeline entries newest first within a single day', () => { + const task = addTask(base(1, 'Ordered')); + ['first', 'second', 'third'].forEach((text) => addTimelineEntry(task.id, text)); + renderColumn('todo'); + expect(entryTexts('todo')).toEqual(['third', 'second', 'first']); + }); + + it('lists timeline entries newest first across days', () => { + const task = addTask(base(1, 'Ordered')); + updateTask(task.id, { + timeline: [ + { id: 11, date: '2025-01-01', content: 'older', type: 'manual-entry', user: 'Ada' }, + { id: 12, date: '2025-06-01', content: 'newer', type: 'manual-entry', user: 'Ada' }, + ], + }); + renderColumn('todo'); + expect(entryTexts('todo')).toEqual(['newer', 'older']); + }); + + it('lists entries carrying an unusable date last', () => { + const task = addTask(base(1, 'Ordered')); + updateTask(task.id, { + timeline: [ + { id: 11, date: 'sometime', content: 'undated', type: 'manual-entry', user: 'Ada' }, + { id: 12, date: '2025-01-01', content: 'dated', type: 'manual-entry', user: 'Ada' }, + ], + }); + renderColumn('todo'); + expect(entryTexts('todo')).toEqual(['dated', 'undated']); + }); + + it('searches task content without matching card boilerplate', () => { + addTask(base(1, 'Apple', { description: 'crisp fruit', assignee: 'Ada' })); + addTask(base(2, 'Banana', { progressNotes: 'still ripening' })); + renderColumn('todo'); + + search('apple'); + expect(visibleTitles('todo')).toEqual(['Apple']); + search('crisp'); + expect(visibleTitles('todo')).toEqual(['Apple']); + search('ripening'); + expect(visibleTitles('todo')).toEqual(['Banana']); + search('ada'); + expect(visibleTitles('todo')).toEqual(['Apple']); + + [t('timeline'), t('addEntry'), t('edit'), t('progress')].forEach((label) => { + search(label); + expect(visibleTitles('todo')).toEqual([]); + }); + }); + + it('keeps the header count in step with the active search', () => { + addTask(base(1, 'Apple')); + addTask(base(2, 'Banana')); + renderColumn('todo'); + expect(document.querySelector('#todoCount').textContent).toBe('2'); + search('apple'); + expect(document.querySelector('#todoCount').textContent).toBe('1'); + search(''); + expect(document.querySelector('#todoCount').textContent).toBe('2'); + }); + + it('leaves hidden tasks out of the header count while they stay collapsed', () => { + addTask(base(1, 'Visible')); + addTask(base(2, 'Quiet', { isHidden: true })); + renderColumn('todo'); + expect(document.querySelector('#todoCount').textContent).toBe('1'); + setShowHidden('todo', true); + renderColumn('todo'); + expect(document.querySelector('#todoCount').textContent).toBe('2'); + }); + + it('explains a column emptied by the search instead of leaving it blank', () => { + addTask(base(1, 'Apple')); + renderColumn('todo'); + search('kiwi'); + expect(document.querySelector('#todoTasks .search-empty-state').textContent).toBe( + t('noSearchResults'), + ); + search('apple'); + expect(document.querySelector('#todoTasks .search-empty-state')).toBeNull(); + }); + + it('dims hidden todo tasks but leaves in-progress tasks alone', () => { + addTask(base(1, 'Hidden todo', { isHidden: true })); + addTask(base(2, 'Hidden work', { status: 'inProgress', isHidden: true })); + setShowHidden('todo', true); + renderColumn('todo'); + renderColumn('inProgress'); + expect(document.querySelector('#todoTasks .task-card').className).toContain('hidden-task'); + expect(document.querySelector('#inProgressTasks .task-card').className).not.toContain( + 'hidden-task', + ); + expect(document.querySelector('#inProgressCount').textContent).toBe('1'); + }); +}); + +describe('export', () => { + const original = {}; + + beforeEach(() => { + original.createObjectURL = URL.createObjectURL; + original.revokeObjectURL = URL.revokeObjectURL; + document.body.innerHTML = ''; + initializeStore([], true); + }); + afterEach(() => { + URL.createObjectURL = original.createObjectURL; + URL.revokeObjectURL = original.revokeObjectURL; + vi.useRealTimers(); + }); + + it('clicks an attached anchor and revokes the blob url only afterwards', () => { + vi.useFakeTimers(); + addTask(base(1, 'Exported')); + URL.createObjectURL = vi.fn(() => 'blob:fake'); + URL.revokeObjectURL = vi.fn(); + let connectedAtClick = null; + const createElement = document.createElement.bind(document); + vi.spyOn(document, 'createElement').mockImplementation((tag) => { + const element = createElement(tag); + if (tag === 'a') element.click = () => (connectedAtClick = element.isConnected); + return element; + }); + + expect(exportData()).toMatch(/^backup-tasks-\d{4}-\d{2}-\d{2}\.json$/); + expect(connectedAtClick).toBe(true); + expect(document.querySelector('a')).toBeNull(); + expect(URL.revokeObjectURL).not.toHaveBeenCalled(); + vi.runAllTimers(); + expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:fake'); + }); +}); diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..1a62ca4 --- /dev/null +++ b/todo.md @@ -0,0 +1,191 @@ +# 任务管理系统重构 TODO + +> 分支:`dev-refactor` 目标:在**不改变任何现有功能与数据格式**的前提下,把单文件架构重构为模块化、可维护的结构。 +> 原则:小步提交,每个阶段完成后跑一遍「回归测试清单」再进入下一阶段。 + +--- + +## 一、现状分析 + +| 文件 | 行数 | 问题概述 | +|------|------|----------| +| `src/index.html` | 311 | 内联 onclick、3 份重复的排序下拉、favicon 重复 4 次 | +| `src/script.js` | 1788 | 单文件巨石:状态、i18n、渲染、存储、导入导出全部混在一起,约 30 个全局函数 | +| `src/styles.css` | 432 | 颜色硬编码、选择器重复定义、存在疑似死代码 | + +### 主要问题清单 + +1. **全局可变状态散落**:`tasks`、`currentLang`、`sortOrders`、`showHiddenCompletedTasks`、`showHiddenTodoTasks`、`currentEditingTaskId`、`pendingImportData`、`idCounter` 共 8 个全局变量,任何函数都能直接改。 +2. **内联事件处理**:HTML 和 JS 模板字符串里大量 `onclick="xxx()"`,迫使所有函数必须挂在全局作用域。 +3. **成对重复代码**(completed / todo 两套几乎相同的逻辑): + - `toggleShowHiddenCompletedTasks` / `toggleShowHiddenTodoTasks`(script.js:1665 / 1728) + - `updateShowHiddenCompletedTasksUI` / `updateShowHiddenTodoTasksUI`(script.js:1682 / 1745) + - `save/loadShowHiddenCompletedTasks` / `save/loadShowHiddenTodoTasks`(script.js:1708 / 1771) + - `renderTasks` 内部对 completed 和 todo 的隐藏任务处理两段几乎一样(script.js:630-667) +4. **HTML 重复**:三列的排序 `