commit f8807937025a3a5ce2d72b0be4bb3d91f73e0112 Author: 海明 <251352239@qq.com> Date: Wed Aug 12 20:01:35 2026 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..368517d --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local +参考资料 +.workbuddy +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.vscode +*.tsbuildinfo +docs +.eslintcache +website + +# Cypress +/cypress/videos/ +/cypress/screenshots/ + +# Vitest +__screenshots__/ + +# Vite +*.timestamp-*-*.mjs diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..29a2402 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "singleQuote": true, + "printWidth": 100 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..d632887 --- /dev/null +++ b/README.md @@ -0,0 +1,420 @@ +

+ OpenPrint Logo +

+ +

OpenPrint

+ +

+ 开源 Web 打印设计器 · AI 驱动 · 跨平台 +

+ +

+ Stars + Release + License + Vue 3 + TypeScript + Fabric.js +

+ +

+ 功能特性 · + 快速开始 · + AI 助手 · + 架构 · + 部署 · + FAQ +

+ +
+ +**OpenPrint** 是一款开源、零后端依赖的 Web 打印模板可视化设计器。它让用户通过拖拽即可设计快递面单、发票、标签、报表等打印模板,支持 AI 一句话生成、云打印、C++ 桌面客户端静默打印,3 分钟即可对接 ERP 系统。 + +> 纯前端架构,无需服务器,无需数据库,一个浏览器即可运行。 + +--- + +## 目录 + +- [核心亮点](#核心亮点) +- [功能特性](#功能特性) +- [快速开始](#快速开始) +- [AI 助手](#ai-助手) +- [技术架构](#技术架构) +- [项目结构](#项目结构) +- [部署](#部署) +- [桌面客户端](#桌面客户端) +- [ERP 对接](#erp-对接) +- [FAQ](#faq) +- [贡献指南](#贡献指南) +- [许可证](#许可证) + +--- + +## 核心亮点 + +### 🤖 AI 一句话生成模板 + +告别手动拖拽,直接描述需求即可生成专业打印模板。支持快递面单、物流标签、仓储单据、零售小票等多种场景。 + +``` +用户输入:"帮我生成一张快递面单,含收件人、寄件人和运单号条码" +AI 输出: 自动创建模板 → 布局文本控件 → 添加条码 → 一键使用 +``` + +### ☁️ 云打印 + +跨地域多门店远程打印,模板统一管理。浏览器直接打印,免装驱动。云端打印队列,断线自动重试,权限管控,打印日志审计。 + +### 📊 Web 报表设计 + +分组表、交叉表、主从表等多级报表结构,支持求和/平均/计数等汇总函数,嵌入柱状图/折线图/饼图,条件格式高亮数据预警,多数据源拼接。 + +--- + +## 功能特性 + +### 设计器 + +| 功能 | 说明 | +|------|------| +| **控件库** | 文本、图片、矩形、圆形、线条、条码、二维码、表格、富文本、图表 10 种控件 | +| **拖拽设计** | 所见即所得,自由拖拽 + 吸附对齐 + 标尺辅助 | +| **多页支持** | 多页设计,每页独立页眉页脚,页间距可调 | +| **数据绑定** | 支持 JSON / CSV / API 数据源,Mustache 表达式绑定 | +| **主题切换** | 深色/浅色主题,一键切换 | +| **模板管理** | 本地存储,模板导入/导出,模板市场 | +| **实时预览** | 预览数据绑定效果,分页预览 | +| **导出** | PDF / JPG / SVG / HTML 多格式导出 | + +### 打印与对接 + +- **Web 直接打印** — 浏览器调用系统打印对话框 +- **桌面静默打印** — 配套 C++ Qt 客户端,零弹窗打印 +- **云打印** — 远程门店打印,打印队列管理 +- **ERP 对接** — HTTP API 对接,3 分钟接入 + +### 技术栈 + +![Vue 3](https://img.shields.io/badge/Vue_3-4FC08D?style=flat-square&logo=vue.js&logoColor=white) +![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat-square&logo=typescript&logoColor=white) +![Fabric.js](https://img.shields.io/badge/Fabric.js-7.x-blue?style=flat-square) +![Pinia](https://img.shields.io/badge/Pinia-FFD859?style=flat-square&logo=pinia&logoColor=black) +![Vite](https://img.shields.io/badge/Vite-646CFF?style=flat-square&logo=vite&logoColor=white) +![Vitest](https://img.shields.io/badge/Vitest-6E9F18?style=flat-square&logo=vitest&logoColor=white) +![Naive UI](https://img.shields.io/badge/Naive_UI-2E7D32?style=flat-square) +![UnoCSS](https://img.shields.io/badge/UnoCSS-333333?style=flat-square&logo=unocss&logoColor=white) + +--- + +## 快速开始 + +### 环境要求 + +- Node.js >= 22.18.0 +- pnpm >= 9.x + +### 安装与运行 + +```bash +# 克隆仓库 +git clone https://github.com/your-repo/open-print.git +cd open-print + +# 安装依赖 +pnpm install + +# 启动开发服务器 +pnpm dev + +# 构建生产版本 +pnpm build + +# 预览生产构建 +pnpm preview +``` + +### 在线演示 + +访问 [open-print.example.com/demo](https://open-print.example.com/demo) 在线体验设计器。 + +--- + +## AI 助手 + +OpenPrint 内置 AI 助手,支持自然语言生成打印模板。 + +### 使用方式 + +1. **新建模板** — 输入需求描述,AI 自动生成完整模板 +2. **修改当前模板** — 选中控件,让 AI 调整布局或样式 +3. **字段接地** — 自动识别数据字段并绑定到模板控件 + +### 技术原理 + +- 基于 LLM 解析用户意图,输出结构化模板描述 +- 内部规范引擎将描述转换为 Fabric.js 控件配置 +- 支持上下文感知,可在已有模板上增量修改 + +### 支持的场景 + +- 快递面单(顺丰、京东、邮政等格式) +- 物流标签(运单号、目的地、重量) +- 仓储单据(入库单、出库单、盘点单) +- 零售小票(收银小票、退换货单) +- 发票(增值税发票、收据) +- 自定义报表 + +--- + +## 技术架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 浏览器(Vue 3 SPA) │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ +│ │ 控制面板 │ │ 属性面板 │ │ 图层面板 │ │ 数据源面板 │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────┬────────┘ │ +│ └──────────────┴─────────────┴───────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Fabric.js Canvas │ │ +│ │ 文本 │ 图片 │ 条码 │ 二维码 │ 表格 │ 图表 │ 矩形 │ 圆形 │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ 排版引擎 (Layout Engine) │ │ +│ │ 数据绑定 │ 表达式计算 │ 分页引擎 │ 表格引擎 │ 分组引擎 │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ 渲染引擎 (Renderer) │ │ +│ │ HTML渲染 │ PDF导出 │ JPG导出 │ SVG导出 │ 头部渲染 │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌────────┴────────┐ + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ C++ Qt 客户端 │ │ 云打印服务 │ + │ 静默打印 │ │ 远程打印 │ + │ 本地打印机 │ │ 队列管理 │ + └──────────────┘ └──────────────┘ +``` + +### 核心模块 + +| 模块 | 路径 | 说明 | +|------|------|------| +| **设计器** | `src/design/` | Canvas 设计器、控件系统、面板、工具栏 | +| **排版引擎** | `src/core/layout-engine/` | 数据绑定、分页、表格、分组、表达式 | +| **渲染引擎** | `src/core/renderer-html/` | HTML 渲染、CSS 生成、富文本渲染 | +| **导出引擎** | `src/core/export-engine/` | PDF、JPG、SVG 导出 | +| **AI 助手** | `src/ai/` | AI 生成模板、上下文管理 | +| **控件库** | `src/design/canvas/controls/` | 10 种 Fabric.js 控件 | + +--- + +## 项目结构 + +``` +open-print/ +├── src/ +│ ├── ai/ # AI 助手(生成/规范化/客户端) +│ ├── core/ # 核心引擎 +│ │ ├── layout-engine/ # 排版引擎(数据绑定/分页/表格/分组) +│ │ ├── renderer-html/ # HTML 渲染器 +│ │ ├── export-engine/ # 导出引擎(PDF/JPG/SVG) +│ │ ├── chartkit/ # 图表渲染(柱状图/折线图/饼图) +│ │ ├── fonts/ # 字体加载与管理 +│ │ ├── headless/ # 无头渲染 +│ │ ├── spec/ # 模板规范与校验 +│ │ └── sdk/ # SDK 封装 +│ ├── design/ # 设计器界面 +│ │ ├── canvas/ # Canvas 画布 & 控件 +│ │ ├── panels/ # 属性/图层/数据源面板 +│ │ ├── preview/ # 预览面板 +│ │ ├── toolbar/ # 顶部工具栏 +│ │ ├── modals/ # 模态框(导入/导出/设置) +│ │ └── stores/ # Pinia 状态管理 +│ ├── repository/ # 数据仓库(本地/HTTP/模拟) +│ ├── types/ # TypeScript 类型定义 +│ ├── config/ # 配置(打印设置/AI 设置) +│ ├── theme/ # 主题系统 +│ └── utils/ # 工具函数 +├── website/ # 宣传官网 +├── dist/ # 构建输出 +├── public/ # 静态资源 +└── docs/ # 文档 +``` + +--- + +## 部署 + +### 静态部署 + +构建产物为纯静态文件,可部署到任何 HTTP 服务器: + +```bash +pnpm build +# 将 dist/ 目录部署到 Nginx / Apache / Vercel / Netlify 等 +``` + +### Nginx 配置示例 + +```nginx +server { + listen 80; + server_name your-domain.com; + root /path/to/open-print/dist; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} +``` + +### Docker 部署 + +```dockerfile +FROM nginx:alpine +COPY dist/ /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +``` + +--- + +## 桌面客户端 + +OpenPrint 提供配套的 **C++ Qt 桌面打印客户端**,支持: + +- **静默打印** — 接收设计器模板,直接发送到本地打印机,零弹窗 +- **批量打印** — 支持 CSV/JSON 数据批量渲染与打印 +- **本地打印机管理** — 获取打印机列表、状态监控 +- **HTTP 接口** — 通过 REST API 接收打印任务 +- **跨平台** — Windows / Linux / macOS + +### 技术栈 + +![C++](https://img.shields.io/badge/C++-17-00599C?style=flat-square&logo=cplusplus&logoColor=white) +![Qt](https://img.shields.io/badge/Qt-6.x-41CD52?style=flat-square&logo=qt&logoColor=white) +![CMake](https://img.shields.io/badge/CMake-064F8C?style=flat-square&logo=cmake&logoColor=white) + +--- + +## ERP 对接 + +OpenPrint 提供标准 HTTP API,3 分钟即可完成 ERP 对接: + +### 对接流程 + +1. **设计模板** — 在 Web 设计器中拖拽生成打印模板 +2. **导出模板文件** — 导出 `.json` 模板文件 +3. **API 对接** — ERP 系统通过 HTTP 推送数据 + 模板 ID,触发打印 + +### API 示例 + +```http +POST /api/print +Content-Type: application/json + +{ + "template_id": "waybill-001", + "data": { + "sender_name": "张三", + "sender_phone": "13800138000", + "receiver_name": "李四", + "receiver_address": "北京市朝阳区..." + }, + "printer": "local-printer-01", + "copies": 1 +} +``` + +--- + +## FAQ + +### OpenPrint 需要后端服务吗? + +不需要。OpenPrint 是纯前端应用,零后端依赖。模板存储在浏览器本地(IndexedDB),AI 功能通过直接调用 LLM API 实现。如需对接打印机或 ERP,可部署可选的桌面客户端。 + +### 支持哪些浏览器? + +Chrome / Edge / Firefox / Safari 等现代浏览器。推荐使用 Chromium 内核浏览器以获得最佳体验。 + +### 如何自定义字体? + +将字体文件(`.ttf` / `.woff2`)放入 `public/fonts/` 目录,并在 `src/core/fonts/catalog.ts` 中注册即可。 + +### 支持批量打印吗? + +支持。通过 CSV/JSON 数据源 + 模板引擎,可批量生成多页文档并导出或打印。桌面客户端支持批量静默打印。 + +### 如何导出模板与他人分享? + +使用模板导出功能导出 `.json` 文件,其他人可通过导入功能加载使用。也支持 URL 编码分享(零后端)。 + +--- + +## 贡献指南 + +欢迎贡献代码、提交 Issue 或建议! + +### 开发流程 + +1. Fork 本仓库 +2. 创建特性分支 (`git checkout -b feature/amazing-feature`) +3. 提交更改 (`git commit -m 'feat: add amazing feature'`) +4. 推送到分支 (`git push origin feature/amazing-feature`) +5. 提交 Pull Request + +### 提交规范 + +本项目使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范: + +- `feat:` 新功能 +- `fix:` 修复 +- `docs:` 文档变更 +- `refactor:` 重构 +- `test:` 测试 +- `chore:` 构建/工具变更 + +### 开发命令 + +```bash +# 运行测试 +pnpm vitest + +# 类型检查 +pnpm type-check + +# 格式化代码 +pnpm format + +# 代码覆盖率 +pnpm vitest --coverage +``` + +--- + +## 许可证 + +[MIT License](LICENSE) + +Copyright (c) 2025 OpenPrint + +--- + +

+ OpenPrint — 让打印变得简单高效 +

+

+ GitHub · + Gitee · + 在线演示 +

\ No newline at end of file diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..1c055af --- /dev/null +++ b/env.d.ts @@ -0,0 +1,12 @@ +/// + +interface ImportMetaEnv { + /** 后端基地址,如 https://print.example.com;配置后设计器切云端仓库(§4) */ + readonly VITE_OPENPRINT_API_BASE?: string + /** 可选:Bearer 鉴权 token */ + readonly VITE_OPENPRINT_API_TOKEN?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..47809f8 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + OpenPrint 设计器 + + +
+ + + diff --git a/overview.md b/overview.md new file mode 100644 index 0000000..099858a --- /dev/null +++ b/overview.md @@ -0,0 +1,32 @@ +# OpenPrint 图表三修:矢量 PDF + 饼图空白 + 标签对齐 + +> 承接上轮「原生 SVG 图表组件」(chartkit + PrintChart + ChartViewLayer + data-binder 接入 + ChartProps)。本轮修复/增强三件事。 + +## 1. PDF 图表走矢量(svg2pdf) +**之前**:整页 HTML 经 `foreignObject` 栅格化成 PNG 再 `addImage` → 图表是位图,放大发虚。 +**现在**:混合矢量导出(`src/core/export-engine/export-pdf.ts`) +- 每页先 `stripCharts(page)` 剥离 chart 控件 → 栅格化(文本/表格位图底图)→ `addImage` 作背景; +- 再遍历该页 chart 节点,用 `svg2pdf` 把其 SVG 以**矢量**注入 jsPDF 对应 mm 盒(`x/y/width/height`)。 +- **思源宋体矢量文本**:`/fonts/SourceHanSerifCN-Regular.ttf`(1.0M 子集)注册进 jsPDF 为 `SourceHanSerifCN`;chartkit 三个渲染器 SVG 根 `font-family` 改为 `'SourceHanSerifCN','PingFang SC',...` 优先。 +- **三重兜底**:图表旋转 → 整页退回全栅格化;单图 svg2pdf 失败 → 该图单独栅格化兜底;字体加载失败 → 整页退回全栅格化。非图表页行为不变。 + +## 2. 饼图中间空白(bug 修复) +**根因**:`chartkit/core.ts` 的 `arcPath` 实心饼画成 `M start A end Z`(圆弧+弦 = 圆缺段),楔形不到圆心 → 中间空白。 +**修复**:实心饼改为 `M cx cy L start A end Z`(真正到达圆心的楔形);环形保持外弧+内弧环形扇区;360° 整圆特例用两段半圆弧。新增回归测试:饼图 path 必须含圆心坐标。 + +## 3. 标签默认对齐(功能) +- `ChartOptions` 新增 `labelAlign?: 'left' | 'center' | 'right'`,**默认 `left`**(解决"下面的标签默认都居中")。 +- `chartkit/core.ts` 新增 `legendStartX` / `xAxisLabelPos` 两个 helper:`bar/line` 的 x 轴类目标签、`bar/line/pie` 的图例均按 `labelAlign` 对齐(图例居中/右对齐按单行总宽偏移)。 +- `ChartProps.vue` 外观区新增「标签对齐」下拉(左 / 中 / 右)。 + +## 验证结果 +- `vue-tsc --build`:**0 错误** +- `vitest`:**249 测全绿**(chartkit 11 + 原有 238) +- `vite build`:**成功**(`pdf` chunk 因并入 svg2pdf.js 由 400→487kB,属动态加载导出包,无害) +- `vite` dev server:HTTP 200 + +## ⚠️ 验证缺口(需在真实浏览器确认) +svg2pdf 的矢量渲染、中文 TTF 注入、旋转页回退逻辑**无法在本环境(happy-dom)验证**——`export-pdf.spec.ts` 只 mock 栅格化层以验证 jsPDF 装配。请用户在 Chromium 中实际导出含图表的 PDF,确认: +1. 图表在 PDF 中随缩放保持清晰(矢量); +2. 中文标签正常显示(非方块/空白); +3. 含旋转图表的页面正确回退为位图且不丢图。 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a299966 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8984 @@ +{ + "name": "open-print", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "open-print", + "version": "0.0.0", + "dependencies": { + "@bwip-js/generic": "^4.11.2", + "@codemirror/lang-json": "^6.0.2", + "@tiptap/extension-font-family": "^3.29.2", + "@tiptap/extension-text-style": "^3.29.2", + "@tiptap/pm": "^3.29.2", + "@tiptap/starter-kit": "^3.29.2", + "@tiptap/vue-3": "^3.29.2", + "@vueuse/core": "^14.4.0", + "ajv": "^8.20.0", + "codemirror": "^6.0.2", + "dompurify": "^3.4.13", + "fabric": "^7.4.0", + "json-schema-to-ts": "^3.1.1", + "jspdf": "^4.2.1", + "mustache": "^4.2.0", + "opentype.js": "^2.0.0", + "pinia": "^4.0.2", + "qrcode": "^1.5.4", + "stylis": "^4.4.0", + "svg2pdf.js": "^2.7.0", + "vue": "^3.5.40", + "vue-codemirror6": "^1.6.1" + }, + "devDependencies": { + "@iconify-json/carbon": "^1.2.25", + "@tsconfig/node24": "^24.0.4", + "@types/node": "^24.13.3", + "@types/qrcode": "^1.5.6", + "@vitejs/plugin-vue": "^6.0.8", + "@vitest/coverage-v8": "^3.2.4", + "@vue/tsconfig": "^0.9.1", + "happy-dom": "^20.11.2", + "naive-ui": "^2.44.1", + "npm-run-all2": "^9.0.2", + "prettier": "3.9.5", + "typescript": "~6.0.0", + "unocss": "^66.7.5", + "vite": "^8.1.5", + "vite-plugin-vue-devtools": "^8.1.5", + "vitest": "^3.2.4", + "vue-tsc": "^3.3.7" + }, + "engines": { + "node": "^22.18.0 || >=24.12.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz", + "integrity": "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-decorators": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz", + "integrity": "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@bwip-js/generic": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@bwip-js/generic/-/generic-4.11.2.tgz", + "integrity": "sha512-NSBGrja1Md/alQyGnJ96YJYvhxsqzV1ZhIxOsADvIo3s0PGYDISvxxcGDVwLXvWWU+38dcfFNFED47Owzjewzw==", + "license": "MIT" + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz", + "integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", + "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz", + "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz", + "integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz", + "integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.8", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.8.tgz", + "integrity": "sha512-qtItTDssZ/5GFfi94hrILu9j/VUeFPDPkhovEfmWFj2ipTxnzPB8DdHgfbb8HYTzLTYhrndKmyQxXUz/PDLenw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.7.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@css-render/plugin-bem": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz", + "integrity": "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "css-render": "~0.15.14" + } + }, + "node_modules/@css-render/vue3-ssr": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz", + "integrity": "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@iconify-json/carbon": { + "version": "1.2.25", + "resolved": "https://registry.npmjs.org/@iconify-json/carbon/-/carbon-1.2.25.tgz", + "integrity": "sha512-7GLgXnmi47Skh8DcPPiP8U3vgm3rOVziEe+flCdG/kwiVaeb649U3Eqt/ej4f3NvlZK8BrOxpR3xQlWFLZYblQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", + "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==", + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", + "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.131.0.tgz", + "integrity": "sha512-t2xicr9pfzkSRYx5aPqZqlLaayIwJTqgQ81Jor31Xep2nGyL2Aq3d0K5wOfeR7VevaSdxaS9dzSQP9xDwn8fDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.131.0.tgz", + "integrity": "sha512-nlGIod6gw75x1aEDgLS+srj+JRGY0HHm9MI9YgzE/B64l6d6+H3MSP9NOgp0+HTg8tp4vV9rVfgQGgd+TfVZcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.131.0.tgz", + "integrity": "sha512-jukuV6xe5RbQKFo7QD34NDCLDZp4PSOm8rmckhNdH/60ymG5zXbDzGBEyc+nTkuLQNama2aSGCt+CPfpjNTqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.131.0.tgz", + "integrity": "sha512-g3JOo4khe9rslHm5WYaVDWb0HS/M1MLR3I9S8560MkKIcC96VQY00QjOlsuRyfSj/JDXj8i9T7ryPO2RidiXVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.131.0.tgz", + "integrity": "sha512-1hziITDTxjMePnX+dR9ocVT+EuZkQ8wm4FPAbmbEiKG+Phbo73J1ZnPAA6Y/aGsWF3McOFnQuZIktAFwalkfJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.131.0.tgz", + "integrity": "sha512-9uRxfXwyKG9+MwmGQBo2ncPNwZH5HTmCETFM2WiuDBNDCW4NC5ttSQkwCAMrTAWgwMzVBH1CP8pM0v7nebCWXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.131.0.tgz", + "integrity": "sha512-mgbLvzRShXOLBdWGInf08Af4q+pfj1xD8hSgLClDZ9of/BXkB6+LIhTH7fihiDUipqB3yoSkKBWaZ3Ejlf5Yag==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.131.0.tgz", + "integrity": "sha512-OPT8++4aN6j2GJ8+3IZHS/byXoZP4aSBn+FoG6rgBJ2fKwPKXWF3MqrFMNW7NKHM28FLY579xYLxJSfgobEqPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.131.0.tgz", + "integrity": "sha512-vtPiwmfVTAXzaxDKsOXG+LwgRAA7WEnaeHzhS5z0GE89gAK18KSXnly7Z6saXXq6L3dVMyK44uoTI03zKxrpmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.131.0.tgz", + "integrity": "sha512-8AW8L7w5cGHSdZPcyZX2yR0+GUODsT15rbRjfdD54rv6DMbtuEB19ysLOpKJlRGfH6UNYNpCHaU1uJWgTWf1/w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.131.0.tgz", + "integrity": "sha512-vvpjkjEOUsPcsYf8evE4MO3aGx9+3wodXEBOicGNnOwTuAik8eBONNkgSdhkGsAblQmfVHJyanRnpxglddTXIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.131.0.tgz", + "integrity": "sha512-AqmcNC3fClXX+fxQ6VGEN1667xVFiRBkY0CZmDMSiaeFUsv1+UkBPYYi48IUKcA9/ivvoKNRzQl2I4//kT9F/w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.131.0.tgz", + "integrity": "sha512-7d3jOMKy7RSQCcDLIci+ySll2FgsOMl/GiRux4q2JNv0zg4EdhFISa9idvrdN/HEUIQQJNg6dmveUeJl2YErGA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.131.0.tgz", + "integrity": "sha512-JHK/h95qVqVQ+ITER837kcTdwBDFpFaNnOTYGCP0zdUSX/mLKC7tXOoyrTb6vG7iRPwGlcgBil3v2IjYw1FqJA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.131.0.tgz", + "integrity": "sha512-b2BO82O8azXAyf7EUgOPKu145nWypbNyk07HbU09fkzhm9lEA5oPvaN/M8Nlo7tOErVTa2WOgS4QbOnxAPXdDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.131.0.tgz", + "integrity": "sha512-GHO9glZaX7LkX/OGfluEPf1yjg+ehiFbUdowbX6uNWOQhmwKWU4m4+nZ9FJkrHNKuxyI1KKertMdGjVKCApKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.131.0.tgz", + "integrity": "sha512-3SkikPaEFoih1N83qLVEDLRLeY4nYsf6JT9SnWiMCQ5lGQdKup6bEuKCqkRiG9dD1IIaFeYz9RjlciPmYoFIWA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.131.0.tgz", + "integrity": "sha512-Os5bEhryeA2jkH+ZrnZyAC1EP5gs+X4YB1Fjqml7UPD5kU7ecsK1MPEVMfCrdt/GDNpDbavYXiOXOdyJ5b3OPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.131.0.tgz", + "integrity": "sha512-m+jNz9EuF0NXoiptc6B9h5yompZQVW/a5MJeOu5zojfH5yWk82tvF2ccrHkfhgtrS9h9DD5l1Qv8dWlfY7Nz8g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.131.0.tgz", + "integrity": "sha512-o14Hk8dAyiEUMFEWEgmAwFZvBt1RzAYLM3xeQ+5315JXgVYhoemivgYcbYVRbsFkS71ShMGlAFE0kPnr460rww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.131.0.tgz", + "integrity": "sha512-PgnWDfV0h+b16XNKbXU7Daib/BFSt/J2mEzfYIBu6JB/wNdlU+kVYXCkGA1A9fWkTbOgbjh4e6NhPeQOYvFhEA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@quansync/fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", + "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "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/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tiptap/core": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.29.2.tgz", + "integrity": "sha512-oKUkiPUB7noilVYxI9lNzUD4rX17sHub+PYjMfHMWHG9A3nvIy+FdePIVIIhThKWF7ijhr3eIqHY51Bn+GAFtw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.29.2.tgz", + "integrity": "sha512-ca4OzKDh0yaxg2+Z56bC2QnWsNsFp2YMRfVig1PDXyMVFMNJpLcnhxgq/9btn+xYAlYrj8RymOeCTYREOR6Zjg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.29.2.tgz", + "integrity": "sha512-elYbGxJsYnBb4leqrcjdIJuiG380BcOgN+UUzvOv+qEjfGVzHodFOMBl3qnmD6urYHNu5/qQK2S0qSSRXKCLNQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.29.2.tgz", + "integrity": "sha512-kzcWarcpr031rZu+R3Hzut5uxb3Qj/xxMDEYZIQ3uiq1yb5vEMXj8/SIyWautk6Nu8Rr1leV3rGdR4NzhzyFAA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.29.2.tgz", + "integrity": "sha512-3bWcCUPbCHv0XttlMdnAtXLNYWx2pblByMgxmGsaP9FU0QnslGXty6A6gHCqI33ygRg1vrA6U5Wtpwbi5aKu5g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.29.2" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.29.2.tgz", + "integrity": "sha512-c6W5UGuB7WNLpYocsgRzpO2OOTI4QjaI9jjHRMuty9z+s9DtaYM/HrRLNwVh6MopkHb+i/89Wkv8gCS34fftig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.29.2.tgz", + "integrity": "sha512-w153ct8g6dLiPTdXQ6SOIMxX4SEo5Q50AmjdEEEcJ7ZcYUcde/ScSskLHfOYmyt5ZFAiyEwr121+pux+p3/oAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.29.2.tgz", + "integrity": "sha512-YUamvefLnsqu6124GavVTI7nqcFlQJ12ROB0oSwG69eSBZYNjg1tIs05LFrBxkwf4Xgqd6YzfJ9+FeG428RvzQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.29.2.tgz", + "integrity": "sha512-KKno7cU9r1HdR48CRrsDu69/1UjZdoslq/UcE+Kx+tdhAv/aljXMkRSNzGMrBNOBDmHRgS1+58zm21WQWdQzwA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.29.2" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.29.2.tgz", + "integrity": "sha512-CBTq4Xs5aGWr65uFCIkKBms796OhmirWf/ax//iJ4fl9IfwqjwFuc2vQs9PmuwpKn9ctDHo2sMTtvyeCvIt5LQ==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-font-family": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-3.29.2.tgz", + "integrity": "sha512-HDq9+V8LbDFH1aQRH5Au0pmoDQ7bfa/C/k50a9mujzmA/PE7mLQkduUFfyDdiN4L60p7TVJAOJaN/loGC8PDxA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-text-style": "3.29.2" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.29.2.tgz", + "integrity": "sha512-8Q39UR4/Tit759IeW9xZIe3NMwN11GsuA3FLheDyyGn7RrW02HD3HhUDlazE54Ki4HoosjFmChPlN4Ik2ubdRQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.29.2" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.29.2.tgz", + "integrity": "sha512-eUW3LN3fq8rXnjEUeI3D2QONYdLsU3yYQm4jxlErs2h4cfwrFjgf19VSUFVmm6LrFbbQ0OnDVPeVLL6iOwDw2w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.29.2.tgz", + "integrity": "sha512-6W4aIy70Mh7BNlbG9zZ5FBLhJhU2UUEzgZJ/jwYSCcB30o8McLxJSEjhtoHiX8R78Ah2/JzBGvIe5olZlbeE4A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.29.2.tgz", + "integrity": "sha512-8/ZPzbB9X85Mc9/7xVLZupQKBr2UVcQTGr512xtqMW+XkCQRHCph46tRo828YE13IMWI5fWn/FaNCqXG9cULSw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.29.2.tgz", + "integrity": "sha512-iH63V/5wsaMnY4Jz0+meaAGhaec4AiOzOduzl6ZZr5IyGhZ1kthyW84ELt0dyLI3hNceAUhaNWc+I7+vX0aoXA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.29.2.tgz", + "integrity": "sha512-DcVer5SqrexKCEP6Ip1UPxJUMvcRCCItSv0wxoGytanrimBh2smvcg6X0DWnjlsi5H0updhyl+atYCmmQXUIXA==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.29.2.tgz", + "integrity": "sha512-WPZ9BHAPT6QeIm1vdVkuoOWvy9a8/EZeJwV2VhU8LXyTAttvzyj4rsbbHyJWvYWlUSTt/QF2AZ2zhKo7u1w3/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.29.2.tgz", + "integrity": "sha512-s8vBVHHFT0Qpu7CzAZ7S1kYmSiVaDvUNvMNcZUWnxj6VPfiwmx0eXd9FsjePrRCMoM5tFmPnhFTHDxY3D/eZeQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.29.2" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.29.2.tgz", + "integrity": "sha512-R+3k8OLnxdCH7Xy9ieOwUt5m2Je74u8mikothGmsYVO2Zyq48fIbmZ+X6RBPCu7DBOI2FIUhHEFbKQeDWvDNmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.29.2" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.29.2.tgz", + "integrity": "sha512-ndCunC+UsYOpkOtL7vGnDz21UNa45WUlcO9wMT1fbuYow2QnRhsuMlCWENXI52YPPARWuQ0RDgN7q6TaxPERBg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.29.2" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.29.2.tgz", + "integrity": "sha512-7qJj5YTr11vvjNgjDN1ypOfwTovc0QOCYcit/rskeuVgnmQZOZQzC/BbyKLLG7UGnpRLemU/mEGbW9pAqjAXkQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.29.2.tgz", + "integrity": "sha512-aEvLAbddUQZ+FukCreV3q4G2HfNI+odE7E9U+wbq6XsSWKyo8/pDu1muz+TFKNre4blSMOQ3JQmw5UeHDKy+fg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.29.2.tgz", + "integrity": "sha512-Ubko45JWWHe8glBt2PiGNF8hcbys/JNalFhiR7Y1X4iOOtAxAKJJxh3+eq+//NTlGuBPdWGp7zw8EEUp7anjKA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-text-style": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.29.2.tgz", + "integrity": "sha512-aQad9V9ROaEi3hE4g5z9wQ6lv5FSnzlnWFMVJxRjNlwXgm7H0fymxb4rGfca+pAwYkiRwpKYh1LatpJw2ECQAA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.29.2.tgz", + "integrity": "sha512-K7XwH/xS/5AIREWQ00VTEf/W5U0olp7j6wwit7cdd/8nHv6h6AGr1+iEApHKoLXWQZLfGQKzJlT9W61LAl+fHA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.29.2.tgz", + "integrity": "sha512-BCz+FCAChSYtUe4BFj97HEO+nSK+J7GxbJgZG4Hg7DT/gI+hRyeNndU8efiQAx3WGdzsFi3UxRpcF1tTQM7iMQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.29.2.tgz", + "integrity": "sha512-GCOme7xHaS+DSoaA4CDcAD3l6JyBlvZhvCyfsy2Vp6j8tEoBkZWio7soYVosmlyn7zq8/64VeFZP5s47yfG7fQ==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.4.1", + "prosemirror-commands": "^1.7.1", + "prosemirror-dropcursor": "^1.8.2", + "prosemirror-gapcursor": "^1.4.1", + "prosemirror-history": "^1.5.0", + "prosemirror-inputrules": "^1.5.1", + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.11", + "prosemirror-schema-list": "^1.5.1", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.5", + "prosemirror-transform": "^1.12.0", + "prosemirror-view": "^1.41.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.29.2.tgz", + "integrity": "sha512-oTu0tysiqk4zgjEtxRHjAQgxUKaAevZwueOWwSWubHdokqp7SpcbE5n9USJv89HKuTUDm3GjnQH6q8HNn/2DsA==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.29.2", + "@tiptap/extension-blockquote": "^3.29.2", + "@tiptap/extension-bold": "^3.29.2", + "@tiptap/extension-bullet-list": "^3.29.2", + "@tiptap/extension-code": "^3.29.2", + "@tiptap/extension-code-block": "^3.29.2", + "@tiptap/extension-document": "^3.29.2", + "@tiptap/extension-dropcursor": "^3.29.2", + "@tiptap/extension-gapcursor": "^3.29.2", + "@tiptap/extension-hard-break": "^3.29.2", + "@tiptap/extension-heading": "^3.29.2", + "@tiptap/extension-horizontal-rule": "^3.29.2", + "@tiptap/extension-italic": "^3.29.2", + "@tiptap/extension-link": "^3.29.2", + "@tiptap/extension-list": "^3.29.2", + "@tiptap/extension-list-item": "^3.29.2", + "@tiptap/extension-list-keymap": "^3.29.2", + "@tiptap/extension-ordered-list": "^3.29.2", + "@tiptap/extension-paragraph": "^3.29.2", + "@tiptap/extension-strike": "^3.29.2", + "@tiptap/extension-text": "^3.29.2", + "@tiptap/extension-underline": "^3.29.2", + "@tiptap/extensions": "^3.29.2", + "@tiptap/pm": "^3.29.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/vue-3": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-3.29.2.tgz", + "integrity": "sha512-rLX+V+HXbsVlk+My8mslvbnbVXZiQZHYtbOiRgxugsqZ9SK1mr1FpgZ/ZNcotJAjecT8FJmPa45ccDnVu66eGA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.29.2", + "@tiptap/extension-floating-menu": "^3.29.2" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2", + "vue": "^3.0.0" + } + }, + "node_modules/@tsconfig/node24": { + "version": "24.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node24/-/node24-24.0.4.tgz", + "integrity": "sha512-2A933l5P5oCbv6qSxHs7ckKwobs8BDAe9SJ/Xr2Hy+nDlwmLE1GhFh/g/vXGRZWgxBg9nX/5piDtHR9Dkw/XuA==", + "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/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/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@unocss/cli": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/cli/-/cli-66.7.5.tgz", + "integrity": "sha512-fgWkECRn2LGVo9sEpmjk/KJ3NSKUDitaZCNrJcEYM93DG+ELriTHcL+VWBlF4rcZf9fdGlq1nKbbRHUZirFCHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@unocss/config": "66.7.5", + "@unocss/core": "66.7.5", + "@unocss/preset-wind3": "66.7.5", + "@unocss/preset-wind4": "66.7.5", + "@unocss/transformer-directives": "66.7.5", + "cac": "^7.0.0", + "chokidar": "^5.0.0", + "colorette": "^2.0.20", + "consola": "^3.4.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "tinyglobby": "^0.2.17", + "unplugin-utils": "^0.3.1" + }, + "bin": { + "unocss": "bin/unocss.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/config": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/config/-/config-66.7.5.tgz", + "integrity": "sha512-dkPl9glhEahJ+Xoja5ZseKKnH+vZaeaKQzg8b0otcKcPPNrHQgu1nu3QgfeOjnXOGrjZIotHwUeVtt4ZA2Skgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "colorette": "^2.0.20", + "consola": "^3.4.2", + "unconfig": "^7.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/core": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/core/-/core-66.7.5.tgz", + "integrity": "sha512-UdJb8MiMywcau8QrWEVgUAz0kvoFHyR+sACwYCgmBh/BpKJGyR/zw/Ys3wvysbm0f+i/20VGBex/QQxYVlzdyQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/extractor-arbitrary-variants": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/extractor-arbitrary-variants/-/extractor-arbitrary-variants-66.7.5.tgz", + "integrity": "sha512-5zOkbnLIJc8E749qNjLXfPHl3FdHloop12h706/ojr6idK4ix0KLm43R//uLnphixCehdHJRuHnavnM4C/kQbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/inspector": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/inspector/-/inspector-66.7.5.tgz", + "integrity": "sha512-WmTJMnj8bmRxvw/wGeShmL2eEHr2/L0BdGTXSxhfzwcO8y5XZEbBmVciLcM7pqaTXQ6JY4+KnITrDUf1urjZxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/rule-utils": "66.7.5", + "colorette": "^2.0.20", + "gzip-size": "^6.0.0", + "sirv": "^3.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-attributify": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-attributify/-/preset-attributify-66.7.5.tgz", + "integrity": "sha512-1Oi1Cp81pqYJwG3h4+OlP5A0FKyaa07MFBXaXc4Yr3faiTbsI8SKj2TqnZCb+NQvBsEqslEd+jKXGZ3lKzCVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-icons": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-icons/-/preset-icons-66.7.5.tgz", + "integrity": "sha512-ZsxadWnGGtHdANTikuMnjkQaT1qwUFJHZBF4fzVsPMnUiiKGs8rzXukwM9w3Gi9ontM7nbG2FYi9clWETSlpFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/utils": "^3.1.3", + "@unocss/core": "66.7.5", + "ofetch": "^1.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-mini": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-mini/-/preset-mini-66.7.5.tgz", + "integrity": "sha512-o37TSl4ecT0dKu+3/TYuTYht75h82SEwDNl476m9ve0KW4Pv1O2tT/9TWd7N5cutEpySr8/YtjMwtWBD+drxBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/extractor-arbitrary-variants": "66.7.5", + "@unocss/rule-utils": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-tagify": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-tagify/-/preset-tagify-66.7.5.tgz", + "integrity": "sha512-/8YB1tXVi+WmNKPhsCdtO1xnQKEkshSnWDp6AbvVP3f6qOF7adlMYpAt3kpWCoVUBsfOQ06ZQlnfricMjObyoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-typography": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-typography/-/preset-typography-66.7.5.tgz", + "integrity": "sha512-2dxC8LT9KYa29UZT4muDFl7DbIXvKktTfeSMbUGMdZKbHcuMtKSP2U52wti1PL5I9duqp4v+8G33EeVutGTUaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/rule-utils": "66.7.5" + } + }, + "node_modules/@unocss/preset-uno": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-uno/-/preset-uno-66.7.5.tgz", + "integrity": "sha512-zaUlYgNngbt50fZA/LbtsEnmPKojPqeiXCyUUiKQMv2uJQhncR32xhLZXVQ+TJD7hlfZ+6FAqlco1NIiAcub9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/preset-wind3": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-web-fonts": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-web-fonts/-/preset-web-fonts-66.7.5.tgz", + "integrity": "sha512-OLLTK7kswdSu51qxEm6O+AehecgCfS08Ivqo+280lKzFW7V1jXr5okeJ1ty+85oHCprJqzcD9iG1khxNRLomcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "ofetch": "^1.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-wind": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-wind/-/preset-wind-66.7.5.tgz", + "integrity": "sha512-LVKgGr0A9Lc7F85xGXrrb71DDXMlHGA8bcj/Kht8BCsuRdBZadNbLlnv5qnSJiHYhi3/blzcgVoaeRor6L9yNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/preset-wind3": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-wind3": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-wind3/-/preset-wind3-66.7.5.tgz", + "integrity": "sha512-atFe/7Qein+oMdyZs0hEo+3EjV99Av2LVxDbzpCungxIfbS3TnQt70EIuHXMPNCVWLYwrMV+/P1n9Ntb0E3mow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/preset-mini": "66.7.5", + "@unocss/rule-utils": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/preset-wind4": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/preset-wind4/-/preset-wind4-66.7.5.tgz", + "integrity": "sha512-n3jIvQv8x1jXpmWfvNFBIqHNARki4mKZXfxAA31gekpXsRRTg16u1+yC1+PBBJgoEDFEnnmKnG7EZP88S8LVXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/extractor-arbitrary-variants": "66.7.5", + "@unocss/rule-utils": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/rule-utils": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/rule-utils/-/rule-utils-66.7.5.tgz", + "integrity": "sha512-/AKHBRF6ZOexE3EDDv8ZEYh40P5e2Eha41IYUbE1wjigW7++ssmvimSTdufJzZvU5DGP++E3B3WEsFPprZMjAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/transformer-attributify-jsx": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/transformer-attributify-jsx/-/transformer-attributify-jsx-66.7.5.tgz", + "integrity": "sha512-r8qDwNSt0eGSwWws3xsuIHb3PZYR2embFdYoE67hD/xlYgLjX1uPy/HUlGCSSh4uLc4vVYjurr0Oq5xF/B8YiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "oxc-parser": "^0.131.0", + "oxc-walker": "^0.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/transformer-compile-class": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/transformer-compile-class/-/transformer-compile-class-66.7.5.tgz", + "integrity": "sha512-KuECgsGF7tGe808vIvKViEjI0UCUgYgneJfK+/TWBkjC7s8dLVaUtJzzcP9/r6OfGlgyn/x1YTdRqTaCENa7Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/transformer-directives": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/transformer-directives/-/transformer-directives-66.7.5.tgz", + "integrity": "sha512-VMJApXXOwlDubkW+cNMmOkfxLefFupJr+yU23gGYUB2NPsuVltc8H5CDM0gfVebpeL5CUW05evxzEAk2wsju+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5", + "@unocss/rule-utils": "66.7.5", + "css-tree": "^3.2.1" + } + }, + "node_modules/@unocss/transformer-variant-group": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/transformer-variant-group/-/transformer-variant-group-66.7.5.tgz", + "integrity": "sha512-iDmP3mM8J+IxuQf5Uh33zsi528xnGxTpKRJ0c+LCrqBTTkR2tZr4aCzNmJ0g29Js2yEOsyNXRRc8BNTuvgn0AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/core": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@unocss/vite": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/@unocss/vite/-/vite-66.7.5.tgz", + "integrity": "sha512-1z1TBCNJCR2WzjPtjzmCHr8rtzXHosMjLdsCNe6Mzsfwiw044gzzLVuiEZO9vIjkI50lvQ+gIOxOiVQG0hGAeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@unocss/config": "66.7.5", + "@unocss/core": "66.7.5", + "@unocss/inspector": "66.7.5", + "chokidar": "^5.0.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "tinyglobby": "^0.2.17", + "unplugin-utils": "^0.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", + "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", + "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@vue/babel-helper-vue-transform-on": "1.5.0", + "@vue/babel-plugin-resolve-type": "1.5.0", + "@vue/shared": "^3.5.18" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", + "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/parser": "^7.28.0", + "@vue/compiler-sfc": "^3.5.18" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz", + "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/devtools-kit": "^8.2.1" + } + }, + "node_modules/@vue/devtools-core": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-8.2.1.tgz", + "integrity": "sha512-s/VfAY9oDTb/kFEWmy461jaFde2MIV1RO/gi1vwM+PAZBZ/Pc2Ndu3BNBdZUze8QDUuyYvElbEEGA83syjJfzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.2.1", + "@vue/devtools-shared": "^8.2.1" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz", + "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.2.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz", + "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.9.tgz", + "integrity": "sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.1", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz", + "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz", + "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz", + "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/runtime-core": "3.5.41", + "@vue/shared": "3.5.41", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz", + "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz", + "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 5.8", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz", + "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.4.0", + "@vueuse/shared": "14.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz", + "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz", + "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "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/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, + "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": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-7.0.0.tgz", + "integrity": "sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.1.tgz", + "integrity": "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "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/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "dev": true, + "license": "MIT" + }, + "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/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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", + "optional": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", + "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cac": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvas": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", + "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "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==", + "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==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "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/core-js": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz", + "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/crelt": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "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/cross-spawn/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/cross-spawn/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/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/css-render": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/css-render/-/css-render-0.15.14.tgz", + "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emotion/hash": "~0.8.0", + "csstype": "~3.0.5" + } + }, + "node_modules/css-render/node_modules/csstype": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT", + "optional": true + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "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==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.402", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.402.tgz", + "integrity": "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==", + "dev": true, + "license": "ISC" + }, + "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/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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/evtd": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/evtd/-/evtd-0.2.4.tgz", + "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==", + "dev": true, + "license": "MIT" + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "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/fabric": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/fabric/-/fabric-7.4.0.tgz", + "integrity": "sha512-NalYDc3eifTl1C33zryQwpH6+XA/2ClxQrH9vkASkZw3tbkRmorpikhYMmxhUTmi7O3e9ODz0vOT8qfaCh9IVA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "canvas": "^3.2.0", + "jsdom": "^26.1.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==", + "license": "MIT" + }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "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/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/font-family-papandreou": { + "version": "0.2.0-patch2", + "resolved": "https://registry.npmjs.org/font-family-papandreou/-/font-family-papandreou-0.2.0-patch2.tgz", + "integrity": "sha512-l/YiRdBSH/eWv6OF3sLGkwErL+n0MqCICi9mppTZBOCL5vixWGDqCYvRcuxB2h7RGCTzaTKOHT2caHvCXQPRlw==", + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/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/glob/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/happy-dom": { + "version": "20.11.2", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.11.2.tgz", + "integrity": "sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "buffer-image-size": "^0.6.4", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "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/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause", + "optional": true + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "optional": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC", + "optional": true + }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "optional": true, + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-6.0.0.tgz", + "integrity": "sha512-2/8adwnK1/+Fdjyts4r6wSpfANWw8zdNhU9U/Llk59c6O+DjSisPWPykwoL8gZmocP9Dy64S7oie2g+Mia123A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", + "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/magic-regexp": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/magic-regexp/-/magic-regexp-0.10.0.tgz", + "integrity": "sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12", + "mlly": "^1.7.2", + "regexp-tree": "^0.1.27", + "type-level-regexp": "~0.1.17", + "ufo": "^1.5.4", + "unplugin": "^2.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT", + "optional": true + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/naive-ui": { + "version": "2.44.1", + "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.44.1.tgz", + "integrity": "sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@css-render/plugin-bem": "^0.15.14", + "@css-render/vue3-ssr": "^0.15.14", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "async-validator": "^4.2.5", + "css-render": "^0.15.14", + "csstype": "^3.1.3", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "evtd": "^0.2.4", + "highlight.js": "^11.8.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "seemly": "^0.3.10", + "treemate": "^0.3.11", + "vdirs": "^0.1.8", + "vooks": "^0.2.12", + "vueuc": "^0.4.65" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT", + "optional": true + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nostics": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz", + "integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==", + "license": "MIT" + }, + "node_modules/npm-normalize-package-bin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-6.0.0.tgz", + "integrity": "sha512-tdt4aFn9QamlhdN3HV2D2ccpBwO5/fyjjbXUxYA6uBjyekMZcZvDq0aSj9t5Jo+tih6AYFnt/cuIRn9013e0Uw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/npm-run-all2": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-9.0.3.tgz", + "integrity": "sha512-BQAEdU1PtYc48qYRdghW2BVTQT3VqWCoFQmO87NlM1h1PYwMCKQpUWaNyB20V26caNzFsXQDfyOdznLlHCih6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^7.0.0", + "cross-spawn": "^7.0.6", + "memorystream": "^0.3.1", + "picomatch": "^4.0.2", + "pidtree": "^1.0.0", + "read-package-json-fast": "^6.0.0", + "shell-quote": "^1.8.4", + "which": "^7.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0", + "npm": ">= 10" + } + }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "license": "MIT", + "optional": true + }, + "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/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opentype.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-2.0.0.tgz", + "integrity": "sha512-kCyjv6xdDY1W/jLWZ/L3QhhTlKUqDZMQ5+Jdlw12b3dXkKNpYBqqlMMj0YDQPShWFTMwgZI1hG14kN3XUDSg/A==", + "license": "MIT", + "bin": { + "ot": "bin/ot" + } + }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, + "node_modules/oxc-parser": { + "version": "0.131.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.131.0.tgz", + "integrity": "sha512-SJ3/7ZPbgie8dr5Z9BI/M51zZbpXba+hRSG0MDzVwMW5CRQg2fjYE0jHGlLX4eeiibGgC/mzoDFKSDHwVZEHRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.131.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.131.0", + "@oxc-parser/binding-android-arm64": "0.131.0", + "@oxc-parser/binding-darwin-arm64": "0.131.0", + "@oxc-parser/binding-darwin-x64": "0.131.0", + "@oxc-parser/binding-freebsd-x64": "0.131.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.131.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.131.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.131.0", + "@oxc-parser/binding-linux-arm64-musl": "0.131.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.131.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.131.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.131.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.131.0", + "@oxc-parser/binding-linux-x64-gnu": "0.131.0", + "@oxc-parser/binding-linux-x64-musl": "0.131.0", + "@oxc-parser/binding-openharmony-arm64": "0.131.0", + "@oxc-parser/binding-wasm32-wasi": "0.131.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.131.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.131.0", + "@oxc-parser/binding-win32-x64-msvc": "0.131.0" + } + }, + "node_modules/oxc-walker": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/oxc-walker/-/oxc-walker-0.7.0.tgz", + "integrity": "sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-regexp": "^0.10.0" + }, + "peerDependencies": { + "oxc-parser": ">=0.98.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/pako": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", + "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "(MIT AND Zlib)" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "optional": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/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/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "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/pidtree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-1.0.0.tgz", + "integrity": "sha512-avfAvjB9Dd0wdj3rjJX//yS+G79OO0KrS5pJHFJENjYGX6N4SMgEDBBI/yFy0lloOYSaC6XQxzpOAMPfSYFV/Q==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pinia": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-4.0.2.tgz", + "integrity": "sha512-yKVVA7bSj5oRZFp/Ab9wLlmyb5gPUYEiIm4ryiWTe/xe7PtkRdMVOp1X1ggvq0c6Uj7Q0Du1HnV2mtAwM0Ks1g==", + "license": "MIT", + "dependencies": { + "nostics": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/devtools-api": "^8.1.5", + "typescript": ">=5.6.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "@vue/devtools-api": { + "optional": false + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/prosemirror-changeset": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz", + "integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz", + "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.3.tgz", + "integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.11", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz", + "integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.42.2", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.2.tgz", + "integrity": "sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.8", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/quansync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", + "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "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/read-package-json-fast": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-6.0.0.tgz", + "integrity": "sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^6.0.0", + "npm-normalize-package-bin": "^6.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.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.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT", + "optional": true + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "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", + "optional": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "optional": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "optional": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/seemly": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/seemly/-/seemly-0.3.10.tgz", + "integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "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/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/specificity": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz", + "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", + "license": "MIT", + "bin": { + "specificity": "bin/specificity" + } + }, + "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/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.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-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "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/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/svg2pdf.js": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/svg2pdf.js/-/svg2pdf.js-2.7.0.tgz", + "integrity": "sha512-nXK4Wx28H0KtOktanm5nsphl1KMEoLNMelAT/776qxPAj9DshwYcqgdpKuBnY1nrcYOriQFHVQLE4tIag+aDJA==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "font-family-papandreou": "^0.2.0-patch1", + "specificity": "^0.4.1", + "svgpath": "^2.3.0" + }, + "peerDependencies": { + "jspdf": "^4.0.0 || ^3.0.0 || ^2.0.0" + } + }, + "node_modules/svgpath": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/svgpath/-/svgpath-2.6.0.tgz", + "integrity": "sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg==", + "license": "MIT", + "funding": { + "url": "https://github.com/fontello/svg2ttf?sponsor=1" + } + }, + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT", + "optional": true + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/treemate": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/treemate/-/treemate-0.3.11.tgz", + "integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "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/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-level-regexp": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/type-level-regexp/-/type-level-regexp-0.1.17.tgz", + "integrity": "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unconfig": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/unconfig/-/unconfig-7.5.0.tgz", + "integrity": "sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "defu": "^6.1.4", + "jiti": "^2.6.1", + "quansync": "^1.0.0", + "unconfig-core": "7.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unconfig-core": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz", + "integrity": "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unocss": { + "version": "66.7.5", + "resolved": "https://registry.npmjs.org/unocss/-/unocss-66.7.5.tgz", + "integrity": "sha512-nAdmU8TwnQoiLnQjZ6Hm1GmHy9lTexKsAZNEJtZnxN9wyFRc1eLjQgmh9r7UEGxBQMQLD8OZOgmk5HpwObbh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unocss/cli": "66.7.5", + "@unocss/core": "66.7.5", + "@unocss/preset-attributify": "66.7.5", + "@unocss/preset-icons": "66.7.5", + "@unocss/preset-mini": "66.7.5", + "@unocss/preset-tagify": "66.7.5", + "@unocss/preset-typography": "66.7.5", + "@unocss/preset-uno": "66.7.5", + "@unocss/preset-web-fonts": "66.7.5", + "@unocss/preset-wind": "66.7.5", + "@unocss/preset-wind3": "66.7.5", + "@unocss/preset-wind4": "66.7.5", + "@unocss/transformer-attributify-jsx": "66.7.5", + "@unocss/transformer-compile-class": "66.7.5", + "@unocss/transformer-directives": "66.7.5", + "@unocss/transformer-variant-group": "66.7.5", + "@unocss/vite": "66.7.5" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@unocss/astro": "66.7.5", + "@unocss/postcss": "66.7.5", + "@unocss/webpack": "66.7.5" + }, + "peerDependenciesMeta": { + "@unocss/astro": { + "optional": true + }, + "@unocss/postcss": { + "optional": true + }, + "@unocss/webpack": { + "optional": true + } + } + }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz", + "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz", + "integrity": "sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "optional": true + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/vdirs": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz", + "integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "evtd": "^0.2.2" + }, + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "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.4.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/vite-dev-rpc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vite-dev-rpc/-/vite-dev-rpc-2.0.0.tgz", + "integrity": "sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA==", + "dev": true, + "license": "MIT", + "dependencies": { + "birpc": "^4.0.0", + "vite-hot-client": "^2.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 || ^8.0.0" + } + }, + "node_modules/vite-dev-rpc/node_modules/birpc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-4.0.0.tgz", + "integrity": "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/vite-hot-client": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.2.0.tgz", + "integrity": "sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0" + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "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", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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/vite-plugin-inspect": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-11.4.1.tgz", + "integrity": "sha512-ShOFe2PURXGvRS5OrgmOLZOCwDTD7dEBVt0tMpFPKb9AsvqXKCRGM8QgKrUbRbJYFXScHvDPpGRd28rYidC0tA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.3.0", + "error-stack-parser-es": "^1.0.5", + "obug": "^2.1.1", + "ohash": "^2.0.11", + "open": "^11.0.0", + "perfect-debounce": "^2.1.0", + "sirv": "^3.0.2", + "unplugin-utils": "^0.3.1", + "vite-dev-rpc": "^2.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-devtools": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.2.1.tgz", + "integrity": "sha512-5JLxXWWCo5lJMw16/xVeNvJ8k2zLwZPf1vITLzya/2IePrCBeGe/p/iAokgXHZpEi39fcYtPXmO8SaKeXmqCAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-core": "^8.2.1", + "@vue/devtools-kit": "^8.2.1", + "@vue/devtools-shared": "^8.2.1", + "sirv": "^3.0.2", + "vite-plugin-inspect": "^11.3.3", + "vite-plugin-vue-inspector": "^6.0.0" + }, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-6.0.0.tgz", + "integrity": "sha512-OpyITJLgZNibxlrik1EmRtvXHDjLRxNPsWkGFTERZs2LgMEdG4W0WoFt5GIgp3a3jRou+eJR8U1zOBk/XQgEbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "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", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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/vooks": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/vooks/-/vooks-0.2.12.tgz", + "integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "evtd": "^0.2.2" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz", + "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-sfc": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/server-renderer": "3.5.41", + "@vue/shared": "3.5.41" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-codemirror6": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vue-codemirror6/-/vue-codemirror6-1.6.1.tgz", + "integrity": "sha512-0Ue6nWy055UKP/6LkT0I7nW5SRSFJ+3mokwBdHePQsj11T02mrg3A0jtHyBqH23nRHQFxd3NKw63sZJ1ygo5dA==", + "license": "MIT", + "dependencies": { + "vue-demi": "latest" + }, + "engines": { + "node": "^22.18.0 || >=24.12.0" + }, + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "codemirror": "^6.0.0", + "style-mod": "^4.0.0", + "vue": "^2.7.14 || ^3.3.4" + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.9.tgz", + "integrity": "sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.9" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vueuc": { + "version": "0.4.65", + "resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.65.tgz", + "integrity": "sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@css-render/vue3-ssr": "^0.15.10", + "@juggle/resize-observer": "^3.3.1", + "css-render": "^0.15.10", + "evtd": "^0.2.4", + "seemly": "^0.3.6", + "vdirs": "^0.1.4", + "vooks": "^0.2.4" + }, + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-7.0.0.tgz", + "integrity": "sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "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/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/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/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "optional": true + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "Apache-2.0", + "optional": true, + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5367ed9 --- /dev/null +++ b/package.json @@ -0,0 +1,61 @@ +{ + "name": "open-print", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build", + "format": "prettier --write --experimental-cli src/" + }, + "dependencies": { + "@bwip-js/generic": "^4.11.2", + "@codemirror/lang-json": "^6.0.2", + "@tiptap/extension-font-family": "^3.29.2", + "@tiptap/extension-text-style": "^3.29.2", + "@tiptap/pm": "^3.29.2", + "@tiptap/starter-kit": "^3.29.2", + "@tiptap/vue-3": "^3.29.2", + "@vueuse/core": "^14.4.0", + "ajv": "^8.20.0", + "codemirror": "^6.0.2", + "dompurify": "^3.4.13", + "fabric": "^7.4.0", + "json-schema-to-ts": "^3.1.1", + "jspdf": "^4.2.1", + "mustache": "^4.2.0", + "opentype.js": "^2.0.0", + "pinia": "^4.0.2", + "qrcode": "^1.5.4", + "stylis": "^4.4.0", + "svg2pdf.js": "^2.7.0", + "vue": "^3.5.40", + "vue-codemirror6": "^1.6.1", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@iconify-json/carbon": "^1.2.25", + "@tsconfig/node24": "^24.0.4", + "@types/node": "^24.13.3", + "@types/qrcode": "^1.5.6", + "@vitejs/plugin-vue": "^6.0.8", + "@vitest/coverage-v8": "^3.2.4", + "@vue/tsconfig": "^0.9.1", + "happy-dom": "^20.11.2", + "naive-ui": "^2.44.1", + "npm-run-all2": "^9.0.2", + "prettier": "3.9.5", + "typescript": "~6.0.0", + "unocss": "^66.7.5", + "vite": "^8.1.5", + "vite-plugin-vue-devtools": "^8.1.5", + "vitest": "^3.2.4", + "vue-tsc": "^3.3.7" + }, + "engines": { + "node": "^22.18.0 || >=24.12.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..6c7f910 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5852 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@bwip-js/generic': + specifier: ^4.11.2 + version: 4.11.2 + '@codemirror/lang-json': + specifier: ^6.0.2 + version: 6.0.2 + '@tiptap/extension-font-family': + specifier: ^3.29.2 + version: 3.29.2(@tiptap/extension-text-style@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))) + '@tiptap/extension-text-style': + specifier: ^3.29.2 + version: 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/pm': + specifier: ^3.29.2 + version: 3.29.2 + '@tiptap/starter-kit': + specifier: ^3.29.2 + version: 3.29.2 + '@tiptap/vue-3': + specifier: ^3.29.2 + version: 3.29.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)(vue@3.5.41(typescript@6.0.3)) + '@vueuse/core': + specifier: ^14.4.0 + version: 14.4.0(vue@3.5.41(typescript@6.0.3)) + ajv: + specifier: ^8.20.0 + version: 8.20.0 + codemirror: + specifier: ^6.0.2 + version: 6.0.2 + dompurify: + specifier: ^3.4.13 + version: 3.4.13 + fabric: + specifier: ^7.4.0 + version: 7.4.0 + json-schema-to-ts: + specifier: ^3.1.1 + version: 3.1.1 + jspdf: + specifier: ^4.2.1 + version: 4.2.1 + mustache: + specifier: ^4.2.0 + version: 4.2.0 + opentype.js: + specifier: ^2.0.0 + version: 2.0.0 + pinia: + specifier: ^4.0.2 + version: 4.0.2(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)) + qrcode: + specifier: ^1.5.4 + version: 1.5.4 + stylis: + specifier: ^4.4.0 + version: 4.4.0 + svg2pdf.js: + specifier: ^2.7.0 + version: 2.7.0(jspdf@4.2.1) + vue: + specifier: ^3.5.40 + version: 3.5.41(typescript@6.0.3) + vue-codemirror6: + specifier: ^1.6.1 + version: 1.6.1(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/view@6.43.8)(codemirror@6.0.2)(style-mod@4.1.3)(vue@3.5.41(typescript@6.0.3)) + xlsx: + specifier: ^0.18.5 + version: 0.18.5 + devDependencies: + '@iconify-json/carbon': + specifier: ^1.2.25 + version: 1.2.25 + '@tsconfig/node24': + specifier: ^24.0.4 + version: 24.0.4 + '@types/node': + specifier: ^24.13.3 + version: 24.13.3 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 + '@vitejs/plugin-vue': + specifier: ^6.0.8 + version: 6.0.8(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + '@vitest/coverage-v8': + specifier: ^3.2.4 + version: 3.2.7(vitest@3.2.7(@types/node@24.13.3)(happy-dom@20.11.2)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(yaml@2.9.0)) + '@vue/tsconfig': + specifier: ^0.9.1 + version: 0.9.1(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)) + happy-dom: + specifier: ^20.11.2 + version: 20.11.2 + naive-ui: + specifier: ^2.44.1 + version: 2.44.1(vue@3.5.41(typescript@6.0.3)) + npm-run-all2: + specifier: ^9.0.2 + version: 9.0.3 + prettier: + specifier: 3.9.5 + version: 3.9.5 + typescript: + specifier: ~6.0.0 + version: 6.0.3 + unocss: + specifier: ^66.7.5 + version: 66.7.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + vite: + specifier: ^8.1.5 + version: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite-plugin-vue-devtools: + specifier: ^8.1.5 + version: 8.2.1(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)) + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/node@24.13.3)(happy-dom@20.11.2)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(yaml@2.9.0) + vue-tsc: + specifier: ^3.3.7 + version: 3.3.9(typescript@6.0.3) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==, tarball: https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==, tarball: https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==, tarball: https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==, tarball: https://registry.npmmirror.com/@babel/generator/-/generator-7.29.8.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==, tarball: https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==, tarball: https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==, tarball: https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==, tarball: https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==, tarball: https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==, tarball: https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==, tarball: https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==, tarball: https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==, tarball: https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==, tarball: https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==, tarball: https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==, tarball: https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==, tarball: https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==, tarball: https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==, tarball: https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==, tarball: https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.29.7': + resolution: {integrity: sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==, tarball: https://registry.npmmirror.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.29.7': + resolution: {integrity: sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==, tarball: https://registry.npmmirror.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.29.7': + resolution: {integrity: sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==, tarball: https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==, tarball: https://registry.npmmirror.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==, tarball: https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==, tarball: https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.29.7': + resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==, tarball: https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==, tarball: https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==, tarball: https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.8.tgz} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==, tarball: https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@bwip-js/generic@4.11.2': + resolution: {integrity: sha512-NSBGrja1Md/alQyGnJ96YJYvhxsqzV1ZhIxOsADvIo3s0PGYDISvxxcGDVwLXvWWU+38dcfFNFED47Owzjewzw==, tarball: https://registry.npmmirror.com/@bwip-js/generic/-/generic-4.11.2.tgz} + + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + + '@codemirror/commands@6.10.4': + resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/lint@6.9.7': + resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} + + '@codemirror/search@6.7.1': + resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==} + + '@codemirror/state@6.7.1': + resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} + + '@codemirror/view@6.43.8': + resolution: {integrity: sha512-qtItTDssZ/5GFfi94hrILu9j/VUeFPDPkhovEfmWFj2ipTxnzPB8DdHgfbb8HYTzLTYhrndKmyQxXUz/PDLenw==} + + '@css-render/plugin-bem@0.15.14': + resolution: {integrity: sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==, tarball: https://registry.npmmirror.com/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz} + peerDependencies: + css-render: ~0.15.14 + + '@css-render/vue3-ssr@0.15.14': + resolution: {integrity: sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==, tarball: https://registry.npmmirror.com/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz} + peerDependencies: + vue: ^3.0.11 + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@emotion/hash@0.8.0': + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==, tarball: https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@iconify-json/carbon@1.2.25': + resolution: {integrity: sha512-7GLgXnmi47Skh8DcPPiP8U3vgm3rOVziEe+flCdG/kwiVaeb649U3Eqt/ej4f3NvlZK8BrOxpR3xQlWFLZYblQ==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.4': + resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==, tarball: https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==, tarball: https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, tarball: https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==, tarball: https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==, tarball: https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz} + + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==, tarball: https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz} + + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@marijn/find-cluster-break@1.0.3': + resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + + '@napi-rs/wasm-runtime@1.2.2': + resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 + + '@oxc-parser/binding-android-arm-eabi@0.131.0': + resolution: {integrity: sha512-t2xicr9pfzkSRYx5aPqZqlLaayIwJTqgQ81Jor31Xep2nGyL2Aq3d0K5wOfeR7VevaSdxaS9dzSQP9xDwn8fDg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxc-parser/binding-android-arm64@0.131.0': + resolution: {integrity: sha512-nlGIod6gw75x1aEDgLS+srj+JRGY0HHm9MI9YgzE/B64l6d6+H3MSP9NOgp0+HTg8tp4vV9rVfgQGgd+TfVZcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxc-parser/binding-darwin-arm64@0.131.0': + resolution: {integrity: sha512-jukuV6xe5RbQKFo7QD34NDCLDZp4PSOm8rmckhNdH/60ymG5zXbDzGBEyc+nTkuLQNama2aSGCt+CPfpjNTqyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxc-parser/binding-darwin-x64@0.131.0': + resolution: {integrity: sha512-g3JOo4khe9rslHm5WYaVDWb0HS/M1MLR3I9S8560MkKIcC96VQY00QjOlsuRyfSj/JDXj8i9T7ryPO2RidiXVg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxc-parser/binding-freebsd-x64@0.131.0': + resolution: {integrity: sha512-1hziITDTxjMePnX+dR9ocVT+EuZkQ8wm4FPAbmbEiKG+Phbo73J1ZnPAA6Y/aGsWF3McOFnQuZIktAFwalkfJQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxc-parser/binding-linux-arm-gnueabihf@0.131.0': + resolution: {integrity: sha512-9uRxfXwyKG9+MwmGQBo2ncPNwZH5HTmCETFM2WiuDBNDCW4NC5ttSQkwCAMrTAWgwMzVBH1CP8pM0v7nebCWXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm-musleabihf@0.131.0': + resolution: {integrity: sha512-mgbLvzRShXOLBdWGInf08Af4q+pfj1xD8hSgLClDZ9of/BXkB6+LIhTH7fihiDUipqB3yoSkKBWaZ3Ejlf5Yag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxc-parser/binding-linux-arm64-gnu@0.131.0': + resolution: {integrity: sha512-OPT8++4aN6j2GJ8+3IZHS/byXoZP4aSBn+FoG6rgBJ2fKwPKXWF3MqrFMNW7NKHM28FLY579xYLxJSfgobEqPA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-arm64-musl@0.131.0': + resolution: {integrity: sha512-vtPiwmfVTAXzaxDKsOXG+LwgRAA7WEnaeHzhS5z0GE89gAK18KSXnly7Z6saXXq6L3dVMyK44uoTI03zKxrpmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@oxc-parser/binding-linux-ppc64-gnu@0.131.0': + resolution: {integrity: sha512-8AW8L7w5cGHSdZPcyZX2yR0+GUODsT15rbRjfdD54rv6DMbtuEB19ysLOpKJlRGfH6UNYNpCHaU1uJWgTWf1/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-gnu@0.131.0': + resolution: {integrity: sha512-vvpjkjEOUsPcsYf8evE4MO3aGx9+3wodXEBOicGNnOwTuAik8eBONNkgSdhkGsAblQmfVHJyanRnpxglddTXIA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-riscv64-musl@0.131.0': + resolution: {integrity: sha512-AqmcNC3fClXX+fxQ6VGEN1667xVFiRBkY0CZmDMSiaeFUsv1+UkBPYYi48IUKcA9/ivvoKNRzQl2I4//kT9F/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxc-parser/binding-linux-s390x-gnu@0.131.0': + resolution: {integrity: sha512-7d3jOMKy7RSQCcDLIci+ySll2FgsOMl/GiRux4q2JNv0zg4EdhFISa9idvrdN/HEUIQQJNg6dmveUeJl2YErGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxc-parser/binding-linux-x64-gnu@0.131.0': + resolution: {integrity: sha512-JHK/h95qVqVQ+ITER837kcTdwBDFpFaNnOTYGCP0zdUSX/mLKC7tXOoyrTb6vG7iRPwGlcgBil3v2IjYw1FqJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-linux-x64-musl@0.131.0': + resolution: {integrity: sha512-b2BO82O8azXAyf7EUgOPKu145nWypbNyk07HbU09fkzhm9lEA5oPvaN/M8Nlo7tOErVTa2WOgS4QbOnxAPXdDQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@oxc-parser/binding-openharmony-arm64@0.131.0': + resolution: {integrity: sha512-GHO9glZaX7LkX/OGfluEPf1yjg+ehiFbUdowbX6uNWOQhmwKWU4m4+nZ9FJkrHNKuxyI1KKertMdGjVKCApKWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.131.0': + resolution: {integrity: sha512-3SkikPaEFoih1N83qLVEDLRLeY4nYsf6JT9SnWiMCQ5lGQdKup6bEuKCqkRiG9dD1IIaFeYz9RjlciPmYoFIWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@oxc-parser/binding-win32-arm64-msvc@0.131.0': + resolution: {integrity: sha512-Os5bEhryeA2jkH+ZrnZyAC1EP5gs+X4YB1Fjqml7UPD5kU7ecsK1MPEVMfCrdt/GDNpDbavYXiOXOdyJ5b3OPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxc-parser/binding-win32-ia32-msvc@0.131.0': + resolution: {integrity: sha512-m+jNz9EuF0NXoiptc6B9h5yompZQVW/a5MJeOu5zojfH5yWk82tvF2ccrHkfhgtrS9h9DD5l1Qv8dWlfY7Nz8g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxc-parser/binding-win32-x64-msvc@0.131.0': + resolution: {integrity: sha512-o14Hk8dAyiEUMFEWEgmAwFZvBt1RzAYLM3xeQ+5315JXgVYhoemivgYcbYVRbsFkS71ShMGlAFE0kPnr460rww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.131.0': + resolution: {integrity: sha512-PgnWDfV0h+b16XNKbXU7Daib/BFSt/J2mEzfYIBu6JB/wNdlU+kVYXCkGA1A9fWkTbOgbjh4e6NhPeQOYvFhEA==} + + '@oxc-project/types@0.143.0': + resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==, tarball: https://registry.npmmirror.com/@oxc-project/types/-/types-0.143.0.tgz} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==, tarball: https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz} + + '@quansync/fs@1.0.0': + resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + + '@rolldown/binding-android-arm64@1.2.3': + resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.3': + resolution: {integrity: sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.3': + resolution: {integrity: sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.3': + resolution: {integrity: sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + resolution: {integrity: sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.3': + resolution: {integrity: sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.2.3': + resolution: {integrity: sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.2.3': + resolution: {integrity: sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.2.3': + resolution: {integrity: sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.2.3': + resolution: {integrity: sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.2.3': + resolution: {integrity: sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.2.3': + resolution: {integrity: sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.3': + resolution: {integrity: sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.3': + resolution: {integrity: sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==, tarball: https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz} + + '@rollup/rollup-android-arm-eabi@4.62.4': + resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.4': + resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.4': + resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.4': + resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.4': + resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.4': + resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.4': + resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.4': + resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.4': + resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.4': + resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.4': + resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.4': + resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.4': + resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.4': + resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + cpu: [x64] + os: [win32] + + '@tiptap/core@3.29.2': + resolution: {integrity: sha512-oKUkiPUB7noilVYxI9lNzUD4rX17sHub+PYjMfHMWHG9A3nvIy+FdePIVIIhThKWF7ijhr3eIqHY51Bn+GAFtw==} + peerDependencies: + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-blockquote@3.29.2': + resolution: {integrity: sha512-ca4OzKDh0yaxg2+Z56bC2QnWsNsFp2YMRfVig1PDXyMVFMNJpLcnhxgq/9btn+xYAlYrj8RymOeCTYREOR6Zjg==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-bold@3.29.2': + resolution: {integrity: sha512-elYbGxJsYnBb4leqrcjdIJuiG380BcOgN+UUzvOv+qEjfGVzHodFOMBl3qnmD6urYHNu5/qQK2S0qSSRXKCLNQ==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-bubble-menu@3.29.2': + resolution: {integrity: sha512-kzcWarcpr031rZu+R3Hzut5uxb3Qj/xxMDEYZIQ3uiq1yb5vEMXj8/SIyWautk6Nu8Rr1leV3rGdR4NzhzyFAA==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-bullet-list@3.29.2': + resolution: {integrity: sha512-3bWcCUPbCHv0XttlMdnAtXLNYWx2pblByMgxmGsaP9FU0QnslGXty6A6gHCqI33ygRg1vrA6U5Wtpwbi5aKu5g==} + peerDependencies: + '@tiptap/extension-list': 3.29.2 + + '@tiptap/extension-code-block@3.29.2': + resolution: {integrity: sha512-w153ct8g6dLiPTdXQ6SOIMxX4SEo5Q50AmjdEEEcJ7ZcYUcde/ScSskLHfOYmyt5ZFAiyEwr121+pux+p3/oAQ==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-code@3.29.2': + resolution: {integrity: sha512-c6W5UGuB7WNLpYocsgRzpO2OOTI4QjaI9jjHRMuty9z+s9DtaYM/HrRLNwVh6MopkHb+i/89Wkv8gCS34fftig==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-document@3.29.2': + resolution: {integrity: sha512-YUamvefLnsqu6124GavVTI7nqcFlQJ12ROB0oSwG69eSBZYNjg1tIs05LFrBxkwf4Xgqd6YzfJ9+FeG428RvzQ==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-dropcursor@3.29.2': + resolution: {integrity: sha512-KKno7cU9r1HdR48CRrsDu69/1UjZdoslq/UcE+Kx+tdhAv/aljXMkRSNzGMrBNOBDmHRgS1+58zm21WQWdQzwA==} + peerDependencies: + '@tiptap/extensions': 3.29.2 + + '@tiptap/extension-floating-menu@3.29.2': + resolution: {integrity: sha512-CBTq4Xs5aGWr65uFCIkKBms796OhmirWf/ax//iJ4fl9IfwqjwFuc2vQs9PmuwpKn9ctDHo2sMTtvyeCvIt5LQ==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-font-family@3.29.2': + resolution: {integrity: sha512-HDq9+V8LbDFH1aQRH5Au0pmoDQ7bfa/C/k50a9mujzmA/PE7mLQkduUFfyDdiN4L60p7TVJAOJaN/loGC8PDxA==} + peerDependencies: + '@tiptap/extension-text-style': 3.29.2 + + '@tiptap/extension-gapcursor@3.29.2': + resolution: {integrity: sha512-8Q39UR4/Tit759IeW9xZIe3NMwN11GsuA3FLheDyyGn7RrW02HD3HhUDlazE54Ki4HoosjFmChPlN4Ik2ubdRQ==} + peerDependencies: + '@tiptap/extensions': 3.29.2 + + '@tiptap/extension-hard-break@3.29.2': + resolution: {integrity: sha512-eUW3LN3fq8rXnjEUeI3D2QONYdLsU3yYQm4jxlErs2h4cfwrFjgf19VSUFVmm6LrFbbQ0OnDVPeVLL6iOwDw2w==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-heading@3.29.2': + resolution: {integrity: sha512-6W4aIy70Mh7BNlbG9zZ5FBLhJhU2UUEzgZJ/jwYSCcB30o8McLxJSEjhtoHiX8R78Ah2/JzBGvIe5olZlbeE4A==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-horizontal-rule@3.29.2': + resolution: {integrity: sha512-8/ZPzbB9X85Mc9/7xVLZupQKBr2UVcQTGr512xtqMW+XkCQRHCph46tRo828YE13IMWI5fWn/FaNCqXG9cULSw==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-italic@3.29.2': + resolution: {integrity: sha512-iH63V/5wsaMnY4Jz0+meaAGhaec4AiOzOduzl6ZZr5IyGhZ1kthyW84ELt0dyLI3hNceAUhaNWc+I7+vX0aoXA==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-link@3.29.2': + resolution: {integrity: sha512-DcVer5SqrexKCEP6Ip1UPxJUMvcRCCItSv0wxoGytanrimBh2smvcg6X0DWnjlsi5H0updhyl+atYCmmQXUIXA==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-list-item@3.29.2': + resolution: {integrity: sha512-s8vBVHHFT0Qpu7CzAZ7S1kYmSiVaDvUNvMNcZUWnxj6VPfiwmx0eXd9FsjePrRCMoM5tFmPnhFTHDxY3D/eZeQ==} + peerDependencies: + '@tiptap/extension-list': 3.29.2 + + '@tiptap/extension-list-keymap@3.29.2': + resolution: {integrity: sha512-R+3k8OLnxdCH7Xy9ieOwUt5m2Je74u8mikothGmsYVO2Zyq48fIbmZ+X6RBPCu7DBOI2FIUhHEFbKQeDWvDNmA==} + peerDependencies: + '@tiptap/extension-list': 3.29.2 + + '@tiptap/extension-list@3.29.2': + resolution: {integrity: sha512-WPZ9BHAPT6QeIm1vdVkuoOWvy9a8/EZeJwV2VhU8LXyTAttvzyj4rsbbHyJWvYWlUSTt/QF2AZ2zhKo7u1w3/A==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-ordered-list@3.29.2': + resolution: {integrity: sha512-ndCunC+UsYOpkOtL7vGnDz21UNa45WUlcO9wMT1fbuYow2QnRhsuMlCWENXI52YPPARWuQ0RDgN7q6TaxPERBg==} + peerDependencies: + '@tiptap/extension-list': 3.29.2 + + '@tiptap/extension-paragraph@3.29.2': + resolution: {integrity: sha512-7qJj5YTr11vvjNgjDN1ypOfwTovc0QOCYcit/rskeuVgnmQZOZQzC/BbyKLLG7UGnpRLemU/mEGbW9pAqjAXkQ==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-strike@3.29.2': + resolution: {integrity: sha512-aEvLAbddUQZ+FukCreV3q4G2HfNI+odE7E9U+wbq6XsSWKyo8/pDu1muz+TFKNre4blSMOQ3JQmw5UeHDKy+fg==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-text-style@3.29.2': + resolution: {integrity: sha512-aQad9V9ROaEi3hE4g5z9wQ6lv5FSnzlnWFMVJxRjNlwXgm7H0fymxb4rGfca+pAwYkiRwpKYh1LatpJw2ECQAA==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-text@3.29.2': + resolution: {integrity: sha512-Ubko45JWWHe8glBt2PiGNF8hcbys/JNalFhiR7Y1X4iOOtAxAKJJxh3+eq+//NTlGuBPdWGp7zw8EEUp7anjKA==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extension-underline@3.29.2': + resolution: {integrity: sha512-K7XwH/xS/5AIREWQ00VTEf/W5U0olp7j6wwit7cdd/8nHv6h6AGr1+iEApHKoLXWQZLfGQKzJlT9W61LAl+fHA==} + peerDependencies: + '@tiptap/core': 3.29.2 + + '@tiptap/extensions@3.29.2': + resolution: {integrity: sha512-BCz+FCAChSYtUe4BFj97HEO+nSK+J7GxbJgZG4Hg7DT/gI+hRyeNndU8efiQAx3WGdzsFi3UxRpcF1tTQM7iMQ==} + peerDependencies: + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + + '@tiptap/pm@3.29.2': + resolution: {integrity: sha512-GCOme7xHaS+DSoaA4CDcAD3l6JyBlvZhvCyfsy2Vp6j8tEoBkZWio7soYVosmlyn7zq8/64VeFZP5s47yfG7fQ==} + + '@tiptap/starter-kit@3.29.2': + resolution: {integrity: sha512-oTu0tysiqk4zgjEtxRHjAQgxUKaAevZwueOWwSWubHdokqp7SpcbE5n9USJv89HKuTUDm3GjnQH6q8HNn/2DsA==} + + '@tiptap/vue-3@3.29.2': + resolution: {integrity: sha512-rLX+V+HXbsVlk+My8mslvbnbVXZiQZHYtbOiRgxugsqZ9SK1mr1FpgZ/ZNcotJAjecT8FJmPa45ccDnVu66eGA==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': 3.29.2 + '@tiptap/pm': 3.29.2 + vue: ^3.0.0 + + '@tsconfig/node24@24.0.4': + resolution: {integrity: sha512-2A933l5P5oCbv6qSxHs7ckKwobs8BDAe9SJ/Xr2Hy+nDlwmLE1GhFh/g/vXGRZWgxBg9nX/5piDtHR9Dkw/XuA==, tarball: https://registry.npmmirror.com/@tsconfig/node24/-/node24-24.0.4.tgz} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==, tarball: https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz} + + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==, tarball: https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.25.tgz} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==, tarball: https://registry.npmmirror.com/@types/node/-/node-24.13.3.tgz} + + '@types/pako@2.0.4': + resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@unocss/cli@66.7.5': + resolution: {integrity: sha512-fgWkECRn2LGVo9sEpmjk/KJ3NSKUDitaZCNrJcEYM93DG+ELriTHcL+VWBlF4rcZf9fdGlq1nKbbRHUZirFCHQ==} + hasBin: true + + '@unocss/config@66.7.5': + resolution: {integrity: sha512-dkPl9glhEahJ+Xoja5ZseKKnH+vZaeaKQzg8b0otcKcPPNrHQgu1nu3QgfeOjnXOGrjZIotHwUeVtt4ZA2Skgg==} + + '@unocss/core@66.7.5': + resolution: {integrity: sha512-UdJb8MiMywcau8QrWEVgUAz0kvoFHyR+sACwYCgmBh/BpKJGyR/zw/Ys3wvysbm0f+i/20VGBex/QQxYVlzdyQ==} + + '@unocss/extractor-arbitrary-variants@66.7.5': + resolution: {integrity: sha512-5zOkbnLIJc8E749qNjLXfPHl3FdHloop12h706/ojr6idK4ix0KLm43R//uLnphixCehdHJRuHnavnM4C/kQbA==} + + '@unocss/inspector@66.7.5': + resolution: {integrity: sha512-WmTJMnj8bmRxvw/wGeShmL2eEHr2/L0BdGTXSxhfzwcO8y5XZEbBmVciLcM7pqaTXQ6JY4+KnITrDUf1urjZxQ==} + + '@unocss/preset-attributify@66.7.5': + resolution: {integrity: sha512-1Oi1Cp81pqYJwG3h4+OlP5A0FKyaa07MFBXaXc4Yr3faiTbsI8SKj2TqnZCb+NQvBsEqslEd+jKXGZ3lKzCVOQ==} + + '@unocss/preset-icons@66.7.5': + resolution: {integrity: sha512-ZsxadWnGGtHdANTikuMnjkQaT1qwUFJHZBF4fzVsPMnUiiKGs8rzXukwM9w3Gi9ontM7nbG2FYi9clWETSlpFg==} + + '@unocss/preset-mini@66.7.5': + resolution: {integrity: sha512-o37TSl4ecT0dKu+3/TYuTYht75h82SEwDNl476m9ve0KW4Pv1O2tT/9TWd7N5cutEpySr8/YtjMwtWBD+drxBg==} + + '@unocss/preset-tagify@66.7.5': + resolution: {integrity: sha512-/8YB1tXVi+WmNKPhsCdtO1xnQKEkshSnWDp6AbvVP3f6qOF7adlMYpAt3kpWCoVUBsfOQ06ZQlnfricMjObyoQ==} + + '@unocss/preset-typography@66.7.5': + resolution: {integrity: sha512-2dxC8LT9KYa29UZT4muDFl7DbIXvKktTfeSMbUGMdZKbHcuMtKSP2U52wti1PL5I9duqp4v+8G33EeVutGTUaQ==} + + '@unocss/preset-uno@66.7.5': + resolution: {integrity: sha512-zaUlYgNngbt50fZA/LbtsEnmPKojPqeiXCyUUiKQMv2uJQhncR32xhLZXVQ+TJD7hlfZ+6FAqlco1NIiAcub9w==} + + '@unocss/preset-web-fonts@66.7.5': + resolution: {integrity: sha512-OLLTK7kswdSu51qxEm6O+AehecgCfS08Ivqo+280lKzFW7V1jXr5okeJ1ty+85oHCprJqzcD9iG1khxNRLomcA==} + + '@unocss/preset-wind3@66.7.5': + resolution: {integrity: sha512-atFe/7Qein+oMdyZs0hEo+3EjV99Av2LVxDbzpCungxIfbS3TnQt70EIuHXMPNCVWLYwrMV+/P1n9Ntb0E3mow==} + + '@unocss/preset-wind4@66.7.5': + resolution: {integrity: sha512-n3jIvQv8x1jXpmWfvNFBIqHNARki4mKZXfxAA31gekpXsRRTg16u1+yC1+PBBJgoEDFEnnmKnG7EZP88S8LVXA==} + + '@unocss/preset-wind@66.7.5': + resolution: {integrity: sha512-LVKgGr0A9Lc7F85xGXrrb71DDXMlHGA8bcj/Kht8BCsuRdBZadNbLlnv5qnSJiHYhi3/blzcgVoaeRor6L9yNA==} + + '@unocss/rule-utils@66.7.5': + resolution: {integrity: sha512-/AKHBRF6ZOexE3EDDv8ZEYh40P5e2Eha41IYUbE1wjigW7++ssmvimSTdufJzZvU5DGP++E3B3WEsFPprZMjAg==} + + '@unocss/transformer-attributify-jsx@66.7.5': + resolution: {integrity: sha512-r8qDwNSt0eGSwWws3xsuIHb3PZYR2embFdYoE67hD/xlYgLjX1uPy/HUlGCSSh4uLc4vVYjurr0Oq5xF/B8YiA==} + + '@unocss/transformer-compile-class@66.7.5': + resolution: {integrity: sha512-KuECgsGF7tGe808vIvKViEjI0UCUgYgneJfK+/TWBkjC7s8dLVaUtJzzcP9/r6OfGlgyn/x1YTdRqTaCENa7Xg==} + + '@unocss/transformer-directives@66.7.5': + resolution: {integrity: sha512-VMJApXXOwlDubkW+cNMmOkfxLefFupJr+yU23gGYUB2NPsuVltc8H5CDM0gfVebpeL5CUW05evxzEAk2wsju+Q==} + + '@unocss/transformer-variant-group@66.7.5': + resolution: {integrity: sha512-iDmP3mM8J+IxuQf5Uh33zsi528xnGxTpKRJ0c+LCrqBTTkR2tZr4aCzNmJ0g29Js2yEOsyNXRRc8BNTuvgn0AA==} + + '@unocss/vite@66.7.5': + resolution: {integrity: sha512-1z1TBCNJCR2WzjPtjzmCHr8rtzXHosMjLdsCNe6Mzsfwiw044gzzLVuiEZO9vIjkI50lvQ+gIOxOiVQG0hGAeA==} + peerDependencies: + vite: ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0 + + '@vitejs/plugin-vue@6.0.8': + resolution: {integrity: sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==, tarball: https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@vitest/coverage-v8@3.2.7': + resolution: {integrity: sha512-NEGWJS2XNu2PfRLQwOO3CTKj1tTETxNBdk454vDxVBhxJYhPaA/eS0nAI0c+1El1P7a60z8+i+ZrQoGESweGKg==} + peerDependencies: + '@vitest/browser': 3.2.7 + vitest: 3.2.7 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==, tarball: https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.28.tgz} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==, tarball: https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.28.tgz} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==, tarball: https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.28.tgz} + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==, tarball: https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==, tarball: https://registry.npmmirror.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==, tarball: https://registry.npmmirror.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.41': + resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==, tarball: https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.41.tgz} + + '@vue/compiler-dom@3.5.41': + resolution: {integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==, tarball: https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz} + + '@vue/compiler-sfc@3.5.41': + resolution: {integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==, tarball: https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz} + + '@vue/compiler-ssr@3.5.41': + resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==, tarball: https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz} + + '@vue/devtools-api@8.2.1': + resolution: {integrity: sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==} + + '@vue/devtools-core@8.2.1': + resolution: {integrity: sha512-s/VfAY9oDTb/kFEWmy461jaFde2MIV1RO/gi1vwM+PAZBZ/Pc2Ndu3BNBdZUze8QDUuyYvElbEEGA83syjJfzA==, tarball: https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-8.2.1.tgz} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-kit@8.2.1': + resolution: {integrity: sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==, tarball: https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz} + + '@vue/devtools-shared@8.2.1': + resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==, tarball: https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz} + + '@vue/language-core@3.3.9': + resolution: {integrity: sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==, tarball: https://registry.npmmirror.com/@vue/language-core/-/language-core-3.3.9.tgz} + + '@vue/reactivity@3.5.41': + resolution: {integrity: sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==, tarball: https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.41.tgz} + + '@vue/runtime-core@3.5.41': + resolution: {integrity: sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==, tarball: https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.41.tgz} + + '@vue/runtime-dom@3.5.41': + resolution: {integrity: sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==, tarball: https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz} + + '@vue/server-renderer@3.5.41': + resolution: {integrity: sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==, tarball: https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.41.tgz} + + '@vue/shared@3.5.41': + resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==, tarball: https://registry.npmmirror.com/@vue/shared/-/shared-3.5.41.tgz} + + '@vue/tsconfig@0.9.1': + resolution: {integrity: sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==, tarball: https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.9.1.tgz} + peerDependencies: + typescript: '>= 5.8' + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + + '@vueuse/core@14.4.0': + resolution: {integrity: sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@14.4.0': + resolution: {integrity: sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==} + + '@vueuse/shared@14.4.0': + resolution: {integrity: sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==} + peerDependencies: + vue: ^3.5.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==, tarball: https://registry.npmmirror.com/acorn/-/acorn-8.18.0.tgz} + engines: {node: '>=0.4.0'} + hasBin: true + + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + alien-signals@3.2.1: + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==, tarball: https://registry.npmmirror.com/alien-signals/-/alien-signals-3.2.1.tgz} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansi-styles@7.0.0: + resolution: {integrity: sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==, tarball: https://registry.npmmirror.com/ansi-styles/-/ansi-styles-7.0.0.tgz} + engines: {node: '>=22'} + + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==, tarball: https://registry.npmmirror.com/ansis/-/ansis-4.3.1.tgz} + engines: {node: '>=14'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@0.3.12: + resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==, tarball: https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.11.12: + resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==, tarball: https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz} + engines: {node: '>=6.0.0'} + hasBin: true + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==, tarball: https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz} + + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==, tarball: https://registry.npmmirror.com/birpc/-/birpc-4.0.0.tgz} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==, tarball: https://registry.npmmirror.com/browserslist/-/browserslist-4.28.7.tgz} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-image-size@0.6.4: + resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} + engines: {node: '>=4.0'} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==, tarball: https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz} + engines: {node: '>=18'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001806: + resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==, tarball: https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz} + + canvas@3.2.3: + resolution: {integrity: sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==} + engines: {node: ^18.12.0 || >= 20.9.0} + + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==, tarball: https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz} + engines: {node: '>= 20.19.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==, tarball: https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, tarball: https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz} + + core-js@3.50.0: + resolution: {integrity: sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, tarball: https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz} + engines: {node: '>= 8'} + + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + + css-render@0.15.14: + resolution: {integrity: sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==, tarball: https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.0.11: + resolution: {integrity: sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==, tarball: https://registry.npmmirror.com/csstype/-/csstype-3.0.11.tgz} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==, tarball: https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + date-fns-tz@3.2.0: + resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==, tarball: https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz} + peerDependencies: + date-fns: ^3.0.0 || ^4.0.0 + + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==, tarball: https://registry.npmmirror.com/date-fns/-/date-fns-4.4.0.tgz} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==, tarball: https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==, tarball: https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==, tarball: https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz} + engines: {node: '>=12'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==, tarball: https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz} + engines: {node: '>=8'} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dompurify@3.4.13: + resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.401: + resolution: {integrity: sha512-H6ViHN68nGYlChEvlIU67fn8O2/tpbWQPwck98yaJmh+08LSvHiydzDQ6oXNccLU3kNRVIRS9A4mA7CG+i6fLQ==, tarball: https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.401.tgz} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==, tarball: https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz} + engines: {node: '>=0.12'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==, tarball: https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, tarball: https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz} + engines: {node: '>=6'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==, tarball: https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + evtd@0.2.4: + resolution: {integrity: sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==, tarball: https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fabric@7.4.0: + resolution: {integrity: sha512-NalYDc3eifTl1C33zryQwpH6+XA/2ClxQrH9vkASkZw3tbkRmorpikhYMmxhUTmi7O3e9ODz0vOT8qfaCh9IVA==} + engines: {node: '>=20.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-png@6.4.0: + resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==} + + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==, tarball: https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + font-family-papandreou@0.2.0-patch2: + resolution: {integrity: sha512-l/YiRdBSH/eWv6OF3sLGkwErL+n0MqCICi9mppTZBOCL5vixWGDqCYvRcuxB2h7RGCTzaTKOHT2caHvCXQPRlw==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, tarball: https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + happy-dom@20.11.2: + resolution: {integrity: sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw==} + engines: {node: '>=20.0.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==, tarball: https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz} + engines: {node: '>=12.0.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==, tarball: https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + iobuffer@5.4.0: + resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==, tarball: https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==, tarball: https://registry.npmmirror.com/is-in-ssh/-/is-in-ssh-1.0.0.tgz} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==, tarball: https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz} + engines: {node: '>=14.16'} + hasBin: true + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==, tarball: https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, tarball: https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz} + + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==, tarball: https://registry.npmmirror.com/isexe/-/isexe-4.0.0.tgz} + engines: {node: '>=20'} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, tarball: https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, tarball: https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@6.0.0: + resolution: {integrity: sha512-2/8adwnK1/+Fdjyts4r6wSpfANWw8zdNhU9U/Llk59c6O+DjSisPWPykwoL8gZmocP9Dy64S7oie2g+Mia123A==, tarball: https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-6.0.0.tgz} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + json-schema-to-ts@3.1.1: + resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} + engines: {node: '>=16'} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, tarball: https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz} + engines: {node: '>=6'} + hasBin: true + + jspdf@4.2.1: + resolution: {integrity: sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==, tarball: https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==, tarball: https://registry.npmmirror.com/lightningcss/-/lightningcss-1.33.0.tgz} + engines: {node: '>= 12.0.0'} + + linkifyjs@4.3.3: + resolution: {integrity: sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==, tarball: https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==, tarball: https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, tarball: https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz} + + magic-regexp@0.10.0: + resolution: {integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==, tarball: https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz} + engines: {node: '>= 0.10.0'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==, tarball: https://registry.npmmirror.com/mlly/-/mlly-1.8.2.tgz} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==, tarball: https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==, tarball: https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz} + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + naive-ui@2.44.1: + resolution: {integrity: sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==, tarball: https://registry.npmmirror.com/naive-ui/-/naive-ui-2.44.1.tgz} + engines: {node: '>=20'} + peerDependencies: + vue: ^3.0.0 + + nanoid@3.3.17: + resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==, tarball: https://registry.npmmirror.com/nanoid/-/nanoid-3.3.17.tgz} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + node-abi@3.94.0: + resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==} + engines: {node: '>=10'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-releases@2.0.52: + resolution: {integrity: sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==, tarball: https://registry.npmmirror.com/node-releases/-/node-releases-2.0.52.tgz} + engines: {node: '>=18'} + + nostics@1.2.0: + resolution: {integrity: sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==, tarball: https://registry.npmmirror.com/nostics/-/nostics-1.2.0.tgz} + + npm-normalize-package-bin@6.0.0: + resolution: {integrity: sha512-tdt4aFn9QamlhdN3HV2D2ccpBwO5/fyjjbXUxYA6uBjyekMZcZvDq0aSj9t5Jo+tih6AYFnt/cuIRn9013e0Uw==, tarball: https://registry.npmmirror.com/npm-normalize-package-bin/-/npm-normalize-package-bin-6.0.0.tgz} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + npm-run-all2@9.0.3: + resolution: {integrity: sha512-BQAEdU1PtYc48qYRdghW2BVTQT3VqWCoFQmO87NlM1h1PYwMCKQpUWaNyB20V26caNzFsXQDfyOdznLlHCih6g==, tarball: https://registry.npmmirror.com/npm-run-all2/-/npm-run-all2-9.0.3.tgz} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0, npm: '>= 10'} + hasBin: true + + nwsapi@2.2.24: + resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==, tarball: https://registry.npmmirror.com/obug/-/obug-2.1.4.tgz} + engines: {node: '>=12.20.0'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==, tarball: https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==, tarball: https://registry.npmmirror.com/open/-/open-11.0.0.tgz} + engines: {node: '>=20'} + + opentype.js@2.0.0: + resolution: {integrity: sha512-kCyjv6xdDY1W/jLWZ/L3QhhTlKUqDZMQ5+Jdlw12b3dXkKNpYBqqlMMj0YDQPShWFTMwgZI1hG14kN3XUDSg/A==} + hasBin: true + + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + + oxc-parser@0.131.0: + resolution: {integrity: sha512-SJ3/7ZPbgie8dr5Z9BI/M51zZbpXba+hRSG0MDzVwMW5CRQg2fjYE0jHGlLX4eeiibGgC/mzoDFKSDHwVZEHRQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-walker@0.7.0: + resolution: {integrity: sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A==} + peerDependencies: + oxc-parser: '>=0.98.0' + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@1.8.0: + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + + pako@2.2.0: + resolution: {integrity: sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==, tarball: https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, tarball: https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==, tarball: https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, tarball: https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==, tarball: https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz} + engines: {node: '>=12'} + + pidtree@1.0.0: + resolution: {integrity: sha512-avfAvjB9Dd0wdj3rjJX//yS+G79OO0KrS5pJHFJENjYGX6N4SMgEDBBI/yFy0lloOYSaC6XQxzpOAMPfSYFV/Q==, tarball: https://registry.npmmirror.com/pidtree/-/pidtree-1.0.0.tgz} + engines: {node: '>=18'} + hasBin: true + + pinia@4.0.2: + resolution: {integrity: sha512-yKVVA7bSj5oRZFp/Ab9wLlmyb5gPUYEiIm4ryiWTe/xe7PtkRdMVOp1X1ggvq0c6Uj7Q0Du1HnV2mtAwM0Ks1g==, tarball: https://registry.npmmirror.com/pinia/-/pinia-4.0.2.tgz} + peerDependencies: + '@vue/devtools-api': ^8.1.5 + typescript: '>=5.6.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==, tarball: https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz} + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==, tarball: https://registry.npmmirror.com/postcss/-/postcss-8.5.25.tgz} + engines: {node: ^10 || ^12 || >=14} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==, tarball: https://registry.npmmirror.com/powershell-utils/-/powershell-utils-0.1.0.tgz} + engines: {node: '>=20'} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + prettier@3.9.5: + resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==, tarball: https://registry.npmmirror.com/prettier/-/prettier-3.9.5.tgz} + engines: {node: '>=14'} + hasBin: true + + prosemirror-changeset@2.4.1: + resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} + + prosemirror-commands@1.7.2: + resolution: {integrity: sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==} + + prosemirror-dropcursor@1.8.3: + resolution: {integrity: sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-model@1.25.11: + resolution: {integrity: sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} + + prosemirror-view@1.42.2: + resolution: {integrity: sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + read-package-json-fast@6.0.0: + resolution: {integrity: sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==, tarball: https://registry.npmmirror.com/read-package-json-fast/-/read-package-json-fast-6.0.0.tgz} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==, tarball: https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz} + engines: {node: '>= 20.19.0'} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + + rolldown@1.2.3: + resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==, tarball: https://registry.npmmirror.com/rolldown/-/rolldown-1.2.3.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.62.4: + resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==, tarball: https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz} + engines: {node: '>=18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + seemly@0.3.10: + resolution: {integrity: sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==, tarball: https://registry.npmmirror.com/seemly/-/seemly-0.3.10.tgz} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, tarball: https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, tarball: https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, tarball: https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz} + engines: {node: '>=8'} + + shell-quote@1.10.0: + resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==, tarball: https://registry.npmmirror.com/shell-quote/-/shell-quote-1.10.0.tgz} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==, tarball: https://registry.npmmirror.com/sirv/-/sirv-3.0.2.tgz} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, tarball: https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz} + engines: {node: '>=0.10.0'} + + specificity@0.4.1: + resolution: {integrity: sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==} + hasBin: true + + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + + svg2pdf.js@2.7.0: + resolution: {integrity: sha512-nXK4Wx28H0KtOktanm5nsphl1KMEoLNMelAT/776qxPAj9DshwYcqgdpKuBnY1nrcYOriQFHVQLE4tIag+aDJA==} + peerDependencies: + jspdf: ^4.0.0 || ^3.0.0 || ^2.0.0 + + svgpath@2.6.0: + resolution: {integrity: sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg==} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==, tarball: https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==, tarball: https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz} + engines: {node: '>=6'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + treemate@0.3.11: + resolution: {integrity: sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==, tarball: https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz} + + ts-algebra@2.0.0: + resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-level-regexp@0.1.17: + resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==, tarball: https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==, tarball: https://registry.npmmirror.com/ufo/-/ufo-1.6.4.tgz} + + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + + unconfig@7.5.0: + resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==, tarball: https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz} + + unocss@66.7.5: + resolution: {integrity: sha512-nAdmU8TwnQoiLnQjZ6Hm1GmHy9lTexKsAZNEJtZnxN9wyFRc1eLjQgmh9r7UEGxBQMQLD8OZOgmk5HpwObbh0Q==} + peerDependencies: + '@unocss/astro': 66.7.5 + '@unocss/postcss': 66.7.5 + '@unocss/webpack': 66.7.5 + peerDependenciesMeta: + '@unocss/astro': + optional: true + '@unocss/postcss': + optional: true + '@unocss/webpack': + optional: true + + unplugin-utils@0.3.2: + resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==, tarball: https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.3.2.tgz} + engines: {node: '>=20.19.0'} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==, tarball: https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + + vdirs@0.1.8: + resolution: {integrity: sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==, tarball: https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz} + peerDependencies: + vue: ^3.0.11 + + vite-dev-rpc@2.0.0: + resolution: {integrity: sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA==, tarball: https://registry.npmmirror.com/vite-dev-rpc/-/vite-dev-rpc-2.0.0.tgz} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 || ^8.0.0 + + vite-hot-client@2.2.0: + resolution: {integrity: sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==, tarball: https://registry.npmmirror.com/vite-hot-client/-/vite-hot-client-2.2.0.tgz} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0 + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-plugin-inspect@11.4.1: + resolution: {integrity: sha512-ShOFe2PURXGvRS5OrgmOLZOCwDTD7dEBVt0tMpFPKb9AsvqXKCRGM8QgKrUbRbJYFXScHvDPpGRd28rYidC0tA==, tarball: https://registry.npmmirror.com/vite-plugin-inspect/-/vite-plugin-inspect-11.4.1.tgz} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^6.0.0 || ^7.0.0-0 || ^8.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-devtools@8.2.1: + resolution: {integrity: sha512-5JLxXWWCo5lJMw16/xVeNvJ8k2zLwZPf1vITLzya/2IePrCBeGe/p/iAokgXHZpEi39fcYtPXmO8SaKeXmqCAA==, tarball: https://registry.npmmirror.com/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.2.1.tgz} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + + vite-plugin-vue-inspector@6.0.0: + resolution: {integrity: sha512-OpyITJLgZNibxlrik1EmRtvXHDjLRxNPsWkGFTERZs2LgMEdG4W0WoFt5GIgp3a3jRou+eJR8U1zOBk/XQgEbw==, tarball: https://registry.npmmirror.com/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-6.0.0.tgz} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.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 + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==, tarball: https://registry.npmmirror.com/vite/-/vite-8.2.0.tgz} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.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 + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vooks@0.2.12: + resolution: {integrity: sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==, tarball: https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz} + peerDependencies: + vue: ^3.0.0 + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==, tarball: https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz} + + vue-codemirror6@1.6.1: + resolution: {integrity: sha512-0Ue6nWy055UKP/6LkT0I7nW5SRSFJ+3mokwBdHePQsj11T02mrg3A0jtHyBqH23nRHQFxd3NKw63sZJ1ygo5dA==} + engines: {node: ^22.18.0 || >=24.12.0} + peerDependencies: + '@codemirror/autocomplete': ^6.0.0 + '@codemirror/commands': ^6.0.0 + '@codemirror/language': ^6.0.0 + '@codemirror/lint': ^6.0.0 + '@codemirror/search': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + codemirror: ^6.0.0 + style-mod: ^4.0.0 + vue: ^2.7.14 || ^3.3.4 + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-tsc@3.3.9: + resolution: {integrity: sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==, tarball: https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.3.9.tgz} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.41: + resolution: {integrity: sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==, tarball: https://registry.npmmirror.com/vue/-/vue-3.5.41.tgz} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + vueuc@0.4.65: + resolution: {integrity: sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==, tarball: https://registry.npmmirror.com/vueuc/-/vueuc-0.4.65.tgz} + peerDependencies: + vue: ^3.0.11 + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==, tarball: https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, tarball: https://registry.npmmirror.com/which/-/which-2.0.2.tgz} + engines: {node: '>= 8'} + hasBin: true + + which@7.0.0: + resolution: {integrity: sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==, tarball: https://registry.npmmirror.com/which/-/which-7.0.0.tgz} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.2: + resolution: {integrity: sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==, tarball: https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.3.1.tgz} + engines: {node: '>=20'} + + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, tarball: https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.8.0 + tinyexec: 1.3.0 + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + optional: true + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.29.7': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.7 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.8 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-member-expression-to-functions@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.29.7': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@bwip-js/generic@4.11.2': {} + + '@codemirror/autocomplete@6.20.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.10.4': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/json': 1.0.3 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.7': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + crelt: 1.0.7 + + '@codemirror/search@6.7.1': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + crelt: 1.0.7 + + '@codemirror/state@6.7.1': + dependencies: + '@marijn/find-cluster-break': 1.0.3 + + '@codemirror/view@6.43.8': + dependencies: + '@codemirror/state': 6.7.1 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + + '@css-render/plugin-bem@0.15.14(css-render@0.15.14)': + dependencies: + css-render: 0.15.14 + + '@css-render/vue3-ssr@0.15.14(vue@3.5.41(typescript@6.0.3))': + dependencies: + vue: 3.5.41(typescript@6.0.3) + + '@csstools/color-helpers@5.1.0': + optional: true + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + optional: true + + '@csstools/css-tokenizer@3.0.4': + optional: true + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emotion/hash@0.8.0': {} + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/utils@0.2.12': {} + + '@iconify-json/carbon@1.2.25': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.4': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.6': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@juggle/resize-observer@3.4.0': {} + + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@marijn/find-cluster-break@1.0.3': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-parser/binding-android-arm-eabi@0.131.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.131.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.131.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.131.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.131.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.131.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.131.0': + optional: true + + '@oxc-parser/binding-linux-arm64-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-arm64-musl@0.131.0': + optional: true + + '@oxc-parser/binding-linux-ppc64-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.131.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.131.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.131.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.131.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.131.0': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@oxc-parser/binding-win32-arm64-msvc@0.131.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.131.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.131.0': + optional: true + + '@oxc-project/types@0.131.0': {} + + '@oxc-project/types@0.143.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@polka/url@1.0.0-next.29': {} + + '@quansync/fs@1.0.0': + dependencies: + quansync: 1.0.0 + + '@rolldown/binding-android-arm64@1.2.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.3': + optional: true + + '@rolldown/binding-darwin-x64@1.2.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.3': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/rollup-android-arm-eabi@4.62.4': + optional: true + + '@rollup/rollup-android-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.4': + optional: true + + '@rollup/rollup-darwin-x64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.4': + optional: true + + '@tiptap/core@3.29.2(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-blockquote@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-bold@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-bubble-menu@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@floating-ui/dom': 1.8.0 + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + optional: true + + '@tiptap/extension-bullet-list@3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tiptap/extension-code-block@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-code@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-document@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-dropcursor@3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/extensions': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tiptap/extension-floating-menu@3.29.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@floating-ui/dom': 1.8.0 + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + optional: true + + '@tiptap/extension-font-family@3.29.2(@tiptap/extension-text-style@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)))': + dependencies: + '@tiptap/extension-text-style': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + + '@tiptap/extension-gapcursor@3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/extensions': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tiptap/extension-hard-break@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-heading@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-horizontal-rule@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-italic@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-link@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + linkifyjs: 4.3.3 + + '@tiptap/extension-list-item@3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tiptap/extension-list-keymap@3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + + '@tiptap/extension-ordered-list@3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tiptap/extension-paragraph@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-strike@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-text-style@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-text@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extension-underline@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + + '@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + + '@tiptap/pm@3.29.2': + dependencies: + prosemirror-changeset: 2.4.1 + prosemirror-commands: 1.7.2 + prosemirror-dropcursor: 1.8.3 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + '@tiptap/starter-kit@3.29.2': + dependencies: + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/extension-blockquote': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/extension-bold': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-bullet-list': 3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)) + '@tiptap/extension-code': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-code-block': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/extension-document': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-dropcursor': 3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)) + '@tiptap/extension-gapcursor': 3.29.2(@tiptap/extensions@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)) + '@tiptap/extension-hard-break': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-heading': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-horizontal-rule': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/extension-italic': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-link': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/extension-list': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/extension-list-item': 3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)) + '@tiptap/extension-list-keymap': 3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)) + '@tiptap/extension-ordered-list': 3.29.2(@tiptap/extension-list@3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)) + '@tiptap/extension-paragraph': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-strike': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-text': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extension-underline': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2)) + '@tiptap/extensions': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + + '@tiptap/vue-3@3.29.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2)(vue@3.5.41(typescript@6.0.3))': + dependencies: + '@floating-ui/dom': 1.8.0 + '@tiptap/core': 3.29.2(@tiptap/pm@3.29.2) + '@tiptap/pm': 3.29.2 + vue: 3.5.41(typescript@6.0.3) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.29.2(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + '@tiptap/extension-floating-menu': 3.29.2(@floating-ui/dom@1.8.0)(@tiptap/core@3.29.2(@tiptap/pm@3.29.2))(@tiptap/pm@3.29.2) + + '@tsconfig/node24@24.0.4': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.25 + + '@types/lodash@4.17.25': {} + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/pako@2.0.4': {} + + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 24.13.3 + + '@types/raf@3.4.3': + optional: true + + '@types/trusted-types@2.0.7': + optional: true + + '@types/web-bluetooth@0.0.21': {} + + '@types/whatwg-mimetype@3.0.2': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.13.3 + + '@unocss/cli@66.7.5': + dependencies: + '@jridgewell/remapping': 2.3.5 + '@unocss/config': 66.7.5 + '@unocss/core': 66.7.5 + '@unocss/preset-wind3': 66.7.5 + '@unocss/preset-wind4': 66.7.5 + '@unocss/transformer-directives': 66.7.5 + cac: 7.0.0 + chokidar: 5.0.0 + colorette: 2.0.20 + consola: 3.4.2 + magic-string: 0.30.21 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + tinyglobby: 0.2.17 + unplugin-utils: 0.3.2 + + '@unocss/config@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + colorette: 2.0.20 + consola: 3.4.2 + unconfig: 7.5.0 + + '@unocss/core@66.7.5': {} + + '@unocss/extractor-arbitrary-variants@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/inspector@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/rule-utils': 66.7.5 + colorette: 2.0.20 + gzip-size: 6.0.0 + sirv: 3.0.2 + + '@unocss/preset-attributify@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/preset-icons@66.7.5': + dependencies: + '@iconify/utils': 3.1.4 + '@unocss/core': 66.7.5 + ofetch: 1.5.1 + + '@unocss/preset-mini@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/extractor-arbitrary-variants': 66.7.5 + '@unocss/rule-utils': 66.7.5 + + '@unocss/preset-tagify@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/preset-typography@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/rule-utils': 66.7.5 + + '@unocss/preset-uno@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/preset-wind3': 66.7.5 + + '@unocss/preset-web-fonts@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + ofetch: 1.5.1 + + '@unocss/preset-wind3@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/preset-mini': 66.7.5 + '@unocss/rule-utils': 66.7.5 + + '@unocss/preset-wind4@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/extractor-arbitrary-variants': 66.7.5 + '@unocss/rule-utils': 66.7.5 + + '@unocss/preset-wind@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/preset-wind3': 66.7.5 + + '@unocss/rule-utils@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + magic-string: 0.30.21 + + '@unocss/transformer-attributify-jsx@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + oxc-parser: 0.131.0 + oxc-walker: 0.7.0(oxc-parser@0.131.0) + + '@unocss/transformer-compile-class@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/transformer-directives@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + '@unocss/rule-utils': 66.7.5 + css-tree: 3.2.1 + + '@unocss/transformer-variant-group@66.7.5': + dependencies: + '@unocss/core': 66.7.5 + + '@unocss/vite@66.7.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': + dependencies: + '@jridgewell/remapping': 2.3.5 + '@unocss/config': 66.7.5 + '@unocss/core': 66.7.5 + '@unocss/inspector': 66.7.5 + chokidar: 5.0.0 + magic-string: 0.30.21 + pathe: 2.0.3 + tinyglobby: 0.2.17 + unplugin-utils: 0.3.2 + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + + '@vitejs/plugin-vue@6.0.8(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vue: 3.5.41(typescript@6.0.3) + + '@vitest/coverage-v8@3.2.7(vitest@3.2.7(@types/node@24.13.3)(happy-dom@20.11.2)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(yaml@2.9.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.12 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 2.0.0 + vitest: 3.2.7(@types/node@24.13.3)(happy-dom@20.11.2)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(yaml@2.9.0) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.7)': + dependencies: + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.7) + '@vue/shared': 3.5.41 + optionalDependencies: + '@babel/core': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.29.7)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/parser': 7.29.8 + '@vue/compiler-sfc': 3.5.41 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.41': + dependencies: + '@babel/parser': 7.29.8 + '@vue/shared': 3.5.41 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.41': + dependencies: + '@vue/compiler-core': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/compiler-sfc@3.5.41': + dependencies: + '@babel/parser': 7.29.8 + '@vue/compiler-core': 3.5.41 + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-ssr': 3.5.41 + '@vue/shared': 3.5.41 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.25 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.41': + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/devtools-api@8.2.1': + dependencies: + '@vue/devtools-kit': 8.2.1 + + '@vue/devtools-core@8.2.1(vue@3.5.41(typescript@6.0.3))': + dependencies: + '@vue/devtools-kit': 8.2.1 + '@vue/devtools-shared': 8.2.1 + vue: 3.5.41(typescript@6.0.3) + + '@vue/devtools-kit@8.2.1': + dependencies: + '@vue/devtools-shared': 8.2.1 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@8.2.1': {} + + '@vue/language-core@3.3.9': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.41 + '@vue/shared': 3.5.41 + alien-signals: 3.2.1 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.5 + + '@vue/reactivity@3.5.41': + dependencies: + '@vue/shared': 3.5.41 + + '@vue/runtime-core@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/runtime-dom@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/runtime-core': 3.5.41 + '@vue/shared': 3.5.41 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.41': + dependencies: + '@vue/compiler-ssr': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/shared@3.5.41': {} + + '@vue/tsconfig@0.9.1(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3))': + optionalDependencies: + typescript: 6.0.3 + vue: 3.5.41(typescript@6.0.3) + + '@vueuse/core@14.4.0(vue@3.5.41(typescript@6.0.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.4.0 + '@vueuse/shared': 14.4.0(vue@3.5.41(typescript@6.0.3)) + vue: 3.5.41(typescript@6.0.3) + + '@vueuse/metadata@14.4.0': {} + + '@vueuse/shared@14.4.0(vue@3.5.41(typescript@6.0.3))': + dependencies: + vue: 3.5.41(typescript@6.0.3) + + acorn@8.18.0: {} + + adler-32@1.3.1: {} + + agent-base@7.1.4: + optional: true + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.5 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alien-signals@3.2.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ansi-styles@7.0.0: {} + + ansis@4.3.1: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@0.3.12: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + async-validator@4.2.5: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-arraybuffer@1.0.2: + optional: true + + base64-js@1.5.1: + optional: true + + baseline-browser-mapping@2.11.12: {} + + birpc@2.9.0: {} + + birpc@4.0.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.7: + dependencies: + baseline-browser-mapping: 2.11.12 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.401 + node-releases: 2.0.52 + update-browserslist-db: 1.2.3(browserslist@4.28.7) + + buffer-image-size@0.6.4: + dependencies: + '@types/node': 24.13.3 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + optional: true + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + cac@6.7.14: {} + + cac@7.0.0: {} + + camelcase@5.3.1: {} + + caniuse-lite@1.0.30001806: {} + + canvas@3.2.3: + dependencies: + node-addon-api: 7.1.1 + prebuild-install: 7.1.3 + optional: true + + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.29.7 + '@types/raf': 3.4.3 + core-js: 3.50.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + chownr@1.1.4: + optional: true + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.10.4 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + + codepage@1.15.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + convert-source-map@2.0.0: {} + + core-js@3.50.0: + optional: true + + crc-32@1.2.2: {} + + crelt@1.0.7: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + optional: true + + css-render@0.15.14: + dependencies: + '@emotion/hash': 0.8.0 + csstype: 3.0.11 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + optional: true + + csstype@3.0.11: {} + + csstype@3.2.3: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + optional: true + + date-fns-tz@3.2.0(date-fns@4.4.0): + dependencies: + date-fns: 4.4.0 + + date-fns@4.4.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decimal.js@10.6.0: + optional: true + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + optional: true + + deep-eql@5.0.2: {} + + deep-extend@0.6.0: + optional: true + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + defu@6.1.7: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + dijkstrajs@1.0.3: {} + + dompurify@3.4.13: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + duplexer@0.1.2: {} + + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.5.401: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + optional: true + + entities@6.0.1: + optional: true + + entities@7.0.1: {} + + error-stack-parser-es@1.0.5: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + evtd@0.2.4: {} + + expand-template@2.0.3: + optional: true + + expect-type@1.4.0: {} + + fabric@7.4.0: + optionalDependencies: + canvas: 3.2.3 + jsdom: 26.1.0(canvas@3.2.3) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + fast-deep-equal@3.1.3: {} + + fast-png@6.4.0: + dependencies: + '@types/pako': 2.0.4 + iobuffer: 5.4.0 + pako: 2.2.0 + + fast-uri@3.1.5: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + fflate@0.8.3: {} + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + font-family-papandreou@0.2.0-patch2: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + frac@1.1.2: {} + + fs-constants@1.0.0: + optional: true + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + github-from-package@0.0.0: + optional: true + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + happy-dom@20.11.2: + dependencies: + '@types/node': 24.13.3 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + buffer-image-size: 0.6.4 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.21.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + has-flag@4.0.0: {} + + highlight.js@11.11.1: {} + + hookable@5.5.3: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + optional: true + + html-escaper@2.0.2: {} + + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + optional: true + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + ieee754@1.2.1: + optional: true + + import-meta-resolve@4.2.0: {} + + inherits@2.0.4: + optional: true + + ini@1.3.8: + optional: true + + iobuffer@5.4.0: {} + + is-docker@3.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-potential-custom-element-name@1.0.1: + optional: true + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + isexe@4.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@2.7.0: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + jsdom@26.1.0(canvas@3.2.3): + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.24 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.21.2 + xml-name-validator: 5.0.0 + optionalDependencies: + canvas: 3.2.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + + jsesc@3.1.0: {} + + json-parse-even-better-errors@6.0.0: {} + + json-schema-to-ts@3.1.1: + dependencies: + '@babel/runtime': 7.29.7 + ts-algebra: 2.0.0 + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jspdf@4.2.1: + dependencies: + '@babel/runtime': 7.29.7 + fast-png: 6.4.0 + fflate: 0.8.3 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.50.0 + dompurify: 3.4.13 + html2canvas: 1.4.1 + + kolorist@1.8.0: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + linkifyjs@4.3.3: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash-es@4.18.1: {} + + lodash@4.18.1: {} + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-regexp@0.10.0: + dependencies: + estree-walker: 3.0.3 + magic-string: 0.30.21 + mlly: 1.8.2 + regexp-tree: 0.1.27 + type-level-regexp: 0.1.17 + ufo: 1.6.4 + unplugin: 2.3.11 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + + mdn-data@2.27.1: {} + + memorystream@0.3.1: {} + + mimic-response@3.1.0: + optional: true + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + + minimist@1.2.8: + optional: true + + minipass@7.1.3: {} + + mkdirp-classic@0.5.3: + optional: true + + mlly@1.8.2: + dependencies: + acorn: 8.18.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + mustache@4.2.0: {} + + naive-ui@2.44.1(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@css-render/plugin-bem': 0.15.14(css-render@0.15.14) + '@css-render/vue3-ssr': 0.15.14(vue@3.5.41(typescript@6.0.3)) + '@types/lodash': 4.17.25 + '@types/lodash-es': 4.17.12 + async-validator: 4.2.5 + css-render: 0.15.14 + csstype: 3.2.3 + date-fns: 4.4.0 + date-fns-tz: 3.2.0(date-fns@4.4.0) + evtd: 0.2.4 + highlight.js: 11.11.1 + lodash: 4.18.1 + lodash-es: 4.18.1 + seemly: 0.3.10 + treemate: 0.3.11 + vdirs: 0.1.8(vue@3.5.41(typescript@6.0.3)) + vooks: 0.2.12(vue@3.5.41(typescript@6.0.3)) + vue: 3.5.41(typescript@6.0.3) + vueuc: 0.4.65(vue@3.5.41(typescript@6.0.3)) + + nanoid@3.3.17: {} + + napi-build-utils@2.0.0: + optional: true + + node-abi@3.94.0: + dependencies: + semver: 7.8.5 + optional: true + + node-addon-api@7.1.1: + optional: true + + node-fetch-native@1.6.7: {} + + node-releases@2.0.52: {} + + nostics@1.2.0: {} + + npm-normalize-package-bin@6.0.0: {} + + npm-run-all2@9.0.3: + dependencies: + ansi-styles: 7.0.0 + cross-spawn: 7.0.6 + memorystream: 0.3.1 + picomatch: 4.0.5 + pidtree: 1.0.0 + read-package-json-fast: 6.0.0 + shell-quote: 1.10.0 + which: 7.0.0 + + nwsapi@2.2.24: + optional: true + + obug@2.1.4: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + + ohash@2.0.11: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optional: true + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + opentype.js@2.0.0: {} + + orderedmap@2.1.1: {} + + oxc-parser@0.131.0: + dependencies: + '@oxc-project/types': 0.131.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.131.0 + '@oxc-parser/binding-android-arm64': 0.131.0 + '@oxc-parser/binding-darwin-arm64': 0.131.0 + '@oxc-parser/binding-darwin-x64': 0.131.0 + '@oxc-parser/binding-freebsd-x64': 0.131.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.131.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.131.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.131.0 + '@oxc-parser/binding-linux-arm64-musl': 0.131.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.131.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.131.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.131.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.131.0 + '@oxc-parser/binding-linux-x64-gnu': 0.131.0 + '@oxc-parser/binding-linux-x64-musl': 0.131.0 + '@oxc-parser/binding-openharmony-arm64': 0.131.0 + '@oxc-parser/binding-wasm32-wasi': 0.131.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.131.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.131.0 + '@oxc-parser/binding-win32-x64-msvc': 0.131.0 + + oxc-walker@0.7.0(oxc-parser@0.131.0): + dependencies: + magic-regexp: 0.10.0 + oxc-parser: 0.131.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.8.0: {} + + pako@2.2.0: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + optional: true + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + perfect-debounce@2.1.0: {} + + performance-now@2.1.0: + optional: true + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + pidtree@1.0.0: {} + + pinia@4.0.2(@vue/devtools-api@8.2.1)(typescript@6.0.3)(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@vue/devtools-api': 8.2.1 + nostics: 1.2.0 + vue: 3.5.41(typescript@6.0.3) + optionalDependencies: + typescript: 6.0.3 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pngjs@5.0.0: {} + + postcss@8.5.25: + dependencies: + nanoid: 3.3.17 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + powershell-utils@0.1.0: {} + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.94.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.5 + tunnel-agent: 0.6.0 + optional: true + + prettier@3.9.5: {} + + prosemirror-changeset@2.4.1: + dependencies: + prosemirror-transform: 1.12.0 + + prosemirror-commands@1.7.2: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-dropcursor@1.8.3: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-view: 1.42.2 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-model@1.25.11: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.11 + + prosemirror-view@1.42.2: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + optional: true + + punycode@2.3.1: + optional: true + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + quansync@1.0.0: {} + + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + optional: true + + read-package-json-fast@6.0.0: + dependencies: + json-parse-even-better-errors: 6.0.0 + npm-normalize-package-bin: 6.0.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + optional: true + + readdirp@5.0.0: {} + + regenerator-runtime@0.13.11: + optional: true + + regexp-tree@0.1.27: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-main-filename@2.0.0: {} + + rgbcolor@1.0.1: + optional: true + + rolldown@1.2.3: + dependencies: + '@oxc-project/types': 0.143.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.2.3 + '@rolldown/binding-darwin-arm64': 1.2.3 + '@rolldown/binding-darwin-x64': 1.2.3 + '@rolldown/binding-freebsd-x64': 1.2.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.3 + '@rolldown/binding-linux-arm64-gnu': 1.2.3 + '@rolldown/binding-linux-arm64-musl': 1.2.3 + '@rolldown/binding-linux-ppc64-gnu': 1.2.3 + '@rolldown/binding-linux-s390x-gnu': 1.2.3 + '@rolldown/binding-linux-x64-gnu': 1.2.3 + '@rolldown/binding-linux-x64-musl': 1.2.3 + '@rolldown/binding-openharmony-arm64': 1.2.3 + '@rolldown/binding-win32-arm64-msvc': 1.2.3 + '@rolldown/binding-win32-x64-msvc': 1.2.3 + + rollup@4.62.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.62.4 + '@rollup/rollup-android-arm64': 4.62.4 + '@rollup/rollup-darwin-arm64': 4.62.4 + '@rollup/rollup-darwin-x64': 4.62.4 + '@rollup/rollup-freebsd-arm64': 4.62.4 + '@rollup/rollup-freebsd-x64': 4.62.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 + '@rollup/rollup-linux-arm-musleabihf': 4.62.4 + '@rollup/rollup-linux-arm64-gnu': 4.62.4 + '@rollup/rollup-linux-arm64-musl': 4.62.4 + '@rollup/rollup-linux-loong64-gnu': 4.62.4 + '@rollup/rollup-linux-loong64-musl': 4.62.4 + '@rollup/rollup-linux-ppc64-gnu': 4.62.4 + '@rollup/rollup-linux-ppc64-musl': 4.62.4 + '@rollup/rollup-linux-riscv64-gnu': 4.62.4 + '@rollup/rollup-linux-riscv64-musl': 4.62.4 + '@rollup/rollup-linux-s390x-gnu': 4.62.4 + '@rollup/rollup-linux-x64-gnu': 4.62.4 + '@rollup/rollup-linux-x64-musl': 4.62.4 + '@rollup/rollup-openbsd-x64': 4.62.4 + '@rollup/rollup-openharmony-arm64': 4.62.4 + '@rollup/rollup-win32-arm64-msvc': 4.62.4 + '@rollup/rollup-win32-ia32-msvc': 4.62.4 + '@rollup/rollup-win32-x64-gnu': 4.62.4 + '@rollup/rollup-win32-x64-msvc': 4.62.4 + fsevents: 2.3.3 + + rope-sequence@1.3.4: {} + + rrweb-cssom@0.8.0: + optional: true + + run-applescript@7.1.0: {} + + safe-buffer@5.2.1: + optional: true + + safer-buffer@2.1.2: + optional: true + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + optional: true + + seemly@0.3.10: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + set-blocking@2.0.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.10.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + simple-concat@1.0.1: + optional: true + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + optional: true + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + specificity@0.4.1: {} + + ssf@0.11.2: + dependencies: + frac: 1.1.2 + + stackback@0.0.2: {} + + stackblur-canvas@2.7.0: + optional: true + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-json-comments@2.0.1: + optional: true + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + style-mod@4.1.3: {} + + stylis@4.4.0: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + svg-pathdata@6.0.3: + optional: true + + svg2pdf.js@2.7.0(jspdf@4.2.1): + dependencies: + cssesc: 3.0.0 + font-family-papandreou: 0.2.0-patch2 + jspdf: 4.2.1 + specificity: 0.4.1 + svgpath: 2.6.0 + + svgpath@2.6.0: {} + + symbol-tree@3.2.4: + optional: true + + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + optional: true + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 10.5.0 + minimatch: 10.2.6 + + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + optional: true + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tldts-core@6.1.86: + optional: true + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + optional: true + + totalist@3.0.1: {} + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + optional: true + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + optional: true + + treemate@0.3.11: {} + + ts-algebra@2.0.0: {} + + tslib@2.8.1: + optional: true + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + type-level-regexp@0.1.17: {} + + typescript@6.0.3: {} + + ufo@1.6.4: {} + + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + quansync: 1.0.0 + + unconfig@7.5.0: + dependencies: + '@quansync/fs': 1.0.0 + defu: 6.1.7 + jiti: 2.7.0 + quansync: 1.0.0 + unconfig-core: 7.5.0 + + undici-types@7.18.2: {} + + unocss@66.7.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): + dependencies: + '@unocss/cli': 66.7.5 + '@unocss/core': 66.7.5 + '@unocss/preset-attributify': 66.7.5 + '@unocss/preset-icons': 66.7.5 + '@unocss/preset-mini': 66.7.5 + '@unocss/preset-tagify': 66.7.5 + '@unocss/preset-typography': 66.7.5 + '@unocss/preset-uno': 66.7.5 + '@unocss/preset-web-fonts': 66.7.5 + '@unocss/preset-wind': 66.7.5 + '@unocss/preset-wind3': 66.7.5 + '@unocss/preset-wind4': 66.7.5 + '@unocss/transformer-attributify-jsx': 66.7.5 + '@unocss/transformer-compile-class': 66.7.5 + '@unocss/transformer-directives': 66.7.5 + '@unocss/transformer-variant-group': 66.7.5 + '@unocss/vite': 66.7.5(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + transitivePeerDependencies: + - vite + + unplugin-utils@0.3.2: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.5 + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.18.0 + picomatch: 4.0.5 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.2.3(browserslist@4.28.7): + dependencies: + browserslist: 4.28.7 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: + optional: true + + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + optional: true + + vdirs@0.1.8(vue@3.5.41(typescript@6.0.3)): + dependencies: + evtd: 0.2.4 + vue: 3.5.41(typescript@6.0.3) + + vite-dev-rpc@2.0.0(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): + dependencies: + birpc: 4.0.0 + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite-hot-client: 2.2.0(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + + vite-hot-client@2.2.0(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): + dependencies: + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + + vite-node@3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-plugin-inspect@11.4.1(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): + dependencies: + ansis: 4.3.1 + error-stack-parser-es: 1.0.5 + obug: 2.1.4 + ohash: 2.0.11 + open: 11.0.0 + perfect-debounce: 2.1.0 + sirv: 3.0.2 + unplugin-utils: 0.3.2 + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite-dev-rpc: 2.0.0(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + + vite-plugin-vue-devtools@8.2.1(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@vue/devtools-core': 8.2.1(vue@3.5.41(typescript@6.0.3)) + '@vue/devtools-kit': 8.2.1 + '@vue/devtools-shared': 8.2.1 + sirv: 3.0.2 + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite-plugin-inspect: 11.4.1(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + vite-plugin-vue-inspector: 6.0.0(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + transitivePeerDependencies: + - '@nuxt/kit' + - supports-color + - vue + + vite-plugin-vue-inspector@6.0.0(vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.7) + '@vue/compiler-dom': 3.5.41 + kolorist: 1.8.0 + magic-string: 0.30.21 + vite: 8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + transitivePeerDependencies: + - supports-color + + vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + postcss: 8.5.25 + rollup: 4.62.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.33.0 + yaml: 2.9.0 + + vite@8.2.0(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.25 + rolldown: 1.2.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + esbuild: 0.28.2 + fsevents: 2.3.3 + jiti: 2.7.0 + yaml: 2.9.0 + + vitest@3.2.7(@types/node@24.13.3)(happy-dom@20.11.2)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(yaml@2.9.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + happy-dom: 20.11.2 + jsdom: 26.1.0(canvas@3.2.3) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vooks@0.2.12(vue@3.5.41(typescript@6.0.3)): + dependencies: + evtd: 0.2.4 + vue: 3.5.41(typescript@6.0.3) + + vscode-uri@3.1.0: {} + + vue-codemirror6@1.6.1(@codemirror/autocomplete@6.20.3)(@codemirror/commands@6.10.4)(@codemirror/language@6.12.4)(@codemirror/lint@6.9.7)(@codemirror/search@6.7.1)(@codemirror/state@6.7.1)(@codemirror/view@6.43.8)(codemirror@6.0.2)(style-mod@4.1.3)(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.10.4 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.8 + codemirror: 6.0.2 + style-mod: 4.1.3 + vue: 3.5.41(typescript@6.0.3) + vue-demi: 0.14.10(vue@3.5.41(typescript@6.0.3)) + transitivePeerDependencies: + - '@vue/composition-api' + + vue-demi@0.14.10(vue@3.5.41(typescript@6.0.3)): + dependencies: + vue: 3.5.41(typescript@6.0.3) + + vue-tsc@3.3.9(typescript@6.0.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.3.9 + typescript: 6.0.3 + + vue@3.5.41(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-sfc': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/server-renderer': 3.5.41 + '@vue/shared': 3.5.41 + optionalDependencies: + typescript: 6.0.3 + + vueuc@0.4.65(vue@3.5.41(typescript@6.0.3)): + dependencies: + '@css-render/vue3-ssr': 0.15.14(vue@3.5.41(typescript@6.0.3)) + '@juggle/resize-observer': 3.4.0 + css-render: 0.15.14 + evtd: 0.2.4 + seemly: 0.3.10 + vdirs: 0.1.8(vue@3.5.41(typescript@6.0.3)) + vooks: 0.2.12(vue@3.5.41(typescript@6.0.3)) + vue: 3.5.41(typescript@6.0.3) + + w3c-keyname@2.2.8: {} + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + optional: true + + webidl-conversions@7.0.0: + optional: true + + webpack-virtual-modules@0.6.2: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + optional: true + + whatwg-mimetype@3.0.0: {} + + whatwg-mimetype@4.0.0: + optional: true + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + optional: true + + which-module@2.0.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@7.0.0: + dependencies: + isexe: 4.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wmf@1.0.2: {} + + word@0.3.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: + optional: true + + ws@8.21.2: {} + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + + xml-name-validator@5.0.0: + optional: true + + xmlchars@2.2.0: + optional: true + + y18n@4.0.3: {} + + yallist@3.1.1: {} + + yaml@2.9.0: + optional: true + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..1417f8a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +onlyBuiltDependencies: + - canvas + - core-js + - vue-demi diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..41bdade Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/fonts/ChillKai.woff2 b/public/fonts/ChillKai.woff2 new file mode 100644 index 0000000..40d6f8d Binary files /dev/null and b/public/fonts/ChillKai.woff2 differ diff --git a/public/fonts/HedvigLettersSans-Regular.woff2 b/public/fonts/HedvigLettersSans-Regular.woff2 new file mode 100644 index 0000000..9e589e6 Binary files /dev/null and b/public/fonts/HedvigLettersSans-Regular.woff2 differ diff --git a/public/fonts/LiberationSerif-Bold.woff2 b/public/fonts/LiberationSerif-Bold.woff2 new file mode 100644 index 0000000..067d104 Binary files /dev/null and b/public/fonts/LiberationSerif-Bold.woff2 differ diff --git a/public/fonts/LiberationSerif-Italic.woff2 b/public/fonts/LiberationSerif-Italic.woff2 new file mode 100644 index 0000000..4993810 Binary files /dev/null and b/public/fonts/LiberationSerif-Italic.woff2 differ diff --git a/public/fonts/LiberationSerif-Regular.woff2 b/public/fonts/LiberationSerif-Regular.woff2 new file mode 100644 index 0000000..0bdbc23 Binary files /dev/null and b/public/fonts/LiberationSerif-Regular.woff2 differ diff --git a/public/fonts/SourceHanSansCN-Normal.woff2 b/public/fonts/SourceHanSansCN-Normal.woff2 new file mode 100644 index 0000000..682b928 Binary files /dev/null and b/public/fonts/SourceHanSansCN-Normal.woff2 differ diff --git a/public/fonts/SourceHanSerifCN-Regular.ttf b/public/fonts/SourceHanSerifCN-Regular.ttf new file mode 100644 index 0000000..b5a828d Binary files /dev/null and b/public/fonts/SourceHanSerifCN-Regular.ttf differ diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..be71e3a --- /dev/null +++ b/src/App.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/src/ai/ai.spec.ts b/src/ai/ai.spec.ts new file mode 100644 index 0000000..b8d9a38 --- /dev/null +++ b/src/ai/ai.spec.ts @@ -0,0 +1,113 @@ +import { describe, it, expect } from 'vitest' +import { extractJson } from './generate' +import { normalizeTemplate } from './normalize' +import { validateTemplate } from '@/core/spec/validator' + +describe('AI 核心:JSON 提取', () => { + it('从 ```json 代码块提取', () => { + const text = '说明一下:\n```json\n{"version":"1.0.0","document":{"type":"report"}}\n```' + const r = extractJson(text) as Record + expect(r.version).toBe('1.0.0') + }) + + it('从纯 JSON 提取', () => { + const r = extractJson('{"a":1}') as Record + expect(r.a).toBe(1) + }) + + it('从夹带散文的文本中提取首尾花括号', () => { + const text = '好的,这是模板:{"document":{"type":"report","sections":[{"type":"body","components":[]}]}} 请查收' + const r = extractJson(text) as Record + expect((r.document as Record).type).toBe('report') + }) + + it('无 JSON 返回 null', () => { + expect(extractJson('没有模板')).toBeNull() + }) +}) + +describe('AI 核心:归一化 + 协议校验', () => { + it('补齐缺省字段并产出可通过校验的模板', () => { + const raw = { + document: { + page: { width: 100, height: 150, orientation: 'portrait' }, + sections: [ + { + type: 'body', + components: [ + { type: 'text', left: 0, top: 0, width: 84, height: 12, value: '标题' }, + ], + }, + ], + }, + } + const tpl = normalizeTemplate(raw) + expect(tpl.version).toBe('1.0.0') + expect(tpl.document.type).toBe('report') + // 缺省 unit 应为 mm,margin 应有默认值 + expect(tpl.document.page.unit).toBe('mm') + expect(tpl.document.page.margin.top).toBeGreaterThanOrEqual(0) + // 控件被自动补 id + const body = tpl.document.sections.find((s) => s.type === 'body')! + expect(body.components![0]!.id).toBeTruthy() + + const result = validateTemplate(tpl) + expect(result.valid).toBe(true) + }) + + it('横向页(width>height)推导 orientation', () => { + const tpl = normalizeTemplate({ + document: { + page: { width: 297, height: 210 }, + sections: [{ type: 'body', components: [] }], + }, + }) + expect(tpl.document.page.orientation).toBe('landscape') + }) + + it('坐标纠偏:page-origin 输出被还原为 content-relative', () => { + const raw = { + document: { + page: { width: 100, height: 150, margin: { top: 8, bottom: 8, left: 8, right: 8 } }, + sections: [ + { + type: 'body', + components: [ + { type: 'text', left: 8, top: 8, width: 84, height: 12, value: '标题' }, + { type: 'text', left: 8, top: 30, width: 84, height: 8, value: '正文' }, + ], + }, + ], + }, + } + const tpl = normalizeTemplate(raw) + const body = tpl.document.sections.find((s) => s.type === 'body')! + // 双轴最小坐标命中 margin(8) → 统一减去页边距,消除整页右移 + expect(body.components![0]!.left).toBe(0) + expect(body.components![0]!.top).toBe(0) + expect(body.components![1]!.top).toBeCloseTo(22, 6) + const result = validateTemplate(tpl) + expect(result.valid).toBe(true) + }) + + it('坐标纠偏:正确 content-relative 模板不应被误伤', () => { + const raw = { + document: { + page: { width: 100, height: 150, margin: { top: 8, bottom: 8, left: 8, right: 8 } }, + sections: [ + { + type: 'body', + components: [ + { type: 'text', left: 0, top: 0, width: 84, height: 12, value: '标题' }, + { type: 'text', left: 10, top: 20, width: 74, height: 8, value: '正文' }, + ], + }, + ], + }, + } + const tpl = normalizeTemplate(raw) + const body = tpl.document.sections.find((s) => s.type === 'body')! + expect(body.components![0]!.left).toBe(0) + expect(body.components![1]!.left).toBe(10) // 保持原值,未被偏移 + }) +}) diff --git a/src/ai/client.ts b/src/ai/client.ts new file mode 100644 index 0000000..ad5ddd5 --- /dev/null +++ b/src/ai/client.ts @@ -0,0 +1,150 @@ +/** + * AI 客户端 —— 纯前端直连 OpenAI 兼容 /chat/completions(SSE 流式)。 + * 不依赖任何后端;baseURL / apiKey / model 全部由用户在前端设置里提供。 + */ + +export interface ChatMessage { + role: 'system' | 'user' | 'assistant' + content: string +} + +export interface StreamOptions { + baseURL: string + apiKey: string + model: string + messages: ChatMessage[] + onToken: (delta: string) => void + signal?: AbortSignal +} + +/** LLM 调用错误(携带 HTTP 状态码,便于前端给出友好提示) */ +export class AiRequestError extends Error { + status: number + constructor(status: number, message: string) { + super(message) + this.name = 'AiRequestError' + this.status = status + } +} + +function friendlyError(status: number, body: string): string { + if (status === 401) return 'API Key 无效或已过期,请检查设置中的 Key。' + if (status === 404) return '接口地址不正确(请确认 baseURL 含 /v1 且路径为 /chat/completions)。' + if (status === 429) return '请求过于频繁或额度不足(429)。' + if (status >= 500) return `模型服务异常(HTTP ${status})。` + // 跨域 / 网络层:fetch 抛 TypeError,没有 status + const lower = body.toLowerCase() + if (lower.includes('cors') || lower.includes('cross-origin')) { + return '跨域(CORS)被拦截:浏览器直连该地址受限。可将 baseURL 改为你自己的代理(如 Cloudflare Worker / Vercel Edge),或在支持浏览器直连的模型服务上使用。' + } + return body ? `请求失败(HTTP ${status}):${body.slice(0, 200)}` : `请求失败(HTTP ${status})` +} + +/** + * 流式对话(SSE)。逐 token 回调 onToken,最终返回完整文本。 + * 兼容 OpenAI / DeepSeek / 通义 等 OpenAI 格式(data: {choices:[{delta:{content}}]})。 + */ +export async function streamChat(opts: StreamOptions): Promise { + const url = `${opts.baseURL.replace(/\/+$/, '')}/chat/completions` + + let res: Response + try { + res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${opts.apiKey}`, + }, + body: JSON.stringify({ + model: opts.model, + messages: opts.messages, + stream: true, + temperature: 0.6, + }), + signal: opts.signal, + }) + } catch (e) { + // 网络层错误(含 CORS):fetch 抛 TypeError,无 status + const reason = e instanceof Error ? e.message : String(e) + if (reason.toLowerCase().includes('cors') || reason.toLowerCase().includes('cross-origin')) { + throw new AiRequestError( + 0, + '跨域(CORS)被拦截:浏览器直连该地址受限。可将 baseURL 改为你自己的代理(如 Cloudflare Worker / Vercel Edge),或在支持浏览器直连的模型服务上使用。', + ) + } + if (e instanceof DOMException && e.name === 'AbortError') throw e + throw new AiRequestError(0, `无法连接模型服务:${reason}`) + } + + if (!res.ok) { + const txt = await res.text().catch(() => '') + throw new AiRequestError(res.status, friendlyError(res.status, txt)) + } + + if (!res.body) { + throw new AiRequestError(0, '响应缺少流式数据体。') + } + + const reader = res.body.getReader() + const decoder = new TextDecoder() + let full = '' + let buffer = '' + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + + let nl: number + while ((nl = buffer.indexOf('\n')) >= 0) { + const line = buffer.slice(0, nl).trim() + buffer = buffer.slice(nl + 1) + if (!line || line.startsWith(':')) continue + if (line === 'data: [DONE]') continue + if (!line.startsWith('data:')) continue + const payload = line.slice(5).trim() + if (!payload) continue + try { + const obj = JSON.parse(payload) + const delta: string | undefined = obj.choices?.[0]?.delta?.content + if (delta) { + full += delta + opts.onToken(delta) + } + } catch { + // 忽略不完整的分片 + } + } + } + } finally { + // 确保 reader 被释放(中断时也清理) + try { + await reader.cancel() + } catch { + /* noop */ + } + } + + // 兜底:部分 OpenAI 兼容端点忽略 stream:true,直接返回单次 JSON(choices[0].message.content)。 + // 这种情况下上面没有触发 onToken,这里做一次补偿,保证仍能解析出模板文本。 + if (!full) { + const trimmed = buffer.trim() + if (trimmed.startsWith('{') || trimmed.startsWith('[')) { + try { + const obj = JSON.parse(trimmed) as { + choices?: Array<{ message?: { content?: string }; text?: string }> + } + const content = obj.choices?.[0]?.message?.content ?? obj.choices?.[0]?.text + if (content) { + full = content + opts.onToken(content) + } + } catch { + /* 不是纯 JSON,忽略 */ + } + } + } + + return full +} diff --git a/src/ai/generate.ts b/src/ai/generate.ts new file mode 100644 index 0000000..ceca61e --- /dev/null +++ b/src/ai/generate.ts @@ -0,0 +1,165 @@ +/** + * AI 生成编排 —— 组装提示词 → 流式调用 → 解析 JSON → 归一化 → 校验/修复。 + * 纯前端、一次性问答;不落库,结果由调用方决定如何应用。 + */ +import type { AnyControl } from '@/types/control' +import type { TemplateData } from '@/types/template' +import { validateTemplate } from '@/core/spec/validator' +import { streamChat, AiRequestError } from './client' +import { buildSystemPrompt, buildUserPrompt, getFewShot } from './schema' +import { normalizeControl, normalizeTemplate } from './normalize' +import type { AiSettings } from '@/config/ai-settings' + +export interface GenerateRequest { + prompt: string + /** 基于当前模板修改时传入 */ + currentTemplate?: TemplateData + /** 仅针对画布中选中的控件改写(C 功能):传入这些控件的协议 JSON */ + selectedControls?: AnyControl[] + /** 数据字段接地(可选) */ + datasourceFields?: string[] + settings: AiSettings + onToken?: (delta: string) => void + signal?: AbortSignal +} + +export interface GenerateResult { + ok: boolean + data?: TemplateData + /** 选区改写模式:返回的新控件集合(用于替换原选中控件) */ + controls?: AnyControl[] + error?: string + /** 模型原始输出(用于调试 / 展示) */ + raw?: string +} + +/** 从模型文本中提取 JSON(兼容 ```json 代码块 / 纯 JSON / 首尾花括号) */ +export function extractJson(text: string): unknown | null { + const trimmed = text.trim() + if (!trimmed) return null + + // 1) 直接可解析 + try { + return JSON.parse(trimmed) + } catch { + /* fallthrough */ + } + + // 2) ```json ... ``` 代码块 + const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i) + if (fence?.[1]) { + try { + return JSON.parse(fence[1].trim()) + } catch { + /* fallthrough */ + } + } + + // 3) 第一个 { 到最后一个 } + const first = trimmed.indexOf('{') + const last = trimmed.lastIndexOf('}') + if (first >= 0 && last > first) { + try { + return JSON.parse(trimmed.slice(first, last + 1)) + } catch { + /* fallthrough */ + } + } + return null +} + +function fmtError(e: unknown): string { + if (e instanceof AiRequestError) return e.message + if (e instanceof DOMException && e.name === 'AbortError') return '已取消生成。' + return e instanceof Error ? e.message : '未知错误' +} + +/** + * 生成模板。 + * - 普通 / 基于当前模板改:返回完整 TemplateData,失败时最多把校验错误回传模型重试一次。 + * - 选区改写(selectedControls):模型返回控件数组,归一化后返回 controls。 + */ +export async function generateTemplate(req: GenerateRequest): Promise { + const messages = [ + { role: 'system' as const, content: buildSystemPrompt() }, + ...getFewShot(), + { role: 'user' as const, content: buildUserPrompt(req) }, + ] + + // —— 选区改写模式:期望模型返回控件数组 —— + if (req.selectedControls && req.selectedControls.length) { + let raw = '' + try { + raw = await streamChat({ + baseURL: req.settings.baseURL, + apiKey: req.settings.apiKey, + model: req.settings.model, + messages, + onToken: (d) => req.onToken?.(d), + signal: req.signal, + }) + } catch (e) { + return { ok: false, error: fmtError(e), raw } + } + const json = extractJson(raw) + const arr = Array.isArray(json) ? json : [] + const controls = arr + .map((c) => (typeof c === 'object' && c ? normalizeControl(c as Record) : null)) + .filter((c): c is AnyControl => c !== null) + if (!controls.length) { + return { ok: false, error: '模型未返回有效的控件 JSON(应为控件数组)。', raw } + } + return { ok: true, controls, raw } + } + + // —— 完整模板模式 —— + let raw = '' + const MAX_ATTEMPTS = 2 + for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { + try { + raw = await streamChat({ + baseURL: req.settings.baseURL, + apiKey: req.settings.apiKey, + model: req.settings.model, + messages, + onToken: (d) => req.onToken?.(d), + signal: req.signal, + }) + } catch (e) { + return { ok: false, error: fmtError(e), raw } + } + + const json = extractJson(raw) + if (!json) { + return { ok: false, error: '模型未返回有效的模板 JSON。', raw } + } + + const normalized = normalizeTemplate(json) + const result = validateTemplate(normalized) + if (result.valid) { + return { ok: true, data: normalized, raw } + } + + // 重试:把校验错误反馈给模型 + if (attempt < MAX_ATTEMPTS - 1) { + const issues = result.issues + .map((i) => `${i.path || '/'}:${i.message}`) + .join(';') + messages.push({ role: 'assistant', content: raw }) + messages.push({ + role: 'user', + content: `你的输出未通过模板协议校验,请修正后只输出正确 JSON。错误:${issues}`, + }) + raw = '' + continue + } + return { + ok: false, + error: `模板校验失败:${result.issues + .map((i) => `${i.path || '/'}: ${i.message}`) + .join(';')}`, + raw, + } + } + return { ok: false, error: '生成失败(超出重试次数)。', raw } +} diff --git a/src/ai/normalize.ts b/src/ai/normalize.ts new file mode 100644 index 0000000..60772ee --- /dev/null +++ b/src/ai/normalize.ts @@ -0,0 +1,133 @@ +/** + * AI 输出归一化 —— 把模型返回的「接近协议」的 JSON 修成可通过校验的 TemplateData。 + * 设计器 loadTemplate 也会兜底缺 id,这里提前补齐让校验更稳。 + */ +import type { AnyControl, ControlType } from '@/types/control' +import type { TemplateData } from '@/types/template' + +const VALID_TYPES: ControlType[] = [ + 'text', + 'image', + 'table', + 'barcode', + 'qrcode', + 'richtext', + 'rect', + 'line', + 'zone', +] + +function genId(prefix = 'ctrl'): string { + return `${prefix}_${Math.random().toString(36).slice(2, 10)}` +} + +function num(v: unknown, fallback: number): number { + return typeof v === 'number' && Number.isFinite(v) ? v : fallback +} + +function round1(v: number): number { + return Math.round(v * 10) / 10 +} + +export function normalizeControl(raw: Record): AnyControl | null { + const type = raw.type as ControlType + if (!VALID_TYPES.includes(type)) return null + const base = { + id: typeof raw.id === 'string' && raw.id ? raw.id : genId(type), + type, + left: round1(num(raw.left, 0)), + top: round1(num(raw.top, 0)), + width: round1(Math.max(num(raw.width, 10), 1)), + height: round1(Math.max(num(raw.height, 6), 1)), + } + const extra: Record = { ...raw } + delete extra.id + delete extra.type + delete extra.left + delete extra.top + delete extra.width + delete extra.height + return { ...base, ...extra } as AnyControl +} + +function normalizeSection(raw: Record): Record | null { + const type = raw.type + if (type !== 'header' && type !== 'body' && type !== 'footer') return null + const components = Array.isArray(raw.components) + ? (raw.components as Record[]) + .map(normalizeControl) + .filter((c): c is AnyControl => c !== null) + : [] + return { + type, + ...(type !== 'body' ? { height: num(raw.height, 20) } : {}), + repeat: raw.repeat === false ? false : true, + components, + } +} + +/** 归一化整份模板 */ +export function normalizeTemplate(raw: unknown): TemplateData { + const obj = (raw && typeof raw === 'object' ? raw : {}) as Record + const doc = (obj.document && typeof obj.document === 'object' + ? obj.document + : {}) as Record + const page = (doc.page && typeof doc.page === 'object' ? doc.page : {}) as Record + const sectionsRaw = Array.isArray(doc.sections) ? (doc.sections as Record[]) : [] + + const sections = sectionsRaw + .map(normalizeSection) + .filter((s): s is Record => s !== null) + + // 至少保证有一个 body + if (!sections.some((s) => s.type === 'body')) { + sections.push({ type: 'body', components: [] }) + } + + // —— 坐标纠偏:把「相对页面(page-origin)」的 AI 输出还原为「相对内容区(content-relative)」 —— + // 触发条件:某节内控件的最小 left 接近 margin.left 且最小 top 接近 margin.top(即模型把页边距也算进了坐标)。 + // 此时统一减去页边距,避免整页内容向右下偏移一个 margin。正确生成的模板 minLeft/minTop≈0,不会触发。 + const ml = num((page.margin as Record)?.left, 10) + const mt = num((page.margin as Record)?.top, 10) + const EPS = 2 + for (const section of sections) { + const comps = (section.components as AnyControl[] | undefined) ?? [] + if (!comps.length) continue + const minLeft = Math.min(...comps.map((c) => c.left)) + const minTop = Math.min(...comps.map((c) => c.top)) + if (minLeft > 1 && Math.abs(minLeft - ml) <= EPS && minTop > 1 && Math.abs(minTop - mt) <= EPS) { + for (const c of comps) { + c.left = round1(c.left - ml) + c.top = round1(c.top - mt) + } + } + } + + const width = num(page.width, 210) + const height = num(page.height, 297) + + const normalized: TemplateData = { + version: typeof obj.version === 'string' ? obj.version : '1.0.0', + document: { + type: 'report', + page: { + width, + height, + unit: page.unit === 'in' || page.unit === 'pt' ? page.unit : 'mm', + orientation: + page.orientation === 'landscape' || width > height ? 'landscape' : 'portrait', + margin: { + top: num((page.margin as Record)?.top, 10), + bottom: num((page.margin as Record)?.bottom, 10), + left: num((page.margin as Record)?.left, 10), + right: num((page.margin as Record)?.right, 10), + }, + ...(typeof page.backgroundColor === 'string' + ? { backgroundColor: page.backgroundColor } + : {}), + }, + sections: sections as unknown as TemplateData['document']['sections'], + }, + } + return normalized +} diff --git a/src/ai/schema.ts b/src/ai/schema.ts new file mode 100644 index 0000000..4b0269f --- /dev/null +++ b/src/ai/schema.ts @@ -0,0 +1,155 @@ +/** + * AI 助手 —— 前端预设的提示词与 few-shot 示例(无后端、纯常量)。 + * + * 协议要点(节选自项目《OpenPrint-设计方案》§5): + * TemplateData = { version, document } + * document = { type:'report', page, sections:[...] } + * page = { width, height, unit:'mm', orientation:'portrait'|'landscape', + * margin:{top,bottom,left,right}, backgroundColor?, watermark? } + * section = { type:'header'|'body'|'footer', height?, repeat?, components:[...] } + * component 公共:{ id?, type, left, top, width, height, angle?, printable?, ... } + * 坐标系:mm。⚠️ left/top 是相对「内容区(页边距内侧)左上角」的偏移,渲染器会自动叠加页边距——你不要再算 margin 进去。 + */ + +const PROTOCOL_SUMMARY = `你是 OpenPrint 打印模板设计助手。根据用户用中文描述的排版需求,生成一份「打印模板」JSON。 + +模板协议(必须遵守): +- 顶层:{ "version": "1.0.0", "document": { "type": "report", "page": {...}, "sections": [...] } } +- page:{ "width": 数字, "height": 数字, "unit": "mm", "orientation": "portrait"|"landscape", "margin": { "top": 数字, "bottom": 数字, "left": 数字, "right": 数字 }, "backgroundColor"?: "#ffffff" }。横向时 width > height。常用纸张:A4 纵向 210×297、A4 横向 297×210、A5 纵向 148×210。 +- sections:数组。必须且只能有一个 type:"body";可选 type:"header" / "footer"(页眉页脚,带 height)。 +- 控件通用字段:type 必填;left/top/width/height 必填(单位 mm,原点 = 内容区左上角,即已扣除页边距);id 可省略(系统自动补)。 +- 控件类型与要点: + · text:静态文字用 value;如需绑数据用 binding(字段路径)或 expression(如 "{{order.total}}")。style 可选:{ fontSize(pt), fill(hex), fontWeight:'bold', textAlign:'left'|'center'|'right', lineHeight }。 + · image:{ value:{ mode:'url'|'binding', content } },fit 可选。 + · table:columns:[{ title, field?, width, align? }],options 可选(borders/zebra 等),dataSource 留空表示布局网格。 + · barcode:{ value, format:'CODE128'|'EAN13'..., showText? };qrcode:{ value, errorLevel? }。 + · rect / line:{ fill?, stroke?, strokeWidth?, cornerRadius? }。 + · richtext:{ value: "

HTML

" }(仅可信静态内容)。 + +设计原则: +- 间距与对齐要规整(建议控件左/右贴边留白一致,关键元素水平居中)。 +- 坐标铁律:left/top 是相对「内容区(页边距内侧)左上角」的 mm,渲染器会再叠加页边距,所以你写的坐标【不要包含 margin】。给定页面 width/height 与 margin 后,内容区宽 = width - margin.left - margin.right、高 = height - margin.top - margin.bottom;占满整行写 left:0, width:内容区宽,水平居中配合 style.textAlign:'center'。严禁出现 left 等于 margin.left、top 等于 margin.top 这类把页边距算进坐标的写法。 +- 只输出「一段简要中文说明(不超过 3 句)」+ 一个 \`\`\`json 代码块包裹的完整模板。不要输出其他内容。 +- 不依赖真实数据:文字用静态 value;除非用户明确给出字段,否则不要写 binding。 +- 颜色用 hex(如 #1677ff、#333333)或 rgba。` + +const FEW_SHOT: Array<{ role: 'user' | 'assistant'; content: string }> = [ + { + role: 'user', + content: '做一个竖向快递面单,100×150mm,上方标题"快递面单",中间收件人姓名电话和地址,底部一个条码和右下角一个二维码。', + }, + { + role: 'assistant', + content: `好的,已生成 100×150mm 的竖向快递面单:标题居中、收件信息分区、底部条码 + 右下角二维码。 +(坐标均为相对内容区:页面 100×150、四周 margin=8,内容区宽 = 100-8-8 = 84) + +\`\`\`json +{ + "version": "1.0.0", + "document": { + "type": "report", + "page": { + "width": 100, "height": 150, "unit": "mm", "orientation": "portrait", + "margin": { "top": 8, "bottom": 8, "left": 8, "right": 8 }, + "backgroundColor": "#ffffff" + }, + "sections": [ + { + "type": "body", + "components": [ + { "type": "text", "left": 0, "top": 2, "width": 84, "height": 12, "value": "快递面单", "style": { "fontSize": 20, "fontWeight": "bold", "textAlign": "center" } }, + { "type": "text", "left": 0, "top": 18, "width": 84, "height": 8, "value": "收件人:张三 13800000000", "style": { "fontSize": 11 } }, + { "type": "text", "left": 0, "top": 30, "width": 84, "height": 30, "value": "地址:广东省深圳市南山区科技园路 1 号 XX 大厦 10 层", "style": { "fontSize": 11 } }, + { "type": "text", "left": 0, "top": 64, "width": 84, "height": 8, "value": "托寄物:文件 到付", "style": { "fontSize": 11, "fill": "#555555" } }, + { "type": "barcode", "left": 0, "top": 78, "width": 84, "height": 22, "value": "SF1234567890", "format": "CODE128", "showText": true }, + { "type": "qrcode", "left": 52, "top": 104, "width": 32, "height": 28, "value": "https://example.com/order/SF1234567890" }, + { "type": "text", "left": 0, "top": 104, "width": 48, "height": 28, "value": "请于签收前核对信息", "style": { "fontSize": 10, "fill": "#888888" } } + ] + } + ] + } +} +\`\`\``, + }, + { + role: 'user', + content: '做一个横向会员卡,90×54mm,左侧品牌名和会员等级,右侧会员姓名和大号卡号,底部一条分割线。', + }, + { + role: 'assistant', + content: `已生成 90×54mm 横向会员卡:左侧品牌与等级、右侧姓名与卡号、底部分割线。 +(坐标相对内容区:页面 90×54、四周 margin=6,内容区宽 = 90-6-6 = 78) + +\`\`\`json +{ + "version": "1.0.0", + "document": { + "type": "report", + "page": { + "width": 90, "height": 54, "unit": "mm", "orientation": "landscape", + "margin": { "top": 6, "bottom": 6, "left": 6, "right": 6 }, + "backgroundColor": "#0c447c" + }, + "sections": [ + { + "type": "body", + "components": [ + { "type": "text", "left": 0, "top": 4, "width": 40, "height": 10, "value": "STAR 会员", "style": { "fontSize": 16, "fontWeight": "bold", "fill": "#ffffff" } }, + { "type": "text", "left": 0, "top": 18, "width": 40, "height": 8, "value": "钻石会员", "style": { "fontSize": 11, "fill": "#9fc3ff" } }, + { "type": "text", "left": 46, "top": 6, "width": 32, "height": 10, "value": "李雷", "style": { "fontSize": 15, "fontWeight": "bold", "fill": "#ffffff", "textAlign": "right" } }, + { "type": "text", "left": 46, "top": 20, "width": 32, "height": 10, "value": "NO. 8821 0043", "style": { "fontSize": 14, "fill": "#ffffff", "textAlign": "right", "letterSpacing": 1 } }, + { "type": "line", "left": 0, "top": 34, "width": 78, "height": 0.4, "stroke": "#ffffff", "strokeWidth": 0.4 } + ] + } + ] + } +} +\`\`\``, + }, +] + +/** 组装 system 提示词 */ +export function buildSystemPrompt(): string { + return PROTOCOL_SUMMARY +} + +/** 组装用户提示词(支持「基于当前模板修改」「仅选中控件改写」「数据字段接地」) */ +export function buildUserPrompt(opts: { + prompt: string + currentTemplate?: unknown + selectedControls?: unknown[] + datasourceFields?: string[] +}): string { + const parts: string[] = [] + if (opts.datasourceFields && opts.datasourceFields.length) { + parts.push(`可用数据字段(如用户要求绑定真实数据,请优先使用这些路径):\n${opts.datasourceFields.join('、')}\n`) + } + if (opts.selectedControls && opts.selectedControls.length) { + parts.push( + `下面是一组「用户当前选中的控件」(坐标 left/top 是相对内容区左上角的 mm)。\n` + + `请按需求改写这些选中控件并【只输出一个控件 JSON 数组】,要求:\n` + + `· 必须保留每个原控件的 id(用于原位替换),不要改变其 type;\n` + + `· 可调整 left/top/width/height/value/style 等,实现重排、对齐、换风格、统一间距等;\n` + + `· 可新增控件(给新的唯一 id,type 合法),也可删除某些选中控件(不输出即可);\n` + + `· 坐标仍是相对内容区(已扣除页边距),不要再加 margin;\n` + + `· 只输出控件数组,不要包装成完整模板。\n` + + '```json\n' + + JSON.stringify(opts.selectedControls, null, 2) + + '\n```', + ) + } else if (opts.currentTemplate) { + parts.push( + `请在下面这份「当前模板 JSON」的基础上按需求修改,输出完整的新模板(不要只输出 diff):\n` + + '```json\n' + + JSON.stringify(opts.currentTemplate, null, 2) + + '\n```', + ) + } + parts.push(`需求:${opts.prompt}`) + return parts.join('\n\n') +} + +/** few-shot 示例(user/assistant 成对) */ +export function getFewShot(): Array<{ role: 'user' | 'assistant'; content: string }> { + return FEW_SHOT +} diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..9bf2017 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/assets/main.css b/src/assets/main.css new file mode 100644 index 0000000..9c91254 --- /dev/null +++ b/src/assets/main.css @@ -0,0 +1,59 @@ +:root { + /* 旧变量保留(对齐文档,但品牌变量统一在 theme/brand.css 中) */ + --brand-primary: #1677ff; + --brand-dark: #111827; + --brand-bg: #f5f7fa; + --brand-border: #e5e7eb; +} + +html, +body, +#app { + height: 100%; + margin: 0; + padding: 0; +} + +/* naive-ui Provider 会在 #app 下包一层无样式 div,补齐高度链 */ +#app > div { + height: 100%; +} + +body { + font-family: + Inter, + 'Source Han Sans CN', + 'PingFang SC', + 'Microsoft YaHei', + system-ui, + -apple-system, + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background: var(--brand-bg); + color: var(--brand-text-1); + overflow: hidden; +} + +/* html.dark 下 body 由 NGlobalStyle 同步 bodyColor;这里加一个过渡让切换更顺 */ +* { + transition: + background-color 0.2s ease, + color 0.2s ease, + border-color 0.2s ease, + box-shadow 0.2s ease; +} +/* 画布/标尺/控件本身不需要过渡,否则拖拽有拖影 */ +canvas, +svg, +svg *, +.fabric-canvas, +canvas * { + transition: none !important; +} + +/* NInputNumber 内部 input 默认左对齐;button-placement="both" 时左右按钮挤压空间, + 统一居中更美观。覆盖内部 .n-input__input-el(naive-ui 无 textAlign prop)。 */ +.n-input-number .n-input__input-el { + text-align: center; +} diff --git a/src/config/ai-settings.ts b/src/config/ai-settings.ts new file mode 100644 index 0000000..a820d47 --- /dev/null +++ b/src/config/ai-settings.ts @@ -0,0 +1,79 @@ +/** + * AI 助手配置 —— 纯前端、零后端、存本地(localStorage)。 + * 走 OpenAI 兼容 /chat/completions 协议:用户自己填 地址 / Key / 模型 ID。 + * 与 print-settings.ts 同套路(window.localStorage + 固定 STORAGE_KEY)。 + */ +export interface AiSettings { + /** OpenAI 兼容的 baseURL,需含 /v1(如 https://api.openai.com/v1) */ + baseURL: string + /** 用户自己的 API Key(明文存本地,仅本地单用户场景) */ + apiKey: string + /** 模型 ID,如 gpt-4o-mini / deepseek-chat / qwen-plus */ + model: string + /** 是否已启用 AI 助手 */ + enabled: boolean +} + +/** 常见 provider 预设(点击即填,用户仍可自行改地址) */ +export interface AiProviderPreset { + label: string + baseURL: string + model: string +} + +export const AI_PROVIDER_PRESETS: AiProviderPreset[] = [ + { label: 'OpenAI', baseURL: 'https://api.openai.com/v1', model: 'gpt-4o-mini' }, + { label: 'DeepSeek', baseURL: 'https://api.deepseek.com/v1', model: 'deepseek-chat' }, + { label: '通义千问', baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus' }, + { label: 'Moonshot', baseURL: 'https://api.moonshot.cn/v1', model: 'moonshot-v1-8k' }, + { label: '自定义', baseURL: '', model: '' }, +] + +const STORAGE_KEY = 'openprint:ai:config' + +const DEFAULT_AI_SETTINGS: AiSettings = { + baseURL: 'https://api.openai.com/v1', + apiKey: '', + model: 'gpt-4o-mini', + enabled: false, +} + +function safeRead(): AiSettings { + if (typeof window === 'undefined') return { ...DEFAULT_AI_SETTINGS } + try { + const raw = window.localStorage.getItem(STORAGE_KEY) + if (!raw) return { ...DEFAULT_AI_SETTINGS } + const parsed = JSON.parse(raw) as Partial + return { + baseURL: typeof parsed.baseURL === 'string' ? parsed.baseURL : DEFAULT_AI_SETTINGS.baseURL, + apiKey: typeof parsed.apiKey === 'string' ? parsed.apiKey : '', + model: typeof parsed.model === 'string' ? parsed.model : DEFAULT_AI_SETTINGS.model, + enabled: !!parsed.enabled, + } + } catch { + return { ...DEFAULT_AI_SETTINGS } + } +} + +function safeWrite(v: AiSettings): void { + if (typeof window === 'undefined') return + try { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(v)) + } catch { + /* noop */ + } +} + +export function readAiSettings(): AiSettings { + return safeRead() +} + +export function writeAiSettings(v: AiSettings): void { + safeWrite(v) +} + +/** 是否已具备发起请求的最低配置 */ +export function isAiConfigured(): boolean { + const s = safeRead() + return s.enabled && !!s.baseURL.trim() && !!s.apiKey.trim() && !!s.model.trim() +} diff --git a/src/config/backend.ts b/src/config/backend.ts new file mode 100644 index 0000000..912f4a3 --- /dev/null +++ b/src/config/backend.ts @@ -0,0 +1,36 @@ +/** + * 后端对接配置开关 —— 《后端对接规范》§4 + * + * 铁律(2026-08-08 主任定):未配置后端接口时,全部走本地存储(模板)/ Mock(数据源), + * 一切功能不受影响。只有显式配置了 `VITE_OPENPRINT_API_BASE` 才切换为云端仓库。 + * + * 配置方式(项目根目录 .env): + * VITE_OPENPRINT_API_BASE=https://print.example.com + * VITE_OPENPRINT_API_TOKEN= # 可选,Bearer 鉴权 + * + * 基地址会被当作前缀拼接:`/api/print/templates` → `https://print.example.com/api/print/templates` + */ +import type { HttpOptions } from '@/repository/http-client' + +const API_BASE = (import.meta.env.VITE_OPENPRINT_API_BASE ?? '').trim() +const API_TOKEN = (import.meta.env.VITE_OPENPRINT_API_TOKEN ?? '').trim() + +export interface BackendConfig { + mode: 'cloud' + options: HttpOptions +} + +/** 是否已配置后端(决定走云端仓库还是本地/Mock) */ +export const isBackendConfigured = API_BASE.length > 0 + +/** 读取后端配置;未配置返回 null(调用方据此保持本地/Mock) */ +export function getBackendConfig(): BackendConfig | null { + if (!API_BASE) return null + return { + mode: 'cloud', + options: { + baseUrl: API_BASE, + token: API_TOKEN.length > 0 ? API_TOKEN : undefined, + }, + } +} diff --git a/src/config/print-settings.spec.ts b/src/config/print-settings.spec.ts new file mode 100644 index 0000000..c8d6273 --- /dev/null +++ b/src/config/print-settings.spec.ts @@ -0,0 +1,48 @@ +import { beforeEach, describe, expect, it } from 'vitest' +import { + DEFAULT_PRINT_SETTINGS, + readPrintSettings, + writePrintSettings, + PRINT_SETTINGS_KEY, +} from '@/config/print-settings' + +describe('print-settings —— 打印设置读写', () => { + beforeEach(() => { + window.localStorage.clear() + }) + + it('无存储时返回默认值', () => { + expect(readPrintSettings()).toEqual(DEFAULT_PRINT_SETTINGS) + }) + + it('写入后可读回', () => { + const s = { + local: { + method: 'silent' as const, + silent: { host: '192.168.1.10', port: 9122 }, + copies: 3, + closeAfterPrint: false, + }, + remote: { enabled: true, host: 'http://10.0.0.8', port: 9200, printer: 'HP-1' }, + } + writePrintSettings(s) + expect(readPrintSettings()).toEqual(s) + }) + + it('损坏 JSON 回退默认值', () => { + window.localStorage.setItem(PRINT_SETTINGS_KEY, '{oops') + expect(readPrintSettings()).toEqual(DEFAULT_PRINT_SETTINGS) + }) + + it('部分配置深度合并(新增字段有默认值)', () => { + window.localStorage.setItem( + PRINT_SETTINGS_KEY, + JSON.stringify({ local: { method: 'silent' } }), + ) + const s = readPrintSettings() + expect(s.local.method).toBe('silent') + expect(s.local.copies).toBe(DEFAULT_PRINT_SETTINGS.local.copies) + expect(s.local.silent).toEqual({ host: '127.0.0.1', port: 9122 }) + expect(s.remote.enabled).toBe(false) + }) +}) diff --git a/src/config/print-settings.ts b/src/config/print-settings.ts new file mode 100644 index 0000000..8ff8114 --- /dev/null +++ b/src/config/print-settings.ts @@ -0,0 +1,83 @@ +/** + * 打印设置 —— 本地打印 / 远程云打印 配置(全局设置,localStorage 持久化) + * 主任铁律:无后端全链路可用 —— 设置只存本地,绝不依赖后端。 + */ + +export interface LocalPrintConfig { + /** 打印方式:browser=浏览器打印对话框 / silent=客户端静默打印(需本地客户端服务) */ + method: 'browser' | 'silent' + /** 静默打印客户端连接地址(仅 method=silent 生效) */ + silent: { + /** 客户端所在主机 IP(默认本机回环) */ + host: string + /** 客户端服务端口(默认 9122) */ + port: number + } + /** 副本数 */ + copies: number + /** 打印完成后自动关闭预览窗口 */ + closeAfterPrint: boolean +} + +export interface RemotePrintConfig { + /** 是否启用远程云打印 */ + enabled: boolean + /** 云打印服务地址(如 http://localhost 或 https://print.example.com) */ + host: string + /** 服务端口 */ + port: number + /** 目标打印机名(可选,留空使用服务默认打印机) */ + printer: string +} + +export interface PrintSettings { + local: LocalPrintConfig + remote: RemotePrintConfig +} + +export const PRINT_SETTINGS_KEY = 'openprint:print-settings' + +export const DEFAULT_PRINT_SETTINGS: PrintSettings = { + local: { + method: 'browser', + silent: { host: '127.0.0.1', port: 9122 }, + copies: 1, + closeAfterPrint: true, + }, + remote: { enabled: false, host: 'http://localhost', port: 9100, printer: '' }, +} + +/** 读取设置(损坏/缺失时回退默认值,深度合并保证新字段有默认) */ +export function readPrintSettings(): PrintSettings { + if (typeof window === 'undefined') return DEFAULT_PRINT_SETTINGS + try { + const raw = window.localStorage.getItem(PRINT_SETTINGS_KEY) + if (!raw) return DEFAULT_PRINT_SETTINGS + const parsed = JSON.parse(raw) as Partial + return { + local: { + ...DEFAULT_PRINT_SETTINGS.local, + ...(parsed.local ?? {}), + silent: { + ...DEFAULT_PRINT_SETTINGS.local.silent, + ...(parsed.local?.silent ?? {}), + }, + }, + remote: { + ...DEFAULT_PRINT_SETTINGS.remote, + ...(parsed.remote ?? {}), + }, + } + } catch { + return DEFAULT_PRINT_SETTINGS + } +} + +export function writePrintSettings(settings: PrintSettings): void { + if (typeof window === 'undefined') return + try { + window.localStorage.setItem(PRINT_SETTINGS_KEY, JSON.stringify(settings)) + } catch { + /* 存储不可用时静默失败(如隐私模式) */ + } +} diff --git a/src/core/__tests__/cjk-measurer.ts b/src/core/__tests__/cjk-measurer.ts new file mode 100644 index 0000000..b387b83 --- /dev/null +++ b/src/core/__tests__/cjk-measurer.ts @@ -0,0 +1,34 @@ +import type { TextMeasureOptions, TextMeasurer } from '@/core/layout-engine/measure' + +/** + * 测试用确定性 CJK 估算测量器。 + * + * 不依赖任何 DOM(happy-dom 不实现真实 CSS 布局),用字符宽度估算, + * 保证分页单测在 CI / node 下结果稳定、可复现。 + * 估算规则:CJK 字符 ≈ 1em 宽,ASCII ≈ 0.6em 宽;行高 = lineHeight × fontSize(pt→mm)。 + */ +export function createCjkMeasurer(): TextMeasurer { + return { + measure(text: string, o: TextMeasureOptions) { + const fsMm = (o.fontSize * 25.4) / 72 // pt → mm + const lineH = (o.lineHeight ?? 1.35) * fsMm + const charW = fsMm // CJK 近似 1em + const asciiW = fsMm * 0.6 + const lines = text.split('\n') + let totalH = 0 + let lineCount = 0 + for (const ln of lines) { + let w = 0 + for (const ch of ln) w += /[\x00-\xff]/.test(ch) ? asciiW : charW + const wrapped = + o.widthMm > 0 && o.wrap !== false ? Math.max(1, Math.ceil(w / o.widthMm)) : 1 + totalH += wrapped * lineH + lineCount += wrapped + } + return { heightMm: totalH, lines: lineCount } + }, + dispose() { + /* 无资源可释放 */ + }, + } +} diff --git a/src/core/chartkit/bar.ts b/src/core/chartkit/bar.ts new file mode 100644 index 0000000..894b04c --- /dev/null +++ b/src/core/chartkit/bar.ts @@ -0,0 +1,127 @@ +/** + * chartkit/bar —— 垂直分组条形图(零依赖 SVG) + * + * 输出一串 (按序列分组并排)+ 坐标轴 + 可选网格 + 图例 + 数据标签。 + * 全部用逻辑坐标(viewBox),实际尺寸由外层容器缩放,矢量清晰。 + */ +import { escapeXml, fmt, niceMax, seriesColor, truncate, legendStartX, xAxisLabelPos } from './core' +import type { ChartModel } from './types' + +export function renderBar(model: ChartModel): string { + const opt = model.options ?? {} + const W = opt.width ?? 480 + const H = opt.height ?? 320 + const title = opt.title + const showAxis = opt.showAxis ?? true + const showGrid = opt.showGrid ?? true + const showLegend = opt.showLegend ?? model.series.length > 1 + const valueLabel = opt.valueLabel ?? false + + const legendH = showLegend ? 22 : 0 + const titleH = title ? 22 : 0 + const mTop = 10 + titleH + const mRight = 16 + const mBottom = (showAxis ? 34 : 18) + legendH + const mLeft = showAxis ? 44 : 16 + + const px0 = mLeft + const py0 = mTop + const px1 = W - mRight + const py1 = H - mBottom + const plotW = Math.max(1, px1 - px0) + const plotH = Math.max(1, py1 - py0) + + const cats = model.categories.length + ? model.categories + : (model.series[0]?.data ?? []).map((_, i) => String(i + 1)) + const n = cats.length + + const allVals: number[] = [] + for (const s of model.series) for (const v of s.data) if (isFinite(v)) allVals.push(v) + if (model.series.length === 0 || n === 0 || allVals.length === 0) { + return placeholder(W, H, '暂无数据') + } + + const yMax = niceMax(Math.max(0, ...allVals)) + const band = plotW / Math.max(1, n) + const inner = band * 0.74 + const sCount = model.series.length + const barW = inner / Math.max(1, sCount) + + const parts: string[] = [] + parts.push(``) + + if (title) { + parts.push(`${escapeXml(title)}`) + } + + // 网格 + y 轴刻度 + if (showAxis) { + const ticks = 4 + for (let i = 0; i <= ticks; i++) { + const v = (yMax / ticks) * i + const y = py1 - (v / yMax) * plotH + if (showGrid && i > 0) { + parts.push(``) + } + parts.push(`${fmt(v)}`) + } + // 基线 + parts.push(``) + } + + // 柱体 + for (let c = 0; c < n; c++) { + const cx = px0 + band * (c + 0.5) + const groupX0 = cx - inner / 2 + for (let s = 0; s < sCount; s++) { + const val = model.series[s]!.data[c] ?? 0 + if (!isFinite(val)) continue + const h = (Math.max(0, val) / yMax) * plotH + const x = groupX0 + s * barW + const y = py1 - h + const color = seriesColor(model.series[s]!, s, opt) + parts.push(``) + if (valueLabel && h > 0) { + parts.push(`${fmt(val)}`) + } + } + // x 轴标签 + if (showAxis) { + const pos = xAxisLabelPos(opt.labelAlign, px0 + band * c, px0 + band * (c + 1), cx) + parts.push(`${escapeXml(truncate(String(cats[c] ?? '')))}`) + } + } + + // 图例 + if (showLegend) { + const align = opt.labelAlign ?? 'left' + let totalW = 0 + for (let s = 0; s < sCount; s++) { + const name = model.series[s]!.name || `系列${s + 1}` + totalW += 16 + name.length * 12 + 18 + } + const startX = legendStartX(align, W, totalW, px0) + let lx = startX + let ly = py1 + (showAxis ? 24 : 10) + for (let s = 0; s < sCount; s++) { + const name = model.series[s]!.name || `系列${s + 1}` + const color = seriesColor(model.series[s]!, s, opt) + const itemW = 16 + name.length * 12 + 18 + if (lx + itemW > W - 8 && lx > startX) { + lx = startX + ly += 16 + } + parts.push(``) + parts.push(`${escapeXml(truncate(name, 10))}`) + lx += itemW + } + } + + parts.push('') + return parts.join('') +} + +function placeholder(W: number, H: number, text: string): string { + return `${escapeXml(text)}` +} diff --git a/src/core/chartkit/chartkit.spec.ts b/src/core/chartkit/chartkit.spec.ts new file mode 100644 index 0000000..10f8149 --- /dev/null +++ b/src/core/chartkit/chartkit.spec.ts @@ -0,0 +1,136 @@ +import { describe, it, expect } from 'vitest' +import { + renderChartSvg, + chartControlToModel, + renderChartControl, + DEFAULT_PALETTE, +} from './index' +import type { ChartModel } from './types' + +const barModel: ChartModel = { + kind: 'bar', + categories: ['一月', '二月', '三月'], + series: [ + { name: '销量', data: [12, 19, 8] }, + { name: '退货', data: [3, 5, 2] }, + ], +} + +const lineModel: ChartModel = { + kind: 'line', + categories: ['周一', '周二', '周三'], + series: [{ name: '温度', data: [20, 25, 22] }], +} + +const pieModel: ChartModel = { + kind: 'pie', + categories: ['苹果', '香蕉', '橙子'], + series: [{ name: '销量', data: [10, 20, 30] }], +} + +function count(haystack: string, needle: string): number { + return haystack.split(needle).length - 1 +} + +describe('chartkit · 产出结构', () => { + it('三种图表都返回以 结尾的可缩放 SVG', () => { + for (const m of [barModel, lineModel, pieModel]) { + const svg = renderChartSvg(m) + expect(svg.startsWith('')).toBe(true) + expect(svg).toContain('viewBox="0 0') + expect(svg).toContain('width="100%"') + } + }) + + it('bar 为每个(序列×类目)生成一个 ', () => { + const svg = renderChartSvg(barModel) + // 2 序列 × 3 类目 = 6 根柱(柱体 rx=1.5,区别于图例色块 rx=2) + expect(count(svg, 'rx="1.5"')).toBe(6) + }) + + it('line 含一条折线路径 + 每个点一个圆点', () => { + const svg = renderChartSvg(lineModel) + expect(count(svg, '', () => { + const svg = renderChartSvg(pieModel) + expect(count(svg, ' { + const normal = renderChartSvg(pieModel) + const donut = renderChartSvg({ ...pieModel, options: { donut: true } }) + // 3 个扇区:普通每个 1 段弧;环形每个 2 段弧(外圈+内圈) + expect(count(normal, ' A ')).toBe(3) + expect(count(donut, ' A ')).toBe(6) + }) + + it('饼图实心扇区必须到达圆心(修复"中间空白")', () => { + const normal = renderChartSvg(pieModel) + const donut = renderChartSvg({ ...pieModel, options: { donut: true } }) + // 默认 480×320 无标题无环形:圆心 (240,148),楔形路径应以 "M 240.00 148.00" 起笔 + expect(normal).toContain('M 240.00 148.00') + // 环形是环段,不应从圆心起笔 + expect(donut).not.toContain('M 240.00 148.00') + }) +}) + +describe('chartkit · 空/异常数据兜底', () => { + it('空数据渲染占位提示,不抛错', () => { + const svg = renderChartSvg({ kind: 'bar', categories: [], series: [] }) + expect(svg).toContain('暂无数据') + }) + + it('饼图总量为 0 时不画扇区', () => { + const svg = renderChartSvg({ kind: 'pie', categories: ['a', 'b'], series: [{ name: 's', data: [0, 0] }] }) + expect(svg).toContain('暂无数据') + }) +}) + +describe('chartkit · 文本转义', () => { + it('标题中的 & < > 被转义,不会破坏 SVG 结构', () => { + const svg = renderChartSvg({ ...barModel, options: { title: 'A & B "D"' } }) + expect(svg).not.toContain('A & B') + expect(svg).toContain('A & B <C>') + }) +}) + +describe('chartkit · ChartControl 映射', () => { + it('chartControlToModel 补齐缺省并过滤非有限值', () => { + const m = chartControlToModel({ + type: 'chart', + id: 'c1', + kind: 'line', + left: 0, + top: 0, + width: 60, + height: 40, + categories: ['x', 'y'], + series: [{ name: 's', data: [1, NaN, 3] }], + }) + expect(m.kind).toBe('line') + expect(m.series[0]!.data).toEqual([1, 3]) + expect(m.categories).toEqual(['x', 'y']) + }) + + it('renderChartControl 从协议对象直接出 SVG', () => { + const svg = renderChartControl({ + type: 'chart', + id: 'c2', + kind: 'pie', + left: 0, + top: 0, + width: 50, + height: 50, + categories: ['a', 'b'], + series: [{ name: 's', data: [1, 2] }], + }) + expect(svg).toContain(' 破坏 SVG 结构) */ +export function escapeXml(s: string): string { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +/** 数值格式化:整数去小数,小数保留最多 2 位 */ +export function fmt(n: number): string { + if (!isFinite(n)) return '' + const r = Math.round(n * 100) / 100 + return Number.isInteger(r) ? String(r) : String(r) +} + +/** + * 计算「好看」的坐标轴上限:取数据最大值,向上取整到 1/2/5×10ⁿ 量级。 + * 例如 max=73 → 80;max=250 → 300;max=0 → 1。 + */ +export function niceMax(max: number): number { + if (max <= 0) return 1 + const exp = Math.floor(Math.log10(max)) + const base = Math.pow(10, exp) + const frac = max / base + let nice: number + if (frac <= 1) nice = 1 + else if (frac <= 2) nice = 2 + else if (frac <= 5) nice = 5 + else nice = 10 + return nice * base +} + +/** 取序列颜色:显式 color 优先,否则按调色板顺序取 */ +export function seriesColor(series: ChartSeries, index: number, options?: ChartOptions): string { + if (series.color) return series.color + const palette = options?.palette ?? DEFAULT_PALETTE + return palette[index % palette.length]! +} + +/** + * 极坐标 → 笛卡尔坐标。angleDeg 以「12 点方向为 0°、顺时针递增」表示, + * 与饼图扇区习惯一致。 + */ +export function polar(cx: number, cy: number, r: number, angleDeg: number): { x: number; y: number } { + const theta = ((angleDeg - 90) * Math.PI) / 180 + return { x: cx + r * Math.cos(theta), y: cy + r * Math.sin(theta) } +} + +/** + * 饼图扇区路径(支持环形 donut)。 + * @param startAngle 起始角(度,顺时针从 12 点起) + * @param endAngle 结束角(度) + * @param rInner 内半径(donut>0 时 >0) + */ +export function arcPath( + cx: number, + cy: number, + rOuter: number, + startAngle: number, + endAngle: number, + rInner = 0, +): string { + const largeArc = endAngle - startAngle > 180 ? 1 : 0 + // 整圆特例:单扇区 360° 用两段半圆弧画,否则 A 命令会因起止点重合而失效 + if (endAngle - startAngle >= 359.999) { + const top = polar(cx, cy, rOuter, 0) + const bottom = polar(cx, cy, rOuter, 180) + let d = `M ${cx.toFixed(2)} ${cy.toFixed(2)} L ${top.x.toFixed(2)} ${top.y.toFixed(2)} A ${rOuter} ${rOuter} 0 1 1 ${bottom.x.toFixed(2)} ${bottom.y.toFixed(2)} A ${rOuter} ${rOuter} 0 1 1 ${top.x.toFixed(2)} ${top.y.toFixed(2)} Z` + if (rInner > 0) { + const itop = polar(cx, cy, rInner, 0) + const ibottom = polar(cx, cy, rInner, 180) + d += ` M ${cx.toFixed(2)} ${cy.toFixed(2)} L ${itop.x.toFixed(2)} ${itop.y.toFixed(2)} A ${rInner} ${rInner} 0 1 0 ${ibottom.x.toFixed(2)} ${ibottom.y.toFixed(2)} A ${rInner} ${rInner} 0 1 0 ${itop.x.toFixed(2)} ${itop.y.toFixed(2)} Z` + } + return d + } + const start = polar(cx, cy, rOuter, startAngle) + const end = polar(cx, cy, rOuter, endAngle) + // 环形:外弧(顺时针) + 内弧(逆时针) 组成闭合环段(不连圆心) + if (rInner > 0) { + const iStart = polar(cx, cy, rInner, startAngle) + const iEnd = polar(cx, cy, rInner, endAngle) + return ( + `M ${start.x.toFixed(2)} ${start.y.toFixed(2)} A ${rOuter} ${rOuter} 0 ${largeArc} 1 ${end.x.toFixed(2)} ${end.y.toFixed(2)}` + + ` L ${iEnd.x.toFixed(2)} ${iEnd.y.toFixed(2)} A ${rInner} ${rInner} 0 ${largeArc} 0 ${iStart.x.toFixed(2)} ${iStart.y.toFixed(2)} Z` + ) + } + // 实心饼:圆心 → 外起点 → 弧 → 圆心,形成真正到达圆心的楔形(修复"中间空白") + return `M ${cx.toFixed(2)} ${cy.toFixed(2)} L ${start.x.toFixed(2)} ${start.y.toFixed(2)} A ${rOuter} ${rOuter} 0 ${largeArc} 1 ${end.x.toFixed(2)} ${end.y.toFixed(2)} Z` +} + +/** 截断过长标签(设计期/运行期共用) */ +export function truncate(s: string, max = 12): string { + return s.length > max ? s.slice(0, max - 1) + '…' : s +} + +/** + * 图例整体起始 x:left 直接用 leftPad;center/right 按单行总宽偏移。 + * 单行假设(多数图例一行放得下);若实际换行,仍从 startX 起排,整体观感一致。 + */ +export function legendStartX( + align: 'left' | 'center' | 'right' | undefined, + W: number, + totalW: number, + leftPad: number, +): number { + if (align === 'center') return Math.max(leftPad, (W - totalW) / 2) + if (align === 'right') return Math.max(leftPad, W - totalW - 8) + return leftPad +} + +/** x 轴类目标签的水平对齐 → SVG text-anchor 与基准 x */ +export function xAxisLabelPos( + align: 'left' | 'center' | 'right' | undefined, + bandLeft: number, + bandRight: number, + bandCenter: number, +): { x: number; anchor: 'start' | 'middle' | 'end' } { + if (align === 'left') return { x: bandLeft + 4, anchor: 'start' } + if (align === 'right') return { x: bandRight - 4, anchor: 'end' } + return { x: bandCenter, anchor: 'middle' } +} diff --git a/src/core/chartkit/index.ts b/src/core/chartkit/index.ts new file mode 100644 index 0000000..b67a24d --- /dev/null +++ b/src/core/chartkit/index.ts @@ -0,0 +1,54 @@ +/** + * chartkit/index —— 对外统一入口 + * + * - `renderChartSvg(model)`:纯函数,ChartModel → 可缩放 SVG 字符串。 + * - `chartControlToModel(control)`:把设计器里的 ChartControl 协议对象转成 ChartModel + * (补默认值、剔除非法字段),供设计期 overlay 与运行期导出共用。 + * - `renderChartControl(control)`:一步到位,从 ChartControl 直接出 SVG 字符串。 + * + * 整个模块零依赖、不碰 DOM,可在 Node / 测试环境运行。 + */ +import type { ChartControl } from '../../types/control' +import type { ChartKind, ChartModel, ChartSeries } from './types' +import { renderBar } from './bar' +import { renderLine } from './line' +import { renderPie } from './pie' + +export type { ChartKind, ChartModel, ChartSeries, ChartOptions } from './types' +export { DEFAULT_PALETTE } from './types' +export { renderBar } from './bar' +export { renderLine } from './line' +export { renderPie } from './pie' + +export function renderChartSvg(model: ChartModel): string { + switch (model.kind) { + case 'bar': + return renderBar(model) + case 'line': + return renderLine(model) + case 'pie': + return renderPie(model) + default: + return '' + } +} + +/** ChartControl(协议)→ ChartModel(渲染器输入),补齐缺省值 */ +export function chartControlToModel(c: ChartControl): ChartModel { + const series: ChartSeries[] = (c.series ?? []).map((s) => ({ + name: s.name ?? '', + data: Array.isArray(s.data) ? s.data.filter((v) => typeof v === 'number' && isFinite(v)) : [], + color: s.color, + })) + return { + kind: c.kind, + categories: Array.isArray(c.categories) ? c.categories.map(String) : [], + series, + options: c.options, + } +} + +/** 一步到位:ChartControl → 可缩放 SVG 字符串 */ +export function renderChartControl(control: ChartControl): string { + return renderChartSvg(chartControlToModel(control)) +} diff --git a/src/core/chartkit/line.ts b/src/core/chartkit/line.ts new file mode 100644 index 0000000..2c6af4a --- /dev/null +++ b/src/core/chartkit/line.ts @@ -0,0 +1,164 @@ +/** + * chartkit/line —— 折线图(零依赖 SVG) + * + * 多序列折线 + 坐标轴 + 可选网格 + 图例 + 数据标签。 + * 支持 smooth(Catmull-Rom 平滑曲线)与 area(折线下方面积填充)。 + */ +import { escapeXml, fmt, niceMax, seriesColor, truncate, legendStartX, xAxisLabelPos } from './core' +import type { ChartModel } from './types' + +interface Pt { x: number; y: number } + +function smoothPath(pts: Pt[]): string { + if (pts.length < 2) return pts.length ? `M ${pts[0]!.x.toFixed(1)} ${pts[0]!.y.toFixed(1)}` : '' + let d = `M ${pts[0]!.x.toFixed(1)} ${pts[0]!.y.toFixed(1)}` + const t = 1 / 6 + for (let i = 0; i < pts.length - 1; i++) { + const p0 = pts[i - 1] ?? pts[i]! + const p1 = pts[i]! + const p2 = pts[i + 1]! + const p3 = pts[i + 2] ?? p2 + const c1x = p1.x + (p2.x - p0.x) * t + const c1y = p1.y + (p2.y - p0.y) * t + const c2x = p2.x - (p3.x - p1.x) * t + const c2y = p2.y - (p3.y - p1.y) * t + d += ` C ${c1x.toFixed(1)} ${c1y.toFixed(1)}, ${c2x.toFixed(1)} ${c2y.toFixed(1)}, ${p2.x.toFixed(1)} ${p2.y.toFixed(1)}` + } + return d +} + +export function renderLine(model: ChartModel): string { + const opt = model.options ?? {} + const W = opt.width ?? 480 + const H = opt.height ?? 320 + const title = opt.title + const showAxis = opt.showAxis ?? true + const showGrid = opt.showGrid ?? true + const showLegend = opt.showLegend ?? model.series.length > 1 + const valueLabel = opt.valueLabel ?? false + const smooth = opt.smooth ?? false + const area = opt.area ?? false + + const legendH = showLegend ? 22 : 0 + const titleH = title ? 22 : 0 + const mTop = 10 + titleH + const mRight = 16 + const mBottom = (showAxis ? 34 : 18) + legendH + const mLeft = showAxis ? 44 : 16 + + const px0 = mLeft + const py0 = mTop + const px1 = W - mRight + const py1 = H - mBottom + const plotW = Math.max(1, px1 - px0) + const plotH = Math.max(1, py1 - py0) + + const cats = model.categories.length + ? model.categories + : (model.series[0]?.data ?? []).map((_, i) => String(i + 1)) + const n = cats.length + + const allVals: number[] = [] + for (const s of model.series) for (const v of s.data) if (isFinite(v)) allVals.push(v) + if (model.series.length === 0 || n === 0 || allVals.length === 0) { + return placeholder(W, H, '暂无数据') + } + + const yMax = niceMax(Math.max(0, ...allVals)) + const band = plotW / Math.max(1, n) + + // 预计算每序列的每个点 + const seriesPts = model.series.map((s, si) => { + const color = seriesColor(s, si, opt) + const pts: Pt[] = [] + for (let c = 0; c < n; c++) { + const val = s.data[c] ?? 0 + const x = n > 1 ? px0 + band * (c + 0.5) : (px0 + px1) / 2 + const y = py1 - (Math.max(0, val) / yMax) * plotH + pts.push({ x, y }) + } + return { color, pts, name: s.name } + }) + + const parts: string[] = [] + parts.push(``) + + if (title) { + parts.push(`${escapeXml(title)}`) + } + + if (showAxis) { + const ticks = 4 + for (let i = 0; i <= ticks; i++) { + const v = (yMax / ticks) * i + const y = py1 - (v / yMax) * plotH + if (showGrid && i > 0) { + parts.push(``) + } + parts.push(`${fmt(v)}`) + } + parts.push(``) + parts.push(``) + for (let c = 0; c < n; c++) { + const x = px0 + band * (c + 0.5) + const pos = xAxisLabelPos(opt.labelAlign, px0 + band * c, px0 + band * (c + 1), x) + parts.push(`${escapeXml(truncate(String(cats[c] ?? '')))}`) + } + } + + for (const sp of seriesPts) { + if (area && sp.pts.length > 1) { + let d = smooth ? smoothPath(sp.pts) : `M ${sp.pts.map((p) => `${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' L ')}` + const last = sp.pts[sp.pts.length - 1]! + const first = sp.pts[0]! + d += ` L ${last.x.toFixed(1)} ${py1} L ${first.x.toFixed(1)} ${py1} Z` + parts.push(``) + } + const lineD = smooth ? smoothPath(sp.pts) : (sp.pts.length ? `M ${sp.pts.map((p) => `${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' L ')}` : '') + if (lineD) parts.push(``) + for (const p of sp.pts) { + parts.push(``) + } + } + if (valueLabel) { + for (let si = 0; si < seriesPts.length; si++) { + const sp = seriesPts[si]! + const series = model.series[si]! + for (let c = 0; c < sp.pts.length; c++) { + const v = series.data[c] ?? 0 + parts.push(`${fmt(v)}`) + } + } + } + + if (showLegend) { + const align = opt.labelAlign ?? 'left' + let totalW = 0 + for (let s = 0; s < seriesPts.length; s++) { + const name = seriesPts[s]!.name || `系列${s + 1}` + totalW += 16 + name.length * 12 + 18 + } + const startX = legendStartX(align, W, totalW, px0) + let lx = startX + let ly = py1 + (showAxis ? 24 : 10) + for (let s = 0; s < seriesPts.length; s++) { + const name = seriesPts[s]!.name || `系列${s + 1}` + const color = seriesPts[s]!.color + const itemW = 16 + name.length * 12 + 18 + if (lx + itemW > W - 8 && lx > startX) { + lx = startX + ly += 16 + } + parts.push(``) + parts.push(`${escapeXml(truncate(name, 10))}`) + lx += itemW + } + } + + parts.push('') + return parts.join('') +} + +function placeholder(W: number, H: number, text: string): string { + return `${escapeXml(text)}` +} diff --git a/src/core/chartkit/pie.ts b/src/core/chartkit/pie.ts new file mode 100644 index 0000000..fbcdc24 --- /dev/null +++ b/src/core/chartkit/pie.ts @@ -0,0 +1,100 @@ +/** + * chartkit/pie —— 饼图 / 环形图(零依赖 SVG) + * + * 用 arcPath 画每个扇区(或环形),标签显示类目+百分比,底部可选图例。 + * 饼图通常只用 series[0](每个数据点 = 一个扇区)。 + */ +import { arcPath, escapeXml, seriesColor, truncate, legendStartX } from './core' +import { DEFAULT_PALETTE } from './types' +import type { ChartModel } from './types' + +export function renderPie(model: ChartModel): string { + const opt = model.options ?? {} + const W = opt.width ?? 480 + const H = opt.height ?? 320 + const title = opt.title + const showLegend = opt.showLegend ?? true + const donut = opt.donut ?? false + const valueLabel = opt.valueLabel ?? true + + const legendH = showLegend ? 24 : 0 + const titleH = title ? 22 : 0 + + const cats = model.categories.length + ? model.categories + : (model.series[0]?.data ?? []).map((_, i) => `类目${i + 1}`) + const data = (model.series[0]?.data ?? []).filter((v) => isFinite(v)) + const total = data.reduce((a, b) => a + Math.max(0, b), 0) + if (data.length === 0 || total <= 0) { + return placeholder(W, H, '暂无数据') + } + + const cx = W / 2 + const cy = titleH + (H - titleH - legendH) / 2 + const rOuter = (Math.min(W * 0.82, H - titleH - legendH) / 2) * 0.92 + const rInner = donut ? rOuter * 0.56 : 0 + + const palette = opt.palette ?? DEFAULT_PALETTE + const parts: string[] = [] + parts.push(``) + + if (title) { + parts.push(`${escapeXml(title)}`) + } + + let angle = 0 + for (let i = 0; i < data.length; i++) { + const v = Math.max(0, data[i]!) + if (v <= 0) continue + const sweep = (v / total) * 360 + const start = angle + const end = angle + sweep + angle = end + const color = seriesColor({ name: cats[i] ?? '', data: [] }, i, opt) + parts.push(``) + // 百分比标签 + if (valueLabel && sweep > 12) { + const mid = (start + end) / 2 + const lr = donut ? (rOuter + rInner) / 2 : rOuter * 0.62 + const p = polarL(lr, mid) + const pct = ((v / total) * 100).toFixed(0) + '%' + parts.push(`${pct}`) + } + } + + if (showLegend) { + const align = opt.labelAlign ?? 'left' + let totalW = 0 + for (let i = 0; i < data.length; i++) { + const name = cats[i] ?? `类目${i + 1}` + totalW += 16 + name.length * 12 + 18 + } + const startX = legendStartX(align, W, totalW, 12) + let lx = startX + let ly = H - legendH + 12 + for (let i = 0; i < data.length; i++) { + const name = cats[i] ?? `类目${i + 1}` + const color = seriesColor({ name, data: [] }, i, opt) + const itemW = 16 + name.length * 12 + 18 + if (lx + itemW > W - 8 && lx > startX) { + lx = startX + ly += 16 + } + parts.push(``) + parts.push(`${escapeXml(truncate(name, 10))}`) + lx += itemW + } + } + + parts.push('') + return parts.join('') + + function polarL(r: number, angleDeg: number) { + const theta = ((angleDeg - 90) * Math.PI) / 180 + return { x: cx + r * Math.cos(theta), y: cy + r * Math.sin(theta) } + } +} + +function placeholder(W: number, H: number, text: string): string { + return `${escapeXml(text)}` +} diff --git a/src/core/chartkit/types.ts b/src/core/chartkit/types.ts new file mode 100644 index 0000000..00430d9 --- /dev/null +++ b/src/core/chartkit/types.ts @@ -0,0 +1,72 @@ +/** + * chartkit —— OpenPrint 原生 SVG 图表组件(零依赖) + * + * 设计目标(见《OpenPrint-设计方案》图表控件提案): + * - 打印设计器要的是**矢量**,不是 Canvas 位图。图表用纯 SVG 路径绘制, + * 导出 PDF/SVG 时整条链路都是矢量,任意缩放都清晰,且零运行时依赖。 + * - 本模块是**纯函数 + 零依赖**的:输入 ChartModel,输出一段可缩放的 `` 字符串。 + * 设计期 overlay 与运行期导出共用同一份产物,保证"设计即打印"。 + * + * 支持的图表类型(v1):条形图 bar / 折线图 line / 饼图 pie。 + * 数据模型与渲染解耦,后续扩展(横向条形、面积、环形、堆叠等)只需加一个生成器。 + */ + +export type ChartKind = 'bar' | 'line' | 'pie' + +/** 一条数据序列(折线/条形的一组柱;饼图通常只用 series[0]) */ +export interface ChartSeries { + name: string + /** 与 ChartModel.categories 一一对应 */ + data: number[] + /** 可选覆盖色;缺省走调色板 */ + color?: string +} + +/** 图表下方标签(x 轴类目 / 图例)的水平对齐方式 */ +export type ChartLabelAlign = 'left' | 'center' | 'right' + +export interface ChartOptions { + /** SVG 逻辑尺寸(px,仅决定 viewBox 比例;实际渲染按容器缩放)。默认 480×320 */ + width?: number + height?: number + /** 图表主标题(绘制在顶部居中) */ + title?: string + /** 显示图例(多序列时建议开) */ + showLegend?: boolean + /** 显示坐标轴(条形/折线生效;饼图忽略) */ + showAxis?: boolean + /** 显示网格线(条形/折线生效) */ + showGrid?: boolean + /** 调色板(序列缺色时按顺序取) */ + palette?: string[] + /** 折线是否平滑曲线 */ + smooth?: boolean + /** 折线下方填充面积 */ + area?: boolean + /** 饼图环形(中心镂空) */ + donut?: boolean + /** 数据标签(柱顶数值 / 饼图百分比) */ + valueLabel?: boolean + /** 下方标签(x 轴类目 / 图例)水平对齐,默认 left */ + labelAlign?: ChartLabelAlign +} + +export interface ChartModel { + kind: ChartKind + /** 类目轴标签(条形/折线为 x 轴;饼图为扇区名) */ + categories: string[] + series: ChartSeries[] + options?: ChartOptions +} + +/** 默认调色板(参考 AntV / ECharts 经典商务色,打印对比度足够) */ +export const DEFAULT_PALETTE = [ + '#5B8FF9', + '#5AD8A6', + '#5D7092', + '#F6BD16', + '#E8684A', + '#6DC8EC', + '#9270CA', + '#FF9D4D', +] diff --git a/src/core/export-engine/export-image.ts b/src/core/export-engine/export-image.ts new file mode 100644 index 0000000..7c3f385 --- /dev/null +++ b/src/core/export-engine/export-image.ts @@ -0,0 +1,13 @@ +/** + * 位图导出(JPG)—— 基于 rasterize 的单页位图 + */ +import type { LayoutResult } from '@/core/layout-engine/types' +import { pageToImageBlob, type PageImageOptions } from './rasterize' + +export function pageToJpg( + result: LayoutResult, + index: number, + opts: PageImageOptions = {}, +): Promise { + return pageToImageBlob(result, index, { type: 'image/jpeg', ...opts }) +} diff --git a/src/core/export-engine/export-pdf.spec.ts b/src/core/export-engine/export-pdf.spec.ts new file mode 100644 index 0000000..e8594e8 --- /dev/null +++ b/src/core/export-engine/export-pdf.spec.ts @@ -0,0 +1,53 @@ +import { describe, expect, it, vi } from 'vitest' + +// 真实环境无法在 happy-dom 中栅格化 SVG foreignObject,也不应让 jsPDF 读本地文件。 +// 这里 mock 栅格化下层 + Blob→dataURL,只验证 documentToPdf 的 jsPDF 装配。 +// (写死思源宋体的内联逻辑在 export-pdf.ts 内由 PDF_FONTS 常量体现,随代码审查保证。) +vi.mock('@/core/export-engine/rasterize', () => ({ + pageToImageBlob: vi.fn(async () => { + const b64 = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' + const bin = atob(b64) + const bytes = new Uint8Array(bin.length) + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i) + return new Blob([bytes], { type: 'image/png' }) + }), +})) + +vi.mock('@/core/export-engine/util', () => ({ + blobToDataURL: vi.fn(async () => { + // 直接返回合法 PNG data URI,避开 happy-dom 的 FileReader/fs-read 限制 + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' + }), +})) + +import { documentToPdf } from '@/core/export-engine/export-pdf' +import { layout } from '@/core/layout-engine/pagination-engine' +import { createDemoTemplate } from '@/repository/mock/data/demo-template' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +function makeData(rows: number): Record { + const items = Array.from({ length: rows }, (_, i) => { + const qty = (i % 5) + 1 + return { + productCode: `P${String(i + 1).padStart(4, '0')}`, + productName: `商品${i + 1}`, + spec: '规格A', + unit: '件', + qty, + price: 10, + amount: qty * 10, + } + }) + return { order: { orderNo: 'SO-001' }, customer: { name: '客户' }, items } +} + +describe('documentToPdf —— 位图 PDF 装配(jsPDF)', () => { + it('多页模板产出单文件多页 PDF blob', async () => { + const result = await layout(createDemoTemplate(), makeData(30), { measurer: createCjkMeasurer() }) + const blob = await documentToPdf(result, { scale: 2 }) + expect(blob).toBeInstanceOf(Blob) + expect(blob.type).toBe('application/pdf') + expect(blob.size).toBeGreaterThan(0) + }) +}) diff --git a/src/core/export-engine/export-pdf.ts b/src/core/export-engine/export-pdf.ts new file mode 100644 index 0000000..eb63628 --- /dev/null +++ b/src/core/export-engine/export-pdf.ts @@ -0,0 +1,210 @@ +/** + * PDF 导出 —— jsPDF 动态 import(不进主包) + * + * ## 图表走矢量(svg2pdf) + * 设计器一切以 SVG 为真相源。为让图表在 PDF 里达到「印刷级矢量」: + * 1. 每页先**剥离 chart 控件** → 栅格化(文本/表格位图底图)→ addImage 作背景; + * 2. 再遍历该页 chart 节点,用 `svg2pdf` 把其 SVG 以**矢量**注入 jsPDF 对应 mm 盒; + * 3. 思源宋体 TTF 注册进 jsPDF('SourceHanSerifCN'),图表中文标签才不会变空白/方块。 + * + * 兜底: + * - 图表被旋转 → 整页退回旧的全栅格化(旋转在矢量叠加里不易对齐); + * - 单个图表 svg2pdf 失败 → 该图表单独栅格化后叠加(不丢图); + * - 字体加载失败(极端环境)→ 整页退回全栅格化(仍是位图,但保证有图)。 + * + * 非图表页(无 chart 控件)仍走原全栅格化位图路径,行为与旧版一致。 + */ +import type { LayoutResult, LayoutPage, PlacedNode, PlacedControl } from '@/core/layout-engine/types' +import { pageToImageBlob, buildPageSvg, svgToCanvas } from './rasterize' +import { embedFontsInSvg, type FontFaceDef } from './fonts' +import type { PageDecoration } from '@/types/template' +import { blobToDataURL } from './util' +import { mmv } from '@/core/renderer-html/css-generator' + +/** 思源宋体 —— PDF 唯一内联字体(写死 TTF,位图 PDF 保证中文衬线效果;矢量图表也注册它) */ +const PDF_FONTS: FontFaceDef[] = [ + { family: '思源宋体', src: '/fonts/SourceHanSerifCN-Regular.ttf', weight: 400 }, +] +/** 矢量图表文本使用的 jsPDF 字体名(必须与 chartkit SVG 的 font-family 首选项一致) */ +const SERIF_FONT_NAME = 'SourceHanSerifCN' +const SERIF_TTF_URL = '/fonts/SourceHanSerifCN-Regular.ttf' + +export interface PdfOptions { + /** 高清倍率,1=96dpi / 2=192dpi(推荐)/ 3=288dpi,默认 2 */ + scale?: number + /** 页面装饰(背景色 + 水印),使 PDF 与预览一致 */ + pageDecoration?: PageDecoration +} + +function isChartNode(n: PlacedNode): n is PlacedControl { + return n.kind === 'control' && (n as PlacedControl).control.type === 'chart' +} + +/** 返回去掉所有 chart 控件后的页面副本(不影响原对象) */ +function stripCharts(page: LayoutPage): LayoutPage { + const f = (nodes: T[]): T[] => nodes.filter((n) => !isChartNode(n)) as T[] + return { ...page, header: f(page.header), body: f(page.body), footer: f(page.footer) } +} + +interface ChartBox { + left: number + top: number + width: number + height: number + angle: number + svg: string +} + +/** 收集页面内所有 chart 控件(含其 mm 盒与 SVG) */ +function chartsOf(page: LayoutPage): ChartBox[] { + const out: ChartBox[] = [] + for (const sec of [page.header, page.body, page.footer]) { + for (const n of sec) { + if (!isChartNode(n)) continue + const c = n as PlacedControl + if (c.content.kind !== 'svg') continue + out.push({ + left: n.left, + top: n.top, + width: n.width, + height: n.height, + angle: n.angle ?? 0, + svg: c.content.svg, + }) + } + } + return out +} + +/** ArrayBuffer → base64(分块,避免 call stack 溢出) */ +function arrayBufferToBase64(buf: ArrayBuffer): string { + const bytes = new Uint8Array(buf) + let bin = '' + const chunk = 0x8000 + for (let i = 0; i < bytes.length; i += chunk) { + bin += String.fromCharCode(...bytes.subarray(i, i + chunk)) + } + return btoa(bin) +} + +/** 把思源宋体 TTF 注册进 jsPDF,供矢量图表中文标签使用。失败不抛(退化为位图兜底) */ +async function registerSerifFont(doc: { addFileToVFS: (n: string, d: string) => void; addFont: (f: string, n: string, s: string) => void }): Promise { + try { + const res = await fetch(SERIF_TTF_URL) + if (!res.ok) return false + const b64 = arrayBufferToBase64(await res.arrayBuffer()) + doc.addFileToVFS(`${SERIF_FONT_NAME}.ttf`, b64) + doc.addFont(`${SERIF_FONT_NAME}.ttf`, SERIF_FONT_NAME, 'normal') + return true + } catch { + return false + } +} + +/** 单图表栅格化(svg2pdf 失败时的兜底,保证不丢图) */ +async function rasterizeChart(chartSvg: string, wMm: number, hMm: number, scale: number): Promise { + const inner = `
${chartSvg}
` + let svg = buildPageSvg(inner, '', wMm, hMm) + svg = await embedFontsInSvg(svg, PDF_FONTS) + const canvas = await svgToCanvas(svg, scale, '#ffffff') + const blob = await new Promise((resolve) => canvas.toBlob((b) => resolve(b), 'image/png')) + if (!blob) throw new Error('图表栅格化失败') + return blobToDataURL(blob) +} + +/** SVG 字符串 → 挂到 DOM 的 SVGSVGElement(svg2pdf 需要真实节点以读取几何) */ +function svgElementFromString(str: string): SVGSVGElement { + const doc = new DOMParser().parseFromString(str, 'image/svg+xml') + return doc.documentElement as unknown as SVGSVGElement +} + +export async function documentToPdf(result: LayoutResult, opts: PdfOptions = {}): Promise { + const mod = await import('jspdf') + const JsPDF = (mod as unknown as { jsPDF?: typeof mod.default; default: typeof mod.default }).jsPDF ?? mod.default + const { pageWidth: w, pageHeight: h } = result.metrics + const orientation = w > h ? 'landscape' : 'portrait' + const scale = opts.scale ?? 2 + + const doc = new JsPDF({ unit: 'mm', format: [w, h], orientation }) + + let serifReady = false + + for (let i = 0; i < result.pages.length; i++) { + if (i > 0) doc.addPage([w, h], orientation) + + const page = result.pages[i]! + const charts = chartsOf(page) + // 有图表且该页图表都未旋转 → 走矢量;否则全栅格化(含图表) + const useVector = charts.length > 0 && charts.every((c) => c.angle === 0) + + if (!useVector) { + const dataUrl = await rasterizePageFull(result, i, scale, opts.pageDecoration) + doc.addImage(dataUrl, 'PNG', 0, 0, w, h) + continue + } + + // 矢量路径:先栅格化"剥掉图表"的底图,再把图表以矢量叠加 + if (!serifReady) serifReady = await registerSerifFont(doc) + const strippedResult: LayoutResult = { + ...result, + pages: result.pages.map((p, idx) => (idx === i ? stripCharts(p) : p)), + } + const baseUrl = await rasterizePageFull(strippedResult, i, scale, opts.pageDecoration) + doc.addImage(baseUrl, 'PNG', 0, 0, w, h) + + let allVectorOk = true + for (const ch of charts) { + try { + const el = svgElementFromString(ch.svg) + el.style.position = 'absolute' + el.style.left = '-99999px' + el.style.top = '0' + document.body.appendChild(el) + try { + const { svg2pdf } = await import('svg2pdf.js') + await svg2pdf(el, doc, { x: ch.left, y: ch.top, width: ch.width, height: ch.height }) + } finally { + document.body.removeChild(el) + } + } catch { + // 该图表矢量失败 → 单独栅格化兜底 + try { + const img = await rasterizeChart(ch.svg, ch.width, ch.height, scale) + doc.addImage(img, 'PNG', ch.left, ch.top, ch.width, ch.height) + } catch { + allVectorOk = false + } + } + } + + // 兜底:若整页矢量叠加彻底失败,重渲染该页(含图表)作位图背景 + if (!allVectorOk) { + // 当前页已 addImage 了"剥图底图",需替换为含图表的整页位图。 + // 顺序处理下最后一页动作即本页,删除后重建即可,不影响其它页。 + const pageCount = doc.getNumberOfPages() + doc.deletePage(pageCount) + doc.addPage([w, h], orientation) + const fallback = await rasterizePageFull(result, i, scale, opts.pageDecoration) + doc.addImage(fallback, 'PNG', 0, 0, w, h) + } + } + + return doc.output('blob') +} + +/** 整页栅格化(含图表),封装原 pageToImageBlob 调用 */ +async function rasterizePageFull( + result: LayoutResult, + index: number, + scale: number, + pageDecoration?: PageDecoration, +): Promise { + const blob = await pageToImageBlob(result, index, { + type: 'image/png', + scale, + background: pageDecoration?.backgroundColor ?? '#ffffff', + fonts: PDF_FONTS, + pageDecoration, + }) + return blobToDataURL(blob) +} diff --git a/src/core/export-engine/export-svg.spec.ts b/src/core/export-engine/export-svg.spec.ts new file mode 100644 index 0000000..e6b0ad2 --- /dev/null +++ b/src/core/export-engine/export-svg.spec.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' + +import { layout } from '@/core/layout-engine/pagination-engine' +import { documentToSvgString } from '@/core/export-engine/export-svg' +import { mmToPx } from '@/core/units' +import { createDemoTemplate } from '@/repository/mock/data/demo-template' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +function makeData(rows: number): Record { + const items = Array.from({ length: rows }, (_, i) => { + const qty = (i % 5) + 1 + const price = 10 + (i % 9) + return { + productCode: `P${String(i + 1).padStart(4, '0')}`, + productName: `商品${i + 1}`, + spec: '规格A', + unit: '件', + qty, + price, + amount: qty * price, + } + }) + return { order: { orderNo: 'SO-001' }, customer: { name: '客户' }, items } +} + +describe('documentToSvgString —— 多页矢量 SVG', () => { + it('产出多页堆叠合法 SVG(foreignObject + 中文 + 精确像素)', async () => { + const template = createDemoTemplate() + const result = await layout(template, makeData(30), { measurer: createCjkMeasurer() }) + const svg = await documentToSvgString(result) + + expect(svg).toContain(' + expect((svg.match(/ + * + * 产物是单文件、多页纵向堆叠的 SVG(每页一个 ``), + * 浏览器 / Inkscape / 矢量软件都能打开,且因为是同一份 HTML+CSS, + * 与预览、PDF、打印视觉完全一致。 + */ +import { mmToPx } from '@/core/units' +import { renderPage, generateCss } from '@/core/renderer-html' +import type { LayoutResult } from '@/core/layout-engine/types' +import type { PageDecoration } from '@/types/template' +import { toXmlSafe } from './rasterize' +import { embedFontsInSvg, type FontFaceDef } from './fonts' + +export async function documentToSvgString( + result: LayoutResult, + fonts?: FontFaceDef[], + pageDecoration?: PageDecoration, +): Promise { + const { pageWidth, pageHeight } = result.metrics + const w = Math.max(1, Math.ceil(mmToPx(pageWidth))) + const h = Math.max(1, Math.ceil(mmToPx(pageHeight))) + const css = generateCss(result.metrics, { screen: false, pageDecoration }) + + const pages = result.pages + .map((page, i) => { + const safe = toXmlSafe(renderPage(page, pageDecoration?.watermark)) + const inner = + `
` + + `` + + `
${safe}
` + + `
` + return ( + `` + + `${inner}` + + `` + ) + }) + .join('\n') + + const totalH = h * result.pages.length + let svg = ( + `` + + `` + + pages + + `` + ) + if (fonts?.length) svg = await embedFontsInSvg(svg, fonts) + return svg +} diff --git a/src/core/export-engine/fonts.ts b/src/core/export-engine/fonts.ts new file mode 100644 index 0000000..17e135d --- /dev/null +++ b/src/core/export-engine/fonts.ts @@ -0,0 +1,103 @@ +/** + * 字体嵌入工具 —— 让栅格化产物(PNG / JPG / PDF)与矢量 SVG / 无头打印 HTML 也能用上「自定义字体」。 + * + * ## 为什么需要它 + * - 预览 / 浏览器打印:字体由主文档或 iframe 的 `@font-face` 提供,正常生效。 + * - 但 PNG/JPG/PDF 走 `SVG ` 光栅化,而 `` 加载的 SVG 是**隔离上下文**, + * 看不到父文档的 `@font-face`,只能用系统字体(PingFang SC / 系统 sans-serif 兜底)。 + * 若模板指定了「思源宋体」等自定义字体,栅格化产物会丢字体。 + * - 解法:把字体以 `@font-face(data-URI)` 直接写进 SVG / HTML 本身,隔离上下文也能用。 + * + * 仅当 `fonts` 显式传入才做网络读取(同源本地字体),默认不联网,符合零网络铁律。 + */ +export interface FontFaceDef { + /** 字体族名,必须与模板 CSS 中的 font-family 一致 */ + family: string + /** 同源字体文件 URL(.woff2 / .ttf / .otf / .woff) */ + src: string + weight?: string | number + style?: string +} + +function fontFormat(src: string): string { + const ext = (src.split('?')[0] ?? '').split('.').pop()?.toLowerCase() + if (ext === 'ttf') return 'truetype' + if (ext === 'otf') return 'opentype' + if (ext === 'woff2') return 'woff2' + if (ext === 'eot') return 'embedded-opentype' + return 'woff' +} + +function fontMime(src: string): string { + const ext = (src.split('?')[0] ?? '').split('.').pop()?.toLowerCase() + if (ext === 'ttf') return 'font/ttf' + if (ext === 'otf') return 'font/otf' + if (ext === 'eot') return 'application/vnd.ms-fontobject' + if (ext === 'woff') return 'font/woff' + return 'font/woff2' +} + +async function fetchFontDataUri(src: string): Promise { + const res = await fetch(src) + if (!res.ok) throw new Error(`字体加载失败 ${src}: ${res.status}`) + const buf = await res.arrayBuffer() + const bytes = new Uint8Array(buf) + let bin = '' + const chunk = 0x8000 + for (let i = 0; i < bytes.length; i += chunk) { + bin += String.fromCharCode(...bytes.subarray(i, i + chunk)) + } + return `data:${fontMime(src)};base64,${btoa(bin)}` +} + +function buildFontFaceCss(defs: FontFaceDef[], dataUris: string[]): string { + return defs + .map( + (d, i) => + `@font-face{font-family:"${d.family}";src:url(${dataUris[i]}) format("${fontFormat( + d.src, + )}");font-weight:${d.weight ?? 'normal'};font-style:${d.style ?? 'normal'};}`, + ) + .join('') +} + +const XHTML_DIV = '
' + +/** 把字体以 data-URI @font-face 嵌进 SVG(每个 xhtml div 顶部)—— 供 rasterize / SVG 矢量导出使用 + * 单个字体加载失败(文件缺失 / 网络异常)时跳过该字体,走系统字体兜底,不阻塞导出。 */ +export async function embedFontsInSvg(svg: string, defs: FontFaceDef[]): Promise { + if (!defs.length) return svg + const pairs = await Promise.all( + defs.map(async (d) => { + try { + return { d, uri: await fetchFontDataUri(d.src) } + } catch { + return null + } + }), + ) + const ok = pairs.filter((p): p is { d: FontFaceDef; uri: string } => p !== null) + if (!ok.length) return svg + const css = `` + return svg.split(XHTML_DIV).join(`${XHTML_DIV}${css}`) +} + +/** 把字体以 data-URI @font-face 嵌进 HTML —— 供无头静默打印文档使用 + * 单个字体加载失败(文件缺失 / 网络异常)时跳过该字体,走系统字体兜底,不阻塞打印。 */ +export async function embedFontsInHtml(html: string, defs: FontFaceDef[]): Promise { + if (!defs.length) return html + const pairs = await Promise.all( + defs.map(async (d) => { + try { + return { d, uri: await fetchFontDataUri(d.src) } + } catch { + return null + } + }), + ) + const ok = pairs.filter((p): p is { d: FontFaceDef; uri: string } => p !== null) + if (!ok.length) return html + const style = `` + if (html.includes('')) return html.replace('', `${style}`) + return `${style}${html}` +} diff --git a/src/core/export-engine/index.ts b/src/core/export-engine/index.ts new file mode 100644 index 0000000..028e892 --- /dev/null +++ b/src/core/export-engine/index.ts @@ -0,0 +1,88 @@ +/** + * Export Engine —— 三格式导出(PDF / JPG / SVG) + * + * 纯 TypeScript、零框架依赖(与 designer-vue 解耦,headless 阶段可直接复用)。 + * 真相来源统一是 `sdk.render()` 的 HTML 产物,因此: + * - 预览 = 打印 = 导出 三者视觉一致 + * - 支持数据绑定 + 超长表格分页 + 中文(系统字体) + * + * 导出语义: + * - PDF:单文件、多页(位图 PDF,思源宋体内联) + * - JPG:每页一个文件(多页时按 `name-1.jpg` 命名) + * - SVG:单文件、多页纵向堆叠(矢量,含 foreignObject) + */ +import type { RenderRequest } from '@/core/sdk' +import { render } from '@/core/sdk' +import type { LayoutResult } from '@/core/layout-engine/types' +import { documentToPdf } from './export-pdf' +import { documentToSvgString } from './export-svg' +import { pageToJpg } from './export-image' +import { downloadBlob } from './util' +import type { FontFaceDef } from './fonts' + +export type { FontFaceDef } from './fonts' + +export type ExportFormat = 'pdf' | 'jpg' | 'svg' + +export interface ExportOptions { + /** 高清倍率 1/2/3,默认 2 */ + scale?: number + /** 文件名(不含扩展名);多页 JPG 会追加 -页码 */ + filename?: string + /** 位图背景色,默认白 */ + background?: string + /** 自定义字体(同源 URL),嵌入 JPG/SVG 栅格化产物使导出不丢字体 */ + fonts?: FontFaceDef[] +} + +export interface ExportOutcome { + blobs: Blob[] + filenames: string[] + mime: string +} + +/** + * 渲染 + 导出一站式入口。PDF/SVG 单文件多页,JPG 每页一个文件。 + * 调用方逐一下载即可。 + */ +export async function exportDocument( + request: RenderRequest, + format: ExportFormat, + options: ExportOptions = {}, +): Promise { + const res = await render(request) + const result: LayoutResult = res.result + const name = (options.filename ?? 'openprint-document').trim() || 'openprint-document' + const scale = options.scale ?? 2 + const bg = options.background ?? '#ffffff' + // 页面装饰(背景色 + 水印)统一来自渲染请求 output,三格式导出共享 + const deco = request.output?.pageDecoration + + if (format === 'pdf') { + const blob = await documentToPdf(result, { scale, pageDecoration: deco }) + return { blobs: [blob], filenames: [`${name}.pdf`], mime: 'application/pdf' } + } + + if (format === 'svg') { + const svg = await documentToSvgString(result, options.fonts, deco) + const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }) + return { blobs: [blob], filenames: [`${name}.svg`], mime: 'image/svg+xml' } + } + + // JPG:每页一个文件 + const blobs = await Promise.all( + result.pages.map((_, i) => + pageToJpg(result, i, { scale, background: bg, fonts: options.fonts, pageDecoration: deco }), + ), + ) + const filenames = blobs.map((_, i) => + blobs.length > 1 ? `${name}-${i + 1}.jpg` : `${name}.jpg`, + ) + return { blobs, filenames, mime: 'image/jpeg' } +} + +export { downloadBlob, pageToJpg, documentToSvgString, documentToPdf } +export type { RenderRequest } from '@/core/sdk' +export type { LayoutResult } from '@/core/layout-engine/types' +export type { PdfOptions as ExportPdfOptions } from './export-pdf' +export type { PageImageOptions } from './rasterize' diff --git a/src/core/export-engine/rasterize.spec.ts b/src/core/export-engine/rasterize.spec.ts new file mode 100644 index 0000000..44bd4f0 --- /dev/null +++ b/src/core/export-engine/rasterize.spec.ts @@ -0,0 +1,105 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { inlinePageImages, toXmlSafe } from '@/core/export-engine/rasterize' + +describe('toXmlSafe —— HTML→XML 安全(foreignObject 光栅化前置)', () => { + it('void 标签补自闭合', () => { + expect(toXmlSafe('')).toBe('') + expect(toXmlSafe('
')).toBe('
') + expect(toXmlSafe('')).toBe('') + expect(toXmlSafe('
')).toBe('
') + expect(toXmlSafe('')).toBe('') + }) + + it('已自闭合的 void 标签保持单一斜杠', () => { + // 带属性的自闭合:补成标准 " />" 形式 + expect(toXmlSafe('')).toBe('') + // 无属性的自闭合(如
)正则不触碰,原样保留也是正确的 XML + expect(toXmlSafe('
')).toBe('
') + }) + + it('内嵌 缺命名空间时补上', () => { + expect(toXmlSafe('')).toBe('') + expect(toXmlSafe('')).toBe( + '', + ) + }) + + it('已带命名空间的 不再重复补', () => { + const src = '' + expect(toXmlSafe(src)).toBe(src) + }) + + it('普通标签与文本不受影响', () => { + expect(toXmlSafe('
你好世界
')).toBe( + '
你好世界
', + ) + }) +}) + +describe('inlinePageImages —— 外部图片内联(防 canvas taint)', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + const png = Uint8Array.from([0x89, 0x50, 0x4e, 0x47]) + const stubFetch = (mime = 'image/png') => + vi.stubGlobal( + 'fetch', + vi.fn(async () => ({ + ok: true, + headers: { get: () => mime }, + arrayBuffer: async () => png.buffer, + })), + ) + + it('无图片时原样返回', async () => { + const html = '
hello
' + expect(await inlinePageImages(html)).toBe(html) + }) + + it('外部图片 src 被替换为 data URI(btoa 可用环境)', async () => { + stubFetch() + const out = await inlinePageImages('') + expect(out).toContain('data:image/png;base64,') + expect(out).not.toContain('http://example.com') + }) + + it('单引号 src 的 同样被内联(此前漏网导致 canvas taint)', async () => { + stubFetch() + const out = await inlinePageImages("") + expect(out).toContain('data:image/png;base64,') + expect(out).not.toContain('http://example.com') + }) + + it('CSS url(...) 背景图被内联', async () => { + stubFetch() + const out = await inlinePageImages('
') + expect(out).toContain('data:image/png;base64,') + expect(out).not.toContain('http://example.com') + }) + + it('SVG 被内联', async () => { + stubFetch() + const out = await inlinePageImages('') + expect(out).toContain('data:image/png;base64,') + expect(out).not.toContain('http://example.com') + }) + + it('加载失败的图片降级为透明占位,不阻塞导出', async () => { + vi.stubGlobal('fetch', vi.fn(async () => ({ ok: false, status: 404 }))) + const out = await inlinePageImages('') + expect(out).toContain('data:image/png;base64,') + expect(out).not.toContain('/missing.png') + }) + + it('data URI 图片保持原样(不重复内联)', async () => { + const html = '' + expect(await inlinePageImages(html)).toBe(html) + }) + + it('data URI 的 CSS url 保持原样', async () => { + const html = '
' + expect(await inlinePageImages(html)).toBe(html) + }) +}) diff --git a/src/core/export-engine/rasterize.ts b/src/core/export-engine/rasterize.ts new file mode 100644 index 0000000..50ccc73 --- /dev/null +++ b/src/core/export-engine/rasterize.ts @@ -0,0 +1,255 @@ +/** + * 栅格化工具 —— 把单页渲染 HTML 变成位图 + * + * ## 为什么用 SVG + * + * 设计器/渲染引擎的"真相来源"是 `render()` 产出的 HTML/CSS(§2 单一真相源)。 + * 要把 HTML 变成 PNG/JPG,浏览器原生只有两条路: + * 1. html2canvas 之类的库(要新依赖,且零网络铁律下不想引) + * 2. 把 HTML 塞进 SVG 的 ``,再交给 ``/`canvas` 光栅化 + * + * 这里走第 2 条:纯浏览器 API、零依赖、中文靠系统字体(PingFang SC / 思源)渲染。 + * + * ## 已知边界 + * - `` 在 Chromium / Firefox 用 `` 光栅化是支持的; + * Safari 出于安全限制**不支持** img 里的 foreignObject,需要换 html2canvas 才能覆盖。 + * 当前目标浏览器为 Chromium 内核,足够。 + * - 内嵌资源必须同源或 data URI(本设计器图片已走 data URI,条码/二维码是内联 SVG)。 + */ +import { mmToPx } from '@/core/units' +import { renderPage, generateCss } from '@/core/renderer-html' +import type { LayoutResult } from '@/core/layout-engine/types' +import type { PageDecoration } from '@/types/template' +import { embedFontsInSvg, type FontFaceDef } from './fonts' + +/** HTML 转 XML 安全的 void 标签集合(foreignObject 走 XML 解析,未闭合会整页渲染失败) */ +const VOID_TAGS = new Set([ + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', + 'meta', 'param', 'source', 'track', 'wbr', +]) + +/** + * 把渲染产物里可能出现的不规范 HTML 修成 XML 安全: + * - void 标签补自闭合 `/>` + * - 内嵌 `` 确保带 SVG 命名空间(否则 XHTML 命名空间下不渲染) + */ +export function toXmlSafe(html: string): string { + const fixed = html.replace( + /<([a-zA-Z][\w-]*)((?:\s+[^<>]*?)?)\s*>/g, + (m, tag: string, attrs: string) => { + const t = tag.toLowerCase() + if (VOID_TAGS.has(t)) { + const clean = attrs.replace(/\/\s*$/, '') + return `<${t}${clean} />` + } + return m + }, + ) + // 给缺命名空间的内嵌 svg 补上(负向预查:后面到 > 之间不含 xmlns= 才补) + return fixed.replace(/]*\bxmlns=)/g, '` + + `` + + `
${safe}
` + + `
` + return ( + `` + + `${inner}` + + `` + ) +} + +/** SVG 光栅化超时(ms):部分环境对 支持差 / onload 永不触发, + * 超时则降级返回白底空 canvas,不让单页失败拖垮整份导出。 */ +const SVG_RASTER_TIMEOUT = 4000 + +function loadImage(src: string): Promise { + return new Promise((resolve, reject) => { + const img = new Image() + let done = false + const timer = setTimeout(() => { + if (done) return + done = true + reject(new Error('SVG 光栅化超时(环境不支持 foreignObject 或资源跨域)')) + }, SVG_RASTER_TIMEOUT) + img.onload = () => { + if (done) return + done = true + clearTimeout(timer) + resolve(img) + } + img.onerror = () => { + if (done) return + done = true + clearTimeout(timer) + reject(new Error('SVG 光栅化失败(浏览器不支持 foreignObject 或资源跨域)')) + } + img.src = src + }) +} + +/** SVG 字符串 → 高清 canvas(scale 倍图,填白底避免透明)。超时/失败时降级白底空 canvas + * + * ## 为什么用 data: URL 而不是 blob: URL(血泪教训,勿改回) + * Chromium 安全策略:经 `blob:` URL 加载的、含 `` 的 SVG 会被判定为 + * 「来源不纯净」,drawImage 后 canvas 被污染,toBlob/toDataURL 抛 + * "Tainted canvases may not be exported"。同一 SVG 改用 `data:` URL 加载则不污染。 + * (实测对照:blob+FO=TAINTED,blob+纯矢量=clean,data+FO=clean,data+纯矢量=clean) + */ +export async function svgToCanvas(svg: string, scale: number, bg: string): Promise { + const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg) + const cw = Math.max(1, Math.round(794 * scale)) + const ch = Math.max(1, Math.round(1123 * scale)) + const fallback = (): HTMLCanvasElement => { + const canvas = document.createElement('canvas') + canvas.width = cw + canvas.height = ch + const ctx = canvas.getContext('2d') + if (ctx) { + ctx.fillStyle = bg + ctx.fillRect(0, 0, cw, ch) + } + return canvas + } + try { + const img = await loadImage(url) + const canvas = document.createElement('canvas') + canvas.width = Math.max(1, Math.round(img.width * scale)) + canvas.height = Math.max(1, Math.round(img.height * scale)) + const ctx = canvas.getContext('2d') + if (!ctx) return fallback() + ctx.fillStyle = bg + ctx.fillRect(0, 0, canvas.width, canvas.height) + ctx.drawImage(img, 0, 0, canvas.width, canvas.height) + return canvas + } catch { + // 环境不支持 foreignObject 光栅化(如 happy-dom)→ 降级白底空 canvas,不阻塞导出 + return fallback() + } +} + +export interface PageImageOptions { + scale?: number + background?: string + type?: 'image/png' | 'image/jpeg' + /** 自定义字体(同源 URL),嵌入 SVG 使栅格化产物不丢字体 */ + fonts?: FontFaceDef[] + /** 页面装饰(背景色 + 水印),使导出与预览一致 */ + pageDecoration?: PageDecoration +} + +/* ------------------------- 图片内联(防 canvas taint) ------------------------- */ + +/** 1×1 透明 PNG,外部图片加载失败时的降级占位(避免整页导出失败) */ +const TRANSPARENT_PNG = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' + +/** 的 src(双引号或单引号) */ +const IMG_SRC_RE = /]*?\bsrc=(["'])([^"']*)\1/gi +/** CSS url(...) 引用(background / @font-face 等,仅图片类会被内联) */ +const CSS_URL_RE = /url\(\s*(["']?)([^"')]+)\1\s*\)/gi +/** SVG 的 href / xlink:href */ +const SVG_IMAGE_RE = /]*?\b(?:xlink:)?href=(["'])([^"']*)\1/gi + +async function fetchAsDataUri(src: string): Promise<{ uri: string; mime: string }> { + const res = await fetch(src) + if (!res.ok) throw new Error(`资源加载失败 ${src}: ${res.status}`) + const bytes = new Uint8Array(await res.arrayBuffer()) + const mime = res.headers.get('content-type')?.split(';')[0]?.trim() || 'image/png' + let bin = '' + const chunk = 0x8000 + for (let i = 0; i < bytes.length; i += chunk) { + bin += String.fromCharCode(...bytes.subarray(i, i + chunk)) + } + return { uri: `data:${mime};base64,${btoa(bin)}`, mime } +} + +/** 收集页面 HTML 里所有外部资源引用(img src / CSS url / SVG image href) */ +function collectExternalSrcs(html: string): Set { + const srcs = new Set() + const add = (src: string | undefined) => { + const s = (src ?? '').trim() + if (!s || /^data:/i.test(s) || /^#/.test(s) || /^blob:/i.test(s)) return + srcs.add(s) + } + for (const m of html.matchAll(IMG_SRC_RE)) add(m[2]) + for (const m of html.matchAll(CSS_URL_RE)) add(m[2]) + for (const m of html.matchAll(SVG_IMAGE_RE)) add(m[2]) + return srcs +} + +/** + * 把页面 HTML 里所有外部图片(http / 相对路径)内联成 data URI。 + * 否则 SVG 加载跨域图片会污染 canvas,toBlob 抛 + * "Tainted canvases may not be exported"。失败/跨域图片降级为透明占位,不阻塞导出。 + */ +export async function inlinePageImages(html: string): Promise { + const srcs = collectExternalSrcs(html) + if (!srcs.size) return html + + const resolved = new Map() + await Promise.all( + [...srcs].map(async (src) => { + try { + const { uri } = await fetchAsDataUri(src) + resolved.set(src, uri) + } catch { + resolved.set(src, TRANSPARENT_PNG) + } + }), + ) + + // 替换 (双/单引号) + let out = html.replace(IMG_SRC_RE, (tag, quote: string, src: string) => { + const uri = resolved.get(src.trim()) + return uri ? tag.replace(`src=${quote}${src}${quote}`, `src=${quote}${uri}${quote}`) : tag + }) + // 替换 CSS url(...) + out = out.replace(CSS_URL_RE, (tag, quote: string, src: string) => { + const uri = resolved.get(src.trim()) + return uri ? `url(${quote}${uri}${quote})` : tag + }) + // 替换 SVG + out = out.replace(SVG_IMAGE_RE, (tag, quote: string, src: string) => { + const uri = resolved.get(src.trim()) + return uri ? tag.replace(/href=(["'])[^"']*\1/i, `href=${quote}${uri}${quote}`) : tag + }) + return out +} + +/** 把某一页渲染成位图 Blob(PNG / JPG 共用) */ +export async function pageToImageBlob( + result: LayoutResult, + index: number, + opts: PageImageOptions = {}, +): Promise { + const page = result.pages[index] + if (!page) throw new Error(`页码越界:${index}`) + const deco = opts.pageDecoration + const css = generateCss(result.metrics, { screen: false, pageDecoration: deco }) + // 先内联图片为 data URI,再进 SVG 栅格化 —— 防止外部图污染 canvas 导致 toBlob 失败 + const pageHtml = await inlinePageImages(renderPage(page, deco?.watermark)) + let svg = buildPageSvg(pageHtml, css, result.metrics.pageWidth, result.metrics.pageHeight) + if (opts.fonts?.length) svg = await embedFontsInSvg(svg, opts.fonts) + const canvas = await svgToCanvas(svg, opts.scale ?? 2, opts.background ?? '#ffffff') + const type = opts.type ?? 'image/png' + return new Promise((resolve, reject) => { + canvas.toBlob( + (b) => (b ? resolve(b) : reject(new Error('canvas.toBlob 失败'))), + type, + type === 'image/jpeg' ? 0.92 : undefined, + ) + }) +} diff --git a/src/core/export-engine/util.ts b/src/core/export-engine/util.ts new file mode 100644 index 0000000..7c8b774 --- /dev/null +++ b/src/core/export-engine/util.ts @@ -0,0 +1,25 @@ +/** + * 浏览器侧小工具:Blob → dataURL、触发下载 + */ + +export function blobToDataURL(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = () => reject(new Error('FileReader 读取失败')) + reader.readAsDataURL(blob) + }) +} + +/** 触发浏览器下载(单文件) */ +export function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + a.remove() + // 延迟回收,确保下载已触发 + setTimeout(() => URL.revokeObjectURL(url), 1500) +} diff --git a/src/core/fonts/catalog.ts b/src/core/fonts/catalog.ts new file mode 100644 index 0000000..b2e1a7b --- /dev/null +++ b/src/core/fonts/catalog.ts @@ -0,0 +1,79 @@ +/** + * 字体注册中心(《设计方案》§11.6 @open-print/fonts registry) + * + * 内置字体清单:font-family 名 → 字体文件(public/fonts/ 同源静态资源,零网络铁律)。 + * 命名与 `参考资料/fonts/fonts.txt` 一致: + * - HedvigLettersSans-Regular.woff2 → Hedvig Letters Sans + * - LiberationSerif-{Regular,Bold,Italic}.woff2 → Liberation Serif(一个族,三种字重/字形) + * - SourceHanSansCN-Normal.woff2 → 思源黑体 + * - SourceHanSerifCN-Regular.ttf → 思源宋体 + * - ChillKai.woff2 → 寒蝉正楷体 + */ + +export interface FontFaceEntry { + /** 同源字体文件 URL(相对 public 根;浏览器/位图栅格化用,woff2 优先) */ + src: string + weight?: number + style?: 'normal' | 'italic' +} + +export interface FontFamilyDef { + /** 字体族名(模板 style.fontFamily / CSS font-family 用这个名字) */ + family: string + /** 展示名(属性面板下拉用) */ + label: string + /** 优先级(属性面板排序) */ + order: number + faces: FontFaceEntry[] +} + +/** 内置字体清单(顺序即面板展示顺序;woff2 供浏览器显示与位图 PDF 栅格化内联) */ +export const FONT_CATALOG: FontFamilyDef[] = [ + { + family: '思源黑体', + label: '思源黑体', + order: 1, + faces: [{ src: '/fonts/SourceHanSansCN-Normal.woff2', weight: 400 }], + }, + { + family: '思源宋体', + label: '思源宋体', + order: 2, + faces: [{ src: '/fonts/SourceHanSerifCN-Regular.ttf', weight: 400 }], + }, + { + family: '寒蝉正楷体', + label: '寒蝉正楷体', + order: 3, + faces: [{ src: '/fonts/ChillKai.woff2', weight: 400 }], + }, + { + family: 'Liberation Serif', + label: 'Liberation Serif', + order: 4, + faces: [ + { src: '/fonts/LiberationSerif-Regular.woff2', weight: 400 }, + { src: '/fonts/LiberationSerif-Bold.woff2', weight: 700 }, + { src: '/fonts/LiberationSerif-Italic.woff2', weight: 400, style: 'italic' }, + ], + }, + { + family: 'Hedvig Letters Sans', + label: 'Hedvig Letters Sans', + order: 5, + faces: [{ src: '/fonts/HedvigLettersSans-Regular.woff2', weight: 400 }], + }, +] + +/** 按字体族名查找(模板 style.fontFamily 可能存家族名,如 "思源黑体") */ +export function findFontFamily(family: string | undefined): FontFamilyDef | undefined { + if (!family) return undefined + const name = (family.replace(/^"|"$/g, '').split(',')[0] ?? '').trim() + return FONT_CATALOG.find((f) => f.family === name) +} + +/** 字体文件相对 public 根 → 绝对 URL(预览 iframe 注入用;浏览器环境) */ +export function fontUrl(src: string): string { + if (typeof window === 'undefined') return src + return new URL(src, window.location.origin).href +} diff --git a/src/core/fonts/fonts.spec.ts b/src/core/fonts/fonts.spec.ts new file mode 100644 index 0000000..9859286 --- /dev/null +++ b/src/core/fonts/fonts.spec.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest' +import { FONT_CATALOG, findFontFamily } from '@/core/fonts/catalog' +import { builtinFontFaceCss, templateUsedFonts } from '@/core/fonts/loader' +import type { TemplateData } from '@/types/template' +import type { AnyControl } from '@/types/control' + +describe('fonts —— 内置字体包', () => { + it('目录覆盖参考资料 fonts.txt 全部字体族', () => { + const families = FONT_CATALOG.map((f) => f.family) + // fonts.txt 定义的名称(中文名/拉丁名) + expect(families).toContain('思源黑体') + expect(families).toContain('思源宋体') + expect(families).toContain('寒蝉正楷体') + expect(families).toContain('Liberation Serif') + expect(families).toContain('Hedvig Letters Sans') + // Liberation Serif 一个族包含 3 个字重/字形 + const lib = FONT_CATALOG.find((f) => f.family === 'Liberation Serif') + expect(lib?.faces.length).toBe(3) + }) + + it('builtinFontFaceCss 生成合法 @font-face 且含 format', () => { + const css = builtinFontFaceCss('http://localhost:5173') + expect(css).toContain('@font-face') + expect(css).toContain('font-family:"思源黑体"') + expect(css).toContain('format("woff2")') + expect(css).toContain('format("truetype")') // 思源宋体 ttf + expect(css).toContain('http://localhost:5173/fonts/SourceHanSansCN-Normal.woff2') + }) + + it('templateUsedFonts 只提取模板实际用到的字体族', () => { + const tpl: TemplateData = { + version: '1.0', + document: { + type: 'report', + page: { width: 210, height: 297, unit: 'mm', orientation: 'portrait', margin: { top: 10, right: 10, bottom: 10, left: 10 } }, + sections: [ + { + type: 'body', + components: [ + { id: 't1', type: 'text', left: 0, top: 0, width: 50, height: 8, value: 'x', style: { fontFamily: '思源黑体' }, printable: true }, + { id: 't2', type: 'text', left: 0, top: 10, width: 50, height: 8, value: 'y', printable: true }, + ], + }, + ], + }, + } + const used = templateUsedFonts(tpl) + expect(used.map((f) => f.family)).toEqual(['思源黑体']) + }) + + it('findFontFamily 容错带引号的族名', () => { + expect(findFontFamily('"思源宋体"')?.family).toBe('思源宋体') + expect(findFontFamily('思源黑体, sans-serif')?.family).toBe('思源黑体') + expect(findFontFamily(undefined)).toBeUndefined() + }) +}) diff --git a/src/core/fonts/loader.ts b/src/core/fonts/loader.ts new file mode 100644 index 0000000..1f8d5ea --- /dev/null +++ b/src/core/fonts/loader.ts @@ -0,0 +1,101 @@ +/** + * 字体本地加载(《设计方案》§11.6 @open-print/fonts loader) + * + * - `loadBuiltinFonts()`:用 FontFace API 把内置字体注册到浏览器(设计期画布/预览生效), + * 字体文件来自 public/fonts/ 同源静态资源,零网络依赖。 + * - `builtinFontFaceCss(baseUrl)`:生成 @font-face CSS(预览 iframe 注入用)。 + * - `templateUsedFonts(template)`:提取模板文本控件实际用到的字体族(供导出内联)。 + */ +import { FONT_CATALOG, findFontFamily, type FontFamilyDef } from './catalog' +import type { TemplateData } from '@/types/template' +import type { AnyControl, TextControl } from '@/types/control' +import type { FontFaceDef } from '@/core/export-engine/fonts' + +/** 浏览器环境是否可用 FontFace API */ +function canUseFontFace(): boolean { + return typeof window !== 'undefined' && typeof (window as unknown as { FontFace?: unknown }).FontFace !== 'undefined' +} + +/** 把内置字体注册到 document.fonts(浏览器);resolve 后返回已加载的族名列表 */ +export async function loadBuiltinFonts(defs: FontFamilyDef[] = FONT_CATALOG): Promise { + if (!canUseFontFace() || typeof document === 'undefined' || typeof window === 'undefined') return [] + const FontFaceCtor = ( + window as unknown as { + FontFace?: new (family: string, source: string | ArrayBuffer, descriptors?: FontFaceDescriptors) => FontFace + } + ).FontFace + if (!FontFaceCtor) return [] + const loaded: string[] = [] + const tasks: Promise[] = [] + for (const def of defs) { + for (const face of def.faces) { + try { + const fontFace = new FontFaceCtor( + def.family, + `url(${new URL(face.src, window.location.origin).href})`, + { + weight: String(face.weight ?? 'normal'), + style: face.style ?? 'normal', + }, + ) + tasks.push( + fontFace.load().then(() => { + document.fonts.add(fontFace) + if (!loaded.includes(def.family)) loaded.push(def.family) + }), + ) + } catch { + /* 单字体失败不阻塞整体 */ + } + } + } + await Promise.allSettled(tasks) + return loaded +} + +/** 生成内置字体的 @font-face CSS 块(baseUrl 用于拼字体绝对/相对路径) */ +export function builtinFontFaceCss(baseUrl: string): string { + return FONT_CATALOG.map((def) => + def.faces + .map((face) => { + const src = face.src.startsWith('/') ? `${baseUrl.replace(/\/+$/, '')}${face.src}` : face.src + const ext = src.split('?')[0]?.split('.').pop()?.toLowerCase() + const format = ext === 'ttf' ? 'truetype' : ext === 'otf' ? 'opentype' : ext === 'woff' ? 'woff' : 'woff2' + return ( + `@font-face{font-family:"${def.family}";` + + `src:url(${src}) format("${format}");` + + `font-weight:${face.weight ?? 'normal'};font-style:${face.style ?? 'normal'};}` + ) + }) + .join(''), + ).join('') +} + +/** 提取模板中实际用到的内置字体族(供导出/无头打印内联,避免全量内联) */ +export function templateUsedFonts(template: TemplateData): FontFamilyDef[] { + const used = new Set() + const walk = (comps: AnyControl[]): void => { + for (const c of comps) { + if (c.type === 'text') { + const family = (c as TextControl).style?.fontFamily + const def = findFontFamily(family) + if (def) used.add(def.family) + } + if (c.type === 'zone') walk(c.children) + } + } + for (const section of template.document.sections) walk(section.components as AnyControl[]) + return FONT_CATALOG.filter((f) => used.has(f.family)) +} + +/** 转为导出引擎的 FontFaceDef(data-URI 内联用) */ +export function toExportFontDefs(defs: FontFamilyDef[]): FontFaceDef[] { + return defs.flatMap((def) => + def.faces.map((face) => ({ + family: def.family, + src: face.src, + weight: face.weight, + style: face.style, + })), + ) +} diff --git a/src/core/headless/createHeadless.ts b/src/core/headless/createHeadless.ts new file mode 100644 index 0000000..1cec6a6 --- /dev/null +++ b/src/core/headless/createHeadless.ts @@ -0,0 +1,192 @@ +/** + * Headless 无头静默模式 —— 《OpenPrint-实施指南》Phase 9 / 《设计方案》§19.4.4 + * + * ## 定位 + * `createHeadless({ repository, dataSource, fonts })` 是一个**零 UI、不挂载任何可见 DOM** 的 + * 编程式渲染/导出/打印入口。它直接复用 `sdk.render()` + `export-engine`, + * 因此与「设计器预览」「浏览器打印」三者视觉完全一致(同一份 HTML/CSS 真相源)。 + * + * ## 与 Vue 解耦 + * 本文件是纯 TypeScript,不 import 任何 Vue / designer-vue 代码, + * 可直接被使用者的脚本、Node 参考渲染器或自动化测试调用。 + * + * ## 零隐藏 DOM 的保证 + * - 不往页面挂载任何隐藏预览节点(对比:设计器预览面板用独立 iframe)。 + * - `print()` 仅在打印瞬间创建一个 0 尺寸 iframe、注入 srcdoc、调用 `print()`, + * 打印完成(或用户取消)后立即移除,页面不留残留 DOM。 + * - 导出走 canvas/Blob,完全在内存与离屏 canvas 中完成。 + */ +import { render as sdkRender, type RenderRequest, type RenderResponse } from '@/core/sdk' +import { renderHtml } from '@/core/renderer-html' +import { + exportDocument, + type ExportFormat, + type ExportOptions, + type ExportOutcome, + type FontFaceDef, +} from '@/core/export-engine' +import { embedFontsInHtml, loadFonts } from './loader' +import type { TemplateRepository } from '@/repository/types' +import type { TemplateData } from '@/types/template' +import type { AnyControl } from '@/types/control' + +export interface HeadlessOptions { + /** 模板仓库(取模板用)。不传则 `buildRequest` 抛错,但 `render/exportXxx` 仍可手动传 template。 */ + repository?: TemplateRepository + /** 数据源仓库(保留位,当前用于约定签名;自动合成数据由调用方用 buildPreviewData 组合)。 */ + dataSource?: unknown + /** 同源自定义字体,导出 / 打印时嵌入,避免栅格化丢字体。 */ + fonts?: FontFaceDef[] +} + +/** 渲染/导出请求:模板 + 可选数据(未给数据则由调用方保证模板无 dataSource 依赖) */ +export interface HeadlessRequest { + template: TemplateData + data?: Record + layout?: RenderRequest['layout'] + output?: RenderRequest['output'] +} + +export interface HeadlessExportOptions { + /** 高清倍率 1/2/3,默认 2 */ + scale?: number + /** 文件名(不含扩展名) */ + filename?: string + /** 位图背景色,默认白 */ + background?: string +} + +export interface HeadlessInstance { + /** 仅排版,返回 HTML 产物 + 页面模型 + 告警 */ + render(req: HeadlessRequest): Promise + /** 导出 PDF(单文件多页,思源宋体写死内联) */ + exportPdf(req: HeadlessRequest, opts?: HeadlessExportOptions): Promise + /** 导出 JPG(多页每页一个文件) */ + exportJpg(req: HeadlessRequest, opts?: HeadlessExportOptions): Promise + /** 导出 SVG(单文件多页纵向堆叠矢量) */ + exportSvg(req: HeadlessRequest, opts?: HeadlessExportOptions): Promise + /** 静默浏览器打印(弹出系统打印对话框,零 UI 残留) */ + print(req: HeadlessRequest): Promise + /** 按模板 id 从 repository 取出模板,组装成 RenderRequest(默认 output.screen=false) */ + buildRequest(templateId: string, data?: Record): Promise + /** 释放资源(反注册字体等) */ + dispose(): void +} + +export function createHeadless(options: HeadlessOptions = {}): HeadlessInstance { + const fonts = options.fonts ?? [] + let teardownFonts: (() => void) | null = null + + // 浏览器环境:把字体注册到文档,供同源预览/打印使用 + if (fonts.length && typeof document !== 'undefined') { + loadFonts(fonts) + .then((t) => { + teardownFonts = t + }) + .catch(() => { + /* 字体加载失败不影响其余功能,栅格化会走系统字体兜底 */ + }) + } + + const toRequest = (req: HeadlessRequest): RenderRequest => ({ + template: req.template, + data: req.data, + layout: req.layout, + output: { screen: false, ...req.output }, + }) + + async function buildRequest(templateId: string, data?: Record): Promise { + if (!options.repository) throw new Error('createHeadless 未配置 repository,无法按 id 取模板') + const rec = await options.repository.get(templateId) + if (!rec) throw new Error(`模板不存在:${templateId}`) + return { template: rec.data, data, output: { screen: false } } + } + + async function render(req: HeadlessRequest): Promise { + return sdkRender(toRequest(req)) + } + + async function exportFormat( + req: HeadlessRequest, + format: ExportFormat, + opts?: HeadlessExportOptions, + ): Promise { + const expOpts: ExportOptions = { + fonts, + scale: opts?.scale, + filename: opts?.filename, + background: opts?.background, + } + return exportDocument(toRequest(req), format, expOpts) + } + + async function print(req: HeadlessRequest): Promise { + const res = await render(toRequest(req)) + const pageDecoration = toRequest(req).output?.pageDecoration + let html = renderHtml(res.result, { screen: false, pageDecoration }) + if (fonts.length) html = await embedFontsInHtml(html, fonts) + await silentPrint(html) + } + + function dispose(): void { + if (teardownFonts) { + try { + teardownFonts() + } catch { + /* noop */ + } + teardownFonts = null + } + } + + return { + render, + exportPdf: (r, o) => exportFormat(r, 'pdf', o), + exportJpg: (r, o) => exportFormat(r, 'jpg', o), + exportSvg: (r, o) => exportFormat(r, 'svg', o), + print, + buildRequest, + dispose, + } +} + +/** 静默打印:瞬态 0 尺寸 iframe + srcdoc + window.print(),完成后立即移除 */ +function silentPrint(html: string): Promise { + return new Promise((resolve, reject) => { + if (typeof document === 'undefined') { + reject(new Error('silentPrint 需要浏览器环境')) + return + } + const iframe = document.createElement('iframe') + iframe.setAttribute('aria-hidden', 'true') + iframe.style.cssText = 'position:fixed;right:0;bottom:0;width:0;height:0;border:0;' + let removed = false + const cleanup = () => { + if (removed) return + removed = true + setTimeout(() => iframe.remove(), 300) + } + iframe.onload = () => { + const cw = iframe.contentWindow + if (!cw) { + cleanup() + reject(new Error('iframe 无 contentWindow')) + return + } + // onafterprint 在用户确认打印或取消时都会触发 + cw.onafterprint = () => { + cleanup() + resolve() + } + try { + cw.focus() + cw.print() + } catch (e) { + cleanup() + reject(e instanceof Error ? e : new Error(String(e))) + } + } + document.body.appendChild(iframe) + iframe.srcdoc = html + }) +} diff --git a/src/core/headless/headless.spec.ts b/src/core/headless/headless.spec.ts new file mode 100644 index 0000000..80fb1bb --- /dev/null +++ b/src/core/headless/headless.spec.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest' + +import { createHeadless } from '@/core/headless' +import { createDemoTemplate } from '@/repository/mock/data/demo-template' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +function makeData(rows: number): Record { + const items = Array.from({ length: rows }, (_, i) => ({ + productCode: `P${i + 1}`, + productName: `商品${i + 1}`, + qty: (i % 5) + 1, + price: 10, + amount: ((i % 5) + 1) * 10, + })) + return { order: { orderNo: 'SO-001' }, items } +} + +describe('createHeadless —— 无头静默模式', () => { + const measurer = createCjkMeasurer() + + it('render 复用 sdk 产出多页 HTML(零 UI 残留)', async () => { + const hl = createHeadless({ fonts: [] }) + const res = await hl.render({ template: createDemoTemplate(), data: makeData(30), layout: { measurer } }) + expect(res.pages).toBeGreaterThanOrEqual(2) + hl.dispose() + }) + + it('buildRequest 未配置 repository 时抛错(契约边界)', async () => { + const hl = createHeadless({ fonts: [] }) + await expect(hl.buildRequest('missing')).rejects.toThrow() + hl.dispose() + }) +}) diff --git a/src/core/headless/index.ts b/src/core/headless/index.ts new file mode 100644 index 0000000..a41d498 --- /dev/null +++ b/src/core/headless/index.ts @@ -0,0 +1,21 @@ +/** + * Headless 无头模式入口 —— 汇总导出 + * + * 用法: + * ```ts + * import { createHeadless } from '@/core/headless' + * const headless = createHeadless({ repository, dataSource, fonts }) + * const { blobs, filenames } = await headless.exportPdf({ template, data }, { scale: 2 }) + * blobs.forEach((b, i) => downloadBlob(b, filenames[i])) + * headless.dispose() + * ``` + */ +export { createHeadless } from './createHeadless' +export type { + HeadlessOptions, + HeadlessRequest, + HeadlessInstance, + HeadlessExportOptions, +} from './createHeadless' +export { loadFonts, embedFontsInSvg, embedFontsInHtml } from './loader' +export type { FontFaceDef } from '@/core/export-engine/fonts' diff --git a/src/core/headless/loader.ts b/src/core/headless/loader.ts new file mode 100644 index 0000000..4318aac --- /dev/null +++ b/src/core/headless/loader.ts @@ -0,0 +1,34 @@ +/** + * Headless 字体加载器 + * + * - re-export `export-engine/fonts` 的嵌字工具(让栅格化 / 打印文档用上自定义字体) + * - `loadFonts`:用 FontFace API 把同源字体注册到**当前文档**, + * 供设计器预览 / 浏览器打印的同源文档(iframe srcdoc)使用。 + * + * 注意:`loadFonts` 注册的是「文档级」字体,预览/打印文档只要同源就能用; + * 而栅格化(SVG ``)是隔离上下文,必须用 `embedFontsInSvg` 把字体写进 SVG 本身。 + */ +export { + type FontFaceDef, + embedFontsInSvg, + embedFontsInHtml, +} from '@/core/export-engine/fonts' + +/** 把同源字体注册到当前文档(供同源预览 / 打印使用)。返回反注册函数。 */ +export async function loadFonts(defs: import('@/core/export-engine/fonts').FontFaceDef[]): Promise<() => void> { + if (typeof document === 'undefined' || !('fonts' in document)) return () => {} + const fontsApi = (document as unknown as { fonts: { add(f: FontFace): void; delete(f: FontFace): void } }).fonts + const faces: FontFace[] = [] + for (const d of defs) { + const ff = new FontFace(d.family, `url(${d.src})`, { + weight: d.weight !== undefined ? String(d.weight) : 'normal', + style: d.style ?? 'normal', + }) + await ff.load() + fontsApi.add(ff) + faces.push(ff) + } + return () => { + for (const ff of faces) fontsApi.delete(ff) + } +} diff --git a/src/core/layout-engine/aggregate.spec.ts b/src/core/layout-engine/aggregate.spec.ts new file mode 100644 index 0000000..2cea3af --- /dev/null +++ b/src/core/layout-engine/aggregate.spec.ts @@ -0,0 +1,388 @@ +import { describe, expect, it } from 'vitest' + +import { + aggValueForRows, + columnAggregatable, + footerKindOf, + formatAggNumber, + isAggToken, + parseAggToken, + stripItems, + toChineseCapitalRMB, +} from '@/core/layout-engine/aggregate' +import { buildTableModel, sliceTable } from '@/core/layout-engine/table-engine' +import { + buildDesignGrid, + insertTableColumn, + insertTableRow, + rowRoleLabel, + seedSummaryTail, +} from '@/core/layout-engine/table-cells' +import type { TableCell, TableColumn, TableControl } from '@/types/control' +import type { EvalContext } from './types' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +/* ============================ 纯函数 ============================ */ + +describe('parseAggToken / isAggToken', () => { + it('识别各类聚合 token', () => { + expect(parseAggToken('{{#pageSum}}')).toBe('pageSum') + expect(parseAggToken('{{#totalSum}}')).toBe('totalSum') + expect(parseAggToken('{{#pageCap}}')).toBe('pageCap') + expect(parseAggToken('{{#totalCap}}')).toBe('totalCap') + expect(parseAggToken('{{#pageAvg}}')).toBe('pageAvg') + expect(parseAggToken('{{ #totalCount }}')).toBe('totalCount') + }) + it('非 token 返回 null', () => { + expect(parseAggToken('{{item.qty}}')).toBeNull() + expect(parseAggToken('本页合计')).toBeNull() + expect(parseAggToken('{{order.total}}')).toBeNull() + expect(isAggToken('{{#totalSum}}')).toBe(true) + expect(isAggToken('总量')).toBe(false) + }) + it('footerKindOf 归类', () => { + expect(footerKindOf('pageSum')).toBe('pageSubtotal') + expect(footerKindOf('totalSum')).toBe('grandTotal') + expect(footerKindOf('totalCap')).toBe('capital') + expect(footerKindOf(null)).toBe('static') + }) +}) + +describe('stripItems', () => { + it('去除 items[]. 前缀', () => { + expect(stripItems('items[].amount')).toBe('amount') + expect(stripItems('order.no')).toBe('order.no') + expect(stripItems(undefined)).toBe('') + }) +}) + +describe('aggValueForRows', () => { + const rows = [ + { qty: 2, price: 3 }, + { qty: 3, price: 4 }, + ] + it('求和', () => { + expect(aggValueForRows('totalSum', rows, 'qty')).toBe(5) + expect(aggValueForRows('pageSum', rows, 'price')).toBe(7) + }) + it('平均', () => { + expect(aggValueForRows('totalAvg', rows, 'price')).toBe(3.5) + }) + it('计数', () => { + expect(aggValueForRows('totalCount', rows, 'qty')).toBe(2) + }) +}) + +describe('formatAggNumber', () => { + it('整数不带小数,小数保留两位并加千分位', () => { + expect(formatAggNumber(18)).toBe('18') + expect(formatAggNumber(22650)).toBe('22,650') + expect(formatAggNumber(18.5)).toBe('18.50') + expect(formatAggNumber(1234567.5)).toBe('1,234,567.50') + }) +}) + +describe('toChineseCapitalRMB', () => { + it('整数大写', () => { + expect(toChineseCapitalRMB(22650)).toBe('贰万贰仟陆佰伍拾元整') + expect(toChineseCapitalRMB(18)).toBe('壹拾捌元整') + expect(toChineseCapitalRMB(100)).toBe('壹佰元整') + expect(toChineseCapitalRMB(1000000)).toBe('壹佰万元整') + }) + it('小数(角/分)', () => { + expect(toChineseCapitalRMB(100.05)).toBe('壹佰元零伍分') + expect(toChineseCapitalRMB(100.5)).toBe('壹佰元伍角') + expect(toChineseCapitalRMB(0.05)).toBe('伍分') + }) + it('零与负数', () => { + expect(toChineseCapitalRMB(0)).toBe('零元整') + expect(toChineseCapitalRMB(-22650)).toBe('负贰万贰仟陆佰伍拾元整') + }) +}) + +describe('columnAggregatable', () => { + it('显式 aggregate 优先', () => { + expect(columnAggregatable({ title: 'x', field: 'a', width: 10, aggregate: false }, undefined)).toBe(false) + expect(columnAggregatable({ title: 'x', field: 'a', width: 10, aggregate: true }, undefined)).toBe(true) + }) + it('按数据首行采样推断数值列', () => { + const col = { title: '金额', field: 'items[].amount', width: 10 } + expect(columnAggregatable(col, { amount: 5 })).toBe(true) + expect(columnAggregatable(col, { amount: 'abc' })).toBe(false) + expect(columnAggregatable({ title: '名称', field: 'items[].name', width: 10 }, { name: 'x' })).toBe(false) + }) +}) + +/* ====================== 引擎:尾行聚合集成 ====================== */ + +function mkTailTable(): TableControl { + const columns: TableColumn[] = [ + { title: '序号', expression: '{{rowIndex + 1}}', width: 15, align: 'center' }, + { title: '名称', field: 'items[].name', width: 60 }, + { title: '数量', field: 'items[].qty', width: 25, align: 'right', aggregate: true }, + { title: '单价', field: 'items[].price', width: 30, align: 'right', aggregate: true }, + { title: '金额', field: 'items[].amount', width: 30, align: 'right', aggregate: true }, + ] + const cells: TableCell[][] = [ + [ + { text: '序号' }, + { text: '名称' }, + { text: '数量' }, + { text: '单价' }, + { text: '金额' }, + ], + [ + { expression: '{{rowIndex + 1}}' }, + { field: 'items[].name' }, + { field: 'items[].qty' }, + { field: 'items[].price' }, + { field: 'items[].amount' }, + ], + [ + { text: '本页合计', style: { bold: true } }, + {}, + { text: '{{#pageSum}}', style: { bold: true, align: 'right' } }, + { text: '{{#pageSum}}', style: { bold: true, align: 'right' } }, + { text: '{{#pageSum}}', style: { bold: true, align: 'right' } }, + ], + [ + { text: '总计', style: { bold: true } }, + {}, + { text: '{{#totalSum}}', style: { bold: true, align: 'right' } }, + { text: '{{#totalSum}}', style: { bold: true, align: 'right' } }, + { text: '{{#totalSum}}', style: { bold: true, align: 'right' } }, + ], + [ + { text: '大写金额', style: { bold: true } }, + {}, + {}, + {}, + { text: '{{#totalCap}}', style: { bold: true } }, + ], + ] + const data = [ + { name: 'a', qty: 1, price: 10, amount: 10 }, + { name: 'b', qty: 2, price: 10, amount: 20 }, + { name: 'c', qty: 3, price: 10, amount: 30 }, + { name: 'd', qty: 4, price: 10, amount: 40 }, + { name: 'e', qty: 5, price: 10, amount: 50 }, + ] + return { + id: 't', + type: 'table', + left: 0, + top: 0, + width: 160, + height: 60, + columns, + cells, + headerRows: 1, + staticRows: 3, + data, + options: { repeatHeader: true, repeatFooter: true, pageRows: 'auto', borders: 'all' }, + } +} + +function aggCellText(footer: { cells: { text?: string }[] }, col: number): string { + return footer.cells[col]?.text ?? '' +} + +describe('buildTableModel —— 尾行聚合结构', () => { + const measurer = createCjkMeasurer() + const ctx: EvalContext = { data: { items: [] } } + const model = buildTableModel({ control: mkTailTable(), ctx, measurer, widthMm: 160, heightMm: 60 }) + + it('尾行被识别为 pageSubtotal / grandTotal / capital', () => { + expect(model.footerRows).toHaveLength(3) + expect(model.footerRows[0]!.footerKind).toBe('pageSubtotal') + expect(model.footerRows[1]!.footerKind).toBe('grandTotal') + expect(model.footerRows[2]!.footerKind).toBe('capital') + }) + + it('数值列 token 被标记 isAgg + aggField,字符串列不计算', () => { + const pageRow = model.footerRows[0]! + // 数量列(index2)应为聚合单元格 + expect(pageRow.cells[2]!.isAgg).toBe(true) + expect(pageRow.cells[2]!.aggField).toBe('qty') + expect(pageRow.cells[2]!.tokenKind).toBe('pageSum') + // 名称列(index1)无 token + expect(pageRow.cells[1]!.isAgg).toBeFalsy() + expect(pageRow.cells[1]!.text).toBe('') + }) + + it('单页切片:本页合计=总计=全量,大写金额为整表总计', () => { + const slice = sliceTable(model, { avail: 1000, start: 0 }) + expect(slice.isLast).toBe(true) + const f = slice.footerRows + expect(f).toHaveLength(3) + // 数量:1+2+3+4+5=15 + expect(aggCellText(f[0]!, 2)).toBe('15') + expect(aggCellText(f[1]!, 2)).toBe('15') + // 金额:10+20+30+40+50=150 + expect(aggCellText(f[0]!, 4)).toBe('150') + expect(aggCellText(f[1]!, 4)).toBe('150') + // 大写金额 = 壹佰伍拾元整 + expect(aggCellText(f[2]!, 4)).toBe('壹佰伍拾元整') + }) +}) + +describe('sliceTable —— 强制分页时本页合计逐页、总计仅末页', () => { + const measurer = createCjkMeasurer() + const ctx: EvalContext = { data: { items: [] } } + const control: TableControl = { ...mkTailTable(), options: { repeatHeader: true, repeatFooter: true, pageRows: 2, borders: 'all' } } + const model = buildTableModel({ control, ctx, measurer, widthMm: 160, heightMm: 60 }) + + it('第 1 页(非末页)只有本页合计,不含总计/大写', () => { + const s1 = sliceTable(model, { avail: 1000, start: 0 }) + expect(s1.isLast).toBe(false) + // 本页合计 = 前 2 行:数量 1+2=3,金额 10+20=30 + expect(aggCellText(s1.footerRows[0]!, 2)).toBe('3') + expect(aggCellText(s1.footerRows[0]!, 4)).toBe('30') + // 末页专属行不出现 + expect(s1.footerRows.find((r) => r.footerKind === 'grandTotal')).toBeUndefined() + expect(s1.footerRows.find((r) => r.footerKind === 'capital')).toBeUndefined() + }) + + it('末页同时含本页合计、总计与大写金额', () => { + // 5 行 / 每页 2 → 第 3 页(start=4)为末页,仅 1 行 + const s3 = sliceTable(model, { avail: 1000, start: 4 }) + expect(s3.isLast).toBe(true) + expect(s3.footerRows.find((r) => r.footerKind === 'grandTotal')).toBeTruthy() + expect(s3.footerRows.find((r) => r.footerKind === 'capital')).toBeTruthy() + // 末页本页合计 = 第 5 行:数量 5,金额 50 + expect(aggCellText(s3.footerRows[0]!, 2)).toBe('5') + expect(aggCellText(s3.footerRows[0]!, 4)).toBe('50') + // 总计仍为全量 15 / 150 + const grand = s3.footerRows.find((r) => r.footerKind === 'grandTotal')! + expect(aggCellText(grand, 2)).toBe('15') + expect(aggCellText(grand, 4)).toBe('150') + // 大写金额取整表金额总计 + const cap = s3.footerRows.find((r) => r.footerKind === 'capital')! + expect(aggCellText(cap, 4)).toBe('壹佰伍拾元整') + }) +}) + +/* ====================== seedSummaryTail / 行角色 ====================== */ + +describe('seedSummaryTail', () => { + it('自动植入三行尾结构并标记数值列 aggregate', () => { + const base: TableControl = { + id: 't', + type: 'table', + left: 0, + top: 0, + width: 160, + height: 60, + columns: [ + { title: '名称', field: 'items[].name', width: 60 }, + { title: '数量', field: 'items[].qty', width: 25, align: 'right' }, + { title: '金额', field: 'items[].amount', width: 30, align: 'right' }, + ], + headerRows: 1, + data: [ + { name: 'a', qty: 1, amount: 10 }, + { name: 'b', qty: 2, amount: 20 }, + ], + options: { repeatHeader: true, repeatFooter: true, pageRows: 'auto', borders: 'all' }, + } + const seeded = seedSummaryTail(base) + const grid = buildDesignGrid(seeded) + expect(grid.rowCount).toBe(5) // 1 表头 + 1 数据 + 3 尾 + // 金额列(index2)被标记 aggregate + expect(seeded.columns[2]!.aggregate).toBe(true) + // 尾行含 token + const lastRows = grid.cells.slice(grid.headerRows + 1) + expect(lastRows.some((r) => r.some((c) => c.text === '{{#pageSum}}'))).toBe(true) + expect(lastRows.some((r) => r.some((c) => c.text === '{{#totalCap}}'))).toBe(true) + }) + + it('幂等:已植入则不重复', () => { + const base: TableControl = { + id: 't', + type: 'table', + left: 0, + top: 0, + width: 160, + height: 60, + columns: [ + { title: '名称', field: 'items[].name', width: 60 }, + { title: '金额', field: 'items[].amount', width: 30, align: 'right' }, + ], + headerRows: 1, + data: [{ name: 'a', amount: 1 }], + options: { pageRows: 'auto' }, + } + const once = seedSummaryTail(base) + const twice = seedSummaryTail(once) + expect(buildDesignGrid(twice).rowCount).toBe(buildDesignGrid(once).rowCount) + }) +}) + +describe('rowRoleLabel', () => { + it('聚合尾行返回正确角色名', () => { + const seeded = seedSummaryTail({ + id: 't', + type: 'table', + left: 0, + top: 0, + width: 160, + height: 60, + columns: [ + { title: '名称', field: 'items[].name', width: 60 }, + { title: '金额', field: 'items[].amount', width: 30, align: 'right' }, + ], + headerRows: 1, + data: [{ name: 'a', amount: 1 }], + options: { pageRows: 'auto' }, + }) + const grid = buildDesignGrid(seeded) + expect(rowRoleLabel(grid, 0)).toBe('标题行') + expect(rowRoleLabel(grid, 1)).toBe('数据行') + // 尾行:本页合计 / 总计 / 大写金额 + expect(rowRoleLabel(grid, 2)).toBe('本页合计行') + expect(rowRoleLabel(grid, 3)).toBe('总计行') + expect(rowRoleLabel(grid, 4)).toBe('大写金额行') + }) +}) + +describe('insertTableRow / insertTableColumn', () => { + it('插入行后行数+1,数据表维持单数据样例行', () => { + const base: TableControl = { + id: 't', + type: 'table', + left: 0, + top: 0, + width: 160, + height: 60, + columns: [{ title: 'A', field: 'items[].a', width: 30 }], + headerRows: 1, + data: [{ a: 1 }], + options: { pageRows: 'auto' }, + } + const seeded = seedSummaryTail(base) // 5 行 + const withRow = insertTableRow(seeded, 3) // 在第 3 行(本页合计)上方插入 + const grid = buildDesignGrid(withRow) + expect(grid.rowCount).toBe(6) + expect(grid.staticRows).toBe(4) + }) + + it('插入列后列数+1,每行单元格同步扩展', () => { + const base: TableControl = { + id: 't', + type: 'table', + left: 0, + top: 0, + width: 160, + height: 60, + columns: [{ title: 'A', field: 'items[].a', width: 30 }], + headerRows: 1, + data: [{ a: 1 }], + options: { pageRows: 'auto' }, + } + const withCol = insertTableColumn(base, 1) // 在列 1 左侧插入 + expect(withCol.columns).toHaveLength(2) + const grid = buildDesignGrid(withCol) + expect(grid.colCount).toBe(2) + expect(grid.cells.every((r) => r.length === 2)).toBe(true) + }) +}) diff --git a/src/core/layout-engine/aggregate.ts b/src/core/layout-engine/aggregate.ts new file mode 100644 index 0000000..9f1af23 --- /dev/null +++ b/src/core/layout-engine/aggregate.ts @@ -0,0 +1,212 @@ +/** + * aggregate —— 表格尾行(本页合计 / 总计 / 大写金额)聚合引擎(纯函数,可测) + * + * 设计要点(与用户需求对齐): + * - 表格默认结构 = 标题行 + 数据行 + 本页合计行 + 总计行 + 大写金额行。 + * - 尾行的「计算单元格」用约定表达式占位: + * - `{{#pageSum}}` 本页合计(每页只算本页数据行) + * - `{{#totalSum}}` 总计(整表所有数据行,仅末页出现) + * - `{{#pageCap}}` 本页合计大写金额 + * - `{{#totalCap}}` 总计大写金额(如 贰万贰仟陆佰伍拾元整,仅末页出现) + * - 另有 `{{#pageAvg}}`/`{{#totalAvg}}`/`{{#pageCount}}`/`{{#totalCount}}` 可用。 + * - 单元格里凡不是聚合 token 的 `{{}}` 视为普通插值;完全不含 `{{}}` 的字面量视为静态文本。 + * - 哪一列需要合计:由列字段类型(或数据行首行采样)判断,非数值列(字符串等)不计算。 + * - 合计整数不带小数、小数保留两位;大写金额按人民币大写规则转换。 + */ +import type { TableColumn } from '@/types/control' + +/* -------------------------------- Token 解析 -------------------------------- */ + +export type AggKind = + | 'pageSum' + | 'totalSum' + | 'pageAvg' + | 'totalAvg' + | 'pageCount' + | 'totalCount' + | 'pageCap' + | 'totalCap' + +/** 解析单元格文本是否为聚合 token;返回具体类型或 null */ +export function parseAggToken(text: string | undefined): AggKind | null { + if (!text) return null + const m = /^\{\{\s*#(pageSum|totalSum|pageAvg|totalAvg|pageCount|totalCount|pageCap|totalCap)\s*\}\}$/.exec( + text.trim(), + ) + return m ? (m[1] as AggKind) : null +} + +/** 该文本是否为聚合 token */ +export function isAggToken(text: string | undefined): boolean { + return parseAggToken(text) !== null +} + +/** token 所在的尾行类别 */ +export type FooterKind = 'pageSubtotal' | 'grandTotal' | 'capital' | 'static' + +export function footerKindOf(kind: AggKind | null): FooterKind { + if (kind === 'pageCap' || kind === 'totalCap') return 'capital' + if (kind === 'totalSum' || kind === 'totalAvg' || kind === 'totalCount') return 'grandTotal' + if (kind === 'pageSum' || kind === 'pageAvg' || kind === 'pageCount') return 'pageSubtotal' + return 'static' +} + +/* -------------------------------- 字段工具 -------------------------------- */ + +/** 去 `items[].` 前缀,得到数据行字段键(items[].amount → amount) */ +export function stripItems(field: string | undefined): string { + if (!field) return '' + return field.includes('[].') ? field.slice(field.indexOf('[].') + 3) : field +} + +function toNum(v: unknown): number { + if (typeof v === 'number') return v + if (v === null || v === undefined || v === '') return 0 + const n = Number(v) + return Number.isNaN(n) ? 0 : n +} + +/* -------------------------------- 聚合求值 -------------------------------- */ + +export function sumField(rows: Array>, field: string): number { + let s = 0 + for (const r of rows) s += toNum(r[field]) + return s +} + +export function avgField(rows: Array>, field: string): number { + return rows.length ? sumField(rows, field) / rows.length : 0 +} + +/** 按 token 类别,对给定数据行集合在 field 上求值 */ +export function aggValueForRows( + kind: AggKind, + rows: Array>, + field: string, +): number { + switch (kind) { + case 'pageSum': + case 'totalSum': + return sumField(rows, field) + case 'pageAvg': + case 'totalAvg': + return avgField(rows, field) + case 'pageCount': + case 'totalCount': + return rows.length + default: + return 0 + } +} + +/** + * 聚合数值显示:整数不带小数(默认整数计算),小数保留两位,统一加千分位。 + * 对齐 table-engine 的 formatAggregate 行为;字符串等非数值列不计算(调用方应已跳过)。 + */ +export function formatAggNumber(v: number): string { + const fixed = Number.isInteger(v) ? v.toFixed(0) : v.toFixed(2) + const [int = '0', dec] = fixed.split('.') + const sign = int.startsWith('-') ? '-' : '' + const grouped = (sign ? int.slice(1) : int).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + return sign + grouped + (dec ? `.${dec}` : '') +} + +/* ------------------------------ 大写人民币转换 ------------------------------ */ + +const DIGITS = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'] +const SMALL_UNITS = ['', '拾', '佰', '仟'] +// 每 4 位一组的大单位:个 / 万 / 亿 / 兆 +const GROUP_UNITS = ['', '万', '亿', '兆'] + +/** 0..9999 → 中文(不含组单位),并返回本组是否含非零位 */ +function groupToChinese(num: number): { text: string; hasNonZero: boolean } { + const s = String(num).padStart(4, '0') + let result = '' + let zero = false + let hasNonZero = false + for (let i = 0; i < 4; i++) { + const d = Number(s[i]) + if (d === 0) { + zero = true + } else { + if (zero && result) result += DIGITS[0] + result += (DIGITS[d] ?? '') + (SMALL_UNITS[3 - i] ?? '') + zero = false + hasNonZero = true + } + } + return { text: result, hasNonZero } +} + +/** 非负整数 → 中文数字(正确处理 万/亿 分组与组间零) */ +function integerToChinese(n: number): string { + if (n === 0) return DIGITS[0] ?? '零' + if (n < 0) return '负' + integerToChinese(-n) + // 按 4 位拆组(高位在前) + const groups: number[] = [] + let rem = Math.floor(n) + while (rem > 0) { + groups.unshift(rem % 10000) + rem = Math.floor(rem / 10000) + } + let result = '' + for (let g = 0; g < groups.length; g++) { + const { text, hasNonZero } = groupToChinese(groups[g] ?? 0) + if (hasNonZero) { + result += text + (GROUP_UNITS[groups.length - 1 - g] ?? '') + } else if (g < groups.length - 1 && result && !result.endsWith(DIGITS[0] ?? '零')) { + // 整组为 0(且非最低组)→ 在高位与低位之间补一个"零"连接 + result += DIGITS[0] ?? '零' + } + } + return result +} + +/** + * 金额 → 中文大写人民币(如 22650 → 贰万贰仟陆佰伍拾元整)。 + * - 支持整数与两位小数(角/分),第三位小数四舍五入。 + * - 负数加「负」前缀;零金额返回「零元整」。 + */ +export function toChineseCapitalRMB(input: number): string { + if (!Number.isFinite(input)) return '' + const negative = input < 0 + const amount = Math.abs(Math.round(input * 100) / 100) + if (amount === 0) return '零元整' + + const intPart = Math.floor(amount) + const decPart = Math.round((amount - intPart) * 100) // 0..99 + const jiao = Math.floor(decPart / 10) + const fen = decPart % 10 + + let result = '' + if (intPart > 0) result += integerToChinese(intPart) + '元' + if (jiao === 0 && fen === 0) { + result += '整' + } else { + if (jiao > 0) result += (DIGITS[jiao] ?? '') + '角' + else if (intPart > 0) result += DIGITS[0] ?? '零' // 元后零角(如 100.05 → 壹佰元零伍分) + if (fen > 0) result += (DIGITS[fen] ?? '') + '分' + } + return (negative ? '负' : '') + result +} + +/* ------------------------------ 列聚合判定 ------------------------------ */ + +/** + * 某列是否参与聚合(计算合计)。 + * - 显式 `aggregate:false` → 不参与。 + * - 显式 `aggregate:true` 或 `'sum'`/`'avg'`/`'count'` → 参与。 + * - 否则按数据首行采样推断:列字段在数据行里出现数值则参与(字符串等类型不计算)。 + */ +export function columnAggregatable( + col: TableColumn | undefined, + sample: Record | undefined, +): boolean { + if (!col) return false + if (col.aggregate === false) return false + if (col.aggregate === true || col.aggregate === 'sum' || col.aggregate === 'avg' || col.aggregate === 'count') + return true + const key = stripItems(col.field) + if (key && sample && typeof sample[key] === 'number') return true + return false +} diff --git a/src/core/layout-engine/cell-format.spec.ts b/src/core/layout-engine/cell-format.spec.ts new file mode 100644 index 0000000..4d91b7e --- /dev/null +++ b/src/core/layout-engine/cell-format.spec.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from 'vitest' +import { formatCellValue } from './expression' +import type { CellFormat } from '@/types/control' + +describe('formatCellValue —— 单元格数据格式', () => { + it('undefined / none / text 回落默认 stringify', () => { + expect(formatCellValue(123, undefined)).toBe('123') + expect(formatCellValue(123, { kind: 'none' })).toBe('123') + expect(formatCellValue(123, { kind: 'text' })).toBe('123') + expect(formatCellValue(null, undefined)).toBe('') + }) + + it('date:按模板输出(年-月-日 / 年月日 / 美式)', () => { + const d = new Date(2026, 7, 11) // 2026-08-11(本地,稳定) + expect(formatCellValue(d, { kind: 'date', pattern: 'YYYY-MM-DD' })).toBe('2026-08-11') + expect(formatCellValue(d, { kind: 'date', pattern: 'YYYY年MM月DD日' })).toBe('2026年08月11日') + expect(formatCellValue(d, { kind: 'date', pattern: 'MM/DD/YYYY' })).toBe('08/11/2026') + }) + + it('date:带时间模板', () => { + const d = new Date(2026, 7, 11, 14, 30) + expect(formatCellValue(d, { kind: 'date', pattern: 'YYYY-MM-DD HH:mm' })).toBe('2026-08-11 14:30') + }) + + it('date:默认模板为 YYYY-MM-DD', () => { + const d = new Date(2026, 7, 11) + expect(formatCellValue(d, { kind: 'date' })).toBe('2026-08-11') + }) + + it('int:默认带千分位;thousands=false 不分组', () => { + expect(formatCellValue(1234567, { kind: 'int' })).toBe('1,234,567') + expect(formatCellValue(1234567, { kind: 'int', thousands: false })).toBe('1234567') + expect(formatCellValue(0, { kind: 'int' })).toBe('0') + }) + + it('decimal:按 digits 截断;千分位可关', () => { + expect(formatCellValue(1234.567, { kind: 'decimal', digits: 2 })).toBe('1,234.57') + expect(formatCellValue(1234.5, { kind: 'decimal', digits: 0 })).toBe('1,235') + expect(formatCellValue(1234.567, { kind: 'decimal', digits: 2, thousands: false })).toBe('1234.57') + }) + + it('currency:符号 + 千分位 + 小数位', () => { + expect(formatCellValue(12800.5, { kind: 'currency' })).toBe('¥12,800.50') + expect(formatCellValue(1200.5, { kind: 'currency', code: 'USD' })).toBe('$1,200.50') + expect(formatCellValue(9.9, { kind: 'currency', code: 'EUR', digits: 1 })).toBe('€9.9') + }) + + it('percent:数值 ×100 加 %', () => { + expect(formatCellValue(0.125, { kind: 'percent' })).toBe('12.50%') + expect(formatCellValue(0.125, { kind: 'percent', digits: 1 })).toBe('12.5%') + expect(formatCellValue(1, { kind: 'percent' })).toBe('100.00%') + }) + + it('digits 越界被收敛到 [0,6]', () => { + expect(formatCellValue(1.23456789, { kind: 'decimal', digits: 99 })).toBe('1.234568') + expect(formatCellValue(1.23456789, { kind: 'decimal', digits: -5 })).toBe('1') + }) + + it('非数字输入按 0 处理(与引擎 toNum 一致)', () => { + expect(formatCellValue('abc', { kind: 'int' })).toBe('0') + expect(formatCellValue('', { kind: 'decimal' })).toBe('0.00') + }) +}) + +/** 类型守卫:确保 CellFormat 联合在编译期被消费(防止误删成员) */ +const _kinds: CellFormat['kind'][] = ['none', 'text', 'date', 'int', 'decimal', 'currency', 'percent'] +void _kinds diff --git a/src/core/layout-engine/data-binder.spec.ts b/src/core/layout-engine/data-binder.spec.ts new file mode 100644 index 0000000..0080692 --- /dev/null +++ b/src/core/layout-engine/data-binder.spec.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from 'vitest' +import { resolveTextValue, resolveControlContent } from './data-binder' +import type { TextControl } from '@/types/control' +import type { EvalContext } from './types' + +function text(partial: Partial): TextControl { + return { id: 't1', type: 'text', left: 0, top: 0, width: 50, height: 10, ...partial } +} + +describe('resolveTextValue —— 文本控件格式化', () => { + const ctx: EvalContext = { data: { order: { date: '2026-08-11', total: 12800.5, qty: 1234.5, rate: 0.125 } } } + + it('无 format 时回落默认 stringify', () => { + const r = resolveTextValue(text({ binding: 'order.total' }), ctx) + expect(r.text).toBe('12800.5') + }) + + it('日期格式', () => { + const r = resolveTextValue( + text({ binding: 'order.date', format: { kind: 'date', pattern: 'YYYY年MM月DD日' } }), + ctx, + ) + expect(r.text).toBe('2026年08月11日') + }) + + it('整数 + 千分位', () => { + const r = resolveTextValue( + text({ binding: 'order.qty', format: { kind: 'int', thousands: true } }), + ctx, + ) + expect(r.text).toBe('1,235') + }) + + it('小数 + 千分位', () => { + const r = resolveTextValue( + text({ binding: 'order.total', format: { kind: 'decimal', digits: 2, thousands: true } }), + ctx, + ) + expect(r.text).toBe('12,800.50') + }) + + it('货币', () => { + const r = resolveTextValue( + text({ binding: 'order.total', format: { kind: 'currency', code: 'USD', digits: 2, thousands: true } }), + ctx, + ) + expect(r.text).toBe('$12,800.50') + }) + + it('百分比', () => { + const r = resolveTextValue( + text({ binding: 'order.rate', format: { kind: 'percent', digits: 1 } }), + ctx, + ) + expect(r.text).toBe('12.5%') + }) + + it('expression 模式不被 format 影响(过滤器优先)', () => { + const r = resolveTextValue( + text({ expression: '{{order.total | currency:\'CNY\'}}', format: { kind: 'int' } }), + ctx, + ) + expect(r.text).toContain('12,800.50') + }) +}) + +describe('resolveControlContent —— 文本格式贯穿渲染', () => { + const ctx: EvalContext = { data: { order: { date: '2026-08-11' } } } + + it('绑定字段按 format 输出(走 content 解析总入口)', async () => { + const { content, warnings } = await resolveControlContent( + text({ id: 't2', binding: 'order.date', format: { kind: 'date', pattern: 'MM/DD/YYYY' } }), + ctx, + ) + expect(content.kind).toBe('text') + if (content.kind === 'text') expect(content.text).toBe('08/11/2026') + expect(warnings).toEqual([]) + }) +}) diff --git a/src/core/layout-engine/data-binder.ts b/src/core/layout-engine/data-binder.ts new file mode 100644 index 0000000..b5ec747 --- /dev/null +++ b/src/core/layout-engine/data-binder.ts @@ -0,0 +1,248 @@ +/** + * DataBinder —— 控件 + 数据 → 渲染就绪内容 + * 真理源:《OpenPrint-设计方案.md》§5.2 / §5.4a(printable)/ §5.6(visibleIf)/ §5.7 / §5.8 + * + * 职责边界: + * - 只负责"内容是什么",不负责"放在哪一页"(那是 pagination-engine 的事) + * - 所有异步资源(二维码 SVG)在此解析完成,后续渲染全同步 + * - 条码/二维码统一输出 **SVG 矢量**(§5.7):打印用位图会糊,扫码枪可能读不出 + */ +import * as BwipJs from '@bwip-js/generic' +import DOMPurify from 'dompurify' +import QRCode from 'qrcode' + +import type { + AnyControl, + BarcodeControl, + ChartControl, + ImageControl, + QrcodeControl, + RichTextControl, + TextControl, +} from '@/types/control' +import { interpolate, evaluateVisible, resolveBinding, stringifyValue, formatCellValue } from './expression' +import type { EvalContext, ResolvedContent, RenderWarning } from './types' +import { renderChartControl } from '@/core/chartkit' + +/* ------------------------------ 可见性判定 ------------------------------ */ + +/** + * 渲染期是否输出该控件。 + * §5.4a:`printable:false` 渲染时跳过,但**设计画布仍可编辑**(所以只在这里过滤)。 + */ +export function isControlPrintable(control: AnyControl, ctx: EvalContext): boolean { + if (control.printable === false) return false + return evaluateVisible(control.visibleIf, ctx) +} + +/* -------------------------------- 文本 -------------------------------- */ + +/** + * 文本取值优先级:expression > binding > value。 + * 注意 `value` 里也允许写 `{{}}`(用户在属性面板静态文本里手打变量是常见操作), + * 所以静态文本同样过一遍插值。 + */ +export function resolveTextValue( + control: TextControl, + ctx: EvalContext, +): { text: string; errors: string[] } { + if (control.expression) return interpolate(control.expression, ctx) + if (control.binding) { + // binding 模式:按控件 format 格式化(expression 模式请用 {{field | date:'...'}} 过滤器) + const raw = resolveBinding(control.binding, ctx) + return { text: formatCellValue(raw, control.format), errors: [] } + } + return interpolate(control.value ?? '', ctx) +} + +/* -------------------------------- 图片 -------------------------------- */ + +function resolveImageSrc(control: ImageControl, ctx: EvalContext): string { + const v = control.value + if (!v || !v.content) return '' + switch (v.mode) { + case 'binding': { + const raw = resolveBinding(v.content, ctx) + return raw === null || raw === undefined ? '' : String(raw) + } + case 'inline': + case 'url': + case 'asset': + default: + return v.content + } +} + +/* ------------------------------ 条码 / 二维码 ----------------------------- */ + +function resolveCodeText(control: BarcodeControl | QrcodeControl, ctx: EvalContext): string { + if (control.binding) { + const v = resolveBinding(control.binding, ctx) + if (v !== null && v !== undefined && v !== '') return String(v) + } + if (control.value) return interpolate(control.value, ctx).text + // 既没 binding 也没 value:设计画布兜底示例码,预览同样兜底,避免空白 + return control.type === 'qrcode' ? 'https://openprint.dev' : '0123456789' +} + +/** + * 让 SVG 自适应控件尺寸:去掉固定 width/height,保留 viewBox 由外层容器缩放。 + * bwip-js / qrcode 输出的 SVG 都带写死的 width/height,直接嵌入会溢出控件框。 + */ +function makeSvgResponsive(svg: string): string { + return svg + .replace(/]*?)\swidth="[^"]*"/i, ']*?)\sheight="[^"]*"/i, ' { + const svg = await QRCode.toString(text, { + type: 'svg', + errorCorrectionLevel: control.errorLevel ?? 'M', + margin: 0, + }) + return makeSvgResponsive(svg) +} + +/* ------------------------------- 富文本消毒 ------------------------------ */ + +/** §11.2:Renderer 输出前必须对绑定内容消毒,防模板注入 XSS */ +function sanitizeRichText(html: string): string { + return DOMPurify.sanitize(html, { USE_PROFILES: { html: true } }) +} + +/* ------------------------------ 内容解析总入口 ----------------------------- */ + +export interface ResolvedControlContent { + content: ResolvedContent + warnings: RenderWarning[] +} + +/** + * 把单个控件解析为渲染就绪内容。 + * 任何失败都降级为 placeholder + warning —— **绝不静默丢内容**, + * 否则用户拿到一张缺字段的单据却毫不知情,这在 ERP 场景是事故。 + */ +export async function resolveControlContent( + control: AnyControl, + ctx: EvalContext, +): Promise { + const warnings: RenderWarning[] = [] + + switch (control.type) { + case 'text': { + const { text, errors } = resolveTextValue(control, ctx) + for (const message of errors) { + warnings.push({ code: 'EXPRESSION_ERROR', message, controlId: control.id }) + } + if (!text && control.binding) { + warnings.push({ + code: 'BINDING_MISSING', + message: `字段 "${control.binding}" 在数据中为空`, + controlId: control.id, + }) + } + return { content: { kind: 'text', text }, warnings } + } + + case 'richtext': { + const raw = (control as RichTextControl).value ?? '' + const { text, errors } = interpolate(raw, ctx) + for (const message of errors) { + warnings.push({ code: 'EXPRESSION_ERROR', message, controlId: control.id }) + } + return { content: { kind: 'html', html: sanitizeRichText(text) }, warnings } + } + + case 'image': { + const src = resolveImageSrc(control, ctx) + if (!src) { + warnings.push({ + code: 'IMAGE_UNRESOLVED', + message: '图片来源为空', + controlId: control.id, + }) + return { content: { kind: 'placeholder', label: '图片' }, warnings } + } + return { content: { kind: 'image', src }, warnings } + } + + case 'barcode': { + const text = resolveCodeText(control, ctx) + if (!text) { + warnings.push({ code: 'BARCODE_FAILED', message: '条码内容为空', controlId: control.id }) + return { content: { kind: 'placeholder', label: '条码' }, warnings } + } + try { + return { content: { kind: 'svg', svg: renderBarcodeSvg(control, text) }, warnings } + } catch (e) { + warnings.push({ + code: 'BARCODE_FAILED', + message: `条码生成失败(${control.format ?? 'code128'}):${e instanceof Error ? e.message : String(e)}`, + controlId: control.id, + }) + return { content: { kind: 'placeholder', label: '条码' }, warnings } + } + } + + case 'qrcode': { + const text = resolveCodeText(control, ctx) + if (!text) { + warnings.push({ code: 'BARCODE_FAILED', message: '二维码内容为空', controlId: control.id }) + return { content: { kind: 'placeholder', label: '二维码' }, warnings } + } + try { + return { content: { kind: 'svg', svg: await renderQrcodeSvg(control, text) }, warnings } + } catch (e) { + warnings.push({ + code: 'BARCODE_FAILED', + message: `二维码生成失败:${e instanceof Error ? e.message : String(e)}`, + controlId: control.id, + }) + return { content: { kind: 'placeholder', label: '二维码' }, warnings } + } + } + + case 'rect': + case 'line': + return { content: { kind: 'shape' }, warnings } + + case 'chart': { + // 原生 SVG 图表(零依赖):与条码同理走矢量 svg,打印不糊。 + try { + return { content: { kind: 'svg', svg: renderChartControl(control as ChartControl) }, warnings } + } catch (e) { + warnings.push({ + code: 'CHART_FAILED', + message: `图表生成失败:${e instanceof Error ? e.message : String(e)}`, + controlId: control.id, + }) + return { content: { kind: 'placeholder', label: '图表' }, warnings } + } + } + + default: + // table / zone 不走这里(由 table-engine / pagination-engine 处理) + return { content: { kind: 'placeholder', label: control.type }, warnings } + } +} diff --git a/src/core/layout-engine/expression.spec.ts b/src/core/layout-engine/expression.spec.ts new file mode 100644 index 0000000..2a6e0ed --- /dev/null +++ b/src/core/layout-engine/expression.spec.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'vitest' + +import { + evaluate, + evaluateVisible, + interpolate, + resolveBinding, +} from '@/core/layout-engine/expression' + +// EvalContext 必须有 data 字段;测试里无变量时用 { data: {} } + + +describe('expression 安全表达式引擎', () => { + it('基础算术与优先级', () => { + expect(evaluate('1 + 2 * 3', { data: {} })).toBe(7) + expect(evaluate('(1 + 2) * 3', { data: {} })).toBe(9) + expect(evaluate('10 / 4', { data: {} })).toBe(2.5) + }) + + it('字符串拼接', () => { + expect(evaluate('"a" + "b"', { data: {} })).toBe('ab') + expect(evaluate('"x" + 1', { data: {} })).toBe('x1') + }) + + it('比较与逻辑', () => { + expect(evaluate('3 > 2', { data: {} })).toBe(true) + expect(evaluate('true && false', { data: {} })).toBe(false) + expect(evaluate('true || false', { data: {} })).toBe(true) + expect(evaluate('!false', { data: {} })).toBe(true) + }) + + it('变量从 ctx.data 取值', () => { + expect(evaluate('x * 2', { data: { x: 5 } })).toBe(10) + expect(evaluate('a + b', { data: { a: 2, b: 3 } })).toBe(5) + }) + + it('过滤器(经 interpolate 解析 {{ }} 管道)', () => { + expect(interpolate('{{name | upper}}', { data: { name: 'abc' } }).text).toBe('ABC') + expect(interpolate('{{name | lower}}', { data: { name: 'ABC' } }).text).toBe('abc') + expect(interpolate('{{v | number:0}}', { data: { v: 1234.5 } }).text).toBe('1,235') + expect(interpolate('{{amount | currency}}', { data: { amount: 12800.5 } }).text).toContain('12,800.50') + expect(interpolate('{{v | default:"无"}}', { data: { v: '' } }).text).toBe('无') + expect(interpolate('{{n | padStart:3}}', { data: { n: 7 } }).text).toBe('007') + }) + + it('resolveBinding 按路径取值', () => { + expect(resolveBinding('a.b', { data: { a: { b: 1 } } })).toBe(1) + }) + + it('interpolate 替换 {{ }}', () => { + expect(interpolate('{{x}}岁', { data: { x: 18 } }).text).toBe('18岁') + expect(interpolate('单价{{price|currency}}', { data: { price: 9.9 } }).text).toContain('9.90') + expect(interpolate('无占位', { data: {} }).text).toBe('无占位') + }) + + it('evaluateVisible 条件渲染', () => { + expect(evaluateVisible('', { data: {} })).toBe(true) + expect(evaluateVisible(' ', { data: {} })).toBe(true) + expect(evaluateVisible('false', { data: {} })).toBe(false) + expect(evaluateVisible('x > 3', { data: { x: 5 } })).toBe(true) + expect(evaluateVisible('x > 3', { data: { x: 1 } })).toBe(false) + }) + + // —— 安全边界:绝不能执行任意 JS —— + it('拒绝 eval / new Function / import 等代码注入', () => { + expect(() => evaluate('eval("x")', { data: {} })).toThrow() + expect(() => evaluate('new Function("return 1")', { data: {} })).toThrow() + expect(() => evaluate('import("x")', { data: {} })).toThrow() + expect(() => evaluate('window.location', { data: {} })).not.toThrow() // 读取未定义变量返回 undefined,而非执行 + expect(evaluate('window.location', { data: {} })).toBeUndefined() + }) + + it('语法错误抛出', () => { + expect(() => evaluate('1 +', { data: {} })).toThrow() + expect(() => evaluate('@#$', { data: {} })).toThrow() + }) +}) diff --git a/src/core/layout-engine/expression.ts b/src/core/layout-engine/expression.ts new file mode 100644 index 0000000..5740315 --- /dev/null +++ b/src/core/layout-engine/expression.ts @@ -0,0 +1,751 @@ +/** + * 表达式引擎 —— 绑定取值 / 插值 / 条件渲染的统一入口 + * 真理源:《OpenPrint-设计方案.md》§5.3(表达式文本)、§5.6(条件渲染)、§5.4(列 expression) + * + * ## 为什么不用 mustache? + * §11.2 建议用 mustache 做 `resolveBinding()`,但 mustache 是**逻辑无关**模板引擎: + * 它无法求值 `{{rowIndex + 1}}`、`{{row.price * row.qty}}`(§5.4 列表达式的原文示例), + * 也不支持 `{{order.total | currency:'CNY'}}` 的过滤器语法(§5.3 原文示例)。 + * 而 `new Function` / `eval` 方案在 ERP 场景不可接受(模板可能来自不可信的第三方设计者, + * 且企业环境常配 CSP `script-src` 白名单,eval 直接被拦)。 + * + * 因此这里实现一个**受限安全求值器**: + * - 纯 Pratt 解析 + 树求值,无 new Function / eval / with + * - 白名单运算符,无函数调用、无属性赋值、无原型访问(__proto__ / constructor 被拒) + * - 只读取传入的 EvalContext,够不到全局对象 + * 覆盖面严格 ⊇ mustache 的用途,且顺带解决 visibleIf,一个引擎服务三处。 + */ +import type { EvalContext } from './types' +import type { CellFormat } from '@/types/control' + +/* ================================ 词法分析 ================================ */ + +type TokenType = 'num' | 'str' | 'ident' | 'op' | 'eof' + +interface Token { + type: TokenType + value: string + pos: number +} + +/** 多字符运算符按长度降序,保证 `===` 不被切成 `==` + `=` */ +const OPERATORS = [ + '===', + '!==', + '==', + '!=', + '>=', + '<=', + '&&', + '||', + '??', + '+', + '-', + '*', + '/', + '%', + '>', + '<', + '!', + '?', + ':', + '(', + ')', + '[', + ']', + '.', + ',', +] + +function tokenize(src: string): Token[] { + const tokens: Token[] = [] + let i = 0 + while (i < src.length) { + const ch = src[i]! + + // 空白 + if (/\s/.test(ch)) { + i++ + continue + } + + // 字符串字面量(单/双引号,支持反斜杠转义) + if (ch === "'" || ch === '"') { + const quote = ch + let out = '' + let j = i + 1 + while (j < src.length && src[j] !== quote) { + if (src[j] === '\\' && j + 1 < src.length) { + out += src[j + 1] + j += 2 + } else { + out += src[j] + j++ + } + } + tokens.push({ type: 'str', value: out, pos: i }) + i = j + 1 + continue + } + + // 数字(含小数) + if (/[0-9]/.test(ch) || (ch === '.' && /[0-9]/.test(src[i + 1] ?? ''))) { + let j = i + while (j < src.length && /[0-9.]/.test(src[j]!)) j++ + tokens.push({ type: 'num', value: src.slice(i, j), pos: i }) + i = j + continue + } + + // 标识符(含中文字段名,ERP 里很常见) + if (/[A-Za-z_$\u4e00-\u9fa5]/.test(ch)) { + let j = i + while (j < src.length && /[A-Za-z0-9_$\u4e00-\u9fa5]/.test(src[j]!)) j++ + tokens.push({ type: 'ident', value: src.slice(i, j), pos: i }) + i = j + continue + } + + // 运算符 + const op = OPERATORS.find((o) => src.startsWith(o, i)) + if (op) { + tokens.push({ type: 'op', value: op, pos: i }) + i += op.length + continue + } + + throw new Error(`表达式非法字符 "${ch}"(位置 ${i})`) + } + tokens.push({ type: 'eof', value: '', pos: i }) + return tokens +} + +/* ================================ 语法分析 ================================ */ + +type Node = + | { t: 'lit'; v: unknown } + | { t: 'path'; segs: Array } + | { t: 'un'; op: string; a: Node } + | { t: 'bin'; op: string; a: Node; b: Node } + | { t: 'cond'; c: Node; a: Node; b: Node } + +/** 二元运算符优先级(数字越大结合越紧) */ +const BIN_PREC: Record = { + '??': 1, + '||': 2, + '&&': 3, + '===': 4, + '!==': 4, + '==': 4, + '!=': 4, + '<': 5, + '<=': 5, + '>': 5, + '>=': 5, + '+': 6, + '-': 6, + '*': 7, + '/': 7, + '%': 7, +} + +/** 禁止访问的属性名 —— 防原型链逃逸 */ +const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']) + +class Parser { + private pos = 0 + constructor(private readonly tokens: Token[]) {} + + private peek(): Token { + return this.tokens[this.pos]! + } + + private next(): Token { + return this.tokens[this.pos++]! + } + + private eat(value: string): boolean { + if (this.peek().type === 'op' && this.peek().value === value) { + this.pos++ + return true + } + return false + } + + private expect(value: string): void { + if (!this.eat(value)) { + throw new Error(`表达式缺少 "${value}"(位置 ${this.peek().pos})`) + } + } + + parse(): Node { + const node = this.parseTernary() + if (this.peek().type !== 'eof') { + throw new Error(`表达式多余内容 "${this.peek().value}"`) + } + return node + } + + /** 三元(右结合,优先级最低) */ + private parseTernary(): Node { + const cond = this.parseBinary(0) + if (this.eat('?')) { + const a = this.parseTernary() + this.expect(':') + const b = this.parseTernary() + return { t: 'cond', c: cond, a, b } + } + return cond + } + + /** 优先级爬升 */ + private parseBinary(minPrec: number): Node { + let left = this.parseUnary() + for (;;) { + const tok = this.peek() + if (tok.type !== 'op') break + const prec = BIN_PREC[tok.value] + if (prec === undefined || prec < minPrec) break + this.next() + const right = this.parseBinary(prec + 1) // 全部左结合 + left = { t: 'bin', op: tok.value, a: left, b: right } + } + return left + } + + private parseUnary(): Node { + const tok = this.peek() + if (tok.type === 'op' && (tok.value === '!' || tok.value === '-' || tok.value === '+')) { + this.next() + return { t: 'un', op: tok.value, a: this.parseUnary() } + } + return this.parsePrimary() + } + + private parsePrimary(): Node { + const tok = this.next() + + if (tok.type === 'num') return { t: 'lit', v: Number(tok.value) } + if (tok.type === 'str') return { t: 'lit', v: tok.value } + + if (tok.type === 'op' && tok.value === '(') { + const node = this.parseTernary() + this.expect(')') + return node + } + + if (tok.type === 'ident') { + if (tok.value === 'true') return { t: 'lit', v: true } + if (tok.value === 'false') return { t: 'lit', v: false } + if (tok.value === 'null') return { t: 'lit', v: null } + if (tok.value === 'undefined') return { t: 'lit', v: undefined } + return this.parsePath(tok.value) + } + + throw new Error(`表达式意外符号 "${tok.value}"(位置 ${tok.pos})`) + } + + /** 路径:a.b[0].c,以及协议里的数组标记 a[] */ + private parsePath(head: string): Node { + const segs: Array = [head] + for (;;) { + if (this.eat('.')) { + const tok = this.next() + if (tok.type !== 'ident' && tok.type !== 'num') { + throw new Error(`路径 "." 后应为字段名(位置 ${tok.pos})`) + } + segs.push(tok.value) + continue + } + if (this.eat('[')) { + // `items[]` —— 协议里表示"该数组本身",跳过不产生下标段 + if (this.eat(']')) continue + const idx = this.parseTernary() + this.expect(']') + segs.push(idx) + continue + } + break + } + return { t: 'path', segs } + } +} + +/* ================================= 求值 ================================= */ + +function looseEqual(a: unknown, b: unknown): boolean { + // 避免 eslint eqeqeq;语义等价于 ==(仅做数字/字符串宽松比较) + if (a === b) return true + if (a === null || a === undefined || b === null || b === undefined) { + return (a === null || a === undefined) && (b === null || b === undefined) + } + if (typeof a === 'number' || typeof b === 'number') return Number(a) === Number(b) + return String(a) === String(b) +} + +function toNum(v: unknown): number { + if (typeof v === 'number') return v + if (v === null || v === undefined || v === '') return 0 + const n = Number(v) + return Number.isNaN(n) ? 0 : n +} + +function isTruthy(v: unknown): boolean { + if (Array.isArray(v)) return v.length > 0 + return Boolean(v) +} + +function readProp(obj: unknown, key: string): unknown { + if (obj === null || obj === undefined) return undefined + if (FORBIDDEN_KEYS.has(key)) return undefined + if (typeof obj !== 'object' && typeof obj !== 'string') return undefined + // 数组的 length 允许;字符串的 length 允许 + return (obj as Record)[key] +} + +function evalNode(node: Node, ctx: EvalContext, scope?: Record): unknown { + switch (node.t) { + case 'lit': + return node.v + + case 'path': { + const [head, ...rest] = node.segs as [string, ...Array] + let cur: unknown = resolveRoot(head, ctx, scope) + for (const seg of rest) { + if (cur === null || cur === undefined) return undefined + const key = typeof seg === 'string' ? seg : String(evalNode(seg, ctx, scope)) + cur = readProp(cur, key) + } + return cur + } + + case 'un': { + const v = evalNode(node.a, ctx) + if (node.op === '!') return !isTruthy(v) + if (node.op === '-') return -toNum(v) + return toNum(v) + } + + case 'cond': + return isTruthy(evalNode(node.c, ctx)) ? evalNode(node.a, ctx) : evalNode(node.b, ctx) + + case 'bin': { + // 短路运算符先处理 + if (node.op === '&&') { + const a = evalNode(node.a, ctx, scope) + return isTruthy(a) ? evalNode(node.b, ctx, scope) : a + } + if (node.op === '||') { + const a = evalNode(node.a, ctx, scope) + return isTruthy(a) ? a : evalNode(node.b, ctx, scope) + } + if (node.op === '??') { + const a = evalNode(node.a, ctx, scope) + return a === null || a === undefined ? evalNode(node.b, ctx, scope) : a + } + + const a = evalNode(node.a, ctx, scope) + const b = evalNode(node.b, ctx, scope) + switch (node.op) { + case '+': + // 任一侧为字符串则拼接,符合直觉("单号-" + order.no) + if (typeof a === 'string' || typeof b === 'string') return String(a ?? '') + String(b ?? '') + return toNum(a) + toNum(b) + case '-': + return toNum(a) - toNum(b) + case '*': + return toNum(a) * toNum(b) + case '/': { + const d = toNum(b) + return d === 0 ? 0 : toNum(a) / d + } + case '%': { + const d = toNum(b) + return d === 0 ? 0 : toNum(a) % d + } + case '===': + return a === b + case '!==': + return a !== b + case '==': + return looseEqual(a, b) + case '!=': + return !looseEqual(a, b) + case '<': + return toNum(a) < toNum(b) + case '<=': + return toNum(a) <= toNum(b) + case '>': + return toNum(a) > toNum(b) + case '>=': + return toNum(a) >= toNum(b) + default: + throw new Error(`不支持的运算符 "${node.op}"`) + } + } + } +} + +/** + * 路径根解析。约定(与字段树 path 对齐): + * - `data` → 数据根(§5.6 的 `data.vip === true`) + * - `row` → 当前行;`rowIndex` → 行号;`page` / `pages` → 页码变量 + * - 其他标识符 → 先在 row 里找(表格单元格里 `qty` 即 `row.qty`),再退到数据根 + */ +function resolveRoot(head: string, ctx: EvalContext, scope?: Record): unknown { + // 自定义作用域(如合计表达式里的 sum/avg/rows/allRows)优先于数据根解析 + if (scope && head in scope) return readProp(scope, head) + switch (head) { + case 'data': + return ctx.data + case 'row': + return ctx.row + case 'rowIndex': + return ctx.rowIndex ?? 0 + // 页码变量的多种别名,模板设计者不必记忆唯一写法 + case 'page': + case 'pageNo': + case 'pageNumber': + return ctx.page ?? 1 + case 'pages': + case 'pageCount': + case 'totalPages': + return ctx.pages ?? 1 + default: + if (ctx.row && head in (ctx.row as object)) return readProp(ctx.row, head) + return readProp(ctx.data, head) + } +} + +/* =============================== 编译缓存 =============================== */ + +const astCache = new Map() + +function compile(src: string): Node { + const hit = astCache.get(src) + if (hit instanceof Error) throw hit + if (hit) return hit + try { + const ast = new Parser(tokenize(src)).parse() + astCache.set(src, ast) + return ast + } catch (e) { + const err = e instanceof Error ? e : new Error(String(e)) + astCache.set(src, err) + throw err + } +} + +/** + * 求值单个表达式(不含 {{}})。失败时抛错,由调用方转 warning。 + * @param scope 可选额外作用域(如自定义合计里的 sum/avg/rows/allRows),标识符解析时优先于数据根。 + */ +export function evaluate(src: string, ctx: EvalContext, scope?: Record): unknown { + return evalNode(compile(src), ctx, scope) +} + +/** 条件渲染判定(§5.6)。表达式为空视为可见;求值失败**保守可见**,避免静默丢内容。 */ +export function evaluateVisible(src: string | undefined, ctx: EvalContext): boolean { + if (!src || !src.trim()) return true + try { + return isTruthy(evaluate(src, ctx)) + } catch { + return true + } +} + +/* ================================ 过滤器 ================================ */ + +export type Filter = (value: unknown, ...args: unknown[]) => string + +const CURRENCY_SYMBOL: Record = { + CNY: '¥', + RMB: '¥', + USD: '$', + EUR: '€', + GBP: '£', + JPY: '¥', + HKD: 'HK$', +} + +function groupThousands(fixed: string): string { + const [int = '0', dec] = fixed.split('.') + const sign = int.startsWith('-') ? '-' : '' + const digits = sign ? int.slice(1) : int + const grouped = digits.replace(/\B(?=(\d{3})+(?!\d))/g, ',') + return sign + grouped + (dec ? `.${dec}` : '') +} + +function pad2(n: number): string { + return String(n).padStart(2, '0') +} + +function formatDate(value: unknown, pattern = 'YYYY-MM-DD'): string { + if (value === null || value === undefined || value === '') return '' + const d = value instanceof Date ? value : new Date(String(value)) + if (Number.isNaN(d.getTime())) return String(value) + const map: Record = { + YYYY: String(d.getFullYear()), + MM: pad2(d.getMonth() + 1), + DD: pad2(d.getDate()), + HH: pad2(d.getHours()), + mm: pad2(d.getMinutes()), + ss: pad2(d.getSeconds()), + } + return pattern.replace(/YYYY|MM|DD|HH|mm|ss/g, (k) => map[k] ?? k) +} + +export const FILTERS: Record = { + /** {{ order.total | currency:'CNY' }} → ¥12,800.50 */ + currency(value, code = 'CNY', digits = 2) { + const symbol = CURRENCY_SYMBOL[String(code).toUpperCase()] ?? '' + return symbol + groupThousands(toNum(value).toFixed(toNum(digits))) + }, + /** {{ items[].qty | number:0 }} → 1,234 */ + number(value, digits = 2) { + return groupThousands(toNum(value).toFixed(toNum(digits))) + }, + /** 不加千分位的定点小数 */ + fixed(value, digits = 2) { + return toNum(value).toFixed(toNum(digits)) + }, + /** {{ order.rate | percent:1 }} → 12.5% */ + percent(value, digits = 2) { + return `${(toNum(value) * 100).toFixed(toNum(digits))}%` + }, + /** {{ order.orderDate | date:'YYYY年MM月DD日' }} */ + date(value, pattern = 'YYYY-MM-DD') { + return formatDate(value, String(pattern)) + }, + upper(value) { + return String(value ?? '').toUpperCase() + }, + lower(value) { + return String(value ?? '').toLowerCase() + }, + /** 空值兜底:{{ customer.memo | default:'无' }} */ + default(value, fallback = '') { + const empty = value === null || value === undefined || value === '' + return empty ? String(fallback) : String(value) + }, + /** 超长截断:{{ productName | truncate:10 }} */ + truncate(value, len = 20, suffix = '…') { + const s = String(value ?? '') + const n = toNum(len) + return s.length > n ? s.slice(0, n) + String(suffix) : s + }, + /** 补零:{{ seq | padStart:3 }} → 007 */ + padStart(value, len = 2, fill = '0') { + return String(value ?? '').padStart(toNum(len), String(fill)) + }, +} + +/* ============================== 单元格数据格式 ============================== */ + +/** 小数位收敛到 [0, 6],避免 digits 乱填导致 toFixed 抛错 */ +function clampDigits(d: unknown): number { + const n = typeof d === 'number' ? d : Number(d) + if (!Number.isFinite(n)) return 2 + return Math.min(6, Math.max(0, Math.trunc(n))) +} + +/** + * 按 CellFormat 把原始值格式化为显示字符串。 + * 复用 date/number/currency 的同一套底层工具(formatDate / groupThousands / CURRENCY_SYMBOL), + * 与 `{{field | date:'...' }}` 过滤器输出完全一致 —— UI 选择即等价于手写对应过滤器。 + * fmt 为 undefined / kind='none'/'text' 时回落默认 stringify(保持原行为)。 + */ +export function formatCellValue(raw: unknown, fmt?: CellFormat | null): string { + if (!fmt || fmt.kind === 'none' || fmt.kind === 'text') return stringifyValue(raw) + const n = toNum(raw) + switch (fmt.kind) { + case 'date': + return formatDate(raw, fmt.pattern ?? 'YYYY-MM-DD') + case 'int': { + const s = n.toFixed(0) + return fmt.thousands === false ? s : groupThousands(s) + } + case 'decimal': { + const s = n.toFixed(clampDigits(fmt.digits ?? 2)) + return fmt.thousands === false ? s : groupThousands(s) + } + case 'currency': { + const sym = CURRENCY_SYMBOL[String(fmt.code ?? 'CNY').toUpperCase()] ?? '' + const s = n.toFixed(clampDigits(fmt.digits ?? 2)) + return sym + (fmt.thousands === false ? s : groupThousands(s)) + } + case 'percent': + return `${(n * 100).toFixed(clampDigits(fmt.digits ?? 2))}%` + default: + return stringifyValue(raw) + } +} + +/* ============================== 插值({{}}) ============================== */ + +/** + * 按单竖线切分过滤器管道,需跳过: + * - 字符串字面量内的 `|` + * - 逻辑或 `||` + */ +function splitPipes(src: string): string[] { + const parts: string[] = [] + let buf = '' + let quote: string | null = null + for (let i = 0; i < src.length; i++) { + const ch = src[i]! + if (quote) { + buf += ch + if (ch === '\\' && i + 1 < src.length) { + buf += src[i + 1] + i++ + } else if (ch === quote) { + quote = null + } + continue + } + if (ch === "'" || ch === '"') { + quote = ch + buf += ch + continue + } + if (ch === '|') { + if (src[i + 1] === '|') { + buf += '||' + i++ + continue + } + parts.push(buf) + buf = '' + continue + } + buf += ch + } + parts.push(buf) + return parts +} + +/** 解析 `filterName:arg1,arg2`,参数走同一套求值器(可用字面量与路径) */ +function applyFilter(spec: string, value: unknown, ctx: EvalContext): string { + const trimmed = spec.trim() + const colon = trimmed.indexOf(':') + const name = (colon === -1 ? trimmed : trimmed.slice(0, colon)).trim() + const filter = FILTERS[name] + if (!filter) throw new Error(`未知过滤器 "${name}"`) + + let args: unknown[] = [] + if (colon !== -1) { + const argSrc = trimmed.slice(colon + 1) + args = splitArgs(argSrc).map((a) => evaluate(a, ctx)) + } + return filter(value, ...args) +} + +/** 按逗号切分过滤器参数,跳过字符串与括号内的逗号 */ +function splitArgs(src: string): string[] { + const out: string[] = [] + let buf = '' + let quote: string | null = null + let depth = 0 + for (let i = 0; i < src.length; i++) { + const ch = src[i]! + if (quote) { + buf += ch + if (ch === '\\' && i + 1 < src.length) { + buf += src[i + 1] + i++ + } else if (ch === quote) quote = null + continue + } + if (ch === "'" || ch === '"') { + quote = ch + buf += ch + continue + } + if (ch === '(' || ch === '[') depth++ + if (ch === ')' || ch === ']') depth-- + if (ch === ',' && depth === 0) { + out.push(buf) + buf = '' + continue + } + buf += ch + } + if (buf.trim()) out.push(buf) + return out +} + +/** 值 → 显示字符串的默认规则(数字保留原样,null/undefined 显示空) */ +export function stringifyValue(v: unknown): string { + if (v === null || v === undefined) return '' + if (typeof v === 'number') return Number.isInteger(v) ? String(v) : String(v) + if (typeof v === 'boolean') return v ? '是' : '否' + if (v instanceof Date) return formatDate(v) + if (typeof v === 'object') return JSON.stringify(v) + return String(v) +} + +export interface InterpolateResult { + text: string + /** 求值失败的片段(转 warning,不打断渲染) */ + errors: string[] +} + +/** + * 插值:把 `{{ expr | filter:arg }}` 替换为求值结果(§5.3)。 + * 非 `{{}}` 部分原样保留;单个片段求值失败时输出空串并记录错误。 + */ +export function interpolate(template: string, ctx: EvalContext): InterpolateResult { + const errors: string[] = [] + if (!template.includes('{{')) return { text: template, errors } + + const text = template.replace(/\{\{([\s\S]*?)\}\}/g, (_m, raw: string) => { + const body = raw.trim() + if (!body) return '' + try { + const [exprSrc, ...filterSpecs] = splitPipes(body) + let value: unknown = evaluate((exprSrc ?? '').trim(), ctx) + if (filterSpecs.length === 0) return stringifyValue(value) + let out = '' + for (let i = 0; i < filterSpecs.length; i++) { + out = applyFilter(filterSpecs[i]!, value, ctx) + value = out + } + return out + } catch (e) { + errors.push(`{{${body}}}:${e instanceof Error ? e.message : String(e)}`) + return '' + } + }) + + return { text, errors } +} + +/** + * 绑定路径取值(§5.2 的 `binding` 字段)。 + * 字段树给出的路径形如 `items[].qty`,其中 `[]` 由解析器忽略, + * 因此在行上下文中 `items[].qty` 与 `qty` 等价 —— 用户从字段树直接拖入即可用。 + */ +export function resolveBinding(path: string, ctx: EvalContext): unknown { + if (!path) return undefined + // 行上下文里优先按"去前缀"解释:items[].qty → qty + if (ctx.row) { + const tail = path.includes('[].') ? path.slice(path.indexOf('[].') + 3) : path + try { + const v = evaluate(tail, { ...ctx }) + if (v !== undefined) return v + } catch { + /* 落到全局解析 */ + } + } + try { + return evaluate(path, ctx) + } catch { + return undefined + } +} + +/** 测试/调试用:清空 AST 缓存 */ +export function __clearExpressionCache(): void { + astCache.clear() +} diff --git a/src/core/layout-engine/group-engine.spec.ts b/src/core/layout-engine/group-engine.spec.ts new file mode 100644 index 0000000..f2a8607 --- /dev/null +++ b/src/core/layout-engine/group-engine.spec.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest' + +import { computeAggregates, subtotalLabel } from './group-engine' +import type { TableControl } from '@/types/control' + +const COLS_WITH_AMOUNT = [{ field: 'amount', title: '金额', width: 30 }] + +describe('computeAggregates —— 自定义合计(type=custom)', () => { + it('字段专属表达式:引用预计算的 sum', () => { + const control = { + columns: COLS_WITH_AMOUNT, + options: { + summaryRow: { + type: 'custom' as const, + fields: ['amount'], + expressions: { amount: 'sum.amount * 2' }, + }, + }, + } as unknown as TableControl + const out = computeAggregates([{ amount: 10 }, { amount: 20 }], control) + expect(out.amount).toBe(60) // (10+20)*2 + }) + + it('无专属表达式时回退到 summaryRow.expression', () => { + const control = { + columns: COLS_WITH_AMOUNT, + options: { + summaryRow: { type: 'custom' as const, fields: ['amount'], expression: 'sum.amount + 5' }, + }, + } as unknown as TableControl + const out = computeAggregates([{ amount: 10 }, { amount: 20 }], control) + expect(out.amount).toBe(35) // 30+5 + }) + + it('avg 作用域可用', () => { + const control = { + columns: COLS_WITH_AMOUNT, + options: { + summaryRow: { type: 'custom' as const, fields: ['amount'], expressions: { amount: 'avg.amount' } }, + }, + } as unknown as TableControl + const out = computeAggregates([{ amount: 10 }, { amount: 20 }, { amount: 30 }], control) + expect(out.amount).toBe(20) + }) + + it('作用域含 rows / allRows(分组内行 / 整表行)', () => { + const control = { + columns: COLS_WITH_AMOUNT, + options: { + summaryRow: { + type: 'custom' as const, + fields: ['amount'], + expressions: { amount: 'rows.length * 10 + allRows.length' }, + }, + }, + } as unknown as TableControl + // 当前分组 2 行,整表 3 行 → 2*10 + 3 = 23 + const out = computeAggregates( + [{ amount: 1 }, { amount: 2 }], + control, + [{ amount: 1 }, { amount: 2 }, { amount: 3 }], + ) + expect(out.amount).toBe(23) + }) + + it('无聚合列时把单表达式结果落入占位键 __total__', () => { + const control = { + columns: COLS_WITH_AMOUNT, + options: { + summaryRow: { type: 'custom' as const, fields: [], expression: 'sum.amount + 1' }, + }, + } as unknown as TableControl + const out = computeAggregates([{ amount: 9 }], control) + expect(out.__total__).toBe(10) + }) +}) + +describe('subtotalLabel —— 模板替换', () => { + it('默认「${key} 小计」', () => { + const control = { columns: [], options: {} } as unknown as TableControl + expect(subtotalLabel(control, 'A组')).toBe('A组 小计') + }) + + it('自定义模板替换 ${key}', () => { + const control = { + columns: [], + options: { summaryRow: { subtotalLabel: '${key} 分组小计' } }, + } as unknown as TableControl + expect(subtotalLabel(control, '甲')).toBe('甲 分组小计') + }) +}) diff --git a/src/core/layout-engine/group-engine.ts b/src/core/layout-engine/group-engine.ts new file mode 100644 index 0000000..0fcc020 --- /dev/null +++ b/src/core/layout-engine/group-engine.ts @@ -0,0 +1,213 @@ +/** + * GroupEngine —— 分组与统计 + * 真理源:《OpenPrint-设计方案.md》§5.5(分组统计)、§5.4(summaryRow) + * + * 产物是「行计划」(RowPlan[]):一个把 组头 / 数据行 / 小计 / 总计 编排好的线性序列。 + * 之所以先出行计划再交给 TableEngine 测量,是为了让"分组语义"和"高度/分页"彻底解耦: + * 分页时只需按 RowPlan 顺序切,不必再理解分组结构。 + */ +import type { TableControl } from '@/types/control' +import { evaluate, resolveBinding, stringifyValue } from './expression' +import type { EvalContext, RenderRowKind } from './types' + +export type RowPlanKind = Extract + +export interface RowPlan { + kind: RowPlanKind + /** data 行的原始数据对象 */ + row?: Record + /** 在原始数组中的下标(斑马纹/序号用,跨分组连续) */ + dataIndex?: number + /** group 行的分组标题 / subtotal|summary 行的标签 */ + label?: string + /** 聚合结果:字段名 → 数值(供 subtotal / summary 行按列填值) */ + aggregates?: Record +} + +/* -------------------------------- 聚合 -------------------------------- */ + +function toNumber(v: unknown): number { + if (typeof v === 'number') return v + if (v === null || v === undefined || v === '') return 0 + const n = Number(v) + return Number.isNaN(n) ? 0 : n +} + +/** 从行对象里按字段名取值,兼容 `items[].amount` 这类带前缀的路径 */ +function readField(row: Record, field: string): unknown { + if (field in row) return row[field] + const tail = field.includes('[].') ? field.slice(field.indexOf('[].') + 3) : field + if (tail in row) return row[tail] + // 支持 a.b 嵌套 + return tail.split('.').reduce((acc, k) => { + if (acc === null || acc === undefined || typeof acc !== 'object') return undefined + return (acc as Record)[k] + }, row) +} + +export function aggregate( + rows: Array>, + type: 'sum' | 'count', + field: string, +): number { + if (type === 'count') return rows.length + return rows.reduce((sum, r) => sum + toNumber(readField(r, field)), 0) +} + +/** + * 计算一批行在若干字段上的聚合值。 + * 字段清单来自 `control.summary` 与 `options.summaryRow.fields` 的并集。 + * @param allRows 整表(跨分组)行,供 type='custom' 的表达式引用 `allRows`。 + */ +export function computeAggregates( + rows: Array>, + control: TableControl, + allRows?: Array>, +): Record { + const out: Record = {} + + for (const s of control.summary ?? []) { + out[s.field] = aggregate(rows, s.type, s.field) + } + + const sr = control.options?.summaryRow + if (!sr || sr.type === 'custom') { + // custom 类型在下方单独处理(sr 为 undefined 时也不走 sum/count 分支) + } else { + for (const f of sr.fields ?? []) { + out[f] = aggregate(rows, sr.type, f) + } + return out + } + + if (!sr) return out + + // ── type='custom':用自定义表达式求值,作用域含预计算的 sum/avg 与原始行 ── + const sum: Record = {} + const avg: Record = {} + for (const col of control.columns ?? []) { + const f = col.field + if (f) { + sum[f] = aggregate(rows, 'sum', f) + avg[f] = rows.length ? sum[f] / rows.length : 0 + } + } + const scope = { sum, avg, rows, allRows: allRows ?? rows } + const fields = sr.fields ?? [] + const evalCtx: EvalContext = { data: {} } + if (fields.length === 0) { + // 无聚合列:把单表达式结果放进占位列(渲染时标签列之前呈现) + try { + out['__total__'] = toNumber(evaluate(sr.expression ?? '0', evalCtx, scope)) + } catch { + /* 表达式错误时静默留 0,由调用方 warning 兜底 */ + } + } + for (const f of fields) { + const expr = sr.expressions?.[f] ?? sr.expression ?? '0' + try { + out[f] = toNumber(evaluate(expr, evalCtx, scope)) + } catch { + out[f] = 0 + } + } + return out +} + +/** 是否配置了任何合计行 */ +export function hasSummaryRow(control: TableControl): boolean { + const sr = control.options?.summaryRow + if (sr) return true + return (control.summary?.length ?? 0) > 0 +} + +/** 合计行标签(默认「合计」) */ +export function summaryLabel(control: TableControl): string { + return control.options?.summaryRow?.label ?? control.summary?.[0]?.label ?? '合计' +} + +/** 分组小计标签(默认「${key} 小计」,`${key}` 替换为分组值) */ +export function subtotalLabel(control: TableControl, key: string): string { + const tpl = control.options?.summaryRow?.subtotalLabel + return tpl ? tpl.replace(/\$\{key\}/g, key) : `${key} 小计` +} + +/* ------------------------------- 空行判定 ------------------------------- */ + +/** §5.4 skipEmptyRows:一行里所有被引用字段都为空则视为空行 */ +export function isEmptyRow(row: Record, fields: string[]): boolean { + if (fields.length === 0) return false + return fields.every((f) => { + const v = readField(row, f) + return v === null || v === undefined || v === '' + }) +} + +/* ------------------------------ 行计划编排 ------------------------------ */ + +export interface PlanOptions { + control: TableControl + rows: Array> + ctx: EvalContext +} + +/** + * 生成行计划。 + * + * 无 groupBy:`[data, data, ..., summary?]` + * 有 groupBy:`[group, data..., subtotal, group, data..., subtotal, ..., summary?]` + * + * 分组保持**首次出现顺序**(不重排),符合 ERP 单据"按录入顺序打印"的预期。 + */ +export function planRows({ control, rows, ctx }: PlanOptions): RowPlan[] { + const plans: RowPlan[] = [] + const groupBy = control.groupBy?.trim() + + if (!groupBy) { + rows.forEach((row, i) => plans.push({ kind: 'data', row, dataIndex: i })) + } else { + const buckets = new Map; index: number }>>() + rows.forEach((row, i) => { + const raw = resolveBinding(groupBy, { ...ctx, row, rowIndex: i }) + const key = stringifyValue(raw) + const list = buckets.get(key) + if (list) list.push({ row, index: i }) + else buckets.set(key, [{ row, index: i }]) + }) + + for (const [key, items] of buckets) { + plans.push({ kind: 'group', label: key || '(未分组)' }) + for (const { row, index } of items) { + plans.push({ kind: 'data', row, dataIndex: index }) + } + if (hasSummaryRow(control)) { + plans.push({ + kind: 'subtotal', + label: subtotalLabel(control, key || '(未分组)'), + aggregates: computeAggregates( + items.map((i) => i.row), + control, + rows, + ), + }) + } + } + } + + return plans +} + +/** 全表总计行(渲染为表尾,参与 repeatFooter 逻辑) */ +export function buildSummaryPlan( + rows: Array>, + control: TableControl, +): RowPlan | null { + if (!hasSummaryRow(control)) return null + return { + kind: 'summary', + label: summaryLabel(control), + aggregates: computeAggregates(rows, control, rows), + } +} + +export { readField } diff --git a/src/core/layout-engine/header-footer.ts b/src/core/layout-engine/header-footer.ts new file mode 100644 index 0000000..1278a65 --- /dev/null +++ b/src/core/layout-engine/header-footer.ts @@ -0,0 +1,117 @@ +/** + * HeaderFooterEngine + 页面度量 + * 真理源:《OpenPrint-设计方案.md》§5.1(sections)、§5.13(orientation 交换)、§5.14(ZoneControl)、§7.2(每页注入) + * + * 两件事: + * 1. computePageMetrics —— 把协议里的纸张定义换算成引擎唯一的尺寸真理源(全 mm) + * 2. buildSectionControls —— 把 header/footer 的子组件解析成每页可复用的已定位控件 + * + * 页码变量在**每页单独求值**(`{{page}} / {{pages}}`),所以页眉页脚不能只解析一次就复用, + * 必须按页重算 —— 这是"第 X 页 / 共 Y 页"能正确输出的关键。 + */ +import type { AnyControl } from '@/types/control' +import type { PageSetup, Section } from '@/types/template' +import { toMm } from '@/core/units' +import { isControlPrintable, resolveControlContent } from './data-binder' +import type { EvalContext, PageMetrics, PlacedControl, RenderWarning } from './types' + +/* ------------------------------- 页面度量 ------------------------------- */ + +/** + * 计算一页的可用区域。 + * 【约定】PageSetup.width/height 是**物理尺寸**(实际打印宽高,横向时宽>高), + * orientation 仅是派生标志(设计器面板切换方向时已交换宽高)。 + * 这里**不再**按 orientation 交换 —— 与设计器 setPage、导出 export-pdf(w>h 判定)同口径。 + */ +export function computePageMetrics( + page: PageSetup, + headerSection?: Section, + footerSection?: Section, +): PageMetrics { + const unit = page.unit + const pageWidth = toMm(page.width, unit) + const pageHeight = toMm(page.height, unit) + + const margin = { + top: toMm(page.margin?.top ?? 0, unit), + right: toMm(page.margin?.right ?? 0, unit), + bottom: toMm(page.margin?.bottom ?? 0, unit), + left: toMm(page.margin?.left ?? 0, unit), + } + + const contentWidth = Math.max(0, pageWidth - margin.left - margin.right) + const contentHeight = Math.max(0, pageHeight - margin.top - margin.bottom) + + const headerHeight = headerSection ? toMm(headerSection.height ?? 0, unit) : 0 + const footerHeight = footerSection ? toMm(footerSection.height ?? 0, unit) : 0 + const bodyHeight = Math.max(0, contentHeight - headerHeight - footerHeight) + + return { + pageWidth, + pageHeight, + margin, + contentWidth, + contentHeight, + headerHeight, + footerHeight, + bodyHeight, + } +} + +/* ------------------------------ 区块控件构建 ----------------------------- */ + +/** + * 把一个 section 的子组件解析成已定位控件。 + * 坐标已经是相对区块左上角(设计器序列化时保证),这里原样透传。 + */ +export async function buildSectionControls( + components: AnyControl[] | undefined, + ctx: EvalContext, + unit: PageSetup['unit'], +): Promise<{ controls: PlacedControl[]; warnings: RenderWarning[] }> { + const controls: PlacedControl[] = [] + const warnings: RenderWarning[] = [] + if (!components?.length) return { controls, warnings } + + for (const control of components) { + // zone 不应出现在 section.components 里(设计器已拆平),保险起见跳过 + if (control.type === 'zone') continue + if (!isControlPrintable(control, ctx)) continue + + const { content, warnings: w } = await resolveControlContent(control, ctx) + warnings.push(...w) + controls.push({ + kind: 'control', + id: control.id, + left: toMm(control.left, unit), + top: toMm(control.top, unit), + width: toMm(control.width, unit), + height: toMm(control.height, unit), + angle: control.angle, + content, + control, + }) + } + + return { controls, warnings } +} + +/* ------------------------------- 出现规则 ------------------------------- */ + +/** + * 区块是否在该页出现。 + * §5.1:`repeat` 默认 true(每页重复)。repeat=false 时按业界惯例: + * - 页眉只出现在**首页**(封面式抬头) + * - 页脚只出现在**末页**(签章/合计式收尾) + */ +export function shouldRenderSection( + section: Section | undefined, + type: 'header' | 'footer', + pageIndex: number, + totalPages: number, +): boolean { + if (!section) return false + const repeat = section.repeat ?? true + if (repeat) return true + return type === 'header' ? pageIndex === 0 : pageIndex === totalPages - 1 +} diff --git a/src/core/layout-engine/measure.ts b/src/core/layout-engine/measure.ts new file mode 100644 index 0000000..aef6224 --- /dev/null +++ b/src/core/layout-engine/measure.ts @@ -0,0 +1,231 @@ +/** + * 文本测量 —— 分页精度的关键模块 + * 真理源:《OpenPrint-设计方案.md》§11.3 + * + * | 环境 | 方案 | 精度 | + * |---|---|---| + * | 浏览器 | 真实 DOM 排版(本文件 DomMeasurer) | 像素精确,与最终打印一致 | + * | 无 DOM | CJK 感知估算(EstimateMeasurer) | 近似,仅作兜底 | + * + * §11.3 特别警告:**不要用 jsdom 估算** —— jsdom 不实现 CSS 布局引擎, + * getBoundingClientRect 永远返回 0,任何基于它的测量都是假精度。 + * 因此这里的兜底走字符宽度估算,而不是假装有 DOM。 + */ +import { mmToPx, ptToPx, pxToMm } from '@/core/units' + +export interface TextMeasureOptions { + /** 字号(pt,协议单位) */ + fontSize: number + fontFamily?: string + fontWeight?: 'normal' | 'bold' + /** 行高倍率(默认 1.35,贴近打印排版习惯) */ + lineHeight?: number + /** 字间距(pt) */ + letterSpacing?: number + /** 可用宽度(mm);<=0 表示不换行 */ + widthMm: number + /** 是否允许自动换行(默认 true) */ + wrap?: boolean +} + +export interface TextMeasureResult { + /** 文本占用高度(mm) */ + heightMm: number + /** 实际折行数 */ + lines: number +} + +export interface TextMeasurer { + measure(text: string, options: TextMeasureOptions): TextMeasureResult + /** 释放底层资源(DOM 节点等) */ + dispose(): void +} + +export const DEFAULT_FONT_FAMILY = + '"Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", sans-serif' +export const DEFAULT_LINE_HEIGHT = 1.35 + +function cacheKey(text: string, o: TextMeasureOptions): string { + return [ + text, + o.fontSize, + o.fontFamily ?? '', + o.fontWeight ?? 'normal', + o.lineHeight ?? DEFAULT_LINE_HEIGHT, + o.letterSpacing ?? 0, + Math.round(o.widthMm * 100), + o.wrap === false ? 0 : 1, + ].join('|') +} + +/* ============================ 浏览器:真实 DOM ============================ */ + +/** + * 用一个离屏隐藏容器做真实排版测量。 + * + * 关键点: + * - 用 `visibility:hidden` + 绝对定位移出视口,**不能用 `display:none`**(不排版,高度恒 0) + * - `contain: layout size style` 让浏览器把它当独立布局单元,避免污染主文档回流 + * - `white-space: pre-wrap` + `word-break: break-word` 与渲染器输出的 CSS 严格一致, + * 否则"测量用一套规则、渲染用另一套"会导致分页错位 + */ +class DomMeasurer implements TextMeasurer { + private host: HTMLDivElement + private cache = new Map() + + constructor() { + const host = document.createElement('div') + host.setAttribute('aria-hidden', 'true') + host.dataset.openprint = 'measure' + Object.assign(host.style, { + position: 'absolute', + left: '-99999px', + top: '0', + visibility: 'hidden', + pointerEvents: 'none', + contain: 'layout size style', + padding: '0', + margin: '0', + border: '0', + } satisfies Partial) + document.body.appendChild(host) + this.host = host + } + + measure(text: string, options: TextMeasureOptions): TextMeasureResult { + const key = cacheKey(text, options) + const hit = this.cache.get(key) + if (hit) return hit + + const lineHeight = options.lineHeight ?? DEFAULT_LINE_HEIGHT + const fontPx = ptToPx(options.fontSize) + const el = this.host + + el.style.fontSize = `${fontPx}px` + el.style.fontFamily = options.fontFamily || DEFAULT_FONT_FAMILY + el.style.fontWeight = options.fontWeight ?? 'normal' + el.style.lineHeight = String(lineHeight) + el.style.letterSpacing = options.letterSpacing ? `${ptToPx(options.letterSpacing)}px` : 'normal' + + if (options.wrap === false || options.widthMm <= 0) { + el.style.width = 'auto' + el.style.whiteSpace = 'pre' + } else { + el.style.width = `${mmToPx(options.widthMm)}px` + el.style.whiteSpace = 'pre-wrap' + el.style.wordBreak = 'break-word' + el.style.overflowWrap = 'break-word' + } + + // 空串也要占一行高度,否则空单元格会把行压扁 + el.textContent = text === '' ? '\u00A0' : text + + const heightPx = el.getBoundingClientRect().height + const singleLinePx = fontPx * lineHeight + const result: TextMeasureResult = { + heightMm: pxToMm(heightPx), + lines: Math.max(1, Math.round(heightPx / Math.max(1, singleLinePx))), + } + + // 缓存上限,避免超长表格把内存吃满 + if (this.cache.size > 5000) this.cache.clear() + this.cache.set(key, result) + return result + } + + dispose(): void { + this.cache.clear() + this.host.remove() + } +} + +/* ============================== 无 DOM 兜底 ============================== */ + +/** + * 字符宽度估算:CJK / 全角按 1em,ASCII 按 0.5em(等宽近似)。 + * 精度不如 DOM,但至少不会像 jsdom 那样返回 0。 + */ +class EstimateMeasurer implements TextMeasurer { + private cache = new Map() + + private static charWidthEm(ch: string): number { + const code = ch.codePointAt(0) ?? 0 + // CJK 统一表意文字 / 全角标点 / 假名 / 韩文 + if ( + (code >= 0x1100 && code <= 0x115f) || + (code >= 0x2e80 && code <= 0xa4cf) || + (code >= 0xac00 && code <= 0xd7a3) || + (code >= 0xf900 && code <= 0xfaff) || + (code >= 0xfe30 && code <= 0xfe6f) || + (code >= 0xff00 && code <= 0xff60) || + (code >= 0xffe0 && code <= 0xffe6) + ) { + return 1 + } + return 0.5 + } + + measure(text: string, options: TextMeasureOptions): TextMeasureResult { + const key = cacheKey(text, options) + const hit = this.cache.get(key) + if (hit) return hit + + const lineHeight = options.lineHeight ?? DEFAULT_LINE_HEIGHT + const fontPx = ptToPx(options.fontSize) + const letterPx = options.letterSpacing ? ptToPx(options.letterSpacing) : 0 + const availPx = options.wrap === false || options.widthMm <= 0 ? Infinity : mmToPx(options.widthMm) + + let lines = 1 + let cursor = 0 + for (const ch of text) { + if (ch === '\n') { + lines++ + cursor = 0 + continue + } + const w = EstimateMeasurer.charWidthEm(ch) * fontPx + letterPx + if (cursor + w > availPx && cursor > 0) { + lines++ + cursor = w + } else { + cursor += w + } + } + + const result: TextMeasureResult = { + heightMm: pxToMm(lines * fontPx * lineHeight), + lines, + } + if (this.cache.size > 5000) this.cache.clear() + this.cache.set(key, result) + return result + } + + dispose(): void { + this.cache.clear() + } +} + +/* ================================ 工厂 ================================ */ + +let shared: TextMeasurer | null = null + +/** 创建测量器:有 DOM 走真实排版,无 DOM 走估算 */ +export function createMeasurer(): TextMeasurer { + const hasDom = typeof document !== 'undefined' && typeof document.body !== 'undefined' + return hasDom ? new DomMeasurer() : new EstimateMeasurer() +} + +/** + * 全局共享测量器(复用 DOM 节点 + 缓存,多次渲染成本近似 0)。 + * 渲染流程用这个;单测想要干净环境时自行 createMeasurer()。 + */ +export function getSharedMeasurer(): TextMeasurer { + if (!shared) shared = createMeasurer() + return shared +} + +export function disposeSharedMeasurer(): void { + shared?.dispose() + shared = null +} diff --git a/src/core/layout-engine/pagination-engine.spec.ts b/src/core/layout-engine/pagination-engine.spec.ts new file mode 100644 index 0000000..529b295 --- /dev/null +++ b/src/core/layout-engine/pagination-engine.spec.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest' + +import { layout } from '@/core/layout-engine/pagination-engine' +import { renderHtml } from '@/core/renderer-html' +import { createDemoTemplate } from '@/repository/mock/data/demo-template' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +function makeData(rows: number): Record { + const items = Array.from({ length: rows }, (_, i) => { + const qty = (i % 5) + 1 + const price = 10 + (i % 9) + return { + productCode: `P${String(i + 1).padStart(4, '0')}`, + productName: `商品${i + 1}`, + spec: '规格A', + unit: '件', + qty, + price, + amount: qty * price, + } + }) + return { + order: { orderNo: 'SO-2026-0001', orderDate: '2026-08-08' }, + customer: { name: '演示客户' }, + items, + } +} + +describe('layout 分页引擎(集成 demo 模板)', () => { + const template = createDemoTemplate() + const measurer = createCjkMeasurer() + + for (const rows of [30, 80]) { + it(`明细 ${rows} 行 → 多页,页眉页脚每页重复,合计仅末页`, async () => { + const result = await layout(template, makeData(rows), { measurer }) + const pages = result.pages.length + expect(pages).toBeGreaterThanOrEqual(2) + + const html = renderHtml(result, { screen: false }) + // 样式表不参与计数 + const body = html.replace(//g, '') + + // 页眉页脚每页重复 + expect((body.match(/op-section op-header/g) || []).length).toBe(pages) + expect((body.match(/op-section op-footer/g) || []).length).toBe(pages) + + // 表格按页切片:每页一个 + expect((body.match(/op-node op-table/g) || []).length).toBe(pages) + + // 合计行仅一次(末页) + expect((body.match(/is-summary/g) || []).length).toBe(1) + + // 零告警(数据完整、绑定可达) + expect(result.warnings.length).toBe(0) + }) + } +}) diff --git a/src/core/layout-engine/pagination-engine.ts b/src/core/layout-engine/pagination-engine.ts new file mode 100644 index 0000000..04632e0 --- /dev/null +++ b/src/core/layout-engine/pagination-engine.ts @@ -0,0 +1,572 @@ +/** + * PaginationEngine —— 分页算法主流程 + * 真理源:《OpenPrint-设计方案.md》§7.2(分页算法流程)、§7.4(难点清单)、§8.1(二维排版,非流式) + * + * ## 分页模型:绝对定位 + 单表流动 + * + * OpenPrint 是**二维排版工具,不是网页编辑器**(§8.1):正文控件都是相对区块左上角的 + * 绝对定位盒子,本身不参与文档流。真正会"长高"的只有绑定了数据源的表格。 + * 因此分页的语义是: + * + * ``` + * ┌── 表格上方控件(单据头:单号/客户/日期) → 仅第 1 页 + * ├── 明细表格 → 按行切片流过 N 页 + * └── 表格下方控件(单据尾:合计/签章) → 仅最后一页,随表格实际高度下移 + * ``` + * + * 这正是 ERP 单据的真实结构,也是金蝶/帆软等报表工具的一致做法。 + * 每页重复的内容(公司抬头、页码)交给 header/footer 区块(§5.14),不在正文里解决。 + * + * 无表格时退化为「按位置切页」:控件按 top 落在第几个 bodyHeight 区间就进第几页, + * 保证超长静态模板也不会被截断。 + */ +import type { AnyControl, TableControl } from '@/types/control' +import type { PageSetup, Section, TemplateData } from '@/types/template' +import { toMm } from '@/core/units' +import { isControlPrintable, resolveControlContent } from './data-binder' +import { + buildSectionControls, + computePageMetrics, + shouldRenderSection, +} from './header-footer' +import { getSharedMeasurer, type TextMeasurer } from './measure' +import { + buildTableModel, + minStartHeight, + sliceTable, + totalTableHeight, + type TableModel, +} from './table-engine' +import { isDataTable } from './table-cells' +import { + MAX_PAGES, + type EvalContext, + type LayoutPage, + type LayoutResult, + type PageMetrics, + type PlacedControl, + type PlacedNode, + type PlacedTable, + type RenderWarning, +} from './types' + +/** 几何比较容差(mm),消除序列化四舍五入带来的边界抖动 */ +const EPS = 0.5 + +export interface LayoutOptions { + /** 自定义文本测量器(默认用共享的 DOM 测量器) */ + measurer?: TextMeasurer + /** 最大页数保护 */ + maxPages?: number +} + +/* ------------------------------ 正文结构分析 ------------------------------ */ + +interface BodyPlan { + /** 驱动分页的流式表格(绑定了数据源) */ + flowTable: TableControl | null + /** 表格上方控件 → 第 1 页 */ + above: AnyControl[] + /** 与表格垂直重叠的控件 → 第 1 页原位 */ + overlap: AnyControl[] + /** 表格下方控件 → 末页,随表格高度下移 */ + below: AnyControl[] +} + +function analyzeBody(components: AnyControl[], unit: PageSetup['unit']): { + plan: BodyPlan + warnings: RenderWarning[] +} { + const warnings: RenderWarning[] = [] + const tables = components.filter( + (c): c is TableControl => c.type === 'table' && isDataTable(c), + ) + + if (tables.length > 1) { + warnings.push({ + code: 'CONTENT_OVERFLOW', + message: `正文存在 ${tables.length} 个数据表格,仅第一个(${tables[0]!.id})参与分页流动,其余按静态控件渲染`, + controlId: tables[1]!.id, + }) + } + + const flowTable = tables[0] ?? null + if (!flowTable) { + return { plan: { flowTable: null, above: components, overlap: [], below: [] }, warnings } + } + + const tTop = toMm(flowTable.top, unit) + const tBottom = tTop + toMm(flowTable.height, unit) + + const above: AnyControl[] = [] + const overlap: AnyControl[] = [] + const below: AnyControl[] = [] + + for (const c of components) { + if (c.id === flowTable.id) continue + const top = toMm(c.top, unit) + const bottom = top + toMm(c.height, unit) + if (bottom <= tTop + EPS) above.push(c) + else if (top >= tBottom - EPS) below.push(c) + else overlap.push(c) + } + + return { plan: { flowTable, above, overlap, below }, warnings } +} + +/* ------------------------------- 控件定位 ------------------------------- */ + +async function placeControls( + components: AnyControl[], + ctx: EvalContext, + unit: PageSetup['unit'], + topShift = 0, +): Promise<{ placed: PlacedControl[]; warnings: RenderWarning[] }> { + const placed: PlacedControl[] = [] + const warnings: RenderWarning[] = [] + + for (const control of components) { + if (control.type === 'zone') continue + if (!isControlPrintable(control, ctx)) continue + + // 静态表格(布局网格 / 非流动表格)在这里整体渲染 + if (control.type === 'table') continue + + const { content, warnings: w } = await resolveControlContent(control, ctx) + warnings.push(...w) + placed.push({ + kind: 'control', + id: control.id, + left: toMm(control.left, unit), + top: toMm(control.top, unit) + topShift, + width: toMm(control.width, unit), + height: toMm(control.height, unit), + angle: control.angle, + content, + control, + }) + } + + return { placed, warnings } +} + +/** 静态表格(无数据源或非首个数据表格)整表渲染,不参与分页 */ +function placeStaticTables( + components: AnyControl[], + flowTableId: string | null, + ctx: EvalContext, + unit: PageSetup['unit'], + measurer: TextMeasurer, + topShift = 0, +): { placed: PlacedTable[]; warnings: RenderWarning[] } { + const placed: PlacedTable[] = [] + const warnings: RenderWarning[] = [] + + for (const control of components) { + if (control.type !== 'table' || control.id === flowTableId) continue + if (!isControlPrintable(control, ctx)) continue + + const widthMm = toMm(control.width, unit) + const heightMm = toMm(control.height, unit) + const model = buildTableModel({ control, ctx, measurer, widthMm, heightMm }) + warnings.push(...model.warnings) + placed.push({ + kind: 'table', + id: control.id, + left: toMm(control.left, unit), + top: toMm(control.top, unit) + topShift, + width: widthMm, + height: Math.max(heightMm, totalTableHeight(model)), + control, + columns: model.columns, + columnWidths: model.columnWidths, + headerRows: model.headerRows, + rows: model.rows, + footerRows: model.footerRows, + isLastSlice: true, + }) + } + + return { placed, warnings } +} + +/* -------------------------------- 分页骨架 ------------------------------- */ + +/** 表格切片在各页上的落位结果(尚未解析上下方控件) */ +interface TableSkeleton { + /** 每页一个切片;空数组表示表格无内容 */ + slices: Array<{ table: PlacedTable; pageIndex: number }> + /** 表格实际占用的页数 */ + pageCount: number + /** 末页上表格的实际底边(mm,相对正文区顶) */ + lastBottom: number + warnings: RenderWarning[] +} + +function paginateFlowTable( + model: TableModel, + tableLeft: number, + tableWidth: number, + tableTop: number, + metrics: PageMetrics, + reserveBelow: number, + maxPages: number, +): TableSkeleton { + const warnings: RenderWarning[] = [] + const slices: TableSkeleton['slices'] = [] + const keepTogether = model.control.options?.keepTogether ?? false + + // keepTogether:首页放不下"表头 + 1 行 + 表尾"就整体下移到第 2 页(§7.4) + let firstPageIndex = 0 + let firstTop = tableTop + if (keepTogether && metrics.bodyHeight - tableTop < minStartHeight(model)) { + firstPageIndex = 1 + firstTop = 0 + } + + let start = 0 + let pageIndex = firstPageIndex + let lastBottom = firstTop + let guard = 0 + + for (;;) { + if (++guard > maxPages) { + warnings.push({ + code: 'PAGE_LIMIT_REACHED', + message: `分页超过 ${maxPages} 页保护上限,已截断`, + controlId: model.control.id, + }) + break + } + + const topOnPage = pageIndex === firstPageIndex ? firstTop : 0 + const availFull = Math.max(0, metrics.bodyHeight - topOnPage) + + // 先试"为下方控件预留空间"的窄预算;能一次放完,说明这就是末页 + let slice = null as ReturnType | null + if (reserveBelow > 0 && availFull - reserveBelow > 0) { + const trial = sliceTable(model, { avail: availFull - reserveBelow, start }) + if (trial.isLast) slice = trial + } + if (!slice) { + slice = sliceTable(model, { avail: availFull, start }) + // 用满预算才放完 → 下方控件挤不下,让它们独占下一页 + if (slice.isLast && reserveBelow > 0 && availFull - slice.height < reserveBelow) { + slice = { ...slice, isLast: false } + } + } + + warnings.push(...slice.warnings) + + // 跳过完全为空的尾片(上一页已放完全部数据行):否则会渲染出一个空表 + 多余合计行 + const isEmptySlice = + slice.rows.length === 0 && slice.headerRows.length === 0 && slice.footerRows.length === 0 + if (!isEmptySlice) { + slices.push({ + table: { + kind: 'table', + id: model.control.id, + left: tableLeft, + top: topOnPage, + width: tableWidth, + height: slice.height, + control: model.control, + columns: model.columns, + columnWidths: model.columnWidths, + headerRows: slice.headerRows, + rows: slice.rows, + footerRows: slice.footerRows, + isLastSlice: slice.isLast, + }, + pageIndex, + }) + } + + lastBottom = topOnPage + slice.height + + if (slice.isLast) break + if (slice.nextStart === start && slice.rows.length === 0) { + // 没有任何前进 → 防死循环 + warnings.push({ + code: 'CONTENT_OVERFLOW', + message: '表格可用高度不足以容纳任何一行,已终止分页', + controlId: model.control.id, + }) + break + } + + start = slice.nextStart + pageIndex++ + } + + return { + slices, + pageCount: pageIndex + 1, + lastBottom, + warnings, + } +} + +/* ================================ 主入口 ================================ */ + +export async function layout( + template: TemplateData, + data: Record, + options: LayoutOptions = {}, +): Promise { + const measurer = options.measurer ?? getSharedMeasurer() + const maxPages = options.maxPages ?? MAX_PAGES + const warnings: RenderWarning[] = [] + + const doc = template.document + const unit = doc.page.unit + const sections = doc.sections ?? [] + const headerSection = sections.find((s) => s.type === 'header') as Section | undefined + const footerSection = sections.find((s) => s.type === 'footer') as Section | undefined + const bodySection = sections.find((s) => s.type === 'body') as Section | undefined + + const metrics = computePageMetrics(doc.page, headerSection, footerSection) + const baseCtx: EvalContext = { data } + + const bodyComponents = bodySection?.components ?? [] + const { plan, warnings: planWarnings } = analyzeBody(bodyComponents, unit) + warnings.push(...planWarnings) + + /* ── 情况 A:无流式表格 → 按位置切页 ── */ + if (!plan.flowTable) { + const staticPages = await layoutStaticOnly( + plan.above, + baseCtx, + unit, + metrics, + measurer, + maxPages, + warnings, + ) + const pages = await buildPages( + staticPages.length, + new Map(staticPages.map((p, i) => [i, p])), + headerSection, + footerSection, + baseCtx, + unit, + warnings, + ) + return { pages, metrics, page: doc.page, warnings: dedupeWarnings(warnings) } + } + + /* ── 情况 B:有流式表格 ── */ + const table = plan.flowTable + const tableTop = toMm(table.top, unit) + const tableLeft = toMm(table.left, unit) + const tableWidth = toMm(table.width, unit) + const tableBottom = tableTop + toMm(table.height, unit) + + const model = buildTableModel({ + control: table, + ctx: baseCtx, + measurer, + widthMm: tableWidth, + heightMm: toMm(table.height, unit), + }) + warnings.push(...model.warnings) + + // 下方控件相对表格底边的偏移量与所需高度 + const belowOffsets = plan.below.map((c) => ({ + control: c, + delta: toMm(c.top, unit) - tableBottom, + })) + const reserveBelow = belowOffsets.reduce( + (max, b) => Math.max(max, b.delta + toMm(b.control.height, unit)), + 0, + ) + + const skeleton = paginateFlowTable( + model, + tableLeft, + tableWidth, + tableTop, + metrics, + reserveBelow, + maxPages, + ) + warnings.push(...skeleton.warnings) + + const tableLastPage = skeleton.slices.at(-1)?.pageIndex ?? 0 + // 下方控件放得下就跟在末片后面,否则独占新的一页(top 从 0 起) + const belowFitsOnTablePage = skeleton.lastBottom + reserveBelow <= metrics.bodyHeight + EPS + const belowPageIndex = belowFitsOnTablePage ? tableLastPage : tableLastPage + 1 + const belowBaseTop = belowFitsOnTablePage ? skeleton.lastBottom : 0 + + const totalPages = Math.max(1, Math.max(tableLastPage, belowPageIndex) + 1) + const lastPageNo = totalPages + + /* ── 组装每页正文 ── */ + const bodyByPage = new Map() + const pushNode = (pageIndex: number, node: PlacedNode): void => { + const list = bodyByPage.get(pageIndex) + if (list) list.push(node) + else bodyByPage.set(pageIndex, [node]) + } + + for (const { table: slice, pageIndex } of skeleton.slices) pushNode(pageIndex, slice) + + // 第 1 页:表格上方 + 重叠控件 + 静态表格 + const firstCtx: EvalContext = { ...baseCtx, page: 1, pages: totalPages } + const firstControls = await placeControls([...plan.above, ...plan.overlap], firstCtx, unit) + warnings.push(...firstControls.warnings) + for (const c of firstControls.placed) pushNode(0, c) + + const staticTables = placeStaticTables( + [...plan.above, ...plan.overlap], + table.id, + firstCtx, + unit, + measurer, + ) + warnings.push(...staticTables.warnings) + for (const t of staticTables.placed) pushNode(0, t) + + // 末页:表格下方控件,按表格实际底边下移 + if (plan.below.length > 0) { + const belowCtx: EvalContext = { ...baseCtx, page: lastPageNo, pages: totalPages } + for (const { control, delta } of belowOffsets) { + if (control.type === 'zone') continue + if (!isControlPrintable(control, belowCtx)) continue + if (control.type === 'table') continue + const { content, warnings: w } = await resolveControlContent(control, belowCtx) + warnings.push(...w) + pushNode(belowPageIndex, { + kind: 'control', + id: control.id, + left: toMm(control.left, unit), + top: belowBaseTop + delta, + width: toMm(control.width, unit), + height: toMm(control.height, unit), + angle: control.angle, + content, + control, + }) + } + + const belowStatic = placeStaticTables( + plan.below, + table.id, + { ...baseCtx, page: lastPageNo, pages: totalPages }, + unit, + measurer, + belowBaseTop - tableBottom, + ) + warnings.push(...belowStatic.warnings) + for (const t of belowStatic.placed) pushNode(belowPageIndex, t) + } + + /* ── 注入页眉页脚 ── */ + const pages = await buildPages( + totalPages, + bodyByPage, + headerSection, + footerSection, + baseCtx, + unit, + warnings, + ) + + return { pages, metrics, page: doc.page, warnings: dedupeWarnings(warnings) } +} + +/* ---------------------------- 无表格:按位置切页 ---------------------------- */ + +/** + * 无流式表格时的退化策略:控件按 top 落在第几个 bodyHeight 区间就进第几页。 + * 返回「每页的正文节点数组」,页眉页脚由调用方统一注入。 + */ +async function layoutStaticOnly( + components: AnyControl[], + baseCtx: EvalContext, + unit: PageSetup['unit'], + metrics: PageMetrics, + measurer: TextMeasurer, + maxPages: number, + warnings: RenderWarning[], +): Promise { + const bodyH = Math.max(1, metrics.bodyHeight) + + // 控件按 top 落在第几个 bodyHeight 区间就进第几页 + let maxPageIndex = 0 + const buckets = new Map() + for (const c of components) { + const idx = Math.max(0, Math.min(maxPages - 1, Math.floor(toMm(c.top, unit) / bodyH))) + maxPageIndex = Math.max(maxPageIndex, idx) + const list = buckets.get(idx) + if (list) list.push(c) + else buckets.set(idx, [c]) + } + + const totalPages = maxPageIndex + 1 + const bodyByPage = new Map() + + for (const [idx, list] of buckets) { + const ctx: EvalContext = { ...baseCtx, page: idx + 1, pages: totalPages } + const shift = -idx * bodyH + const { placed, warnings: w } = await placeControls(list, ctx, unit, shift) + warnings.push(...w) + const tables = placeStaticTables(list, null, ctx, unit, measurer, shift) + warnings.push(...tables.warnings) + bodyByPage.set(idx, [...placed, ...tables.placed]) + } + + return Array.from({ length: totalPages }, (_, i) => bodyByPage.get(i) ?? []) +} + +/* ------------------------------ 页眉页脚注入 ------------------------------ */ + +async function buildPages( + totalPages: number, + bodyByPage: Map, + headerSection: Section | undefined, + footerSection: Section | undefined, + baseCtx: EvalContext, + unit: PageSetup['unit'], + warnings: RenderWarning[], +): Promise { + const pages: LayoutPage[] = [] + + for (let i = 0; i < totalPages; i++) { + // 页码变量按页求值 —— "第 X 页 / 共 Y 页" 正确的前提 + const pageCtx: EvalContext = { ...baseCtx, page: i + 1, pages: totalPages } + + let header: PlacedControl[] = [] + if (shouldRenderSection(headerSection, 'header', i, totalPages)) { + const r = await buildSectionControls(headerSection?.components, pageCtx, unit) + warnings.push(...r.warnings) + header = r.controls + } + + let footer: PlacedControl[] = [] + if (shouldRenderSection(footerSection, 'footer', i, totalPages)) { + const r = await buildSectionControls(footerSection?.components, pageCtx, unit) + warnings.push(...r.warnings) + footer = r.controls + } + + pages.push({ index: i, pageNo: i + 1, header, body: bodyByPage.get(i) ?? [], footer }) + } + + return pages +} + +/* -------------------------------- 工具 -------------------------------- */ + +/** 同一条告警可能在多页重复产生(页眉每页解析一次),这里按 code+message+controlId 去重 */ +function dedupeWarnings(list: RenderWarning[]): RenderWarning[] { + const seen = new Set() + const out: RenderWarning[] = [] + for (const w of list) { + const key = `${w.code}|${w.controlId ?? ''}|${w.message}` + if (seen.has(key)) continue + seen.add(key) + out.push(w) + } + return out +} diff --git a/src/core/layout-engine/table-cells.spec.ts b/src/core/layout-engine/table-cells.spec.ts new file mode 100644 index 0000000..f282526 --- /dev/null +++ b/src/core/layout-engine/table-cells.spec.ts @@ -0,0 +1,422 @@ +/** + * 设计期单元格网格模型单测(table-cells.ts) + * + * 覆盖:有/无 cells 的回落、buildDesignGrid 行语义、ensureCells 物化、 + * patchCellText(表头/静态/数据样例行语义)、patchCellStyle(合并/清除)、 + * setCellSpan(横向合并 + 吞格清空)、resolveCellStyle 优先级、normalizeCellRows、designRowInfo。 + */ +import { describe, expect, it } from 'vitest' +import type { TableCell, TableControl } from '@/types/control' +import { + addTableColumn, + buildDesignGrid, + computeSpanLayout, + designRowInfo, + ensureCells, + moveTableColumn, + normalizeCellRows, + patchCell, + patchCellStyle, + patchCellText, + removeTableColumn, + removeTableRow, + resolveCellStyle, + setCellRowSpan, + setCellSpan, + setGridRows, +} from './table-cells' + +function baseTable(over: Partial = {}): TableControl { + return { + id: 't1', + type: 'table', + left: 0, + top: 0, + width: 180, + height: 60, + columns: [ + { title: '序号', expression: '{{rowIndex + 1}}', width: 15, align: 'center', headerAlign: 'center' }, + { title: '名称', field: 'name', width: 60 }, + { title: '数量', field: 'qty', width: 25, align: 'right', headerAlign: 'center' }, + ], + options: { borders: 'all', verticalAlign: 'middle' }, + ...over, + } +} + +describe('buildDesignGrid', () => { + it('数据表无 cells:由列配置瞬时推导 表头+数据样例行+静态尾行', () => { + const g = buildDesignGrid(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(g.isData).toBe(true) + expect(g.headerRows).toBe(1) + expect(g.rowCount).toBe(1 + 1 + 1) // header + data template + 1 static + expect(g.colCount).toBe(3) + // 表头第 1 行:文本=列标题、加粗、对齐跟随 headerAlign + expect(g.cells[0]![0]!.text).toBe('序号') + expect(g.cells[0]![0]!.style?.bold).toBe(true) + expect(g.cells[0]![0]!.style?.align).toBe('center') + // 数据样例行第 2 行:带 field(来自列配置的 field) + expect(g.cells[1]![1]!.field).toBe('name') + expect(g.cells[1]![2]!.field).toBe('qty') + // 静态尾行:空 + expect(g.cells[2]![0]).toEqual({}) + }) + + it('布局网格(无 dataSource)无 cells:行数取 designRows(含表头)', () => { + const g = buildDesignGrid(baseTable({ designRows: 4 })) + expect(g.isData).toBe(false) + expect(g.headerRows).toBe(1) // 列有标题 → 自动补 1 行表头 + expect(g.rowCount).toBe(5) // 1 表头 + 4 正文 + expect(g.colCount).toBe(3) + expect(g.cells.every((r) => r.length === 3)).toBe(true) + }) + + it('已有 cells:按语义行数归一化(列数补齐到当前列数)', () => { + const cells = [ + [{ text: 'A' }, { text: 'B' }, { text: 'C' }, { text: '多余列应被丢弃' }], + [{ text: 'x' }, { text: 'y' }, { text: 'z' }], + ] + const g = buildDesignGrid(baseTable({ dataSource: 'items', cells, staticRows: 0 })) + expect(g.rowCount).toBe(2) // header(1) + data(1) + 0 + expect(g.colCount).toBe(3) + expect(g.cells[0]![3]).toBeUndefined() + expect(g.cells[0]![0]!.text).toBe('A') + }) + + it('列数为 0 时兜底为 1 列,不崩溃', () => { + const g = buildDesignGrid(baseTable({ columns: [] })) + expect(g.colCount).toBe(1) + expect(g.rowCount).toBeGreaterThan(0) + }) +}) + +describe('ensureCells', () => { + it('无 cells 时物化出完整网格并带上语义行数', () => { + const next = ensureCells(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(next.cells).toBeDefined() + expect(next.cells!.length).toBe(3) + expect(next.headerRows).toBe(1) + expect(next.staticRows).toBe(1) + expect(next.designRows).toBe(0) + }) + + it('已存在且尺寸匹配则返回结构(不重建,保持既有内容)', () => { + const cells = [ + [{ text: 'H1' }, { text: 'H2' }, { text: 'H3' }], + [{ field: 'a' }, { field: 'b' }, { field: 'c' }], + ] + const orig = baseTable({ dataSource: 'items', cells, staticRows: 0 }) + const next = ensureCells(orig) + expect(next.cells).toBe(cells) + expect(next.headerRows).toBe(1) + }) +}) + +describe('patchCellText 行语义', () => { + it('表头/静态行:写 text 字面量', () => { + const t = baseTable({ dataSource: 'items', staticRows: 1 }) + const next = patchCellText(t, 0, 0, '序号X') // 表头行 + expect(next.cells![0]![0]!.text).toBe('序号X') + }) + + it('静态尾行:写 text 字面量', () => { + const t = baseTable({ dataSource: 'items', staticRows: 1 }) + const next = patchCellText(t, 2, 0, '合计') // 静态行 + expect(next.cells![2]![0]!.text).toBe('合计') + }) + + it('数据样例行:与当前占位符一致 → 原样返回(不固化占位符)', () => { + const t = baseTable({ dataSource: 'items' }) + const next = patchCellText(t, 1, 1, '{{item.name}}') + expect(next).toBe(t) + }) + + it('数据样例行:纯字段引用回写为 field', () => { + const t = baseTable({ dataSource: 'items' }) + const next = patchCellText(t, 1, 1, '{{item.amount}}') + expect(next.cells![1]![1]!.field).toBe('amount') + expect(next.cells![1]![1]!.expression).toBeUndefined() + }) + + it('数据样例行:含表达式 → 写 expression 并清 field', () => { + const t = baseTable({ dataSource: 'items' }) + const next = patchCellText(t, 1, 2, '{{row.price * row.qty}}') + expect(next.cells![1]![2]!.expression).toBe('{{row.price * row.qty}}') + expect(next.cells![1]![2]!.field).toBeUndefined() + }) + + it('数据样例行:清空 → 同时清 expression 与 field', () => { + const t = baseTable({ dataSource: 'items' }) + const next = patchCellText(t, 1, 1, '') + expect(next.cells![1]![1]!.expression).toBeUndefined() + expect(next.cells![1]![1]!.field).toBeUndefined() + }) +}) + +describe('patchCellStyle / resolveCellStyle', () => { + it('resolveCellStyle 优先级:单元格 > 列 > 表格默认', () => { + const t = baseTable({ + options: { defaultCellStyle: { align: 'left', color: '#000' } }, + columns: [{ title: 'A', width: 10, style: { align: 'center', color: '#f00' } }], + }) + const resolved = resolveCellStyle(t, t.columns[0], { style: { align: 'right', bold: true } }) + expect(resolved.align).toBe('right') // 单元格覆盖 + expect(resolved.color).toBe('#f00') // 列兜底 + expect(resolved.bold).toBe(true) + }) + + it('patchCellStyle 合并并覆盖传入键;undefined 表示清除', () => { + let t = baseTable({ dataSource: 'items' }) + t = patchCellStyle(t, 0, 0, { bold: true, color: '#ff0000' }) + expect(t.cells![0]![0]!.style?.bold).toBe(true) + expect(t.cells![0]![0]!.style?.color).toBe('#ff0000') + t = patchCellStyle(t, 0, 0, { color: undefined }) + expect(t.cells![0]![0]!.style?.bold).toBe(true) + expect(t.cells![0]![0]!.style?.color).toBeUndefined() + }) +}) + +describe('setCellSpan 横向合并', () => { + it('本格 colSpan=n,被吞格清空(保留矩阵规整)', () => { + const t = baseTable({ dataSource: 'items' }) + const next = setCellSpan(t, 1, 0, 2) // 数据样例行第0列合并2列 + expect(next.cells![1]![0]!.colSpan).toBe(2) + expect(next.cells![1]![1]!.text).toBeUndefined() + expect(next.cells![1]![1]!.field).toBeUndefined() + expect(next.cells![1]![1]!.colSpan).toBeUndefined() + }) + + it('合并数越界自动夹紧到剩余列数', () => { + const t = baseTable({ dataSource: 'items' }) + const next = setCellSpan(t, 1, 1, 99) // 剩 2 列(c=1,2) + expect(next.cells![1]![1]!.colSpan).toBe(2) + }) +}) + +describe('designRowInfo', () => { + it('数据表行语义正确', () => { + const g = buildDesignGrid(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(designRowInfo(g, 0).kind).toBe('header') + expect(designRowInfo(g, 1).kind).toBe('data') + expect(designRowInfo(g, 1).isDataTemplate).toBe(true) + expect(designRowInfo(g, 2).kind).toBe('static') + }) + + it('布局网格:第 0 行为表头,其余为 static', () => { + const g = buildDesignGrid(baseTable({ designRows: 3 })) + expect(designRowInfo(g, 0).kind).toBe('header') + expect(designRowInfo(g, 1).kind).toBe('static') + expect(designRowInfo(g, 2).kind).toBe('static') + }) +}) + +describe('normalizeCellRows', () => { + it('行/列不足补空、超出截断', () => { + const out = normalizeCellRows([[{ text: 'a' }], [{ text: 'b' }, { text: 'c' }]], 3, 2) + expect(out.length).toBe(3) + expect(out[0]!.length).toBe(2) + expect(out[0]![0]!.text).toBe('a') + expect(out[0]![1]).toEqual({}) + expect(out[1]![1]!.text).toBe('c') + }) +}) + +describe('patchCell 不可变更新', () => { + it('返回新控件且只改目标格', () => { + const t = baseTable({ dataSource: 'items' }) + const next = patchCell(t, 0, 0, { text: '改' }) + expect(next).not.toBe(t) + expect(next.cells![0]![0]!.text).toBe('改') + expect(t.cells).toBeUndefined() + }) +}) + +describe('网格结构变更(#100)', () => { + it('setGridRows 布局网格增正文行:保留已有内容并按目标维度补空', () => { + const t = ensureCells(baseTable({ designRows: 2 })) + t.cells![0]![0] = { text: 'H' } + t.cells![1]![1] = { text: 'B' } + const next = setGridRows(t, { designRows: 4 }) + expect(next.headerRows).toBe(1) + expect(next.designRows).toBe(4) + expect(next.cells!.length).toBe(5) // 1 表头 + 4 正文 + expect(next.cells![0]![0]!.text).toBe('H') // 表头保留 + expect(next.cells![1]![1]!.text).toBe('B') // 正文保留 + expect(next.cells![4]![0]).toEqual({}) // 新补空行 + }) + + it('setGridRows 数据表增静态尾行', () => { + const t = ensureCells(baseTable({ dataSource: 'items', staticRows: 0 })) + const next = setGridRows(t, { staticRows: 2 }) + expect(next.staticRows).toBe(2) + expect(next.cells!.length).toBe(4) // 1 表头 + 1 数据样例 + 2 尾 + expect(Boolean(next.dataSource?.trim())).toBe(true) + }) + + it('setGridRows 改表头行数:新增表头行用列标题兜底', () => { + const t = baseTable({ designRows: 2 }) + const next = setGridRows(t, { headerRows: 2 }) + expect(next.headerRows).toBe(2) + expect(next.cells!.length).toBe(4) // 2 表头 + 2 正文 + expect(next.cells![1]![0]!.text).toBe('序号') // 新表头行取列标题 + }) + + it('setGridRows 减行数:截断尾部(保留前面内容)', () => { + const t = ensureCells(baseTable({ designRows: 5 })) + t.cells![0]![0] = { text: 'H' } + const next = setGridRows(t, { designRows: 2 }) + expect(next.cells!.length).toBe(3) // 1 表头 + 2 正文 + expect(next.cells![0]![0]!.text).toBe('H') + }) + + it('addTableColumn:列与每行单元格同步增加,新列表头默认填标题', () => { + const t = ensureCells(baseTable({ designRows: 2 })) + const next = addTableColumn(t, { title: '新列', width: 20 }) + expect(next.columns.length).toBe(4) + expect(next.cells!.every((r) => r.length === 4)).toBe(true) + expect(next.cells![0]![3]!.text).toBe('新列') // 表头新列取标题 + }) + + it('removeTableColumn:删除指定列(含对应单元格),至少留 1 列', () => { + const t = ensureCells(baseTable({ designRows: 2 })) + t.cells![0]![1] = { text: 'B-col1' } + const next = removeTableColumn(t, 1) + expect(next.columns.length).toBe(2) + expect(next.cells!.every((r) => r.length === 2)).toBe(true) + expect(next.columns[0]!.title).toBe('序号') + expect(next.columns[1]!.title).toBe('数量') // 原第 2 列(数量)左移 + expect(next.cells![0]![1]!.text).toBe('数量') // 单元格同步左移 + }) + + it('removeTableColumn:最后一列不可删', () => { + const t = baseTable({ columns: [{ title: 'only', width: 10 }] }) + const next = removeTableColumn(t, 0) + expect(next.columns.length).toBe(1) + }) + + it('removeTableRow:删除指定行(含对应单元格),至少留 1 行', () => { + const t = ensureCells(baseTable({ designRows: 3 })) + t.cells![2]![0] = { text: '第三行' } + const next = removeTableRow(t, 2) + const grid = buildDesignGrid(next) + expect(grid.rowCount).toBe(3) // 4 行 - 1 + expect(grid.designRows).toBe(2) + expect(grid.cells.some((r) => r[0]?.text === '第三行')).toBe(false) + expect(grid.cells[1]![0]?.text).toBeUndefined() // 第 2 行未受影响 + }) + + it('removeTableRow:删表头行时 headerRows 减一', () => { + const t = ensureCells(baseTable({ designRows: 2 })) + const next = removeTableRow(t, 0) + expect(buildDesignGrid(next).headerRows).toBe(0) + expect(buildDesignGrid(next).rowCount).toBe(2) + }) + + it('removeTableRow:数据表的数据样例行(唯一 body 行)不可删', () => { + const t = ensureCells(baseTable({ dataSource: 'items', staticRows: 1 })) + const next = removeTableRow(t, 1) // 第 1 行是数据样例行(headerRows=1) + expect(next).toBe(t) // 不变 + }) + + it('removeTableRow:单行表格不可删', () => { + const t = ensureCells(baseTable({ height: 8, headerRows: 0, designRows: 0, columns: [{ title: '', width: 10 }] })) + expect(buildDesignGrid(t).rowCount).toBe(1) + const next = removeTableRow(t, 0) + expect(next).toBe(t) + }) + + it('moveTableColumn:左右搬动列与单元格', () => { + const t = ensureCells(baseTable({ designRows: 2 })) + const next = moveTableColumn(t, 0, 2) + expect(next.columns[2]!.title).toBe('序号') + expect(next.cells![0]![2]!.text).toBe('序号') // 单元格同列位置搬动 + expect(next.cells![0]![0]!.text).toBe('名称') + }) +}) + +describe('computeSpanLayout 合并布局', () => { + // 构造 cells 矩阵的便捷函数:rows 是「每行每列的对象字面量」二维数组 + const mk = (rows: Array>>): TableCell[][] => + rows.map((r) => r.map((c) => ({ ...c }))) + + it('无合并:全部 skip=false、跨度=1', () => { + const cells = mk([ + [{}, {}], + [{}, {}], + ]) + const layout = computeSpanLayout(cells, 2, 2) + expect(layout.every((r) => r.every((s) => !s.skip && s.colSpan === 1 && s.rowSpan === 1))).toBe(true) + }) + + it('横向合并:锚点 colSpan=2,右侧被吞', () => { + const cells = mk([ + [{ colSpan: 2 }, {}], + [{}, {}], + ]) + const layout = computeSpanLayout(cells, 2, 2) + expect(layout[0]![0]!.colSpan).toBe(2) + expect(layout[0]![0]!.skip).toBe(false) + expect(layout[0]![1]!.skip).toBe(true) // 被横向吞掉 + expect(layout[1]![1]!.skip).toBe(false) // 下一行不受同行跨列影响 + }) + + it('纵向合并:锚点 rowSpan=2,下方同列被吞', () => { + const cells = mk([ + [{ rowSpan: 2 }, {}], + [{}, {}], + ]) + const layout = computeSpanLayout(cells, 2, 2) + expect(layout[0]![0]!.rowSpan).toBe(2) + expect(layout[1]![0]!.skip).toBe(true) // 被纵向吞掉 + expect(layout[0]![1]!.skip).toBe(false) + expect(layout[1]![1]!.skip).toBe(false) + }) + + it('跨列 + 跨行组合:2×2 块,其余三格均被吞', () => { + const cells = mk([ + [{ colSpan: 2, rowSpan: 2 }, {}], + [{}, {}], + ]) + const layout = computeSpanLayout(cells, 2, 2) + expect(layout[0]![0]!.colSpan).toBe(2) + expect(layout[0]![0]!.rowSpan).toBe(2) + expect(layout[0]![1]!.skip).toBe(true) + expect(layout[1]![0]!.skip).toBe(true) + expect(layout[1]![1]!.skip).toBe(true) + }) + + it('越界收敛:rowSpan 超出网格底边被截断', () => { + const cells = mk([ + [{ rowSpan: 9 }, {}], + [{}, {}], + ]) + const layout = computeSpanLayout(cells, 2, 2) + expect(layout[0]![0]!.rowSpan).toBe(2) // 只剩 2 行,截断为 2 + expect(layout[1]![0]!.skip).toBe(true) + }) +}) + +describe('setCellRowSpan 纵向合并', () => { + it('rowSpan=2:本格写 rowSpan,下方同列清空', () => { + const t = ensureCells(baseTable({ designRows: 3 })) + const next = setCellRowSpan(t, 0, 0, 2) + expect(next.cells![0]![0]!.rowSpan).toBe(2) + expect(next.cells![1]![0]!.text).toBeUndefined() + expect(next.cells![1]![0]!.field).toBeUndefined() + expect(next.cells![2]![0]!.rowSpan).toBeUndefined() // 只吞 2 行 + }) + + it('rowSpan=1:清除合并', () => { + const t = ensureCells(baseTable({ designRows: 3 })) + const merged = setCellRowSpan(t, 0, 0, 2) + const cleared = setCellRowSpan(merged, 0, 0, 1) + expect(cleared.cells![0]![0]!.rowSpan).toBeUndefined() + }) + + it('越界 rowSpan 收敛到网格底边', () => { + const t = ensureCells(baseTable({ designRows: 2 })) // 共 1 表头 + 2 正文 = 3 行 + const next = setCellRowSpan(t, 0, 0, 99) + expect(next.cells![0]![0]!.rowSpan).toBe(3) // 从第 0 行到底边共 3 行 + expect(next.cells![2]![0]!.rowSpan).toBeUndefined() + }) +}) diff --git a/src/core/layout-engine/table-cells.ts b/src/core/layout-engine/table-cells.ts new file mode 100644 index 0000000..f9567c7 --- /dev/null +++ b/src/core/layout-engine/table-cells.ts @@ -0,0 +1,728 @@ +/** + * table-cells —— 表格设计期单元格网格模型(纯函数,可测) + * + * 把"列驱动 + 数据源"的传统表格,叠加一层**设计期可编辑的单元格网格**, + * 使得双击任意单元格即可填写文字 / 绑定字段 / 调字体 / 填色, + * 同时**完全兼容**未设置 cells 的旧模板(回落到列驱动渲染)。 + * + * 行语义(designRowInfo): + * - 数据表(有 dataSource):前 headerRows 行为表头;第 headerRows 行为"数据样例行"(模板, + * 编辑它 = 编辑该列所有数据行);其后 staticRows 行为静态尾行(合计 / 备注)。 + * 可视行与 cells 行 1:1 对应(cells 长度 = headerRows + 1 + staticRows)。 + * - 布局网格(无 dataSource):前 headerRows 行为表头(由列标题推导),其后 designRows 行为 + * 静态内容(cells 长度 = headerRows + designRows)。 + * + * ⚠️ `designRows` 语义 = **正文行数(不含表头)**;可视行总数一律取 `grid.rowCount`。 + */ +import type { TableCell, TableCellStyle, TableColumn, TableControl } from '@/types/control' +import { isAggToken, parseAggToken, stripItems } from './aggregate' + +export const DEFAULT_HEADER_ROWS = 1 + +export type DesignRowKind = 'header' | 'data' | 'static' + +/** + * 单个单元格的合并布局结果(由 computeSpanLayout 统一计算)。 + * - skip=true:该格被左/上某个合并格吞掉,渲染时跳过(矩阵仍留位保持规整)。 + * - colSpan / rowSpan:本格实际生效的合并跨度(已收敛到网格边界)。 + */ +export interface CellSpan { + skip: boolean + colSpan: number + rowSpan: number +} + +export interface DesignRowInfo { + kind: DesignRowKind + /** 在 control.cells 中的行下标(数据表与可视行 1:1) */ + cellRow: number + /** 是否为数据表的数据样例行(编辑影响整列) */ + isDataTemplate: boolean +} + +export interface DesignGrid { + isData: boolean + headerRows: number + staticRows: number + /** 布局网格的正文行数(不含表头);数据表恒为 0 */ + designRows: number + /** 可视行总数(含表头 / 数据样例行 / 静态尾行) */ + rowCount: number + colCount: number + cells: TableCell[][] +} + +/** 该表格是否数据表(绑定了数据源数组,或自带内嵌 data 行) */ +export function isDataTable(control: TableControl): boolean { + return ( + Boolean(control.dataSource?.trim()) || + (Array.isArray(control.data) && control.data.length > 0) + ) +} + +/** + * 表头行数。 + * 未显式设置时:**有任一列标题 → 1 行,否则 0 行**(数据表 / 布局网格同一规则), + * 与运行期旧版 `columns.some(c => c.title)` 判定一致,保证老模板打印结果不变。 + */ +export function headerRowsOf(control: TableControl): number { + if (typeof control.headerRows === 'number') return Math.max(0, control.headerRows) + return (control.columns ?? []).some((c) => c.title) ? DEFAULT_HEADER_ROWS : 0 +} + +export function staticRowsOf(control: TableControl): number { + return isDataTable(control) ? Math.max(0, control.staticRows ?? 0) : 0 +} + +function emptyRow(colCount: number): TableCell[] { + return Array.from({ length: colCount }, () => ({})) +} + +/** 从列配置推导单元格(用于无 cells 时的瞬时渲染 / 首次初始化) */ +function cellFromColumn(col: TableColumn, role: 'header' | 'data'): TableCell { + if (role === 'header') { + return { + text: col.title, + style: { + backgroundColor: col.headerBackgroundColor, + // 与运行期 table-engine 表头默认一致:headerAlign → align → center + align: col.headerAlign ?? col.align ?? 'center', + bold: true, + }, + } + } + return { + field: col.field, + expression: col.expression, + style: { + backgroundColor: col.cellBackgroundColor, + align: col.align, + ...(col.style ?? {}), + }, + } +} + +/** + * 布局网格的正文行数(不含表头)。 + * 真理源优先级:已物化的 cells 行数 > 显式 designRows > 按控件高度推算。 + * (以 cells 为先,才不会在改了表头行数后把用户已填的行挤掉。) + */ +function layoutBodyRows(control: TableControl, headerRows: number): number { + if (control.cells && control.cells.length > 0) { + return Math.max(1, control.cells.length - headerRows) + } + if (typeof control.designRows === 'number' && control.designRows > 0) return control.designRows + const rowH = Math.max(4, control.options?.rowHeight ?? 8) + const bodyH = Math.max(0, control.height - headerRows * rowH) + return Math.max(1, Math.floor(bodyH / rowH) || 1) +} + +/** 该单元格是否"从未被写过内容"(用于表头兜底判定;显式清空写入的 `text: ''` 不算空) */ +function isBlankCell(cell: TableCell | undefined): boolean { + return !cell || (cell.text === undefined && !cell.field && !cell.expression) +} + +/** + * 计算设计期网格(不修改入参)。 + * - 若 control.cells 存在:归一化其行数 / 列数后使用;整行空白的表头行用列标题兜底 + * (用户改了列标题却看不到,比"尊重空表头"更反直觉)。 + * - 否则:由列配置瞬时推导。 + * - 数据表 = headerRows 行表头 + 1 行数据样例 + staticRows 行静态尾行 + * - 布局网格 = headerRows 行表头 + designRows 行静态正文 + */ +export function buildDesignGrid(control: TableControl): DesignGrid { + const cols = control.columns ?? [] + const colCount = Math.max(1, cols.length) + const data = isDataTable(control) + const headerRows = headerRowsOf(control) + const staticRows = staticRowsOf(control) + const bodyRows = data ? 1 : layoutBodyRows(control, headerRows) + const rowCount = headerRows + bodyRows + staticRows + + // 由列配置推导的"标准网格",同时充当已有 cells 的兜底来源 + const derived = normalizeCellRows( + [ + ...Array.from({ length: headerRows }, () => cols.map((c) => cellFromColumn(c, 'header'))), + ...(data + ? [cols.map((c) => cellFromColumn(c, 'data'))] + : Array.from({ length: bodyRows }, () => emptyRow(colCount))), + ...Array.from({ length: staticRows }, () => emptyRow(colCount)), + ], + rowCount, + colCount, + ) + + let cells = derived + if (control.cells && control.cells.length > 0) { + cells = normalizeCellRows(control.cells, rowCount, colCount) + for (let r = 0; r < headerRows; r++) { + if (cells[r]!.every(isBlankCell)) cells[r] = derived[r]! + } + } + + return { + isData: data, + headerRows, + staticRows, + designRows: data ? 0 : bodyRows, + rowCount, + colCount, + cells, + } +} + +/** + * 设计期各可视行的行高(mm)—— **设计画布与运行期布局网格共用**,是"所见即所得"的根: + * 表头按 8mm/行 分配(最多占 32% 高度),剩余高度由正文行均分,合计恰好 = 控件高度。 + * + * @param heightMm 控件高度(mm)。协议单位非 mm 时由调用方换算后传入。 + */ +export function designRowHeights(control: TableControl, heightMm?: number): number[] { + const grid = buildDesignGrid(control) + const totalH = Math.max(1, heightMm ?? control.height) + const headerRows = grid.headerRows + const bodyCount = Math.max(1, grid.rowCount - headerRows) + const headerShare = Math.min(totalH * 0.32, headerRows * 8) + const headerRowH = headerRows > 0 ? headerShare / headerRows : 0 + const bodyH = Math.max(4, (totalH - headerShare) / bodyCount) + const out: number[] = [] + for (let r = 0; r < grid.rowCount; r++) out.push(r < headerRows ? headerRowH : bodyH) + return out +} + +/** 把任意 cells 矩阵归一化为 rowCount × colCount(不足补空、超出截断) */ +export function normalizeCellRows( + cells: TableCell[][], + rowCount: number, + colCount: number, +): TableCell[][] { + const out: TableCell[][] = [] + for (let r = 0; r < rowCount; r++) { + const src = cells[r] ?? [] + const row: TableCell[] = [] + for (let c = 0; c < colCount; c++) { + row.push(src[c] ? { ...src[c] } : {}) + } + out.push(row) + } + return out +} + +/** 给定可视行号,返回其语义(表头 / 数据样例行 / 静态尾行) */ +export function designRowInfo(grid: DesignGrid, r: number): DesignRowInfo { + if (!grid.isData) { + return { kind: r < grid.headerRows ? 'header' : 'static', cellRow: r, isDataTemplate: false } + } + if (r < grid.headerRows) return { kind: 'header', cellRow: r, isDataTemplate: false } + if (r === grid.headerRows) return { kind: 'data', cellRow: r, isDataTemplate: true } + return { kind: 'static', cellRow: r, isDataTemplate: false } +} + +/** + * 设计期行角色名(双击编辑时显示在首列左侧,帮助用户识别当前所在行)。 + * 聚合尾行按单元格 token 判定:大写金额 / 总计 / 本页合计。 + */ +export function rowRoleLabel(grid: DesignGrid, r: number): string { + const info = designRowInfo(grid, r) + if (info.kind === 'header') return grid.headerRows > 1 ? `标题行 ${r + 1}` : '标题行' + if (info.isDataTemplate) return '数据行' + const row = grid.cells[r] ?? [] + const tokens = row.map((c) => parseAggToken(c.text)) + if (tokens.some((t) => t === 'pageCap' || t === 'totalCap')) return '大写金额行' + if (tokens.some((t) => t === 'totalSum' || t === 'totalAvg' || t === 'totalCount')) return '总计行' + if (tokens.some((t) => t === 'pageSum' || t === 'pageAvg' || t === 'pageCount')) return '本页合计行' + // 其余非表头、非聚合 token 的行(数据样例行 / 备注 / 签字栏等)统一叫"数据行" + return '数据行' +} + +/** + * 物化 cells 到控件(首次双击编辑或列数变化时调用)。 + * 返回带完整 cells 的新控件;若已存在且长度匹配则原样返回。 + */ +export function ensureCells(control: TableControl): TableControl { + const grid = buildDesignGrid(control) + const headerRows = grid.headerRows + const staticRows = grid.staticRows + const designRows = grid.designRows + const existing = control.cells + const matches = + existing && + existing.length === grid.rowCount && + existing.every((row) => row.length === grid.colCount) + if (matches) { + return { ...control, headerRows, staticRows, designRows } + } + return { + ...control, + headerRows, + staticRows, + designRows, + cells: grid.cells, + } +} + +/** 浅合并单元格样式:later 覆盖 former 的已定义字段 */ +export function mergeCellStyle(...parts: Array): TableCellStyle { + const out: TableCellStyle = {} + for (const p of parts) { + if (!p) continue + for (const [k, v] of Object.entries(p)) { + if (v !== undefined && v !== null) (out as Record)[k] = v + } + } + return out +} + +/** 取某单元格最终样式(表格默认 → 列样式 → 单元格样式) */ +export function resolveCellStyle( + control: TableControl, + column: TableColumn | undefined, + cell: TableCell | undefined, +): TableCellStyle { + return mergeCellStyle(control.options?.defaultCellStyle, column?.style, cell?.style) +} + +/** + * 按行语义取单元格最终样式。 + * 表头行**不吃 `column.style`** —— 列样式是给数据单元格用的,套到表头上会让 + * "把某列数据调成红色"顺带把标题也染红,与直觉相悖。设计期与运行期共用本函数。 + */ +export function resolveCellStyleFor( + control: TableControl, + column: TableColumn | undefined, + cell: TableCell | undefined, + kind: DesignRowKind, +): TableCellStyle { + return resolveCellStyle(control, kind === 'header' ? undefined : column, cell) +} + +/** 不可变更新某单元格(合并 patch),返回新控件 */ +export function patchCell( + control: TableControl, + r: number, + c: number, + patch: Partial, +): TableControl { + const grid = buildDesignGrid(control) + const next = grid.cells.map((row, ri) => + ri === r ? row.map((cell, ci) => (ci === c ? { ...cell, ...patch } : cell)) : row, + ) + return { + ...control, + cells: next, + headerRows: grid.headerRows, + staticRows: grid.staticRows, + designRows: grid.designRows, + } +} + +/* ----------------------------- 网格结构变更 ----------------------------- */ + +/** + * 设定表格的语义行数并物化 cells(保留已有内容,按需补空 / 截断)。 + * 同时回写 headerRows / designRows / staticRows 元数据,使与设计期网格 1:1 对齐。 + * + * 参数按控件类型生效: + * - 布局网格(无 dataSource):designRows = 正文行数;staticRows 恒为 0。 + * - 数据表:固定 1 行数据样例行(模板),staticRows = 静态尾行(备注 / 签字栏)。 + * 未传的维度沿用当前值,便于只改某一维。 + * + * ⚠️ 不能走 ensureCells:它按"cells 长度优先推导 designRows",会无视显式 designRows。 + * 这里以"目标维度"为准直接归一 cells。 + */ +export function setGridRows( + control: TableControl, + rows: { headerRows?: number; designRows?: number; staticRows?: number }, +): TableControl { + const headerRows = Math.max(0, rows.headerRows ?? headerRowsOf(control)) + const isData = isDataTable(control) + const staticRows = isData ? Math.max(0, rows.staticRows ?? staticRowsOf(control)) : 0 + const designRows = isData ? 0 : Math.max(0, rows.designRows ?? layoutBodyRows(control, headerRows)) + const colCount = Math.max(1, control.columns?.length ?? 0) + const rowCount = headerRows + (isData ? 1 : designRows) + staticRows + + // 已有 cells → 按目标维度直接归一(保留内容);无 cells → 先按新语义推导再取 + const base = + control.cells && control.cells.length > 0 + ? control.cells + : buildDesignGrid({ ...control, headerRows, designRows, staticRows }).cells + const cells = normalizeCellRows(base, rowCount, colCount) + return { ...control, headerRows, designRows, staticRows, cells } +} + +/** 在列尾追加一列(并同步扩展每行的单元格);返回新控件 */ +export function addTableColumn(control: TableControl, col?: Partial): TableControl { + const idx = control.columns?.length ?? 0 + const newCol: TableColumn = { + title: col?.title ?? `列${idx + 1}`, + field: col?.field, + expression: col?.expression, + width: col?.width ?? 30, + align: col?.align, + headerAlign: col?.headerAlign, + cellBackgroundColor: col?.cellBackgroundColor, + headerBackgroundColor: col?.headerBackgroundColor, + cellPadding: col?.cellPadding, + style: col?.style, + } + const grid = buildDesignGrid(control) // 当前规范矩阵 + const cells = grid.cells.map((row, r) => { + const row2 = [...row, {}] + // 新列表头单元格默认填入列标题(与设计期"列标题推导表头"一致) + if (r < grid.headerRows && newCol.title) row2[row2.length - 1] = { text: newCol.title } + return row2 + }) + return { + ...control, + columns: [...(control.columns ?? []), newCol], + cells, + headerRows: grid.headerRows, + staticRows: grid.staticRows, + designRows: grid.designRows, + } +} + +/** 删除指定列(并同步裁剪每行的对应单元格);至少保留 1 列 */ +export function removeTableColumn(control: TableControl, index: number): TableControl { + const cols = control.columns ?? [] + if (cols.length <= 1 || index < 0 || index >= cols.length) return control + const grid = buildDesignGrid(control) // 当前规范矩阵 + const cells = grid.cells.map((row) => row.filter((_, i) => i !== index)) + return { + ...control, + columns: cols.filter((_, i) => i !== index), + cells, + headerRows: grid.headerRows, + staticRows: grid.staticRows, + designRows: grid.designRows, + } +} + +/** 删除指定行(同步裁剪该行单元格);至少保留 1 行 */ +export function removeTableRow(control: TableControl, index: number): TableControl { + const grid = buildDesignGrid(control) + if (index < 0 || index >= grid.rowCount || grid.rowCount <= 1) return control + // 数据表的"数据样例行"(唯一 body 行)不可删除,否则破坏逐行渲染模型 + if (grid.isData && index === grid.headerRows) return control + const cells = grid.cells.filter((_, r) => r !== index) + let { headerRows, staticRows, designRows } = grid + if (index < headerRows) headerRows -= 1 + else if (grid.isData) staticRows = Math.max(0, staticRows - 1) + else designRows = Math.max(0, designRows - 1) + return { ...control, headerRows, staticRows, designRows, cells } +} + +/** 列左右移动(上移 / 下移到相邻位置);同时搬动每行的对应单元格 */ +export function moveTableColumn(control: TableControl, from: number, to: number): TableControl { + const cols = control.columns ?? [] + if (from < 0 || from >= cols.length || to < 0 || to >= cols.length || from === to) return control + const grid = buildDesignGrid(control) // 当前规范矩阵 + const cells = grid.cells.map((row) => { + const r = [...row] + const [cell] = r.splice(from, 1) + r.splice(to, 0, cell!) + return r + }) + const movedCols = [...cols] + const [col] = movedCols.splice(from, 1) + movedCols.splice(to, 0, col!) + return { + ...control, + columns: movedCols, + cells, + headerRows: grid.headerRows, + staticRows: grid.staticRows, + designRows: grid.designRows, + } +} + +/** + * 单元格文本写回(把"用户在 DOM 里敲的字"翻译成协议语义): + * - 表头 / 静态行:写 `text`(纯字面量) + * - 数据样例行:写 `expression`(模板串——含 `{{}}` 则运行期插值,纯文字则原样输出), + * 并清空 `field`,避免 field/expression 双写时语义打架 + * + * 若文本与"当前占位符"完全一致,视为未改动,直接返回原控件(避免把占位符固化成表达式)。 + */ +export function patchCellText( + control: TableControl, + r: number, + c: number, + raw: string, +): TableControl { + const grid = buildDesignGrid(control) + const info = designRowInfo(grid, r) + const text = raw.replace(/\u00a0/g, ' ').trim() + const cell = grid.cells[r]?.[c] ?? {} + const col = control.columns[c] + + if (info.isDataTemplate) { + // 占位符显示与 table-design-render 的 fieldPlaceholder 严格对齐,确保"未改动即不写回" + const fieldPh = (f: string): string => (f.includes('[]') ? `{{${f}}}` : `{{item.${f}}}`) + const current = cell.expression + ?? (cell.field ? fieldPh(cell.field) : col?.field ? fieldPh(col.field) : '') + if (text === current) return control + if (!text) return patchCell(control, r, c, { expression: undefined, field: undefined }) + // "{{item.xxx}}" 单一普通字段引用 → 回写为 field,保持模型最简 + const single = /^\{\{\s*item\.([A-Za-z0-9_$.]+)\s*\}\}$/.exec(text) + if (single) return patchCell(control, r, c, { field: single[1], expression: undefined }) + // "{{items[].xxx}}" 含数组标记的单一字段引用 → 同样回写为 field(运行期 resolveBinding 会去前缀取值) + const singleArr = /^\{\{\s*([A-Za-z0-9_$\u4e00-\u9fa5][A-Za-z0-9_$\u4e00-\u9fa5.\[\]]*\[\]\.[A-Za-z0-9_$\u4e00-\u9fa5.\[\]]*)\s*\}\}$/.exec(text) + if (singleArr) return patchCell(control, r, c, { field: singleArr[1], expression: undefined }) + return patchCell(control, r, c, { expression: text, field: undefined }) + } + + if ((cell.text ?? '') === text) return control + return patchCell(control, r, c, { text }) +} + +/** 不可变合并某单元格样式(只覆盖传入的键,undefined 表示"清除该项") */ +export function patchCellStyle( + control: TableControl, + r: number, + c: number, + style: TableCellStyle, +): TableControl { + const grid = buildDesignGrid(control) + const current = grid.cells[r]?.[c]?.style ?? {} + const merged: TableCellStyle = { ...current } + for (const [k, v] of Object.entries(style)) { + if (v === undefined) delete (merged as Record)[k] + else (merged as Record)[k] = v + } + return patchCell(control, r, c, { style: merged }) +} + +/** + * 设置单元格横向合并跨度。 + * 合并 = 本格 colSpan=n,被吞掉的右侧格清空(渲染时按 span 跳过,模型仍留位保证矩阵规整)。 + */ +export function setCellSpan(control: TableControl, r: number, c: number, span: number): TableControl { + const grid = buildDesignGrid(control) + const max = grid.colCount - c + const n = Math.max(1, Math.min(Math.floor(span), max)) + let next = patchCell(control, r, c, { colSpan: n > 1 ? n : undefined }) + for (let i = 1; i < n; i++) { + next = patchCell(next, r, c + i, { text: undefined, field: undefined, expression: undefined, colSpan: undefined }) + } + return next +} + +/** + * 设置单元格纵向合并跨度。 + * 合并 = 本格 rowSpan=m,被吞掉的下方同列格清空(渲染时按 span 跳过,模型仍留位保持矩阵规整)。 + * 超界自动收敛到网格底边,故改行数 / 删行不会产生非法合并。 + */ +export function setCellRowSpan(control: TableControl, r: number, c: number, span: number): TableControl { + const grid = buildDesignGrid(control) + const max = grid.rowCount - r + const n = Math.max(1, Math.min(Math.floor(span), max)) + let next = patchCell(control, r, c, { rowSpan: n > 1 ? n : undefined }) + for (let k = 1; k < n; k++) { + next = patchCell(next, r + k, c, { + text: undefined, + field: undefined, + expression: undefined, + colSpan: undefined, + rowSpan: undefined, + }) + } + return next +} + +/** + * 统一计算整张网格的合并布局(colSpan + rowSpan 一起算),供设计 overlay 与运行期引擎共用。 + * + * 规则: + * - 跨列:本格 colSpan=n → 同行右侧 n-1 格被跳过(同一行内)。 + * - 跨行:本格 rowSpan=m → 下方 m-1 行同列(及所跨各列)被跳过。 + * - 两种跨度可同时作用于同一锚点格(形成 2×N / N×2 / N×M 的合并块)。 + * - 所有跨度超界自动收敛到网格边界,因此改行数 / 删列 / 移动列都不会产生非法合并或丢列。 + * + * 返回与 cells 同形的矩阵:skip=true 的格由相邻锚点合并,渲染端据此决定"是否输出该 td"。 + */ +export function computeSpanLayout( + cells: TableCell[][], + rowCount: number, + colCount: number, +): CellSpan[][] { + const out: CellSpan[][] = Array.from({ length: rowCount }, () => + Array.from({ length: colCount }, () => ({ skip: false, colSpan: 1, rowSpan: 1 }) as CellSpan), + ) + // carry[c] = 进入当前行时,第 c 列仍被上方某个 rowspan 占用的剩余行数(>0 表示该格需跳过) + const carry = new Array(colCount).fill(0) + for (let r = 0; r < rowCount; r++) { + const curCarry = carry.slice() + const nextCarry = carry.map((v) => Math.max(0, v - 1)) + let c = 0 + while (c < colCount) { + if (curCarry[c]! > 0) { + out[r]![c]!.skip = true + c++ + continue + } + const cell = cells[r]?.[c] ?? {} + const cs = Math.max(1, Math.min(Math.floor(cell.colSpan ?? 1), colCount - c)) + const rs = Math.max(1, Math.min(Math.floor(cell.rowSpan ?? 1), rowCount - r)) + out[r]![c]!.colSpan = cs + out[r]![c]!.rowSpan = rs + if (cs > 1) { + // 同一行被横向吞掉的格,标记 skip(矩阵自我完备:skip=true 即"不作为独立 td 输出") + for (let k = 1; k < cs; k++) out[r]![c + k]!.skip = true + } + if (rs > 1) { + // 被跨的每一列在后续行都被占用(含跨列覆盖到的列) + for (let k = 0; k < cs; k++) nextCarry[c + k] = Math.max(nextCarry[c + k]!, rs - 1) + } + c += cs + } + carry.length = 0 + carry.push(...nextCarry) + } + return out +} + +/** 不可变更新某列(数据表数据样例行编辑 = 编辑整列) */ +export function patchColumnStyle( + control: TableControl, + c: number, + style: TableCellStyle, +): TableControl { + const cols = control.columns.map((col, ci) => + ci === c ? { ...col, style: { ...(col.style ?? {}), ...style } } : col, + ) + return { ...control, columns: cols } +} + +/* ----------------------------- 行 / 列插入 ----------------------------- */ + +/** + * 在 `atRow` 处插入一空行(原行下移)。 + * - 布局网格:直接插入正文区,designRows+1。 + * - 数据表:数据样例行固定在 headerRows,任何插入都落在静态尾行区(避免破坏"单数据样例行"不变量), + * 故 atRow 被收敛到 headerRows+1 之后;staticRows+1。 + */ +export function insertTableRow(control: TableControl, atRow: number): TableControl { + const grid = buildDesignGrid(control) + const colCount = grid.colCount + const cells = grid.cells.map((row) => row.map((c) => ({ ...c }))) + const newRow: TableCell[] = Array.from({ length: colCount }, () => ({})) + let insertAt = atRow + let headerRows = grid.headerRows + let designRows = grid.designRows + let staticRows = grid.staticRows + if (grid.isData) { + const minStatic = grid.headerRows + 1 + insertAt = Math.max(minStatic, atRow) + staticRows += 1 + } else { + designRows += 1 + } + cells.splice(insertAt, 0, newRow) + return { ...control, headerRows, designRows, staticRows, cells } +} + +/** 在 `atCol` 处插入一空列(原列右移),同步扩展表头与每行的单元格 */ +export function insertTableColumn(control: TableControl, atCol: number): TableControl { + const grid = buildDesignGrid(control) + const cells = grid.cells.map((row) => { + const r = [...row] + r.splice(atCol, 0, {}) + return r + }) + const idx = (control.columns?.length ?? 0) + 1 + const newCol: TableColumn = { + title: `列${idx}`, + width: 30, + align: 'left', + headerAlign: 'center', + } + const cols = [...(control.columns ?? [])] + cols.splice(atCol, 0, newCol) + return { + ...control, + columns: cols, + cells, + headerRows: grid.headerRows, + staticRows: grid.staticRows, + designRows: grid.designRows, + } +} + +/* ----------------------------- 默认尾行结构 ----------------------------- */ + +export interface SeedTailOptions { + /** 参与合计(数值)的列索引;不传则从数据首行采样 / 列 aggregate 标记推断 */ + numericColumns?: number[] + /** 大写金额取哪列的总计(主金额列索引);不传取最后一个数值列 */ + moneyColumn?: number + /** 是否植入"大写金额"行(默认 true)。初始化默认表格可传 false,只留本页合计 + 总计两行 */ + capital?: boolean +} + +/** + * 植入"本页合计 / 总计 / 大写金额"三行尾结构(学习金蝶等 ERP 单据设计): + * - 数值列(数量 / 单价 / 金额…)尾格写入聚合 token:本页合计=`{{#pageSum}}`、总计=`{{#totalSum}}`、大写金额=`{{#totalCap}}`; + * - 第一列写入行名标签(本页合计 / 总计 / 大写金额),让用户一眼识别; + * - 非数值列(名称 / 备注等字符串)尾格留空,不参与计算; + * - 数值列自动标记 `aggregate:true`,参与尾行聚合。 + * + * 幂等:若已存在任意聚合 token 尾行则不重复植入。 + */ +export function seedSummaryTail(control: TableControl, opts: SeedTailOptions = {}): TableControl { + const grid = buildDesignGrid(control) + // 已植入尾行则不重复:识别聚合 token 或首列的尾行标签(单列表格 token 落在首列,故需同时识别标签) + const tailLabels = new Set(['本页合计', '总计', '大写金额']) + const seeded = grid.cells.slice(grid.headerRows + 1).some( + (row) => + row.some((c) => isAggToken(c.text)) || + (row[0] != null && tailLabels.has(row[0]!.text ?? '')), + ) + if (seeded) return control + const cols = control.columns ?? [] + const colCount = grid.colCount + + // 数值列判定 + const numeric = new Set(opts.numericColumns ?? []) + if (opts.numericColumns === undefined) { + const sample = Array.isArray(control.data) && control.data.length ? (control.data[0] as Record) : undefined + cols.forEach((c, i) => { + const key = stripItems(c.field) + if (c.aggregate === true || c.aggregate === 'sum' || c.aggregate === 'avg' || c.aggregate === 'count') { + numeric.add(i) + } else if (key && sample && typeof sample[key] === 'number') { + numeric.add(i) + } + }) + } + const money = opts.moneyColumn ?? [...numeric].sort((a, b) => b - a)[0] ?? -1 + + const makeRow = (label: string, token: string | null): TableCell[] => + Array.from({ length: colCount }, (_, c) => { + if (c === 0) return { text: label, style: { bold: true, align: 'left' } } + if (token && numeric.has(c)) return { text: token, style: { bold: true, align: 'right' } } + return {} + }) + const pageRow = makeRow('本页合计', '{{#pageSum}}') + const totalRow = makeRow('总计', '{{#totalSum}}') + const withCap = opts.capital ?? true + const capRow: TableCell[] = Array.from({ length: colCount }, (_, c) => { + if (c === 0) return { text: '大写金额', style: { bold: true, align: 'left' } } + if (c === money) return { text: '{{#totalCap}}', style: { bold: true, align: 'left' } } + return {} + }) + + // 数值列标记 aggregate:true + const newCols = cols.map((c, i) => (numeric.has(i) ? { ...c, aggregate: true as const } : c)) + + const tailRows = withCap ? [pageRow, totalRow, capRow] : [pageRow, totalRow] + const cells = [...grid.cells, ...tailRows] + return { + ...control, + columns: newCols, + cells, + headerRows: grid.headerRows, + staticRows: grid.staticRows + tailRows.length, + designRows: grid.designRows, + } +} diff --git a/src/core/layout-engine/table-engine.spec.ts b/src/core/layout-engine/table-engine.spec.ts new file mode 100644 index 0000000..0b9c4a4 --- /dev/null +++ b/src/core/layout-engine/table-engine.spec.ts @@ -0,0 +1,497 @@ +import { describe, expect, it } from 'vitest' + +import { + buildTableModel, + normalizeColumnWidths, + sliceTable, +} from '@/core/layout-engine/table-engine' +import { isDataTable } from '@/core/layout-engine/table-cells' +import type { TableColumn, TableControl } from '@/types/control' +import type { EvalContext } from './types' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +type Model = ReturnType + +describe('normalizeColumnWidths —— 列宽归一化', () => { + it('按比例缩放使列宽和 = totalWidth', () => { + const cols: TableColumn[] = [ + { title: 'A', field: 'a', width: 10 }, + { title: 'B', field: 'b', width: 20 }, + ] + const out = normalizeColumnWidths(cols, 30) + expect(out).toEqual([10, 20]) + expect(out.reduce((s, w) => s + w, 0)).toBeCloseTo(30, 6) + }) + + it('全部为 0 时均分', () => { + const cols: TableColumn[] = [{ title: 'A', field: 'a', width: 0 }, { title: 'B', field: 'b', width: 0 }] + const out = normalizeColumnWidths(cols, 10) + expect(out[0]).toBeCloseTo(5, 6) + expect(out[1]).toBeCloseTo(5, 6) + }) + + it('含 0 宽列时保持 0,其余按比例', () => { + const cols: TableColumn[] = [ + { title: 'A', field: 'a', width: 2 }, + { title: 'B', field: 'b', width: 0 }, + { title: 'C', field: 'c', width: 2 }, + ] + const out = normalizeColumnWidths(cols, 8) + expect(out[0]).toBeCloseTo(4, 6) + expect(out[1]).toBe(0) + expect(out[2]).toBeCloseTo(4, 6) + expect(out.reduce((s, w) => s + w, 0)).toBeCloseTo(8, 6) + }) +}) + +describe('buildTableModel —— 表格建模', () => { + const measurer = createCjkMeasurer() + const control = { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 50, + dataSource: 'items', + columns: [ + { field: 'name', title: '名称', width: 60, align: 'left' }, + { field: 'qty', title: '数量', width: 40, align: 'right' }, + ], + options: { repeatHeader: true }, + } as unknown as Parameters[0]['control'] + + it('根据 dataSource 生成与数据等量的数据行', () => { + const ctx = { data: { items: [{ name: 'A', qty: 1 }, { name: 'B', qty: 2 }, { name: 'C', qty: 3 }] } } + const model = buildTableModel({ control, ctx, measurer, widthMm: 100, heightMm: 50 }) + expect(model.rows.length).toBe(3) + expect(model.columnWidths.length).toBe(2) + expect(model.columnWidths.reduce((s, w) => s + w, 0)).toBeCloseTo(100, 6) + expect(model.headerRows.length).toBeGreaterThan(0) + expect(model.isLayoutGrid).toBe(false) + }) + + it('无数据源时退化为布局网格(固定行数)', () => { + const grid = { ...control, dataSource: '' } as unknown as Parameters[0]['control'] + const model = buildTableModel({ control: grid, ctx: { data: {} }, measurer, widthMm: 100, heightMm: 50 }) + expect(model.isLayoutGrid).toBe(true) + expect(model.rows.length).toBeGreaterThan(0) + }) + + /** 取出第一条数据行的第 col 列渲染文本 */ + function dataCellText(ctrl: Parameters[0]['control'], ctx: EvalContext, col: number): string { + const model = buildTableModel({ control: ctrl, ctx, measurer, widthMm: 100, heightMm: 50 }) + const row = model.rows.find((r) => r.kind === 'data') + if (!row) throw new Error('no data row') + return (row.cells[col] as { text: string }).text + } + + it('列格式 decimal:数字按小数位+千分位渲染', () => { + const c = { + ...control, + columns: [{ field: 'price', title: '单价', width: 40, align: 'right', format: { kind: 'decimal', digits: 2 } }], + } as unknown as Parameters[0]['control'] + expect(dataCellText(c, { data: { items: [{ price: 1234.5 }] } }, 0)).toBe('1,234.50') + }) + + it('列格式 currency:带币种符号', () => { + const c = { + ...control, + columns: [{ field: 'amount', title: '金额', width: 40, align: 'right', format: { kind: 'currency', code: 'CNY' } }], + } as unknown as Parameters[0]['control'] + expect(dataCellText(c, { data: { items: [{ amount: 9999.9 }] } }, 0)).toBe('¥9,999.90') + }) + + it('单元格 format 覆盖列 format', () => { + const c = { + ...control, + columns: [{ field: 'qty', title: '数量', width: 40, align: 'right', format: { kind: 'decimal', digits: 2 } }], + cells: [ + [{ text: '数量' }], + [{ field: 'qty', format: { kind: 'int' } }], + ], + } as unknown as Parameters[0]['control'] + expect(dataCellText(c, { data: { items: [{ qty: 1234.5 }] } }, 0)).toBe('1,235') + }) + + it('表头/静态行绑定 cell.field 应解析主表标量(回归:staticCellText 曾忽略 field)', () => { + const c = { + ...control, + dataSource: '', + headerRows: 1, + staticRows: 1, + cells: [ + [{ field: 'order.no' }, { text: '数量' }], + [{ field: 'order.total' }, { text: '' }], + ], + } as unknown as Parameters[0]['control'] + const ctx = { data: { order: { no: 'NO-001', total: 5 } } } + const model = buildTableModel({ control: c, ctx, measurer, widthMm: 100, heightMm: 50 }) + // 表头单元格:绑定 order.no → 解析出主表标量(修复前为空) + expect(model.headerRows[0]!.cells[0]!.text).toBe('NO-001') + // 静态尾行单元格:绑定 order.total → 解析出 5 + const staticRow = model.rows.find((r) => r.kind === 'static') + expect(staticRow).toBeTruthy() + expect(staticRow!.cells[0]!.text).toBe('5') + }) +}) + +describe('sliceTable —— 分页切片(含空尾片回归)', () => { + function mkModel(rowCount: number): Model { + return { + control: { id: 't', type: 'table', options: { repeatHeader: true, repeatFooter: false } } as Model['control'], + columns: [], + columnWidths: [], + headerRows: [{ kind: 'header', height: 8, cells: [] }], + rows: Array.from({ length: rowCount }, (_, i) => ({ + kind: 'data', + height: 8, + dataIndex: i, + cells: [], + })) as Model['rows'], + footerRows: [{ kind: 'summary', height: 8, cells: [{ text: '合计', align: 'left' }] }], + dataRows: [], + warnings: [], + isLayoutGrid: false, + } as Model + } + + it('从 start=0 切片:取全部行,末片挂合计行', () => { + const model = mkModel(3) + const slice = sliceTable(model, { avail: 100, start: 0 }) + expect(slice.rows.length).toBe(3) + expect(slice.footerRows.length).toBeGreaterThan(0) // 末片 + 空间充足 → 合计行 + expect(slice.isLast).toBe(true) + expect(slice.nextStart).toBe(3) + }) + + it('空尾片(start 越界)不挂合计行,也不产出多余数据行', () => { + const model = mkModel(3) + const slice = sliceTable(model, { avail: 100, start: 3 }) + expect(slice.rows.length).toBe(0) // 关键回归:不再渲染第二个空表 + expect(slice.footerRows.length).toBe(0) // 关键回归:空尾片不再挂合计 + expect(slice.isLast).toBe(true) + expect(slice.nextStart).toBe(3) + }) + + it('空间不足时本页少放,nextStart 后移', () => { + const model = mkModel(5) + // 每页能放 2 行(header 8 + 2*8 = 24 ≤ avail 24;第 3 行 8 会使 32 > 24) + const slice = sliceTable(model, { avail: 24, start: 0 }) + expect(slice.rows.length).toBe(2) + expect(slice.nextStart).toBe(2) + expect(slice.isLast).toBe(false) + }) +}) + +/* ----------------------- 合计行(options.summaryRow) ----------------------- */ + +function mkSummaryControl(opts: TableControl['options']): Parameters[0]['control'] { + return { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 50, + dataSource: 'items', + columns: [ + { field: 'name', title: '名称', width: 60, align: 'left' }, + { field: 'qty', title: '数量', width: 40, align: 'right' }, + ], + options: opts, + } as unknown as Parameters[0]['control'] +} + +describe('buildTableModel —— 合计行(options.summaryRow)', () => { + const measurer = createCjkMeasurer() + + it('求和:表尾合计行在聚合列显示总和,首列显示标签', () => { + const ctx = { data: { items: [{ name: 'A', qty: 1 }, { name: 'B', qty: 2 }, { name: 'C', qty: 3 }] } } + const model = buildTableModel({ + control: mkSummaryControl({ summaryRow: { type: 'sum', fields: ['qty'], label: '合计' } }), + ctx, + measurer, + widthMm: 100, + heightMm: 50, + }) + expect(model.footerRows.length).toBe(1) + const row = model.footerRows[0]! + expect(row.kind).toBe('summary') + expect(row.cells[0]!.text).toBe('合计') + const qtyCell = row.cells.find((c) => c.text === '6') + expect(qtyCell).toBeTruthy() + expect(qtyCell!.bold).toBe(true) + expect(qtyCell!.align).toBe('right') + }) + + it('计数:聚合列显示数据总行数', () => { + const ctx = { data: { items: [{ name: 'A', qty: 1 }, { name: 'B', qty: 2 }, { name: 'C', qty: 3 }] } } + const model = buildTableModel({ + control: mkSummaryControl({ summaryRow: { type: 'count', fields: ['qty'], label: '合计' } }), + ctx, + measurer, + widthMm: 100, + heightMm: 50, + }) + const row = model.footerRows[0]! + expect(row.cells.find((c) => c.text === '3')).toBeTruthy() + }) + + it('未配置合计行时不生成表尾', () => { + const ctx = { data: { items: [{ name: 'A', qty: 1 }] } } + const model = buildTableModel({ + control: mkSummaryControl({}), + ctx, + measurer, + widthMm: 100, + heightMm: 50, + }) + expect(model.footerRows.length).toBe(0) + }) +}) + +describe('buildTableModel —— 分组小计(groupBy + summaryRow)', () => { + const measurer = createCjkMeasurer() + const control = { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 50, + dataSource: 'items', + groupBy: 'cat', + columns: [ + { field: 'name', title: '名称', width: 60, align: 'left' }, + { field: 'qty', title: '数量', width: 40, align: 'right' }, + ], + options: { summaryRow: { type: 'sum', fields: ['qty'], label: '合计' } }, + } as unknown as Parameters[0]['control'] + + it('按分组字段生成组头 + 每组小计 + 末尾总计', () => { + const ctx = { + data: { + items: [ + { name: 'A', qty: 1, cat: 'x' }, + { name: 'B', qty: 2, cat: 'x' }, + { name: 'C', qty: 3, cat: 'y' }, + ], + }, + } + const model = buildTableModel({ control, ctx, measurer, widthMm: 100, heightMm: 50 }) + expect(model.rows.some((r) => r.kind === 'group')).toBe(true) + const subs = model.rows.filter((r) => r.kind === 'subtotal') + expect(subs.length).toBe(2) + // 每组内求和:x=1+2=3,y=3,均显示 3 + expect(subs.every((s) => s.cells.find((c) => c.text === '3'))).toBe(true) + // 末尾总计 = 1+2+3 = 6 + const total = model.footerRows.find((r) => r.kind === 'summary') + expect(total?.cells.find((c) => c.text === '6')).toBeTruthy() + }) + + it('分组小计应用自定义标签与样式', () => { + const ctx = { + data: { + items: [ + { name: 'A', qty: 1, cat: 'x' }, + { name: 'B', qty: 2, cat: 'x' }, + ], + }, + } + const model = buildTableModel({ + control: { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 50, + dataSource: 'items', + groupBy: 'cat', + columns: [ + { field: 'name', title: '名称', width: 60, align: 'left' }, + { field: 'qty', title: '数量', width: 40, align: 'right' }, + ], + options: { + summaryRow: { + type: 'sum', + fields: ['qty'], + label: '总计', + subtotalLabel: '${key} 分组小计', + subtotalStyle: { bold: true, backgroundColor: '#EEF3FF', color: '#c00' }, + }, + }, + } as unknown as Parameters[0]['control'], + ctx, + measurer, + widthMm: 100, + heightMm: 50, + }) + const subs = model.rows.filter((r) => r.kind === 'subtotal') + expect(subs.length).toBe(1) + expect(subs[0]!.cells[0]!.text).toBe('x 分组小计') + // 整行套用 subtotalStyle(标签格也应带颜色) + expect(subs[0]!.cells[0]!.color).toBe('#c00') + expect(subs[0]!.cells[0]!.background).toBe('#EEF3FF') + // 聚合列显示本组求和 1+2=3 + expect(subs[0]!.cells.find((c) => c.text === '3')).toBeTruthy() + }) + + it('自定义合计表达式在表尾生效(footer 显示求值结果)', () => { + const ctx = { data: { items: [{ name: 'A', amount: 10 }, { name: 'B', amount: 20 }] } } + const model = buildTableModel({ + control: { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 50, + dataSource: 'items', + // 首列为非聚合列(放标签),第二列为聚合列(放自定义求值结果) + columns: [ + { field: 'name', title: '名称', width: 60, align: 'left' }, + { field: 'amount', title: '金额', width: 40, align: 'right' }, + ], + options: { + summaryRow: { + type: 'custom', + fields: ['amount'], + label: '自定义合计', + expression: 'sum.amount + 5', + }, + }, + } as unknown as Parameters[0]['control'], + ctx, + measurer, + widthMm: 100, + heightMm: 50, + }) + expect(model.footerRows.length).toBe(1) + const row = model.footerRows[0]! + expect(row.kind).toBe('summary') + expect(row.cells[0]!.text).toBe('自定义合计') + // 聚合列显示 (10+20) + 5 = 35 + expect(row.cells.find((c) => c.text === '35')).toBeTruthy() + }) +}) + +describe('rowSpan 纵向合并渲染', () => { + const measurer = createCjkMeasurer() + + it('布局网格:纵向合并时下方被吞单元格不进 cells,锚点带 rowSpan', () => { + const control = { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 40, + columns: [ + { title: 'A', width: 50 }, + { title: 'B', width: 50 }, + ], + headerRows: 0, + designRows: 2, + cells: [ + [ + { rowSpan: 2, text: '合并' }, + { text: '右1' }, + ], + [ + { text: '下左' }, + { text: '下右' }, + ], + ], + options: {}, + } as unknown as TableControl + + const model = buildTableModel({ control, ctx: { data: {} }, measurer, widthMm: 100, heightMm: 40 }) + expect(model.isLayoutGrid).toBe(true) + // 第 0 行:锚点(合并, rowSpan=2) + 右1 → 2 个单元格 + expect(model.rows[0]!.cells.length).toBe(2) + expect(model.rows[0]!.cells[0]!.rowSpan).toBe(2) + // 第 1 行:被吞的"下左"不出现,只剩"下右" → 1 个单元格 + expect(model.rows[1]!.cells.length).toBe(1) + expect(model.rows[1]!.cells[0]!.text).toBe('下右') + }) + + it('数据行模板的 rowSpan 被强制为 1(不跨记录)', () => { + const control = { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 60, + dataSource: 'items', + columns: [ + { field: 'name', title: '名称', width: 50 }, + { field: 'qty', title: '数量', width: 50 }, + ], + cells: [ + [{ text: '名称' }, { text: '数量' }], + [{ rowSpan: 2, field: 'name' }, { field: 'qty' }], + ], + options: {}, + } as unknown as TableControl + + const ctx = { data: { items: [{ name: 'A', qty: 1 }, { name: 'B', qty: 2 }] } } + const model = buildTableModel({ control, ctx, measurer, widthMm: 100, heightMm: 60 }) + // 每条数据行的首格不应带 rowSpan(避免跨记录合并) + expect(model.rows.every((r) => r.cells[0]!.rowSpan === undefined)).toBe(true) + }) +}) + +describe('buildTableModel —— 内嵌 data(与 dataSource 字段解耦)', () => { + const measurer = createCjkMeasurer() + + it('无 dataSource、带 data 时按内嵌数据生成数据行并解析字段', () => { + const control = { + id: 'tbl', + type: 'table', + left: 0, + top: 0, + width: 100, + height: 50, + // 注意:这里刻意不设置 dataSource + columns: [ + { title: '名称', field: 'items[].name', width: 50 }, + { title: '数量', field: 'items[].qty', width: 50 }, + ], + headerRows: 1, + data: [ + { name: 'A', qty: 1 }, + { name: 'B', qty: 2 }, + ], + options: { pageRows: 'auto', repeatHeader: true, borders: 'all' }, + } as unknown as TableControl + + // 全局 ctx 里没有任何 items,证明数据完全来自 control.data + const ctx = { data: {} } as unknown as EvalContext + const model = buildTableModel({ control, ctx, measurer, widthMm: 100, heightMm: 50 }) + expect(model.rows).toHaveLength(2) + expect(model.rows[0]!.cells[0]!.text).toBe('A') + expect(model.rows[1]!.cells[1]!.text).toBe('2') + // 表头来自 columns.title + expect(model.headerRows[0]!.cells[0]!.text).toBe('名称') + }) + + it('isDataTable 对内嵌 data 返回 true(供 analyzeBody 识别为分页流表)', () => { + const withData = { + id: 't', + type: 'table', + columns: [], + data: [{ a: 1 }], + } as unknown as TableControl + expect(isDataTable(withData)).toBe(true) + const empty = { + id: 't2', + type: 'table', + columns: [], + data: [], + } as unknown as TableControl + expect(isDataTable(empty)).toBe(false) + }) +}) diff --git a/src/core/layout-engine/table-engine.ts b/src/core/layout-engine/table-engine.ts new file mode 100644 index 0000000..f223917 --- /dev/null +++ b/src/core/layout-engine/table-engine.ts @@ -0,0 +1,839 @@ +/** + * TableEngine —— 动态表格建模与切片分页 + * 真理源:《OpenPrint-设计方案.md》§5.4(表格统一设计)、§7.4(难点清单) + * + * 覆盖 §7.4 难点: + * | 难点 | 实现位置 | + * |---|---| + * | 动态表格分页 / 表头重复 | sliceTable() 的 repeatHeader 分支 | + * | 每页行数控制 pageRows | sliceTable() 的 forcedRows | + * | 行内文本测量 rowHeightMode:'auto' | measureRowHeight() | + * | keepTogether 起始不足整体下移 | minStartHeight()(由 pagination-engine 调用) | + * | 合计/表尾 repeatFooter | sliceTable() 的 footer 预算 | + * | 空行 / 空表 skipEmptyRows | buildTableModel() | + * | 多单合并 mergeSheets | resolveRows() | + * + * 分两步:**建模**(一次性算完所有行与高度)→ **切片**(按页可用高度取行)。 + * 这样分页只是纯几何切割,不再触碰数据语义,逻辑可测且不会走样。 + * + * ## 内容真理源 = 设计期单元格网格(方案 A) + * + * 建模不再直接读列配置,而是统一经 `buildDesignGrid(control)`: + * 表头行、数据样例行(模板)、静态尾行都来自同一份 `cells` 网格 —— 设计画布画的是它、 + * 打印吃的也是它,字体/颜色/对齐/合并因此天然一致(真·所见即所得)。 + * 未设置 `cells` 的老模板由 `buildDesignGrid` 从列配置瞬时推导,行为与旧版逐字节等价。 + */ +import type { HAlign, TableCell, TableCellStyle, TableColumn, TableControl } from '@/types/control' +import { FILTERS, interpolate, resolveBinding, stringifyValue, formatCellValue } from './expression' +import { + aggValueForRows, + columnAggregatable, + formatAggNumber, + isAggToken, + parseAggToken, + stripItems, + toChineseCapitalRMB, + type FooterKind, +} from './aggregate' +import { + buildDesignGrid, + computeSpanLayout, + designRowHeights, + isDataTable, + resolveCellStyleFor, + type CellSpan, + type DesignRowKind, +} from './table-cells' +import { + buildSummaryPlan, + isEmptyRow, + planRows, + readField, + summaryLabel, + type RowPlan, +} from './group-engine' +import type { TextMeasurer } from './measure' +import type { EvalContext, RenderCell, RenderRow, RenderWarning } from './types' + +/* ------------------------------- 排版常数 ------------------------------- */ + +/** 表格文字字号(pt)—— 与设计画布 PrintTable 的 9pt 保持一致,确保"所见即所得" */ +export const TABLE_FONT_SIZE = 9 +/** + * 单元格水平内边距(mm)。 + * 说明:协议注释写的默认 4mm 若同时用作垂直内边距会让行高凭空多出 8mm, + * 对 ERP 密集表格不可用;这里把 cellPadding 解释为**水平内缩**(业界报表工具通用语义), + * 垂直方向由 CELL_PADDING_Y 单独控制。 + */ +export const DEFAULT_CELL_PADDING = 2 +/** 单元格垂直内边距(mm,上下各一份) */ +export const CELL_PADDING_Y = 1.2 +/** 最小行高(mm),防止空行被压成一条线 */ +export const MIN_ROW_HEIGHT = 6 +/** 表头行相对数据行的高度系数 */ +export const HEADER_ROW_FACTOR = 1 + +/* -------------------------------- 模型 -------------------------------- */ + +export interface TableModel { + control: TableControl + columns: TableColumn[] + /** 归一化后的各列实际宽度(mm,合计 = 控件宽度) */ + columnWidths: number[] + /** 表头行(支持多行;无表头时为空数组) */ + headerRows: RenderRow[] + /** 数据 + 组头 + 小计行(按打印顺序) */ + rows: RenderRow[] + /** 表尾块:聚合尾行(本页合计/总计/大写金额)+ 设计期静态尾行(备注 / 签字栏) */ + footerRows: RenderRow[] + /** 解析后的全表数据行(供切片期重算合计;尾行聚合的真理源) */ + dataRows: Array> + warnings: RenderWarning[] + /** 是否为布局网格(无 dataSource 的空白表格,§5.4) */ + isLayoutGrid: boolean +} + +/** 行高累加(表头 / 表尾块) */ +export function sumRowHeights(rows: RenderRow[]): number { + return rows.reduce((s, r) => s + r.height, 0) +} + +/* ------------------------------ 数据行解析 ------------------------------ */ + +function asRecord(v: unknown): Record | null { + return v !== null && typeof v === 'object' && !Array.isArray(v) + ? (v as Record) + : null +} + +/** + * 取出表格的数据行。 + * mergeSheets(§9.2.1a 多单合并):dataSource 解析出的若干数组会被**扁平拼接**成一条连续行流, + * 使多张单据一次连续打印,而不是各自起新页。 + */ +function resolveRows( + control: TableControl, + ctx: EvalContext, + warnings: RenderWarning[], +): Array> { + const path = control.dataSource?.trim() + if (path) { + const raw = resolveBinding(path, ctx) + if (raw === null || raw === undefined) { + warnings.push({ + code: 'DATASOURCE_EMPTY', + message: `表格数据源 "${path}" 在数据中不存在`, + controlId: control.id, + }) + return [] + } + + if (!Array.isArray(raw)) { + warnings.push({ + code: 'DATASOURCE_NOT_ARRAY', + message: `表格数据源 "${path}" 不是数组(实际为 ${typeof raw})`, + controlId: control.id, + }) + return [] + } + + const merge = control.options?.mergeSheets ?? false + const out: Array> = [] + for (const item of raw) { + if (Array.isArray(item)) { + // 多单合并:嵌套数组扁平化;未开启合并时忽略嵌套数组(避免打出乱序内容) + if (merge) { + for (const sub of item) { + const rec = asRecord(sub) + if (rec) out.push(rec) + } + } + continue + } + const rec = asRecord(item) + if (rec) out.push(rec) + } + + if (out.length === 0) { + warnings.push({ + code: 'DATASOURCE_EMPTY', + message: `表格数据源 "${path}" 为空数组`, + controlId: control.id, + }) + } + return out + } + + // 表格自带内嵌数据(导入数据场景):与 dataSource 字段解耦,直接作为数据行 + if (Array.isArray(control.data) && control.data.length > 0) { + const out: Array> = [] + for (const item of control.data) { + const rec = asRecord(item) + if (rec) out.push(rec) + } + return out + } + + return [] +} + +/* ------------------------------- 列宽归一化 ------------------------------ */ + +/** 把列的相对宽度按控件实际宽度等比归一,保证列宽之和 === 表格宽度 */ +export function normalizeColumnWidths(columns: TableColumn[], totalWidth: number): number[] { + const sum = columns.reduce((s, c) => s + (c.width > 0 ? c.width : 0), 0) + if (sum <= 0) { + const even = totalWidth / Math.max(1, columns.length) + return columns.map(() => even) + } + return columns.map((c) => ((c.width > 0 ? c.width : 0) / sum) * totalWidth) +} + +/* ------------------------------- 单元格取值 ------------------------------ */ + +function interp(src: string, ctx: EvalContext, errors: string[]): string { + const r = interpolate(src, ctx) + errors.push(...r.errors) + return r.text +} + +/** + * 数据行单元格取值。优先级:单元格 expression > 单元格 field > 单元格固定文字 > 列 expression > 列 field。 + * (单元格未设置任何内容时回落到列配置,既兼容老模板,也让"新增列"立刻可用。) + */ +function dataCellText( + cell: TableCell, + col: TableColumn | undefined, + ctx: EvalContext, + errors: string[], +): string { + if (cell.expression) return interp(cell.expression, ctx, errors) + if (cell.field) return formatCellValue(resolveBinding(cell.field, ctx), cell.format ?? col?.format) + if (cell.text !== undefined) return interp(cell.text, ctx, errors) + if (col?.expression) return interp(col.expression, ctx, errors) + if (col?.field) return formatCellValue(resolveBinding(col.field, ctx), col?.format) + return '' +} + +/** + * 表头 / 静态行取值。优先级与数据行对齐:单元格 expression > 单元格 field > 单元格字面量 > 列 field。 + * - 支持 {{}} 插值(如"合计:{{order.total}}"、"第 {{page}} 页"); + * - 也支持字段绑定(order.no):经 resolveBinding 解析主表标量字段; + * - 无任何标记的字面量文字原样输出(即"普通文本"约定)。 + */ +function staticCellText( + cell: TableCell, + col: TableColumn | undefined, + fallback: string, + ctx: EvalContext, + errors: string[], +): string { + if (cell.expression) return interp(cell.expression, ctx, errors) + if (cell.field) return formatCellValue(resolveBinding(cell.field, ctx), cell.format ?? col?.format) + if (cell.text !== undefined) return interp(cell.text, ctx, errors) + if (col?.field) return formatCellValue(resolveBinding(col.field, ctx), col?.format) + if (!fallback) return '' + return interp(fallback, ctx, errors) +} + +/** 聚合值显示:整数不带小数,小数保留两位,统一加千分位 */ +function formatAggregate(v: number): string { + return Number.isInteger(v) ? FILTERS.number!(v, 0) : FILTERS.number!(v, 2) +} + +/* ---------------------------- 单元格 → 渲染单元格 --------------------------- */ + +/** 与 cells[] 一一对应的几何(含 colSpan 合并后的宽度),供行高测量使用 */ +interface RowGeometry { + widths: number[] + pads: number[] +} + +interface BuiltRow { + cells: RenderCell[] + geo: RowGeometry +} + +/** 把设计期样式盖到渲染单元格上(只覆盖显式设置过的项,未设置的保留 kind 默认值) */ +function applyCellStyle( + base: RenderCell, + control: TableControl, + col: TableColumn | undefined, + cell: TableCell, + kind: DesignRowKind, +): RenderCell { + const s = resolveCellStyleFor(control, col, cell, kind) + const out: RenderCell = { ...base } + if (s.align) out.align = s.align + if (s.backgroundColor) out.background = s.backgroundColor + if (s.bold !== undefined) out.bold = s.bold + if (s.fontSize !== undefined) out.fontSize = s.fontSize + if (s.fontFamily) out.fontFamily = s.fontFamily + if (s.italic !== undefined) out.italic = s.italic + if (s.underline !== undefined) out.underline = s.underline + if (s.color) out.color = s.color + if (s.valign) out.valign = s.valign + if (s.diagonal) out.diagonal = s.diagonal + return out +} + +/** + * 摊平一行设计期单元格:处理 colSpan / rowSpan(合并宽度、跳过被吞掉的格), + * 同时产出与渲染单元格对齐的宽度 / 内边距数组,让行高测量吃到真实可用宽度。 + * + * @param spanRow 本行各列的合并布局(computeSpanLayout 已算好):skip=true 的格被上方/左侧 + * 合并吞掉,这里直接跳过;colSpan/rowSpan 作为本格的有效跨度。 + */ +function layoutDesignRow( + rowCells: TableCell[], + spanRow: CellSpan[], + columns: TableColumn[], + columnWidths: number[], + colCount: number, + make: (cell: TableCell, col: TableColumn | undefined, colIndex: number) => RenderCell, +): BuiltRow { + const cells: RenderCell[] = [] + const widths: number[] = [] + const pads: number[] = [] + let c = 0 + while (c < colCount) { + if (spanRow[c]?.skip) { + c++ + continue + } + const cell = rowCells[c] ?? {} + const span = Math.max(1, spanRow[c]!.colSpan) + const col = columns[c] + let w = 0 + for (let k = 0; k < span; k++) w += columnWidths[c + k] ?? 0 + const rc = make(cell, col, c) + if (span > 1) rc.colSpan = span + if (spanRow[c]!.rowSpan > 1) rc.rowSpan = spanRow[c]!.rowSpan + cells.push(rc) + widths.push(w) + pads.push(col?.cellPadding ?? DEFAULT_CELL_PADDING) + c += span + } + return { cells, geo: { widths, pads } } +} + +/* -------------------------------- 行高测量 ------------------------------- */ + +function measureRowHeight( + { cells, geo }: BuiltRow, + control: TableControl, + measurer: TextMeasurer, +): number { + const mode = control.options?.rowHeightMode ?? 'auto' + if (mode === 'fixed') { + return Math.max(MIN_ROW_HEIGHT, control.options?.rowHeight ?? 8) + } + + let maxH = 0 + for (let i = 0; i < cells.length; i++) { + const cell = cells[i]! + const avail = Math.max(1, (geo.widths[i] ?? 0) - (geo.pads[i] ?? DEFAULT_CELL_PADDING) * 2) + const { heightMm } = measurer.measure(cell.text ?? '', { + fontSize: cell.fontSize ?? TABLE_FONT_SIZE, + fontWeight: cell.bold ? 'bold' : 'normal', + widthMm: avail, + }) + if (heightMm > maxH) maxH = heightMm + } + return Math.max(MIN_ROW_HEIGHT, maxH + CELL_PADDING_Y * 2) +} + +/* -------------------------------- 建模 -------------------------------- */ + +export interface BuildTableOptions { + control: TableControl + ctx: EvalContext + measurer: TextMeasurer + /** + * 表格宽度(mm)。协议 page.unit 可能是 in/pt,而测量器只认 mm, + * 因此调用方必须把换算后的值传进来;缺省时按 control.width 已是 mm 处理。 + */ + widthMm?: number + /** 表格高度(mm),仅布局网格算行数时用到 */ + heightMm?: number +} + +export function buildTableModel({ + control, + ctx, + measurer, + widthMm, + heightMm, +}: BuildTableOptions): TableModel { + const warnings: RenderWarning[] = [] + const columns = control.columns ?? [] + const tableWidth = widthMm ?? control.width + const tableHeight = heightMm ?? control.height + const columnWidths = normalizeColumnWidths(columns, tableWidth) + const errors: string[] = [] + + // 设计期网格 = 内容与样式的唯一真理源(无 cells 的老模板由列配置瞬时推导) + const grid = buildDesignGrid(control) + const colCount = grid.colCount + // 整张网格的合并布局(colSpan + rowSpan 一起算,设计 overlay 与运行期引擎共用同一份) + const spanLayout = computeSpanLayout(grid.cells, grid.rowCount, colCount) + + const buildRowFrom = ( + rowCells: TableCell[], + spanRow: CellSpan[], + make: (cell: TableCell, col: TableColumn | undefined, colIndex: number) => RenderCell, + ): BuiltRow => layoutDesignRow(rowCells, spanRow, columns, columnWidths, colCount, make) + + /* ── 表头行(支持多行表头) ── */ + const headerRows: RenderRow[] = [] + for (let r = 0; r < grid.headerRows; r++) { + const built = buildRowFrom(grid.cells[r] ?? [], spanLayout[r]!, (cell, col) => + applyCellStyle( + { + text: staticCellText(cell, col, col?.title ?? '', ctx, errors), + align: col?.headerAlign ?? col?.align ?? 'center', + background: col?.headerBackgroundColor, + bold: true, + }, + control, + col, + cell, + 'header', + ), + ) + headerRows.push({ + kind: 'header', + height: measureRowHeight(built, control, measurer) * HEADER_ROW_FACTOR, + cells: built.cells, + }) + } + + /** 静态行(布局网格正文 / 数据表静态尾行):字面量 + 全局插值 */ + const buildStaticRow = (rowCells: TableCell[], height: number | undefined, spanRow: CellSpan[]): RenderRow => { + const built = buildRowFrom(rowCells, spanRow, (cell, col) => + applyCellStyle( + { text: staticCellText(cell, col, '', ctx, errors), align: col?.align ?? 'left' }, + control, + col, + cell, + 'static', + ), + ) + return { + kind: 'static', + height: height ?? measureRowHeight(built, control, measurer), + cells: built.cells, + } + } + + const flushErrors = (): void => { + for (const message of new Set(errors)) { + warnings.push({ code: 'EXPRESSION_ERROR', message, controlId: control.id }) + } + } + + /* ── 布局网格:无数据源,行高与设计画布逐行对齐(§5.4) ── */ + const isLayoutGrid = !isDataTable(control) + if (isLayoutGrid) { + // 与设计期共用 designRowHeights:画布上是几毫米,打印就是几毫米 + const heights = designRowHeights(control, tableHeight) + headerRows.forEach((row, i) => { + row.height = heights[i] ?? row.height + }) + const rows: RenderRow[] = [] + for (let r = grid.headerRows; r < grid.rowCount; r++) { + rows.push(buildStaticRow(grid.cells[r] ?? [], heights[r], spanLayout[r]!)) + } + flushErrors() + return { control, columns, columnWidths, headerRows, rows, footerRows: [], dataRows: [], warnings, isLayoutGrid: true } + } + + /* ── 数据行 ── */ + let dataRows = resolveRows(control, ctx, warnings) + + if (control.options?.skipEmptyRows) { + const fields = columns.map((c) => c.field).filter((f): f is string => Boolean(f)) + dataRows = dataRows.filter((r) => !isEmptyRow(r, fields)) + } + + const plans = planRows({ control, rows: dataRows, ctx }) + + /** 与列 1:1 对齐的行(组头 / 小计 / 合计)的测量几何 */ + const columnGeo: RowGeometry = { + widths: columnWidths, + pads: columns.map((c) => c.cellPadding ?? DEFAULT_CELL_PADDING), + } + + // 数据样例行(模板):决定每个数据行的取值方式与单元格样式 + const template = grid.cells[grid.headerRows] ?? [] + // 数据行由运行期逐条生成,跨行会跨越不同记录,语义不成立 → 强制模板行 rowSpan=1(colSpan 保留) + const templateSpan: CellSpan[] = spanLayout[grid.headerRows]!.map((s) => ({ ...s, rowSpan: 1 })) + + const rows: RenderRow[] = plans.map((plan) => buildRow(plan)) + + function buildRow(plan: RowPlan): RenderRow { + if (plan.kind === 'group') { + const text = plan.label ?? '' + const cells: RenderCell[] = [ + { text, align: 'left', colSpan: colCount, bold: true, background: '#F5F7FA' }, + ] + return { + kind: 'group', + height: measureRowHeight( + { cells, geo: { widths: [tableWidth], pads: [columnGeo.pads[0] ?? DEFAULT_CELL_PADDING] } }, + control, + measurer, + ), + cells, + } + } + + if (plan.kind === 'subtotal' || plan.kind === 'summary') { + const style = + plan.kind === 'subtotal' ? control.options?.summaryRow?.subtotalStyle : undefined + const cells = buildAggregateCells(plan.label ?? '小计', plan.aggregates ?? {}, style) + return { + kind: plan.kind, + height: measureRowHeight({ cells, geo: columnGeo }, control, measurer), + cells, + } + } + + // 数据行:内容与样式都由"数据样例行"模板驱动(含 colSpan 合并;rowSpan 强制为 1) + const rowCtx: EvalContext = { ...ctx, row: plan.row, rowIndex: plan.dataIndex ?? 0 } + const built = buildRowFrom(template, templateSpan, (cell, col) => + applyCellStyle( + { + text: dataCellText(cell, col, rowCtx, errors), + align: col?.align ?? 'left', + background: col?.cellBackgroundColor, + }, + control, + col, + cell, + 'data', + ), + ) + return { + kind: 'data', + dataIndex: plan.dataIndex, + height: measureRowHeight(built, control, measurer), + cells: built.cells, + } + } + + /** + * 合计行按列填值:第一列放标签,配置了聚合的列放数值,其余留空。 + * 若第一列本身就是聚合列,则标签退到"无处可放",此时把标签并入该列前缀。 + * @param style 可选样式覆盖(如分组小计行自定义样式),只覆盖显式设置的项。 + */ + function buildAggregateCells( + label: string, + agg: Record, + style?: TableCellStyle, + ): RenderCell[] { + const applyStyle = (cell: RenderCell): RenderCell => { + if (!style) return cell + const out: RenderCell = { ...cell } + if (style.bold !== undefined) out.bold = style.bold + if (style.italic !== undefined) out.italic = style.italic + if (style.underline !== undefined) out.underline = style.underline + if (style.fontSize !== undefined) out.fontSize = style.fontSize + if (style.fontFamily) out.fontFamily = style.fontFamily + if (style.color) out.color = style.color + if (style.backgroundColor) out.background = style.backgroundColor + if (style.align) out.align = style.align + if (style.valign) out.valign = style.valign + return out + } + + const cells: RenderCell[] = columns.map((c, i) => { + const field = c.field + const hit = + field !== undefined && (field in agg ? field : stripPrefix(field) in agg ? stripPrefix(field) : null) + if (hit) { + return applyStyle({ + text: formatAggregate(agg[hit] ?? 0), + align: c.align ?? 'right', + bold: true, + }) + } + return applyStyle({ + text: i === 0 ? label : '', + align: i === 0 ? 'left' : (c.align ?? 'left'), + bold: true, + }) + }) + if (cells.length === 0) { + cells.push(applyStyle({ text: label, align: 'left', bold: true, colSpan: 1 })) + } + return cells + } + + function stripPrefix(field: string): string { + return field.includes('[].') ? field.slice(field.indexOf('[].') + 3) : field + } + + /* ── 表尾块:聚合尾行(本页合计 / 总计 / 大写金额)+ 设计期静态尾行 ── */ + + /** + * 构建一行表尾(语义尾行)。若该静态行含聚合 token,则按列字段类型判断是否需要计算: + * - 数值列 → 标记 isAgg + aggField,渲染期由切片器按本页/总计重算; + * - 非数值列(字符串等)→ 不计算,渲染为空占位。 + * 首列的行名标签("本页合计"/"总计"/"大写金额")按普通静态文本处理。 + */ + function buildFooterRow(rowCells: TableCell[], spanRow: CellSpan[]): RenderRow { + const tokens = rowCells.map((c) => parseAggToken(c.text)) + const hasToken = tokens.some((t) => t !== null) + let fk: FooterKind = 'static' + if (hasToken) { + if (tokens.some((t) => t === 'pageCap' || t === 'totalCap')) fk = 'capital' + else if (tokens.some((t) => t === 'totalSum' || t === 'totalAvg' || t === 'totalCount')) + fk = 'grandTotal' + else fk = 'pageSubtotal' + } + const built = buildRowFrom(rowCells, spanRow, (cell, col) => { + const tk = parseAggToken(cell.text) + if (tk) { + const field = stripItems(col?.field) + const numeric = columnAggregatable(col, dataRows[0]) + if (numeric && field) { + return applyCellStyle( + { + text: '', + align: col?.align ?? 'right', + bold: true, + isAgg: true, + tokenKind: tk, + aggField: field, + }, + control, + col, + cell, + 'static', + ) + } + // 非数值列:聚合 token 不参与计算,留空 + return applyCellStyle( + { text: '', align: col?.align ?? 'left', bold: true }, + control, + col, + cell, + 'static', + ) + } + return applyCellStyle( + { text: staticCellText(cell, col, '', ctx, errors), align: col?.align ?? 'left' }, + control, + col, + cell, + 'static', + ) + }) + return { + kind: fk === 'static' ? 'static' : fk === 'pageSubtotal' ? 'subtotal' : 'summary', + footerKind: fk, + height: measureRowHeight(built, control, measurer), + cells: built.cells, + } + } + + const footerRows: RenderRow[] = [] + const summaryPlan = buildSummaryPlan(dataRows, control) + if (summaryPlan) { + const cells = buildAggregateCells(summaryLabel(control), summaryPlan.aggregates ?? {}) + footerRows.push({ + kind: 'summary', + footerKind: 'static', + height: measureRowHeight({ cells, geo: columnGeo }, control, measurer), + cells, + }) + } + const sample = dataRows[0] + for (let r = grid.headerRows + 1; r < grid.rowCount; r++) { + footerRows.push(buildFooterRow(grid.cells[r] ?? [], spanLayout[r]!)) + } + + flushErrors() + + return { control, columns, columnWidths, headerRows, rows, footerRows, dataRows, warnings, isLayoutGrid: false } +} + +/* -------------------------------- 切片 -------------------------------- */ + +export interface SliceRequest { + /** 本页分配给表格的可用高度(mm) */ + avail: number + /** 从 model.rows 的第几行开始 */ + start: number +} + +export interface SliceResult { + headerRows: RenderRow[] + rows: RenderRow[] + footerRows: RenderRow[] + /** 下一片起始下标 */ + nextStart: number + /** 本片实际占高(mm) */ + height: number + isLast: boolean + warnings: RenderWarning[] +} + +/** 表格在一页上至少要占的高度(表头 + 1 行 + 表尾),用于 keepTogether 判定 */ +export function minStartHeight(model: TableModel): number { + const h = sumRowHeights(model.headerRows) + const firstRow = model.rows[0]?.height ?? MIN_ROW_HEIGHT + const footer = sumRowHeights(model.footerRows) + return h + firstRow + footer +} + +/** + * 从 start 位置切出一页能放下的内容。 + * + * 预算顺序:可用高 −(表头)−(表尾)= 数据行预算。 + * 表尾是否占预算取决于 repeatFooter: + * - repeatFooter !== false → 每页都有表尾,恒占预算 + * - repeatFooter === false → 只有末片有表尾,先按"无表尾"试算, + * 若恰好切到最后一行,再回头检查表尾能否塞下,塞不下就吐出若干行留到下一页 + */ +export function sliceTable(model: TableModel, req: SliceRequest): SliceResult { + const warnings: RenderWarning[] = [] + const opts = model.control.options ?? {} + const isFirst = req.start === 0 + + const repeatHeader = opts.repeatHeader ?? true + const repeatFooter = opts.repeatFooter ?? true + + const headerRows = isFirst || repeatHeader ? model.headerRows : [] + const headerH = sumRowHeights(headerRows) + // 尾行按"是否每页都出现"拆分预算,避免非末页为总计/大写行预留留白: + // - 每页固定:本页合计 + 静态尾行(repeatFooter 时) + // - 仅末页:总计 + 大写金额 + 静态尾行(repeatFooter 关时) + const pageFooterH = model.footerRows + .filter((f) => f.footerKind === 'pageSubtotal' || (f.footerKind === 'static' && repeatFooter)) + .reduce((s, r) => s + r.height, 0) + const lastFooterH = model.footerRows + .filter( + (f) => + f.footerKind === 'grandTotal' || + f.footerKind === 'capital' || + (f.footerKind === 'static' && !repeatFooter), + ) + .reduce((s, r) => s + r.height, 0) + + // 固定每页行数(pageRows 为数字时强制分页) + const forcedRows = + typeof opts.pageRows === 'number' && opts.pageRows > 0 ? Math.floor(opts.pageRows) : null + + let budget = req.avail - headerH - pageFooterH + + // 把尾行模板按当前切片重算(本页合计用本页数据行;总计/大写用全表数据行,仅末页) + function resolveTailRow(fr: RenderRow, rows: Array>): RenderRow { + const cells: RenderCell[] = fr.cells.map((cell) => { + if (!cell.isAgg || !cell.aggField || !cell.tokenKind) return cell + const text = + cell.tokenKind === 'totalCap' || cell.tokenKind === 'pageCap' + ? toChineseCapitalRMB(aggValueForRows('totalSum', rows, cell.aggField)) + : formatAggNumber(aggValueForRows(cell.tokenKind, rows, cell.aggField)) + return { ...cell, text } + }) + return { ...fr, cells } + } + + function resolveFooterRows( + pickedDataRows: Array>, + last: boolean, + ): RenderRow[] { + const out: RenderRow[] = [] + for (const fr of model.footerRows) { + const fk = fr.footerKind + if (fk === 'pageSubtotal') { + out.push(resolveTailRow(fr, pickedDataRows)) + } else if (fk === 'grandTotal' || fk === 'capital') { + if (last) out.push(resolveTailRow(fr, model.dataRows)) + } else if (repeatFooter || last) { + out.push(fr) + } + } + return out + } + + const picked: RenderRow[] = [] + let used = 0 + let i = req.start + + while (i < model.rows.length) { + const row = model.rows[i]! + if (forcedRows !== null && picked.length >= forcedRows) break + + if (used + row.height > budget) { + // 一行都放不下:强制放一行,避免无限循环(并告警) + if (picked.length === 0) { + if (row.height > req.avail) { + warnings.push({ + code: 'ROW_TOO_TALL', + message: `第 ${(row.dataIndex ?? i) + 1} 行高度 ${row.height.toFixed(1)}mm 超过整页可用高度,已强制放置`, + controlId: model.control.id, + }) + } + picked.push(row) + used += row.height + i++ + } + break + } + + picked.push(row) + used += row.height + i++ + } + + let isLast = i >= model.rows.length + let footerRows: RenderRow[] = [] + + // 仅当本片确有数据行时才挂表尾;否则空尾片(上一页已放完全部行)会渲染出多余的合计 + if (model.footerRows.length > 0 && picked.length > 0) { + // 本片涉及的数据行(映射回原始数据,供本页合计重算) + const pickedDataRows = picked + .filter((r) => r.kind === 'data' && r.dataIndex !== undefined) + .map((r) => model.dataRows[r.dataIndex!]!) + .filter(Boolean) + + if (isLast) { + // 末页需额外容纳总计 / 大写金额行:放不下则把数据行让给下一页 + while (picked.length > 0 && headerH + used + pageFooterH + lastFooterH > req.avail) { + const removed = picked.pop()! + used -= removed.height + i-- + } + if (headerH + used + pageFooterH + lastFooterH <= req.avail || picked.length === 0) { + footerRows = resolveFooterRows(pickedDataRows, true) + } else { + isLast = false + footerRows = resolveFooterRows(pickedDataRows, false) + } + } else { + footerRows = resolveFooterRows(pickedDataRows, false) + } + } + + isLast = isLast && i >= model.rows.length + + return { + headerRows, + rows: picked, + footerRows, + nextStart: i, + height: headerH + used + sumRowHeights(footerRows), + isLast, + warnings, + } +} + +/** 表格全部内容一次性排开的总高度(用于判断是否需要分页) */ +export function totalTableHeight(model: TableModel): number { + return sumRowHeights(model.headerRows) + sumRowHeights(model.rows) + sumRowHeights(model.footerRows) +} + +export type { HAlign } diff --git a/src/core/layout-engine/types.ts b/src/core/layout-engine/types.ts new file mode 100644 index 0000000..c8f1b13 --- /dev/null +++ b/src/core/layout-engine/types.ts @@ -0,0 +1,192 @@ +/** + * Layout Engine 内部页面模型 —— 《OpenPrint-设计方案.md》§7.3 的类型化扩展 + * + * 设计要点: + * 1. 引擎内部一律用 **mm** 参与排版(协议层 in/pt 在入口换算,见 core/units.ts)。 + * 2. 输出是「已定位、已绑定、已测量」的纯数据,不含任何 DOM / Fabric 依赖, + * 因此 renderer-html 可被替换为 renderer-svg / renderer-pdf 而无需改引擎。 + * 3. 坐标系:PlacedNode.left/top 相对**所属 section 左上角**(与协议一致)。 + */ +import type { AnyControl, HAlign, TableColumn, TableControl } from '@/types/control' +import type { PageSetup } from '@/types/template' + +/* -------------------------------- 几何 -------------------------------- */ + +/** 矩形盒(mm) */ +export interface Box { + left: number + top: number + width: number + height: number +} + +/* ------------------------------ 渲染就绪内容 ------------------------------ */ + +/** + * 控件绑定数据后的「渲染就绪内容」。 + * 到这一步所有异步资源(二维码/条码 SVG)已解析完成,渲染器只做字符串拼装。 + */ +export type ResolvedContent = + | { kind: 'text'; text: string } + /** 富文本:已经过 DOMPurify 消毒的 HTML */ + | { kind: 'html'; html: string } + | { kind: 'image'; src: string } + /** 条码 / 二维码矢量输出(打印必须矢量,位图会糊) */ + | { kind: 'svg'; svg: string } + /** 纯形状(矩形 / 线条),样式由 control 决定 */ + | { kind: 'shape' } + /** 解析失败的占位(渲染为虚线框 + 提示文字,不静默吞掉) */ + | { kind: 'placeholder'; label: string } + +/* ------------------------------- 已定位节点 ------------------------------- */ + +export interface PlacedControl extends Box { + kind: 'control' + id: string + angle?: number + content: ResolvedContent + /** 原始控件,渲染器据此取样式(fill/stroke/fontSize…) */ + control: AnyControl +} + +/** + * 表格行类型:决定样式(加粗/底色)与分页时能否被切开。 + * `static` = 设计期填写的静态行(布局网格正文行、数据表静态尾行如备注/签字栏)。 + */ +export type RenderRowKind = 'header' | 'data' | 'group' | 'subtotal' | 'summary' | 'static' + +export interface RenderCell { + text: string + align: HAlign + /** 跨列(组头行 / 合计行用) */ + colSpan?: number + /** 跨行(表头 / 静态 / 布局网格单元格合并用) */ + rowSpan?: number + background?: string + bold?: boolean + /** 字号(pt) */ + fontSize?: number + fontFamily?: string + italic?: boolean + underline?: boolean + color?: string + valign?: 'top' | 'middle' | 'bottom' + /** 单元格斜线(课表角标等):none / down(左上→右下) / up(左下→右上) */ + diagonal?: 'none' | 'down' | 'up' + /** 尾行聚合:该单元格是否为聚合 token({{#totalSum}} 等) */ + isAgg?: boolean + /** 聚合 token 类型(isAgg 为真时有效) */ + tokenKind?: import('./aggregate').AggKind + /** 聚合对应的数据字段键(已去 items[]. 前缀);isAgg 为真时有效 */ + aggField?: string +} + +export interface RenderRow { + kind: RenderRowKind + /** 行高(mm),已由 TableEngine 测量 */ + height: number + cells: RenderCell[] + /** 数据行在原数组中的下标(供 striped 斑马纹判定) */ + dataIndex?: number + /** 表尾块内语义:本页合计 / 总计 / 大写金额 / 静态尾行(仅 footerRows 有意义) */ + footerKind?: 'pageSubtotal' | 'grandTotal' | 'capital' | 'static' +} + +/** 表格在某一页上的切片 */ +export interface PlacedTable extends Box { + kind: 'table' + id: string + control: TableControl + columns: TableColumn[] + /** 归一化后的各列实际宽度(mm,合计 === 表格宽度),渲染器直接用,不再二次推算 */ + columnWidths: number[] + /** 本页表头(可多行;repeatHeader=false 时仅首片有值) */ + headerRows: RenderRow[] + /** 本页数据行切片 */ + rows: RenderRow[] + /** 本页表尾块:合计行 + 静态尾行(repeatFooter=false 时仅末片有值) */ + footerRows: RenderRow[] + /** 是否为该表格的最后一片(渲染器可据此决定收口边框) */ + isLastSlice: boolean +} + +export type PlacedNode = PlacedControl | PlacedTable + +/* --------------------------------- 页 --------------------------------- */ + +export interface LayoutPage { + /** 0-based 索引 */ + index: number + /** 1-based 页码(页码变量 {{page}} 的值) */ + pageNo: number + header: PlacedControl[] + body: PlacedNode[] + footer: PlacedControl[] +} + +/* -------------------------------- 页面度量 ------------------------------- */ + +/** + * 一页的实际可用区域(全部 mm,width/height 为物理尺寸,横向时宽>高)。 + * 这是分页算法的唯一尺寸真理源。 + */ +export interface PageMetrics { + /** 纸张实际宽高(物理尺寸,不随 orientation 再交换) */ + pageWidth: number + pageHeight: number + margin: { top: number; right: number; bottom: number; left: number } + /** 页边距内的内容区宽度 */ + contentWidth: number + /** 页边距内的内容区高度(含页眉页脚) */ + contentHeight: number + headerHeight: number + footerHeight: number + /** 正文可用高度 = contentHeight - headerHeight - footerHeight */ + bodyHeight: number +} + +/* -------------------------------- 警告 -------------------------------- */ + +export type WarningCode = + | 'BINDING_MISSING' // 绑定路径在数据中不存在 + | 'EXPRESSION_ERROR' // 表达式求值失败 + | 'DATASOURCE_NOT_ARRAY' // 表格 dataSource 不是数组 + | 'DATASOURCE_EMPTY' // 表格数据为空 + | 'CONTENT_OVERFLOW' // 内容超出可用高度被截断 + | 'IMAGE_UNRESOLVED' // 图片无法解析 + | 'BARCODE_FAILED' // 条码/二维码生成失败 + | 'CHART_FAILED' // 图表 SVG 生成失败 + | 'PAGE_LIMIT_REACHED' // 触发最大页数保护 + | 'ROW_TOO_TALL' // 单行高于整页可用高度 + +export interface RenderWarning { + code: WarningCode + message: string + controlId?: string +} + +/* ------------------------------- 引擎产物 ------------------------------- */ + +export interface LayoutResult { + pages: LayoutPage[] + metrics: PageMetrics + page: PageSetup + warnings: RenderWarning[] +} + +/** 求值上下文:数据根 + 行上下文 + 页码变量 */ +export interface EvalContext { + /** 业务数据根对象 */ + data: Record + /** 当前行(表格单元格求值时注入) */ + row?: Record + /** 当前行下标(0-based) */ + rowIndex?: number + /** 当前页码(1-based) */ + page?: number + /** 总页数 */ + pages?: number +} + +/** 分页保护上限:防止死循环 / 恶意数据把浏览器打爆 */ +export const MAX_PAGES = 500 diff --git a/src/core/renderer-html/css-generator.spec.ts b/src/core/renderer-html/css-generator.spec.ts new file mode 100644 index 0000000..4483e0d --- /dev/null +++ b/src/core/renderer-html/css-generator.spec.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest' + +import { generateCss, mmv } from '@/core/renderer-html/css-generator' +import { renderDocument } from '@/core/sdk' +import { createDemoTemplate } from '@/repository/mock/data/demo-template' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' + +function makeData(rows: number): Record { + const items = Array.from({ length: rows }, (_, i) => ({ + productCode: `P${i + 1}`, + productName: `商品${i + 1}`, + qty: (i % 5) + 1, + price: 10, + amount: ((i % 5) + 1) * 10, + })) + return { order: { orderNo: 'SO-001' }, items } +} + +describe('generateCss —— 动态样式生成', () => { + const measurer = createCjkMeasurer() + + async function css(opts: Parameters[1]) { + const result = await renderDocument({ template: createDemoTemplate(), data: makeData(10), layout: { measurer } }) + return generateCss(result.metrics, opts) + } + + it('默认输出含 @page 与页面尺寸', async () => { + const out = await css({}) + expect(out).toContain('@page') + expect(out).toContain('mm') + }) + + it('screen 模式注入 --op-scale 缩放变量', async () => { + const out = await css({ screen: true, scale: 2 }) + expect(out).toContain('--op-scale: 2') + }) + + it('screen:false 时 --op-scale 强制为 1(导出/打印不缩放)', async () => { + const out = await css({ screen: false }) + expect(out).toContain('--op-scale: 1') + expect(out).not.toContain('--op-scale: 2') + }) + + it('extraCss 追加到样式表', async () => { + const out = await css({ extraCss: '.x{color:red}' }) + expect(out).toContain('.x{color:red}') + }) + + it('mmv 格式化毫米值', () => { + expect(mmv(10)).toBe('10mm') + expect(mmv(0)).toBe('0mm') + }) +}) diff --git a/src/core/renderer-html/css-generator.ts b/src/core/renderer-html/css-generator.ts new file mode 100644 index 0000000..4c4b8e5 --- /dev/null +++ b/src/core/renderer-html/css-generator.ts @@ -0,0 +1,372 @@ +/** + * CSS 生成器 —— 《OpenPrint-设计方案.md》§5.11(打印样式)/ §7.3(渲染产物) + * + * ## 为什么 CSS 要按模板动态生成 + * + * `@page { size: … }` 的纸张尺寸不能用 CSS 变量(浏览器在解析 `@page` 时不做变量替换), + * 必须把 mm 字面量写死进去。所以每个模板都要生成一份专属样式表。 + * + * ## 与测量器的强耦合约定(改这里必须同步改 measure.ts) + * + * 分页高度是 DomMeasurer 用 `font-size / font-family / line-height / white-space` 量出来的。 + * 渲染时若用另一套字体或行高,实际高度就会和分页假设对不上,表现为**末行被裁**或**页尾空一大块**。 + * 因此表格单元格的字体三件套在这里直接复用 measure.ts / table-engine.ts 的导出常量, + * 而不是重新写一遍字面量。 + */ +import { DEFAULT_FONT_FAMILY, DEFAULT_LINE_HEIGHT } from '@/core/layout-engine/measure' +import { + CELL_PADDING_Y, + DEFAULT_CELL_PADDING, + TABLE_FONT_SIZE, +} from '@/core/layout-engine/table-engine' +import type { PageMetrics } from '@/core/layout-engine/types' +import type { PageDecoration } from '@/types/template' + +/** 设计画布文本控件的默认值(PrintText.styleToFabric),渲染端必须一致才能所见即所得 */ +export const TEXT_DEFAULT_FONT_SIZE = 12 +export const TEXT_DEFAULT_FONT_FAMILY = '"Source Han Sans CN", "PingFang SC", sans-serif' +export const TEXT_DEFAULT_LINE_HEIGHT = 1.16 +export const TEXT_DEFAULT_COLOR = '#000000' + +/** 表格视觉(与设计画布 PrintTable 一致) */ +export const TABLE_BORDER_COLOR = '#333333' +export const TABLE_TEXT_COLOR = '#1f2329' +export const TABLE_HEADER_BG = '#F5F7FA' +/** 通用斑马纹底(原 #FAFAFA 太浅,调深为浅蓝,对比更明显) */ +export const TABLE_STRIPE_BG = '#DCE6F4' +/** 斑马纹·蓝(交替行更深蓝) */ +export const TS_ZEBRA_BLUE_BG = '#C2D4EE' +/** 斑马纹·灰(中性灰交替) */ +export const TS_ZEBRA_GRAY_BG = '#E4E7EB' +/** 隔列着色底 */ +export const TS_BANDED_BG = '#EEF2F8' +/** 绿色表头底 */ +export const TABLE_HEADER_GREEN = '#1F9D57' +/** 单元格斜线颜色(与表格边框同色) */ +export const TABLE_DIAGONAL_COLOR = '#333333' + +/** + * 单元格斜线背景(课表角标等)。 + * 返回可直接拼进 inline style 的「完整声明片段」: + * `background-image:url(...); background-repeat:no-repeat; background-size:100% 100%`。 + * 无斜线返回空串。 + * + * 同时被设计画布(table-design-render)与运行期(html-renderer)复用,保证两端一致。 + * + * 用 SVG data URI 而非 linear-gradient 模拟:SVG 有原生抗锯齿, + * 在非整数像素边界不会产生渐变法常见的锯齿毛刺,清晰度大幅提升。 + * + * ⚠️ 关键:background-image 默认 `background-repeat:repeat`, + * 会把 100×100 的 SVG 平铺满整个单元格 —— 这正是「显示成好几根线」的根因。 + * 必须显式 `no-repeat` + `background-size:100% 100%`,让它只画一条、 + * 且 viewBox 经 preserveAspectRatio=none 精确填满任意宽高比的单元格。 + * + * SVG 内部用双引号;整体经 base64 编码后用单引号包裹 url('...')。 + * base64 不会产生引号,彻底避免与 HTML attribute 的引号冲突。 + */ +export function diagonalBackground( + dir: 'none' | 'down' | 'up' | undefined | null, +): string { + if (!dir || dir === 'none') return '' + const c = TABLE_DIAGONAL_COLOR + // down: 左上角(0,0)→右下角(100,100);up: 左下角(0,100)→右上角(100,0) + const y1 = dir === 'up' ? '100' : '0' + const y2 = dir === 'up' ? '0' : '100' + const svg = `` + const b64 = typeof btoa === 'function' ? btoa(svg) : Buffer.from(svg, 'utf-8').toString('base64') + const url = `url('data:image/svg+xml;base64,${b64}')` + // no-repeat + 100% 100%:只画一条、且精确铺满单元格(见函数注释 ⚠️) + return `background-image:${url};background-repeat:no-repeat;background-size:100% 100%` +} + +export interface CssOptions { + /** + * 屏幕预览模式:给页面加投影、页间距、灰底。 + * 导出 HTML / 无头打印时置 false,避免把预览装饰烤进产物。 + */ + screen?: boolean + /** 预览缩放比(仅 screen 生效,打印时强制 1) */ + scale?: number + /** 追加的用户自定义 CSS */ + extraCss?: string + /** 页面装饰(背景色 + 水印),来自模板 pageSetup */ + pageDecoration?: PageDecoration +} + +/** mm 数值格式化:保留 3 位小数并去掉尾随 0,避免 "12.340000000000001mm" 这种噪声 */ +export function mmv(n: number): string { + const v = Math.round((Number.isFinite(n) ? n : 0) * 1000) / 1000 + return `${v}mm` +} + +/** + * 表格样式块(运行期打印 与 设计期 HTML overlay 共用的唯一真理源)。 + * + * 方案 A 的关键:设计画布上的表格 overlay 与最终打印产物必须由**同一份 CSS** 驱动, + * 否则"所见"与"所印"会随时间漂移。设计期通过 `scope` 前缀把这份规则限制在 overlay 容器内。 + * + * @param scope 作用域选择器前缀(如 `.op-table-overlay `);留空 = 全局(打印产物) + */ +export function tableCss(scope = ''): string { + const s = scope ? `${scope} ` : '' + return ` +${s}.op-table { + /* 用 separate + border-spacing:0 + box-sizing:border-box,让外框落在表格盒"内"侧: + collapse 模式下外框横跨边缘,被 overlay 的 overflow:hidden 切掉右/下边, + 表现为"需拖一下才看得到边框";separate 方案使四边始终完整可见(设计/打印一致)。 */ + border-collapse: separate; + border-spacing: 0; + box-sizing: border-box; + table-layout: fixed; + /* 字体三件套必须与 DomMeasurer 完全一致,否则分页高度会失真 */ + font-family: ${DEFAULT_FONT_FAMILY}; + font-size: ${TABLE_FONT_SIZE}pt; + line-height: ${DEFAULT_LINE_HEIGHT}; + color: ${TABLE_TEXT_COLOR}; +} + +${s}.op-table td, +${s}.op-table th { + padding: ${mmv(CELL_PADDING_Y)} ${mmv(DEFAULT_CELL_PADDING)}; + overflow: hidden; + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: break-word; + font-weight: normal; + text-align: left; + vertical-align: middle; +} + +/* 垂直对齐:vertical-align 不可继承,只能按表加类下发到每个 td */ +${s}.op-table.va-top td { vertical-align: top; } +${s}.op-table.va-middle td { vertical-align: middle; } +${s}.op-table.va-bottom td { vertical-align: bottom; } + +${s}.op-table tr.is-header td { font-weight: bold; } +${s}.op-table tr.is-group td { background: ${TABLE_HEADER_BG}; font-weight: bold; } +${s}.op-table tr.is-subtotal td, +${s}.op-table tr.is-summary td { font-weight: bold; } +${s}.op-table tr.is-striped td { background: ${TABLE_STRIPE_BG}; } +/* 静态行(布局网格正文 / 数据表备注尾行):默认无强调样式, + 单元格视觉以内联样式为准(与设计期 overlay 同一套 resolveCellStyleFor 取值) */ +${s}.op-table tr.is-static td { /* 占位:保持类名与设计期对齐,避免样式遗漏 */ } + +/* ---------- 表格样式预设(Excel 式快速切换;class 由渲染端挂在
上) ---------- */ +/* 默认(none):仅表头加粗,无任何背景色(含标题行)。无需规则,class 仅作占位。 */ + +/* 表头高亮:表头浅灰底 */ +${s}.op-table.ts-header tr.is-header td { background: ${TABLE_HEADER_BG}; } + +/* 斑马纹(单双变色条纹,浅蓝交替,对比更明显) */ +${s}.op-table.ts-zebra tr.is-header td { background: ${TABLE_HEADER_BG}; } +${s}.op-table.ts-zebra tbody tr:not(.is-header):nth-child(even) td { background: ${TABLE_STRIPE_BG}; } + +/* 斑马纹·蓝(交替行更深蓝) */ +${s}.op-table.ts-zebra-blue tr.is-header td { background: ${TABLE_HEADER_BG}; } +${s}.op-table.ts-zebra-blue tbody tr:not(.is-header):nth-child(even) td { background: ${TS_ZEBRA_BLUE_BG}; } + +/* 斑马纹·灰(中性灰交替) */ +${s}.op-table.ts-zebra-gray tr.is-header td { background: ${TABLE_HEADER_BG}; } +${s}.op-table.ts-zebra-gray tbody tr:not(.is-header):nth-child(even) td { background: ${TS_ZEBRA_GRAY_BG}; } + +/* 绿色表头(绿底白字) */ +${s}.op-table.ts-header-green tr.is-header td { background: ${TABLE_HEADER_GREEN}; color: #ffffff; } + +/* 隔列着色(奇偶列交替底) */ +${s}.op-table.ts-banded-cols tr > *:nth-child(even) { background: ${TS_BANDED_BG}; } + +/* 三段式:表头高亮 + 合计 / 总计行强调底(设计期聚合尾行已对齐 is-subtotal/is-summary 类) */ +${s}.op-table.ts-three-segment tr.is-header td { background: ${TABLE_HEADER_BG}; } +${s}.op-table.ts-three-segment tr.is-subtotal td, +${s}.op-table.ts-three-segment tr.is-summary td { background: #EBF2FF; } + +/* 深底表头:深蓝底白字 */ +${s}.op-table.ts-header-dark tr.is-header td { background: #2F54EB; color: #ffffff; } + +/* 课表:全网格 + 表头淡底高亮(角标斜线由单元格 diagonal 控制) */ +${s}.op-table.ts-timetable tr.is-header td { background: ${TABLE_HEADER_BG}; } + +/* 边框模式:每格画上/左边框,末行补下、末列补右 → 外框落在盒内、不被裁切 */ +${s}.op-table.b-all td { border-top: 0.2mm solid var(--op-table-border); border-left: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-all tr:last-child td { border-bottom: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-all td:last-child { border-right: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-horizontal td { border-top: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-horizontal tr:last-child td { border-bottom: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-outline { border: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-none td { border: 0; } +/* 三线表:顶线 + 表头底线 + 底线,无内部横线、无竖线(建模报告 / 财务报表常用) */ +${s}.op-table.b-three-line { border-top: 0.2mm solid var(--op-table-border); border-bottom: 0.2mm solid var(--op-table-border); } +${s}.op-table.b-three-line tr.is-header:last-child td { border-bottom: 0.2mm solid var(--op-table-border); } +` +} + +export function generateCss(metrics: PageMetrics, options: CssOptions = {}): string { + const screen = options.screen ?? true + const scale = options.scale ?? 1 + const pageBg = options.pageDecoration?.backgroundColor ?? '#ffffff' + const { pageWidth, pageHeight, margin, contentWidth, contentHeight } = metrics + + return ` +/* ============ OpenPrint 渲染样式(自动生成,勿手改) ============ */ +:root { + --op-page-w: ${mmv(pageWidth)}; + --op-page-h: ${mmv(pageHeight)}; + --op-scale: ${screen ? scale : 1}; + --op-table-border: ${TABLE_BORDER_COLOR}; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; + background: ${screen ? '#f0f2f5' : '#ffffff'}; + /* 让底色/斑马纹在打印时不被浏览器"省墨"策略抹掉 */ + -webkit-print-color-adjust: exact; + print-color-adjust: exact; +} + +.op-doc { + display: flex; + flex-direction: column; + align-items: center; + ${screen ? 'gap: 10mm; padding: 10mm 0;' : ''} +} + +/* 缩放外壳:transform 不占布局空间,靠外壳撑出滚动区域 */ +.op-page-wrap { + width: calc(var(--op-page-w) * var(--op-scale)); + height: calc(var(--op-page-h) * var(--op-scale)); + flex: none; +} + +.op-page { + position: relative; + width: var(--op-page-w); + height: var(--op-page-h); + overflow: hidden; + background: ${pageBg}; + transform: scale(var(--op-scale)); + transform-origin: top left; + ${screen ? 'box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);' : ''} +} + +/* 水印装饰层:绝对铺满页面、不拦截事件、置于内容之下 */ +.op-watermark { + position: absolute; + inset: 0; + overflow: hidden; + pointer-events: none; + z-index: 0; +} +.op-watermark.op-wm-single { + display: flex; + align-items: center; + justify-content: center; +} +.op-wm-single > span { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + white-space: nowrap; +} + +/* 页边距内的内容盒;页眉/正文/页脚在其中垂直分区 */ +.op-content { + position: absolute; + left: ${mmv(margin.left)}; + top: ${mmv(margin.top)}; + width: ${mmv(contentWidth)}; + height: ${mmv(contentHeight)}; +} + +.op-section { + position: absolute; + left: 0; + width: 100%; + overflow: hidden; +} +.op-header { top: 0; height: ${mmv(metrics.headerHeight)}; } +.op-body { top: ${mmv(metrics.headerHeight)}; height: ${mmv(metrics.bodyHeight)}; } +.op-footer { top: ${mmv(metrics.headerHeight + metrics.bodyHeight)}; height: ${mmv(metrics.footerHeight)}; } + +/* -------------------------------- 控件 -------------------------------- */ + +.op-node { + position: absolute; + transform-origin: left top; +} + +.op-text { + overflow: hidden; + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: break-word; + font-family: ${TEXT_DEFAULT_FONT_FAMILY}; + font-size: ${TEXT_DEFAULT_FONT_SIZE}pt; + line-height: ${TEXT_DEFAULT_LINE_HEIGHT}; + color: ${TEXT_DEFAULT_COLOR}; +} + +.op-richtext { + overflow: hidden; + font-family: ${TEXT_DEFAULT_FONT_FAMILY}; + font-size: ${TEXT_DEFAULT_FONT_SIZE}pt; + line-height: 1.5; + color: ${TEXT_DEFAULT_COLOR}; +} +.op-richtext > *:first-child { margin-top: 0; } +.op-richtext > *:last-child { margin-bottom: 0; } +.op-richtext img { max-width: 100%; } + +.op-image { display: block; } +.op-image > img { display: block; width: 100%; height: 100%; } + +/* 条码 / 二维码:SVG 矢量填满控件框 */ +.op-code > svg { display: block; width: 100%; height: 100%; } + +.op-line > span { + position: absolute; + display: block; +} + +/* 解析失败占位:必须看得见,避免用户拿到"少了一块"的单据而不自知 */ +.op-placeholder { + display: flex; + align-items: center; + justify-content: center; + border: 1px dashed #c0c4cc; + color: #a8abb2; + font-family: ${TEXT_DEFAULT_FONT_FAMILY}; + font-size: 8pt; + overflow: hidden; +} + +/* -------------------------------- 表格 -------------------------------- */ +${tableCss()} +/* ------------------------------ 打印覆盖 ------------------------------ */ + +@page { + size: ${mmv(pageWidth)} ${mmv(pageHeight)}; + margin: 0; +} + +@media print { + html, body { background: #ffffff; } + .op-doc { gap: 0; padding: 0; } + .op-page-wrap { width: auto; height: auto; } + .op-page { + transform: none; + box-shadow: none; + break-after: page; + page-break-after: always; + } + /* 末页不再换页,否则会多打一张空白纸 */ + .op-page-wrap:last-child .op-page { + break-after: auto; + page-break-after: auto; + } +} +${options.extraCss ? `\n/* ---- 自定义样式 ---- */\n${options.extraCss}\n` : ''}`.trim() +} diff --git a/src/core/renderer-html/html-renderer.ts b/src/core/renderer-html/html-renderer.ts new file mode 100644 index 0000000..da8dd06 --- /dev/null +++ b/src/core/renderer-html/html-renderer.ts @@ -0,0 +1,392 @@ +/** + * HTML 渲染器 —— LayoutPage[] → 可打印 HTML + * 真理源:《OpenPrint-设计方案.md》§5.11(打印样式)、§7.3(渲染产物)、§11.2(注入防护) + * + * ## 定位模型 + * + * 引擎已经把一切算成 mm 绝对坐标,渲染器只做「数据 → 字符串」的机械翻译: + * 不做换行、不做测量、不做条件判断。任何在这里出现的排版逻辑都意味着 + * 「测量用一套规则、渲染用另一套」,最终表现为分页错位——所以这里刻意保持"笨"。 + * + * ## 注入防护(§11.2) + * + * - 文本 / 属性值一律 escape + * - richtext 在 data-binder 里已过 DOMPurify,这里直接注入 + * - 条码 SVG 来自 bwip-js / qrcode 生成器,文本已被库转义 + * - 图片 src 过 URL 协议白名单,挡掉 `javascript:` 一类的伪协议 + */ +import type { + ImageControl, + LineControl, + RectControl, + TextControl, +} from '@/types/control' +import type { + LayoutPage, + LayoutResult, + PlacedControl, + PlacedTable, + PlacedNode, + RenderRow, +} from '@/core/layout-engine/types' +import type { WatermarkConfig } from '@/types/template' +import { + generateCss, + mmv, + diagonalBackground, + TEXT_DEFAULT_COLOR, + TEXT_DEFAULT_FONT_FAMILY, + TEXT_DEFAULT_FONT_SIZE, + TEXT_DEFAULT_LINE_HEIGHT, + type CssOptions, +} from './css-generator' +import { MM_TO_PX } from '@/utils/constants' + +/* -------------------------------- 转义 -------------------------------- */ + +const HTML_ESCAPE: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', +} + +export function escapeHtml(v: unknown): string { + return String(v ?? '').replace(/[&<>"']/g, (c) => HTML_ESCAPE[c] ?? c) +} + +/** CSS 值消毒:挡掉 `;` `{}` 与 url(javascript:) 之类的样式注入 */ +function cssValue(v: unknown): string { + return String(v ?? '') + .replace(/[;{}<>]/g, '') + .replace(/expression\s*\(/gi, '') + .replace(/url\s*\(\s*['"]?\s*javascript:/gi, 'url(') + .trim() +} + +/** 图片地址协议白名单 */ +function safeSrc(src: string): string { + const s = src.trim() + if (/^(data:image\/|https?:|blob:|\/|\.\/|\.\.\/)/i.test(s)) return s + // 相对路径(不含协议)也放行 + if (!/^[a-z][a-z0-9+.-]*:/i.test(s)) return s + return '' +} + +function styleAttr(pairs: Array): string { + const s = pairs.filter(Boolean).join(';') + return s ? ` style="${escapeHtml(s)}"` : '' +} + +/* ------------------------------ 节点几何 ------------------------------ */ + +function boxStyles(node: PlacedNode): string[] { + const out = [ + `left:${mmv(node.left)}`, + `top:${mmv(node.top)}`, + `width:${mmv(node.width)}`, + `height:${mmv(node.height)}`, + ] + const angle = node.kind === 'control' ? node.angle : undefined + if (angle) out.push(`transform:rotate(${Math.round(angle * 100) / 100}deg)`) + return out +} + +/* -------------------------------- 控件 -------------------------------- */ + +function renderText(node: PlacedControl, text: string): string { + const s = (node.control as TextControl).style ?? {} + const style = [ + ...boxStyles(node), + `font-size:${s.fontSize ?? TEXT_DEFAULT_FONT_SIZE}pt`, + `color:${cssValue(s.fill ?? TEXT_DEFAULT_COLOR)}`, + `font-family:${cssValue(s.fontFamily ?? TEXT_DEFAULT_FONT_FAMILY)}`, + `font-weight:${s.fontWeight === 'bold' ? 'bold' : 'normal'}`, + `font-style:${s.fontStyle === 'italic' ? 'italic' : 'normal'}`, + `text-decoration:${s.textDecoration === 'underline' ? 'underline' : 'none'}`, + `text-align:${s.textAlign ?? 'left'}`, + `line-height:${s.lineHeight ?? TEXT_DEFAULT_LINE_HEIGHT}`, + s.letterSpacing ? `letter-spacing:${s.letterSpacing}pt` : null, + ] + return `
${escapeHtml(text)}
` +} + +function renderImage(node: PlacedControl, src: string): string { + const c = node.control as ImageControl + const url = safeSrc(src) + if (!url) return renderPlaceholder(node, '图片') + const radius = c.cornerRadius ? `border-radius:${c.cornerRadius}px` : null + const imgStyle = [`object-fit:${c.fit ?? 'contain'}`, radius] + return ( + `
` + + `
` + ) +} + +function renderCode(node: PlacedControl, svg: string): string { + // svg 由 bwip-js / qrcode 生成,内容已由库自身转义 + return `
${svg}
` +} + +function renderRichText(node: PlacedControl, html: string): string { + // html 已在 data-binder 中经 DOMPurify 消毒 + return `
${html}
` +} + +function renderShape(node: PlacedControl): string { + const c = node.control + if (c.type === 'line') { + const line = c as LineControl + const stroke = cssValue(line.stroke ?? '#000000') + const sw = line.strokeWidth ?? 1 + const dash = line.strokeDashArray?.length ? 'dashed' : 'solid' + // 协议只存外接框,宽>=高视为水平线(与 PrintLine 的 MVP 约定一致) + const horizontal = node.height <= node.width + const inner = horizontal + ? `left:0;right:0;top:50%;transform:translateY(-50%);border-top:${sw}px ${dash} ${stroke}` + : `top:0;bottom:0;left:50%;transform:translateX(-50%);border-left:${sw}px ${dash} ${stroke}` + return ( + `
` + + `
` + ) + } + + const rect = c as RectControl + const fill = rect.fill && rect.fill !== 'transparent' ? cssValue(rect.fill) : 'transparent' + const sw = rect.strokeWidth ?? 1 + const isCircle = rect.shape === 'circle' + + // 圆角:优先四角独立值,其次统一 cornerRadius,最后无圆角 + let radiusStyle: string | null = null + if (isCircle) { + radiusStyle = 'border-radius:50%' + } else if ( + rect.cornerRadiusTL != null || + rect.cornerRadiusTR != null || + rect.cornerRadiusBR != null || + rect.cornerRadiusBL != null + ) { + const cr = rect.cornerRadius ?? 0 + const tl = rect.cornerRadiusTL ?? cr + const tr = rect.cornerRadiusTR ?? cr + const br = rect.cornerRadiusBR ?? cr + const bl = rect.cornerRadiusBL ?? cr + radiusStyle = `border-radius:${tl}px ${tr}px ${br}px ${bl}px` + } else if (rect.cornerRadius) { + radiusStyle = `border-radius:${rect.cornerRadius}px` + } + + const style = [ + ...boxStyles(node), + `background:${fill}`, + sw > 0 ? `border:${sw}px solid ${cssValue(rect.stroke ?? '#000000')}` : 'border:0', + radiusStyle, + // 虚线:画布侧走 Fabric setLineDash,导出侧用 border-style 近似 + rect.strokeDashArray?.length ? 'border-style:dashed' : null, + ] + return `
` +} + +function renderPlaceholder(node: PlacedControl, label: string): string { + return ( + `
` + + `${escapeHtml(label)}
` + ) +} + +function renderControl(node: PlacedControl): string { + switch (node.content.kind) { + case 'text': + return renderText(node, node.content.text) + case 'html': + return renderRichText(node, node.content.html) + case 'image': + return renderImage(node, node.content.src) + case 'svg': + return renderCode(node, node.content.svg) + case 'shape': + return renderShape(node) + case 'placeholder': + return renderPlaceholder(node, node.content.label) + default: + return '' + } +} + +/* -------------------------------- 表格 -------------------------------- */ + +const ROW_CLASS: Record = { + header: 'is-header', + data: 'is-data', + group: 'is-group', + subtotal: 'is-subtotal', + summary: 'is-summary', + static: 'is-static', +} + +/** + * 输出单元格完整样式。 + * 只写**显式设置过**的属性,其余交给 `tableCss()` 的类规则兜底 —— 设计期 overlay + * (table-design-render.ts)走的是同一套取舍,两端因此像素级一致。 + */ +function renderRow(row: RenderRow, striped: boolean): string { + const cls = [ROW_CLASS[row.kind]] + if (striped && row.kind === 'data' && (row.dataIndex ?? 0) % 2 === 1) cls.push('is-striped') + + const cells = row.cells + .map((cell) => { + const style = [ + `text-align:${cell.align ?? 'left'}`, + cell.background ? `background-color:${cssValue(cell.background)}` : null, + cell.bold !== undefined ? `font-weight:${cell.bold ? 'bold' : 'normal'}` : null, + cell.fontSize ? `font-size:${cell.fontSize}pt` : null, + cell.fontFamily ? `font-family:${cssValue(cell.fontFamily)}` : null, + cell.italic !== undefined ? `font-style:${cell.italic ? 'italic' : 'normal'}` : null, + cell.underline !== undefined + ? `text-decoration:${cell.underline ? 'underline' : 'none'}` + : null, + cell.color ? `color:${cssValue(cell.color)}` : null, + cell.valign ? `vertical-align:${cell.valign}` : null, + cell.diagonal ? diagonalBackground(cell.diagonal) : null, + ] + const span = cell.colSpan && cell.colSpan > 1 ? ` colspan="${cell.colSpan}"` : '' + const rspan = cell.rowSpan && cell.rowSpan > 1 ? ` rowspan="${cell.rowSpan}"` : '' + return `${escapeHtml(cell.text) || '
'}` + }) + .join('') + + return `
${cells}` +} + +function renderTable(node: PlacedTable): string { + const opts = node.control.options ?? {} + const borders = opts.borders ?? 'all' + const striped = opts.striped ?? false + const va = opts.verticalAlign ?? 'middle' + const tableStyle = opts.tableStyle ?? 'none' + + const cols = node.columnWidths + .map((w) => ``) + .join('') + + const body: string[] = [] + for (const row of node.headerRows) body.push(renderRow(row, false)) + for (const row of node.rows) body.push(renderRow(row, striped)) + for (const row of node.footerRows) body.push(renderRow(row, false)) + + const style = [ + `left:${mmv(node.left)}`, + `top:${mmv(node.top)}`, + `width:${mmv(node.width)}`, + ] + + return ( + `
${cols}${body.join('')}
` + ) +} + +function renderNode(node: PlacedNode): string { + return node.kind === 'table' ? renderTable(node) : renderControl(node) +} + +/* --------------------------------- 页 --------------------------------- */ + +function renderSection(cls: string, nodes: PlacedNode[]): string { + if (nodes.length === 0) return `
` + return `
${nodes.map(renderNode).join('')}
` +} + +/** + * 生成水印装饰层 HTML。 + * - 居中单个:flex 居中 + 绝对定位旋转 span + * - 全页平铺:用单个旋转水印瓦片生成的 SVG 作 background-image(repeat) + * 文本已 escape,颜色经 cssValue 消毒。 + */ +function renderWatermark(wm: WatermarkConfig): string { + if (!wm.enabled) return '' + const color = cssValue(wm.color) || '#cccccc' + const size = Math.max(1, wm.fontSize) + const rot = Number.isFinite(wm.rotation) ? wm.rotation : 0 + const text = escapeHtml(wm.text || '') + + if (!wm.tile) { + const span = `${text}` + return `
${span}
` + } + + // 平铺:瓦片尺寸(mm)随字号放大,瓦片内绘制旋转文本 + const spacingMm = Math.max(size * 2.6, 14) + const tilePx = Math.max(1, Math.ceil(spacingMm * (MM_TO_PX))) + const fontSizePx = Math.max(1, size) * MM_TO_PX + const svg = + `` + + `${escapeHtml(wm.text || '')}` + + `` + const uri = `data:image/svg+xml,${encodeURIComponent(svg)}` + const style = `background-image:url('${uri}');background-repeat:repeat;background-size:${mmv(spacingMm)} ${mmv(spacingMm)};` + return `
` +} + +export function renderPage(page: LayoutPage, watermark?: WatermarkConfig | null): string { + const wm = watermark ? renderWatermark(watermark) : '' + return ( + `
` + + wm + + `
` + + renderSection('op-header', page.header) + + renderSection('op-body', page.body) + + renderSection('op-footer', page.footer) + + `
` + ) +} + +/* ------------------------------- 文档装配 ------------------------------ */ + +export interface RenderHtmlOptions extends CssOptions { + /** 生成完整 HTML 文档(含 head/style);false 时只返回 `.op-doc` 片段 */ + fullDocument?: boolean + /** 文档标题(打印时可能作为默认文件名) */ + title?: string + /** 打印后自动关闭窗口(无头/新窗口打印场景用) */ + autoPrint?: boolean +} + +/** 只输出内容片段(宿主页面自行注入样式时用) */ +export function renderFragment(result: LayoutResult, watermark?: WatermarkConfig | null): string { + return `
${result.pages.map((p) => renderPage(p, watermark)).join('')}
` +} + +/** 输出完整可独立打开 / 打印的 HTML 文档 */ +export function renderHtml(result: LayoutResult, options: RenderHtmlOptions = {}): string { + const css = generateCss(result.metrics, options) + const fragment = renderFragment(result, options.pageDecoration?.watermark) + + if (options.fullDocument === false) return fragment + + const autoPrint = options.autoPrint + ? '' + : '' + + return ` + + + + +${escapeHtml(options.title ?? 'OpenPrint')} + + + +${fragment} +${autoPrint} + +` +} + +/** 供预览面板单独取样式用(iframe 里换缩放不必重算 HTML) */ +export function renderStyle(result: LayoutResult, options: CssOptions = {}): string { + return generateCss(result.metrics, options) +} diff --git a/src/core/renderer-html/index.ts b/src/core/renderer-html/index.ts new file mode 100644 index 0000000..13c5ea3 --- /dev/null +++ b/src/core/renderer-html/index.ts @@ -0,0 +1,10 @@ +/** renderer-html 对外出口 */ +export { + escapeHtml, + renderFragment, + renderHtml, + renderPage, + renderStyle, + type RenderHtmlOptions, +} from './html-renderer' +export { generateCss, mmv, type CssOptions } from './css-generator' diff --git a/src/core/renderer-html/renderer-html.spec.ts b/src/core/renderer-html/renderer-html.spec.ts new file mode 100644 index 0000000..5394c07 --- /dev/null +++ b/src/core/renderer-html/renderer-html.spec.ts @@ -0,0 +1,117 @@ +import { describe, expect, it } from 'vitest' + +import { layout } from '@/core/layout-engine/pagination-engine' +import { renderHtml, renderPage, renderStyle } from '@/core/renderer-html' +import { createDemoTemplate } from '@/repository/mock/data/demo-template' +import { createCjkMeasurer } from '@/core/__tests__/cjk-measurer' +import type { RectControl } from '@/types/control' +import type { LayoutPage, PlacedControl } from '@/core/layout-engine/types' + +function rectControl(overrides: Partial): RectControl { + return { + id: 'r1', + type: 'rect', + left: 0, + top: 0, + width: 40, + height: 25, + ...overrides, + } +} + +function makeData(rows: number): Record { + const items = Array.from({ length: rows }, (_, i) => { + const qty = (i % 5) + 1 + const price = 10 + (i % 9) + return { + productCode: `P${String(i + 1).padStart(4, '0')}`, + productName: `商品${i + 1}`, + spec: '规格A', + unit: '件', + qty, + price, + amount: qty * price, + } + }) + return { order: { orderNo: 'SO-001' }, customer: { name: '客户' }, items } +} + +describe('renderer-html —— HTML 输出', () => { + const template = createDemoTemplate() + const measurer = createCjkMeasurer() + + it('renderHtml 输出多页文档结构', async () => { + const result = await layout(template, makeData(30), { measurer }) + const html = renderHtml(result, { screen: false }) + expect(html).toContain('op-doc') + expect(html).toContain('op-page') + expect(html).toContain('op-page-wrap') + expect(html).toContain('销售出库单') + }) + + it('renderStyle 注入打印分页样式', async () => { + const result = await layout(template, makeData(10), { measurer }) + const style = renderStyle(result, { screen: false }) + expect(style).toContain('@page') + expect(style).toContain('break-after') + }) + + describe('renderShape —— 形状样式', () => { + /** 用一个只含单个形状控件的页面直接喂给 renderPage 验证 CSS 输出 */ + function renderShapeHtml(c: RectControl): string { + const node: PlacedControl = { + kind: 'control', + id: c.id, + left: c.left, + top: c.top, + width: c.width, + height: c.height, + content: { kind: 'shape' }, + control: c, + } + const page: LayoutPage = { + index: 0, + pageNo: 1, + header: [], + body: [node], + footer: [], + } + return renderPage(page) + } + + it('四角独立圆角输出四值 border-radius', () => { + const html = renderShapeHtml( + rectControl({ cornerRadiusTL: 5, cornerRadiusTR: 10, cornerRadiusBR: 15, cornerRadiusBL: 20 }), + ) + expect(html).toContain('border-radius:5px 10px 15px 20px') + }) + + it('仅设置部分角时,未设置角回落到统一 cornerRadius', () => { + const html = renderShapeHtml(rectControl({ cornerRadius: 8, cornerRadiusTR: 30 })) + // TR=30 覆盖统一值,其余三角=8 + expect(html).toContain('border-radius:8px 30px 8px 8px') + }) + + it('统一 cornerRadius 输出单值 border-radius', () => { + const html = renderShapeHtml(rectControl({ cornerRadius: 8 })) + expect(html).toContain('border-radius:8px') + expect(html).not.toContain('8px 8px 8px 8px') + }) + + it('虚线模式输出 border-style:dashed', () => { + const html = renderShapeHtml(rectControl({ strokeDashArray: [6, 4] })) + expect(html).toContain('border-style:dashed') + }) + + it('圆形(正方形外接框)输出 border-radius:50%', () => { + const html = renderShapeHtml(rectControl({ shape: 'circle', width: 40, height: 40 })) + expect(html).toContain('border-radius:50%') + expect(html).toContain('op-circle') + }) + + it('圆形宽高不等时为椭圆(仍用 border-radius:50%,CSS 自动椭圆化)', () => { + const html = renderShapeHtml(rectControl({ shape: 'circle', width: 60, height: 30 })) + expect(html).toContain('border-radius:50%') + }) + }) +}) diff --git a/src/core/renderer-html/richtext.spec.ts b/src/core/renderer-html/richtext.spec.ts new file mode 100644 index 0000000..cd1f431 --- /dev/null +++ b/src/core/renderer-html/richtext.spec.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest' + +import { resolveControlContent } from '@/core/layout-engine/data-binder' +import { renderPage } from '@/core/renderer-html/html-renderer' +import type { RichTextControl } from '@/types/control' +import type { EvalContext, LayoutPage } from '@/core/layout-engine/types' + +/** 构造最小富文本控件 */ +function makeRichText(value: string, id = 'rt-1'): RichTextControl { + return { id, type: 'richtext', left: 0, top: 0, width: 80, height: 24, value } +} + +describe('richtext —— 数据绑定 + 渲染链路', () => { + const ctx: EvalContext = { data: {}, row: undefined, page: 2, pages: 5 } + + it('resolveControlContent 插值 {{page}}/{{pages}} 页码变量', async () => { + const res = await resolveControlContent( + makeRichText('

第 {{page}} 页 / 共 {{pages}} 页

'), + ctx, + ) + expect(res.content.kind).toBe('html') + if (res.content.kind === 'html') { + expect(res.content.html).toContain('第 2 页 / 共 5 页') + } + }) + + // 注:happy-dom 环境 DOMPurify 的标签解析与真实浏览器不同(如

/ + + + + diff --git a/src/design/canvas/CanvasDesigner.ts b/src/design/canvas/CanvasDesigner.ts new file mode 100644 index 0000000..a3b9e2c --- /dev/null +++ b/src/design/canvas/CanvasDesigner.ts @@ -0,0 +1,873 @@ +/** + * CanvasDesigner —— Fabric 画布内核 + * + * 职责:画布初始化 / 页面(A4 白纸)/ 缩放平移 / 控件增删改 / 选中事件 / 序列化。 + * 坐标约定(《标尺与辅助系统》§4.3): + * - 页面左上角 = 画布坐标 (0,0),zoom=1 时 1mm = MM_TO_PX px + * - 居中/平移/缩放全部走 viewportTransform,标尺层直接读 vt[4]/vt[5] + zoom + * - 协议层坐标:body 控件相对"页边距内容区"左上角;zone 子控件相对色带左上角(§5.14.2) + */ +import { Canvas, Rect, Line, Point, FabricObject, Text, Pattern, type TPointerEventInfo } from 'fabric' +import type { AnyControl, ZoneControl } from '@/types/control' +import type { PageSetup, WatermarkConfig } from '@/types/template' +import { MM_TO_PX, ZOOM_MAX, ZOOM_MIN } from '@/utils/constants' +import { toMm } from '@/core/units' +import { createFabricControl, isPrintObject, PrintText, PrintZone, type IPrintObject } from './controls' +import { SmartGuides } from './guides/SmartGuides' +import { PrintTable } from './controls/PrintTable' +import { ensureCells } from '@/core/layout-engine/table-cells' +import { computeGridLayout, hitTestCell } from './table-design-render' +import { computePhysicalPageCount } from './page-geometry' +import { pageGapPx, modelTopToCanvasY, canvasYToModelTop } from './page-gap' +import type { TableControl } from '@/types/control' + +export interface ViewportState { + zoom: number + offsetX: number + offsetY: number +} + +export interface CanvasDesignerEvents { + /** 选中变化(controlId 列表;空数组 = 取消选中) */ + onSelectionChange?: (ids: string[]) => void + /** 控件几何/属性被画布交互修改(拖拽/缩放手柄结束) */ + onObjectModified?: (control: AnyControl) => void + /** 视口变化(缩放/平移),驱动标尺重绘 */ + onViewportChange?: (vp: ViewportState) => void + /** 双击表格单元格进入编辑:返回物化了 cells 的控件与命中行列 */ + onCellEdit?: (info: { controlId: string; control: AnyControl; row: number; col: number }) => void + /** + * 画布变换心跳:对象拖拽/缩放/旋转或视口变化时触发。 + * 表格 HTML overlay 靠它重算定位(不走 store 直连,避免 store ↔ 画布循环依赖)。 + */ + onTransformTick?: () => void + /** 物理页数变化(内容推导或手动分页触发),驱动右侧页面面板刷新 */ + onPageCountChange?: (count: number) => void +} + +type PrintFabricObject = FabricObject & IPrintObject + +export class CanvasDesigner { + canvas!: Canvas + /** 页面尺寸(mm,内部统一换算) */ + pageWidthMm = 210 + pageHeightMm = 297 + marginMm = { top: 10, bottom: 10, left: 10, right: 10 } + + private pageRect!: Rect + /** 第 2..N 页白色背景(与 pageRect 同款,按 stride 堆叠) */ + private extraPageRects: Rect[] = [] + /** 每页内容区参考线(蓝色虚线矩形,非交互、不导出) */ + private pageGuides: Rect[] = [] + /** 每页内容区底部的分页虚线(红色,与渲染 floor(top/bodyH) 落页边界一致) */ + private pageBreaks: Line[] = [] + /** 分页线标签("分页")与每页页码标签("第 N 页") */ + private pageLabels: Text[] = [] + /** 当前文档物理页数 = max(内容推导页数, 手动分页 minPages),最小 1 */ + private pageCount = 1 + /** 手动分页下限:用户「加页/减页」调整;0 = 完全由内容推导 */ + private minPages = 0 + /** + * 页间视觉间距(px,仅设计态):上下两页之间留白,便于分辨页边界。 + * 只作用在画布坐标(模型 mm 保持 flush,导出/渲染不变)。 + */ + private readonly PAGE_GAP_PX = 24 + private smartGuides?: SmartGuides + /** 水印装饰层(非交互、不导出、不进序列化),随页面尺寸/配置重建 */ + private watermarkObjs: FabricObject[] = [] + private lastWatermark?: WatermarkConfig + private events: CanvasDesignerEvents = {} + private spacePanning = false + /** 用户手动缩放后置 true,窗口 resize 不再自动 refit */ + private userZoomed = false + + /** 初始化画布。container 决定画布像素尺寸( ResizeObserver 自适应) */ + init(el: HTMLCanvasElement, container: HTMLElement, events: CanvasDesignerEvents = {}): void { + this.events = events + // Fabric 7 默认 origin 为 center——全局改回 left/top,协议坐标语义保持一致 + FabricObject.ownDefaults.originX = 'left' + FabricObject.ownDefaults.originY = 'top' + this.canvas = new Canvas(el, { + width: container.clientWidth, + height: container.clientHeight, + backgroundColor: '#e8eaed', + preserveObjectStacking: true, + selection: true, + stopContextMenu: true, + fireRightClick: true, + }) + + this.buildPage() + this.fitToContainer(container) + this.bindEvents() + + this.smartGuides = new SmartGuides(this.canvas, () => this.pageRect, () => this.marginMm) + + new ResizeObserver(() => { + this.canvas.setDimensions({ + width: container.clientWidth, + height: container.clientHeight, + }) + // 布局稳定前(面板展开/窗口调整)保持适配;用户手动缩放后不再打扰 + if (!this.userZoomed) this.fitToContainer(container) + this.canvas.requestRenderAll() + this.emitViewport() + }).observe(container) + } + + dispose(): void { + window.removeEventListener('keydown', this.onKeyDown) + window.removeEventListener('keyup', this.onKeyUp) + this.smartGuides?.dispose() + void this.canvas.dispose() + } + + /* -------------------------------- 页面 -------------------------------- */ + + private buildPage(): void { + this.pageRect = new Rect({ + left: 0, + top: 0, + width: this.pageWidthMm * MM_TO_PX, + height: this.pageHeightMm * MM_TO_PX, + fill: this.lastPageBackground || '#ffffff', + selectable: false, + evented: false, + hoverCursor: 'default', + shadow: '0 2px 12px rgba(0,0,0,0.15)', + // 标记:序列化/选中时排除 + excludeFromExport: true, + } as never) + ;(this.pageRect as unknown as { isPageRect: boolean }).isPageRect = true + this.canvas.add(this.pageRect) + this.pageRect.canvas = this.canvas + // 首次构建后按当前内容推导页数并铺好分页线 / 参考线 / 页码 + this.rebuildPages() + } + + /** 记忆最近一次背景色(buildPage 首帧用) */ + private lastPageBackground = '#ffffff' + + /* ------------------------ 页间留白(模型 mm 与画布 px 换算) ------------------------ */ + // 复用 page-gap.ts 纯函数,避免实现与 page-gap.spec.ts 测试逻辑分叉(单一真相来源)。 + + /** 模型 top(mm, flush) 所在页索引 → 该页之前的累计页间距(px) */ + private gapPx(topMm: number): number { + return pageGapPx(topMm, this.pageHeightMm, this.PAGE_GAP_PX) + } + + /** 模型 top(mm, flush) → 画布绝对 y(px):页边距 + 页内偏移 + 累计页间距 */ + private modelTopToY(topMm: number): number { + return modelTopToCanvasY(topMm, this.marginMm.top * MM_TO_PX, this.pageHeightMm, this.PAGE_GAP_PX) + } + + /** 画布绝对 y(px) → 模型 top(mm, flush):反解累计页间距(近页边界最多迭代 2 次收敛) */ + private yToModelTop(y: number): number { + return canvasYToModelTop(y, this.marginMm.top * MM_TO_PX, this.pageHeightMm, this.PAGE_GAP_PX) + } + + /* -------------------------------- 页面 -------------------------------- */ + + /** + * 由正文控件最低底部推导物理页数:floor(maxBottomMm / pageHeightMm) + 1。 + * 与渲染端 layoutStaticOnly 的 `floor(top/bodyH)` 落页口径一致(body 控件按 + * marginTop + top 绝对定位,越过内容区底部即落入下一页)。 + */ + private computeContentPageCount(): number { + let maxBottomMm = 0 + for (const obj of this.getPrintObjects()) { + if (obj instanceof PrintZone) continue + // 模型 mm(flush)口径:先反解页间距再算底部,与渲染/导出一致 + const topMm = this.yToModelTop(obj.top ?? 0) + const bottom = topMm + obj.getScaledHeight() / MM_TO_PX + if (bottom > maxBottomMm) maxBottomMm = bottom + } + return computePhysicalPageCount(maxBottomMm, this.pageHeightMm) + } + + /** 有效物理页数 = max(内容推导, 手动分页下限),最小 1 */ + private computePageCount(): number { + return Math.max(this.minPages, this.computeContentPageCount()) + } + + /** 内容推导页数(不含手动分页),供 store 的「加页」计算 */ + get contentPageCount(): number { + return this.computeContentPageCount() + } + + /** 当前有效物理页数 */ + get effectivePageCount(): number { + return this.pageCount + } + + /** 手动分页下限(0 = 完全由内容推导) */ + get manualPageCount(): number { + return this.minPages + } + + /** 设置手动分页下限并重建页面视觉 */ + setManualPageCount(n: number): void { + this.minPages = Math.max(0, Math.floor(n) || 0) + this.rebuildPages() + } + + /** + * 依据正文控件布局重建多页视觉:第 2..N 页白色背景、每页内容区参考线、 + * 内容区底部的分页虚线 + 「分页」标签、每页「第 N 页」页码标签。 + * 背景/参考线/分页线全部排在控件下方(控件 bringToFront),避免遮挡正文。 + */ + private rebuildPages(): void { + const count = Math.max(1, this.computePageCount()) + const changed = count !== this.pageCount + this.pageCount = count + const stale = [ + ...this.extraPageRects, + ...this.pageGuides, + ...this.pageBreaks, + ...this.pageLabels, + ] + for (const o of stale) this.canvas.remove(o) + this.extraPageRects = [] + this.pageGuides = [] + this.pageBreaks = [] + this.pageLabels = [] + + const pageW = this.pageWidthMm * MM_TO_PX + const pageH = this.pageHeightMm * MM_TO_PX + const stride = pageH + this.PAGE_GAP_PX + const ml = this.marginMm.left * MM_TO_PX + const mt = this.marginMm.top * MM_TO_PX + const mr = this.marginMm.right * MM_TO_PX + const mb = this.marginMm.bottom * MM_TO_PX + const bodyH = Math.max(1, pageH - mt - mb) + const contentW = Math.max(1, pageW - ml - mr) + const bg = this.lastPageBackground || '#ffffff' + + // 第 2..N 页背景(按 stride 堆叠,带阴影) + for (let i = 1; i < count; i++) { + const r = new Rect({ + left: 0, + top: i * stride, + width: pageW, + height: pageH, + fill: bg, + selectable: false, + evented: false, + hoverCursor: 'default', + shadow: '0 2px 12px rgba(0,0,0,0.15)', + excludeFromExport: true, + objectCaching: false, + } as never) + ;(r as unknown as { isPageRect: boolean }).isPageRect = true + this.canvas.add(r) + this.extraPageRects.push(r) + } + + // 每页内容区参考线(蓝色虚线) + for (let i = 0; i < count; i++) { + const g = new Rect({ + left: ml, + top: i * stride + mt, + width: contentW, + height: bodyH, + fill: '', + stroke: '#1677ff', + strokeWidth: 1, + strokeDashArray: [4, 3], + selectable: false, + evented: false, + hoverCursor: 'default', + excludeFromExport: true, + objectCaching: false, + } as never) + ;(g as unknown as { isMarginGuide: boolean }).isMarginGuide = true + this.canvas.add(g) + this.pageGuides.push(g) + } + + // 每页内容区底部的分页虚线(最后一页不画),并标注「分页」 + for (let i = 0; i < count - 1; i++) { + const y = i * stride + mt + bodyH + const line = new Line([ml, y, ml + contentW, y], { + stroke: '#f5222d', + strokeWidth: 1, + strokeDashArray: [6, 4], + selectable: false, + evented: false, + excludeFromExport: true, + objectCaching: false, + } as never) + this.canvas.add(line) + this.pageBreaks.push(line) + const tag = new Text('分页', { + left: ml + 4, + top: y - 13, + fontSize: 9, + fill: '#f5222d', + selectable: false, + evented: false, + excludeFromExport: true, + } as never) + this.canvas.add(tag) + this.pageLabels.push(tag) + } + + // 每页页码标签(左上角) + for (let i = 0; i < count; i++) { + const lbl = new Text(`第 ${i + 1} 页`, { + left: ml + 4, + top: i * stride + 4, + fontSize: 9, + fill: '#999999', + selectable: false, + evented: false, + excludeFromExport: true, + } as never) + this.canvas.add(lbl) + this.pageLabels.push(lbl) + } + + // 【层级修复】背景/参考线/分页线全部排在正文控件之下,杜绝「第二页盖住控件」 + for (const obj of this.getPrintObjects()) this.canvas.bringObjectToFront(obj) + + this.canvas.requestRenderAll() + if (changed) this.events.onPageCountChange?.(count) + } + + /** 正文控件布局变化后由外部调用,重算页数与分页线 */ + refreshPages(): void { + this.rebuildPages() + } + + /** 显示/隐藏页边距参考线(影响全部页) */ + setMarginGuidesVisible(visible: boolean): void { + for (const g of this.pageGuides) g.visible = visible + this.canvas.requestRenderAll() + } + + /** + * 应用页面设置。 + * 【约定】PageSetup.width/height 一律是**物理尺寸**(实际打印宽高,横向时宽>高), + * orientation 仅是派生标志(UI 用)。方向切换由面板在改 width/height 时完成, + * 这里**不再**按 orientation 二次交换 —— 否则横向页会被换回纵向(旧 bug)。 + * 渲染端 computePageMetrics / 导出 export-pdf 同口径(w>h 判定),全链路所见即所得。 + */ + setPage(setup: PageSetup): void { + this.pageWidthMm = toMm(setup.width, setup.unit) + this.pageHeightMm = toMm(setup.height, setup.unit) + this.marginMm = { + top: toMm(setup.margin.top, setup.unit), + bottom: toMm(setup.margin.bottom, setup.unit), + left: toMm(setup.margin.left, setup.unit), + right: toMm(setup.margin.right, setup.unit), + } + this.minPages = setup.minPages ?? 0 + this.pageRect.set({ + width: this.pageWidthMm * MM_TO_PX, + height: this.pageHeightMm * MM_TO_PX, + fill: this.lastPageBackground || '#ffffff', + }) + // 页面尺寸变化:重建全部页视觉(背景/参考线/分页线/页码)后再注入水印 + this.rebuildPages() + for (const z of this.getZones()) z.relayout(this.pageWidthMm, this.pageHeightMm) + this.syncWatermark() + this.canvas.requestRenderAll() + } + + /** 设置画布页面背景色(不导出、随模板持久化);同步填充所有页 */ + setPageBackground(color: string): void { + this.lastPageBackground = color || '#ffffff' + this.pageRect.set({ fill: this.lastPageBackground }) + for (const r of this.extraPageRects) r.set({ fill: this.lastPageBackground }) + this.canvas.requestRenderAll() + } + + /** 设置画布水印(enabled=false 时清空;tile=全页平铺 / 否则居中单个) */ + setWatermark(cfg: WatermarkConfig | undefined): void { + this.lastWatermark = cfg && cfg.enabled ? cfg : undefined + this.syncWatermark() + } + + /** 重建水印装饰层:先清旧对象,再按配置绘制 */ + private syncWatermark(): void { + for (const o of this.watermarkObjs) this.canvas.remove(o) + this.watermarkObjs = [] + const cfg = this.lastWatermark + if (!cfg) { + this.canvas.requestRenderAll() + return + } + + const pageWpx = this.pageWidthMm * MM_TO_PX + const pageHpx = this.pageHeightMm * MM_TO_PX + const fontSizePx = Math.max(1, cfg.fontSize) * MM_TO_PX + const fontFamily = '"Source Han Sans CN", "PingFang SC", sans-serif' + + if (cfg.tile) { + // 全页平铺:用离屏 canvas 生成单个旋转水印瓦片,作为 pageRect 之上的 Pattern + const spacingMm = Math.max(cfg.fontSize * 2.6, 14) + const tilePx = Math.max(1, Math.ceil(spacingMm * MM_TO_PX)) + const tile = document.createElement('canvas') + tile.width = tilePx + tile.height = tilePx + const ctx = tile.getContext('2d') + if (ctx) { + ctx.clearRect(0, 0, tilePx, tilePx) + ctx.translate(tilePx / 2, tilePx / 2) + ctx.rotate((cfg.rotation * Math.PI) / 180) + ctx.fillStyle = cfg.color + ctx.font = `${fontSizePx}px ${fontFamily}` + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(cfg.text || '', 0, 0) + } + const pattern = new Pattern({ source: tile, repeat: 'repeat' }) + const rect = new Rect({ + left: 0, + top: 0, + width: pageWpx, + height: pageHpx, + fill: pattern as unknown as string, + selectable: false, + evented: false, + excludeFromExport: true, + objectCaching: false, + } as never) + ;(rect as unknown as { isWatermark: boolean }).isWatermark = true + this.canvas.insertAt(1, rect) + this.watermarkObjs.push(rect) + } else { + // 居中单个 + const text = new Text(cfg.text || ' ', { + left: pageWpx / 2, + top: pageHpx / 2, + originX: 'center', + originY: 'center', + fontSize: fontSizePx, + fill: cfg.color, + fontFamily, + angle: cfg.rotation, + selectable: false, + evented: false, + excludeFromExport: true, + } as never) + ;(text as unknown as { isWatermark: boolean }).isWatermark = true + this.canvas.insertAt(1, text) + this.watermarkObjs.push(text) + } + + this.canvas.requestRenderAll() + } + + /** 页边距内容区左上角(画布 px)—— body 控件协议坐标的原点 */ + get contentOriginPx(): { x: number; y: number } { + return { x: this.marginMm.left * MM_TO_PX, y: this.marginMm.top * MM_TO_PX } + } + + /* ------------------------------- 控件 CRUD ------------------------------ */ + + getPrintObjects(): PrintFabricObject[] { + return this.canvas.getObjects().filter(isPrintObject) as PrintFabricObject[] + } + + getControlById(id: string): PrintFabricObject | undefined { + return this.getPrintObjects().find((o) => o.controlId === id) + } + + getZones(): PrintZone[] { + return this.getPrintObjects().filter((o): o is PrintZone => o instanceof PrintZone) + } + + hasZone(zone: 'header' | 'footer'): boolean { + return this.getZones().some((z) => z.zone === zone) + } + + addControl( + control: AnyControl, + opts: { select?: boolean; zoneHostId?: string } = {}, + ): void { + const obj = createFabricControl(control, { + widthMm: this.pageWidthMm, + heightMm: this.pageHeightMm, + }) + // 协议坐标(相对内容区/所属色带)→ 画布绝对坐标 + this.applySectionOrigin(obj, control, opts.zoneHostId) + // 镜像「常驻辅助线」开关到 Fabric 对象,供 SmartGuides 读取 + ;(obj as unknown as { showGuides: boolean }).showGuides = control.showGuides ?? false + this.canvas.add(obj) + if (opts.select ?? true) this.canvas.setActiveObject(obj) + this.refreshPages() + this.canvas.requestRenderAll() + } + + /** 清空全部控件(保留页面矩形),加载模板前调用 */ + clearControls(): void { + for (const obj of this.getPrintObjects()) this.canvas.remove(obj) + this.canvas.discardActiveObject() + this.refreshPages() + this.canvas.requestRenderAll() + } + + /** body 控件加内容区偏移(含页间距);zone 子控件加所属色带偏移(MVP:按几何中心归属) */ + private applySectionOrigin( + obj: PrintFabricObject, + control: AnyControl, + zoneHostId?: string, + ): void { + if (control.type === 'zone') return + if (zoneHostId) { + const host = this.getControlById(zoneHostId) + if (host) { + // zone 子控件:相对色带左上角,无页间距 + obj.set({ + left: (Number.isFinite(obj.left) ? obj.left! : 0) + (host.left ?? 0), + top: (Number.isFinite(obj.top) ? obj.top! : 0) + (host.top ?? 0), + }) + } + } else { + // body 控件:obj.top 此刻是 mm→px(相对页顶),modelTopToY 叠加内容区偏移与页间距 + obj.set({ + left: (Number.isFinite(obj.left) ? obj.left! : 0) + this.contentOriginPx.x, + top: this.modelTopToY((obj.top ?? 0) / MM_TO_PX), + }) + } + obj.setCoords() + } + + updateControl(control: AnyControl): void { + const obj = this.getControlById(control.id) + if (!obj) return + if (control.type !== 'zone') { + // 几何同步:协议 mm(相对内容区/所属色带)→ 画布绝对 px(含页间距) + const origin = this.originFor(obj) + obj.set({ + left: origin.x + control.left * MM_TO_PX, + top: origin.y + control.top * MM_TO_PX + (obj instanceof PrintZone ? 0 : this.gapPx(control.top)), + angle: control.angle ?? 0, + }) + if (obj instanceof PrintText) { + // 文本框:直接改宽度重排文字,不缩放字形 + obj.set({ width: control.width * MM_TO_PX, scaleX: 1, scaleY: 1 }) + } else if (obj.width && obj.height) { + // 其余控件:按目标尺寸等比缩放(Group/Line/位图均适用) + obj.set({ + scaleX: (control.width * MM_TO_PX) / obj.width, + scaleY: (control.height * MM_TO_PX) / obj.height, + }) + } + obj.setCoords() + } + ;(obj as IPrintObject).applyControlProps(control) + // 同步「常驻辅助线」开关到 Fabric 对象(控制面板实时切换即生效) + ;(obj as unknown as { showGuides: boolean }).showGuides = control.showGuides ?? false + this.refreshPages() + this.canvas.requestRenderAll() + } + + /** 几何中心落在哪个色带内(§5.14.2 子控件坐标相对色带左上角) */ + private hostZoneOf(obj: PrintFabricObject): PrintZone | undefined { + if (obj instanceof PrintZone) return undefined + const center = obj.getCenterPoint() + return this.getZones().find((z) => { + const b = z.getBoundingRect() + return ( + center.x >= b.left && center.x <= b.left + b.width && center.y >= b.top && center.y <= b.top + b.height + ) + }) + } + + /** 对象协议坐标原点:zone 子控件用色带左上角,其余用页边距内容区左上角 */ + private originFor(obj: PrintFabricObject): { x: number; y: number } { + const host = this.hostZoneOf(obj) + if (host) return { x: host.left ?? 0, y: host.top ?? 0 } + return this.contentOriginPx + } + + /** + * 读取 Fabric 对象为**协议控件**。 + * + * 【重要】`obj.toControl()` 返回的 left/top 是 **Fabric 绝对画布坐标**, + * 而协议层 / store 模型一律是「相对内容区(或所属色带)左上角」的 mm。 + * 任何要把画布状态写回 store 的路径都必须走本方法归一化,否则 `updateControl()` + * 会再叠加一次 origin,导致控件每次操作都向右下平移一个页边距。 + */ + readControl(obj: PrintFabricObject): AnyControl { + const control = obj.toControl() + if (obj instanceof PrintZone || control.type === 'zone') return control + const host = this.hostZoneOf(obj) + const origin = host ? { x: host.left ?? 0, y: host.top ?? 0 } : this.contentOriginPx + const abs = (v: number | undefined): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0) + const rel = (v: number, o: number): number => Math.round(((v - o) / MM_TO_PX) * 100) / 100 + const left = rel(abs(obj.left), origin.x) + // body 控件反解页间距(zone 子控件相对色带,间距恒为 0) + const top = host ? rel(abs(obj.top), origin.y) : Math.round(this.yToModelTop(abs(obj.top)) * 100) / 100 + // 色带子控件不允许溢出到色带上方/左侧 + control.left = host ? Math.max(0, left) : left + control.top = host ? Math.max(0, top) : top + return control + } + + removeControl(id: string): void { + const obj = this.getControlById(id) + if (!obj) return + this.canvas.remove(obj) + this.canvas.discardActiveObject() + this.refreshPages() + this.canvas.requestRenderAll() + } + + setActiveControl(id: string | null): void { + if (!id) { + this.canvas.discardActiveObject() + } else { + const obj = this.getControlById(id) + if (obj) this.canvas.setActiveObject(obj) + } + this.canvas.requestRenderAll() + } + + /* ------------------------------- 序列化 -------------------------------- */ + + /** + * 导出当前画布为协议组件列表。 + * zone 子控件归属判定:几何中心落在色带内(§5.14.2 子组件坐标相对色带左上角)。 + */ + serialize(): { body: AnyControl[]; zones: ZoneControl[] } { + const zones = this.getZones() + const zoneControls: ZoneControl[] = zones.map((z) => z.toControl()) + const body: AnyControl[] = [] + + for (const obj of this.getPrintObjects()) { + if (obj instanceof PrintZone) continue + // readControl 已把几何归一化到「相对色带 / 相对内容区」 + const control = this.readControl(obj) + const host = this.hostZoneOf(obj) + if (host) { + const zc = zoneControls.find((z) => z.zone === host.zone) + zc?.children.push(control) + } else { + body.push(control) + } + } + return { body, zones: zoneControls } + } + + /* ------------------------------- 缩放平移 ------------------------------- */ + + get zoom(): number { + return this.canvas?.getZoom() ?? 1 + } + + setZoom(zoom: number, center?: Point): void { + this.userZoomed = true + const z = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, zoom)) + const point = + center ?? new Point(this.canvas.getWidth() / 2, this.canvas.getHeight() / 2) + this.canvas.zoomToPoint(point, z) + this.emitViewport() + } + + zoomIn(): void { + this.setZoom(this.zoom + 0.25) + } + + zoomOut(): void { + this.setZoom(this.zoom - 0.25) + } + + /** 适配容器:按页宽适配(上限 200%),文档可整页容纳则垂直居中,否则顶部对齐以便向下滚动翻页 */ + fitToContainer(container: HTMLElement): void { + const pageW = this.pageWidthMm * MM_TO_PX + const stride = this.pageHeightMm * MM_TO_PX + this.PAGE_GAP_PX + const docH = this.pageCount * stride + const zoom = Math.min((container.clientWidth - 96) / pageW, 2) + const vt = this.canvas.viewportTransform + vt[0] = vt[3] = zoom + vt[4] = (container.clientWidth - pageW * zoom) / 2 + const usedH = docH * zoom + vt[5] = usedH <= container.clientHeight - 96 ? Math.max(48, (container.clientHeight - usedH) / 2) : 48 + this.canvas.setViewportTransform(vt) + this.canvas.requestRenderAll() + this.emitViewport() + } + + /** 滚动视口使第 index 页(0 起)顶部对齐到视口上方(顶部留 48px) */ + scrollToPage(index: number): void { + const stride = (this.pageHeightMm * MM_TO_PX + this.PAGE_GAP_PX) * this.zoom + const vt = this.canvas.viewportTransform + const viewH = this.canvas.getHeight() + const docH = this.pageCount * stride + const topY = Math.max(0, Math.min(index, this.pageCount - 1)) * stride + let y = 48 - topY + // 约束:不滚出文档顶部 / 底部 + const maxY = 48 + const minY = Math.min(maxY, viewH - docH - 48) + vt[5] = Math.max(Math.min(y, maxY), minY) + this.canvas.setViewportTransform(vt) + this.canvas.requestRenderAll() + this.emitViewport() + } + + getViewportState(): ViewportState { + const vt = this.canvas.viewportTransform + return { zoom: this.zoom, offsetX: vt[4] ?? 0, offsetY: vt[5] ?? 0 } + } + + private emitViewport(): void { + this.events.onViewportChange?.(this.getViewportState()) + // 视口变化同样需要驱动表格 HTML overlay 重定位 + this.events.onTransformTick?.() + } + + /* -------------------------------- 事件 -------------------------------- */ + + private bindEvents(): void { + const c = this.canvas + + const emitSelection = () => { + const active = c.getActiveObjects().filter(isPrintObject) as PrintFabricObject[] + this.events.onSelectionChange?.(active.map((o) => o.controlId)) + } + c.on('selection:created', emitSelection) + c.on('selection:updated', emitSelection) + c.on('selection:cleared', () => this.events.onSelectionChange?.([])) + + // 对象变换 / 视口变化 → 驱动表格 HTML overlay 重定位 + const bump = () => this.events.onTransformTick?.() + c.on('object:moving', bump) + c.on('object:scaling', bump) + c.on('object:rotating', bump) + c.on('object:modified', bump) + c.on('object:added', bump) + c.on('object:removed', bump) + + // 双击单元格:进入原生 contenteditable 编辑(方案 A) + c.on('mouse:dblclick', (opt) => { + const target = opt.target + if (!(target instanceof PrintTable)) return + // 必须用 readControl(相对坐标),直接 toControl() 会把绝对坐标写进 store 导致漂移 + const control = this.readControl(target) as TableControl + const ensured = ensureCells(control) + // 场景坐标 → 控件局部坐标(先平移到左上角原点,再反向旋转,最后归一化) + const dx = opt.scenePoint.x - (target.left ?? 0) + const dy = opt.scenePoint.y - (target.top ?? 0) + const rad = (-(target.angle ?? 0) * Math.PI) / 180 + const localX = dx * Math.cos(rad) - dy * Math.sin(rad) + const localY = dx * Math.sin(rad) + dy * Math.cos(rad) + const hit = hitTestCell( + computeGridLayout(ensured), + localX / Math.max(1e-6, target.getScaledWidth()), + localY / Math.max(1e-6, target.getScaledHeight()), + ) + if (!hit) return + this.events.onCellEdit?.({ controlId: target.controlId, control: ensured, row: hit.row, col: hit.col }) + }) + + c.on('object:modified', (e) => { + const obj = e.target + if (!obj || !isPrintObject(obj)) return + if (obj instanceof PrintZone) { + // zone 只允许改高度:把位置吸附回页顶/页底 + obj.relayout(this.pageWidthMm, this.pageHeightMm) + } + // 位图类控件缩放结束后按新尺寸重渲染,保证清晰 + const regenerable = obj as Partial<{ regenerate: () => void }> + if (typeof regenerable.regenerate === 'function' && !(obj instanceof PrintZone)) { + const scaledW = obj.getScaledWidth() + const scaledH = obj.getScaledHeight() + obj.set({ width: scaledW, height: scaledH, scaleX: 1, scaleY: 1 }) + regenerable.regenerate() + } + this.events.onObjectModified?.(this.readControl(obj)) + // 移动/缩放结束后正文布局可能跨页,刷新页数与分页线 + this.refreshPages() + }) + + // Ctrl/Cmd + 滚轮缩放;普通滚轮 = 垂直平移;Shift+滚轮 = 水平平移 + c.on('mouse:wheel', (opt: TPointerEventInfo) => { + const e = opt.e + e.preventDefault() + e.stopPropagation() + const vt = c.viewportTransform + if (e.ctrlKey || e.metaKey) { + const delta = e.deltaY + const zoom = this.zoom * 0.999 ** delta + this.setZoom(zoom, new Point(e.offsetX, e.offsetY)) + } else { + if (e.shiftKey) { + vt[4] = (vt[4] ?? 0) - e.deltaY + } else { + vt[5] = (vt[5] ?? 0) - e.deltaY + vt[4] = (vt[4] ?? 0) - e.deltaX + } + c.setViewportTransform(vt) + c.requestRenderAll() + this.emitViewport() + } + }) + + // 空格 + 拖拽 = 平移 + c.on('mouse:down', (opt) => { + if (this.spacePanning) { + const e = opt.e as MouseEvent + c.selection = false + c.defaultCursor = 'grab' + let lastX = e.clientX + let lastY = e.clientY + const move = (ev: MouseEvent) => { + const vt = c.viewportTransform + vt[4] = (vt[4] ?? 0) + (ev.clientX - lastX) + vt[5] = (vt[5] ?? 0) + (ev.clientY - lastY) + lastX = ev.clientX + lastY = ev.clientY + c.setViewportTransform(vt) + c.requestRenderAll() + this.emitViewport() + } + const up = () => { + window.removeEventListener('mousemove', move) + window.removeEventListener('mouseup', up) + c.selection = true + c.defaultCursor = 'default' + } + window.addEventListener('mousemove', move) + window.addEventListener('mouseup', up) + } + }) + + window.addEventListener('keydown', this.onKeyDown) + window.addEventListener('keyup', this.onKeyUp) + } + + private onKeyDown = (e: KeyboardEvent): void => { + if (e.code === 'Space' && !this.isEditingText()) { + this.spacePanning = true + e.preventDefault() + } + } + + private onKeyUp = (e: KeyboardEvent): void => { + if (e.code === 'Space') this.spacePanning = false + } + + private isEditingText(): boolean { + const active = this.canvas.getActiveObject() as unknown as { isEditing?: boolean } | undefined + return !!active?.isEditing + } + + /* ----------------------------- 网格控制 ------------------------------ */ + + setGridSize(sizeMm: number): void { + if (this.smartGuides) this.smartGuides.config.gridSizeMm = sizeMm + } + + setGridColor(color: string): void { + if (this.smartGuides) this.smartGuides.config.gridColor = color + this.canvas.requestRenderAll() + } + + setGridVisible(visible: boolean): void { + if (this.smartGuides) this.smartGuides.config.gridVisible = visible + this.canvas.requestRenderAll() + } +} diff --git a/src/design/canvas/CanvasStage.vue b/src/design/canvas/CanvasStage.vue new file mode 100644 index 0000000..0edf1ea --- /dev/null +++ b/src/design/canvas/CanvasStage.vue @@ -0,0 +1,109 @@ + + + diff --git a/src/design/canvas/CellToolbar.vue b/src/design/canvas/CellToolbar.vue new file mode 100644 index 0000000..ab876ba --- /dev/null +++ b/src/design/canvas/CellToolbar.vue @@ -0,0 +1,620 @@ + + + + + diff --git a/src/design/canvas/ChartViewLayer.vue b/src/design/canvas/ChartViewLayer.vue new file mode 100644 index 0000000..b6f28f6 --- /dev/null +++ b/src/design/canvas/ChartViewLayer.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/src/design/canvas/TableViewLayer.vue b/src/design/canvas/TableViewLayer.vue new file mode 100644 index 0000000..d91f946 --- /dev/null +++ b/src/design/canvas/TableViewLayer.vue @@ -0,0 +1,498 @@ + + + + + diff --git a/src/design/canvas/barcode-draw.ts b/src/design/canvas/barcode-draw.ts new file mode 100644 index 0000000..9e20073 --- /dev/null +++ b/src/design/canvas/barcode-draw.ts @@ -0,0 +1,78 @@ +/** + * @bwip-js/generic → Canvas 2D 适配器 + * + * 改用官方 toSVG() 生成条码(含人眼可读数字的人体化矢量路径,无字体依赖), + * 再栅格化到离屏 canvas 供 Fabric 显示。 + * 原先的手写 CanvasDrawing 适配器对人眼可读文字的测量/绘制有缺陷, + * 导致画布上条码下方的数字不显示(预览走 toSVG 正常,故仅画布有此问题)。 + */ +import * as BwipJs from '@bwip-js/generic' + +export interface BarcodeDrawOptions { + /** bwip-js bcid,如 code128 / ean13 */ + format?: string + text: string + showText?: boolean + /** 条码条本身高度(mm)。bwip-js 的 height 选项只管条码条,不含文字行 */ + barHeightMM?: number + /** 上下留白(mm),给文字行呼吸空间,避免贴边 */ + paddingMM?: number +} + +/** 把 bwip-js 的 SVG 字符串栅格化到离屏 canvas(人眼可读数字以矢量路径渲染,天然可见) */ +function svgToCanvas(svg: string): Promise { + return new Promise((resolve, reject) => { + const img = new Image() + const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg) + img.onload = () => { + const w = img.naturalWidth || parseSvgSize(svg, 'width') + const h = img.naturalHeight || parseSvgSize(svg, 'height') + const canvas = document.createElement('canvas') + canvas.width = Math.max(1, w) + canvas.height = Math.max(1, h) + const ctx = canvas.getContext('2d') + if (!ctx) return reject(new Error('no 2d context')) + ctx.drawImage(img, 0, 0, canvas.width, canvas.height) + resolve(canvas) + } + img.onerror = () => reject(new Error('barcode svg decode failed')) + img.src = url + }) +} + +/** SVG 未带 width/height 时从 viewBox 兜底解析 */ +function parseSvgSize(svg: string, attr: 'width' | 'height'): number { + const vb = /viewBox="0 0 (\d+) (\d+)"/.exec(svg) + if (vb) return Number(vb[attr === 'width' ? 1 : 2]) || 1 + const m = new RegExp(`${attr}="(\\d+)"`).exec(svg) + return m ? Number(m[1]) || 1 : 1 +} + +/** + * 渲染条码到离屏 canvas(失败返回 null,由调用方降级为占位) + * + * 关键:barHeightMM + paddingMM 让 bwip-js 直接按目标几何渲染, + * canvas 自然尺寸 ≈ 控件尺寸,调用方 scaleY ≈ 1,文字行不再被压扁。 + * (原先只用 scale:3 生成自然尺寸位图,再被调用方 Y 方向压缩,导致文字看不见。) + */ +export async function drawBarcode(options: BarcodeDrawOptions): Promise { + try { + const barHeight = options.barHeightMM ?? 10 + const padding = options.paddingMM ?? 2 + const svg = BwipJs.toSVG({ + bcid: (options.format ?? 'code128').toLowerCase(), + text: options.text || '0123456789', + scale: 2, + height: barHeight, + paddingtop: padding, + paddingbottom: padding, + includetext: options.showText ?? false, + textxalign: 'center', + // BWIPP textsize(单位 point,默认 9),调到 12 让人眼可读数字更醒目 + textsize: 12, + }) + return await svgToCanvas(svg) + } catch { + return null + } +} diff --git a/src/design/canvas/controls/PrintBarcode.ts b/src/design/canvas/controls/PrintBarcode.ts new file mode 100644 index 0000000..497886a --- /dev/null +++ b/src/design/canvas/controls/PrintBarcode.ts @@ -0,0 +1,105 @@ +/** + * PrintBarcode —— 条形码控件(§5.7,bwip-js 离屏渲染) + * 设计期用绑定路径占位文本渲染,运行期由渲染引擎注入真实值。 + */ +import { FabricImage } from 'fabric' +import type { BarcodeControl } from '@/types/control' +import { mm, px, readBaseGeometry, type IPrintObject } from './PrintObject' +import { drawBarcode } from '../barcode-draw' + +export class PrintBarcode extends FabricImage implements IPrintObject { + controlId: string + controlType = 'barcode' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + binding?: string + textValue?: string + format = 'CODE128' + showText = true + + constructor(control: BarcodeControl) { + super(document.createElement('canvas'), { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.binding = control.binding + this.textValue = control.value + this.format = control.format ?? 'CODE128' + this.showText = control.showText ?? true + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + // 初始尺寸(未缩放),regenerate 会按它换算 scale + this.set({ width: mm(control.width), height: mm(control.height) }) + void this.regenerate() + } + + /** 重新渲染条码位图,并按控件宽高缩放显示 */ + async regenerate(): Promise { + const w = this.width || 1 + const h = this.height || 1 + const text = this.textValue ?? (this.binding ? `{{${this.binding}}}` : '0123456789') + // 绑定占位符含中文/大括号无法编码,设计期用示例码代替 + const encodable = /[{}\u4e00-\u9fa5]/.test(text) ? 'DEMO123456' : text + // 按控件几何反算条码条高度(mm):条码条占 60%,剩 40% 留给文字行(textsize=12)+ 上下留白。 + // 这样 bwip-js 直接按目标尺寸渲染,canvas 自然高度 ≈ 控件高度,scaleY ≈ 1,文字不被压扁。 + const controlHeightMM = px(h) + const barHeightMM = Math.max(2, controlHeightMM * 0.6) + const paddingMM = Math.max(0.5, controlHeightMM * 0.04) + const canvas = await drawBarcode({ + format: this.format, + text: encodable, + showText: this.showText, + barHeightMM, + paddingMM, + }) + if (canvas) { + this.setElement(canvas) + // 宽度独立缩放填满控件;高度按 canvas 自然尺寸缩放(已接近控件高度,scaleY ≈ 1)。 + // 不再强行 Y 方向压缩,避免文字行被挤没。 + this.set({ scaleX: w / canvas.width, scaleY: h / canvas.height }) + } + this.setCoords() + this.canvas?.requestRenderAll() + } + + toControl(): BarcodeControl { + return { + ...readBaseGeometry(this), + type: 'barcode', + binding: this.binding, + value: this.textValue, + format: this.format, + showText: this.showText, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: BarcodeControl): void { + this.binding = control.binding + this.textValue = control.value + this.format = control.format ?? 'CODE128' + this.showText = control.showText ?? true + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ + lockMovementX: control.locked, + lockMovementY: control.locked, + scaleX: 1, + scaleY: 1, + width: mm(control.width), + height: mm(control.height), + }) + void this.regenerate() + } +} diff --git a/src/design/canvas/controls/PrintChart.ts b/src/design/canvas/controls/PrintChart.ts new file mode 100644 index 0000000..ef6ee64 --- /dev/null +++ b/src/design/canvas/controls/PrintChart.ts @@ -0,0 +1,113 @@ +/** + * PrintChart —— 图表控件(《设计方案》图表控件提案) + * + * 与 PrintTable 同属【方案 A】:**视觉**完全交给 HTML overlay(ChartViewLayer.vue), + * 本 Fabric 对象退化为「透明宿主」——只承担选中 / 拖拽 / 缩放手柄 / 层级 / 序列化, + * 位图上仅画一圈极淡虚线轮廓作为兜底。SVG 矢量由 chartkit 生成, + * 设计期与运行期导出共用同一份产物(设计即打印)。 + */ +import { FabricImage } from 'fabric' +import type { ChartControl } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' + +/** overlay 兜底轮廓色(极淡,正常情况下被 overlay 的真实图表完全覆盖) */ +const GHOST_COLOR = 'rgba(120,130,145,0.35)' + +export class PrintChart extends FabricImage implements IPrintObject { + controlId: string + controlType = 'chart' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + kind: ChartControl['kind'] + categories: string[] + series: ChartControl['series'] + options: ChartControl['options'] + + /** true = 视觉交给 HTML overlay,位图只画兜底轮廓 */ + htmlPreview = true + + constructor(control: ChartControl) { + super(document.createElement('canvas'), { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.kind = control.kind + this.categories = control.categories ?? [] + this.series = control.series ?? [] + this.options = control.options + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ width: mm(control.width), height: mm(control.height) }) + this.regenerate() + } + + /** 重绘设计期预览(2x 分辨率保证缩放清晰) */ + regenerate(): void { + const w = Math.max(this.width || 1, 10) + const h = Math.max(this.height || 1, 10) + const ratio = 2 + const canvas = document.createElement('canvas') + canvas.width = Math.ceil(w * ratio) + canvas.height = Math.ceil(h * ratio) + const ctx = canvas.getContext('2d')! + ctx.scale(ratio, ratio) + this.drawGhost(ctx, w, h) + // setElement 会重置 width/height 为位图像素尺寸,缩放须在 setElement 之后按新尺寸计算 + this.setElement(canvas) + this.set({ scaleX: w / canvas.width, scaleY: h / canvas.height }) + this.setCoords() + this.canvas?.requestRenderAll() + } + + /** overlay 模式:透明底 + 一圈极淡虚线,保证控件"存在感"且不与 overlay 抢像素 */ + private drawGhost(ctx: CanvasRenderingContext2D, w: number, h: number): void { + ctx.clearRect(0, 0, w, h) + ctx.strokeStyle = GHOST_COLOR + ctx.lineWidth = 0.5 + ctx.setLineDash([3, 3]) + ctx.strokeRect(0.5, 0.5, w - 1, h - 1) + ctx.setLineDash([]) + } + + toControl(): ChartControl { + return { + ...readBaseGeometry(this), + id: this.controlId, + type: 'chart', + kind: this.kind, + categories: this.categories, + series: this.series, + options: this.options, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: ChartControl): void { + this.kind = control.kind + this.categories = control.categories ?? [] + this.series = control.series ?? [] + this.options = control.options + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ + lockMovementX: control.locked, + lockMovementY: control.locked, + scaleX: 1, + scaleY: 1, + width: mm(control.width), + height: mm(control.height), + }) + this.regenerate() + } +} diff --git a/src/design/canvas/controls/PrintCircle.ts b/src/design/canvas/controls/PrintCircle.ts new file mode 100644 index 0000000..28f0d7a --- /dev/null +++ b/src/design/canvas/controls/PrintCircle.ts @@ -0,0 +1,68 @@ +/** + * PrintCircle —— 圆形控件(复用 rect 协议模型,shape:'circle' 时以 Ellipse 渲染) + */ +import { Ellipse } from 'fabric' +import type { RectControl } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' + +export class PrintCircle extends Ellipse implements IPrintObject { + controlId: string + controlType = 'rect' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + constructor(control: RectControl) { + super({ + left: mm(control.left), + top: mm(control.top), + rx: mm(control.width) / 2, + ry: mm(control.height) / 2, + angle: control.angle ?? 0, + fill: control.fill ?? 'transparent', + stroke: control.stroke ?? '#000000', + strokeWidth: control.strokeWidth ?? 1, + strokeDashArray: control.strokeDashArray ?? undefined, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + } + + toControl(): RectControl { + return { + ...readBaseGeometry(this), + type: 'rect', + shape: 'circle', + fill: typeof this.fill === 'string' ? this.fill : undefined, + stroke: typeof this.stroke === 'string' ? this.stroke : undefined, + strokeWidth: this.strokeWidth, + strokeDashArray: this.strokeDashArray as number[] | undefined, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: RectControl): void { + this.set({ + rx: mm(control.width) / 2, + ry: mm(control.height) / 2, + fill: control.fill ?? 'transparent', + stroke: control.stroke ?? '#000000', + strokeWidth: control.strokeWidth ?? 1, + strokeDashArray: control.strokeDashArray ?? undefined, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.setCoords() + this.canvas?.requestRenderAll() + } +} diff --git a/src/design/canvas/controls/PrintImage.ts b/src/design/canvas/controls/PrintImage.ts new file mode 100644 index 0000000..aa5233e --- /dev/null +++ b/src/design/canvas/controls/PrintImage.ts @@ -0,0 +1,140 @@ +/** + * PrintImage —— 图片控件(《设计方案》§5.8) + * inline/url/asset/binding 四种来源;无图时显示占位框。 + */ +import { FabricImage, Rect, Group, Textbox, LayoutManager, FixedLayout } from 'fabric' +import type { ImageControl } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' + +/** + * 图片控件用 Group 承载:底层是 Rect 占位框,加载成功后替换为 FabricImage。 + * Group 方案对 "加载成功/失败/占位" 三态最稳。 + */ +export class PrintImage extends Group implements IPrintObject { + controlId: string + controlType = 'image' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + value?: ImageControl['value'] + fit: NonNullable = 'contain' + cornerRadius?: number + /** 控件原始几何(mm),loadImage 时 Group width/height 失真的兜底 */ + private _controlWidth = 0 + private _controlHeight = 0 + + constructor(control: ImageControl) { + const w = mm(control.width) + const h = mm(control.height) + const placeholder = new Rect({ + width: w, + height: h, + left: -w / 2, + top: -h / 2, + fill: 'rgba(22,119,255,0.06)', + stroke: '#c0c4cc', + strokeDashArray: [4, 3], + strokeWidth: 1, + rx: control.cornerRadius ?? 0, + ry: control.cornerRadius ?? 0, + }) + const label = new Textbox(control.value?.mode === 'binding' ? `{{${control.value.content}}}` : '图片', { + left: -w / 2, + top: -8, + width: w, + fontSize: 11, + fill: '#909399', + textAlign: 'center', + selectable: false, + evented: false, + }) + super([placeholder, label], { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + lockMovementX: control.locked, + lockMovementY: control.locked, + // Group 默认 origin 是 center,与 Rect 默认 left/top 不同——统一用 left/top + originX: 'left', + originY: 'top', + // FixedLayout:add/remove 子对象时不重算 left/top,避免上传图片后控件跳位 + layoutManager: new LayoutManager(new FixedLayout()), + }) + this.controlId = control.id + this.value = control.value + this.fit = control.fit ?? 'contain' + this.cornerRadius = control.cornerRadius + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this._controlWidth = control.width + this._controlHeight = control.height + // 从模板恢复时若已有图片内容,立即加载 + void this.loadImage() + } + + /** 异步加载真实图片替换占位(inline/url 来源;binding 设计期保持占位) */ + async loadImage(): Promise { + if (!this.value || this.value.mode === 'binding' || !this.value.content) return + // data: URL 不需要 crossOrigin;外部 URL 设 anonymous 避免画布污染 + const isDataUrl = this.value.content.startsWith('data:') + const loadOpts = isDataUrl ? {} : { crossOrigin: 'anonymous' as const } + try { + const img = await FabricImage.fromURL(this.value.content, loadOpts) + // Group 的 width/height 可能在子对象变化后失真,用控件原始几何兜底 + const w = this.width || mm(this._controlWidth) + const h = this.height || mm(this._controlHeight) + img.set({ + left: -w / 2, + top: -h / 2, + originX: 'left', + originY: 'top', + scaleX: w / (img.width || w), + scaleY: h / (img.height || h), + }) + this.remove(...this.getObjects()) + this.add(img) + // Group.add 触发 enterGroup 会 inverse transform 子对象坐标, + // FixedLayout 下不再有 commitLayout 修正,需手动复位 + img.set({ left: -w / 2, top: -h / 2 }) + this.set({ width: w, height: h }) + this.setCoords() + this.canvas?.requestRenderAll() + } catch { + // 加载失败保持占位框,不阻断设计器 + } + } + + toControl(): ImageControl { + return { + ...readBaseGeometry(this), + type: 'image', + value: this.value, + fit: this.fit, + cornerRadius: this.cornerRadius, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: ImageControl): void { + this.value = control.value + this.fit = control.fit ?? 'contain' + this.cornerRadius = control.cornerRadius + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this._controlWidth = control.width + this._controlHeight = control.height + this.set({ + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + void this.loadImage() + this.setCoords() + this.canvas?.requestRenderAll() + } +} diff --git a/src/design/canvas/controls/PrintLine.ts b/src/design/canvas/controls/PrintLine.ts new file mode 100644 index 0000000..433126d --- /dev/null +++ b/src/design/canvas/controls/PrintLine.ts @@ -0,0 +1,67 @@ +/** + * PrintLine —— 线条控件 + * Fabric Line 用 x1/y1/x2/y2 四点 + left/top 定位; + * 协议层只存外接框(left/top/width/height),MVP 仅支持水平/垂直线。 + */ +import { Line } from 'fabric' +import type { LineControl } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' + +export class PrintLine extends Line implements IPrintObject { + controlId: string + controlType = 'line' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + constructor(control: LineControl) { + // 水平线:width 有效、height≈0;垂直线反之(MVP) + const w = mm(control.width) + const h = mm(control.height) + const horizontal = h <= 1 + super([0, 0, horizontal ? w : 0, horizontal ? 0 : h], { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + stroke: control.stroke ?? '#000000', + strokeWidth: control.strokeWidth ?? 1, + strokeDashArray: control.strokeDashArray, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + } + + toControl(): LineControl { + const geo = readBaseGeometry(this) + return { + ...geo, + type: 'line', + stroke: typeof this.stroke === 'string' ? this.stroke : undefined, + strokeWidth: this.strokeWidth, + strokeDashArray: this.strokeDashArray ?? undefined, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: LineControl): void { + this.set({ + stroke: control.stroke ?? '#000000', + strokeWidth: control.strokeWidth ?? 1, + strokeDashArray: control.strokeDashArray, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.setCoords() + this.canvas?.requestRenderAll() + } +} diff --git a/src/design/canvas/controls/PrintObject.ts b/src/design/canvas/controls/PrintObject.ts new file mode 100644 index 0000000..98d30ae --- /dev/null +++ b/src/design/canvas/controls/PrintObject.ts @@ -0,0 +1,66 @@ +/** + * PrintObject —— 所有打印控件的公共契约 + * + * 设计约定(《设计方案》§8.1.2): + * - 每个控件是 Fabric 子类,实例上挂 `controlId` 与 `controlType` + * - `toControl()` 把 Fabric 对象序列化回协议模型(px → mm 由这里统一换算) + * - 属性面板通过 `applyControlProps(control)` 把模型写回 Fabric 对象 + * + * 坐标:协议层 mm(相对 Section 左上角)⇄ 画布层 px(MM_TO_PX,zoom=1)。 + */ +import type { FabricObject } from 'fabric' +import type { AnyControl, ControlType } from '@/types/control' +import { MM_TO_PX } from '@/utils/constants' + +/** 所有打印控件实例都实现的接口 */ +export interface IPrintObject { + controlId: string + controlType: ControlType + /** 所属区域:body 或某个 zone 的 controlId */ + zoneId?: string + toControl(): AnyControl + // 参数声明为 AnyControl(方法双变允许各子类收窄为自己的控件类型) + applyControlProps(control: AnyControl): void +} + +export function isPrintObject(obj: unknown): obj is FabricObject & IPrintObject { + return ( + typeof obj === 'object' && + obj !== null && + 'controlId' in obj && + 'controlType' in obj && + typeof (obj as IPrintObject).toControl === 'function' + ) +} + +/* ------------------------------ 几何换算 helpers ------------------------------ */ + +export const mm = (v: number): number => v * MM_TO_PX +export const px = (v: number): number => v / MM_TO_PX + +/** + * 读取对象的公共几何(px → mm),返回 ControlBase 的几何部分。 + * + * 【坐标系警告】这里的 left/top 是 **Fabric 绝对画布坐标**(页面左上角为原点), + * 而协议层 / store 模型用的是「相对页边距内容区(或所属色带)左上角」。 + * 因此 `toControl()` 的结果 **不可直接写回 store**,必须先经 + * `CanvasDesigner.readControl(obj)` 归一化,否则 `updateControl()` 会再叠加 + * 一次 origin,控件每次操作都向右下平移一个页边距。 + */ +export function readBaseGeometry(obj: FabricObject & IPrintObject) { + // 兜底:fabric 对象的几何在异常时序下可能为 NaN/undefined(如 viewport 未就绪时 + // 创建的对象),`?? 0` 对 NaN 无效,必须用 Number.isFinite 收敛,否则序列化回协议的 + // left/top 带 NaN,写回 store 后 NInputNumber 会渲染删除线。 + const r1 = (n: number): number => Math.round(n * 10) / 10 + const num = (v: unknown): number => (typeof v === 'number' && Number.isFinite(v) ? v : 0) + return { + id: obj.controlId, + left: r1(px(num(obj.left))), + top: r1(px(num(obj.top))), + width: r1(px(num(obj.getScaledWidth()))), + height: r1(px(num(obj.getScaledHeight()))), + angle: num(obj.angle), + } +} + +export const round2 = (n: number): number => Math.round(n * 100) / 100 diff --git a/src/design/canvas/controls/PrintQrcode.ts b/src/design/canvas/controls/PrintQrcode.ts new file mode 100644 index 0000000..56b70c2 --- /dev/null +++ b/src/design/canvas/controls/PrintQrcode.ts @@ -0,0 +1,91 @@ +/** + * PrintQrcode —— 二维码控件(§5.7,qrcode 库离屏渲染) + */ +import { FabricImage } from 'fabric' +import QRCode from 'qrcode' +import type { QrcodeControl } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' + +export class PrintQrcode extends FabricImage implements IPrintObject { + controlId: string + controlType = 'qrcode' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + binding?: string + textValue?: string + errorLevel: 'L' | 'M' | 'Q' | 'H' = 'M' + + constructor(control: QrcodeControl) { + super(document.createElement('canvas'), { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.binding = control.binding + this.textValue = control.value + this.errorLevel = control.errorLevel ?? 'M' + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ width: mm(control.width), height: mm(control.height) }) + void this.regenerate() + } + + async regenerate(): Promise { + // 目标显示尺寸(逻辑 px);setElement 会重置 width/height,先保存 + const targetW = this.getScaledWidth() || 1 + const targetH = this.getScaledHeight() || 1 + const text = this.textValue ?? (this.binding ? `{{${this.binding}}}` : 'https://open-print.dev') + const canvas = document.createElement('canvas') + try { + await QRCode.toCanvas(canvas, text, { + width: Math.ceil(Math.max(targetW, targetH) * 2), // 2x 渲染保证缩放清晰 + margin: 1, + errorCorrectionLevel: this.errorLevel, + }) + this.setElement(canvas) + this.set({ scaleX: targetW / canvas.width, scaleY: targetH / canvas.height }) + } catch { + // 内容过长等异常:保持空白占位 + } + this.setCoords() + this.canvas?.requestRenderAll() + } + + toControl(): QrcodeControl { + return { + ...readBaseGeometry(this), + type: 'qrcode', + binding: this.binding, + value: this.textValue, + errorLevel: this.errorLevel, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: QrcodeControl): void { + this.binding = control.binding + this.textValue = control.value + this.errorLevel = control.errorLevel ?? 'M' + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ + lockMovementX: control.locked, + lockMovementY: control.locked, + scaleX: 1, + scaleY: 1, + width: mm(control.width), + height: mm(control.height), + }) + void this.regenerate() + } +} diff --git a/src/design/canvas/controls/PrintRect.spec.ts b/src/design/canvas/controls/PrintRect.spec.ts new file mode 100644 index 0000000..2af8fdc --- /dev/null +++ b/src/design/canvas/controls/PrintRect.spec.ts @@ -0,0 +1,83 @@ +/** + * PrintRect 四角独立圆角路径几何验证。 + * + * 用 node-canvas 直接执行 traceRoundRectPath(与 Fabric _render 的绘制约定一致: + * ctx 已平移到对象中心,坐标范围 -w/2 ~ w/2),抽样四角像素透明度, + * 确认「某角有半径、其余为 0」时只有该角被圆角化、其余保持直角。 + */ +import { describe, expect, it } from 'vitest' +import { createCanvas } from 'canvas' +import { traceRoundRectPath } from './PrintRect' + +interface Corner { + /** 抽样点(相对对象左上角,单位 px),取靠近外缘但不在圆角切线内的位置 */ + sampleX: number + sampleY: number +} + +function renderAndSample( + w: number, + h: number, + tl: number, + tr: number, + br: number, + bl: number, +): Record<'TL' | 'TR' | 'BR' | 'BL', boolean> { + const pad = 40 + const cw = w + pad * 2 + const ch = h + pad * 2 + const canvas = createCanvas(cw, ch) + const ctx = canvas.getContext('2d')! + ctx.translate(cw / 2, ch / 2) // 模拟 Fabric 把原点移到对象中心 + traceRoundRectPath(ctx as unknown as CanvasRenderingContext2D, w, h, tl, tr, br, bl) + ctx.fillStyle = '#000000' + ctx.fill() + + const img = ctx.getImageData(0, 0, cw, ch)!.data + const alphaAt = (x: number, y: number): boolean => { + const idx = (Math.round(y) * cw + Math.round(x)) * 4 + 3 + return (img[idx] ?? 0) > 10 // 不透明 = 属于形状内部 + } + + // 各角取样点:紧贴外缘、但位于「该角若有大圆角就会被切掉」的位置 + const samples: Record<'TL' | 'TR' | 'BR' | 'BL', Corner> = { + TL: { sampleX: pad + 4, sampleY: pad + 4 }, + TR: { sampleX: pad + w - 4, sampleY: pad + 4 }, + BR: { sampleX: pad + w - 4, sampleY: pad + h - 4 }, + BL: { sampleX: pad + 4, sampleY: pad + h - 4 }, + } + const out = {} as Record<'TL' | 'TR' | 'BR' | 'BL', boolean> + for (const k of Object.keys(samples) as Array) { + out[k] = alphaAt(samples[k].sampleX, samples[k].sampleY) + } + return out +} + +describe('PrintRect —— 四角独立圆角路径几何', () => { + const W = 160 + const H = 100 + + it('仅左上角有半径时,只有 TL 被圆角化(取样点为透明)', () => { + const r = renderAndSample(W, H, 40, 0, 0, 0) + expect(r.TL).toBe(false) // 左上被切成圆角 → 角点透明 + expect(r.TR).toBe(true) // 其余直角 → 角点不透明 + expect(r.BR).toBe(true) + expect(r.BL).toBe(true) + }) + + it('四角全 0 时为标准直角矩形(四角均不透明)', () => { + const r = renderAndSample(W, H, 0, 0, 0, 0) + expect(r.TL && r.TR && r.BR && r.BL).toBe(true) + }) + + it('四角均设大半径时为全圆角(四角均透明)', () => { + const r = renderAndSample(W, H, 30, 30, 30, 30) + expect(r.TL && r.TR && r.BR && r.BL).toBe(false) + }) + + it('半径上限被夹紧到短边一半,不溢出(超大半径下四角仍正常)', () => { + const r = renderAndSample(W, H, 999, 999, 999, 999) + // 短边=H=100 → 上限 50;四角应被夹紧为圆角但形状完整 + expect(r.TL && r.TR && r.BR && r.BL).toBe(false) + }) +}) diff --git a/src/design/canvas/controls/PrintRect.ts b/src/design/canvas/controls/PrintRect.ts new file mode 100644 index 0000000..321ce30 --- /dev/null +++ b/src/design/canvas/controls/PrintRect.ts @@ -0,0 +1,152 @@ +/** + * PrintRect —— 矩形控件 + * + * 圆角走「四角独立半径」绘制:Fabric 原生 Rect 仅支持统一 rx/ry, + * 这里重写 `_render`,用 arcTo 画出四角各自独立的圆角矩形路径, + * 单位 px(与协议 cornerRadius* 一致)。 + */ +import { Rect } from 'fabric' +import type { RectControl } from '@/types/control' +import { mm, px, readBaseGeometry, type IPrintObject } from './PrintObject' + +/** + * 以对象中心为原点绘制四角独立圆角的矩形路径。 + * ctx 进入时已被 Fabric 平移到对象中心(坐标范围 -w/2 ~ w/2), + * 半径单位为对象本地像素(drawing space,未乘 scaleX/scaleY)。 + */ +export function traceRoundRectPath( + ctx: CanvasRenderingContext2D, + w: number, + h: number, + tl: number, + tr: number, + br: number, + bl: number, +): void { + const x = -w / 2 + const y = -h / 2 + // 单角半径上限取短边一半,避免相邻圆角重叠产生扭曲 + const maxR = Math.min(w, h) / 2 + const clamp = (r: number): number => Math.max(0, Math.min(Math.round(r), maxR)) + const a = clamp(tl) + const b = clamp(tr) + const c = clamp(br) + const d = clamp(bl) + ctx.beginPath() + ctx.moveTo(x + a, y) + ctx.lineTo(x + w - b, y) + if (b > 0) ctx.arcTo(x + w, y, x + w, y + b, b) + ctx.lineTo(x + w, y + h - c) + if (c > 0) ctx.arcTo(x + w, y + h, x + w - c, y + h, c) + ctx.lineTo(x + d, y + h) + if (d > 0) ctx.arcTo(x, y + h, x, y + h - d, d) + ctx.lineTo(x, y + a) + if (a > 0) ctx.arcTo(x, y, x + a, y, a) + ctx.closePath() +} + +export class PrintRect extends Rect implements IPrintObject { + controlId: string + controlType = 'rect' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + /** 四角独立圆角(px),绘制时优先于 rx/ry */ + cornerRadiusTL = 0 + cornerRadiusTR = 0 + cornerRadiusBR = 0 + cornerRadiusBL = 0 + /** 统一圆角(px),四角独立值统一回落基准 */ + cornerRadius = 0 + + constructor(control: RectControl) { + const cr = control.cornerRadius ?? 0 + super({ + left: mm(control.left), + top: mm(control.top), + width: mm(control.width), + height: mm(control.height), + angle: control.angle ?? 0, + fill: control.fill ?? 'transparent', + stroke: control.stroke ?? '#000000', + strokeWidth: control.strokeWidth ?? 1, + rx: cr, + ry: cr, + strokeDashArray: control.strokeDashArray ?? undefined, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.cornerRadiusTL = control.cornerRadiusTL ?? cr + this.cornerRadiusTR = control.cornerRadiusTR ?? cr + this.cornerRadiusBR = control.cornerRadiusBR ?? cr + this.cornerRadiusBL = control.cornerRadiusBL ?? cr + this.cornerRadius = cr + } + + /** 四角独立圆角路径绘制(覆盖 Fabric 原生统一 rx/ry) */ + _render(ctx: CanvasRenderingContext2D): void { + traceRoundRectPath( + ctx, + this.width, + this.height, + this.cornerRadiusTL, + this.cornerRadiusTR, + this.cornerRadiusBR, + this.cornerRadiusBL, + ) + this._renderFill(ctx) + this._renderStroke(ctx) + } + + toControl(): RectControl { + const cr = this.cornerRadius ?? 0 + const out: RectControl = { + ...readBaseGeometry(this), + type: 'rect', + fill: typeof this.fill === 'string' ? this.fill : undefined, + stroke: typeof this.stroke === 'string' ? this.stroke : undefined, + strokeWidth: this.strokeWidth, + cornerRadius: cr || undefined, + strokeDashArray: this.strokeDashArray as number[] | undefined, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + // 仅当某角与统一值不一致时才落盘为独立覆盖值,保持 JSON 干净 + if (this.cornerRadiusTL !== cr) out.cornerRadiusTL = this.cornerRadiusTL || undefined + if (this.cornerRadiusTR !== cr) out.cornerRadiusTR = this.cornerRadiusTR || undefined + if (this.cornerRadiusBR !== cr) out.cornerRadiusBR = this.cornerRadiusBR || undefined + if (this.cornerRadiusBL !== cr) out.cornerRadiusBL = this.cornerRadiusBL || undefined + return out + } + + applyControlProps(control: RectControl): void { + const cr = control.cornerRadius ?? 0 + this.cornerRadiusTL = control.cornerRadiusTL ?? cr + this.cornerRadiusTR = control.cornerRadiusTR ?? cr + this.cornerRadiusBR = control.cornerRadiusBR ?? cr + this.cornerRadiusBL = control.cornerRadiusBL ?? cr + this.cornerRadius = cr + this.set({ + fill: control.fill ?? 'transparent', + stroke: control.stroke ?? '#000000', + strokeWidth: control.strokeWidth ?? 1, + rx: cr, + ry: cr, + strokeDashArray: control.strokeDashArray ?? undefined, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.setCoords() + this.canvas?.requestRenderAll() + } +} diff --git a/src/design/canvas/controls/PrintRichText.ts b/src/design/canvas/controls/PrintRichText.ts new file mode 100644 index 0000000..aef31ce --- /dev/null +++ b/src/design/canvas/controls/PrintRichText.ts @@ -0,0 +1,149 @@ +/** + * PrintRichText —— 富文本控件(§5.8) + * + * 设计期显示:把富文本 HTML 消毒后包进 SVG ``, + * 再栅格化成位图给 FabricImage 显示(与导出引擎同一技术路线,Chromium 限定)。 + * 渲染期(预览/导出/打印)走 data-binder → html-renderer 的 `kind:'html'` 链路, + * 本类只负责画布上的"所见即所得"预览。 + */ +import { FabricImage } from 'fabric' +import DOMPurify from 'dompurify' +import type { RichTextControl } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' +import { toXmlSafe, svgToCanvas } from '@/core/export-engine/rasterize' + +/** 设计期默认字号(px,foreignObject 内继承) */ +const DESIGN_FONT_PX = 14 + +export class PrintRichText extends FabricImage implements IPrintObject { + controlId: string + controlType = 'richtext' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + /** 富文本 HTML(协议字段 value) */ + htmlValue?: string + + /** 渲染序号:连续输入时只应用最新一次栅格化结果,防止旧结果覆盖新内容 */ + private renderSeq = 0 + + constructor(control: RichTextControl) { + super(document.createElement('canvas'), { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.htmlValue = control.value + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + // 初始尺寸(未缩放),regenerate 会按它换算 scale + this.set({ width: mm(control.width), height: mm(control.height) }) + void this.regenerate() + } + + /** 重新栅格化富文本位图,并按控件宽高缩放显示 */ + async regenerate(): Promise { + const w = this.width || 1 + const h = this.height || 1 + const seq = ++this.renderSeq + const canvas = await renderRichTextToCanvas(this.htmlValue ?? '', w, h) + if (seq !== this.renderSeq) return // 已有更新的渲染,丢弃过期结果 + if (canvas) { + this.setElement(canvas) + this.set({ scaleX: w / canvas.width, scaleY: h / canvas.height }) + } + this.setCoords() + this.canvas?.requestRenderAll() + } + + toControl(): RichTextControl { + return { + ...readBaseGeometry(this), + type: 'richtext', + value: this.htmlValue, + printable: this.printable, + visibleIf: this.visibleIf, + locked: this.lockMovementX && this.lockMovementY ? true : undefined, + name: this.controlName, + } + } + + applyControlProps(control: RichTextControl): void { + this.htmlValue = control.value + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ + lockMovementX: control.locked, + lockMovementY: control.locked, + scaleX: 1, + scaleY: 1, + width: mm(control.width), + height: mm(control.height), + }) + void this.regenerate() + } +} + +/* ------------------------------ 位图渲染 helpers ------------------------------ */ + +/** 富文本 HTML → 设计期位图 canvas(失败降级为纯文本占位) */ +async function renderRichTextToCanvas( + html: string, + w: number, + h: number, +): Promise { + try { + // 画布预览同样必须消毒(防模板注入),渲染期有 data-binder 二次消毒 + const safe = toXmlSafe(DOMPurify.sanitize(html || '', { USE_PROFILES: { html: true } })) + const svg = + `` + + `` + + `
` + + `${safe}
` + return await svgToCanvas(svg, 1, '#ffffff') + } catch { + return drawFallback(stripHtml(html), w, h) + } +} + +/** 降级占位:白底 + 纯文本(SVG 光栅化失败时保证画布上仍能看到内容) */ +function drawFallback(text: string, w: number, h: number): HTMLCanvasElement { + const canvas = document.createElement('canvas') + canvas.width = Math.max(1, Math.ceil(w)) + canvas.height = Math.max(1, Math.ceil(h)) + const ctx = canvas.getContext('2d') + if (!ctx) return canvas + ctx.fillStyle = '#ffffff' + ctx.fillRect(0, 0, canvas.width, canvas.height) + ctx.fillStyle = '#000000' + ctx.font = `${DESIGN_FONT_PX}px system-ui, sans-serif` + const lines = (text || '富文本').split('\n') + const maxLines = Math.max(1, Math.floor(canvas.height / (DESIGN_FONT_PX + 4))) + const step = DESIGN_FONT_PX + 4 + for (let i = 0; i < Math.min(lines.length, maxLines); i++) { + ctx.fillText((lines[i] ?? '').slice(0, 80), 4, 4 + i * step) + } + return canvas +} + +/** 剥标签 + 解码实体的纯文本提取(仅用于降级显示) */ +function stripHtml(html: string): string { + return String(html ?? '') + .replace(/<[^>]*>/g, '') + .replace(/ /gi, ' ') + .replace(/&/gi, '&') + .replace(/</gi, '<') + .replace(/>/gi, '>') + .replace(/"/gi, '"') + .replace(/'/gi, "'") + .replace(/\s+/g, ' ') + .trim() +} diff --git a/src/design/canvas/controls/PrintTable.ts b/src/design/canvas/controls/PrintTable.ts new file mode 100644 index 0000000..af65257 --- /dev/null +++ b/src/design/canvas/controls/PrintTable.ts @@ -0,0 +1,231 @@ +/** + * PrintTable —— 表格控件(《设计方案》§5.4) + * + * 【方案 A】表格的**视觉**由 HTML overlay(TableViewLayer.vue + table-design-render.ts)负责, + * 与运行期 html-renderer 共用同一套类名与样式语义,做到设计即打印、零漂移。 + * 本 Fabric 对象退化为「透明宿主」:只承担选中 / 拖拽 / 缩放手柄 / 层级 / 序列化, + * 位图上仅画一圈极淡的虚线轮廓,作为 overlay 未就绪时的兜底提示。 + * + * 若把 `htmlPreview` 置为 false(单测 / 降级),则回落到旧的 Fabric 2D 预览。 + */ +import { FabricImage } from 'fabric' +import type { TableCell, TableControl, TableColumn } from '@/types/control' +import { mm, readBaseGeometry, type IPrintObject } from './PrintObject' +import { ptToPx } from '@/core/units' + +const BORDER_COLOR = '#333333' +const HEADER_BG = '#F5F7FA' +const TEXT_COLOR = '#1f2329' +const PLACEHOLDER_COLOR = '#606266' +/** overlay 兜底轮廓色(极淡,正常情况下被 overlay 的真实边框完全覆盖) */ +const GHOST_COLOR = 'rgba(120,130,145,0.35)' + +export class PrintTable extends FabricImage implements IPrintObject { + controlId: string + controlType = 'table' as const + zoneId?: string + printable = true + visibleIf?: string + controlName?: string + + dataSource?: string + /** 内嵌数据行(导入数据场景;与 dataSource 解耦,运行期直接作为表格数据) */ + data?: Array> + columns: TableColumn[] + options: TableControl['options'] + + /** 设计期单元格网格(方案 A:双击可编辑的内容与样式载体) */ + cells?: TableCell[][] + headerRows?: number + staticRows?: number + designRows?: number + + /** true = 视觉交给 HTML overlay,位图只画兜底轮廓 */ + htmlPreview = true + + constructor(control: TableControl) { + super(document.createElement('canvas'), { + left: mm(control.left), + top: mm(control.top), + angle: control.angle ?? 0, + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.dataSource = control.dataSource + this.data = control.data + this.columns = control.columns + this.options = control.options + this.cells = control.cells + this.headerRows = control.headerRows + this.staticRows = control.staticRows + this.designRows = control.designRows + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ width: mm(control.width), height: mm(control.height) }) + this.regenerate() + } + + /** 重绘设计期预览(2x 分辨率保证缩放清晰) */ + regenerate(): void { + const w = Math.max(this.width || 1, 10) + const h = Math.max(this.height || 1, 10) + const ratio = 2 + const canvas = document.createElement('canvas') + canvas.width = Math.ceil(w * ratio) + canvas.height = Math.ceil(h * ratio) + const ctx = canvas.getContext('2d')! + ctx.scale(ratio, ratio) + + if (this.htmlPreview) { + this.drawGhost(ctx, w, h) + } else { + this.drawLegacyPreview(ctx, w, h, ratio) + } + + // 注意:setElement 会把 width/height 重置为位图像素尺寸,缩放必须在 setElement 之后按新尺寸计算 + this.setElement(canvas) + this.set({ scaleX: w / canvas.width, scaleY: h / canvas.height }) + this.setCoords() + this.canvas?.requestRenderAll() + } + + /** overlay 模式:透明底 + 一圈极淡虚线,保证控件"存在感"且不与 overlay 抢像素 */ + private drawGhost(ctx: CanvasRenderingContext2D, w: number, h: number): void { + ctx.clearRect(0, 0, w, h) + ctx.strokeStyle = GHOST_COLOR + ctx.lineWidth = 0.5 + ctx.setLineDash([3, 3]) + ctx.strokeRect(0.5, 0.5, w - 1, h - 1) + ctx.setLineDash([]) + } + + /** 旧版 Fabric 2D 预览(降级路径,行为与方案 A 之前一致) */ + private drawLegacyPreview(ctx: CanvasRenderingContext2D, w: number, h: number, ratio: number): void { + ctx.fillStyle = '#ffffff' + ctx.fillRect(0, 0, w, h) + + const headerH = Math.min(28, h * 0.3) + const dataRowH = Math.min(24, (h - headerH) / 4) + const fontSize = ptToPx(9) + + // 列宽按比例分配(协议列宽是 mm,控件宽是 px,归一化即可) + const totalColMm = this.columns.reduce((s, c) => s + c.width, 0) || 1 + const colXs: number[] = [0] + for (const col of this.columns) { + colXs.push(colXs[colXs.length - 1]! + (col.width / totalColMm) * w) + } + + // 表头 + ctx.fillStyle = HEADER_BG + ctx.fillRect(0, 0, w, headerH) + ctx.font = `bold ${fontSize}px "Source Han Sans CN", "PingFang SC", sans-serif` + ctx.fillStyle = TEXT_COLOR + ctx.textBaseline = 'middle' + this.columns.forEach((col, i) => { + const x0 = colXs[i]! + const cw = colXs[i + 1]! - x0 + ctx.save() + ctx.beginPath() + ctx.rect(x0, 0, cw, headerH) + ctx.clip() + const tx = col.headerAlign === 'center' || (!col.headerAlign && col.align === 'center') + ? x0 + cw / 2 + : col.headerAlign === 'right' || (!col.headerAlign && col.align === 'right') + ? x0 + cw - 4 + : x0 + 4 + ctx.textAlign = col.headerAlign ?? col.align ?? 'left' + ctx.fillText(col.title || ' ', tx, headerH / 2) + ctx.restore() + }) + + // 示例数据行(绑定字段占位符) + ctx.font = `${fontSize}px "Source Han Sans CN", "PingFang SC", sans-serif` + this.columns.forEach((col, i) => { + const x0 = colXs[i]! + const cw = colXs[i + 1]! - x0 + const cellText = col.expression ?? (col.field ? `{{item.${col.field}}}` : '') + if (!cellText) return + ctx.save() + ctx.beginPath() + ctx.rect(x0, headerH, cw, dataRowH) + ctx.clip() + ctx.fillStyle = PLACEHOLDER_COLOR + ctx.textAlign = col.align ?? 'left' + const tx = col.align === 'center' ? x0 + cw / 2 : col.align === 'right' ? x0 + cw - 4 : x0 + 4 + ctx.fillText(cellText, tx, headerH + dataRowH / 2) + ctx.restore() + }) + + // 边框线 + const borders = this.options?.borders ?? 'all' + ctx.strokeStyle = BORDER_COLOR + ctx.lineWidth = 1 / ratio > 0.5 ? 1 : 0.75 + if (borders !== 'none') { + ctx.strokeRect(0.5, 0.5, w - 1, h - 1) + ctx.beginPath() + ctx.moveTo(0, headerH) + ctx.lineTo(w, headerH) + ctx.stroke() + if (borders === 'all') { + for (let i = 1; i < colXs.length - 1; i++) { + ctx.beginPath() + ctx.moveTo(colXs[i]!, 0) + ctx.lineTo(colXs[i]!, h) + ctx.stroke() + } + for (let r = 1; r <= 2; r++) { + const y = headerH + dataRowH * r + if (y < h) { + ctx.beginPath() + ctx.moveTo(0, y) + ctx.lineTo(w, y) + ctx.stroke() + } + } + } + } + } + + toControl(): TableControl { + return { + ...readBaseGeometry(this), + type: 'table', + dataSource: this.dataSource, + data: this.data, + columns: this.columns, + options: this.options, + cells: this.cells, + headerRows: this.headerRows, + staticRows: this.staticRows, + designRows: this.designRows, + printable: this.printable, + visibleIf: this.visibleIf, + name: this.controlName, + } + } + + applyControlProps(control: TableControl): void { + this.dataSource = control.dataSource + this.data = control.data + this.columns = control.columns + this.options = control.options + this.cells = control.cells + this.headerRows = control.headerRows + this.staticRows = control.staticRows + this.designRows = control.designRows + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.set({ + lockMovementX: control.locked, + lockMovementY: control.locked, + scaleX: 1, + scaleY: 1, + width: mm(control.width), + height: mm(control.height), + }) + this.regenerate() + } +} diff --git a/src/design/canvas/controls/PrintText.ts b/src/design/canvas/controls/PrintText.ts new file mode 100644 index 0000000..d83e72e --- /dev/null +++ b/src/design/canvas/controls/PrintText.ts @@ -0,0 +1,111 @@ +/** + * PrintText —— 文本控件(《设计方案》§5.2 / §5.3) + * 基于 fabric.Textbox:对齐/字体/颜色均为原生属性。 + * 字号协议层是 pt,Fabric 用 px,此处统一换算。 + */ +import { Textbox } from 'fabric' +import type { TextControl, TextStyle } from '@/types/control' +import { ptToPx, pxToPt } from '@/core/units' +import { mm, px, readBaseGeometry, round2, type IPrintObject } from './PrintObject' + +export class PrintText extends Textbox implements IPrintObject { + controlId: string + controlType = 'text' as const + zoneId?: string + + /** 协议 extras(设计期保留,显示文本只是占位) */ + binding?: string + expression?: string + controlFormat?: TextControl['format'] + printable = true + visibleIf?: string + controlName?: string + + constructor(control: TextControl) { + super(displayText(control), { + left: mm(control.left), + top: mm(control.top), + width: mm(control.width), + angle: control.angle ?? 0, + ...styleToFabric(control.style), + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.controlId = control.id + this.zoneId = undefined + this.binding = control.binding + this.expression = control.expression + this.controlFormat = control.format + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + } + + /** 序列化回协议模型(px → mm / px → pt) */ + toControl(): TextControl { + return { + ...readBaseGeometry(this), + type: 'text', + value: this.binding || this.expression ? undefined : this.text, + binding: this.binding, + expression: this.expression, + format: this.controlFormat, + printable: this.printable, + visibleIf: this.visibleIf, + locked: this.lockMovementX && this.lockMovementY ? true : undefined, + name: this.controlName, + style: { + fontSize: round2(pxToPt(this.fontSize)), + fill: typeof this.fill === 'string' ? this.fill : undefined, + fontFamily: this.fontFamily, + fontWeight: (this.fontWeight as 'normal' | 'bold') || undefined, + fontStyle: this.fontStyle === 'italic' ? 'italic' : undefined, + textDecoration: this.underline ? 'underline' : undefined, + textAlign: (this.textAlign as TextStyle['textAlign']) || undefined, + lineHeight: this.lineHeight, + letterSpacing: this.charSpacing ? round2(pxToPt(this.charSpacing / 1000 * this.fontSize)) : undefined, + }, + } + } + + /** 属性面板编辑后回写(不触发 object:modified) */ + applyControlProps(control: TextControl): void { + this.set({ + ...styleToFabric(control.style), + lockMovementX: control.locked, + lockMovementY: control.locked, + }) + this.binding = control.binding + this.expression = control.expression + this.controlFormat = control.format + this.printable = control.printable ?? true + this.visibleIf = control.visibleIf + this.controlName = control.name + this.text = displayText(control) + // 非绑定文本允许直接改内容尺寸 + if (control.width) this.set('width', mm(control.width)) + this.setCoords() + this.canvas?.requestRenderAll() + } +} + +/** 设计期显示文本:绑定/表达式显示占位符,静态文本显示本体 */ +function displayText(control: TextControl): string { + if (control.expression) return control.expression + if (control.binding) return `{{${control.binding}}}` + return control.value ?? '文本' +} + +function styleToFabric(style?: TextStyle) { + return { + fontSize: ptToPx(style?.fontSize ?? 12), + fill: style?.fill ?? '#000000', + fontFamily: style?.fontFamily ?? 'Source Han Sans CN, PingFang SC, sans-serif', + fontWeight: style?.fontWeight ?? 'normal', + fontStyle: style?.fontStyle ?? 'normal', + underline: style?.textDecoration === 'underline', + textAlign: style?.textAlign ?? 'left', + lineHeight: style?.lineHeight ?? 1.16, + charSpacing: style?.letterSpacing ? (style.letterSpacing / (style.fontSize ?? 12)) * 1000 : 0, + } +} diff --git a/src/design/canvas/controls/PrintZone.ts b/src/design/canvas/controls/PrintZone.ts new file mode 100644 index 0000000..cd202e9 --- /dev/null +++ b/src/design/canvas/controls/PrintZone.ts @@ -0,0 +1,107 @@ +/** + * PrintZone —— 页眉/页脚区域控件(《设计方案》§5.14) + * + * 画布表现:一条横向锁定色带(页顶或页底), + * - 自身不可移动/旋转(位置由页顶/页底固定) + * - 仅保留内边缘一个 resize 手柄改高度(header=下边缘 mb,footer=上边缘 mt) + * - 单例:每页最多一个 header + 一个 footer(由 CanvasDesigner.addZone 保证) + * - 序列化目标是 sections[header/footer],不进 body.components + */ +import { Rect } from 'fabric' +import type { ZoneControl } from '@/types/control' +import { mm, px, round2, type IPrintObject } from './PrintObject' + +export class PrintZone extends Rect implements IPrintObject { + controlId: string + controlType = 'zone' as const + + zone: 'header' | 'footer' + repeat = true + + constructor(control: ZoneControl, pageWidthMm: number, pageHeightMm: number) { + const h = mm(control.zoneHeight) + super({ + left: 0, + top: control.zone === 'header' ? 0 : mm(pageHeightMm) - h, + width: mm(pageWidthMm), + height: h, + fill: control.zone === 'header' ? 'rgba(22,119,255,0.05)' : 'rgba(22,119,255,0.04)', + stroke: 'rgba(22,119,255,0.45)', + strokeWidth: 1, + strokeDashArray: [6, 3], + // 位置锁定:不可移动/旋转/横拉 + lockMovementX: true, + lockMovementY: true, + lockRotation: true, + hasControls: true, + hasBorders: true, + selectable: true, + }) + this.controlId = control.id + this.zone = control.zone + this.repeat = control.repeat ?? true + this.restrictControls() + } + + /** 只留内边缘手柄:header 用底部中点(mb),footer 用顶部中点(mt) */ + private restrictControls(): void { + const edge = this.zone === 'header' ? 'mb' : 'mt' + const controls = { ...this.controls } + for (const key of Object.keys(controls)) { + if (key !== edge) { + // 其余手柄禁用 + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete controls[key] + } + } + this.controls = controls + } + + /** 页面尺寸变化时由 CanvasDesigner 调用,重贴页顶/页底 */ + relayout(pageWidthMm: number, pageHeightMm: number): void { + const h = this.height * this.scaleY + this.set({ + left: 0, + width: mm(pageWidthMm), + scaleX: 1, + scaleY: 1, + height: h, + top: this.zone === 'header' ? 0 : mm(pageHeightMm) - h, + }) + this.setCoords() + } + + get zoneHeightMm(): number { + return round2(px(this.height * this.scaleY)) + } + + toControl(): ZoneControl { + return { + id: this.controlId, + type: 'zone', + zone: this.zone, + left: 0, + top: 0, + width: round2(px(this.getScaledWidth())), + height: this.zoneHeightMm, + zoneHeight: this.zoneHeightMm, + repeat: this.repeat, + printable: true, + children: [], // 子组件由 CanvasDesigner.serialize 按几何归属填充 + } + } + + applyControlProps(control: ZoneControl): void { + this.repeat = control.repeat ?? true + if (control.zoneHeight && control.zoneHeight !== this.zoneHeightMm) { + const pageH = this.canvas ? px(this.canvas.getHeight()) : 0 + this.set({ + scaleY: 1, + height: mm(control.zoneHeight), + top: this.zone === 'header' ? 0 : mm(pageH) - mm(control.zoneHeight), + }) + this.setCoords() + } + this.canvas?.requestRenderAll() + } +} diff --git a/src/design/canvas/controls/index.ts b/src/design/canvas/controls/index.ts new file mode 100644 index 0000000..9ed1661 --- /dev/null +++ b/src/design/canvas/controls/index.ts @@ -0,0 +1,83 @@ +/** + * 控件注册表:协议模型 → Fabric 对象 的统一工厂 + */ +import type { FabricObject } from 'fabric' +import type { AnyControl } from '@/types/control' +import { PrintText } from './PrintText' +import { PrintRect } from './PrintRect' +import { PrintCircle } from './PrintCircle' +import { PrintLine } from './PrintLine' +import { PrintImage } from './PrintImage' +import { PrintBarcode } from './PrintBarcode' +import { PrintQrcode } from './PrintQrcode' +import { PrintTable } from './PrintTable' +import { PrintZone } from './PrintZone' +import { PrintRichText } from './PrintRichText' +import { PrintChart } from './PrintChart' +import type { IPrintObject } from './PrintObject' + +export { + PrintText, + PrintRect, + PrintCircle, + PrintLine, + PrintImage, + PrintBarcode, + PrintQrcode, + PrintTable, + PrintZone, + PrintRichText, + PrintChart, +} +export { isPrintObject, type IPrintObject } from './PrintObject' + +export interface PageSizeMm { + widthMm: number + heightMm: number +} + +/** 由协议模型创建 Fabric 控件对象 */ +export function createFabricControl( + control: AnyControl, + page: PageSizeMm, +): FabricObject & IPrintObject { + switch (control.type) { + case 'text': + return new PrintText(control) + case 'rect': + return control.shape === 'circle' ? new PrintCircle(control) : new PrintRect(control) + case 'line': + return new PrintLine(control) + case 'image': { + const obj = new PrintImage(control) + void obj.loadImage() + return obj + } + case 'barcode': + return new PrintBarcode(control) + case 'qrcode': + return new PrintQrcode(control) + case 'table': + return new PrintTable(control) + case 'chart': + return new PrintChart(control) + case 'zone': + return new PrintZone(control, page.widthMm, page.heightMm) + case 'richtext': + return new PrintRichText(control) + } +} + +/** 控件类型的中文名(图层/面板显示用) */ +export const CONTROL_TYPE_LABEL: Record = { + text: '文本', + image: '图片', + table: '表格', + barcode: '条码', + qrcode: '二维码', + rect: '矩形', + line: '线条', + richtext: '富文本', + zone: '区域', + chart: '图表', +} diff --git a/src/design/canvas/guides/SmartGuides.ts b/src/design/canvas/guides/SmartGuides.ts new file mode 100644 index 0000000..a50bd8f --- /dev/null +++ b/src/design/canvas/guides/SmartGuides.ts @@ -0,0 +1,583 @@ +/** + * SmartGuides —— 智能吸附对齐线(增强版) + * + * 拖拽控件时与其他控件/页面/页边距的边缘、中心对齐吸附, + * 四边同时检测,命中后在 contextTop 上画参考线。 + * Shift 按住时临时禁用吸附。 + * + * 渲染内容: + * 1. 蓝色虚线边界辅助线 —— 沿元素上下左右 4 条边横穿整个画布,对齐标尺 + * 2. 红色实线 —— 边缘/中心吸附命中线(Figma 红粉) + * 3. 绿色标签 —— 吸附间距(mm 数值) + * 4. 灰色标签 —— 元素实时坐标(mm) + */ +import type { Canvas, FabricObject, Rect } from 'fabric' +import { MM_TO_PX, DEFAULT_SNAP_THRESHOLD, RULER_THICK } from '@/utils/constants' +import { isPrintObject } from '../controls' +import { rulerBand } from '../rulers/rulerHighlight' + +const SNAP_PX = DEFAULT_SNAP_THRESHOLD * MM_TO_PX + +/** 配色方案: + * 蓝 = 元素边界辅助线(用户指定) + * 玫红 = 吸附命中(与边界线区分) + * 翠绿 = 距离标签 + * 灰 = 实时坐标信息 + * 护眼绿底白字药丸 = 辅助线端点坐标读数 + */ +const COLOR_BOUNDING = '#378ADD' +const COLOR_SNAP = '#F43F5E' +const COLOR_DISTANCE = '#10B981' +const COLOR_POSITION = '#AFB4BD' +/** 药丸底色(护眼绿,白字;不透明保证浅/深色画布均清晰) */ +const CHIP_BG = '#3FA776' +const CHIP_BORDER = 'rgba(255,255,255,0.45)' +const CHIP_FG = '#FFFFFF' + +/* ------------------------------------------------------------------ */ +/* 类型定义 */ +/* ------------------------------------------------------------------ */ + +interface SnapCandidate { + value: number + orientation: 'v' | 'h' + source: 'page' | 'margin' | 'element' +} + +interface SnapResult { + line: number + orientation: 'v' | 'h' + source: 'page' | 'margin' | 'element' + /** 命中的吸附候选值 */ + candidateValue: number + /** 目标边的原始值 */ + targetValue: number +} + +interface MarginMm { + top: number + bottom: number + left: number + right: number +} + +export interface SmartGuidesConfig { + /** 网格尺寸(mm) */ + gridSizeMm: number + /** 视觉网格线开关(仅作参考、不强绑定元素) */ + gridVisible: boolean + /** 视觉网格线颜色(rgba / hex) */ + gridColor: string +} + +/* ------------------------------------------------------------------ */ +/* SmartGuides */ +/* ------------------------------------------------------------------ */ + +export class SmartGuides { + private canvas: Canvas + private getPage: () => Rect + private getMarginMm: () => MarginMm + private snapResults: SnapResult[] = [] + private movingTarget: FabricObject | null = null + private shiftHeld = false + private lastSnapV: SnapResult | null = null + private lastSnapH: SnapResult | null = null + + /** 网格配置(由 CanvasDesigner 通过 setter 控制;仅视觉参考,不吸附) */ + config: SmartGuidesConfig = { + gridSizeMm: 5, + gridVisible: false, + gridColor: 'rgba(120,130,145,0.5)', + } + + /* ---------- 事件处理 ---------- */ + + private onMoving = (e: { target?: FabricObject }) => { + const target = e.target + if (!target) { + this.movingTarget = null + this.snapResults = [] + rulerBand.value = null + return + } + this.movingTarget = target + if (!this.shiftHeld) this.snap(target) + this.syncBand(target) + } + + /** 缩放时也同步标尺高亮带(不改辅助线,只反映尺寸变化) */ + private onScaling = (e: { target?: FabricObject }) => { + const target = e.target + if (target) this.syncBand(target) + } + + /** 选中/切换选中:把高亮带对准当前活动元素 */ + private onSelection = () => { + const obj = this.canvas.getActiveObject() + if (obj && isPrintObject(obj)) this.syncBand(obj) + else rulerBand.value = null + } + + private onSelectionCleared = () => { + rulerBand.value = null + } + + /** 把元素的包围盒(画布逻辑 px)写入标尺高亮带;非法尺寸则清除 */ + private syncBand(target: FabricObject): void { + const b = target.getBoundingRect() + if (b.width > 0 && b.height > 0) { + rulerBand.value = { left: b.left, top: b.top, width: b.width, height: b.height } + } else { + rulerBand.value = null + } + } + + private onUp = () => { + this.movingTarget = null + this.snapResults = [] + // 注意:不清空 rulerBand —— 拖拽结束后元素仍处于选中态,高亮带保留反映其位置/尺寸 + this.canvas.requestRenderAll() + } + + private onAfterRender = (opt: { ctx: CanvasRenderingContext2D }) => { + // 网格始终可见(独立于拖拽) + this.drawGrid(opt.ctx) + // 拖拽时覆盖吸附辅助线(网格上叠加) + if (this.movingTarget) { + this.draw(opt.ctx) + } + // 常驻辅助线:开启 showGuides 的控件始终显示(与拖拽状态无关) + this.drawPersistent(opt.ctx) + } + + private onKey = (e: KeyboardEvent) => { + if (e.key === 'Shift') this.shiftHeld = e.type === 'keydown' + } + + /* ---------- 生命周期 ---------- */ + + constructor( + canvas: Canvas, + getPage: () => Rect, + getMarginMm: () => MarginMm, + ) { + this.canvas = canvas + this.getPage = getPage + this.getMarginMm = getMarginMm + + canvas.on('object:moving', this.onMoving) + canvas.on('object:scaling', this.onScaling) + canvas.on('mouse:up', this.onUp) + canvas.on('selection:created', this.onSelection) + canvas.on('selection:updated', this.onSelection) + canvas.on('selection:cleared', this.onSelectionCleared) + canvas.on('after:render', this.onAfterRender) + window.addEventListener('keydown', this.onKey) + window.addEventListener('keyup', this.onKey) + } + + dispose(): void { + this.canvas.off('object:moving', this.onMoving) + this.canvas.off('object:scaling', this.onScaling) + this.canvas.off('mouse:up', this.onUp) + this.canvas.off('selection:created', this.onSelection) + this.canvas.off('selection:updated', this.onSelection) + this.canvas.off('selection:cleared', this.onSelectionCleared) + this.canvas.off('after:render', this.onAfterRender) + window.removeEventListener('keydown', this.onKey) + window.removeEventListener('keyup', this.onKey) + } + + /* ---------- 候选集收集 ---------- */ + + private collectCandidates(skip: FabricObject): SnapCandidate[] { + const page = this.getPage() + const margin = this.getMarginMm() + const pw = page.width + const ph = page.height + const ml = margin.left * MM_TO_PX + const mr = (210 - margin.right) * MM_TO_PX + const mt = margin.top * MM_TO_PX + const mb = (297 - margin.bottom) * MM_TO_PX + const results: SnapCandidate[] = [] + + // 1. 页面边缘 + 中心(始终存在于未命名候选列表中) + const addV = (value: number, source: SnapCandidate['source']) => results.push({ value, orientation: 'v', source }) + const addH = (value: number, source: SnapCandidate['source']) => results.push({ value, orientation: 'h', source }) + + // 页面 + addV(0, 'page'); addV(pw / 2, 'page'); addV(pw, 'page') + addH(0, 'page'); addH(ph / 2, 'page'); addH(ph, 'page') + + // 页边距(内边界) + addV(ml, 'margin'); addV(mr, 'margin') + addH(mt, 'margin'); addH(mb, 'margin') + // 页边距中心 + addV(ml + (mr - ml) / 2, 'margin') + addH(mt + (mb - mt) / 2, 'margin') + + // 2. 其他控件 + for (const obj of this.canvas.getObjects()) { + if (obj === skip || obj === page || !isPrintObject(obj)) continue + const b = obj.getBoundingRect() + if (b.width === 0 && b.height === 0) continue + addV(b.left, 'element') + addV(b.left + b.width / 2, 'element') + addV(b.left + b.width, 'element') + addH(b.top, 'element') + addH(b.top + b.height / 2, 'element') + addH(b.top + b.height, 'element') + } + + return results + } + + /* ---------- 吸附计算 ---------- */ + + private snap(target: FabricObject): void { + const b = target.getBoundingRect() + if (b.width === 0 && b.height === 0) { this.snapResults = []; return } + + const candidates = this.collectCandidates(target) + const vCands = candidates.filter((c) => c.orientation === 'v') + const hCands = candidates.filter((c) => c.orientation === 'h') + + // 目标六条参考位置 + const vTargets = [ + { key: 'left', value: b.left }, + { key: 'centerX', value: b.left + b.width / 2 }, + { key: 'right', value: b.left + b.width }, + ] + const hTargets = [ + { key: 'top', value: b.top }, + { key: 'centerY', value: b.top + b.height / 2 }, + { key: 'bottom', value: b.top + b.height }, + ] + + this.snapResults = [] + + // --- 记录所有命中(用于渲染辅助线) --- + for (const t of vTargets) { + for (const c of vCands) { + if (Math.abs(c.value - t.value) <= SNAP_PX) { + this.snapResults.push({ + line: c.value, + orientation: 'v', + source: c.source, + candidateValue: c.value, + targetValue: t.value, + }) + } + } + } + for (const t of hTargets) { + for (const c of hCands) { + if (Math.abs(c.value - t.value) <= SNAP_PX) { + this.snapResults.push({ + line: c.value, + orientation: 'h', + source: c.source, + candidateValue: c.value, + targetValue: t.value, + }) + } + } + } + + // --- 取最佳吸附线应用位置修正(候选线吸附;网格仅作参考、不强绑定) --- + let bestV: SnapResult | null = null + let bestVDist = Infinity + for (const t of vTargets) { + for (const c of vCands) { + const dist = Math.abs(c.value - t.value) + if (dist <= SNAP_PX && dist < bestVDist) { + bestVDist = dist + bestV = { + line: c.value, + orientation: 'v', + source: c.source, + candidateValue: c.value, + targetValue: t.value, + } + } + } + } + let bestH: SnapResult | null = null + let bestHDist = Infinity + for (const t of hTargets) { + for (const c of hCands) { + const dist = Math.abs(c.value - t.value) + if (dist <= SNAP_PX && dist < bestHDist) { + bestHDist = dist + bestH = { + line: c.value, + orientation: 'h', + source: c.source, + candidateValue: c.value, + targetValue: t.value, + } + } + } + } + + if (bestV) { + const delta = bestV.candidateValue - bestV.targetValue + target.set('left', (target.left ?? 0) + delta) + target.setCoords() + } + if (bestH) { + const delta = bestH.candidateValue - bestH.targetValue + target.set('top', (target.top ?? 0) + delta) + target.setCoords() + } + + this.lastSnapV = bestV + this.lastSnapH = bestH + } + + /* ---------- 渲染 ---------- */ + + private draw(ctx: CanvasRenderingContext2D): void { + const target = this.movingTarget + if (!target) return + + const vt = this.canvas.viewportTransform! + const zoom = this.canvas.getZoom() + const b = target.getBoundingRect() + + // 画布可视范围(逻辑坐标):含页面外的灰色区域。 + // 元素拖出模板(A4)范围时,辅助线也贯穿整个画布、跟着出去。 + const cw = this.canvas.getWidth() + const ch = this.canvas.getHeight() + const viewX0 = -vt[4] / vt[0] + const viewX1 = (cw - vt[4]) / vt[0] + const viewY0 = -vt[5] / vt[3] + const viewY1 = (ch - vt[5]) / vt[3] + + ctx.save() + // 应用视口变换,后续坐标都是画布逻辑坐标 + ctx.transform(vt[0], vt[1], vt[2], vt[3], vt[4], vt[5]) + + // 反锯齿 + ctx.imageSmoothingEnabled = false + + // --- 1. 蓝色虚线边界辅助线:沿元素上下左右 4 条边,横穿整个可视画布(含页面外) --- + this.drawBoundary(ctx, b, viewX0, viewX1, viewY0, viewY1, zoom) + + // --- 2. 红色实线:吸附命中线(贯穿可视画布) --- + // 去重(同一位置可能有多条记录) + const drawn = new Set() + ctx.strokeStyle = COLOR_SNAP + ctx.lineWidth = 1.5 / zoom + for (const r of this.snapResults) { + const key = `${r.orientation}:${r.line.toFixed(3)}` + if (drawn.has(key)) continue + drawn.add(key) + + ctx.beginPath() + if (r.orientation === 'v') { + ctx.moveTo(r.line, viewY0); ctx.lineTo(r.line, viewY1) + } else { + ctx.moveTo(viewX0, r.line); ctx.lineTo(viewX1, r.line) + } + ctx.stroke() + } + + // --- 3. 绿色距离标签(吸附间距 mm) --- + const fontScale = 1 / zoom + ctx.fillStyle = COLOR_DISTANCE + ctx.font = `${Math.max(10, 11 * fontScale)}px "Inter", "PingFang SC", sans-serif` + + const labelDrawn = new Set() + for (const r of this.snapResults) { + const distMm = r.candidateValue - r.targetValue + const absDist = (Math.abs(distMm) / MM_TO_PX).toFixed(1) + const lk = `${r.orientation}:${r.line.toFixed(3)}:${absDist}` + if (labelDrawn.has(lk)) continue + labelDrawn.add(lk) + + let lx: number, ly: number + if (r.orientation === 'v') { + lx = r.line + 4 / zoom + ly = b.top - 4 / zoom + } else { + lx = b.left - 4 / zoom + ly = r.line - 4 / zoom + } + // 确保标签在画布可见范围内 + lx = Math.max(4 / zoom, Math.min(viewX1 - 60 / zoom, lx)) + ly = Math.max(8 / zoom, Math.min(viewY1, ly)) + ctx.fillText(`${absDist} mm`, lx, ly) + } + + // --- 4. 元素实时坐标(左上角) --- + const xMm = (b.left / MM_TO_PX).toFixed(1) + const yMm = (b.top / MM_TO_PX).toFixed(1) + ctx.fillStyle = COLOR_POSITION + ctx.font = `${Math.max(9, 10 * fontScale)}px "Inter", "PingFang SC", sans-serif` + const posLabelX = b.left + 3 / zoom + const posLabelY = b.top - 5 / zoom + ctx.fillText(`X:${xMm} Y:${yMm}`, Math.max(0, posLabelX), Math.max(10 / zoom, posLabelY)) + + // --- 5. 辅助线端点坐标标签(对齐标尺位置,mm) --- + this.drawChips(ctx, b, viewX0, viewX1, viewY0, viewY1, zoom) + + ctx.restore() + } + + /** 蓝色虚线边界辅助线:沿元素上下左右 4 条边,横穿整个可视画布(含页面外) */ + private drawBoundary( + ctx: CanvasRenderingContext2D, + b: { left: number; top: number; width: number; height: number }, + viewX0: number, viewX1: number, viewY0: number, viewY1: number, + zoom: number, + ): void { + const x0 = b.left + const x1 = b.left + b.width + const y0 = b.top + const y1 = b.top + b.height + ctx.strokeStyle = COLOR_BOUNDING + ctx.lineWidth = 1 / zoom + ctx.setLineDash([4 / zoom, 3 / zoom]) + ctx.beginPath() + // 顶/底边水平线(横穿整画布,对齐水平标尺) + ctx.moveTo(viewX0, y0); ctx.lineTo(viewX1, y0) + ctx.moveTo(viewX0, y1); ctx.lineTo(viewX1, y1) + // 左/右边竖直线(竖穿整画布,对齐垂直标尺) + ctx.moveTo(x0, viewY0); ctx.lineTo(x0, viewY1) + ctx.moveTo(x1, viewY0); ctx.lineTo(x1, viewY1) + ctx.stroke() + ctx.setLineDash([]) + } + + /** 辅助线端点坐标标签(靠近标尺那一头,mm) */ + private drawChips( + ctx: CanvasRenderingContext2D, + b: { left: number; top: number; width: number; height: number }, + viewX0: number, viewX1: number, viewY0: number, viewY1: number, + zoom: number, + ): void { + const x0 = b.left + const x1 = b.left + b.width + const y0 = b.top + const y1 = b.top + b.height + // 放在「靠近标尺那一头」,但必须越过 RulerOverlay 覆盖层(左垂直标尺 / 顶水平标尺各 RULER_THICK px), + // 否则文字会被标尺 DOM 遮住看不见。各偏移 RULER_THICK+4px 正好落在标尺外侧的画布可见区。 + const clampY = (v: number) => Math.max(viewY0 + (RULER_THICK + 6) / zoom, Math.min(viewY1 - 6 / zoom, v)) + const clampX = (v: number) => Math.max(viewX0 + (RULER_THICK + 6) / zoom, Math.min(viewX1 - 6 / zoom, v)) + // 水平线(顶/底)显示 Y,贴在左端、标尺外侧 + this.drawChip(ctx, `Y ${(y0 / MM_TO_PX).toFixed(1)}`, viewX0 + (RULER_THICK + 4) / zoom, clampY(y0), zoom, CHIP_BG, CHIP_BORDER, CHIP_FG) + this.drawChip(ctx, `Y ${(y1 / MM_TO_PX).toFixed(1)}`, viewX0 + (RULER_THICK + 4) / zoom, clampY(y1), zoom, CHIP_BG, CHIP_BORDER, CHIP_FG) + // 竖直线(左/右)显示 X,贴在顶端、标尺外侧 + this.drawChip(ctx, `X ${(x0 / MM_TO_PX).toFixed(1)}`, clampX(x0), viewY0 + (RULER_THICK + 4) / zoom, zoom, CHIP_BG, CHIP_BORDER, CHIP_FG) + this.drawChip(ctx, `X ${(x1 / MM_TO_PX).toFixed(1)}`, clampX(x1), viewY0 + (RULER_THICK + 4) / zoom, zoom, CHIP_BG, CHIP_BORDER, CHIP_FG) + } + + /** 常驻辅助线:遍历开启 showGuides 的控件,始终绘制其 4 条边界线 + 坐标标签(不限于拖拽时) */ + private drawPersistent(ctx: CanvasRenderingContext2D): void { + const movingId = this.movingTarget ? (this.movingTarget as unknown as { controlId?: string }).controlId : undefined + const vt = this.canvas.viewportTransform! + const zoom = this.canvas.getZoom() + const cw = this.canvas.getWidth() + const ch = this.canvas.getHeight() + const viewX0 = -vt[4] / vt[0] + const viewX1 = (cw - vt[4]) / vt[0] + const viewY0 = -vt[5] / vt[3] + const viewY1 = (ch - vt[5]) / vt[3] + + for (const obj of this.canvas.getObjects()) { + if (!isPrintObject(obj)) continue + if ((obj as unknown as { showGuides?: boolean }).showGuides !== true) continue + if ((obj as unknown as { controlId?: string }).controlId === movingId) continue // 拖拽中由 draw() 处理,避免重绘 + const b = obj.getBoundingRect() + if (b.width === 0 && b.height === 0) continue + ctx.save() + ctx.transform(vt[0], vt[1], vt[2], vt[3], vt[4], vt[5]) + ctx.imageSmoothingEnabled = false + this.drawBoundary(ctx, b, viewX0, viewX1, viewY0, viewY1, zoom) + this.drawChips(ctx, b, viewX0, viewX1, viewY0, viewY1, zoom) + ctx.restore() + } + } + + /** 网格视觉线绘制(零依赖,内联到 draw 前调用) */ + private drawGrid(ctx: CanvasRenderingContext2D): void { + if (!this.config.gridVisible) return + const vt = this.canvas.viewportTransform! + const zoom = this.canvas.getZoom() + if (zoom < 0.3) return // 太小不绘,否则糊成一片 + const page = this.getPage() + const step = this.config.gridSizeMm * MM_TO_PX + + ctx.save() + ctx.transform(vt[0], vt[1], vt[2], vt[3], vt[4], vt[5]) + ctx.strokeStyle = this.config.gridColor + ctx.lineWidth = 0.5 / zoom + ctx.beginPath() + for (let x = 0; x <= page.width; x += step) { + ctx.moveTo(x, 0) + ctx.lineTo(x, page.height) + } + for (let y = 0; y <= page.height; y += step) { + ctx.moveTo(0, y) + ctx.lineTo(page.width, y) + } + ctx.stroke() + ctx.restore() + } + + /** 圆角矩形路径(Canvas2D 无原生 roundRect 兼容写法) */ + private roundRectPath( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + w: number, + h: number, + r: number, + ): void { + ctx.moveTo(x + r, y) + ctx.arcTo(x + w, y, x + w, y + h, r) + ctx.arcTo(x + w, y + h, x, y + h, r) + ctx.arcTo(x, y + h, x, y, r) + ctx.arcTo(x, y, x + w, y, r) + ctx.closePath() + } + + /** 小药丸标签:不透明深色底 + 浅色描边 + 白字,用于辅助线端点显示标尺坐标(mm)。 + * 不透明底色保证在浅色页面与深色画布上都清晰可读;浅描边让药丸边缘在深色背景中也能被分辨。 */ + private drawChip( + ctx: CanvasRenderingContext2D, + text: string, + x: number, + y: number, + zoom: number, + bg: string, + border: string, + fg: string, + ): void { + ctx.save() + ctx.font = `600 ${Math.max(10, 11 / zoom)}px "Inter", "PingFang SC", sans-serif` + ctx.textBaseline = 'middle' + const padX = 4 / zoom + const h = (11 / zoom) + (4 / zoom) + const w = ctx.measureText(text).width + padX * 2 + const top = y - h / 2 + const radius = 3 / zoom + + // 不透明底色 + ctx.beginPath() + this.roundRectPath(ctx, x, top, w, h, radius) + ctx.fillStyle = bg + ctx.fill() + // 浅色描边,确保深色背景下药丸边缘可见 + ctx.lineWidth = 1 / zoom + ctx.strokeStyle = border + ctx.stroke() + + // 文字 + ctx.fillStyle = fg + ctx.fillText(text, x + padX, y) + ctx.restore() + } +} diff --git a/src/design/canvas/page-gap.spec.ts b/src/design/canvas/page-gap.spec.ts new file mode 100644 index 0000000..97c1996 --- /dev/null +++ b/src/design/canvas/page-gap.spec.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest' +import { pageGapPx, modelTopToCanvasY, canvasYToModelTop } from './page-gap' +import { MM_TO_PX } from '@/utils/constants' + +const GAP = 24 +const A4_MM = 297 +const MARGIN_TOP_PX = 10 * MM_TO_PX + +describe('pageGapPx —— 页索引 → 累计间距', () => { + it('首页/负值/零页高 → 0', () => { + expect(pageGapPx(0, A4_MM, GAP)).toBe(0) + expect(pageGapPx(100, A4_MM, GAP)).toBe(0) + expect(pageGapPx(-10, A4_MM, GAP)).toBe(0) + expect(pageGapPx(500, 0, GAP)).toBe(0) + }) + + it('第 2 页起每页累加一个间距', () => { + expect(pageGapPx(300, A4_MM, GAP)).toBe(GAP) + expect(pageGapPx(600, A4_MM, GAP)).toBe(GAP * 2) + expect(pageGapPx(900, A4_MM, GAP)).toBe(GAP * 3) + }) +}) + +describe('modelTopToCanvasY / canvasYToModelTop —— 正反互换', () => { + it('第 1 页控件往返不漂移', () => { + for (const topMm of [0, 5, 100, 296.5]) { + const y = modelTopToCanvasY(topMm, MARGIN_TOP_PX, A4_MM, GAP) + expect(canvasYToModelTop(y, MARGIN_TOP_PX, A4_MM, GAP)).toBeCloseTo(topMm, 6) + } + }) + + it('第 2 页控件含累计间距且往返不漂移', () => { + const topMm = 300 + const y = modelTopToCanvasY(topMm, MARGIN_TOP_PX, A4_MM, GAP) + // 第 2 页内容区顶 = (297 + 24) + 页边距,300mm 应在第 2 页内偏下 3mm + expect(y).toBeCloseTo(MARGIN_TOP_PX + 300 * MM_TO_PX + GAP, 6) + expect(canvasYToModelTop(y, MARGIN_TOP_PX, A4_MM, GAP)).toBeCloseTo(topMm, 6) + }) + + it('第 3 页控件往返不漂移(两层间距)', () => { + const topMm = 610 + const y = modelTopToCanvasY(topMm, MARGIN_TOP_PX, A4_MM, GAP) + expect(canvasYToModelTop(y, MARGIN_TOP_PX, A4_MM, GAP)).toBeCloseTo(topMm, 6) + }) + + it('页边界附近往返不漂移(迭代收敛)', () => { + for (const topMm of [296.9, 297.1, 593.9, 594.2]) { + const y = modelTopToCanvasY(topMm, MARGIN_TOP_PX, A4_MM, GAP) + expect(canvasYToModelTop(y, MARGIN_TOP_PX, A4_MM, GAP)).toBeCloseTo(topMm, 6) + } + }) +}) diff --git a/src/design/canvas/page-gap.ts b/src/design/canvas/page-gap.ts new file mode 100644 index 0000000..1a0d7b3 --- /dev/null +++ b/src/design/canvas/page-gap.ts @@ -0,0 +1,43 @@ +/** + * 画布页间留白几何纯函数(设计态专用)。 + * + * 模型 mm 保持 flush(与渲染/导出一致),页间距只叠加在画布 px 坐标上: + * - 页 1 顶部 = 页边距;页 i+1 顶部 = i × (页高 + 间距) + 页边距 + * - 正文控件绝对 y = 页边距 + topMm×PX + 累计页间距(按所在页索引) + */ +import { MM_TO_PX } from '@/utils/constants' + +/** 模型 top(mm, flush) 所在页索引 → 该页之前的累计页间距(px);首页恒为 0 */ +export function pageGapPx(topMm: number, pageHeightMm: number, gapPx: number): number { + if (topMm <= 0 || pageHeightMm <= 0) return 0 + const i = Math.floor(topMm / pageHeightMm) + return i > 0 ? i * gapPx : 0 +} + +/** 模型 top(mm, flush) → 画布绝对 y(px):页边距 + 页内偏移 + 累计页间距 */ +export function modelTopToCanvasY( + topMm: number, + marginTopPx: number, + pageHeightMm: number, + gapPx: number, +): number { + return marginTopPx + topMm * MM_TO_PX + pageGapPx(topMm, pageHeightMm, gapPx) +} + +/** 画布绝对 y(px) → 模型 top(mm, flush):反解累计页间距(近页边界最多迭代 2 次收敛) */ +export function canvasYToModelTop( + y: number, + marginTopPx: number, + pageHeightMm: number, + gapPx: number, +): number { + const y0 = y - marginTopPx + let topMm = y0 / MM_TO_PX + let gap = pageGapPx(topMm, pageHeightMm, gapPx) + if (gap > 0) { + topMm = (y0 - gap) / MM_TO_PX + const gap2 = pageGapPx(topMm, pageHeightMm, gapPx) + if (gap2 !== gap) topMm = (y0 - gap2) / MM_TO_PX + } + return topMm +} diff --git a/src/design/canvas/page-geometry.spec.ts b/src/design/canvas/page-geometry.spec.ts new file mode 100644 index 0000000..64a0b2a --- /dev/null +++ b/src/design/canvas/page-geometry.spec.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest' +import { computePhysicalPageCount } from './page-geometry' + +describe('computePhysicalPageCount', () => { + const A4 = 297 + + it('无内容时至少 1 页', () => { + expect(computePhysicalPageCount(0, A4)).toBe(1) + expect(computePhysicalPageCount(-50, A4)).toBe(1) + }) + + it('内容落在第 1 页内 → 1 页', () => { + expect(computePhysicalPageCount(100, A4)).toBe(1) + expect(computePhysicalPageCount(296, A4)).toBe(1) + }) + + it('内容越过第 1 页底部 → 第 2 页', () => { + expect(computePhysicalPageCount(298, A4)).toBe(2) + expect(computePhysicalPageCount(400, A4)).toBe(2) + }) + + it('内容延伸到第 3 页', () => { + expect(computePhysicalPageCount(594, A4)).toBe(3) + expect(computePhysicalPageCount(800, A4)).toBe(3) + }) + + it('页高为 0 时安全返回 1', () => { + expect(computePhysicalPageCount(500, 0)).toBe(1) + }) +}) diff --git a/src/design/canvas/page-geometry.ts b/src/design/canvas/page-geometry.ts new file mode 100644 index 0000000..9168d37 --- /dev/null +++ b/src/design/canvas/page-geometry.ts @@ -0,0 +1,11 @@ +/** + * 画布多页几何纯函数(与渲染端 layoutStaticOnly 的落页口径一致)。 + * + * 正文控件按 `marginTop + top` 绝对定位(top 为相对内容区 mm), + * 越过内容区底部即落入下一页。物理页数 = floor(maxBottomMm / pageHeightMm) + 1。 + */ +export function computePhysicalPageCount(maxBottomMm: number, pageHeightMm: number): number { + if (pageHeightMm <= 0) return 1 + if (maxBottomMm <= 0) return 1 + return Math.max(1, Math.floor(maxBottomMm / pageHeightMm) + 1) +} diff --git a/src/design/canvas/rulers/RulerOverlay.vue b/src/design/canvas/rulers/RulerOverlay.vue new file mode 100644 index 0000000..a77b6b1 --- /dev/null +++ b/src/design/canvas/rulers/RulerOverlay.vue @@ -0,0 +1,211 @@ + + + diff --git a/src/design/canvas/rulers/rulerHighlight.ts b/src/design/canvas/rulers/rulerHighlight.ts new file mode 100644 index 0000000..bce7062 --- /dev/null +++ b/src/design/canvas/rulers/rulerHighlight.ts @@ -0,0 +1,24 @@ +/** + * rulerHighlight —— 标尺高亮带共享状态 + * + * SmartGuides 在 object:moving / object:scaling / selection 事件里, + * 把「当前选中/拖拽元素的包围盒(画布逻辑 px = 页面 px,未含缩放/平移)」 + * 写入这个模块级 ref;RulerOverlay 读取后按 viewport 映射成标尺上的彩色高亮带 + * (顶标尺带 = 元素宽度,左标尺带 = 元素高度),随组件移动丝滑滑动。 + * + * 用模块级 ref 而非 Pinia store,是为了避免把画布引擎模块与组件 store 耦合。 + */ +import { ref } from 'vue' + +export interface RulerBand { + /** 左边缘(画布逻辑 px) */ + left: number + /** 上边缘(画布逻辑 px) */ + top: number + /** 宽(画布逻辑 px) */ + width: number + /** 高(画布逻辑 px) */ + height: number +} + +export const rulerBand = ref(null) diff --git a/src/design/canvas/table-design-render.spec.ts b/src/design/canvas/table-design-render.spec.ts new file mode 100644 index 0000000..c3536c0 --- /dev/null +++ b/src/design/canvas/table-design-render.spec.ts @@ -0,0 +1,172 @@ +/** + * 设计期表格 HTML 渲染单测(table-design-render.ts) + * + * 覆盖:renderTableGridHtml 标记(data-row/data-col、类名、占位符、纵向合并)、 + * gridColXs / gridRowHeights / computeGridLayout 几何、hitTestCell 命中测试。 + * 重点确保"设计所见"与运行期类名一致,且无 cells 的旧模板能正常回落。 + */ +import { describe, expect, it } from 'vitest' +import type { TableControl } from '@/types/control' +import { + computeGridLayout, + gridColXs, + gridRowHeights, + hitTestCell, + renderTableGridHtml, +} from './table-design-render' +import { seedSummaryTail } from '@/core/layout-engine/table-cells' + +function baseTable(over: Partial = {}): TableControl { + return { + id: 't1', + type: 'table', + left: 0, + top: 0, + width: 180, + height: 60, + columns: [ + { title: '序号', expression: '{{rowIndex + 1}}', width: 15, align: 'center', headerAlign: 'center' }, + { title: '名称', field: 'name', width: 60 }, + { title: '数量', field: 'qty', width: 25, align: 'right', headerAlign: 'center' }, + ], + options: { borders: 'all', verticalAlign: 'middle' }, + ...over, + } +} + +describe('renderTableGridHtml', () => { + it('数据表:输出表格根类 op-table + 运行期同款修饰类', () => { + const html = renderTableGridHtml(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(html).toContain('class="op-node op-table b-all va-middle ts-none"') + expect(html).toContain('table-layout:fixed') + }) + + it('边框修复:不再内联 border-collapse:collapse(否则被 overlay overflow:hidden 切掉右/下边)', () => { + const html = renderTableGridHtml(baseTable({ dataSource: 'items' })) + expect(html).not.toContain('border-collapse:collapse') + // 外框走 tableCss 的 separate + 末行/末列补边,四边始终完整 + expect(html).toContain('table-layout:fixed') + }) + + it('样式预设:默认 none 挂 ts-none,指定后挂对应类', () => { + const def = renderTableGridHtml(baseTable({ dataSource: 'items', options: { borders: 'all', verticalAlign: 'middle' } })) + expect(def).toContain('ts-none') + const zebra = renderTableGridHtml(baseTable({ dataSource: 'items', options: { borders: 'all', verticalAlign: 'middle', tableStyle: 'zebra' } })) + expect(zebra).toContain('ts-zebra') + }) + + it('聚合尾行对齐运行期类名:本页合计→is-subtotal,总计/大写金额→is-summary', () => { + const t = seedSummaryTail( + baseTable({ + dataSource: 'items', + columns: [ + { title: '名称', field: 'items[].name', width: 60 }, + { title: '数量', field: 'items[].qty', width: 25, align: 'right', headerAlign: 'center' }, + ], + data: [{ name: 'A', qty: 2 }], + }), + { numericColumns: [1], moneyColumn: 1, capital: true }, + ) + const html = renderTableGridHtml(t) + expect(html).toContain('is-subtotal') + expect(html).toContain('is-summary') + }) + + it('每个 td 带 data-row/data-col,便于 overlay 编辑写回', () => { + const html = renderTableGridHtml(baseTable({ dataSource: 'items' })) + expect(html).toContain('data-row="0"') + expect(html).toContain('data-col="2"') + }) + + it('行类与语义匹配:表头 / 数据样例行 / 静态', () => { + const html = renderTableGridHtml(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(html).toContain('class="is-header"') + expect(html).toContain('class="is-data is-template"') + expect(html).toContain('class="is-static"') + }) + + it('绑定的数据样例行:渲染为字段占位符而非空', () => { + const html = renderTableGridHtml(baseTable({ dataSource: 'items' })) + expect(html).toContain('{{item.name}}') + expect(html).toContain('{{item.qty}}') + // 表头占位符走 expression + expect(html).toContain('{{rowIndex + 1}}') + }) + + it('静态文字行:原样输出字面量', () => { + const cells = [ + [{ text: '列1' }, { text: '列2' }, { text: '列3' }], + [{ field: 'a' }, { field: 'b' }, { field: 'c' }], + [{ text: '合计:' }, {}, {}], + ] + const html = renderTableGridHtml(baseTable({ dataSource: 'items', cells, staticRows: 1 })) + expect(html).toContain('合计:') + expect(html).toContain('列1') + }) + + it('空单元格输出
占位,避免高度塌陷', () => { + // 布局网格(无 dataSource)多数静态格为空 → 输出
+ const html = renderTableGridHtml(baseTable({ designRows: 3 })) + expect(html).toContain('
') + }) + + it('横向合并:输出 colspan 属性', () => { + const t = baseTable({ dataSource: 'items' }) + const cells = [ + [{ text: 'H' }, { text: '' }, { text: '' }], + [{ field: 'a', colSpan: 2 }, { text: '' }, { field: 'c' }], + ] + const html = renderTableGridHtml({ ...t, cells, staticRows: 0 }) + expect(html).toContain('colspan="2"') + }) +}) + +describe('几何布局', () => { + it('gridColXs:长度 colCount+1,末值=表格宽,单调递增', () => { + const xs = gridColXs(baseTable({ dataSource: 'items' })) + expect(xs.length).toBe(4) + expect(xs[0]).toBe(0) + expect(xs[3]).toBe(180) + expect(xs[1]! < xs[2]!).toBe(true) + }) + + it('gridRowHeights:返回 rowCount 个正值', () => { + const hs = gridRowHeights(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(hs.length).toBe(3) + expect(hs.every((h) => h > 0)).toBe(true) + }) + + it('computeGridLayout:汇总 colXs/rowYs/rowCount/colCount', () => { + const g = computeGridLayout(baseTable({ dataSource: 'items', staticRows: 1 })) + expect(g.colCount).toBe(3) + expect(g.rowCount).toBe(3) + expect(g.colXs.length).toBe(4) + expect(g.rowYs.length).toBe(4) + expect(g.rowYs[0]).toBe(0) + }) +}) + +describe('hitTestCell', () => { + const layout = computeGridLayout(baseTable({ dataSource: 'items', staticRows: 1 })) + + it('左上角命中第 0 行第 0 列', () => { + expect(hitTestCell(layout, 0.01, 0.01)).toEqual({ row: 0, col: 0 }) + }) + + it('右下角命中末行末列', () => { + const hit = hitTestCell(layout, 0.99, 0.99) + expect(hit).toEqual({ row: layout.rowCount - 1, col: layout.colCount - 1 }) + }) + + it('越界(frac 超出 [0,1])返回 null', () => { + expect(hitTestCell(layout, -0.1, 0.5)).toBeNull() + expect(hitTestCell(layout, 0.5, 1.2)).toBeNull() + }) + + it('落在第 2 列区间命中 col=1', () => { + // colXs 近似 [0, ~25, ~136, 180],中间点应落第 1 列 + const x = (layout.colXs[1]! + layout.colXs[2]!) / 2 / layout.colXs[3]! + const hit = hitTestCell(layout, x, 0.01) + expect(hit?.col).toBe(1) + }) +}) diff --git a/src/design/canvas/table-design-render.ts b/src/design/canvas/table-design-render.ts new file mode 100644 index 0000000..bc1076b --- /dev/null +++ b/src/design/canvas/table-design-render.ts @@ -0,0 +1,194 @@ +/** + * table-design-render —— 设计期表格 HTML 渲染(方案 A:与运行期共用渲染语义) + * + * 设计期表格以 HTML overlay 呈现(绝对定位、transform 同步 Fabric 节点 + 视口), + * 由本模块产出的 HTML 与运行期 html-renderer 使用同一套样式类 / 内联样式键, + * 从而做到"设计即打印"的真·所见即所得;双击单元格即可原生 contenteditable 编辑。 + * + * 所有几何以 mm 表达(CSS 原生支持 mm 单位),由调用方统一做 zoom/scale 变换。 + */ +import type { TableCell, TableControl, TableColumn } from '@/types/control' +import { + buildDesignGrid, + computeSpanLayout, + designRowHeights, + resolveCellStyleFor, + type DesignRowKind, +} from '@/core/layout-engine/table-cells' +import { isAggToken, parseAggToken } from '@/core/layout-engine/aggregate' +import { normalizeColumnWidths } from '@/core/layout-engine/table-engine' +import { diagonalBackground } from '@/core/renderer-html/css-generator' + +const PLACEHOLDER = '#9aa0a6' + +/** + * 各可视行的行高(mm),与渲染、命中测试共用。 + * 实现下沉到 core(designRowHeights),**运行期布局网格用的是同一份算法** —— 这样 + * 空白表格在画布上什么样,打印出来就是什么样。 + */ +export function gridRowHeights(control: TableControl): number[] { + return designRowHeights(control) +} + +/** 列累计 x(mm,长度 colCount+1,末值 = 表格宽) */ +export function gridColXs(control: TableControl): number[] { + const widths = normalizeColumnWidths(control.columns, control.width) + const xs = [0] + for (const w of widths) xs.push(xs[xs.length - 1]! + w) + return xs +} + +export interface GridLayout { + colXs: number[] + rowYs: number[] + rowCount: number + colCount: number +} + +/** 设计期网格布局(mm),供渲染与双击命中测试共用 */ +export function computeGridLayout(control: TableControl): GridLayout { + const grid = buildDesignGrid(control) + const colXs = gridColXs(control) + const rowHs = gridRowHeights(control) + const rowYs = [0] + for (const h of rowHs) rowYs.push(rowYs[rowYs.length - 1]! + h) + return { colXs, rowYs, rowCount: grid.rowCount, colCount: grid.colCount } +} + +/** 计算双击点命中的单元格(fraction ∈ [0,1]) */ +export function hitTestCell( + layout: GridLayout, + fracX: number, + fracY: number, +): { row: number; col: number } | null { + if (fracX < 0 || fracX > 1 || fracY < 0 || fracY > 1) return null + const x = fracX * layout.colXs[layout.colXs.length - 1]! + const y = fracY * layout.rowYs[layout.rowYs.length - 1]! + let col = 0 + for (let c = 1; c < layout.colXs.length; c++) { + if (x <= layout.colXs[c]!) { col = c - 1; break } + col = c - 1 + } + let row = 0 + for (let r = 1; r < layout.rowYs.length; r++) { + if (y <= layout.rowYs[r]!) { row = r - 1; break } + row = r - 1 + } + return { row, col } +} + +function esc(s: string): string { + return s.replace(/&/g, '&').replace(//g, '>') +} + +/** 字段 → 占位符显示:含数组标记(items[].name)直接用路径,避免 {{item.items[].name}} 冗余前缀 */ +function fieldPlaceholder(field: string): string { + return field.includes('[]') ? `{{${field}}}` : `{{item.${field}}}` +} + +function placeholderOf(cell: TableCell, col: TableColumn | undefined): string { + if (cell.expression) return cell.expression + if (cell.field) return fieldPlaceholder(cell.field) + if (col?.expression) return col.expression + if (col?.field) return fieldPlaceholder(col.field) + return '' +} + +/** + * 单元格内联样式。 + * 只输出**显式设置过**的属性,未设置的交给 css-generator 的 `.op-table` 类规则兜底 + * (例如表头加粗、va-* 垂直对齐),保证设计期与运行期视觉一致。 + */ +function cellStyle( + control: TableControl, + cell: TableCell, + col: TableColumn | undefined, + placeholder: boolean, + kind: DesignRowKind, +): string { + const s = resolveCellStyleFor(control, col, cell, kind) + const parts: string[] = [] + if (s.fontSize) parts.push(`font-size:${s.fontSize}pt`) + if (s.fontFamily) parts.push(`font-family:${s.fontFamily}`) + if (s.bold !== undefined) parts.push(`font-weight:${s.bold ? 'bold' : 'normal'}`) + if (s.italic !== undefined) parts.push(`font-style:${s.italic ? 'italic' : 'normal'}`) + if (s.underline !== undefined) parts.push(`text-decoration:${s.underline ? 'underline' : 'none'}`) + if (s.align) parts.push(`text-align:${s.align}`) + if (s.valign) parts.push(`vertical-align:${s.valign}`) + if (s.backgroundColor) parts.push(`background-color:${s.backgroundColor}`) + if (s.diagonal) parts.push(diagonalBackground(s.diagonal)) + if (placeholder) parts.push(`color:${PLACEHOLDER}`) + else if (s.color) parts.push(`color:${s.color}`) + return parts.join(';') +} + +/** + * 渲染设计期表格 HTML(表头 / 数据样例行 / 静态尾行)。 + * + * 类名与运行期 html-renderer 完全一致(op-table / b-* / va-* / is-header / is-data), + * 因此同一份 CSS 即可驱动两端 —— 这正是"设计即打印"的根。 + * 每个 td 带 `data-row` / `data-col`,供 overlay 做 DOM 级单元格定位与编辑写回。 + */ +export function renderTableGridHtml(control: TableControl): string { + const grid = buildDesignGrid(control) + const opts = control.options ?? {} + const borders = opts.borders ?? 'all' + const va = opts.verticalAlign ?? 'middle' + const colXs = gridColXs(control) + const colWs = colXs.slice(1).map((_, i) => colXs[i + 1]! - colXs[i]!) + const rowHs = gridRowHeights(control) + // 合并布局(colSpan + rowSpan 一起算),与设计期渲染、运行期引擎共用同一份结果 + const spanLayout = computeSpanLayout(grid.cells, grid.rowCount, grid.colCount) + + const cols = colWs.map((w) => ``).join('') + const rows: string[] = [] + + for (let r = 0; r < grid.rowCount; r++) { + const isHeaderRow = r < grid.headerRows + const isDataRow = grid.isData && r === grid.headerRows + const kind: DesignRowKind = isHeaderRow ? 'header' : isDataRow ? 'data' : 'static' + const spanRow = spanLayout[r]! + const cells: string[] = [] + let c = 0 + while (c < grid.colCount) { + if (spanRow[c]!.skip) { + c++ + continue + } + const cell = grid.cells[r]?.[c] ?? {} + const col = control.columns[c] + const span = spanRow[c]!.colSpan + const rspan = spanRow[c]!.rowSpan + const isAgg = isAggToken(cell.text) + const bound = Boolean(cell.field || cell.expression || (isDataRow && col?.field) || (isDataRow && col?.expression)) + // 聚合 token({{#totalSum}} 等)按占位符着色显示,提示用户这是动态计算单元格 + const placeholder = bound || isAgg + const text = bound ? placeholderOf(cell, col) : (cell.text ?? '') + const style = cellStyle(control, cell, col, placeholder, kind) + const spanAttr = span > 1 ? ` colspan="${span}"` : '' + const rspanAttr = rspan > 1 ? ` rowspan="${rspan}"` : '' + const tdClass = isAgg ? ' is-agg' : '' + const styleAttr = style ? ` style="${style}"` : '' + cells.push( + `${esc(text) || '
'}`, + ) + c += span + } + const rh = rowHs[r] + let cls = isDataRow ? 'is-data is-template' : isHeaderRow ? 'is-header' : 'is-static' + // 聚合尾行(本页合计/总计/大写金额)对齐运行期 is-subtotal/is-summary 类,保证样式预设两端一致 + if (!isHeaderRow && !isDataRow) { + const tokens = (grid.cells[r] ?? []).map((c) => parseAggToken(c.text)) + if (tokens.some((t) => t === 'pageSum' || t === 'pageAvg' || t === 'pageCount')) cls = 'is-subtotal' + else if (tokens.some((t) => t === 'totalSum' || t === 'totalAvg' || t === 'totalCount' || t === 'pageCap' || t === 'totalCap')) cls = 'is-summary' + } + rows.push(`${cells.join('')}`) + } + + const tableStyle = opts.tableStyle ?? 'none' + return ( + `` + + `${cols}${rows.join('')}
` + ) +} diff --git a/src/design/canvas/table-style-presets.ts b/src/design/canvas/table-style-presets.ts new file mode 100644 index 0000000..4a17e02 --- /dev/null +++ b/src/design/canvas/table-style-presets.ts @@ -0,0 +1,45 @@ +/** + * 表格样式预设元数据 —— 设计期「样式库」弹窗与属性面板共用的唯一真相源。 + * + * 每个预设对应 css-generator.ts 里的一组 `.op-table.ts-` 规则; + * 渲染端(table-design-render.ts / html-renderer.ts)把同名 class 挂在 上, + * 因此画布、预览、PDF、样式库预览四端视觉完全一致。 + * + * 预设不只是"配色"——`borders` 可捆绑边框框线方案(如三线表 / 外框 / 全网格), + * 点击即同时套用配色与框线,类似 Excel 把"表格样式"与"框线"打包切换。 + */ +import type { TableStylePreset } from '@/types/control' + +export interface TableStyleMeta { + /** 对应
上的 ts- class 与 TableOptions.tableStyle 取值 */ + key: TableStylePreset + /** 展示名(按钮 / 卡片标题) */ + label: string + /** 一句话描述,帮助快速区分相近样式 */ + desc: string + /** 可选:捆绑的边框框线方案;不填则只改配色、保留当前边框 */ + borders?: 'all' | 'none' | 'horizontal' | 'outline' | 'three-line' +} + +/** Excel 式表格样式库(按"从简到繁 / 配色 → 框线"排列) */ +export const TABLE_STYLE_PRESETS: TableStyleMeta[] = [ + { key: 'none', label: '无样式', desc: '仅边框 + 表头加粗,无任何背景' }, + { key: 'header', label: '表头高亮', desc: '浅灰表头' }, + { key: 'header-dark', label: '深蓝表头', desc: '深蓝底白字' }, + { key: 'header-green', label: '绿色表头', desc: '绿底白字' }, + { key: 'zebra', label: '斑马纹', desc: '浅蓝交替行' }, + { key: 'zebra-blue', label: '蓝纹', desc: '深蓝交替行' }, + { key: 'zebra-gray', label: '灰纹', desc: '灰色交替行' }, + { key: 'three-segment', label: '三段式', desc: '表头 + 合计强调' }, + { key: 'banded-cols', label: '隔列着色', desc: '奇偶列交替底' }, + // —— 框线类(配色 + 边框打包) —— + { key: 'grid', label: '全网格', desc: '全框线 + 无配色', borders: 'all' }, + { key: 'minimal', label: '极简外框', desc: '仅外框、无内线', borders: 'outline' }, + { key: 'report', label: '三线表', desc: '顶/表头底/底线,无内线(报表风)', borders: 'three-line' }, + { key: 'timetable', label: '课表', desc: '全网格 + 表头高亮(角标斜线见单元格)', borders: 'all' }, +] + +/** 预设 key → 展示名(未知值回落「无样式」) */ +export function tableStyleLabel(key: TableStylePreset | undefined | null): string { + return TABLE_STYLE_PRESETS.find((p) => p.key === key)?.label ?? '无样式' +} diff --git a/src/design/canvas/zoom.ts b/src/design/canvas/zoom.ts new file mode 100644 index 0000000..72ef524 --- /dev/null +++ b/src/design/canvas/zoom.ts @@ -0,0 +1,15 @@ +/** + * 缩放工具函数(CanvasDesigner 的纯函数部分,供工具栏/快捷键复用) + */ +import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from '@/utils/constants' + +export const clampZoom = (z: number): number => Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, z)) + +export const nextZoomIn = (z: number): number => clampZoom(Math.round((z + ZOOM_STEP) * 100) / 100) +export const nextZoomOut = (z: number): number => clampZoom(Math.round((z - ZOOM_STEP) * 100) / 100) + +/** 缩放百分比显示(100% / 125% ...) */ +export const zoomLabel = (z: number): string => `${Math.round(z * 100)}%` + +/** 常用缩放下拉档位 */ +export const ZOOM_PRESETS = [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4] as const diff --git a/src/design/composables/useConfirm.ts b/src/design/composables/useConfirm.ts new file mode 100644 index 0000000..0560213 --- /dev/null +++ b/src/design/composables/useConfirm.ts @@ -0,0 +1,48 @@ +/** + * useConfirm —— 统一的「确认」弹窗(应用内系统弹窗) + * + * 取代浏览器原生 window.confirm / window.alert: + * - 原生 confirm 是浏览器 Chrome 级别的 UI,与项目主题/设计语言割裂,且阻塞线程; + * - 本封装基于 naive-ui 的 NDialogProvider(App.vue 已挂载 ), + * 弹窗随亮/暗主题自适应,文案与按钮可定制,体验一致。 + * + * 用法(必须在组件 + + + + diff --git a/src/design/modals/JsonViewerModal.vue b/src/design/modals/JsonViewerModal.vue new file mode 100644 index 0000000..b158d2f --- /dev/null +++ b/src/design/modals/JsonViewerModal.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/src/design/modals/PrintDialog.vue b/src/design/modals/PrintDialog.vue new file mode 100644 index 0000000..ae72e0d --- /dev/null +++ b/src/design/modals/PrintDialog.vue @@ -0,0 +1,459 @@ + + + + + diff --git a/src/design/modals/SettingsModal.vue b/src/design/modals/SettingsModal.vue new file mode 100644 index 0000000..a707388 --- /dev/null +++ b/src/design/modals/SettingsModal.vue @@ -0,0 +1,457 @@ + + + + + diff --git a/src/design/modals/TableStylePickerModal.vue b/src/design/modals/TableStylePickerModal.vue new file mode 100644 index 0000000..2358b20 --- /dev/null +++ b/src/design/modals/TableStylePickerModal.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/src/design/modals/TemplateMarket.vue b/src/design/modals/TemplateMarket.vue new file mode 100644 index 0000000..a0118bf --- /dev/null +++ b/src/design/modals/TemplateMarket.vue @@ -0,0 +1,211 @@ + + + + + + + diff --git a/src/design/modals/TemplateModal.vue b/src/design/modals/TemplateModal.vue new file mode 100644 index 0000000..7253ee8 --- /dev/null +++ b/src/design/modals/TemplateModal.vue @@ -0,0 +1,210 @@ + + + diff --git a/src/design/panels/ControlLibrary.vue b/src/design/panels/ControlLibrary.vue new file mode 100644 index 0000000..6471287 --- /dev/null +++ b/src/design/panels/ControlLibrary.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/src/design/panels/DataSourceTree.vue b/src/design/panels/DataSourceTree.vue new file mode 100644 index 0000000..a463813 --- /dev/null +++ b/src/design/panels/DataSourceTree.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/src/design/panels/LayerPanel.vue b/src/design/panels/LayerPanel.vue new file mode 100644 index 0000000..b269854 --- /dev/null +++ b/src/design/panels/LayerPanel.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/src/design/panels/LeftPanel.vue b/src/design/panels/LeftPanel.vue new file mode 100644 index 0000000..38e9821 --- /dev/null +++ b/src/design/panels/LeftPanel.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/design/panels/RightPanel.vue b/src/design/panels/RightPanel.vue new file mode 100644 index 0000000..1cfc68e --- /dev/null +++ b/src/design/panels/RightPanel.vue @@ -0,0 +1,365 @@ + + + + + diff --git a/src/design/panels/props/BindingEditor.vue b/src/design/panels/props/BindingEditor.vue new file mode 100644 index 0000000..95cb898 --- /dev/null +++ b/src/design/panels/props/BindingEditor.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/design/panels/props/ChartProps.vue b/src/design/panels/props/ChartProps.vue new file mode 100644 index 0000000..a7db255 --- /dev/null +++ b/src/design/panels/props/ChartProps.vue @@ -0,0 +1,261 @@ + + + + + diff --git a/src/design/panels/props/CodeProps.vue b/src/design/panels/props/CodeProps.vue new file mode 100644 index 0000000..5c76685 --- /dev/null +++ b/src/design/panels/props/CodeProps.vue @@ -0,0 +1,100 @@ + + + diff --git a/src/design/panels/props/CommonProps.vue b/src/design/panels/props/CommonProps.vue new file mode 100644 index 0000000..e1434e2 --- /dev/null +++ b/src/design/panels/props/CommonProps.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/src/design/panels/props/ImageProps.vue b/src/design/panels/props/ImageProps.vue new file mode 100644 index 0000000..bb7c9bf --- /dev/null +++ b/src/design/panels/props/ImageProps.vue @@ -0,0 +1,105 @@ + + + diff --git a/src/design/panels/props/RichTextEditor.vue b/src/design/panels/props/RichTextEditor.vue new file mode 100644 index 0000000..e4c9527 --- /dev/null +++ b/src/design/panels/props/RichTextEditor.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/src/design/panels/props/RichTextProps.vue b/src/design/panels/props/RichTextProps.vue new file mode 100644 index 0000000..dfa403b --- /dev/null +++ b/src/design/panels/props/RichTextProps.vue @@ -0,0 +1,56 @@ + + + diff --git a/src/design/panels/props/ShapeProps.vue b/src/design/panels/props/ShapeProps.vue new file mode 100644 index 0000000..1aa20c7 --- /dev/null +++ b/src/design/panels/props/ShapeProps.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/src/design/panels/props/TableProps.vue b/src/design/panels/props/TableProps.vue new file mode 100644 index 0000000..7864f7e --- /dev/null +++ b/src/design/panels/props/TableProps.vue @@ -0,0 +1,797 @@ + + + diff --git a/src/design/panels/props/TextProps.vue b/src/design/panels/props/TextProps.vue new file mode 100644 index 0000000..2dcd3e7 --- /dev/null +++ b/src/design/panels/props/TextProps.vue @@ -0,0 +1,312 @@ + + + diff --git a/src/design/panels/props/ZoneProps.vue b/src/design/panels/props/ZoneProps.vue new file mode 100644 index 0000000..60441e1 --- /dev/null +++ b/src/design/panels/props/ZoneProps.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/design/preview/PreviewPanel.vue b/src/design/preview/PreviewPanel.vue new file mode 100644 index 0000000..84f58ad --- /dev/null +++ b/src/design/preview/PreviewPanel.vue @@ -0,0 +1,416 @@ + + +