From 4f7c909de363d96870c33255b5c17e945293e987 Mon Sep 17 00:00:00 2001
From: LiuDailin <920643082@qq.com>
Date: Fri, 3 Jul 2026 13:51:35 +0800
Subject: [PATCH 1/4] Add plugin Serious Reading v1.0.0
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- v0.1
- v0.2
- v1.0
- v1.0发布
- v1.0发布
- v1.0发布
---
plugins/serious-reading/.gitignore | 6 +
plugins/serious-reading/AGENTS.md | 137 +
plugins/serious-reading/CHANGELOG.md | 1 +
plugins/serious-reading/README.md | 156 +
plugins/serious-reading/index.html | 13 +
plugins/serious-reading/logo.svg | 4 +
plugins/serious-reading/package-lock.json | 3596 +++++++++++++++++
plugins/serious-reading/package.json | 41 +
plugins/serious-reading/plugin.json | 49 +
plugins/serious-reading/postcss.config.js | 6 +
plugins/serious-reading/preload/main.js | 349 ++
.../serious-reading/preload/package-lock.json | 57 +
plugins/serious-reading/preload/package.json | 11 +
plugins/serious-reading/preload/reader.js | 115 +
plugins/serious-reading/reader.html | 14 +
.../src/components/ui/button.tsx | 42 +
.../src/components/ui/checkbox.tsx | 23 +
.../src/components/ui/command.tsx | 72 +
.../src/components/ui/dialog.tsx | 66 +
.../src/components/ui/dropdown-menu.tsx | 46 +
.../src/components/ui/input.tsx | 19 +
.../src/components/ui/scroll-area.tsx | 30 +
.../src/components/ui/slider.tsx | 20 +
.../src/components/ui/switch.tsx | 20 +
plugins/serious-reading/src/lib/utils.ts | 11 +
plugins/serious-reading/src/main/App.tsx | 278 ++
.../src/main/components/BookCard.tsx | 59 +
.../src/main/components/ChapterDialog.tsx | 50 +
.../src/main/components/SearchDialog.tsx | 80 +
.../src/main/components/SettingsDialog.tsx | 293 ++
plugins/serious-reading/src/main/main.tsx | 10 +
plugins/serious-reading/src/main/theme.ts | 38 +
plugins/serious-reading/src/reader/App.tsx | 453 +++
.../src/reader/components/PdfView.tsx | 47 +
plugins/serious-reading/src/reader/main.tsx | 10 +
.../src/reader/usePagination.ts | 55 +
.../serious-reading/src/shared/constants.ts | 65 +
plugins/serious-reading/src/shared/ipc.ts | 31 +
plugins/serious-reading/src/shared/parser.ts | 155 +
plugins/serious-reading/src/shared/storage.ts | 131 +
plugins/serious-reading/src/shared/types.ts | 137 +
.../serious-reading/src/shared/ztools.d.ts | 140 +
.../serious-reading/src/styles/globals.css | 63 +
plugins/serious-reading/src/vite-env.d.ts | 1 +
plugins/serious-reading/tailwind.config.js | 46 +
plugins/serious-reading/tsconfig.json | 26 +
plugins/serious-reading/vite.config.ts | 47 +
...21\350\200\205\346\226\207\346\241\243.md" | 1642 ++++++++
48 files changed, 8761 insertions(+)
create mode 100644 plugins/serious-reading/.gitignore
create mode 100644 plugins/serious-reading/AGENTS.md
create mode 100644 plugins/serious-reading/CHANGELOG.md
create mode 100644 plugins/serious-reading/README.md
create mode 100644 plugins/serious-reading/index.html
create mode 100644 plugins/serious-reading/logo.svg
create mode 100644 plugins/serious-reading/package-lock.json
create mode 100644 plugins/serious-reading/package.json
create mode 100644 plugins/serious-reading/plugin.json
create mode 100644 plugins/serious-reading/postcss.config.js
create mode 100644 plugins/serious-reading/preload/main.js
create mode 100644 plugins/serious-reading/preload/package-lock.json
create mode 100644 plugins/serious-reading/preload/package.json
create mode 100644 plugins/serious-reading/preload/reader.js
create mode 100644 plugins/serious-reading/reader.html
create mode 100644 plugins/serious-reading/src/components/ui/button.tsx
create mode 100644 plugins/serious-reading/src/components/ui/checkbox.tsx
create mode 100644 plugins/serious-reading/src/components/ui/command.tsx
create mode 100644 plugins/serious-reading/src/components/ui/dialog.tsx
create mode 100644 plugins/serious-reading/src/components/ui/dropdown-menu.tsx
create mode 100644 plugins/serious-reading/src/components/ui/input.tsx
create mode 100644 plugins/serious-reading/src/components/ui/scroll-area.tsx
create mode 100644 plugins/serious-reading/src/components/ui/slider.tsx
create mode 100644 plugins/serious-reading/src/components/ui/switch.tsx
create mode 100644 plugins/serious-reading/src/lib/utils.ts
create mode 100644 plugins/serious-reading/src/main/App.tsx
create mode 100644 plugins/serious-reading/src/main/components/BookCard.tsx
create mode 100644 plugins/serious-reading/src/main/components/ChapterDialog.tsx
create mode 100644 plugins/serious-reading/src/main/components/SearchDialog.tsx
create mode 100644 plugins/serious-reading/src/main/components/SettingsDialog.tsx
create mode 100644 plugins/serious-reading/src/main/main.tsx
create mode 100644 plugins/serious-reading/src/main/theme.ts
create mode 100644 plugins/serious-reading/src/reader/App.tsx
create mode 100644 plugins/serious-reading/src/reader/components/PdfView.tsx
create mode 100644 plugins/serious-reading/src/reader/main.tsx
create mode 100644 plugins/serious-reading/src/reader/usePagination.ts
create mode 100644 plugins/serious-reading/src/shared/constants.ts
create mode 100644 plugins/serious-reading/src/shared/ipc.ts
create mode 100644 plugins/serious-reading/src/shared/parser.ts
create mode 100644 plugins/serious-reading/src/shared/storage.ts
create mode 100644 plugins/serious-reading/src/shared/types.ts
create mode 100644 plugins/serious-reading/src/shared/ztools.d.ts
create mode 100644 plugins/serious-reading/src/styles/globals.css
create mode 100644 plugins/serious-reading/src/vite-env.d.ts
create mode 100644 plugins/serious-reading/tailwind.config.js
create mode 100644 plugins/serious-reading/tsconfig.json
create mode 100644 plugins/serious-reading/vite.config.ts
create mode 100644 "plugins/serious-reading/zools\345\274\200\345\217\221\350\200\205\346\226\207\346\241\243.md"
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..cefd791a
--- /dev/null
+++ b/plugins/serious-reading/CHANGELOG.md
@@ -0,0 +1 @@
+V1.0 发布
\ No newline at end of file
diff --git a/plugins/serious-reading/README.md b/plugins/serious-reading/README.md
new file mode 100644
index 00000000..da4abc3d
--- /dev/null
+++ b/plugins/serious-reading/README.md
@@ -0,0 +1,156 @@
+# Serious Reading
+
+> 一款对待摸鱼阅读很严肃的阅读插件
+
+Serious Reading 是 [ZTools](https://github.com/ZToolsCenter/ZTools) 平台上的本地阅读器插件,支持 TXT / EPUB / PDF 三种格式。专为「在工作间隙低调阅读」设计——悬浮透明阅读窗、老板键伪装隐藏、自动翻页、全屏取色配色,让你严肃地摸鱼。
+
+## 核心特性
+
+### 阅读体验
+
+- **三格式支持** — TXT(自动编码检测 BOM/jschardet/GBK fallback)、EPUB(adm-zip 解析章节 + 封面提取)、PDF(pdfjs-dist canvas 渲染)
+- **书架管理** — 网格书架,EPUB 显示封面缩略图,TXT/PDF 显示书名色块;支持进度百分比展示、最近阅读历史
+- **章节跳转** — 右键菜单打开章节列表,支持标题搜索过滤
+- **全文搜索** — TXT 全文字符流搜索,关键字上下文高亮,分页加载更多,点击直接跳转到对应章节和位置
+- **百分比跳转** — 阅读窗右下角输入百分比,精确跳转到全书对应位置
+- **高度测量分页** — 按实际渲染高度自动分页,字号/行高/窗口尺寸变化时自动重排
+- **阅读进度记忆** — 自动保存每本书的章节、页码、字符偏移,下次打开恢复到上次位置
+
+### 摸鱼伪装
+
+- **透明留窗(Stealth)** — 阅读窗变为 `#00000001` 近全透明状态,窗口仍在但内容不可见,鼠标移回即可恢复
+- **真隐藏** — `win.hide()` 彻底消失,通过 ZTools 命令或全局快捷键恢复
+- **三功能触发器自定义** — 隐身切换 / 显示 / 真隐藏三个功能可分别绑定触发动作(双击、中键、右键、Esc、鼠标离开边缘、鼠标进入边缘),系统实时检测冲突
+- **自动翻页暂停** — Stealth 隐藏时可配置自动暂停翻页
+
+| 功能 | 默认触发 | 含义 |
+|------|----------|------|
+| 隐身(显→隐) | Esc / 双击 / 鼠标离开边缘 | 内容透明化,窗口保留 |
+| 显示(隐→显) | 中键 | 恢复内容可见 |
+| 真隐藏 | 右键 | 窗口彻底消失,命令恢复 |
+
+### 阅读窗操作
+
+- **原地拖拽移动** — 鼠标按住中间区域拖动,通过 IPC 调用 `win.setPosition` 实现(不使用 drag region,避免吞掉右键/中键事件)
+- **边缘缩放** — 四边 + 四角缩放把手,纯 JS 实现
+- **窗口位置记忆** — 自动保存阅读窗位置和尺寸
+- **翻页方式可配置** — 键盘 ←→ / 滚轮 / 点击左右 / PageUp Down / 空格 / 触摸滑动,可任意组合开关
+- **翻页过渡动画** — 无动画 / 滑动两种模式
+
+### 外观定制
+
+- **明暗主题** — 跟随系统 / 手动明亮 / 手动暗黑
+- **阅读配色** — 背景色、文字色自定义,支持**全屏截图取色**(调用 ZTools screenCapture)
+- **排版控制** — 字号(8-32px)、行高(1.0-3.0)、字重(50-1000)、字体选择(11 种中英文字体)、透明度(10%-100%)、清理空行
+- **设置实时生效** — 修改设置后自动推送到已打开的阅读窗,无需重新打开
+
+## 技术栈
+
+| 层 | 技术 |
+|----|------|
+| 平台 | ZTools(Electron) |
+| UI | React 18 + TypeScript + Vite(双入口:主窗 `index.html` / 悬浮阅读窗 `reader.html`) |
+| 样式 | Tailwind CSS + shadcn/ui(Radix Primitives) |
+| PDF | pdfjs-dist(前端 canvas 渲染) |
+| TXT 编码 | BOM 检测 + jschardet + iconv-lite(preload 层,不编译) |
+| EPUB | adm-zip 解析(提取章节 + 封面) |
+| 存储 | `ztools.dbStorage`(键值对)+ `ztools.db`(书架文档 / 封面附件) |
+
+## 架构
+
+```
+主窗 (index.html) 阅读窗 (reader.html)
+ ├─ 书架网格 ├─ 分页渲染(高度测量)
+ ├─ 设置面板 ├─ Stealth 透明伪装
+ ├─ 章节跳转 Dialog ├─ PDF canvas 渲染
+ ├─ 全文搜索 Dialog ├─ 纯 JS 窗口拖拽/缩放
+ ├─ 最近阅读历史 ├─ 自动翻页
+ └─ preload/main.js └─ preload/reader.js
+ ├─ 文件读取/解码 ├─ 文件读取/解码
+ ├─ EPUB 解析 └─ IPC 回传(进度/隐藏/窗口操作)
+ ├─ 阅读窗 BrowserWindow 管理
+ └─ IPC 转发(真隐藏/窗口移动)
+```
+
+阅读窗自闭环设计:翻页、切章、存进度直接调用 preload 读取文件,不回主窗中转。仅真隐藏和窗口位置保存需经 IPC 回主窗。
+
+## 目录结构
+
+```
+serious-reading-zt/
+├── plugin.json # ZTools 插件配置
+├── index.html # 主窗入口(书架/设置)
+├── reader.html # 悬浮阅读窗入口
+├── logo.svg # 插件 Logo
+├── preload/ # 不编译的 CommonJS(+ node_modules)
+│ ├── main.js # 主窗 preload(文件读取/EPUB解析/阅读窗管理)
+│ ├── reader.js # 阅读窗 preload(文件读取/IPC 回传)
+│ └── package.json # adm-zip / iconv-lite / jschardet
+├── src/
+│ ├── main/ # 主窗 React
+│ │ ├── App.tsx
+│ │ ├── main.tsx
+│ │ ├── theme.ts
+│ │ └── components/ # BookCard / SettingsDialog / ChapterDialog / SearchDialog
+│ ├── reader/ # 阅读窗 React
+│ │ ├── App.tsx
+│ │ ├── main.tsx
+│ │ ├── usePagination.ts
+│ │ └── components/ # PdfView
+│ ├── shared/ # 共享模块
+│ │ ├── types.ts # 类型定义
+│ │ ├── constants.ts # 默认设置/触发器选项/字体列表
+│ │ ├── parser.ts # TXT/EPUB/PDF 解析 + 全文搜索 + HTML 渲染
+│ │ ├── storage.ts # 书架/进度/设置/封面的存储读写
+│ │ ├── ipc.ts # IPC 通道常量
+│ │ └── ztools.d.ts # ZTools API 类型声明
+│ ├── components/ui/ # shadcn/ui 基础组件
+│ └── styles/globals.css
+├── vite.config.ts # 双入口构建 + logo 拷贝
+├── tailwind.config.js
+├── tsconfig.json
+└── package.json
+```
+
+## 开发
+
+```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
+```
+
+构建后 `dist/` 包含 `index.html`、`reader.html` 及打包的 assets,preload 目录保持原样不参与编译。
+
+## 打包发布
+
+```bash
+# 安装 ZTools 插件 CLI
+npm install -g @ztools-center/plugin-cli
+
+# 初始化 Git 并提交
+git init && git add . && git commit -m "Initial commit"
+
+# 发布到 ZTools 插件中心
+ztools publish
+```
+
+> **注意**:preload 依赖(`iconv-lite`、`adm-zip`、`jschardet`)必须原样放在 `preload/node_modules/` 且不要压缩/混淆;ZTools 要求 preload 代码清晰可读。
+
+## 触发命令
+
+| 命令 | 说明 |
+|------|------|
+| `阅读` / `书架` / `serious` | 打开阅读器书架 |
+| `继续阅读` | 继续上次阅读 |
+| `显示阅读器` / `show` | 显示阅读窗 |
+| `切换阅读器` / `toggle` | 切换阅读窗显隐 |
+| 拖入 TXT/EPUB/PDF 文件 | 直接打开阅读 |
diff --git a/plugins/serious-reading/index.html b/plugins/serious-reading/index.html
new file mode 100644
index 00000000..9d1c24f2
--- /dev/null
+++ b/plugins/serious-reading/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Serious Reading
+
+
+
+
+
+
\ 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..e56e76be
--- /dev/null
+++ b/plugins/serious-reading/package-lock.json
@@ -0,0 +1,3596 @@
+{
+ "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",
+ "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"
+ }
+ },
+ "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/@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/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..4e1bab9d
--- /dev/null
+++ b/plugins/serious-reading/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "serious-reading-zt",
+ "version": "1.0.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",
+ "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"
+ }
+}
\ No newline at end of file
diff --git a/plugins/serious-reading/plugin.json b/plugins/serious-reading/plugin.json
new file mode 100644
index 00000000..778597fd
--- /dev/null
+++ b/plugins/serious-reading/plugin.json
@@ -0,0 +1,49 @@
+{
+ "name": "serious-reading",
+ "title": "Serious Reading",
+ "description": "一款对待摸鱼阅读很严肃的阅读插件",
+ "version": "1.0.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": "用 Serious Reading 打开",
+ "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..38239f76
--- /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 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 = 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 = 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) {
+ 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 { ztools.dbStorage.setItem(DB_PREFIX + 'progress/' + pg.filePath, pg) } catch (e2) {}
+ // 更新书架书籍的 lastChapter
+ try {
+ const doc = 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
+ 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 { 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()
+ 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..8c111aee
--- /dev/null
+++ b/plugins/serious-reading/preload/reader.js
@@ -0,0 +1,115 @@
+/**
+ * 阅读窗 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 = /
]*>([^<]+)<\/dc:title>/)
+ return {
+ title: titleMatch ? titleMatch[1] : filePath.split(/[\\/]/).pop(),
+ filePath: filePath, format: 'epub', chapters: chapters, totalChapters: chapters.length,
+ }
+ } catch (e) { return null }
+ },
+}
+
+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 (typeof ztools !== 'undefined' && ztools.sendToParent) {
+ 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..5bf0870d
--- /dev/null
+++ b/plugins/serious-reading/src/main/App.tsx
@@ -0,0 +1,278 @@
+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
+ const svc = window.services
+ try {
+ if (format === 'txt') {
+ const txt = svc?.readTxt(filePath)
+ if (txt) book = parseTxt(txt, filePath)
+ } else if (format === 'epub') {
+ const eb = svc?.readEpub(filePath)
+ if (eb) book = buildEpub(eb, 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') {
+ // save cover (extracted from EBook.cover via services.readEpub already returns cover buffer)
+ const eb = svc?.readEpub(filePath)
+ if (eb?.cover) {
+ saveCover(sb.id, eb.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 (
+
+
+
+
+ Serious
+ Reading
+
+
+
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..c5d9592a
--- /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++) {
+ if ((chapters[i].charOffset ?? 0) <= offset && offset < (chapters[i].charOffset ?? 0) + (chapters[i].charLength ?? 0)) {
+ chapter = i
+ break
+ }
+ if (i === chapters.length - 1) chapter = i
+ }
+ 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..dc196e13
--- /dev/null
+++ b/plugins/serious-reading/src/reader/App.tsx
@@ -0,0 +1,453 @@
+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(
+ () => (book && chapter ? renderChapterHtml(chapter.content, book.format, settings.reader.cleanEmptyLines) : ''),
+ [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 }[] = []
+ measure.childNodes.forEach((node) => {
+ if (node.nodeType === 3 && node.textContent?.trim()) {
+ const span = document.createElement('span')
+ span.textContent = node.textContent
+ measure.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 cs = getComputedStyle(el)
+ nodes.push({ height: el.offsetHeight + parseFloat(cs.marginTop) + parseFloat(cs.marginBottom), 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 : ['本章无内容
'])
+ 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..82e929a2
--- /dev/null
+++ b/plugins/serious-reading/src/shared/parser.ts
@@ -0,0 +1,155 @@
+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(剥离 epub 的 img/style/script) */
+export function renderChapterHtml(content: string, format: BookFormat, cleanEmptyLines = false): string {
+ if (format === 'epub') {
+ return content
+ .replace(/ ]*>/g, '')
+ .replace(/