diff --git a/plugins/serious-reading/.gitignore b/plugins/serious-reading/.gitignore
new file mode 100644
index 00000000..eb85031e
--- /dev/null
+++ b/plugins/serious-reading/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+!preload/node_modules/
+dist/
+.git/
+*.log
+.env*
\ No newline at end of file
diff --git a/plugins/serious-reading/AGENTS.md b/plugins/serious-reading/AGENTS.md
new file mode 100644
index 00000000..36f5e498
--- /dev/null
+++ b/plugins/serious-reading/AGENTS.md
@@ -0,0 +1,137 @@
+# AGENTS.md
+
+----
+
+## 0. Non-negotiables
+
+These rules override everything else in this file when in conflict:
+
+1. **No flattery, no filler.** Skip openers like "Great question", "You're absolutely right", "Excellent idea", "I'd be happy to". Start with the answer or the action.
+2. **Disagree when you disagree.** If the user's premise is wrong, say so before doing the work. Agreeing with false premises to be polite is the single worst failure mode in coding agents.
+3. **Never fabricate.** Not file paths, not commit hashes, not API names, not test results, not library functions. If you don't know, read the file, run the command, or say "I don't know, let me check."
+4. **Stop when confused.** If the task has two plausible interpretations, ask. Do not pick silently and proceed.
+5. **Touch only what you must.** Every changed line must trace directly to the user's request. No drive-by refactors, reformatting, or "while I was in there" cleanups.
+
+---
+
+## 1. Before writing code
+
+**Goal: understand the problem and the codebase before producing a diff.**
+
+- State your plan in one or two sentences before editing. For anything non-trivial, produce a numbered list of steps with a verification check for each.
+- Read the files you will touch. Read the files that call the files you will touch. Claude Code: use subagents for exploration so the main context stays clean.
+- Match existing patterns in the codebase. If the project uses pattern X, use pattern X, even if you'd do it differently in a greenfield repo.
+- Surface assumptions out loud: "I'm assuming you want X, Y, Z. If that's wrong, say so." Do not bury assumptions inside the implementation.
+- If two approaches exist, present both with tradeoffs. Do not pick one silently. Exception: trivial tasks (typo, rename, log line) where the diff fits in one sentence.
+
+---
+
+## 2. Writing code: simplicity first
+
+**Goal: the minimum code that solves the stated problem. Nothing speculative.**
+
+- No features beyond what was asked.
+- No abstractions for single-use code. No configurability, flexibility, or hooks that were not requested.
+- No error handling for impossible scenarios. Handle the failures that can actually happen.
+- If the solution runs 200 lines and could be 50, rewrite it before showing it.
+- If you find yourself adding "for future extensibility", stop. Future extensibility is a future decision.
+- Bias toward deleting code over adding code. Shipping less is almost always better.
+
+The test: would a senior engineer reading the diff call this overcomplicated? If yes, simplify.
+
+---
+
+## 3. Surgical changes
+
+**Goal: clean, reviewable diffs. Change only what the request requires.**
+
+- Do not "improve" adjacent code, comments, formatting, or imports that are not part of the task.
+- Do not refactor code that works just because you are in the file.
+- Do not delete pre-existing dead code unless asked. If you notice it, mention it in the summary.
+- Do clean up orphans created by your own changes (unused imports, variables, functions your edit made obsolete).
+- Match the project's existing style exactly: indentation, quotes, naming, file layout.
+
+The test: every changed line traces directly to the user's request. If a line fails that test, revert it.
+
+---
+
+## 4. Goal-driven execution
+
+**Goal: define success as something you can verify, then loop until verified.**
+
+Rewrite vague asks into verifiable goals before starting:
+
+- "Add validation" becomes "Write tests for invalid inputs (empty, malformed, oversized), then make them pass."
+- "Fix the bug" becomes "Write a failing test that reproduces the reported symptom, then make it pass."
+- "Refactor X" becomes "Ensure the existing test suite passes before and after, and no public API changes."
+- "Make it faster" becomes "Benchmark the current hot path, identify the bottleneck with profiling, change it, show the benchmark is faster."
+
+For every task:
+
+1. State the success criteria before writing code.
+2. Write the verification (test, script, benchmark, screenshot diff) where practical.
+3. Run the verification. Read the output. Do not claim success without checking.
+4. If the verification fails, fix the cause, not the test.
+
+---
+
+## 5. Tool use and verification
+
+- Prefer running the code to guessing about the code. If a test suite exists, run it. If a linter exists, run it. If a type checker exists, run it.
+- Never report "done" based on a plausible-looking diff alone. Plausibility is not correctness.
+- When debugging, address root causes, not symptoms. Suppressing the error is not fixing the error.
+- For UI changes, verify visually: screenshot before, screenshot after, describe the diff.
+- Use CLI tools (gh, aws, gcloud, kubectl) when they exist. They are more context-efficient than reading docs or hitting APIs unauthenticated.
+- When reading logs, errors, or stack traces, read the whole thing. Half-read traces produce wrong fixes.
+
+---
+
+## 6. Session hygiene
+
+- Context is the constraint. Long sessions with accumulated failed attempts perform worse than fresh sessions with a better prompt.
+- After two failed corrections on the same issue, stop. Summarize what you learned and ask the user to reset the session with a sharper prompt.
+- Use subagents (Claude Code: "use subagents to investigate X") for exploration tasks that would otherwise pollute the main context with dozens of file reads.
+- When committing, write descriptive commit messages (subject under 72 chars, body explains the why). No "update file" or "fix bug" commits. No "Co-Authored-By: Claude" attribution unless the project explicitly wants it.
+
+---
+
+## 7. Communication style
+
+- Direct, not diplomatic. "This won't scale because X" beats "That's an interesting approach, but have you considered...".
+- Concise by default. Two or three short paragraphs unless the user asks for depth. No padding, no restating the question, no ceremonial closings.
+- When a question has a clear answer, give it. When it does not, say so and give your best read on the tradeoffs.
+- Celebrate only what matters: shipping, solving genuinely hard problems, metrics that moved. Not feature ideas, not scope creep, not "wouldn't it be cool if".
+- No excessive bullet points, no unprompted headers, no emoji. Prose is usually clearer than structure for short answers.
+
+---
+
+## 8. When to ask, when to proceed
+
+**Ask before proceeding when:**
+
+- The request has two plausible interpretations and the choice materially affects the output.
+- The change touches something you've been told is load-bearing, versioned, or has a migration path.
+- You need a credential, a secret, or a production resource you don't have access to.
+- The user's stated goal and the literal request appear to conflict.
+
+**Proceed without asking when:**
+
+- The task is trivial and reversible (typo, rename a local variable, add a log line).
+- The ambiguity can be resolved by reading the code or running a command.
+- The user has already answered the question once in this session.
+
+---
+
+## 9. Self-improvement loop
+
+**This file is living. Keep it short by keeping it honest.**
+
+After every session where the agent did something wrong:
+
+1. Ask: was the mistake because this file lacks a rule, or because the agent ignored a rule?
+2. If lacking: add the rule under "Project Learnings" below, written as concretely as possible ("Always use X for Y" not "be careful with Y").
+3. If ignored: the rule may be too long, too vague, or buried. Tighten it or move it up.
+4. Every few weeks, prune. For each line, ask: "Would removing this cause the agent to make a mistake?" If no, delete. Bloated AGENTS.md files get ignored wholesale.
+
+Boris Cherny (creator of Claude Code) keeps his team's file around 100 lines. Under 300 is a good ceiling. Over 500 and you are fighting your own config.
+
diff --git a/plugins/serious-reading/CHANGELOG.md b/plugins/serious-reading/CHANGELOG.md
new file mode 100644
index 00000000..5815b801
--- /dev/null
+++ b/plugins/serious-reading/CHANGELOG.md
@@ -0,0 +1,52 @@
+# Changelog
+
+本文件记录严肃阅读的版本变更历史。
+
+格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。
+
+## [1.1.0] - 2026-07-03
+
+### 变更
+
+- 插件名称从 "Serious Reading" 改为 "严肃阅读"(plugin.json title、UI 标题、README、index.html)
+
+### 安全修复
+
+- **XSS 漏洞修复** — EPUB 章节内容的 HTML 清洗从正则替换改为 DOMPurify,防止恶意 EPUB 通过 XSS 载荷读取本地文件 (`src/shared/parser.ts`)
+
+### Bug 修复
+
+- **EPUB 嵌套分页** — 高度测量分页算法改为递归展平嵌套块级元素,修复 EPUB 章节被 `
` 包裹时整章塞入一页导致内容截断的问题 (`src/reader/App.tsx`)
+- **搜索定位跳转末章** — 搜索结果落在章节标题空隙时不再错误跳转到最后一章,改用下一章节的 charOffset 作为上界判断 (`src/main/components/SearchDialog.tsx`)
+- **EPUB 冗余解析** — 打开 EPUB 文件时不再重复调用 `readEpub`,首次解析结果缓存后直接复用提取封面 (`src/main/App.tsx`)
+- **preload 全局引用** — `preload/main.js` 和 `preload/reader.js` 中裸 `ztools` 引用改为 `window.ztools`,避免上下文隔离环境下 ReferenceError (`preload/main.js`, `preload/reader.js`)
+- **EPUB 章节标题不一致** — 阅读窗 preload 的 `readEpub` 引入 NCX TOC 解析,与主窗 preload 保持一致,不再将所有章节标题硬编码为"第 X 章" (`preload/reader.js`)
+
+## [1.0.0] - 2026-07-03
+
+### 新增
+
+- 字体选择下拉菜单,内置 11 种中英文字体
+- 「清理空行」阅读选项,压缩 TXT 连续空行为单个段落间距
+- `toggle_reader` 命令(切换阅读窗显隐)
+- `reader_open` 文件打开命令,支持拖入 TXT/EPUB/PDF 直接阅读
+
+### 变更
+
+- Logo 由 `logo.png` 更换为 `logo.svg`
+- 字体设置从文本输入框改为下拉菜单选择
+- TXT 渲染逻辑重构:默认保留原文换行,「清理空行」模式压缩连续空行
+- `keepFormat` 设置项重命名为 `cleanEmptyLines`
+
+## [0.1.0] - 2026-06-30
+
+### 新增
+
+- TXT/EPUB/PDF 三格式支持(编码检测、章节解析、PDF canvas 渲染)
+- 书架管理(网格书架、封面缩略图、进度展示、最近阅读历史)
+- 章节跳转、全文搜索、百分比跳转
+- 高度测量自动分页
+- 透明留窗(Stealth)伪装模式、真隐藏、三功能触发器自定义
+- 自动翻页、阅读窗拖拽/缩放、窗口位置记忆
+- 明暗主题、阅读配色、全屏截图取色、排版控制
+- React 18 + TypeScript + Vite 双入口架构
diff --git a/plugins/serious-reading/README.md b/plugins/serious-reading/README.md
new file mode 100644
index 00000000..4536efc3
--- /dev/null
+++ b/plugins/serious-reading/README.md
@@ -0,0 +1,83 @@
+# 严肃阅读
+
+> 一款对待摸鱼阅读很严肃的阅读插件
+支持 TXT / EPUB / PDF 三种格式,专为「在工作间隙低调阅读」设计——悬浮透明阅读窗、老板键伪装隐藏、自动翻页、全屏取色配色,让你严肃地摸鱼。
+
+## 快速开始
+
+
+
+| 命令 | 说明 |
+|------|------|
+| `阅读` / `书架` / `serious` | 打开书架 |
+| `继续阅读` | 继续上次阅读 |
+| `显示阅读器` / `show` | 显示阅读窗 |
+| `切换阅读器` / `toggle` | 切换阅读窗显隐 |
+| 拖入 TXT/EPUB/PDF 文件 | 直接打开阅读 |
+
+## 阅读体验
+
+- **三格式支持** — TXT(自动编码检测,GBK/UTF-8/UTF-16 均可)、EPUB(自动解析章节和封面)、PDF(canvas 渲染)
+- **书架管理** — 网格书架,EPUB 显示封面缩略图,TXT/PDF 显示书名色块;进度百分比一目了然,最近阅读历史快速回到上次的书
+- **章节跳转** — 右键打开章节列表,支持标题搜索过滤,一键跳转
+- **全文搜索** — TXT 全文搜索,关键字上下文高亮,点击结果直接跳转到对应位置
+- **百分比跳转** — 阅读窗右下角输入百分比,精确跳转到全书对应位置
+- **自动分页** — 按实际渲染高度自动分页,调整字号或窗口大小后自动重排
+- **进度记忆** — 自动保存每本书的阅读位置,下次打开恢复到上次位置
+
+## 摸鱼伪装
+
+老板来了怎么办?三种隐藏方式,触发动作全部可自定义:
+
+| 功能 | 默认触发 | 效果 |
+|------|----------|------|
+| 隐身 | Esc / 双击 / 鼠标离开窗口边缘 | 内容透明化,窗口保留,鼠标移回即恢复 |
+| 显示 | 中键 | 恢复内容可见 |
+| 真隐藏 | 右键 | 窗口彻底消失,需命令恢复 |
+
+- 在设置面板中可为三个功能分别绑定触发动作(双击、中键、右键、Esc、鼠标离开/进入边缘),系统自动检测冲突
+- 隐身状态下可配置自动暂停翻页,避免恢复时位置跑偏
+
+## 阅读窗操作
+
+- **拖拽移动** — 鼠标按住窗口中间区域拖动
+- **缩放** — 四边和四角均有缩放把手
+- **窗口记忆** — 自动保存阅读窗位置和尺寸
+- **翻页方式** — 键盘 ←→ / 滚轮 / 点击左右两侧 / PageUp Down / 空格 / 触摸滑动,可任意组合开关
+- **翻页动画** — 无动画 / 滑动两种模式
+
+## 外观定制
+
+- **明暗主题** — 跟随系统 / 手动明亮 / 手动暗黑
+- **阅读配色** — 背景色、文字色自定义,支持**全屏截图取色**(截取屏幕任意区域的颜色作为背景或文字色)
+- **排版控制** — 字号(8-32px)、行高(1.0-3.0)、字重(50-1000)、11 种中英文字体、透明度(10%-100%)、清理空行
+- **实时生效** — 修改设置后自动推送到已打开的阅读窗,无需重新打开
+
+## 开发
+
+```bash
+# 1. 安装前端依赖
+npm install
+
+# 2. 安装 preload 原生依赖(不编译,随源码提交)
+cd preload && npm install && cd ..
+
+# 3. 开发模式(Vite dev server :5173,ZTools 开发者工具以本目录为根加载)
+npm run dev
+
+# 4. 构建产物到 dist/
+npm run build
+```
+
+## 打包发布
+
+```bash
+# 安装 ZTools 插件 CLI
+npm install -g @ztools-center/plugin-cli
+
+# 发布到 ZTools 插件中心
+ztools publish
+```
+为什么做这个插件?
+
+之前一直使用utools的插件摸鱼阅读,但是使用时一直感觉摸鱼阅读限制太多,不能随时移动和调整阅读框大小,于是用AI重新开发了这个插件,感谢摸鱼阅读的开发者。
diff --git a/plugins/serious-reading/index.html b/plugins/serious-reading/index.html
new file mode 100644
index 00000000..62830221
--- /dev/null
+++ b/plugins/serious-reading/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
严肃阅读
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/serious-reading/logo.svg b/plugins/serious-reading/logo.svg
new file mode 100644
index 00000000..4c39fd48
--- /dev/null
+++ b/plugins/serious-reading/logo.svg
@@ -0,0 +1,4 @@
+
+
+ R
+
diff --git a/plugins/serious-reading/package-lock.json b/plugins/serious-reading/package-lock.json
new file mode 100644
index 00000000..04623da8
--- /dev/null
+++ b/plugins/serious-reading/package-lock.json
@@ -0,0 +1,3616 @@
+{
+ "name": "serious-reading-zt",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "serious-reading-zt",
+ "version": "1.0.0",
+ "dependencies": {
+ "@radix-ui/react-checkbox": "^1.1.2",
+ "@radix-ui/react-dialog": "^1.1.2",
+ "@radix-ui/react-dropdown-menu": "^2.1.2",
+ "@radix-ui/react-scroll-area": "^1.2.0",
+ "@radix-ui/react-slider": "^1.2.1",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-switch": "^1.1.1",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.0.4",
+ "dompurify": "^3.4.11",
+ "lucide-react": "^0.453.0",
+ "pdfjs-dist": "^4.6.82",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "tailwind-merge": "^2.5.4"
+ },
+ "devDependencies": {
+ "@types/node": "^22.7.5",
+ "@types/react": "^18.3.11",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.2",
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.4.47",
+ "tailwindcss": "^3.4.14",
+ "typescript": "^5.6.3",
+ "vite": "^5.4.9"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "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",
+ "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-plugin-utils": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "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.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "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-react-jsx-source": {
+ "version": "7.29.7",
+ "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/template": {
+ "version": "7.29.7",
+ "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.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.8",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "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",
+ "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",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/canvas": {
+ "version": "0.1.100",
+ "license": "MIT",
+ "optional": true,
+ "workspaces": [
+ "e2e/*"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas-android-arm64": "0.1.100",
+ "@napi-rs/canvas-darwin-arm64": "0.1.100",
+ "@napi-rs/canvas-darwin-x64": "0.1.100",
+ "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100",
+ "@napi-rs/canvas-linux-arm64-gnu": "0.1.100",
+ "@napi-rs/canvas-linux-arm64-musl": "0.1.100",
+ "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100",
+ "@napi-rs/canvas-linux-x64-gnu": "0.1.100",
+ "@napi-rs/canvas-linux-x64-musl": "0.1.100",
+ "@napi-rs/canvas-win32-arm64-msvc": "0.1.100",
+ "@napi-rs/canvas-win32-x64-msvc": "0.1.100"
+ }
+ },
+ "node_modules/@napi-rs/canvas-android-arm64": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz",
+ "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-arm64": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz",
+ "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-darwin-x64": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz",
+ "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz",
+ "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz",
+ "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-arm64-musl": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz",
+ "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz",
+ "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-gnu": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz",
+ "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-linux-x64-musl": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz",
+ "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
+ "version": "0.1.100",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz",
+ "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@napi-rs/canvas-win32-x64-msvc": {
+ "version": "0.1.100",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.2",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.4",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.10",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.6"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.3.5",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.10",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.3",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.4",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.17",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.10",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-slot": "1.3.0",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.13",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-escape-keydown": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.18",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-menu": "2.1.18",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.4",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.10",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.18",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.13",
+ "@radix-ui/react-focus-guards": "1.1.4",
+ "@radix-ui/react-focus-scope": "1.1.10",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-popper": "1.3.1",
+ "@radix-ui/react-portal": "1.1.12",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-roving-focus": "1.1.13",
+ "@radix-ui/react-slot": "1.3.0",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.3.1",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-rect": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2",
+ "@radix-ui/rect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.12",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.6",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.6",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.13",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-id": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.12",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-presence": "1.1.6",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-callback-ref": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slider": {
+ "version": "1.4.1",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.2",
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-collection": "1.1.10",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-direction": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.3.0",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.3.1",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.3",
+ "@radix-ui/react-context": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.6",
+ "@radix-ui/react-use-controllable-state": "1.2.3",
+ "@radix-ui/react-use-previous": "1.1.2",
+ "@radix-ui/react-use-size": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.3",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.3",
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.3",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.2",
+ "license": "MIT"
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
+ "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
+ "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
+ "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
+ "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
+ "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
+ "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
+ "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
+ "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
+ "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
+ "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
+ "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
+ "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
+ "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
+ "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
+ "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
+ "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
+ "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
+ "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
+ "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
+ "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
+ "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
+ "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.62.2",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.62.2",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.20.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.31",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "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/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.5.2",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.4",
+ "caniuse-lite": "^1.0.30001799",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.38",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.4",
+ "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.38",
+ "caniuse-lite": "^1.0.30001799",
+ "electron-to-chromium": "^1.5.376",
+ "node-releases": "^2.0.48",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001799",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cmdk": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "^1.1.1",
+ "@radix-ui/react-dialog": "^1.1.6",
+ "@radix-ui/react-id": "^1.1.0",
+ "@radix-ui/react-primitive": "^2.0.2"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^18 || ^19 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "license": "MIT"
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dompurify": {
+ "version": "3.4.11",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
+ "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.378",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.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/function-bind": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.453.0",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.15",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.50",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pdfjs-dist": {
+ "version": "4.10.38",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas": "^0.1.65"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.62.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.62.2",
+ "@rollup/rollup-android-arm64": "4.62.2",
+ "@rollup/rollup-darwin-arm64": "4.62.2",
+ "@rollup/rollup-darwin-x64": "4.62.2",
+ "@rollup/rollup-freebsd-arm64": "4.62.2",
+ "@rollup/rollup-freebsd-x64": "4.62.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.2",
+ "@rollup/rollup-linux-arm64-musl": "4.62.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.2",
+ "@rollup/rollup-linux-loong64-musl": "4.62.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-musl": "4.62.2",
+ "@rollup/rollup-openbsd-x64": "4.62.2",
+ "@rollup/rollup-openharmony-arm64": "4.62.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.2",
+ "@rollup/rollup-win32-x64-gnu": "4.62.2",
+ "@rollup/rollup-win32-x64-msvc": "4.62.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "2.6.1",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "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/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "license": "0BSD"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "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/use-callback-ref": {
+ "version": "1.3.3",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/plugins/serious-reading/package.json b/plugins/serious-reading/package.json
new file mode 100644
index 00000000..ee494718
--- /dev/null
+++ b/plugins/serious-reading/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "serious-reading-zt",
+ "version": "1.1.0",
+ "description": "严肃阅读 - ZTools 插件,支持 TXT/EPUB/PDF,透明留窗伪装模式",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc --noEmit && vite build",
+ "typecheck": "tsc --noEmit",
+ "publish": "ztools publish"
+ },
+ "dependencies": {
+ "@radix-ui/react-checkbox": "^1.1.2",
+ "@radix-ui/react-dialog": "^1.1.2",
+ "@radix-ui/react-dropdown-menu": "^2.1.2",
+ "@radix-ui/react-scroll-area": "^1.2.0",
+ "@radix-ui/react-slider": "^1.2.1",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-switch": "^1.1.1",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.0.4",
+ "dompurify": "^3.4.11",
+ "lucide-react": "^0.453.0",
+ "pdfjs-dist": "^4.6.82",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "tailwind-merge": "^2.5.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "devDependencies": {
+ "@types/node": "^22.7.5",
+ "@types/react": "^18.3.11",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.2",
+ "autoprefixer": "^10.4.20",
+ "postcss": "^8.4.47",
+ "tailwindcss": "^3.4.14",
+ "typescript": "^5.6.3",
+ "vite": "^5.4.9"
+ }
+}
diff --git a/plugins/serious-reading/plugin.json b/plugins/serious-reading/plugin.json
new file mode 100644
index 00000000..0de42e6b
--- /dev/null
+++ b/plugins/serious-reading/plugin.json
@@ -0,0 +1,49 @@
+{
+ "name": "serious-reading",
+ "title": "严肃阅读",
+ "description": "一款对待摸鱼阅读很严肃的阅读插件",
+ "version": "1.1.0",
+ "main": "dist/index.html",
+ "logo": "logo.svg",
+ "preload": "preload/main.js",
+ "pluginSetting": {
+ "single": true,
+ "height": 660
+ },
+ "features": [
+ {
+ "code": "reader",
+ "explain": "打开阅读器书架",
+ "cmds": ["阅读", "书架", "serious"]
+ },
+ {
+ "code": "reader_continue",
+ "explain": "继续上次阅读",
+ "cmds": ["继续阅读", "阅读 继续"]
+ },
+ {
+ "code": "show_reader",
+ "explain": "显示阅读窗",
+ "cmds": ["显示阅读器", "show"]
+ },
+ {
+ "code": "toggle_reader",
+ "explain": "切换阅读窗显隐",
+ "cmds": ["切换阅读器", "toggle"]
+ },
+ {
+ "code": "reader_open",
+ "explain": "打开书籍文件",
+ "cmds": [
+ {
+ "type": "files",
+ "label": "用严肃阅读打开",
+ "fileType": "file",
+ "extensions": ["txt", "epub", "pdf"],
+ "minLength": 1,
+ "maxLength": 1
+ }
+ ]
+ }
+ ]
+}
diff --git a/plugins/serious-reading/postcss.config.js b/plugins/serious-reading/postcss.config.js
new file mode 100644
index 00000000..e99ebc2c
--- /dev/null
+++ b/plugins/serious-reading/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/preload/main.js b/plugins/serious-reading/preload/main.js
new file mode 100644
index 00000000..094bfd8c
--- /dev/null
+++ b/plugins/serious-reading/preload/main.js
@@ -0,0 +1,349 @@
+/**
+ * 主窗 preload —— CommonJS,不参与编译打包。
+ * 职责:文件读取/解码、EPUB 解析(含封面)、PDF 取 ArrayBuffer、
+ * 创建并持有悬浮阅读窗 BrowserWindow、转发 IPC(真隐藏等)。
+ */
+const fs = require('fs')
+const path = require('path')
+const { ipcRenderer } = require('electron')
+const DB_PREFIX = 'serious_reading/'
+const BOOKS_DOC_ID = DB_PREFIX + 'books'
+
+// 第三方依赖放 preload/node_modules,原样提交,不压缩
+const iconv = require('./node_modules/iconv-lite')
+const AdmZip = require('./node_modules/adm-zip')
+let jschardet = null
+try {
+ jschardet = require('./node_modules/jschardet')
+} catch (e) {
+ /* jschardet 缺失时退化为 BOM+GBK */
+}
+
+/* ---------------- 编码检测 + 解码 ---------------- */
+
+function detectByBom(buf) {
+ if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) return 'utf-8'
+ if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) return 'utf-16le'
+ if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) return 'utf-16be'
+ return null
+}
+
+function detectEncoding(buf) {
+ const bom = detectByBom(buf)
+ if (bom) return bom
+ if (jschardet && buf.length > 0) {
+ try {
+ const sample = buf.length > 2500 ? buf.subarray(0, 2500) : buf
+ const r = jschardet.detect(Buffer.from(sample))
+ if (r && r.confidence > 0.5 && r.encoding) return r.encoding.toLowerCase()
+ } catch (e) {}
+ }
+ // 无 BOM 的中文小说大概率 GBK(GB18030 为 GBK 超集,iconv 的 gbk 解码器兼容)
+ return 'gbk'
+}
+
+function decodeBuffer(buf) {
+ const enc = detectEncoding(buf)
+ try {
+ return iconv.decode(buf, enc)
+ } catch (e) {
+ return iconv.decode(buf, 'utf-8')
+ }
+}
+
+/* ---------------- 暴露给渲染进程的服务 ---------------- */
+
+window.services = {
+ _readerWin: null,
+
+ readTxt(filePath) {
+ try {
+ const buf = fs.readFileSync(filePath)
+ let s = decodeBuffer(buf)
+ if (s.charCodeAt(0) === 0xfeff) s = s.substring(1)
+ return s
+ } catch (e) {
+ return null
+ }
+ },
+
+ readPdf(filePath) {
+ try {
+ const buf = fs.readFileSync(filePath)
+ // 转 ArrayBuffer 交给 pdfjs
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)
+ } catch (e) {
+ return null
+ }
+ },
+
+ readBuffer(filePath) {
+ try {
+ return fs.readFileSync(filePath)
+ } catch (e) {
+ return null
+ }
+ },
+
+ readEpub(filePath) {
+ try {
+ const zip = new AdmZip(filePath)
+ const containerXml = zip.readAsText('META-INF/container.xml')
+ if (!containerXml) return null
+ const opfPathMatch = containerXml.match(/full-path="([^"]+)"/)
+ if (!opfPathMatch) return null
+ const opfPath = opfPathMatch[1]
+ const opfDir = opfPath.includes('/') ? opfPath.substring(0, opfPath.lastIndexOf('/') + 1) : ''
+ const opfXml = zip.readAsText(opfPath)
+ if (!opfXml) return null
+
+ // manifest
+ const manifest = {}
+ const manifestRegex = /
- ]*id="([^"]+)"[^>]*href="([^"]+)"[^>]*media-type="([^"]+)"[^>]*/g
+ let mMatch
+ while ((mMatch = manifestRegex.exec(opfXml)) !== null) {
+ manifest[mMatch[1]] = { href: mMatch[2], type: mMatch[3] }
+ }
+ // spine
+ const spineItems = []
+ const spineRegex = /
]*toc="([^"]+)"/)
+ const toc = []
+ if (ncxIdMatch) {
+ const ncxItem = manifest[ncxIdMatch[1]]
+ if (ncxItem) {
+ const ncxXml = zip.readAsText(opfDir + ncxItem.href)
+ if (ncxXml) {
+ const navRegex = /]*playOrder="(\d+)"[^>]*>[\s\S]*?([^<]+)<\/text>[\s\S]*?]*name="cover"[^>]*content="([^"]+)"/)
+ if (coverMeta && manifest[coverMeta[1]]) {
+ try {
+ cover = zip.readFile(opfDir + manifest[coverMeta[1]].href)
+ } catch (e) {}
+ }
+
+ const chapters = []
+ spineItems.forEach(function (id, idx) {
+ const item = manifest[id]
+ if (!item || !item.type.match(/html|xhtml/)) return
+ const html = zip.readAsText(opfDir + item.href)
+ if (!html) return
+ let title = ''
+ const tocItem = toc.find((t) => t.src && t.src.includes(item.href))
+ if (tocItem) title = tocItem.title
+ if (!title) title = '第 ' + (idx + 1) + ' 章'
+ chapters.push({ title: title, content: html, index: idx })
+ })
+
+ const titleMatch = opfXml.match(/]*>([^<]+)<\/dc:title>/)
+ return {
+ title: titleMatch ? titleMatch[1] : extractName(filePath),
+ filePath: filePath,
+ format: 'epub',
+ chapters: chapters,
+ totalChapters: chapters.length,
+ cover: cover,
+ }
+ } catch (e) {
+ return null
+ }
+ },
+
+ showOpenDialog(options) {
+ return window.ztools.showOpenDialog(options)
+ },
+
+ /* ---------------- 悬浮阅读窗管理 ---------------- */
+
+ createReaderWindow(state) {
+ window.services._readerState = state
+ if (window.services._readerWin && !window.services._readerWin.isDestroyed()) {
+ try {
+ window.services._readerWin.show()
+ window.services._readerWin.focus()
+ window.services._readerWin.webContents.send('sr:reading-state', state)
+ } catch (e) {}
+ return window.services._readerWin
+ }
+ const settings = state.settings || {}
+ const saved = window.ztools.dbStorage.getItem('serious_reading/winpos') || {}
+ const w = saved.width || settings.window?.width || 520
+ const h = saved.height || settings.window?.height || 780
+ const x = saved.x != null ? saved.x : window.screenLeft + 90
+ const y = saved.y != null ? saved.y : window.screenTop + 180
+
+ const readerUrl = 'dist/reader.html'
+
+ const readerPreloadPath = path.join(__dirname, 'reader.js')
+
+ const win = window.ztools.createBrowserWindow(readerUrl, {
+ width: w,
+ height: h,
+ x: x,
+ y: y,
+ title: '',
+ transparent: true,
+ frame: false,
+ alwaysOnTop: true,
+ resizable: true,
+ backgroundColor: 'rgba(255,255,255,0.01)',
+ skipTaskbar: true,
+ hasShadow: false,
+ thickFrame: false,
+ roundedCorners: false,
+ movable: true,
+ minimizable: false,
+ maximizable: false,
+ closeable: true,
+ webPreferences: { preload: readerPreloadPath },
+ }, function () {
+ if (!win) return
+ win.webContents.send('sr:reading-state', state)
+ try { win.setAlwaysOnTop(true, 'screen-saver') } catch (e) {}
+ })
+ if (!win) { console.warn('[SR] createReaderWindow returned null (readerUrl=' + readerUrl + ')'); return null }
+
+ // 保存窗口位置/尺寸
+ let saveTimer = null
+ const scheduleSave = function () {
+ clearTimeout(saveTimer)
+ saveTimer = setTimeout(function () {
+ if (!win || win.isDestroyed()) { try { console.log('[SR] win destroyed, skip save') } catch(e) {}; return }
+ try {
+ const p = win.getPosition()
+ const s = win.getSize()
+ if (s[0] > 120 && s[1] > 120 && s[0] < 8000 && s[1] < 8000) {
+ window.ztools.dbStorage.setItem('serious_reading/winpos', { x: p[0], y: p[1], width: s[0], height: s[1] })
+ }
+ } catch (e) {}
+ }, 300)
+ }
+ try { win.on('move', scheduleSave) } catch (e) {}
+ try { win.on('resize', scheduleSave) } catch (e) {}
+ try { win.on('resized', scheduleSave) } catch (e) {}
+ try { win.on('moved', scheduleSave) } catch (e) {}
+
+ window.services._readerWin = win
+ return win
+ },
+
+ sendToReader(channel, data) {
+ const win = window.services._readerWin
+ if (win && !win.isDestroyed()) {
+ try { win.webContents.send(channel, data) } catch (e) {}
+ }
+ },
+
+ showReader() {
+ const win = window.services._readerWin
+ if (win && !win.isDestroyed()) {
+ if (!win.isVisible()) win.show()
+ win.focus()
+ window.services.sendToReader('sr:show-reader')
+ return true
+ }
+ return false
+ },
+
+ toggleReader() {
+ const win = window.services._readerWin
+ if (win && !win.isDestroyed()) {
+ if (win.isVisible()) win.hide()
+ else { win.show(); win.focus(); window.services.sendToReader('sr:show-reader') }
+ return true
+ }
+ return false
+ },
+}
+
+window._ipcRenderer = ipcRenderer
+
+// 阅读窗 → 主窗:保存进度 + 更新书架 lastChapter
+ipcRenderer.on('sr:save-progress', function (e, pg) {
+ if (!pg || !pg.filePath) return
+ // 保存 ReadingProgress
+ try { window.ztools.dbStorage.setItem(DB_PREFIX + 'progress/' + pg.filePath, pg) } catch (e2) {}
+ // 更新书架书籍的 lastChapter
+ try {
+ const doc = window.ztools.db.get(BOOKS_DOC_ID)
+ if (doc && Array.isArray(doc.data)) {
+ const idx = doc.data.findIndex(function (b) { return b.path === pg.filePath })
+ if (idx >= 0) {
+ doc.data[idx].lastChapter = pg.chapterIndex
+ doc.data[idx].progress = pg.charOffset
+ doc.data[idx].lastRead = Date.now()
+ if (pg.totalChapters != null) doc.data[idx].totalChapters = pg.totalChapters
+ window.ztools.db.put(doc)
+ }
+ }
+ } catch (e2) {}
+ // 通知渲染进程刷新书架
+ try { window.dispatchEvent(new CustomEvent('sr:shelf-changed')) } catch (e3) {}
+})
+
+// 阅读窗 → 主窗:真隐藏
+ipcRenderer.on('sr:hide-reader', function () {
+ const win = window.services._readerWin
+ if (win && !win.isDestroyed()) { try { win.hide() } catch (e) {} }
+})
+
+// 阅读窗 → 主窗:保存窗口位置/尺寸
+ipcRenderer.on('sr:save-bounds', function (e, data) {
+ if (data && data.x != null && data.width > 0) {
+ try { window.ztools.dbStorage.setItem('serious_reading/winpos', data) } catch (e2) {}
+ }
+})
+
+// 阅读窗 → 主窗:纯 JS 窗口移动 / 缩放(不依赖 -webkit-app-region:drag)
+let _winStartBounds = null
+ipcRenderer.on('sr:win-start', function () {
+ const win = window.services._readerWin
+ if (win && !win.isDestroyed()) { try { _winStartBounds = win.getBounds() } catch (e) {} }
+})
+ipcRenderer.on('sr:win-delta', function (e, data) {
+ const win = window.services._readerWin
+ if (!win || win.isDestroyed() || !_winStartBounds) return
+ const { type, dx, dy } = data || {}
+ if (!type) return
+ const b = _winStartBounds
+ let { x, y, width, height } = b
+ if (type === 'move') {
+ x = b.x + dx
+ y = b.y + dy
+ } else {
+ if (type.includes('e')) width = b.width + dx
+ if (type.includes('w')) { width = b.width - dx; x = b.x + dx }
+ if (type.includes('s')) height = b.height + dy
+ if (type.includes('n')) { height = b.height - dy; y = b.y + dy }
+ const MINW = 100, MINH = 50
+ if (width < MINW) { if (type.includes('w')) x = b.x + b.width - MINW; width = MINW }
+ if (height < MINH) { if (type.includes('n')) y = b.y + b.height - MINH; height = MINH }
+ }
+ try { win.setBounds({ x, y, width, height }) } catch (e2) {}
+})
+ipcRenderer.on('sr:win-end', function () {
+ _winStartBounds = null
+ const win = window.services._readerWin
+ if (win && !win.isDestroyed()) {
+ try {
+ const b = win.getBounds()
+ window.ztools.dbStorage.setItem('serious_reading/winpos', { x: b.x, y: b.y, width: b.width, height: b.height })
+ } catch (e) {}
+ }
+})
+
+function extractName(p) {
+ return (p.split(/[\\/]/).pop() || p).replace(/\.[^.]+$/, '')
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/preload/package-lock.json b/plugins/serious-reading/preload/package-lock.json
new file mode 100644
index 00000000..5e81be52
--- /dev/null
+++ b/plugins/serious-reading/preload/package-lock.json
@@ -0,0 +1,57 @@
+{
+ "name": "serious-reading-preload-deps",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "serious-reading-preload-deps",
+ "version": "1.0.0",
+ "dependencies": {
+ "adm-zip": "^0.5.17",
+ "iconv-lite": "^0.7.2",
+ "jschardet": "^3.1.4"
+ }
+ },
+ "node_modules/adm-zip": {
+ "version": "0.5.18",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz",
+ "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/jschardet": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.4.tgz",
+ "integrity": "sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==",
+ "license": "LGPL-2.1+",
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "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"
+ }
+ }
+}
diff --git a/plugins/serious-reading/preload/package.json b/plugins/serious-reading/preload/package.json
new file mode 100644
index 00000000..68851ced
--- /dev/null
+++ b/plugins/serious-reading/preload/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "serious-reading-preload-deps",
+ "version": "1.0.0",
+ "private": true,
+ "description": "阅读窗/主窗 preload 所需原生依赖(不编译,不压缩,随插件源码提交)",
+ "dependencies": {
+ "adm-zip": "^0.5.17",
+ "iconv-lite": "^0.7.2",
+ "jschardet": "^3.1.4"
+ }
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/preload/reader.js b/plugins/serious-reading/preload/reader.js
new file mode 100644
index 00000000..3f30df01
--- /dev/null
+++ b/plugins/serious-reading/preload/reader.js
@@ -0,0 +1,139 @@
+/**
+ * 阅读窗 preload —— CommonJS,不参与编译打包。
+ * 职责:为阅读窗提供文件读取/解码能力(阅读窗自闭环,无需回主窗读章节),
+ * 暴露 ipcRenderer 用于接收主窗推送的状态、回主窗请求真隐藏/保存进度。
+ */
+const fs = require('fs')
+const { ipcRenderer } = require('electron')
+const iconv = require('./node_modules/iconv-lite')
+const AdmZip = require('./node_modules/adm-zip')
+let jschardet = null
+try { jschardet = require('./node_modules/jschardet') } catch (e) {}
+
+function detectByBom(buf) {
+ if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) return 'utf-8'
+ if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) return 'utf-16le'
+ if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) return 'utf-16be'
+ return null
+}
+
+function detectEncoding(buf) {
+ const bom = detectByBom(buf)
+ if (bom) return bom
+ if (jschardet && buf.length > 0) {
+ try {
+ const sample = buf.length > 2500 ? buf.subarray(0, 2500) : buf
+ const r = jschardet.detect(Buffer.from(sample))
+ if (r && r.confidence > 0.5 && r.encoding) return r.encoding.toLowerCase()
+ } catch (e) {}
+ }
+ return 'gbk'
+}
+
+function decodeBuffer(buf) {
+ try { return iconv.decode(buf, detectEncoding(buf)) } catch (e) { return iconv.decode(buf, 'utf-8') }
+}
+
+window.services = {
+ readTxt(filePath) {
+ try {
+ const buf = fs.readFileSync(filePath)
+ let s = decodeBuffer(buf)
+ if (s.charCodeAt(0) === 0xfeff) s = s.substring(1)
+ return s
+ } catch (e) { return null }
+ },
+
+ readPdf(filePath) {
+ try {
+ const buf = fs.readFileSync(filePath)
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)
+ } catch (e) { return null }
+ },
+
+ readEpub(filePath) {
+ try {
+ const zip = new AdmZip(filePath)
+ const containerXml = zip.readAsText('META-INF/container.xml')
+ if (!containerXml) return null
+ const opfPathMatch = containerXml.match(/full-path="([^"]+)"/)
+ if (!opfPathMatch) return null
+ const opfPath = opfPathMatch[1]
+ const opfDir = opfPath.includes('/') ? opfPath.substring(0, opfPath.lastIndexOf('/') + 1) : ''
+ const opfXml = zip.readAsText(opfPath)
+ if (!opfXml) return null
+ const manifest = {}
+ const manifestRegex = /- ]*id="([^"]+)"[^>]*href="([^"]+)"[^>]*media-type="([^"]+)"[^>]*/g
+ let mMatch
+ while ((mMatch = manifestRegex.exec(opfXml)) !== null) manifest[mMatch[1]] = { href: mMatch[2], type: mMatch[3] }
+ const spineItems = []
+ const spineRegex = /
]*toc="([^"]+)"/)
+ const toc = []
+ if (ncxIdMatch) {
+ const ncxItem = manifest[ncxIdMatch[1]]
+ if (ncxItem) {
+ const ncxXml = zip.readAsText(opfDir + ncxItem.href)
+ if (ncxXml) {
+ const navRegex = /]*playOrder="(\d+)"[^>]*>[\s\S]*?([^<]+)<\/text>[\s\S]*?]*>([^<]+)<\/dc:title>/)
+ return {
+ title: titleMatch ? titleMatch[1] : extractName(filePath),
+ filePath: filePath, format: 'epub', chapters: chapters, totalChapters: chapters.length,
+ }
+ } catch (e) { return null }
+ },
+}
+
+function extractName(p) {
+ return (p.split(/[\\/]/).pop() || p).replace(/\.[^.]+$/, '')
+}
+
+window._ipcRenderer = ipcRenderer
+
+// 捕获主窗发来的 reply 句柄,用于阅读窗 → 主窗回传 IPC
+let _replyToParent = null
+const _origOn = ipcRenderer.on.bind(ipcRenderer)
+ipcRenderer.on = function (channel, handler) {
+ _origOn(channel, function (event) {
+ if (typeof event.reply === 'function') {
+ _replyToParent = event.reply.bind(event)
+ }
+ handler.apply(null, arguments)
+ })
+}
+
+// 阅读窗 → 主窗:优先用 event.reply,回退 sendTo(parentId)/send
+window._sendToParent = function (channel, data) {
+ try {
+ if (_replyToParent) {
+ _replyToParent(channel, data)
+ } else if (window.ztools && window.ztools.sendToParent) {
+ window.ztools.sendToParent(channel, data)
+ } else {
+ ipcRenderer.send(channel, data)
+ }
+ } catch (e) {}
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/reader.html b/plugins/serious-reading/reader.html
new file mode 100644
index 00000000..ecedcca9
--- /dev/null
+++ b/plugins/serious-reading/reader.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/serious-reading/src/components/ui/button.tsx b/plugins/serious-reading/src/components/ui/button.tsx
new file mode 100644
index 00000000..0cff78c7
--- /dev/null
+++ b/plugins/serious-reading/src/components/ui/button.tsx
@@ -0,0 +1,42 @@
+import * as React from 'react'
+import { Slot } from '@radix-ui/react-slot'
+import { cva, type VariantProps } from 'class-variance-authority'
+import { cn } from '@/lib/utils'
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-9 px-4 py-2',
+ sm: 'h-8 rounded-md px-3 text-xs',
+ lg: 'h-10 rounded-md px-8',
+ icon: 'h-9 w-9',
+ },
+ },
+ defaultVariants: { variant: 'default', size: 'default' },
+ },
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+export const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button'
+ return
+ },
+)
+Button.displayName = 'Button'
+export { buttonVariants }
\ No newline at end of file
diff --git a/plugins/serious-reading/src/components/ui/checkbox.tsx b/plugins/serious-reading/src/components/ui/checkbox.tsx
new file mode 100644
index 00000000..f3a65e0b
--- /dev/null
+++ b/plugins/serious-reading/src/components/ui/checkbox.tsx
@@ -0,0 +1,23 @@
+import * as React from 'react'
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
+import { Check } from 'lucide-react'
+import { cn } from '@/lib/utils'
+
+export const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+))
+Checkbox.displayName = 'Checkbox'
\ No newline at end of file
diff --git a/plugins/serious-reading/src/components/ui/command.tsx b/plugins/serious-reading/src/components/ui/command.tsx
new file mode 100644
index 00000000..7b2e112a
--- /dev/null
+++ b/plugins/serious-reading/src/components/ui/command.tsx
@@ -0,0 +1,72 @@
+import * as React from 'react'
+import { Command as CommandPrimitive } from 'cmdk'
+import { Search } from 'lucide-react'
+import { cn } from '@/lib/utils'
+
+export const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Command.displayName = 'Command'
+
+export const CommandInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+CommandInput.displayName = 'CommandInput'
+
+export const CommandList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+CommandList.displayName = 'CommandList'
+
+export const CommandEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => )
+CommandEmpty.displayName = 'CommandEmpty'
+
+export const CommandGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+CommandGroup.displayName = 'CommandGroup'
+
+export const CommandItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+CommandItem.displayName = 'CommandItem'
\ No newline at end of file
diff --git a/plugins/serious-reading/src/components/ui/dialog.tsx b/plugins/serious-reading/src/components/ui/dialog.tsx
new file mode 100644
index 00000000..7c7e7900
--- /dev/null
+++ b/plugins/serious-reading/src/components/ui/dialog.tsx
@@ -0,0 +1,66 @@
+import * as React from 'react'
+import * as DialogPrimitive from '@radix-ui/react-dialog'
+import { X } from 'lucide-react'
+import { cn } from '@/lib/utils'
+
+export const Dialog = DialogPrimitive.Root
+export const DialogTrigger = DialogPrimitive.Trigger
+export const DialogClose = DialogPrimitive.Close
+export const DialogPortal = DialogPrimitive.Portal
+
+export const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = 'DialogOverlay'
+
+export const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ 关闭
+
+
+
+))
+DialogContent.displayName = 'DialogContent'
+
+export const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
+
+)
+export const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
+
+)
+export const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = 'DialogTitle'
+export const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = 'DialogDescription'
\ No newline at end of file
diff --git a/plugins/serious-reading/src/components/ui/dropdown-menu.tsx b/plugins/serious-reading/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 00000000..b13359c8
--- /dev/null
+++ b/plugins/serious-reading/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,46 @@
+import * as React from 'react'
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
+import { cn } from '@/lib/utils'
+
+export const DropdownMenu = DropdownMenuPrimitive.Root
+export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+export const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = 'DropdownMenuContent'
+
+export const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuItem.displayName = 'DropdownMenuItem'
+export const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = 'DropdownMenuSeparator'
\ No newline at end of file
diff --git a/plugins/serious-reading/src/components/ui/input.tsx b/plugins/serious-reading/src/components/ui/input.tsx
new file mode 100644
index 00000000..4c375091
--- /dev/null
+++ b/plugins/serious-reading/src/components/ui/input.tsx
@@ -0,0 +1,19 @@
+import * as React from 'react'
+import { cn } from '@/lib/utils'
+
+export interface InputProps extends React.InputHTMLAttributes {}
+
+export const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => (
+
+ ),
+)
+Input.displayName = 'Input'
\ No newline at end of file
diff --git a/plugins/serious-reading/src/components/ui/scroll-area.tsx b/plugins/serious-reading/src/components/ui/scroll-area.tsx
new file mode 100644
index 00000000..621cabb3
--- /dev/null
+++ b/plugins/serious-reading/src/components/ui/scroll-area.tsx
@@ -0,0 +1,30 @@
+import * as React from 'react'
+import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
+import { cn } from '@/lib/utils'
+
+export const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+
+
+))
+ScrollArea.displayName = 'ScrollArea'
+
+export const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = 'vertical', ...props }, ref) => (
+
+
+
+))
+ScrollBar.displayName = 'ScrollBar'
\ No newline at end of file
diff --git a/plugins/serious-reading/src/components/ui/slider.tsx b/plugins/serious-reading/src/components/ui/slider.tsx
new file mode 100644
index 00000000..2ce6d7a4
--- /dev/null
+++ b/plugins/serious-reading/src/components/ui/slider.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react'
+import * as SliderPrimitive from '@radix-ui/react-slider'
+import { cn } from '@/lib/utils'
+
+export const Slider = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+
+))
+Slider.displayName = 'Slider'
\ No newline at end of file
diff --git a/plugins/serious-reading/src/components/ui/switch.tsx b/plugins/serious-reading/src/components/ui/switch.tsx
new file mode 100644
index 00000000..eb018331
--- /dev/null
+++ b/plugins/serious-reading/src/components/ui/switch.tsx
@@ -0,0 +1,20 @@
+import * as React from 'react'
+import * as SwitchPrimitives from '@radix-ui/react-switch'
+import { cn } from '@/lib/utils'
+
+export const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = 'Switch'
\ No newline at end of file
diff --git a/plugins/serious-reading/src/lib/utils.ts b/plugins/serious-reading/src/lib/utils.ts
new file mode 100644
index 00000000..e7351594
--- /dev/null
+++ b/plugins/serious-reading/src/lib/utils.ts
@@ -0,0 +1,11 @@
+import { type ClassValue, clsx } from 'clsx'
+import { twMerge } from 'tailwind-merge'
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
+
+export function fileExt(p: string): string {
+ const m = p.match(/\.([^.]+)$/)
+ return m ? m[1].toLowerCase() : ''
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/main/App.tsx b/plugins/serious-reading/src/main/App.tsx
new file mode 100644
index 00000000..1594ca66
--- /dev/null
+++ b/plugins/serious-reading/src/main/App.tsx
@@ -0,0 +1,274 @@
+import { useEffect, useRef, useState } from 'react'
+import { Plus, Settings as SettingsIcon, BookOpen } from 'lucide-react'
+import type { BookFormat, ParsedBook, ShelfBook, Settings, RecentBook } from '@/shared/types'
+import { getSettings, saveSettings, getShelf, addBookToShelf, removeBookFromShelf, updateBookInShelf, coverId, getCover, saveCover, bufferToDataUrl, getProgress, addRecentBook, getHistory } from '@/shared/storage'
+import { parseTxt, buildEpub, buildPdf, searchChapters } from '@/shared/parser'
+import { useTheme } from './theme'
+import { Button } from '@/components/ui/button'
+import { ScrollArea } from '@/components/ui/scroll-area'
+import { BookCard } from './components/BookCard'
+import { SettingsDialog } from './components/SettingsDialog'
+import { ChapterDialog } from './components/ChapterDialog'
+import { SearchDialog } from './components/SearchDialog'
+
+export default function App() {
+ const [settings, setSettings] = useState(() => getSettings())
+ const [shelf, setShelf] = useState([])
+ const [covers, setCovers] = useState>({})
+ const [recents, setRecents] = useState([])
+ // 缓存已解析的书籍(内存),供跳转/搜索复用
+ const parsedRef = useRef>({})
+ const [activeBook, setActiveBook] = useState(null)
+ const [openChapter, setOpenChapter] = useState(null)
+ const [openSearch, setOpenSearch] = useState(null)
+ const [settingsOpen, setSettingsOpen] = useState(false)
+ const { setTheme } = useTheme(settings, setSettings)
+
+ useEffect(() => {
+ setShelf(getShelf())
+ setRecents(getHistory())
+ refreshCovers()
+ bindFeatures()
+ const onShelfChange = () => { setShelf(getShelf()); refreshCovers() }
+ window.addEventListener('sr:shelf-changed', onShelfChange)
+ return () => window.removeEventListener('sr:shelf-changed', onShelfChange)
+ }, [])
+
+ function refreshCovers() {
+ const next: Record = {}
+ for (const b of getShelf()) {
+ if (b.cover) {
+ const buf = getCover(b.id)
+ if (buf) next[b.id] = bufferToDataUrl(buf)
+ }
+ }
+ setCovers(next)
+ }
+
+ function bindFeatures() {
+ const zt = window.ztools
+ if (!zt) return
+ zt.onPluginEnter((p) => {
+ setShelf(getShelf())
+ refreshCovers()
+ const code = p.code
+ if (code === 'reader_open' && p.type === 'files' && p.payload?.length) {
+ openFile(p.payload[0].path)
+ return
+ }
+ if (code === 'reader_continue') {
+ const h = getHistory()
+ if (h.length) openFile(h[0].filePath, h[0].format as BookFormat)
+ return
+ }
+ if (code === 'show_reader') {
+ window.services?.showReader()
+ zt.hideMainWindow?.(false)
+ return
+ }
+ if (code === 'toggle_reader') {
+ const ok = window.services?.toggleReader()
+ if (ok) zt.hideMainWindow?.(false)
+ else openContinue()
+ return
+ }
+ if (code === 'reader') {
+ zt.setExpendHeight?.(660)
+ }
+ })
+ }
+
+ function openContinue() {
+ const h = getHistory()
+ if (h.length) openFile(h[0].filePath, h[0].format as BookFormat)
+ }
+
+ /** 解析文件 → 入书架 → 创建悬浮阅读窗 */
+ function openFile(filePath: string, fmt?: BookFormat) {
+ const ext = (filePath.split('.').pop() || '').toLowerCase() as BookFormat
+ const format = fmt ?? (['txt', 'epub', 'pdf'].includes(ext) ? ext : 'txt')
+ let book: ParsedBook | null = null
+ let epubData: EBook | null = null
+ const svc = window.services
+ try {
+ if (format === 'txt') {
+ const txt = svc?.readTxt(filePath)
+ if (txt) book = parseTxt(txt, filePath)
+ } else if (format === 'epub') {
+ epubData = svc?.readEpub(filePath) ?? null
+ if (epubData) book = buildEpub(epubData, filePath)
+ } else if (format === 'pdf') {
+ const buf = svc?.readPdf(filePath)
+ if (buf) book = buildPdf(filePath, 0) // PDF 总页数由阅读窗 pdfjs 获取
+ }
+ } catch (e) {}
+ if (!book) {
+ ztShow('无法打开: ' + filePath)
+ return
+ }
+ parsedRef.current[filePath] = book
+ const id = Date.now().toString()
+ const sb: ShelfBook = { id, type: format, name: book.title, path: filePath, totalChapters: book.totalChapters, progress: 0, lastRead: Date.now() }
+ if (!addBookToShelf(sb)) {
+ // 已存在,复用原 id,并更新 totalChapters(旧数据可能缺失)
+ const exist = getShelf().find((b) => b.path === filePath)
+ if (exist) {
+ sb.id = exist.id
+ if (book.totalChapters && exist.totalChapters !== book.totalChapters) {
+ updateBookInShelf(exist.id, { totalChapters: book.totalChapters })
+ }
+ }
+ } else if (format === 'epub' && epubData?.cover) {
+ saveCover(sb.id, epubData.cover)
+ refreshCovers()
+ }
+ setShelf(getShelf())
+ addRecentBook({ filePath, title: book.title, format, lastRead: Date.now() })
+ setRecents(getHistory())
+ setActiveBook(sb)
+ launchReader(sb, book)
+ }
+
+ function launchReader(sb: ShelfBook, book: ParsedBook, skipTo?: { chapterIndex: number; charOffset?: number }) {
+ const prog = skipTo ? null : getProgress(sb.path)
+ const state: any = {
+ filePath: sb.path,
+ format: sb.type,
+ settings: getSettings(),
+ chapterIndex: skipTo?.chapterIndex ?? prog?.chapterIndex ?? 0,
+ pageIndex: skipTo ? 0 : (prog?.pageIndex ?? 0),
+ charOffset: skipTo?.charOffset ?? prog?.charOffset,
+ pdfPage: skipTo?.chapterIndex ?? prog?.chapterIndex,
+ }
+ zt_hide()
+ window.services?.createReaderWindow(state)
+ }
+
+ function zt_hide() {
+ window.ztools?.hideMainWindow?.(false)
+ }
+ function ztShow(msg: string) {
+ window.ztools?.showNotification?.(msg)
+ }
+
+ return (
+
+
+
+
+ 严肃阅读
+
+
+
pickFile()}>
+ 打开文件
+
+
setSettingsOpen(true)} title="设置">
+
+
+
+
+
+
+ {shelf.length === 0 ? (
+
+ ) : (
+
+ {shelf.map((b) => (
+
openBook(b)}
+ onChapter={() => setOpenChapter(b)}
+ onSearch={() => setOpenSearch(b)}
+ onDelete={() => {
+ removeBookFromShelf(b.id)
+ setShelf(getShelf())
+ }}
+ />
+ ))}
+
+
+
+
+ )}
+
+ {recents.length > 0 && shelf.length > 0 && (
+
+
最近阅读
+
+ {recents.slice(0, 8).map((r) => (
+ openFile(r.filePath, r.format)}
+ className="flex w-full items-center justify-between rounded-md px-3 py-2 text-sm hover:bg-accent"
+ >
+ {r.title}
+ {new Date(r.lastRead).toLocaleDateString()}
+
+ ))}
+
+
+ )}
+
+
+
+ {openChapter && (
+
{
+ jumpTo(openChapter, idx, offset)
+ setOpenChapter(null)
+ }}
+ />
+ )}
+ {openSearch && parsedRef.current[openSearch.path] && (
+ {
+ jumpTo(openSearch, idx, offset)
+ setOpenSearch(null)
+ }}
+ />
+ )}
+
+ )
+
+ function openBook(b: ShelfBook) {
+ if (parsedRef.current[b.path]) {
+ launchReader(b, parsedRef.current[b.path])
+ } else {
+ openFile(b.path, b.type)
+ }
+ }
+
+ function jumpTo(b: ShelfBook, chapterIndex: number, charOffset?: number) {
+ updateBookInShelf(b.id, { lastChapter: chapterIndex, progress: charOffset })
+ b.lastChapter = chapterIndex
+ b.progress = charOffset
+ const book = parsedRef.current[b.path]
+ if (book) launchReader(b, book, { chapterIndex, charOffset })
+ }
+
+ function pickFile() {
+ const files = window.services?.showOpenDialog?.({
+ title: '选择阅读文件',
+ filters: [{ name: '支持的格式', extensions: ['txt', 'epub', 'pdf'] }],
+ properties: ['openFile'],
+ })
+ if (files?.length) openFile(files[0])
+ }
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/main/components/BookCard.tsx b/plugins/serious-reading/src/main/components/BookCard.tsx
new file mode 100644
index 00000000..c71c19e4
--- /dev/null
+++ b/plugins/serious-reading/src/main/components/BookCard.tsx
@@ -0,0 +1,59 @@
+import { useState } from 'react'
+import { MoreVertical, BookMarked, Search, Trash2 } from 'lucide-react'
+import type { ShelfBook } from '@/shared/types'
+import {
+ DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator,
+} from '@/components/ui/dropdown-menu'
+
+export function BookCard(props: {
+ book: ShelfBook
+ cover?: string
+ progress: number
+ onOpen: () => void
+ onChapter: () => void
+ onSearch: () => void
+ onDelete: () => void
+}) {
+ const [menu, setMenu] = useState(false)
+ return (
+
+
{ e.preventDefault(); props.onOpen() }}
+ >
+ {props.cover ? (
+
+ ) : (
+ {props.book.name}
+ )}
+
+ {props.book.lastChapter != null && (
+
+ 第{props.book.totalChapters ? `${props.book.lastChapter + 1}/${props.book.totalChapters}` : `${props.book.lastChapter + 1}`}章
+
+ )}
+ {props.progress > 0 && (
+ {Math.min(100, props.progress)}%
+ )}
+
+
+
{props.book.name}
+
+
+
+
+
+
+
+
+ 章节跳转
+ 搜索跳转
+
+ 删除
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/main/components/ChapterDialog.tsx b/plugins/serious-reading/src/main/components/ChapterDialog.tsx
new file mode 100644
index 00000000..35826079
--- /dev/null
+++ b/plugins/serious-reading/src/main/components/ChapterDialog.tsx
@@ -0,0 +1,50 @@
+import { useState, useMemo } from 'react'
+import type { ShelfBook, ParsedBook, Settings } from '@/shared/types'
+import { searchChapters } from '@/shared/parser'
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import { Input } from '@/components/ui/input'
+import { Button } from '@/components/ui/button'
+import { Search } from 'lucide-react'
+
+export function ChapterDialog(props: {
+ book: ShelfBook
+ parsed: ParsedBook | undefined
+ settings: Settings
+ onSkip: (chapterIndex: number, charOffset?: number) => void
+}) {
+ const [kw, setKw] = useState('')
+ const [committed, setCommitted] = useState('')
+ const list = useMemo(() => {
+ if (!props.parsed) return []
+ if (!committed) return props.parsed.chapters
+ return searchChapters(props.parsed.chapters, committed)
+ }, [props.parsed, committed])
+
+ const curIdx = props.book.lastChapter ?? 0
+
+ return (
+ props.onSkip(curIdx)}>
+
+
+ 章节跳转 · {props.book.name}
+
+
+ setKw(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') setCommitted(kw) }} />
+ setCommitted(kw)}>
+
+
+ {list.length === 0 &&
无匹配章节
}
+ {list.map((ch) => (
+
props.onSkip(ch.index, ch.charOffset)}
+ >
+ {ch.title}
+
+ ))}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/main/components/SearchDialog.tsx b/plugins/serious-reading/src/main/components/SearchDialog.tsx
new file mode 100644
index 00000000..910bbfed
--- /dev/null
+++ b/plugins/serious-reading/src/main/components/SearchDialog.tsx
@@ -0,0 +1,80 @@
+import { useState } from 'react'
+import type { ShelfBook, ParsedBook, Settings } from '@/shared/types'
+import { searchFullText } from '@/shared/parser'
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import { Input } from '@/components/ui/input'
+import { Button } from '@/components/ui/button'
+import { Search } from 'lucide-react'
+
+export function SearchDialog(props: {
+ book: ShelfBook
+ parsed: ParsedBook
+ settings: Settings
+ onSkip: (chapterIndex: number, charOffset: number) => void
+}) {
+ const [kw, setKw] = useState('')
+ const [items, setItems] = useState<{ index: number; snippet: string }[]>([])
+ const [hasMore, setHasMore] = useState(false)
+ const [from, setFrom] = useState(0)
+
+ const fullText = props.parsed.fullText
+ if (!fullText) {
+ return (
+ props.onSkip(props.book.lastChapter ?? 0, props.book.progress ?? 0)}>
+
+ 搜索跳转
+ PDF 暂不支持全文搜索,仅支持按页跳转。
+
+
+ )
+ }
+
+ function doSearch(reset: boolean) {
+ const start = reset ? 0 : from
+ const { results, hasMore: hm } = searchFullText(fullText!, kw, 10, start)
+ setItems(reset ? results : [...items, ...results])
+ setHasMore(hm)
+ setFrom(start + results.length + 1)
+ }
+
+ /** 由字符 offset 定位所属章节 + 页内偏移 */
+ function locate(offset: number) {
+ const chapters = props.parsed.chapters
+ let chapter = 0
+ for (let i = 0; i < chapters.length; i++) {
+ const nextOffset = chapters[i + 1] ? (chapters[i + 1].charOffset ?? Infinity) : Infinity
+ if ((chapters[i].charOffset ?? 0) <= offset && offset < nextOffset) {
+ chapter = i
+ break
+ }
+ }
+ props.onSkip(chapter, offset)
+ }
+
+ return (
+ props.onSkip(props.book.lastChapter ?? 0, props.book.progress ?? 0)}>
+
+
+ 搜索跳转 · {props.book.name}
+
+
+ setKw(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') doSearch(true) }} />
+ doSearch(true)}>
+
+
+ {items.map((it) => (
+ locate(it.index)}
+ className="block w-full rounded-md bg-muted/50 px-3 py-2 text-left text-xs hover:bg-accent"
+ dangerouslySetInnerHTML={{ __html: it.snippet }}
+ />
+ ))}
+ {hasMore && (
+ doSearch(false)}>加载更多
+ )}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/main/components/SettingsDialog.tsx b/plugins/serious-reading/src/main/components/SettingsDialog.tsx
new file mode 100644
index 00000000..2b152e75
--- /dev/null
+++ b/plugins/serious-reading/src/main/components/SettingsDialog.tsx
@@ -0,0 +1,293 @@
+import { useState, useEffect, useRef } from 'react'
+import type { Settings, TriggerKey, HideActions } from '@/shared/types'
+import { TRIGGER_OPTIONS, detectConflicts, DEFAULT_SETTINGS, FONT_OPTIONS } from '@/shared/constants'
+import { saveSettings } from '@/shared/storage'
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu'
+import { Slider } from '@/components/ui/slider'
+import { Switch } from '@/components/ui/switch'
+import { Checkbox } from '@/components/ui/checkbox'
+
+const PAGE_KEYS: { key: 'arrow' | 'wheel' | 'click' | 'pgupdn' | 'space' | 'touch'; label: string }[] = [
+ { key: 'arrow', label: '键盘 ←→' },
+ { key: 'wheel', label: '滚轮' },
+ { key: 'click', label: '点击左右' },
+ { key: 'pgupdn', label: 'PageUp/Down' },
+ { key: 'space', label: '空格' },
+ { key: 'touch', label: '触摸滑动' },
+]
+
+export function SettingsDialog(props: {
+ open: boolean
+ onOpenChange: (v: boolean) => void
+ settings: Settings
+ setSettings: (s: Settings) => void
+ setTheme: (t: Settings['theme']) => void
+}) {
+ const [draft, setDraft] = useState(props.settings)
+ const conflicts = detectConflicts(draft.hide)
+ const conflictCount = Object.values(conflicts).filter(Boolean).length
+
+ useEffect(() => { setDraft(props.settings) }, [props.settings, props.open])
+
+ function patch(p: Partial) { setDraft({ ...draft, ...p }) }
+ function patchReader(p: Partial) { setDraft({ ...draft, reader: { ...draft.reader, ...p } }) }
+ function patchPage(p: Partial) { setDraft({ ...draft, page: { ...draft.page, ...p } }) }
+ function patchHide(p: Partial) { setDraft({ ...draft, hide: { ...draft.hide, ...p } }) }
+
+ function toggleTrigger(group: keyof HideActions, key: TriggerKey) {
+ const arr = draft.hide[group]
+ patchHide({ [group]: arr.includes(key) ? arr.filter((k) => k !== key) : [...arr, key] } as any)
+ }
+
+ function save() {
+ if (conflictCount > 0) return
+ saveSettings(draft)
+ props.setSettings(draft)
+ // 推送给已打开的阅读窗,实时生效(字号/行高/透明度/触发器等)
+ ;(window as any).services?.sendToReader?.('sr:settings', draft)
+ props.onOpenChange(false)
+ }
+ function reset() { setDraft(JSON.parse(JSON.stringify(DEFAULT_SETTINGS))) }
+
+ return (
+
+
+
+ 设置
+
+
+
+ {/* 主题 */}
+
+
+ {(['auto', 'light', 'dark'] as const).map((t) => (
+ { patch({ theme: t }); props.setTheme(t) }}>
+ {t === 'auto' ? '跟随系统' : t === 'light' ? '明亮' : '暗黑'}
+
+ ))}
+
+
+
+ {/* 阅读外观 */}
+
+
+ patchReader({ bgColor: v })} />
+ patchReader({ textColor: v })} />
+
+ patchReader({ opacity: v[0] / 100 })} />
+
+
+ patchReader({ fontSize: v[0] })} />
+
+
+ patchReader({ lineHeight: v[0] })} />
+
+
+ patchReader({ fontWeight: v[0] })} />
+
+
+ patchReader({ fontFamily: v })} />
+
+
+ patchReader({ cleanEmptyLines: v })} />
+
+
+
+
+ {/* 窗口 */}
+
+
+ {/* 翻页 */}
+
+
+ {PAGE_KEYS.map((p) => (
+
+ patchPage({ [p.key]: !!v } as any)} />
+ {p.label}
+
+ ))}
+
+
+ 翻页过渡
+ patchPage({ transition: 'none' })}>无动画
+ patchPage({ transition: 'slide' })}>滑动
+
+
+
+ {/* 隐藏动作(三功能 · 冲突检测) */}
+
+ {(['stealthHide', 'stealthShow', 'realHide'] as const).map((g) => (
+
+
+ {g === 'stealthHide' ? '隐身(显→隐)' : g === 'stealthShow' ? '显示(隐→显)' : '真隐藏(彻底消失,命令恢复)'}
+
+
+ {TRIGGER_OPTIONS.map((o) => (
+
+ toggleTrigger(g, o.key)} />
+ {o.label}
+
+ ))}
+
+
+ ))}
+ {conflictCount > 0 && (
+ ⚠ 触发动作冲突:{Object.entries(conflicts).filter(([,v]) => v).map(([k,v]) => `${k}(${v})`).join('、')},每个动作只能绑一个功能。
+ )}
+
+
+ {/* 自动翻页 */}
+
+
+ setDraft({ ...draft, autoPage: { ...draft.autoPage, interval: v[0] } })} />
+
+
+ setDraft({ ...draft, autoPage: { ...draft.autoPage, pauseOnStealth: v } })} />
+ stealth 隐藏时自动暂停
+
+
+
+
+
+ 恢复默认
+ 0}>保存
+
+
+
+ )
+}
+
+function Section({ title, children }: { title: string; children: React.ReactNode }) {
+ return (
+
+ )
+}
+function Field({ label, children }: { label: string; children: React.ReactNode }) {
+ return (
+
+ )
+}
+
+function ColorPicker({ value, onChange }: { value: string; onChange: (v: string) => void }) {
+ const ref = useRef(null)
+ const [hex, setHex] = useState(value)
+ const [picking, setPicking] = useState(false)
+ const [screenImg, setScreenImg] = useState(null)
+ const canvasRef = useRef(null)
+ useEffect(() => { setHex(value) }, [value])
+
+ useEffect(() => {
+ if (!screenImg || !canvasRef.current) return
+ const img = new Image()
+ img.onload = () => {
+ const el = canvasRef.current!
+ el.width = img.width; el.height = img.height
+ el.getContext('2d')?.drawImage(img, 0, 0)
+ }
+ img.src = screenImg
+ }, [screenImg])
+
+ function startPick() {
+ const zt = window.ztools
+ if (!zt?.screenCapture) { ref.current?.click(); return }
+ zt.hideMainWindow?.(true)
+ setTimeout(() => {
+ try {
+ zt.screenCapture!((img) => {
+ zt.showMainWindow?.()
+ if (typeof img === 'string' && img.startsWith('data:')) {
+ setScreenImg(img)
+ setPicking(true)
+ } else {
+ ref.current?.click()
+ }
+ })
+ } catch {
+ zt.showMainWindow?.()
+ ref.current?.click()
+ }
+ }, 300)
+ }
+
+ function handlePickClick(e: React.MouseEvent) {
+ if (!canvasRef.current) return
+ const rect = canvasRef.current.getBoundingClientRect()
+ const sx = canvasRef.current.width / rect.width
+ const sy = canvasRef.current.height / rect.height
+ const x = Math.floor((e.clientX - rect.left) * sx)
+ const y = Math.floor((e.clientY - rect.top) * sy)
+ const ctx = canvasRef.current.getContext('2d')
+ if (!ctx) return
+ const px = ctx.getImageData(x, y, 1, 1).data
+ const c = '#' + [px[0], px[1], px[2]].map((v) => v.toString(16).padStart(2, '0')).join('')
+ onChange(c); setHex(c); setPicking(false); setScreenImg(null)
+ }
+
+ return (
+ <>
+ {picking && screenImg && (
+ { if (e.key === 'Escape') { setPicking(false); setScreenImg(null) } }}>
+
+
点击任意位置取色 · Esc 取消
+
+ )}
+
+
+ 吸
+
+
{ onChange(e.target.value); setHex(e.target.value) }} className="h-9 flex-1 rounded border" />
+
+ >
+ )
+}
+
+function isLight(hex: string): boolean {
+ const r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16)
+ return (r * 299 + g * 587 + b * 114) / 1000 > 128
+}
+
+function FontSelect({ value, onChange }: { value: string; onChange: (v: string) => void }) {
+ const current = FONT_OPTIONS.find((f) => f.value === value)
+ return (
+
+
+
+ {current?.label ?? value}
+ ▾
+
+
+
+ {FONT_OPTIONS.map((f) => (
+ onChange(f.value)} style={{ fontFamily: f.value === 'default' ? 'inherit' : f.value }}>
+ {f.label}
+
+ ))}
+
+
+ )
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/main/main.tsx b/plugins/serious-reading/src/main/main.tsx
new file mode 100644
index 00000000..f137e0ac
--- /dev/null
+++ b/plugins/serious-reading/src/main/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App'
+import '@/styles/globals.css'
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
\ No newline at end of file
diff --git a/plugins/serious-reading/src/main/theme.ts b/plugins/serious-reading/src/main/theme.ts
new file mode 100644
index 00000000..0c698015
--- /dev/null
+++ b/plugins/serious-reading/src/main/theme.ts
@@ -0,0 +1,38 @@
+import { useEffect } from 'react'
+import type { Settings } from '@/shared/types'
+import { getSettings, saveSettings } from '@/shared/storage'
+
+const MEDIA = typeof window !== 'undefined' ? window.matchMedia('(prefers-color-scheme: dark)') : null
+
+export function resolveTheme(theme: Settings['theme']): 'light' | 'dark' {
+ if (theme === 'auto') return MEDIA?.matches ? 'dark' : 'light'
+ return theme
+}
+
+export function useTheme(settings: Settings, setSettings: (s: Settings) => void) {
+ useEffect(() => {
+ const root = document.documentElement
+ root.classList.toggle('dark', resolveTheme(settings.theme) === 'dark')
+ }, [settings.theme])
+
+ useEffect(() => {
+ if (!MEDIA) return
+ const handler = () => {
+ if (settings.theme === 'auto') {
+ document.documentElement.classList.toggle('dark', MEDIA.matches)
+ }
+ }
+ MEDIA.addEventListener('change', handler)
+ return () => MEDIA.removeEventListener('change', handler)
+ }, [settings.theme])
+
+ const setTheme = (theme: Settings['theme']) => {
+ const next = { ...settings, theme }
+ saveSettings(next)
+ setSettings(next)
+ ;(window as any).services?.sendToReader?.('sr:settings', next)
+ }
+ return { setTheme }
+}
+
+export { getSettings, saveSettings }
\ No newline at end of file
diff --git a/plugins/serious-reading/src/reader/App.tsx b/plugins/serious-reading/src/reader/App.tsx
new file mode 100644
index 00000000..0eb1327a
--- /dev/null
+++ b/plugins/serious-reading/src/reader/App.tsx
@@ -0,0 +1,471 @@
+import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
+import type { ParsedBook, Chapter, BookFormat, Settings, ReadingProgress, TriggerKey } from '@/shared/types'
+import { getSettings, getProgress, saveProgress, addRecentBook } from '@/shared/storage'
+import { parseTxt, buildEpub, renderChapterHtml } from '@/shared/parser'
+import { PdfView } from './components/PdfView'
+
+export default function App() {
+ const [settings, setSettings] = useState(() => getSettings())
+ const [book, setBook] = useState(null)
+ const [chapter, setChapter] = useState(null)
+ const [chapterIdx, setChapterIdx] = useState(0)
+ const [pageIndex, setPageIndex] = useState(0)
+ const [charOffset, setCharOffset] = useState(0)
+ const [stealth, setStealth] = useState(false)
+ const [resizeN, setResizeN] = useState(0)
+ // PDF
+ const [pdfData, setPdfData] = useState(null)
+ const [pdfPage, setPdfPage] = useState(1)
+ const [pdfTotal, setPdfTotal] = useState(0)
+ // 分页
+ const [pages, setPages] = useState(['正在加载…
'])
+ const measureRef = useRef(null)
+ const [dragWin, setDragWin] = useState<{ sx: number; sy: number } | null>(null)
+
+ const ipc = (window as any)._ipcRenderer
+ const sendParent = (window as any)._sendToParent as ((c: string, d?: any) => void) | undefined
+
+ /* ---- 接收主窗推送 ---- */
+ useEffect(() => {
+ ipc?.on('sr:reading-state', (_e: unknown, state: any) => loadState(state))
+ ipc?.on('sr:show-reader', () => setStealth(false))
+ ipc?.on('sr:settings', (_e: unknown, s: Settings) => { if (s) setSettings(s) })
+ return () => {}
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ function loadState(state: any) {
+ if (!state?.filePath) return
+ setStealth(false)
+ void loadBook(state.filePath, state.format as BookFormat, state)
+ }
+
+ async function loadBook(filePath: string, format: BookFormat, state?: any) {
+ const svc = window.services
+ let pb: ParsedBook | null = null
+ if (format === 'txt') {
+ const txt = svc?.readTxt(filePath)
+ if (txt) pb = parseTxt(txt, filePath)
+ } else if (format === 'epub') {
+ const eb = svc?.readEpub(filePath)
+ if (eb) pb = buildEpub(eb, filePath)
+ } else if (format === 'pdf') {
+ const buf = svc?.readPdf(filePath)
+ if (buf) {
+ setPdfData(buf)
+ setPdfPage(state?.pdfPage ?? state?.chapterIndex ?? 1)
+ const fake = { title: filePath.split(/[\\/]/).pop()!, filePath, format: 'pdf' as BookFormat, chapters: [], totalChapters: 1 }
+ setBook(fake)
+ addRecentBook({ filePath, title: fake.title, format: 'pdf', lastRead: Date.now() })
+ saveProgress({ filePath, format: 'pdf', chapterIndex: state?.pdfPage ?? 1, pageIndex: 0, timestamp: Date.now() })
+ }
+ return
+ }
+ if (!pb) return
+ setPdfData(null)
+ setBook(pb)
+ addRecentBook({ filePath, title: pb.title, format, lastRead: Date.now() })
+ const prog = getProgress(filePath)
+ const ci = state?.chapterIndex ?? prog?.chapterIndex ?? 0
+ setChapterIdx(ci)
+ setCharOffset(state?.charOffset ?? prog?.charOffset ?? (pb.chapters[ci]?.charOffset ?? 0))
+ setPageIndex(state?.pageIndex ?? prog?.pageIndex ?? 0)
+ }
+
+ const currentChapter = book?.chapters[chapterIdx] ?? null
+ useEffect(() => { setChapter(currentChapter) }, [currentChapter])
+ const chapterHtml = useMemo(
+ () => {
+ if (!book || !chapter) return ''
+ const body = renderChapterHtml(chapter.content, book.format, settings.reader.cleanEmptyLines)
+ if (book.format === 'txt' && chapter.title && chapter.title !== '全文') {
+ const esc = chapter.title.replace(/&/g, '&').replace(//g, '>')
+ return `${esc} ${body}`
+ }
+ return body
+ },
+ [book, chapter, settings.reader.cleanEmptyLines],
+ )
+
+ /* ---- 高度测量分页(txt/epub) ---- */
+ useLayoutEffect(() => {
+ if (!book || book.format === 'pdf' || !chapterHtml) return
+ const measure = measureRef.current
+ if (!measure) return
+ const pageH = window.innerHeight - 16
+ const pageW = window.innerWidth - 24
+ measure.style.width = pageW + 'px'
+ measure.style.fontSize = settings.reader.fontSize + 'px'
+ measure.style.lineHeight = String(settings.reader.lineHeight)
+ measure.style.fontFamily = settings.reader.fontFamily || 'inherit'
+ measure.style.fontWeight = String(settings.reader.fontWeight)
+ measure.innerHTML = chapterHtml
+ const nodes: { height: number; html: string }[] = []
+ const blockTags = ['P', 'DIV', 'BLOCKQUOTE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI', 'PRE', 'TABLE', 'SECTION', 'ARTICLE', 'BODY']
+ function collectFrom(parent: Node) {
+ const children = Array.from(parent.childNodes)
+ for (const node of children) {
+ if (node.nodeType === 3 && node.textContent?.trim()) {
+ const span = document.createElement('span')
+ span.textContent = node.textContent
+ node.parentNode?.replaceChild(span, node)
+ const cs = getComputedStyle(span)
+ nodes.push({ height: span.offsetHeight + parseFloat(cs.marginTop) + parseFloat(cs.marginBottom), html: span.outerHTML })
+ } else if (node.nodeType === 1) {
+ const el = node as HTMLElement
+ const hasBlockChildren = Array.from(el.children).some((child) => blockTags.includes(child.tagName))
+ if (hasBlockChildren) {
+ collectFrom(el)
+ } else {
+ const cs = getComputedStyle(el)
+ nodes.push({ height: el.offsetHeight + parseFloat(cs.marginTop) + parseFloat(cs.marginBottom), html: el.outerHTML })
+ }
+ }
+ }
+ }
+ collectFrom(measure)
+ const result: string[] = []
+ let cur = ''
+ let used = 0
+ for (const n of nodes) {
+ if (used + n.height > pageH && used > 0) { result.push(cur); cur = ''; used = 0 }
+ cur += n.html
+ used += n.height
+ }
+ if (cur) result.push(cur)
+ measure.innerHTML = ''
+ setPages(result.length ? result : ['本章无内容
'])
+ setPageIndex((p) => p === -1 ? result.length - 1 : Math.min(p, result.length - 1))
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [chapterHtml, settings.reader.fontSize, settings.reader.lineHeight, settings.reader.fontWeight, settings.reader.fontFamily, resizeN])
+
+ /* ---- 百分比 ---- */
+ const percent = useMemo(() => {
+ if (!book) return 0
+ if (book.format === 'pdf') return pdfTotal ? (pdfPage / pdfTotal) * 100 : 0
+ if (book.fullText) return ((charOffset / book.fullText.length) * 100)
+ return book.totalChapters ? ((chapterIdx + 1) / book.totalChapters) * 100 : 0
+ }, [book, charOffset, chapterIdx, pdfPage, pdfTotal])
+
+ /* ---- 翻页 ---- */
+ const nextPage = useCallback(() => {
+ if (book?.format === 'pdf') { setPdfPage((p) => Math.min(p + 1, pdfTotal || p + 1)); return }
+ if (pageIndex < pages.length - 1) setPageIndex(pageIndex + 1)
+ else nextChapter()
+ }, [book, pageIndex, pages.length])
+
+ const prevPage = useCallback(() => {
+ if (book?.format === 'pdf') { setPdfPage((p) => Math.max(1, p - 1)); return }
+ if (pageIndex > 0) setPageIndex(pageIndex - 1)
+ else prevChapter()
+ }, [book, pageIndex])
+
+ // 用 ref 持有最新回调/状态,避免 document 事件监听反复挂载
+ const nextPageRef = useRef(nextPage); nextPageRef.current = nextPage
+ const prevPageRef = useRef(prevPage); prevPageRef.current = prevPage
+ const pagesRef = useRef(pages); pagesRef.current = pages
+ const hideRef = useRef(settings.hide); hideRef.current = settings.hide
+ const pageCfgRef = useRef(settings.page); pageCfgRef.current = settings.page
+ const stealthRef = useRef(stealth); stealthRef.current = stealth
+
+ function nextChapter() {
+ if (!book || book.format === 'pdf') return
+ if (chapterIdx < book.totalChapters - 1) {
+ const ni = chapterIdx + 1
+ setPages(['…
'])
+ setChapterIdx(ni)
+ setCharOffset(book.chapters[ni]?.charOffset ?? 0)
+ setPageIndex(0)
+ }
+ }
+ function prevChapter() {
+ if (!book || book.format === 'pdf') return
+ if (chapterIdx > 0) {
+ const pi = chapterIdx - 1
+ setPages(['…
'])
+ setChapterIdx(pi)
+ setCharOffset(book.chapters[pi]?.charOffset ?? 0)
+ setPageIndex(-1)
+ }
+ }
+ function goChapter(idx: number) {
+ if (!book || book.format === 'pdf') return
+ if (idx >= 0 && idx < book.totalChapters) {
+ setChapterIdx(idx)
+ setCharOffset(book.chapters[idx]?.charOffset ?? 0)
+ setPageIndex(0)
+ }
+ }
+
+ /* ---- 进度保存 ---- */
+ useEffect(() => {
+ if (!book) return
+ const t = setTimeout(() => {
+ const pg: ReadingProgress = {
+ filePath: book.filePath, format: book.format,
+ chapterIndex: book.format === 'pdf' ? pdfPage : chapterIdx,
+ pageIndex, charOffset, totalChapters: book.totalChapters, timestamp: Date.now(),
+ }
+ saveProgress(pg)
+ sendParent?.('sr:save-progress', pg)
+ }, 400)
+ return () => clearTimeout(t)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [chapterIdx, pageIndex, charOffset, pdfPage])
+
+ /* ---- document 级事件:阅读窗容器是 drag region,React 事件会被吞掉 ---- */
+ function dispatchTrigger(key: TriggerKey, e?: Event) {
+ const hide = hideRef.current
+ if (hide.realHide.includes(key)) { e?.preventDefault?.(); sendParent?.('sr:hide-reader'); return }
+ const isStealth = stealthRef.current
+ if (!isStealth && hide.stealthHide.includes(key)) { setStealth(true); return }
+ if (isStealth && hide.stealthShow.includes(key)) { setStealth(false); return }
+ }
+ useEffect(() => {
+ window.focus()
+ const onKey = (e: KeyboardEvent) => {
+ const hide = hideRef.current
+ if (e.key === 'Escape' && (hide.stealthHide.includes('escape') || hide.stealthShow.includes('escape') || hide.realHide.includes('escape'))) { dispatchTrigger('escape', e); e.preventDefault(); return }
+ const p = pageCfgRef.current
+ if (p.arrow && e.key === 'ArrowRight') { nextPageRef.current(); e.preventDefault() }
+ if (p.arrow && e.key === 'ArrowLeft') { prevPageRef.current(); e.preventDefault() }
+ if (p.pgupdn && e.key === 'PageDown') { nextPageRef.current(); e.preventDefault() }
+ if (p.pgupdn && e.key === 'PageUp') { prevPageRef.current(); e.preventDefault() }
+ if (p.space && e.key === ' ') { nextPageRef.current(); e.preventDefault() }
+ if (e.key === 'Home') { setPageIndex(0) }
+ if (e.key === 'End') { setPageIndex(pagesRef.current.length - 1) }
+ }
+ const onDbl = (e: MouseEvent) => { dispatchTrigger('dblclick', e) }
+ const onDown = (e: MouseEvent) => { if (e.button === 1) dispatchTrigger('middleClick', e) }
+ const onCtx = (e: MouseEvent) => {
+ const hide = hideRef.current
+ if (hide.realHide.includes('rightClick') || hide.stealthHide.includes('rightClick') || hide.stealthShow.includes('rightClick')) { e.preventDefault(); dispatchTrigger('rightClick', e) }
+ }
+ const onLeave = () => { dispatchTrigger('mouseleave') }
+ const onEnter = () => { dispatchTrigger('mouseenter') }
+ const onWheel = (e: WheelEvent) => {
+ if (pageCfgRef.current.wheel) { e.preventDefault(); if (e.deltaY > 0 || e.deltaX > 0) nextPageRef.current(); else prevPageRef.current() }
+ }
+ let tx = 0, ty = 0
+ const onTouchStart = (e: TouchEvent) => { tx = e.touches[0].clientX; ty = e.touches[0].clientY }
+ const onTouchEnd = (e: TouchEvent) => {
+ if (!pageCfgRef.current.touch) return
+ const dx = e.changedTouches[0].clientX - tx
+ const dy = e.changedTouches[0].clientY - ty
+ if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) { if (dx < 0) nextPageRef.current(); else prevPageRef.current() }
+ }
+ document.addEventListener('keydown', onKey)
+ document.addEventListener('dblclick', onDbl)
+ document.addEventListener('mousedown', onDown)
+ document.addEventListener('contextmenu', onCtx)
+ document.addEventListener('mouseleave', onLeave)
+ document.addEventListener('mouseenter', onEnter)
+ document.addEventListener('wheel', onWheel, { passive: false })
+ document.addEventListener('touchstart', onTouchStart)
+ document.addEventListener('touchend', onTouchEnd)
+ return () => {
+ document.removeEventListener('keydown', onKey)
+ document.removeEventListener('dblclick', onDbl)
+ document.removeEventListener('mousedown', onDown)
+ document.removeEventListener('contextmenu', onCtx)
+ document.removeEventListener('mouseleave', onLeave)
+ document.removeEventListener('mouseenter', onEnter)
+ document.removeEventListener('wheel', onWheel)
+ document.removeEventListener('touchstart', onTouchStart)
+ document.removeEventListener('touchend', onTouchEnd)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ /* ---- 自动翻页 ---- */
+ useEffect(() => {
+ const interval = settings.autoPage.interval
+ if (!interval || interval <= 0) return
+ if (stealth && settings.autoPage.pauseOnStealth) return
+ const t = setInterval(() => nextPage(), interval * 1000)
+ return () => clearInterval(t)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [settings.autoPage, nextPage, stealth])
+
+ /* ---- 中间区域拖动移动窗口 ---- */
+ useEffect(() => {
+ if (!dragWin) return
+ const onMove = (e: MouseEvent) => {
+ sendParent?.('sr:win-delta', { type: 'move', dx: e.screenX - dragWin.sx, dy: e.screenY - dragWin.sy })
+ }
+ const onUp = () => {
+ sendParent?.('sr:win-end')
+ setDragWin(null)
+ }
+ document.addEventListener('mousemove', onMove)
+ document.addEventListener('mouseup', onUp)
+ return () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp) }
+ }, [dragWin, sendParent])
+
+ /* ---- resize 重排 ---- */
+ useEffect(() => {
+ const onRes = () => setResizeN((n) => n + 1)
+ window.addEventListener('resize', onRes)
+ const saveT = setInterval(() => sendBounds(), 5000)
+ window.addEventListener('beforeunload', sendBounds)
+ return () => { window.removeEventListener('resize', onRes); clearInterval(saveT); window.removeEventListener('beforeunload', sendBounds) }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ function sendBounds() {
+ const x = window.screenLeft, y = window.screenTop, w = window.innerWidth, h = window.innerHeight
+ if (w > 120 && h > 120 && w < 8000 && h < 8000) sendParent?.('sr:save-bounds', { x, y, width: w, height: h })
+ }
+
+ /* ---- 百分比跳转 ---- */
+ function jumpPercent(v: number) {
+ if (!book) return
+ if (book.format === 'pdf') { setPdfPage(Math.max(1, Math.ceil((v / 100) * pdfTotal))); return }
+ if (book.fullText) {
+ const target = Math.round((v / 100) * book.fullText.length)
+ let ci = 0
+ for (let i = 0; i < book.chapters.length; i++) {
+ const c = book.chapters[i]
+ if ((c.charOffset ?? 0) <= target) ci = i
+ }
+ setChapterIdx(ci); setCharOffset(target); setPageIndex(0)
+ } else {
+ const ci = Math.min(book.totalChapters - 1, Math.floor((v / 100) * book.totalChapters))
+ goChapter(ci)
+ }
+ }
+
+ if (!book && !pdfData) {
+ return 正在加载…
+ }
+
+ return (
+
+ {/* 纯 JS 窗口移动 + 缩放把手(不使用 drag region,避免吞掉右键/中键) */}
+
+
+ {/* 内容区:整窗无 drag region,右键/中键/双击/滚轮/翻页全部正常 */}
+
e.preventDefault()}
+ >
+ {/* 测量容器 */}
+
+
+ {/* PDF 模式 */}
+ {book?.format === 'pdf' && pdfData ? (
+
+ ) : (
+ <>
+ {/* 分页内容条:绝对定位单页切换 */}
+
+ {pages.map((p, i) => (
+
+ ))}
+
+
+ {/* 翻页点击区(左右各 30%)+ 中间拖动移动区(40%) */}
+ {settings.page.click && !stealth ? (
+ <>
+
+
+
{ if (e.button === 0) { e.preventDefault(); sendParent?.('sr:win-start', { type: 'move' }); setDragWin({ sx: e.screenX, sy: e.screenY }) } }}
+ />
+ >
+ ) : !stealth ? (
+
{ if (e.button === 0) { e.preventDefault(); sendParent?.('sr:win-start', { type: 'move' }); setDragWin({ sx: e.screenX, sy: e.screenY }) } }}
+ />
+ ) : null}
+
+ {/* 进度条 */}
+ {!stealth && (
+
+ )}
+ >
+ )}
+
+
+
+ )
+}
+
+/**
+ * 纯 JS 窗口移动 + 缩放把手。
+ * 不使用 -webkit-app-region:drag(会吞掉右键/中键),改为鼠标按下后通过 IPC
+ * 让主窗调用 win.setPosition / win.setBounds 完成移动与缩放。
+ * 整窗保持 no-drag,右键/中键/双击/滚轮/翻页触发全部正常。
+ */
+function WindowHandles({ sendParent }: { sendParent?: (c: string, d?: any) => void }) {
+ const [drag, setDrag] = useState<{ type: string; sx: number; sy: number } | null>(null)
+
+ useEffect(() => {
+ if (!drag) return
+ const onMove = (e: MouseEvent) => {
+ sendParent?.('sr:win-delta', { type: drag.type, dx: e.screenX - drag.sx, dy: e.screenY - drag.sy })
+ }
+ const onUp = () => {
+ sendParent?.('sr:win-end')
+ setDrag(null)
+ }
+ document.addEventListener('mousemove', onMove)
+ document.addEventListener('mouseup', onUp)
+ return () => {
+ document.removeEventListener('mousemove', onMove)
+ document.removeEventListener('mouseup', onUp)
+ }
+ }, [drag, sendParent])
+
+ const start = (type: string) => (e: React.MouseEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ sendParent?.('sr:win-start', { type })
+ setDrag({ type, sx: e.screenX, sy: e.screenY })
+ }
+
+ const handles = [
+ // 缩放:四边/四角,全部放在窗口内并置于最上层
+ { type: 'w', cls: 'left-0 top-0 bottom-0 w-1.5 z-[60] cursor-ew-resize' },
+ { type: 'e', cls: 'right-0 top-0 bottom-0 w-1.5 z-[60] cursor-ew-resize' },
+ { type: 's', cls: 'left-0 right-0 bottom-0 h-1.5 z-[60] cursor-ns-resize' },
+ { type: 'nw', cls: 'left-0 top-0 w-3 h-3 z-[60] cursor-nw-resize' },
+ { type: 'ne', cls: 'right-0 top-0 w-3 h-3 z-[60] cursor-ne-resize' },
+ { type: 'sw', cls: 'left-0 bottom-0 w-3 h-3 z-[60] cursor-sw-resize' },
+ { type: 'se', cls: 'right-0 bottom-0 w-3 h-3 z-[60] cursor-se-resize' },
+ ]
+
+ return (
+ <>
+ {handles.map((h) => (
+
+ ))}
+ >
+ )
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/reader/components/PdfView.tsx b/plugins/serious-reading/src/reader/components/PdfView.tsx
new file mode 100644
index 00000000..16892be1
--- /dev/null
+++ b/plugins/serious-reading/src/reader/components/PdfView.tsx
@@ -0,0 +1,47 @@
+import { useEffect, useRef, useState } from 'react'
+import * as pdfjsLib from 'pdfjs-dist'
+// Vite 以 url 加载 worker(构建后为独立 asset)
+import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
+pdfjsLib.GlobalWorkerOptions.workerSrc = workerUrl
+
+export function PdfView(props: {
+ data: ArrayBuffer
+ page: number
+ scale: number
+ onPageReady: (totalPages: number) => void
+}) {
+ const canvasRef = useRef
(null)
+ const [total, setTotal] = useState(0)
+
+ useEffect(() => {
+ let cancelled = false
+ ;(async () => {
+ const buf = props.data
+ const loadingTask = pdfjsLib.getDocument({ data: buf })
+ const pdf = await loadingTask.promise
+ if (cancelled) return
+ if (!total) { setTotal(pdf.numPages); props.onPageReady(pdf.numPages) }
+ const num = Math.max(1, Math.min(props.page, pdf.numPages))
+ const page = await pdf.getPage(num)
+ const viewport = page.getViewport({ scale: props.scale || 1.2 })
+ const canvas = canvasRef.current
+ if (!canvas) { if (cancelled) return; return }
+ const ctx = canvas.getContext('2d')!
+ const dpr = window.devicePixelRatio || 1
+ canvas.width = viewport.width * dpr
+ canvas.height = viewport.height * dpr
+ canvas.style.width = viewport.width + 'px'
+ canvas.style.height = viewport.height + 'px'
+ await page.render({ canvasContext: ctx, viewport, transform: dpr !== 1 ? [dpr, 0, 0, dpr, 0, 0] : undefined } as any).promise
+ })().catch((e) => console.error('pdf render', e))
+ return () => { cancelled = true }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [props.data, props.page, props.scale])
+
+ return (
+
+
+ {total > 0 && {props.page}/{total} }
+
+ )
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/reader/main.tsx b/plugins/serious-reading/src/reader/main.tsx
new file mode 100644
index 00000000..f137e0ac
--- /dev/null
+++ b/plugins/serious-reading/src/reader/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App'
+import '@/styles/globals.css'
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
\ No newline at end of file
diff --git a/plugins/serious-reading/src/reader/usePagination.ts b/plugins/serious-reading/src/reader/usePagination.ts
new file mode 100644
index 00000000..70dd7994
--- /dev/null
+++ b/plugins/serious-reading/src/reader/usePagination.ts
@@ -0,0 +1,55 @@
+import { useEffect, useRef, useState } from 'react'
+
+/** 简单的高度测量分页:渲染内容到隐藏容器,按节点 height 累加断页。 */
+export function usePaginate(html: string, fontSize: number, lineHeight: number) {
+ const [pages, setPages] = useState([''])
+ const measureRef = useRef(null)
+
+ useLayoutPaginate(() => {
+ const measure = measureRef.current
+ if (!measure) return
+ const pageH = window.innerHeight - 48
+ const pageW = window.innerWidth - 64
+ measure.style.width = pageW + 'px'
+ measure.style.fontSize = fontSize + 'px'
+ measure.style.lineHeight = String(lineHeight)
+ measure.innerHTML = html
+ // 文本节点包 span 以便测量
+ const nodes: { height: number; html: string }[] = []
+ measure.childNodes.forEach((node) => {
+ if (node.nodeType === 3 && node.textContent?.trim()) {
+ const span = document.createElement('span')
+ span.textContent = node.textContent
+ measure.replaceChild(span, node)
+ nodes.push({ height: span.offsetHeight, html: span.outerHTML })
+ } else if (node.nodeType === 1) {
+ const el = node as HTMLElement
+ nodes.push({ height: el.offsetHeight, html: el.outerHTML })
+ }
+ })
+ const result: string[] = []
+ let cur = ''
+ let used = 0
+ for (const n of nodes) {
+ if (used + n.height > pageH && used > 0) {
+ result.push(cur)
+ cur = ''
+ used = 0
+ }
+ cur += n.html
+ used += n.height
+ }
+ if (cur) result.push(cur)
+ measure.innerHTML = ''
+ setPages(result.length ? result : [''])
+ }, [html, fontSize, lineHeight, window.innerHeight, window.innerWidth])
+
+ return { pages, measureRef }
+}
+
+import { useLayoutEffect } from 'react'
+function useLayoutPaginate(fn: () => void, deps: any[]) {
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ useLayoutEffect(fn, deps)
+ // debounce re-paginate on resize handled by signal in deps (window size)
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/shared/constants.ts b/plugins/serious-reading/src/shared/constants.ts
new file mode 100644
index 00000000..651a5c94
--- /dev/null
+++ b/plugins/serious-reading/src/shared/constants.ts
@@ -0,0 +1,65 @@
+import type { Settings, TriggerKey } from './types'
+
+/** 存储键前缀 */
+export const DB_PREFIX = 'serious_reading/'
+
+/** 触发动作的可选项(用于设置面板勾选) */
+export const TRIGGER_OPTIONS: { key: TriggerKey; label: string }[] = [
+ { key: 'dblclick', label: '双击' },
+ { key: 'middleClick', label: '中键' },
+ { key: 'rightClick', label: '右键' },
+ { key: 'escape', label: 'Esc' },
+ { key: 'mouseleave', label: '鼠标离开边缘' },
+ { key: 'mouseenter', label: '鼠标进入边缘' },
+]
+
+/** 三功能之间的冲突判定:同一触发键不能被多个功能同时启用 */
+export function detectConflicts(hide: Settings['hide']): Record {
+ const assigned: Record = {}
+ for (const k of hide.stealthHide) assigned[k] = assigned[k] ? assigned[k] + '/隐身' : '隐身'
+ for (const k of hide.stealthShow) assigned[k] = assigned[k] ? assigned[k] + '/显示' : '显示'
+ for (const k of hide.realHide) assigned[k] = assigned[k] ? assigned[k] + '/真隐藏' : '真隐藏'
+ const out = {} as Record
+ for (const k of Object.keys(assigned) as TriggerKey[]) {
+ out[k] = assigned[k].includes('/') ? assigned[k] : null
+ }
+ return out
+}
+
+export const DEFAULT_SETTINGS: Settings = {
+ theme: 'auto',
+ window: { width: 520, height: 780, x: -1, y: -1 },
+ reader: {
+ bgColor: '#f5f5f5',
+ textColor: '#1a1a1a',
+ opacity: 1,
+ fontSize: 17,
+ lineHeight: 1.85,
+ fontFamily: 'default',
+ fontWeight: 400,
+ cleanEmptyLines: false,
+ },
+ page: { arrow: true, wheel: true, click: true, pgupdn: true, space: false, touch: true, transition: 'none' },
+ hide: {
+ stealthHide: ['escape', 'dblclick', 'mouseleave'],
+ stealthShow: ['middleClick'],
+ realHide: ['rightClick'],
+ },
+ autoPage: { interval: 0, pauseOnStealth: true },
+}
+
+export const SUPPORTED_EXTS = ['txt', 'epub', 'pdf']
+
+export const FONT_OPTIONS: { value: string; label: string }[] = [
+ { value: 'default', label: '默认' },
+ { value: "'Microsoft YaHei', '微软雅黑', sans-serif", label: '微软雅黑' },
+ { value: "'SimSun', '宋体', serif", label: '宋体' },
+ { value: "'SimHei', '黑体', sans-serif", label: '黑体' },
+ { value: "'KaiTi', '楷体', serif", label: '楷体' },
+ { value: "'FangSong', '仿宋', serif", label: '仿宋' },
+ { value: "'PingFang SC', '苹方', sans-serif", label: '苹方' },
+ { value: "'Source Han Sans SC', '思源黑体', sans-serif", label: '思源黑体' },
+ { value: "'Source Han Serif SC', '思源宋体', serif", label: '思源宋体' },
+ { value: "Georgia, 'Times New Roman', serif", label: 'Georgia' },
+ { value: "'Courier New', monospace", label: 'Courier New' },
+]
\ No newline at end of file
diff --git a/plugins/serious-reading/src/shared/ipc.ts b/plugins/serious-reading/src/shared/ipc.ts
new file mode 100644
index 00000000..ecbc0eae
--- /dev/null
+++ b/plugins/serious-reading/src/shared/ipc.ts
@@ -0,0 +1,31 @@
+/**
+ * 父(主窗)↔ 子(阅读窗)之间的 IPC 通道。
+ * 阅读窗的 BrowserWindow proxy 由主窗 preload 持有,真隐藏等操作需经 IPC 回主窗;
+ * 其余阅读交互在阅读窗内自闭环,不经过 IPC。
+ */
+export const IPC = {
+ /** 阅读窗 → 主窗:通知主窗隐藏阅读窗(真隐藏 win.hide()) */
+ HIDE_READER: 'sr:hide-reader',
+ /** 阅读窗 → 主窗:保存阅读窗的位置/尺寸 */
+ SAVE_BOUNDS: 'sr:save-bounds',
+ /** 阅读窗 → 主窗:阅读进度变化(用于主窗刷新书架进度展示) */
+ PROGRESS: 'sr:progress',
+ /** 主窗 → 阅读窗:推送新的阅读状态(打开新文件/切章) */
+ READING_STATE: 'sr:reading-state',
+ /** 主窗 → 阅读窗:请求显示阅读窗(命令恢复) */
+ SHOW_READER: 'sr:show-reader',
+} as const
+
+export type IpcChannel = (typeof IPC)[keyof typeof IPC]
+
+/** 通过 reading-state 通道下发给阅读窗的状态 */
+export interface ReadingState {
+ filePath: string
+ format: 'txt' | 'epub' | 'pdf'
+ settings?: any
+ chapterIndex?: number
+ pageIndex?: number
+ charOffset?: number
+ /** PDF 页码 */
+ pdfPage?: number
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/shared/parser.ts b/plugins/serious-reading/src/shared/parser.ts
new file mode 100644
index 00000000..41de354c
--- /dev/null
+++ b/plugins/serious-reading/src/shared/parser.ts
@@ -0,0 +1,155 @@
+import DOMPurify from 'dompurify'
+import type { ParsedBook, Chapter, BookFormat } from './types'
+
+/** 中文小说章节标题正则(沿用 Serious-Reading 验证过的模式) */
+const CHAPTER_RE = /^(第[零一二三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟\d]+[章节回卷集部篇](?![的得了地]))\s*(.*)/gm
+
+function extractTitle(filePath: string): string {
+ const name = filePath.replace(/\\/g, '/').split('/').pop() ?? filePath
+ return name.replace(/\.[^.]+$/, '')
+}
+
+/**
+ * 解析 TXT:通过 preload 解码后切章。
+ * 同时计算每章在全文字符流中的偏移,供百分比跳转。
+ */
+export function parseTxt(content: string, filePath: string): ParsedBook {
+ if (content.charCodeAt(0) === 0xfeff) content = content.substring(1)
+ const chapters: Chapter[] = []
+ let lastChapterEnd = 0
+ let match: RegExpExecArray | null
+ CHAPTER_RE.lastIndex = 0
+ while ((match = CHAPTER_RE.exec(content)) !== null) {
+ if (chapters.length === 0 && match.index > 0) {
+ const pre = content.substring(0, match.index).trim()
+ if (pre.length > 0) chapters.push({ title: '开篇', index: 0, content: pre, charOffset: 0 })
+ } else if (chapters.length > 0) {
+ chapters[chapters.length - 1].content = content
+ .substring(lastChapterEnd, match.index)
+ .trim()
+ }
+ chapters.push({ title: match[0].trim(), index: chapters.length, content: '', charOffset: match.index })
+ lastChapterEnd = match.index + match[0].length
+ if (chapters.length > 5000) break
+ }
+ if (chapters.length > 0) {
+ chapters[chapters.length - 1].content = content.substring(lastChapterEnd).trim()
+ } else {
+ chapters.push({ title: '全文', index: 0, content: content.trim(), charOffset: 0 })
+ }
+ // 计算 charLength
+ let acc = 0
+ for (const ch of chapters) {
+ ch.charOffset = ch.charOffset ?? acc
+ ch.charLength = ch.content.length
+ acc = (ch.charOffset ?? 0) + ch.charLength
+ }
+ return { title: extractTitle(filePath), filePath, format: 'txt' as BookFormat, chapters, totalChapters: chapters.length, fullText: content }
+}
+
+/** EPUB:preload 已通过 adm-zip 提取,直接接收 */
+export function buildEpub(ebook: EBook, filePath: string): ParsedBook {
+ return {
+ title: ebook.title || extractTitle(filePath),
+ filePath,
+ format: 'epub',
+ chapters: ebook.chapters.map((c, i) => ({ title: c.title, index: i, content: c.content })),
+ totalChapters: ebook.chapters.length,
+ }
+}
+
+/** PDF:仅返回元信息,真正渲染由阅读窗用 pdfjs 处理 */
+export function buildPdf(filePath: string, totalPdfPages: number): ParsedBook {
+ return {
+ title: extractTitle(filePath),
+ filePath,
+ format: 'pdf',
+ chapters: [{ title: 'PDF', index: 0, content: '' }],
+ totalChapters: 1,
+ totalPdfPages,
+ }
+}
+
+/** 全文搜索:返回关键字上下文片段(含高亮),按 offset 排序 */
+export function searchFullText(
+ fullText: string,
+ keyword: string,
+ limit = 30,
+ from = 0,
+): { results: { index: number; snippet: string }[]; hasMore: boolean } {
+ if (!keyword || keyword.length < 2) return { results: [], hasMore: false }
+ const out: { index: number; snippet: string }[] = []
+ let idx = from
+ const ctx = 17
+ while (out.length < limit) {
+ idx = fullText.indexOf(keyword, idx + 1)
+ if (idx === -1) break
+ const begin = Math.max(0, idx - ctx)
+ const end = Math.min(fullText.length, idx + keyword.length + ctx)
+ const seg = fullText.substring(begin, end).replace(/\s/g, '')
+ const snippet = seg.replace(keyword, `${keyword} `)
+ out.push({ index: idx, snippet })
+ }
+ const hasMore = idx !== -1
+ return { results: out, hasMore }
+}
+
+/** 章节标题搜索 */
+export function searchChapters(chapters: Chapter[], keyword: string) {
+ const kw = keyword.toLowerCase()
+ return chapters.filter((c) => c.title.toLowerCase().includes(kw))
+}
+
+/** 渲染章节正文为安全 HTML(使用 DOMPurify 清洗 EPUB 的 XSS 载荷) */
+export function renderChapterHtml(content: string, format: BookFormat, cleanEmptyLines = false): string {
+ if (format === 'epub') {
+ const clean = DOMPurify.sanitize(content, {
+ FORBID_TAGS: ['img', 'a', 'style', 'script', 'iframe', 'object', 'embed', 'link', 'meta', 'form', 'input'],
+ FORBID_ATTR: ['src', 'href', 'srcset'],
+ })
+ return clean.replace(/]*>/g, '
')
+ }
+ // TXT:按段落包
,首行缩进两字,段间留白
+ if (!cleanEmptyLines) {
+ // 默认:保留原文换行,每个 \n 产生一个
,空行变为间隔行
+ const lines = content.split(/\n/)
+ return lines.map((line) => {
+ const t = escapeHtml(line.trimEnd())
+ if (!t.trim()) return '
'
+ const startsWithQuote = /^[“"].*["”]/.test(t)
+ const indent = startsWithQuote ? '0' : '2em'
+ return `${t}
`
+ }).join('')
+ }
+ // 清理空行:每行独立成段,连续空行压缩为1个间距(段落间保留一个空行,去除多余空行)
+ const rawLines = content.split(/\n/)
+ const paragraphs: string[] = []
+ let prevEmpty = false
+ for (const line of rawLines) {
+ if (line.trim() === '') {
+ if (paragraphs.length > 0 && !prevEmpty) paragraphs.push('')
+ prevEmpty = true
+ } else {
+ prevEmpty = false
+ paragraphs.push(line.trim())
+ }
+ }
+ // 去除首尾空行
+ while (paragraphs.length && paragraphs[0] === '') paragraphs.shift()
+ while (paragraphs.length && paragraphs[paragraphs.length - 1] === '') paragraphs.pop()
+ return paragraphs.map((p) => {
+ if (!p) return '
'
+ const t = escapeHtml(p)
+ const startsWithQuote = /^[“"].*["”]/.test(t)
+ const indent = startsWithQuote ? '0' : '2em'
+ return `${t}
`
+ }).join('')
+}
+
+function escapeHtml(s: string): string {
+ return s
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/shared/storage.ts b/plugins/serious-reading/src/shared/storage.ts
new file mode 100644
index 00000000..9b6f6520
--- /dev/null
+++ b/plugins/serious-reading/src/shared/storage.ts
@@ -0,0 +1,131 @@
+import type {
+ Settings,
+ ReadingProgress,
+ RecentBook,
+ ShelfBook,
+} from './types'
+import { DB_PREFIX, DEFAULT_SETTINGS } from './constants'
+
+/** 安全获取 ztools 全局(开发期可能未注入) */
+function zt() {
+ return typeof window !== 'undefined' ? window.ztools : undefined
+}
+
+const SET_KEY = DB_PREFIX + 'settings'
+const HIST_KEY = DB_PREFIX + 'history'
+const WINPOS_KEY = DB_PREFIX + 'winpos'
+const BOOKS_DOC_ID = DB_PREFIX + 'books'
+
+/* ---------------- KV(dbStorage) ---------------- */
+
+export function getSettings(): Settings {
+ const z = zt()
+ const raw = z ? z.dbStorage.getItem(SET_KEY) : null
+ if (!raw) return JSON.parse(JSON.stringify(DEFAULT_SETTINGS))
+ // 合并默认值,避免新增字段缺失
+ return { ...DEFAULT_SETTINGS, ...raw, reader: { ...DEFAULT_SETTINGS.reader, ...raw.reader } }
+}
+
+export function saveSettings(s: Settings) {
+ zt()?.dbStorage.setItem(SET_KEY, s)
+}
+
+export function getWindowPos(): { x: number; y: number; width: number; height: number } | null {
+ return zt()?.dbStorage.getItem(WINPOS_KEY) ?? null
+}
+
+export function saveWindowPos(b: { x: number; y: number; width: number; height: number }) {
+ zt()?.dbStorage.setItem(WINPOS_KEY, b)
+}
+
+export function getProgress(filePath: string): ReadingProgress | null {
+ return zt()?.dbStorage.getItem(DB_PREFIX + 'progress/' + filePath) ?? null
+}
+
+export function saveProgress(p: ReadingProgress) {
+ zt()?.dbStorage.setItem(DB_PREFIX + 'progress/' + p.filePath, p)
+}
+
+export function getHistory(): RecentBook[] {
+ return zt()?.dbStorage.getItem(HIST_KEY) ?? []
+}
+
+export function addRecentBook(b: RecentBook) {
+ const list = getHistory()
+ const i = list.findIndex((x) => x.filePath === b.filePath)
+ if (i >= 0) list.splice(i, 1)
+ list.unshift(b)
+ zt()?.dbStorage.setItem(HIST_KEY, list.slice(0, 20))
+}
+
+/* ---------------- 文档(db) ---------------- */
+
+/** 书架列表:单文档,data 为 ShelfBook 数组 */
+export function getShelf(): ShelfBook[] {
+ const z = zt()
+ if (!z) return []
+ const doc = z.db.get(BOOKS_DOC_ID)
+ return (doc?.data as ShelfBook[]) ?? []
+}
+
+export function saveShelf(list: ShelfBook[]) {
+ const z = zt()
+ if (!z) return
+ const existing = z.db.get(BOOKS_DOC_ID)
+ const doc = existing ?? { _id: BOOKS_DOC_ID }
+ doc.data = list
+ z.db.put(doc)
+}
+
+export function addBookToShelf(book: ShelfBook): boolean {
+ const list = getShelf()
+ if (list.some((b) => b.path === book.path)) return false
+ list.push(book)
+ saveShelf(list)
+ return true
+}
+
+export function removeBookFromShelf(id: string) {
+ const list = getShelf().filter((b) => b.id !== id)
+ saveShelf(list)
+}
+
+export function updateBookInShelf(id: string, patch: Partial) {
+ const list = getShelf().map((b) => (b.id === id ? { ...b, ...patch } : b))
+ saveShelf(list)
+}
+
+/** 封面附件 id(按 nativeId 隔离) */
+export function coverId(bookId: string): string {
+ const z = zt()
+ const native = z ? z.getNativeId() : 'dev'
+ return `${native}/${bookId}/cover`
+}
+
+export function saveCover(bookId: string, buf: ArrayBuffer | Buffer | Uint8Array, mime = 'image/png') {
+ // 运行期 Buffer 经结构化克隆到渲染进程后通常为 Uint8Array,postAttachment 兼容
+ zt()?.db.postAttachment(coverId(bookId), buf as any, mime)
+}
+
+export function getCover(bookId: string): Buffer | null {
+ return zt()?.db.getAttachment(coverId(bookId)) ?? null
+}
+
+export function removeCover(bookId: string) {
+ try {
+ zt()?.db.remove(coverId(bookId))
+ } catch {
+ /* 封面可能未存,忽略 */
+ }
+}
+
+/** Buffer 转 data URL(用于 显示) */
+export function bufferToDataUrl(buf: Buffer | ArrayBuffer, mime = 'image/png'): string {
+ const bytes = buf instanceof ArrayBuffer ? new Uint8Array(buf) : new Uint8Array(buf)
+ let bin = ''
+ const chunk = 0x8000
+ for (let i = 0; i < bytes.length; i += chunk) {
+ bin += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + chunk)) as any)
+ }
+ return `data:${mime};base64,${btoa(bin)}`
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/shared/types.ts b/plugins/serious-reading/src/shared/types.ts
new file mode 100644
index 00000000..08ff1613
--- /dev/null
+++ b/plugins/serious-reading/src/shared/types.ts
@@ -0,0 +1,137 @@
+/** 支持的书籍格式 */
+export type BookFormat = 'txt' | 'epub' | 'pdf'
+
+/** 单本书的书架元数据记录 */
+export interface ShelfBook {
+ id: string
+ type: BookFormat
+ name: string
+ /** TXT/EPUB/PDF 的本地绝对路径;EPUB 解析后内容会缓存为附件,这里仍留源路径以便重新解析 */
+ path: string
+ /** 封面附件 id(EPUB 有,其它为空) */
+ cover?: string
+ /** 章节总数(用于章节跳转显示) */
+ totalChapters?: number
+ /** 上次阅读的章节索引(章节模型进度) */
+ lastChapter?: number
+ /** 上次阅读的字符/页偏移(用于恢复进度) */
+ progress?: number
+ lastRead?: number
+}
+
+/** 全文章节(解析后内存模型,不入库) */
+export interface Chapter {
+ title: string
+ index: number
+ content: string
+ /** 章节在全文字符流中的起始偏移(用于百分比跳转) */
+ charOffset?: number
+ /** 章节在全文字符流中的长度 */
+ charLength?: number
+}
+
+/** 解析后的内存书籍对象 */
+export interface ParsedBook {
+ title: string
+ filePath: string
+ format: BookFormat
+ chapters: Chapter[]
+ totalChapters: number
+ /** 全文拼接字符串(TXT 用,用于全文搜索与百分比跳转) */
+ fullText?: string
+ /** PDF 的总页数 */
+ totalPdfPages?: number
+}
+
+/** 隐藏相关的触发动作标识 */
+export type TriggerKey =
+ | 'dblclick'
+ | 'middleClick'
+ | 'rightClick'
+ | 'escape'
+ | 'mouseenter'
+ | 'mouseleave'
+
+/** 三个隐藏/显示动作的可配置触发器集合 */
+export interface HideActions {
+ /** 隐身:阅读窗可见时触发,进入 stealth 透明状态 */
+ stealthHide: TriggerKey[]
+ /** 显示:阅读窗处于 stealth 状态时触发,恢复可见 */
+ stealthShow: TriggerKey[]
+ /** 真隐藏 win.hide(),彻底消失,需用命令恢复 */
+ realHide: TriggerKey[]
+}
+
+/** 翻页方式开关 */
+export interface PageActions {
+ arrow: boolean
+ wheel: boolean
+ click: boolean
+ pgupdn: boolean
+ space: boolean
+ touch: boolean
+ /** 翻页过渡:'none' 无动画,'slide' 滑动 */
+ transition: 'none' | 'slide'
+}
+
+/** 阅读窗外观配色(独立于 UI 明暗主题) */
+export interface ReaderStyle {
+ bgColor: string
+ textColor: string
+ opacity: number
+ fontSize: number
+ lineHeight: number
+ fontFamily: string
+ fontWeight: number
+ cleanEmptyLines: boolean
+}
+
+/** 自动翻页设置 */
+export interface AutoPageSetting {
+ /** 间隔秒数,0 = 关闭 */
+ interval: number
+ /** stealth 隐藏时是否暂停 */
+ pauseOnStealth: boolean
+}
+
+/** 完整设置对象 */
+export interface Settings {
+ theme: 'auto' | 'light' | 'dark'
+ window: { width: number; height: number; x: number; y: number }
+ reader: ReaderStyle
+ page: PageActions
+ hide: HideActions
+ autoPage: AutoPageSetting
+}
+
+/** 阅读进度记录(按书籍路径存储) */
+export interface ReadingProgress {
+ filePath: string
+ format: BookFormat
+ /** 章节索引或 PDF 页码 */
+ chapterIndex: number
+ /** 章内页索引(TXT/EPUB 高度分页)或 0 */
+ pageIndex: number
+ /** 字符偏移(百分比跳转用,TXT) */
+ charOffset?: number
+ /** 章节总数(用于书架封面进度展示) */
+ totalChapters?: number
+ timestamp: number
+}
+
+/** 全文搜索结果项 */
+export interface SearchResult {
+ /** 关键字在全文字符流中的起始偏移 */
+ index: number
+ /** 含高亮标记的上下文片段 HTML */
+ snippet: string
+}
+
+/** 最近阅读历史项 */
+export interface RecentBook {
+ filePath: string
+ title: string
+ format: BookFormat
+ lastChapter?: number
+ lastRead: number
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/shared/ztools.d.ts b/plugins/serious-reading/src/shared/ztools.d.ts
new file mode 100644
index 00000000..d2d77c40
--- /dev/null
+++ b/plugins/serious-reading/src/shared/ztools.d.ts
@@ -0,0 +1,140 @@
+/**
+ * ZTools 运行时全局 API 的环境类型声明。
+ * 这些 API 由 ZTools 宿主在运行时注入到 window.ztools,不在依赖包中。
+ */
+export {}
+
+declare global {
+ interface ZToolsDbStorage {
+ getItem(key: string): any
+ setItem(key: string, value: any): void
+ removeItem(key: string): void
+ }
+
+ interface ZToolsDbDoc {
+ _id: string
+ _rev?: string
+ [k: string]: any
+ }
+
+ interface ZToolsDb {
+ put(doc: ZToolsDbDoc): { ok: boolean; rev?: string; id: string; error?: boolean; message?: string }
+ get(id: string): ZToolsDbDoc | null
+ remove(docOrId: ZToolsDbDoc | string): { ok: boolean }
+ postAttachment(id: string, attachment: string | Buffer, type: string): { ok: boolean }
+ getAttachment(id: string): Buffer | null
+ }
+
+ interface ZToolsClipboard {
+ write: (id: string, shouldPaste?: boolean) => Promise
+ writeContent: (data: { type: 'text' | 'image'; content: string }, shouldPaste?: boolean) => Promise
+ }
+
+ interface BrowserWindowProxy {
+ show(): void
+ hide(): void
+ isVisible(): boolean
+ isDestroyed(): boolean
+ focus(): void
+ close(): void
+ getPosition(): [number, number]
+ getSize(): [number, number]
+ getBounds(): { x: number; y: number; width: number; height: number }
+ setBounds(bounds: { x?: number; y?: number; width?: number; height?: number }): void
+ setPosition(x: number, y: number): void
+ setSize(width: number, height: number): void
+ setAlwaysOnTop(flag: boolean, level?: string): void
+ webContents: {
+ id: number
+ send(channel: string, ...args: any[]): void
+ openDevTools(): void
+ }
+ }
+
+ interface LaunchParam {
+ payload: any
+ type: 'text' | 'regex' | 'over' | 'files' | 'img'
+ code: string
+ }
+
+ interface ZToolsApi {
+ getAppName(): string
+ isMacOs(): boolean
+ isWindows(): boolean
+ isLinux(): boolean
+ isDarkColors(): boolean
+ isDev(): boolean
+ getNativeId(): string
+ getAppVersion(): string
+ getWindowType(): string
+ setExpendHeight(height: number): void
+ showNotification(body: string): void
+ showMainWindow(): Promise
+ hideMainWindow(isRestorePreWindow?: boolean): Promise
+ outPlugin(isKill?: boolean): Promise
+ onPluginEnter(cb: (p: LaunchParam) => void): void
+ onPluginOut(cb: (isKill: boolean) => void): void
+ onPluginDetach(cb: () => void): void
+ onPluginReady(cb: (p: LaunchParam) => void): void
+ createBrowserWindow(
+ url: string,
+ options: Record,
+ callback?: () => void,
+ ): BrowserWindowProxy | null
+ sendToParent(channel: string, ...args: any[]): void
+ showOpenDialog(options: Record): string[] | undefined
+ showSaveDialog(options: Record): string | undefined
+ screenCapture(cb: (image: string) => void): void
+ copyText(text: string): boolean
+ getPath(name: string): string
+ getPathForFile(file: File): string
+ db: ZToolsDb
+ dbStorage: ZToolsDbStorage
+ clipboard: ZToolsClipboard
+ }
+
+ const ztools: ZToolsApi | undefined
+
+ interface Window {
+ ztools?: ZToolsApi
+ /**
+ * 主窗 preload 暴露给渲染进程的服务。
+ * 阅读窗有自己的 reader services,详见 preload/reader.js。
+ */
+ services?: ReaderServices
+ _ipcRenderer?: { on(channel: string, cb: (e: unknown, ...args: any[]) => void): void; send(channel: string, ...args: any[]): void }
+ _sendToParent?: (channel: string, data?: any) => void
+ }
+
+ /** preload 暴露的文件读取/解析服务 */
+ interface ReaderServices {
+ /** 读取 TXT 文件,自动检测编码并解码为字符串 */
+ readTxt(filePath: string): string | null
+ /** 解析 EPUB,返回书籍元信息 + 章节列表(正文为 HTML 字符串) */
+ readEpub(filePath: string): EBook | null
+ /** 读取 PDF 为 ArrayBuffer,交由前端 pdfjs 渲染 */
+ readPdf(filePath: string): ArrayBuffer | null
+ /** 取文件 Buffer(封面等) */
+ readBuffer(filePath: string): ArrayBuffer | null
+ /** 选择文件对话框 */
+ showOpenDialog(options: Record): string[] | undefined
+ /** 创建并持有悬浮阅读窗 */
+ createReaderWindow(state: any): BrowserWindowProxy | null
+ /** 向阅读窗发送消息 */
+ sendToReader(channel: string, data?: any): void
+ /** 显示阅读窗 */
+ showReader(): boolean
+ /** 切换阅读窗显隐 */
+ toggleReader(): boolean
+ }
+
+ /** EPUB 解析结果(readEpub 返回) */
+ interface EBook {
+ title: string
+ filePath: string
+ format: 'epub'
+ chapters: { title: string; content: string; index: number }[]
+ totalChapters: number
+ cover?: ArrayBuffer | null
+ }
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/src/styles/globals.css b/plugins/serious-reading/src/styles/globals.css
new file mode 100644
index 00000000..1e0a41b5
--- /dev/null
+++ b/plugins/serious-reading/src/styles/globals.css
@@ -0,0 +1,63 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222 47% 11%;
+ --card: 0 0% 100%;
+ --card-foreground: 222 47% 11%;
+ --primary: 160 84% 39%;
+ --primary-foreground: 0 0% 100%;
+ --secondary: 220 14% 96%;
+ --secondary-foreground: 222 47% 11%;
+ --muted: 220 14% 96%;
+ --muted-foreground: 220 9% 46%;
+ --accent: 220 14% 96%;
+ --accent-foreground: 222 47% 11%;
+ --destructive: 0 84% 60%;
+ --destructive-foreground: 0 0% 100%;
+ --border: 220 13% 91%;
+ --input: 220 13% 91%;
+ --ring: 160 84% 39%;
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222 47% 11%;
+ --foreground: 210 40% 98%;
+ --card: 222 47% 13%;
+ --card-foreground: 210 40% 98%;
+ --primary: 160 84% 45%;
+ --primary-foreground: 0 0% 100%;
+ --secondary: 217 33% 17%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217 33% 17%;
+ --muted-foreground: 215 20% 65%;
+ --accent: 217 33% 17%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 63% 31%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217 33% 20%;
+ --input: 217 33% 20%;
+ --ring: 160 84% 45%;
+ }
+}
+
+html, body, #root { height: 100%; }
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
+ -webkit-font-smoothing: antialiased;
+}
+
+/* 阅读窗透明留窗(stealth)全局样式:内容近全透明,窗口仍在 */
+.sr-stealth {
+ color: #00000001 !important;
+ background: #00000001 !important;
+}
+.sr-stealth * { visibility: hidden !important; }
+
+/* 阅读窗拖拽区 */
+.sr-drag { -webkit-app-region: drag; }
+.sr-no-drag { -webkit-app-region: no-drag; }
\ No newline at end of file
diff --git a/plugins/serious-reading/src/vite-env.d.ts b/plugins/serious-reading/src/vite-env.d.ts
new file mode 100644
index 00000000..151aa685
--- /dev/null
+++ b/plugins/serious-reading/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
\ No newline at end of file
diff --git a/plugins/serious-reading/tailwind.config.js b/plugins/serious-reading/tailwind.config.js
new file mode 100644
index 00000000..0fd71ee3
--- /dev/null
+++ b/plugins/serious-reading/tailwind.config.js
@@ -0,0 +1,46 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ darkMode: 'class',
+ content: ['./index.html', './reader.html', './src/**/*.{ts,tsx}'],
+ theme: {
+ extend: {
+ colors: {
+ border: 'hsl(var(--border))',
+ input: 'hsl(var(--input))',
+ ring: 'hsl(var(--ring))',
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))',
+ },
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))',
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))',
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))',
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))',
+ },
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))',
+ },
+ },
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)',
+ },
+ },
+ },
+ plugins: [],
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/tsconfig.json b/plugins/serious-reading/tsconfig.json
new file mode 100644
index 00000000..f0944c19
--- /dev/null
+++ b/plugins/serious-reading/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ },
+ "types": ["node"]
+ },
+ "include": ["src", "vite.config.ts"]
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/vite.config.ts b/plugins/serious-reading/vite.config.ts
new file mode 100644
index 00000000..3e1af77a
--- /dev/null
+++ b/plugins/serious-reading/vite.config.ts
@@ -0,0 +1,47 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { copyFileSync } from 'node:fs'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+export default defineConfig({
+ plugins: [
+ react(),
+ {
+ name: 'copy-logo',
+ closeBundle() {
+ copyFileSync(
+ path.resolve(__dirname, 'logo.svg'),
+ path.resolve(__dirname, 'dist/logo.svg')
+ )
+ },
+ },
+ ],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src'),
+ },
+ },
+ base: './',
+ build: {
+ outDir: 'dist',
+ emptyOutDir: true,
+ rollupOptions: {
+ input: {
+ main: path.resolve(__dirname, 'index.html'),
+ reader: path.resolve(__dirname, 'reader.html'),
+ },
+ output: {
+ entryFileNames: 'assets/[name]-[hash].js',
+ chunkFileNames: 'assets/[name]-[hash].js',
+ assetFileNames: 'assets/[name]-[hash][extname]',
+ },
+ },
+ chunkSizeWarningLimit: 1500,
+ },
+ optimizeDeps: {
+ exclude: ['pdfjs-dist'],
+ },
+})
\ No newline at end of file
diff --git "a/plugins/serious-reading/zools\345\274\200\345\217\221\350\200\205\346\226\207\346\241\243.md" "b/plugins/serious-reading/zools\345\274\200\345\217\221\350\200\205\346\226\207\346\241\243.md"
new file mode 100644
index 00000000..bfbaa1ef
--- /dev/null
+++ "b/plugins/serious-reading/zools\345\274\200\345\217\221\350\200\205\346\226\207\346\241\243.md"
@@ -0,0 +1,1642 @@
+# 快速开始
+
+hey,开发者,终于和你见面了。
+
+从这里开始,将会慢慢的给你介绍如何开发一个 ZTools 插件应用,帮助你一步步的完成开发、构建和发布。
+
+## 插件应用是什么
+
+**Node.js 本地原生能力 + Web 前端网页**。(本地软件能做到的,理论上它也能做到)
+
+ZTools 插件应用结合了 Web 前端技术的灵活性和 Node.js 的强大本地能力,让你可以:
+
+- 🎨 使用 HTML、CSS、JavaScript 构建美观的用户界面
+- ⚡ 通过 Node.js 访问系统原生能力(文件系统、网络、进程等)
+- 🔌 使用丰富的 ZTools API(通知、剪贴板、窗口管理等)
+- 📦 支持 Vue、React 等现代前端框架
+- 🌍 跨平台运行(Windows、macOS、Linux)
+
+## 环境要求
+
+在开始开发你的第一个插件应用之前,请保证你已经做好以下准备:
+
+### 必需工具
+
+- **ZTools** - [下载地址](https://github.com/ZToolsCenter/ZTools/releases)
+ - macOS: 下载 `ZTools-x.x.x-arm64.dmg` 或 `ZTools-x.x.x-arm64.zip`
+ - Windows: 下载 `ZTools-x.x.x-setup.exe` 或 `ZTools-x.x.x-win.zip`
+- **Node.js** >= 16.0.0 - [下载地址](https://nodejs.org/)
+- **一个好用的代码编辑器** - 推荐 [VSCode](https://code.visualstudio.com/) 或者 [WebStorm](https://www.jetbrains.com/webstorm/)
+- **Git** - 用于版本控制和插件发布([下载地址](https://git-scm.com/))
+
+### 基础知识
+
+- **熟悉 JavaScript** - 基础开发语言
+- **熟悉 HTML 和 CSS** - 掌握基础的界面构建能力
+- **了解 Node.js** - 接入强大的本地原生能力
+
+### 进阶
+
+可借助 **Vue** 或者 **React** 等主流的 Web 前端开发框架,增强你的应用界面。
+
+ZTools 插件 CLI 工具提供了以下模板:
+
+- **Vue + TypeScript + Vite** - 使用 Vue 3 开发插件 UI
+- **React + TypeScript + Vite** - 使用 React 开发插件 UI
+- **Preload Only (TypeScript)** - 仅使用 Preload API,无 UI 界面
+
+# 插件应用目录结构
+
+此部分会帮助你了解,通常情况下,一个插件应用的文件目录结构。
+
+插件应用至少要有一个 `plugin.json` 作为入口,并配置 `logo` 字段以及 `main` 或者 `preload` 字段。
+
+一个相对完整可打包成插件应用的目录可能是这样的:
+
+
+
+```
+/{plugin}
+|-- plugin.json
+|-- preload.js
+|-- index.html
+|-- index.js
+|-- index.css
+|-- logo.png
+```
+
+## 源码编译
+
+ZTools 仅识别 `html + css + javascript`, 通常我们在开发过程中可能会使用各种的工具来辅助开发,比如 `vite`、`webpack` 等等,也可能会引入各种前端框架,比如 `vue`、`react`、`svelte` 等等,而这些代码并不是直接可以被 ZTools 识别的,当我们打包插件应用前应该先将框架代码编译成普通的 html 、css、js 文件。通常是将源码编译输出到 dist 文件夹,然后将 dist 文件夹打包成插件应用,切勿将整个项目的根目录打包成插件应用。
+
+## 第三方依赖
+
+当你使用第三方依赖时,根据项目情况进行区分:
+
+当你使用前端依赖时,只需要在项目的根目录下安装即可,对前端项目进行正常的编译,输出到 `dist` 文件夹。
+
+当你使用 nodejs 的第三方依赖时,应当保证你的模块存在于 `preload.js` 同级目录,并且不要对它们进行编译操作,保证提交插件应用时的目录结构不变,并且源码清晰可读。
+
+# 第一个插件
+
+本指南将帮助你使用 `@ztools-center/plugin-cli` 快速创建你的第一个 ZTools 插件,并发布到插件中心。
+
+## 前置要求
+
+在开始之前,请确保你已经安装了以下工具:
+
+- **Node.js** >= 16.0.0
+- **npm** 或 **pnpm** 包管理器
+- **Git**(用于版本控制和发布)
+
+## 安装 CLI 工具
+
+首先,全局安装 ZTools 插件 CLI 工具:
+
+
+
+```
+npm install -g @ztools-center/plugin-cli
+# 或
+pnpm add -g @ztools-center/plugin-cli
+```
+
+安装完成后,你可以使用 `ztools` 命令来创建和管理插件。
+
+## 创建第一个插件
+
+### 步骤 1: 创建插件项目
+
+使用 CLI 工具创建一个新的插件项目:
+
+
+
+```
+ztools create my-first-plugin
+```
+
+这个命令会引导你完成以下步骤:
+
+1. **选择模板** - 你可以选择以下三种模板之一:
+ - **Vue + TypeScript + Vite** - 使用 Vue 3 开发插件 UI
+ - **React + TypeScript + Vite** - 使用 React 开发插件 UI
+ - **Preload Only (TypeScript)** - 仅使用 Preload API,无 UI 界面
+2. **输入插件信息**:
+ - **Plugin name** - 插件唯一标识(ID),用于系统内部识别
+ - **Plugin title** - 插件显示名称(在 ZTools 中展示给用户的标题)
+ - **Plugin description** - 插件描述
+ - **Author** - 作者名称
+
+### 步骤 2: 进入项目目录
+
+创建完成后,进入项目目录:
+
+
+
+```
+cd my-first-plugin
+```
+
+### 步骤 3: 安装依赖
+
+根据你选择的包管理器安装依赖:
+
+
+
+```
+npm install
+# 或
+pnpm install
+```
+
+### 步骤 4: 开发插件
+
+现在你可以开始开发你的插件了。根据你选择的模板,项目结构会有所不同:
+
+- **Vue/React 模板**:在 `src/` 目录下开发 UI 组件
+- **Preload Only 模板**:在 `src/` 目录下编写 Preload 脚本
+
+开发时,你可以运行开发服务器:
+
+
+
+```
+npm run dev
+# 或
+pnpm run dev
+```
+
+### 步骤 5: 构建插件
+
+开发完成后,构建插件:
+
+
+
+```
+npm run build
+# 或
+pnpm run build
+```
+
+构建产物会输出到 `dist/` 目录。这个目录就是你的插件应用,可以打包提交。
+
+## 发布插件
+
+当你完成插件开发并准备好发布时,可以使用 CLI 工具将插件发布到 ZTools 插件中心。
+
+### 前置条件
+
+在发布之前,请确保:
+
+1. ✅ 项目包含 `plugin.json` 文件(CLI 会自动生成)
+2. ✅ 已初始化 Git 仓库(`git init`)
+3. ✅ 至少有一次提交记录
+4. ✅ 工作区干净(没有未提交的改动)
+
+### 初始化 Git 仓库
+
+如果还没有初始化 Git 仓库,请执行:
+
+
+
+```
+git init
+git add .
+git commit -m "Initial commit"
+```
+
+### 发布流程
+
+执行发布命令:
+
+
+
+```
+ztools publish
+```
+
+#### 首次发布
+
+首次执行 `ztools publish` 时,CLI 会自动完成:
+
+1. **GitHub OAuth 认证** - 通过 Device Flow 引导你在浏览器授权一次(含 `workflow` scope),token 保存在 `~/.config/ztools/cli-config.json`
+2. **Fork 中心仓库** - 自动在你账号下 fork `ZToolsCenter/ZTools-plugins`(已存在则复用)
+3. **同步 fork main** - 调用 GitHub merge-upstream API 把 fork 的 main 拉齐到上游,避免后续分支基于落后的 main 导致冲突
+4. **判定 Add / Update** - 检查上游 `plugins/<你的插件 ID>/` 目录是否存在,决定 PR 标题用 `Add` 还是 `Update`
+5. **复制工作目录文件** - 把当前目录内容复制到 fork 的 `plugins/<插件 ID>/`(自动忽略 `node_modules`、`dist`、`.env*` 等)
+6. **生成 commit + 推送分支** - 在 fork 的 `plugin/<插件 ID>` 分支上做**一个** commit 并普通 push(不 force)
+7. **创建 Draft Pull Request** - 自动开 PR 到中心仓库,默认 draft 状态
+
+#### 后续发布(增量更新)
+
+每次 `ztools publish` 都是**增量追加**:
+
+- 远端分支保留旧 commit,只 fast-forward 追加一个新 commit
+- 同一个 PR 自动复用,链接不变
+- 不会 force-push,旧的 review 评论上下文不会丢失
+
+> 例:你本地累计 5 个 commit 发布出去后,远端 PR 上是 1 个 "Add plugin Foo v0.1.0" commit;又改了 3 个 commit 再发布,远端就 fast-forward 多 1 个 "Update plugin Foo v0.1.1" commit,旧的不动。
+
+更详细的发布与协作机制(CHANGELOG 自动注入、智能 commit 标题、`pull-contributions` 拉回审核者改动等)请参考 [发布与协作流程](https://ztoolscenter.github.io/ZTools-doc/publish-and-update.html)。
+
+### 发布成功后
+
+CLI 会输出类似:
+
+
+
+```
+✨ 插件发布成功!
+🔗 Pull Request: https://github.com/ZToolsCenter/ZTools-plugins/pull/123
+
+💡 下一步:去 PR 网页完善以下内容(CLI 无法自动生成)
+ 📸 上传截图 / 演示 GIF
+ ✅ 勾选自检清单
+ 🚦 把 PR 从 Draft 切到 "Ready for review"
+```
+
+务必完成这 3 件事,否则维护者不会进入审核:
+
+1. **截图 / 演示 GIF** - 直接拖图到 PR description 编辑框,GitHub 会自动上传
+2. **自检清单** - PR description 里有 5 项 checkbox,逐条勾上
+3. **Mark as ready for review** - 右下角按钮,把 PR 从 Draft 切到正式审核状态
+
+## 项目结构
+
+创建的项目通常包含以下结构:
+
+
+
+```
+my-first-plugin/
+├── plugin.json # 插件配置文件
+├── package.json # 项目依赖配置
+├── tsconfig.json # TypeScript 配置
+├── vite.config.js # Vite 构建配置(如果使用 Vite 模板)
+├── src/ # 源代码目录
+│ ├── preload.ts # Preload 脚本
+│ └── ... # 其他源文件
+├── public/ # 静态资源
+│ └── logo.png # 插件 Logo
+└── dist/ # 构建输出目录(构建后生成)
+```
+
+## 常见问题
+
+### Q: 如何修改插件配置?
+
+A: 编辑项目根目录下的 `plugin.json` 文件。你可以修改插件标识(name)、显示名称(title)、描述、功能列表等。更多信息请参考 [plugin.json 配置](https://ztoolscenter.github.io/ZTools-doc/plugin-json.html)。
+
+### Q: 如何添加插件功能?
+
+A: 在 `plugin.json` 的 `features` 数组中添加功能配置。每个功能需要定义:
+
+- `code` - 功能唯一标识
+- `explain` - 功能说明
+- `cmds` - 触发指令列表
+
+### Q: 发布失败怎么办?
+
+A: 检查以下几点:
+
+- 确保在插件项目根目录下执行命令
+- 确保已初始化 Git 并至少有一次提交
+- 确保 `plugin.json` 文件存在且格式正确
+- 检查网络连接和 GitHub 认证状态
+
+### Q: 如何更新已发布的插件?
+
+A: 修改代码、`git commit`,然后再次执行 `ztools publish`。CLI 会自动在**同一个 PR** 上 fast-forward 追加一个新 commit,链接不变。如果上一次的 PR 已经合并,新的 publish 会以 `Update` 标题开一个新 PR。
+
+### Q: 审核者直接在 PR 分支上改了代码,我下次 publish 会被拒,怎么办?
+
+A: 跑 `ztools pull-contributions`,它会把审核者的 commit 三方合并回你本地,再 `ztools publish` 即可。详见 [发布与协作流程](https://ztoolscenter.github.io/ZTools-doc/publish-and-update.html#pull-contributions)。
+
+### Q: PR 标题是怎么决定的?
+
+A: PR 标题始终是 `Add plugin <名称> v<版本>` 或 `Update plugin <名称> v<版本>`,由"中心仓库 main 是否已有该插件目录"决定。每次发布在 commit message 里会附带你本地自上次发布以来的 commit subjects 作为变更明细。
+
+### Q: 我删了上一个 PR 重新 publish 没反应?
+
+A: 你之前 publish 过时 fork 上分支已经存在;CLI 检测到本地内容与 fork 一致就不会再 commit。这种情况会**自动复用已有 branch 重开一个 PR**——直接重新跑 `ztools publish` 即可,新 PR 链接会显示在终端。
+
+# plugin.json 配置
+
+plugin.json 文件是插件应用的配置文件,它是最重要的一个文件,用来定义这个插件应用将如何与 ZTools 集成。 每当你创建一个插件应用时,都需要从创建一个 plugin.json 文件开始。
+
+## 配置文件格式
+
+plugin.json 文件是一个标准的 JSON 文件,它的结构如下:
+
+
+
+```
+{
+ "name": "example",
+ "title": "示例插件",
+ "description": "这是一个示例插件",
+ "version": "1.0.0",
+ "main": "index.html",
+ "logo": "logo.png",
+ "preload": "preload.js",
+ "features": [
+ {
+ "code": "hello",
+ "explain": "hello world",
+ "cmds": ["hello", "你好"]
+ }
+ ]
+}
+```
+
+## 基础字段说明
+
+### `name`
+
+- 类型:`string`
+- 必填:是
+
+插件应用唯一标识(ID),用于在系统中唯一标识该插件。
+
+### `title`
+
+- 类型:`string`
+- 必填:是
+
+插件应用显示名称,在 ZTools 中展示给用户看的标题。
+
+### `description`
+
+- 类型:`string`
+- 必填:否
+
+插件应用描述
+
+### `version`
+
+- 类型:`string`
+- 必填:否
+
+插件应用版本
+
+### `main`
+
+- 类型:`string`
+- 必填:是
+
+插件入口,可以是一个相对于 `plugin.json` 的相对路径的 `.html` 文件,或者是一个在线地址
+
+### `logo`
+
+- 类型:`string`
+- 必填:是
+
+插件应用 Logo,必须为 png 或 jpg 文件
+
+### `preload`
+
+- 类型:`string`
+- 必填:是
+
+预加载 js 文件,这是一个关键文件,你可以在此文件内调用 nodejs、 electron 提供的 api。 查看更多关于 [preload.js](https://ztoolscenter.github.io/ZTools-doc/preload.js)
+
+## 开发模式字段说明
+
+### `development`
+
+- 类型:`object`
+- 必填:否
+
+开发模式下的配置,对象的同名字段会覆盖基础配置字段。
+
+### `development.main`
+
+- 类型:`string`
+- 必填:否
+
+开发模式下,插件应用的入口文件,与基础配置字段 main 字段相同
+
+## 插件应用功能字段说明
+
+### `features`
+
+- 类型:`Array`
+- 必填:否
+
+插件功能列表,定义插件支持的功能及其触发方式。
+
+### `feature.code`
+
+- 类型:`string`
+- 说明:功能唯一标识,用于区分不同功能。
+
+### `feature.explain`
+
+- 类型:`string`
+- 说明:功能说明,显示在搜索结果中。
+
+### `feature.cmds`
+
+- 类型:`Array`
+- 说明:触发指令列表。可以是简单的字符串,也可以是匹配对象(正则、全局、图片、文件等)。
+
+### `feature.platform`
+
+- 类型:`Array<'win32' | 'darwin' | 'linux'>`
+- 必填:否
+- 说明:支持的平台。如果不填,默认支持所有平台。
+
+## 指令类型详解
+
+### 文本指令 (String)
+
+最简单的触发方式,当用户输入完全匹配该文本时触发。
+
+
+
+```
+{
+ "features": [
+ {
+ "code": "hello",
+ "explain": "打招呼",
+ "cmds": ["hello", "你好"]
+ }
+ ]
+}
+```
+
+### 正则表达式指令 (RegexCmd)
+
+使用正则表达式匹配用户输入。
+
+
+
+```
+{
+ "features": [
+ {
+ "code": "color",
+ "explain": "颜色预览",
+ "cmds": [
+ {
+ "type": "regex",
+ "label": "颜色预览",
+ "match": "/^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/i",
+ "minLength": 4
+ }
+ ]
+ }
+ ]
+}
+```
+
+- `type`: 固定为 `"regex"`
+- `label`: 匹配成功后显示的名称
+- `match`: 正则表达式字符串 (例如 `"/^abc/i"`)
+- `minLength`: 触发匹配的最小字符长度
+
+### 全局匹配指令 (OverCmd)
+
+匹配任意文本,通常用于需要处理所有输入的场景(如翻译、搜索插件)。
+
+
+
+```
+{
+ "features": [
+ {
+ "code": "translate",
+ "explain": "翻译",
+ "cmds": [
+ {
+ "type": "over",
+ "label": "翻译",
+ "exclude": "/^exclude/i",
+ "minLength": 1,
+ "maxLength": 1000
+ }
+ ]
+ }
+ ]
+}
+```
+
+- `type`: 固定为 `"over"`
+- `label`: 显示的名称
+- `exclude`: (可选) 排除匹配的正则表达式字符串
+- `minLength`: (可选) 最小字符数
+- `maxLength`: (可选) 最大字符数 (默认 10000)
+
+### 图片匹配指令 (ImgCmd)
+
+当用户粘贴图片到 ZTools 时触发,用于处理图片的插件(如图片压缩、格式转换、OCR 识别等)。
+
+
+
+```
+{
+ "features": [
+ {
+ "code": "image-process",
+ "explain": "图片处理",
+ "cmds": [
+ {
+ "type": "img",
+ "label": "图片处理"
+ }
+ ]
+ }
+ ]
+}
+```
+
+- `type`: 固定为 `"img"`
+- `label`: 显示的名称
+
+**使用场景**:
+
+- 图片压缩
+- 图片格式转换
+- OCR 文字识别
+- 图片编辑
+- 图片上传
+
+### 文件匹配指令 (FilesCmd)
+
+当用户粘贴文件或文件夹到 ZTools 时触发,支持多种过滤条件来精确匹配文件。
+
+
+
+```
+{
+ "features": [
+ {
+ "code": "file-process",
+ "explain": "文件处理",
+ "cmds": [
+ {
+ "type": "files",
+ "label": "批量重命名",
+ "fileType": "file",
+ "extensions": ["txt", "md", "json"],
+ "match": "/^test/i",
+ "minLength": 1,
+ "maxLength": 100
+ }
+ ]
+ }
+ ]
+}
+```
+
+- `type`: 固定为 `"files"`
+- `label`: 显示的名称
+- `fileType`: (可选) 文件类型,`"file"` 表示只匹配文件,`"directory"` 表示只匹配文件夹。不指定则文件和文件夹都匹配
+- `extensions`: (可选) 文件扩展名数组,只对文件有效(不检查文件夹)。例如 `["jpg", "png", "gif"]`
+- `match`: (可选) 匹配文件(夹)名称的正则表达式字符串。例如 `"/^test/i"` 表示匹配以 "test" 开头的文件名(不区分大小写)
+- `minLength`: (可选) 最少文件数,默认 1
+- `maxLength`: (可选) 最多文件数,默认 10000
+
+**匹配规则**:
+
+1. 首先检查文件数量是否在 `minLength` 和 `maxLength` 范围内
+2. 然后检查每个文件是否满足以下条件(如果指定):
+ - 文件类型(`fileType`)
+ - 文件扩展名(`extensions`)
+ - 文件名正则匹配(`match`)
+
+**使用场景**:
+
+- 批量文件重命名
+- 文件格式转换
+- 文件压缩打包
+- 文件批量上传
+- 代码文件批量处理
+
+
+
+
+
+# 认识 preload
+
+当你在 `plugin.json` 文件配置了 `preload` 字段,指定的 js 文件将被预加载,该 js 文件可以调用 Node.js API 的本地原生能力和 Electron 渲染进程 API。
+
+## 为什么需要 `preload`
+
+在传统的 web 开发中,为了保持用户运行环境的安全,JavaScript 被做了很强的沙箱限制,比如不能访问本地文件,不能访问跨域网络资源,不能访问本地存储等。
+
+ZTools 基于 Electron 构建,通过 preload 机制,在渲染线程中,释放了沙箱限制,使得用户可以通过调用 Node.js 的 API 来访问本地文件、跨域网络资源、本地存储等。
+
+## `preload` 的定义
+
+`preload` 是完全独立于前端项目的一个特殊文件,它应当与 `plugin.json` 位于同一目录或其子目录下,保证可以在打包插件应用时可以被一起打包。
+
+`preload` js 文件遵循 CommonJS 规范,因此你可以使用 `require` 来引入 Node.js 模块,此部分可以参考 Node.js 文档。
+
+## 前端使用 `preload`
+
+只需给 `window` 对象自定义一个属性,前端就可直接访问该属性。
+
+**preload.js**
+
+
+
+```
+const fs = require("fs");
+
+window.customApis = {
+ readFile: (path) => {
+ return fs.readFileSync(path, "utf8");
+ },
+};
+```
+
+## preload js 规范
+
+由于 `preload` js 文件可使用本地原生能力,为了防止开发者滥用各种读写文件、网络等能力,ZTools 规定:
+
+- `preload` js 文件代码不能进行打包/压缩/混淆等操作,要保证每一行代码清晰可读。
+- 引入的第三方模块也必须清晰可读,在提交时将源码一同提交,同样不允许压缩/混淆。
+
+
+
+
+
+```
+ } else {
+ console.log("Email sent: " + info.response);
+ }
+ });
+};
+window.services = {
+ sendMail: () => {
+ return sendMail();
+ },
+};
+```
+
+## 引入 Electron 渲染进程 API
+
+preload.js
+
+
+
+```
+const { clipboard, nativeImage } = require("electron");
+
+window.services = {
+ copyImage: (imageFilePath) => {
+ clipboard.writeImage(nativeImage.createFromPath(imageFilePath))
+ },
+};
+```
+
+
+
+# 插件 API 文档
+
+ZTools 为插件提供了一套丰富的 API,通过全局对象 `window.ztools` 暴露。
+
+## 基础 API
+
+### `ztools.getAppName()`
+
+获取应用名称。
+
+- **返回**: `string` - 应用名称,固定返回 `'ZTools'`。
+
+### `ztools.getPathForFile(file)`
+
+获取拖放文件的真实路径。用于处理用户拖放文件到插件界面的场景(基于 Electron `webUtils.getPathForFile`)。
+
+- **file**: `File` - 拖放事件中的 File 对象。
+- **返回**: `string` - 文件的本地路径。
+
+### `ztools.isMacOs()` / `ztools.isMacOS()`
+
+检测当前是否为 macOS 系统。
+
+- **返回**: `boolean` - 是否为 macOS。
+
+### `ztools.isWindows()`
+
+检测当前是否为 Windows 系统。
+
+- **返回**: `boolean` - 是否为 Windows。
+
+### `ztools.isLinux()`
+
+检测当前是否为 Linux 系统。
+
+- **返回**: `boolean` - 是否为 Linux。
+
+### `ztools.getNativeId()`
+
+获取设备唯一标识符(32位字符串)。
+
+- **返回**: `string` - 设备唯一标识符。
+
+### `ztools.getAppVersion()`
+
+获取应用版本号。
+
+- **返回**: `string` - 应用版本号。
+
+### `ztools.getWindowType()`
+
+获取当前窗口类型。
+
+- **返回**: `string` - 窗口类型。
+
+### `ztools.isDarkColors()`
+
+检测当前是否为深色主题。
+
+- **返回**: `boolean` - 是否为深色主题。
+
+### `ztools.isDev()`
+
+检查当前插件是否处于开发模式。
+
+- **返回**: `boolean` - 是否处于开发模式。
+
+### `ztools.getWebContentsId()`
+
+获取当前 WebContents ID。
+
+- **返回**: `number` - WebContents ID。
+
+### `ztools.setExpendHeight(height)`
+
+设置插件视图的高度。
+
+- **height**: `number` - 期望的高度(像素)。
+
+### `ztools.showNotification(body)`
+
+显示系统通知。
+
+- **body**: `string` - 通知内容。
+
+### `ztools.sendInputEvent(event)`
+
+发送模拟输入事件。
+
+- **event**: `MouseInputEvent | MouseWheelInputEvent | KeyboardInputEvent` - 输入事件对象。
+
+#### 事件对象结构
+
+**KeyboardInputEvent (键盘事件)**
+
+- `type`: `'keyDown'` | `'keyUp'` | `'char'`
+- `keyCode`: `string` - 键盘代码
+- `modifiers`: `string[]` - 修饰键数组 (例如 `['shift', 'control']`)
+
+**MouseInputEvent (鼠标事件)**
+
+- `type`: `'mouseDown'` | `'mouseUp'` | `'mouseEnter'` | `'mouseLeave'` | `'contextMenu'` | `'mouseMove'`
+- `x`: `number` - X 坐标
+- `y`: `number` - Y 坐标
+- `button`: `'left'` | `'middle'` | `'right'` - 按钮类型
+- `clickCount`: `number` - 点击次数
+
+**MouseWheelInputEvent (滚轮事件)**
+
+- `type`: `'mouseWheel'`
+- `deltaX`: `number`
+- `deltaY`: `number`
+- `wheelTicksX`: `number`
+- `wheelTicksY`: `number`
+- `accelerationRatioX`: `number`
+- `accelerationRatioY`: `number`
+- `hasPreciseScrollingDeltas`: `boolean`
+- `canScroll`: `boolean`
+
+### `ztools.simulateKeyboardTap(key, ...modifiers)`
+
+模拟键盘按键。
+
+- **key**: `string` - 要按下的键。
+- **modifiers**: `string[]` - 修饰键数组(可选)。
+- **返回**: `boolean` - 是否成功。
+
+### `ztools.showMainWindow()`
+
+显示主窗口。
+
+- **返回**: `Promise` - 是否成功。
+
+### `ztools.hideMainWindow(isRestorePreWindow)`
+
+隐藏主窗口,包括此时正在主窗口运行的插件应用。
+
+- **isRestorePreWindow**: `boolean` - (可选) 是否焦点回归到前面的活动窗口,默认 `true`。
+- **返回**: `Promise` - 是否成功。
+
+### `ztools.outPlugin(isKill)`
+
+退出插件应用,默认将插件应用隐藏后台。
+
+- **isKill**: `boolean` - (可选) 为 `true` 时,将结束运行插件应用 (杀死进程)。
+- **返回**: `Promise` - 是否成功。
+
+## 事件 API
+
+### `ztools.onPluginEnter(callback)`
+
+监听插件进入事件。当用户打开插件时触发。
+
+- **callback**: `(param: LaunchParam) => void` - 回调函数,接收启动参数。
+
+#### LaunchParam 结构
+
+- `payload`: `any` - 传递的数据(例如搜索框内容)
+- `type`: `'text' | 'regex' | 'over'` - 命令类型
+ - `'text'`: 文本匹配
+ - `'regex'`: 正则表达式匹配
+ - `'over'`: 任意文本匹配
+- `code`: `string` - 插件 Feature Code (如果是由 Feature 触发)
+
+### `ztools.onPluginOut(callback)`
+
+监听插件退出事件。
+
+- **callback**: `(isKill: boolean) => void` - 回调函数,接收退出参数。
+ - `isKill`: 是否为强制退出(杀死进程)。
+
+### `ztools.onPluginDetach(callback)`
+
+监听插件被分离为独立窗口的事件。当用户将插件从主窗口分离时触发。
+
+- **callback**: `() => void` - 回调函数。
+
+### `ztools.onMainPush(callback, selectCallback)`
+
+注册主搜索推送功能。插件可以在主搜索框中提供搜索结果,用户无需进入插件即可看到结果。
+
+- **callback**: `(queryData: any) => object[]` - 查询回调函数,接收搜索数据,返回搜索结果数组。
+- **selectCallback**: `(selectData: any) => boolean` - (可选) 用户选择搜索结果时的回调函数。返回 `true` 表示需要进入插件。
+
+### `ztools.onPluginReady(callback)`
+
+兼容旧 API,功能与 `onPluginEnter` 相同。
+
+- **callback**: `(param: LaunchParam) => void` - 回调函数,接收启动参数。
+
+## 搜索框 API
+
+### `ztools.setSubInput(onChange, placeholder, isFocus)`
+
+设置主窗口搜索框的行为(当插件处于活动状态时)。
+
+- **onChange**: `(text: string) => void` - 当用户在搜索框输入时触发的回调函数。
+- **placeholder**: `string` - 搜索框的占位符文本。
+- **isFocus**: `boolean` - (可选) 是否自动聚焦搜索框,默认 `true`。
+
+### `ztools.setSubInputValue(text)`
+
+设置子输入框的值。
+
+- **text**: `string` - 要设置的值。
+
+### `ztools.subInputFocus()`
+
+聚焦子输入框。
+
+- **返回**: `boolean` - 是否成功。
+
+### `ztools.subInputBlur()`
+
+子输入框失去焦点,插件应用获得焦点。
+
+- **返回**: `boolean` - 是否成功。
+
+### `ztools.subInputSelect()`
+
+子输入框获得焦点并选中全部内容。
+
+- **返回**: `boolean` - 是否成功。
+
+### `ztools.removeSubInput()`
+
+移除(隐藏)子输入框。
+
+- **返回**: `Promise` - 是否成功。
+
+## 数据库 API
+
+插件拥有独立的数据库存储空间(Bucket),以插件名称隔离。
+
+### `ztools.db.put(doc)`
+
+保存数据。
+
+- **doc**: `object` - 文档对象(必须包含 `_id` 字段)。
+- **返回**: `object` - 保存后的文档对象(包含 `_id` 和 `_rev`)。
+
+### `ztools.db.get(id)`
+
+获取数据。
+
+- **id**: `string` - 文档 ID。
+- **返回**: `object | null` - 文档对象,不存在则返回 `null`。
+
+### `ztools.db.remove(docOrId)`
+
+删除数据。
+
+- **docOrId**: `object | string` - 要删除的文档对象(通常包含 `_id` 和 `_rev`)或文档 ID。
+- **返回**: `object` - 删除结果。
+
+### `ztools.db.bulkDocs(docs)`
+
+批量操作文档。
+
+- **docs**: `object[]` - 文档数组。
+- **返回**: `object[]` - 操作结果数组。
+
+### `ztools.db.allDocs(key)`
+
+获取所有文档或按 key 前缀查询。
+
+- **key**: `string` - (可选) 文档 ID 前缀,用于过滤。
+- **返回**: `object[]` - 文档数组。
+
+### `ztools.db.postAttachment(id, attachment, type)`
+
+为文档添加附件。
+
+- **id**: `string` - 文档 ID。
+- **attachment**: `string | Buffer` - 附件内容(base64 字符串或 Buffer)。
+- **type**: `string` - 附件 MIME 类型。
+- **返回**: `object` - 操作结果。
+
+### `ztools.db.getAttachment(id)`
+
+获取文档附件。
+
+- **id**: `string` - 文档 ID。
+- **返回**: `Buffer` - 附件内容。
+
+### `ztools.db.getAttachmentType(id)`
+
+获取文档附件的 MIME 类型。
+
+- **id**: `string` - 文档 ID。
+- **返回**: `string` - MIME 类型。
+
+### Promise API
+
+数据库 API 还提供了 Promise 版本,位于 `window.ztools.db.promises` 下,所有方法签名与同步版本相同,但返回 `Promise`。
+
+- `window.ztools.db.promises.put(doc)`
+- `window.ztools.db.promises.get(id)`
+- `window.ztools.db.promises.remove(docOrId)`
+- `window.ztools.db.promises.bulkDocs(docs)`
+- `window.ztools.db.promises.allDocs(key)`
+- `window.ztools.db.promises.postAttachment(id, attachment, type)`
+- `window.ztools.db.promises.getAttachment(id)`
+- `window.ztools.db.promises.getAttachmentType(id)`
+
+## dbStorage API
+
+类似 `localStorage` 的简化接口,用于简单的键值对存储。
+
+### `ztools.dbStorage.setItem(key, value)`
+
+保存数据。
+
+- **key**: `string` - 键名。
+- **value**: `any` - 要保存的数据(会自动序列化为 JSON)。
+
+### `ztools.dbStorage.getItem(key)`
+
+获取数据。
+
+- **key**: `string` - 键名。
+- **返回**: `any` - 数据内容,不存在则返回 `null`。
+
+### `ztools.dbStorage.removeItem(key)`
+
+删除数据。
+
+- **key**: `string` - 键名。
+
+## 动态 Feature API
+
+### `ztools.getFeatures(codes)`
+
+获取动态添加的 features。
+
+- **codes**: `string[]` - (可选) 指定要获取的 feature codes,不传则返回所有。
+- **返回**: `object[]` - Feature 数组。
+
+### `ztools.setFeature(feature)`
+
+设置动态 feature(如果已存在则更新)。
+
+- **feature**: `object` - Feature 对象。
+- **返回**: `boolean` - 是否成功。
+
+### `ztools.removeFeature(code)`
+
+删除指定的动态 feature。
+
+- **code**: `string` - Feature code。
+- **返回**: `boolean` - 是否成功。
+
+## 剪贴板 API
+
+### `ztools.clipboard.getHistory(page, pageSize, filter)`
+
+获取剪贴板历史记录。
+
+- **page**: `number` - 页码,从 1 开始。
+- **pageSize**: `number` - 每页数量。
+- **filter**: `string` - (可选) 过滤条件。
+- **返回**: `Promise` - 历史记录数据。
+
+### `ztools.clipboard.search(keyword)`
+
+搜索剪贴板历史。
+
+- **keyword**: `string` - 搜索关键词。
+- **返回**: `Promise` - 匹配的记录数组。
+
+### `ztools.clipboard.delete(id)`
+
+删除剪贴板记录。
+
+- **id**: `string` - 记录 ID。
+- **返回**: `Promise` - 是否成功。
+
+### `ztools.clipboard.clear(type)`
+
+清空剪贴板历史。
+
+- **type**: `string` - (可选) 类型过滤。
+- **返回**: `Promise` - 是否成功。
+
+### `ztools.clipboard.getStatus()`
+
+获取剪贴板状态。
+
+- **返回**: `Promise` - 状态信息。
+
+### `ztools.clipboard.write(id, shouldPaste)`
+
+将指定记录写入剪贴板。
+
+- **id**: `string` - 记录 ID。
+- **shouldPaste**: `boolean` - (可选) 是否同时模拟粘贴操作,默认 `true`。
+- **返回**: `Promise` - 是否成功。
+
+### `ztools.clipboard.writeContent(data, shouldPaste)`
+
+写入内容到剪贴板。
+
+- **data**: `object` - 数据对象。
+ - `type`: `'text' | 'image'` - 内容类型。
+ - `content`: `string` - 内容(文本或 base64 图片)。
+- **shouldPaste**: `boolean` - (可选) 是否同时模拟粘贴操作,默认 `true`。
+- **返回**: `Promise` - 是否成功。
+
+### `ztools.clipboard.updateConfig(config)`
+
+更新剪贴板配置。
+
+- **config**: `object` - 配置对象。
+- **返回**: `Promise` - 是否成功。
+
+### `ztools.clipboard.onChange(callback)`
+
+监听剪贴板变化事件。
+
+- **callback**: `(item: object) => void` - 回调函数,接收剪贴板变化项。
+
+### `ztools.copyText(text)`
+
+复制文本到剪贴板。
+
+- **text**: `string` - 要复制的文本。
+- **返回**: `boolean` - 是否成功。
+
+### `ztools.copyImage(image)`
+
+复制图片到剪贴板。
+
+- **image**: `string` - 图片 base64 Data URL 或文件路径。
+- **返回**: `boolean` - 是否成功。
+
+### `ztools.copyFile(filePath)`
+
+复制文件到剪贴板。
+
+- **filePath**: `string` - 文件路径。
+- **返回**: `boolean` - 是否成功。
+
+## 文件操作 API
+
+### `ztools.getPath(name)`
+
+获取系统路径。
+
+- **name**: `string` - 路径名称(如 `'home'`, `'desktop'`, `'documents'` 等)。
+- **返回**: `string` - 路径。
+
+### `ztools.showSaveDialog(options)`
+
+弹出文件保存对话框。
+
+- **options**: `SaveDialogOptions` - 对话框配置,与 Electron `showSaveDialogSync` 保持一致。
+- **返回**: `string | undefined` - 选择的路径。用户取消则返回 `undefined`。
+
+### `ztools.showOpenDialog(options)`
+
+弹出文件打开对话框。
+
+- **options**: `OpenDialogOptions` - 对话框配置,与 Electron `showOpenDialogSync` 保持一致。
+- **返回**: `string[] | undefined` - 选择的文件路径数组。用户取消则返回 `undefined`。
+
+### `ztools.screenCapture(callback)`
+
+屏幕截图,会进入截图模式,用户截图完执行回调函数。
+
+- **callback**: `(image: string) => void` - 截图完的回调函数。
+ - `image`: 截图的图像 base64 Data Url。
+
+## 窗口 API
+
+### `ztools.createBrowserWindow(url, options, callback)`
+
+创建独立窗口。
+
+- **url**: `string` - 窗口加载的 URL。
+- **options**: `object` - 窗口选项,与 Electron `BrowserWindow` 构造函数选项保持一致。
+- **callback**: `() => void` - (可选) 窗口加载完成后的回调函数。
+- **返回**: `Proxy | null` - 返回一个模拟 BrowserWindow 的 Proxy 对象,可用于调用窗口方法和访问属性。创建失败返回 `null`。
+
+### `ztools.sendToParent(channel, ...args)`
+
+发送消息到父窗口。
+
+- **channel**: `string` - 通道名称。
+- **args**: `any[]` - 要传递的参数。
+
+## 显示器 API
+
+### `ztools.getPrimaryDisplay()`
+
+获取主显示器信息。
+
+- **返回**: `object` - 显示器信息对象。
+
+### `ztools.getAllDisplays()`
+
+获取所有显示器。
+
+- **返回**: `object[]` - 显示器信息数组。
+
+### `ztools.getCursorScreenPoint()`
+
+获取鼠标光标的屏幕坐标。
+
+- **返回**: `object` - 坐标对象 `{ x: number, y: number }`。
+
+### `ztools.getDisplayNearestPoint(point)`
+
+获取最接近指定点的显示器。
+
+- **point**: `object` - 坐标对象 `{ x: number, y: number }`。
+- **返回**: `object` - 显示器信息对象。
+
+### `ztools.desktopCaptureSources(options)`
+
+获取桌面捕获源。
+
+- **options**: `object` - 捕获选项。
+- **返回**: `Promise` - 捕获源数组。
+
+### `ztools.dipToScreenPoint(point)`
+
+DIP 坐标转屏幕物理坐标。
+
+- **point**: `object` - DIP 坐标对象 `{ x: number, y: number }`。
+- **返回**: `object` - 屏幕物理坐标对象 `{ x: number, y: number }`。
+
+### `ztools.screenToDipPoint(point)`
+
+屏幕物理坐标转 DIP 坐标。
+
+- **point**: `object` - 屏幕物理坐标对象 `{ x: number, y: number }`。
+- **返回**: `object` - DIP 坐标对象 `{ x: number, y: number }`。
+
+### `ztools.dipToScreenRect(rect)`
+
+DIP 区域转屏幕物理区域。
+
+- **rect**: `object` - DIP 区域对象 `{ x: number, y: number, width: number, height: number }`。
+- **返回**: `object` - 屏幕物理区域对象 `{ x: number, y: number, width: number, height: number }`。
+
+## Shell API
+
+### `ztools.shellOpenExternal(url)`
+
+使用系统默认程序打开 URL。
+
+- **url**: `string` - 要打开的 URL。
+- **返回**: `boolean` - 是否成功。
+
+### `ztools.shellOpenPath(fullPath)`
+
+使用系统默认方式打开文件或文件夹。
+
+- **fullPath**: `string` - 文件或文件夹路径。
+- **返回**: `boolean` - 是否成功。
+
+### `ztools.shellShowItemInFolder(fullPath)`
+
+在文件管理器中显示文件。
+
+- **fullPath**: `string` - 文件路径。
+- **返回**: `boolean` - 是否成功。
+
+## 其他 API
+
+### `ztools.redirect(label, payload)`
+
+插件跳转。
+
+- **label**: `string` - 目标插件的 label。
+- **payload**: `any` - 传递的数据。
+- **返回**: `boolean` - 是否成功。
+
+### `ztools.http.setHeaders(headers)`
+
+设置 HTTP 请求头。
+
+- **headers**: `object` - 请求头对象。
+- **返回**: `boolean` - 是否成功。
+
+### `ztools.http.getHeaders()`
+
+获取当前请求头配置。
+
+- **返回**: `object` - 请求头对象。
+
+### `ztools.http.clearHeaders()`
+
+清除请求头配置。
+
+- **返回**: `boolean` - 是否成功。
+
+## AI API
+
+### `ztools.ai(option, streamCallback)`
+
+调用 AI 模型。支持流式和非流式两种模式。返回一个 PromiseLike 对象,同时具有 `abort()` 方法可中断请求。
+
+- **option**: `object` - AI 调用配置。
+- **streamCallback**: `(chunk: any) => void` - (可选) 流式回调函数。传入此参数时启用流式模式,每收到一段数据会调用此回调。
+- **返回**: `PromiseLike & { abort: () => void }` - 可 await 的 Promise 对象。
+ - 非流式模式:resolve 时返回 AI 响应数据。
+ - 流式模式:数据通过 `streamCallback` 逐步推送,Promise resolve 时表示完成。
+ - 调用 `.abort()` 可中断请求。
+
+#### 使用示例
+
+
+
+```
+// 非流式调用
+const result = await ztools.ai({ prompt: '你好' })
+
+// 流式调用
+const request = ztools.ai({ prompt: '你好' }, (chunk) => {
+ console.log('收到数据:', chunk)
+})
+await request
+
+// 中断请求
+request.abort()
+```
+
+### `ztools.allAiModels()`
+
+获取所有可用的 AI 模型列表。
+
+- **返回**: `Promise` - AI 模型数组。
+- **异常**: 获取失败时抛出 Error。
+
+
+
+# 发布与协作流程
+
+本页详细介绍 `ztools publish` 和 `ztools pull-contributions` 的内部机制与高级使用场景。如果你只想快速发布一次插件,先看 [第一个插件](https://ztoolscenter.github.io/ZTools-doc/first-plugin.html) 的发布章节就够了。
+
+## 整体模型
+
+ZTools 插件中心采用 **fork + PR** 模式:
+
+- 中心仓库:`ZToolsCenter/ZTools-plugins`
+- 每位作者在自己 GitHub 账号下 fork 一份
+- 每次发布走「复制工作目录 → 在 fork 的 `plugin/<插件 ID>` 分支上 commit → push → 开 PR」
+
+`ztools-plugin-cli` 把这套流程自动化了,并提供以下保障:
+
+- **增量发布**:每次发布只 fast-forward 追加一个 commit,不 force-push
+- **持久化缓存**:fork 在本地 clone 一次(`~/.config/ztools/ZTools-plugins/`),后续发布复用
+- **正确的 Add/Update 判定**:基于上游 main 的实际状态而非分支存在性
+- **协作友好**:审核者直推后通过 `pull-contributions` 三方合并回本地
+
+## 发布命令详解
+
+### `ztools publish` 完整流程
+
+
+
+```
+1. 校验 plugin.json + git 仓库 + 工作区干净
+2. CHANGELOG 检查(缺失当前版本节时交互式录入)
+3. GitHub OAuth 认证(首次会引导浏览器授权)
+4. Fork 中心仓库(不存在则自动创建)
+5. ensureForkClone:clone 或 fetch 本地 fork 缓存
+6. syncForkMain:调用 merge-upstream API 同步 fork main
+7. pluginExistsUpstream:探测上游 plugins// 目录决定 Add/Update
+8. prepareBranch:在 fork 分支上 checkout(已存在则复用,否则基于 upstream/main 新建)
+9. copyPluginFiles:把工作目录复制进 plugins//,自动忽略 node_modules、dist 等
+10. commitPluginChanges:组装智能 commit message 并提交(无变更则跳过)
+11. pushPluginBranch:普通 push,不 force(first push 用 -u)
+12. createPullRequest:复用已有 open PR 或开 draft PR
+13. tagLastPublishLocally:在你本地仓库 HEAD 打 ztools-last-publish 标签
+```
+
+### 智能 commit / PR 标题
+
+CLI 会读取你本地 `ztools-last-publish..HEAD` 之间的 commit subjects(即"自上次发布以来你写了哪些 commit"),按以下规则组装:
+
+| subjects 数量 | PR 标题 | fork 端 commit message |
+| :------------ | :--------------------------------- | :------------------------------------------- |
+| 0 | `Add/Update plugin <名称> v<版本>` | 同 PR 标题 |
+| 1 | `Add/Update plugin <名称> v<版本>` | **直接用你的 commit subject 原文** |
+| ≥2 | `Add/Update plugin <名称> v<版本>` | fallback 标题 + bullet list 列出所有 subject |
+
+**关键设计**:PR 标题永远是规整的 `Add/Update ...` 格式,方便维护者扫 PR 列表。详细语义放在 commit body 里,审核者点进 Commits tab 仍能看到你的原话。
+
+### Add vs Update 自动判定
+
+CLI 调 `GET /repos/ZToolsCenter/ZTools-plugins/contents/plugins/<插件 ID>`:
+
+- **404** → 上游没这个目录 → **Add**
+- **200** → 上游已合并过此插件 → **Update**
+
+> 这比"看 fork 分支是否存在"更准确:合并后分支被自动删除时仍能正确报告 Update。
+
+API 网络异常时退化为 Add(保守策略,避免误导)。
+
+## CHANGELOG.md 处理
+
+### 自动抽取
+
+如果你的项目根目录有 `CHANGELOG.md`,CLI 会按以下规则抓出"本次变更"内容注入到 PR description:
+
+1. 找到匹配当前版本的标题(支持 `## 0.1.0`、`## v0.1.0`、`## [0.1.0]`、`# 0.1.0` 等多种写法)
+2. 抽取该节内容到下一个同级或更高级标题之前
+3. 注入到 PR body 的「本次变更」段
+
+边界保护:`0.1.0` 不会误匹配 `0.10.0`,反之亦然。
+
+### 找不到当前版本节怎么办?
+
+CLI 在交互式终端会主动提示:
+
+
+
+```
+📝 未在 CHANGELOG.md 中找到 v0.3.0 的变更说明
+? 选择处理方式 ›
+❯ 现在编辑(打开 $EDITOR 录入本次变更)
+ 跳过(PR 中显示 placeholder,稍后在网页填)
+ 中止发布
+```
+
+**选 "现在编辑"** → CLI 启动 `$EDITOR`(默认 `vi`)打开预填模板的临时文件:
+
+
+
+```
+# 请简述 Demo Plugin v0.3.0 的本次变更。
+# 以 # 开头的行会被忽略;保存并关闭编辑器即可继续发布。
+# 直接关闭(不保存)或留空 → 跳过本次录入。
+#
+# 例如:
+# ### Added
+# - 新增批量导入
+#
+# ### Fixed
+# - 修复空输入崩溃
+```
+
+保存退出后,CLI 会再确认是否把这一节写回 `CHANGELOG.md`:
+
+
+
+```
+? 把这一节写入 CHANGELOG.md(同时自动 git commit)? › (Y/n)
+```
+
+确认 → 在 H1 之后、首个 H2 之前插入新版本节 → `git add CHANGELOG.md && git commit -m "chore(changelog): add vX.Y.Z entry"` → 继续发布主流程。
+
+### 非交互式 / CI 环境
+
+`stdin` 或 `stdout` 不是 TTY 时(CI、`< /dev/null` 重定向等),CLI 不会弹任何 prompt,自动按以下优先级填充 PR 的「本次变更」段:
+
+1. CHANGELOG.md 当前版本节(如有)
+2. 本地 commit subjects bullet list(如有)
+3. `` placeholder
+
+这样 `ztools publish < /dev/null` 在脚本里跑也不会卡。
+
+### 长 CHANGELOG 截断
+
+如果当前版本节找不到、且整个 CHANGELOG 文件超过 80 行,CLI 只截前 50 行注入并附 `_…(CHANGELOG 已截断,完整内容请见仓库)_` 标记,避免 PR description 过长。
+
+## PR description 模板
+
+每个 PR 自动生成的 description 包含 4 节:
+
+
+
+```
+## 插件信息
+
+- **名称**: ...
+- **插件ID**: ...
+- **版本**: ...
+- **描述**: ...
+- **作者**: ...
+- **类型**: 新增 / 更新
+
+## 本次变更
+
+{CHANGELOG 节 / commit subjects / placeholder}
+
+## 截图 / 演示
+
+
+
+## 自检清单
+
+- [ ] plugin.json 的 name / title / version / description / author 字段均已检查
+- [ ] 已移除调试日志、未使用文件、敏感信息(.env、token、密钥等)
+- [ ] 本次 PR 的 diff 仅涉及 `plugins//` 目录
+- [ ] 已在本地 ZTools 客户端实际加载并测试过此插件,主要功能正常
+- [ ] 同意以仓库声明的开源协议发布此插件
+```
+
+CLI 把所有可自动生成的部分都填好;**截图、自检清单勾选、Ready for review 切换**这三件事必须你手动到 PR 网页上完成。
+
+## 增量发布:远端只追加,不改写
+
+### 每次 publish 远端发生什么
+
+| 场景 | 远端分支变化 |
+| :--------------------- | :----------------------------------------------------------- |
+| 首次 publish | 创建 `plugin/` 分支,1 个 commit |
+| 后续 publish(有改动) | 在分支末尾 fast-forward 追加 1 个 commit,旧 commit SHA 不变 |
+| 后续 publish(无改动) | 不动分支;如果当前没有 open PR,会基于现有分支重开一个 |
+| 审核者直推后 publish | 被拒 → 提示跑 `pull-contributions` |
+
+### 为什么不 force-push
+
+force-push 会让审核者本地 checkout 的分支无法 fast-forward、PR 评论上下文错位、其他贡献者直推的 commit 被覆盖。Raycast 也是这么设计的。
+
+## `ztools pull-contributions`
+
+当审核者或其他贡献者直接在你的 PR 分支上推送过 commit,下次 `ztools publish` 会被拒:
+
+
+
+```
+❌ 发布失败
+错误: 远端 plugin/ 分支有你本地缓存中没有的新 commit。
+请先把这些改动同步回本地,再次发布:
+ $ ztools pull-contributions
+ $ ztools publish
+```
+
+### 工作机制(三方合并)
+
+1. 校验工作区干净 + 找到本地的 `ztools-last-publish` 标签作为 merge base
+2. 拉取 fork 端 plugin 分支最新内容
+3. 在你本地仓库新建临时分支 `ztools/pull-<时间戳>`,从 `ztools-last-publish` 出发
+4. 把 fork 当前文件镜像到临时分支并 commit "Pull contributions from PR"
+5. 切回原分支,`git merge --no-ff` 临时分支 → 触发 git 三方合并
+6. 不冲突的文件 → 自动并入;冲突文件 → 暴露给你手工解决
+
+### 合并示例
+
+设上次发布后:
+
+- **你本地**:在 `index.js` 末尾加了一行 `// my change`,对 `util.ts` 加了一个新函数
+- **fork PR 分支**:审核者在 `index.js` 末尾加了 `// reviewer fix`,新建了 `REVIEWER.md`
+
+`ztools pull-contributions` 之后:
+
+- `util.ts` :保留你的新函数(双方无冲突,自动合并)
+- `REVIEWER.md`:被并入你本地(双方无冲突,自动合并)
+- `index.js`:触发冲突,文件里出现 `<<<<<<< HEAD` / `=======` / `>>>>>>>` 标记,需要你手工编辑后 `git add` + `git commit`
+
+### 冲突时怎么办
+
+CLI 会停在合并未完成状态并提示:
+
+
+
+```
+❌ 合并出现冲突,需要你手工解决。
+ 当前处于合并未完成状态:
+ - 临时分支: ztools/pull-1700000000000
+
+ 解决步骤:
+ 1) 编辑冲突文件 → git add <文件>
+ 2) git commit 完成合并
+ 3) 然后再 ztools publish
+
+ 或者放弃此次拉取:git merge --abort && git branch -D ztools/pull-1700000000000
+```
+
+按提示走即可。解决冲突后再 `ztools publish` 就能继续追加发布。
+
+## 故障排查
+
+### 推送被拒:`refusing to allow an OAuth App to ... workflow`
+
+中心仓库 main 含 `.github/workflows/*.yml`,OAuth token 必须有 `workflow` scope。CLI 默认请求该 scope;如果你是从老版本升级上来的、本地 token 没这个 scope:
+
+
+
+```
+# 清掉旧 token,重新授权一次(这次会要求 workflow scope)
+rm ~/.config/ztools/cli-config.json
+ztools publish
+```
+
+### `merge-upstream` 422 错误
+
+通常是 fork main 已经偏离上游(你手动改过 fork 的 main)。处理:
+
+
+
+```
+# 进入 fork 缓存
+cd ~/.config/ztools/ZTools-plugins
+git checkout main
+git fetch upstream
+git reset --hard upstream/main
+git push -f origin main
+```
+
+或更简单:删除本地 fork 缓存 + 重新发布
+
+
+
+```
+rm -rf ~/.config/ztools/ZTools-plugins
+ztools publish
+```
+
+### 工作区有未提交改动
+
+CLI 拒绝带脏工作区发布:
+
+
+
+```
+❌ 发布失败
+错误: 工作区存在未提交的改动,请先 commit 或 discard 后再发布
+```
+
+按字面意思 `git commit` 或 `git restore` 处理即可。
+
+### 想看 CLI 的本地缓存仓库
+
+所有 fork 操作发生在 `~/.config/ztools/ZTools-plugins/`,里面是个普通 git 仓库,可以正常 `git log` / `git branch -a` 检查。
+
+### 想完全重置
+
+
+
+```
+rm -rf ~/.config/ztools/ # 清掉 token + fork 缓存
+git tag -d ztools-last-publish 2>/dev/null # 清掉本地 publish 标签
+```
+
+下次 `ztools publish` 会重走 OAuth + 重新 clone fork。
+
+## 相关链接
+
+- [ztools-plugin-cli (npm)](https://www.npmjs.com/package/@ztools-center/plugin-cli)
+- [ZTools-plugins 中心仓库](https://github.com/ZToolsCenter/ZTools-plugins)
+- [GitHub merge-upstream API](https://docs.github.com/en/rest/branches/branches#sync-a-fork-branch-with-the-upstream-repository)
\ No newline at end of file