diff --git a/plugins/top-screenshot/.gitignore b/plugins/top-screenshot/.gitignore
new file mode 100644
index 00000000..3cf2ec54
--- /dev/null
+++ b/plugins/top-screenshot/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+dist/
+coverage/
+release/
+.superpowers/
+.worktrees/
+*.log
+.DS_Store
\ No newline at end of file
diff --git a/plugins/top-screenshot/README.md b/plugins/top-screenshot/README.md
new file mode 100644
index 00000000..9d9b5769
--- /dev/null
+++ b/plugins/top-screenshot/README.md
@@ -0,0 +1,72 @@
+# 截图置顶
+
+截图置顶是一个 ZTools 插件,用于快速框选屏幕区域,并将选中的画面以独立置顶窗口显示。适合在查看资料、对照页面、记录界面局部内容时使用。
+
+## 功能特点
+
+- 启动后直接进入截图流程。
+- 框选区域后自动生成置顶截图。
+- 支持同时保留多张置顶截图。
+- 支持拖动移动置顶截图。
+- 支持鼠标滚轮缩放截图。
+- 支持按 Esc 关闭当前置顶截图窗口。
+
+## 使用方式
+
+1. 在 ZTools 中安装插件包。
+2. 搜索并运行“截图置顶”。
+3. 拖拽鼠标框选需要置顶的区域。
+4. 松开鼠标后,选区会生成置顶截图。
+5. 拖动截图窗口可移动位置。
+6. 滚动鼠标滚轮可缩放截图。
+7. 聚焦截图窗口后按 Esc 可关闭该窗口。
+
+## 开发命令
+
+```bash
+npm install
+npm test
+npm run build
+npm run package
+```
+
+命令说明:
+
+- `npm install`:安装项目依赖。
+- `npm test`:运行测试。
+- `npm run build`:构建插件。
+- `npm run package`:生成可安装的插件包。
+
+## 打包教程
+
+1. 安装依赖:
+
+ ```bash
+ npm install
+ ```
+
+2. 运行测试:
+
+ ```bash
+ npm test
+ ```
+
+3. 执行打包:
+
+ ```bash
+ npm run package
+ ```
+
+4. 打包完成后,找到生成的 `.zpx` 插件包。
+
+5. 在 ZTools 中导入该 `.zpx` 文件完成安装或更新。
+
+## 插件配置
+
+插件信息和搜索命令在 `plugin.json` 中配置。
+
+当前支持的搜索词:
+
+- 截图置顶
+- 截图
+- 置顶截图
diff --git a/plugins/top-screenshot/assets/logo.png b/plugins/top-screenshot/assets/logo.png
new file mode 100644
index 00000000..6d9e05a1
Binary files /dev/null and b/plugins/top-screenshot/assets/logo.png differ
diff --git a/plugins/top-screenshot/docs/superpowers/plans/2026-06-05-screenshot-pin-implementation.md b/plugins/top-screenshot/docs/superpowers/plans/2026-06-05-screenshot-pin-implementation.md
new file mode 100644
index 00000000..7fa7620d
--- /dev/null
+++ b/plugins/top-screenshot/docs/superpowers/plans/2026-06-05-screenshot-pin-implementation.md
@@ -0,0 +1,1768 @@
+# 截图置顶 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 构建一个 TypeScript 编写的 ztools 插件,启动后立即截图,并把框选区域以多窗口形式原地置顶。
+
+**Architecture:** 使用 Vue + Vite + TypeScript 渲染插件界面、截图覆盖层和置顶图片窗口。使用 ztools 的 `createBrowserWindow`、`desktopCaptureSources` 和显示器 API 创建全屏截图窗口与透明置顶窗口;用 localStorage 在同源窗口之间传递截图会话和置顶窗口状态。
+
+**Tech Stack:** Vue 3、TypeScript、Vite、Vitest、ZTools 插件 API、Electron BrowserWindow 选项。
+
+---
+
+## Source Notes
+
+ZTools 文档说明 `ztools.screenCapture(callback)` 只返回截图 Data URL,不返回截图区域坐标。为了满足“在图片对应位置原地置顶”,实现使用 `ztools.desktopCaptureSources(options)` 获取屏幕图像,用自定义全屏覆盖层记录选区 bounds,再用 `ztools.createBrowserWindow(url, options, callback)` 创建置顶图片窗口。
+
+## File Structure
+
+- `plugin.json` — ztools 插件元信息,定义入口、preload 和搜索命令。
+- `package.json` — npm 脚本、Vue/Vite/TypeScript/Vitest 依赖。
+- `index.html` — Vite HTML 入口。
+- `vite.config.ts` — Vue 应用构建配置。
+- `vite.preload.config.ts` — preload TypeScript 构建配置。
+- `tsconfig.json` — TypeScript 配置。
+- `.gitignore` — 忽略依赖、构建产物和可视化 brainstorming 临时文件。
+- `assets/logo.png` — ztools 插件图标。
+- `preload/index.ts` — preload 入口,保留 ztools 注入环境。
+- `src/main.ts` — Vue 应用入口。
+- `src/App.vue` — 根据 hash 路由渲染 launcher、capture、pin 三类视图。
+- `src/styles.css` — 全局样式、透明窗口 body 样式。
+- `src/types/ztools.ts` — 最小 ZTools API 类型声明。
+- `src/core/geometry.ts` — 选区、窗口装饰、中心缩放、拖动 bounds 计算。
+- `src/core/storage.ts` — 截图会话和置顶窗口状态的 localStorage 读写。
+- `src/core/routes.ts` — hash 路由解析与同源窗口 URL 生成。
+- `src/core/ztoolsBridge.ts` — ZTools API 包装、显示器截图源匹配、窗口创建。
+- `src/core/crop.ts` — canvas 裁剪屏幕截图。
+- `src/views/LauncherView.vue` — ztools 启动后创建截图会话和覆盖层窗口。
+- `src/views/CaptureView.vue` — 显示全屏截图覆盖层,处理拖拽选区。
+- `src/views/PinView.vue` — 渲染置顶截图,处理拖动、中心缩放和 Esc 关闭。
+- `tests/geometry.test.ts` — 几何纯函数测试。
+- `tests/storage.test.ts` — localStorage 状态测试。
+- `tests/routes.test.ts` — hash 路由与 URL 生成测试。
+- `tests/ztoolsBridge.test.ts` — 显示器截图源匹配测试。
+
+---
+
+### Task 1: Scaffold Vue + TypeScript plugin project
+
+**Files:**
+- Create: `.gitignore`
+- Create: `package.json`
+- Create: `tsconfig.json`
+- Create: `vite.config.ts`
+- Create: `vite.preload.config.ts`
+- Create: `index.html`
+- Create: `src/main.ts`
+- Create: `src/App.vue`
+- Create: `src/styles.css`
+- Create: `preload/index.ts`
+- Create: `scripts/create-logo.mjs`
+- Create: `plugin.json`
+- Create: `tests/geometry.test.ts`
+
+- [ ] **Step 1: Write a failing smoke test**
+
+Create `tests/geometry.test.ts` with this content:
+
+```ts
+import { describe, expect, it } from 'vitest';
+import { normalizeRect } from '../src/core/geometry';
+
+describe('normalizeRect', () => {
+ it('normalizes a drag from bottom-right to top-left', () => {
+ expect(normalizeRect({ x: 30, y: 40 }, { x: 10, y: 15 })).toEqual({
+ x: 10,
+ y: 15,
+ width: 20,
+ height: 25,
+ });
+ });
+});
+```
+
+- [ ] **Step 2: Add package and TypeScript/Vite config**
+
+Create `.gitignore`:
+
+```gitignore
+node_modules/
+dist/
+coverage/
+.superpowers/
+*.log
+.DS_Store
+```
+
+Create `package.json`:
+
+```json
+{
+ "name": "top-screenshot-ztools-plugin",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 127.0.0.1",
+ "build": "vue-tsc --noEmit && vite build && vite build --config vite.preload.config.ts",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "create:logo": "node scripts/create-logo.mjs"
+ },
+ "dependencies": {
+ "@vitejs/plugin-vue": "latest",
+ "@vue/test-utils": "latest",
+ "jsdom": "latest",
+ "typescript": "latest",
+ "vite": "latest",
+ "vitest": "latest",
+ "vue": "latest",
+ "vue-tsc": "latest"
+ },
+ "devDependencies": {
+ "@types/node": "latest"
+ }
+}
+```
+
+Create `tsconfig.json`:
+
+```json
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "jsx": "preserve",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "types": ["vitest/globals", "node"],
+ "skipLibCheck": true,
+ "noEmit": true
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.vue",
+ "preload/**/*.ts",
+ "tests/**/*.ts",
+ "vite.config.ts",
+ "vite.preload.config.ts"
+ ]
+}
+```
+
+Create `vite.config.ts`:
+
+```ts
+import vue from '@vitejs/plugin-vue';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [vue()],
+ test: {
+ environment: 'jsdom',
+ globals: true,
+ },
+});
+```
+
+Create `vite.preload.config.ts`:
+
+```ts
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ build: {
+ emptyOutDir: false,
+ lib: {
+ entry: 'preload/index.ts',
+ formats: ['cjs'],
+ fileName: () => 'preload.cjs',
+ },
+ outDir: 'dist',
+ rollupOptions: {
+ external: ['electron'],
+ },
+ },
+});
+```
+
+Create `index.html`:
+
+```html
+
+
+
+
+
+ 截图置顶
+
+
+
+
+
+
+```
+
+Create `preload/index.ts`:
+
+```ts
+export {};
+```
+
+Create `scripts/create-logo.mjs`:
+
+```js
+import { mkdirSync, writeFileSync } from 'node:fs';
+
+mkdirSync('assets', { recursive: true });
+const pngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAfElEQVR4nO3QQQ0AIBDAMMC/5+ONAvZoFSzZnR1JkpyeA7g1wABggAHAAAOAAQYAAwwABhgADDAAGGAAAHAAMMAAYIABwAADgAEGAAcAAAwwABhgADDAAGGAAAHAAMMAAYIABwAADgAEGAAcAAAwwABhgADDAAGGAAcJ+UAQAA//YCrvCk7QAAAABJRU5ErkJggg==';
+writeFileSync('assets/logo.png', Buffer.from(pngBase64, 'base64'));
+```
+
+Create `plugin.json`:
+
+```json
+{
+ "name": "top-screenshot",
+ "title": "截图置顶",
+ "description": "框选屏幕区域并原地置顶显示截图",
+ "version": "0.1.0",
+ "main": "dist/index.html",
+ "logo": "assets/logo.png",
+ "preload": "dist/preload.cjs",
+ "features": [
+ {
+ "code": "capture-pin",
+ "explain": "截图并置顶",
+ "cmds": ["截图置顶", "截图", "置顶截图"]
+ }
+ ]
+}
+```
+
+- [ ] **Step 3: Add temporary app shell**
+
+Create `src/main.ts`:
+
+```ts
+import { createApp } from 'vue';
+import App from './App.vue';
+import './styles.css';
+
+createApp(App).mount('#app');
+```
+
+Create `src/App.vue`:
+
+```vue
+
+
+
+ {{ message }}
+
+```
+
+Create `src/styles.css`:
+
+```css
+html,
+body,
+#app {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+
+body {
+ background: transparent;
+}
+
+.app-shell {
+ display: grid;
+ min-height: 100vh;
+ place-items: center;
+ color: #e5e7eb;
+ background: #111827;
+}
+```
+
+- [ ] **Step 4: Install dependencies and generate logo**
+
+Run:
+
+```bash
+npm install
+npm run create:logo
+```
+
+Expected: `node_modules` exists, `package-lock.json` exists, and `assets/logo.png` exists.
+
+- [ ] **Step 5: Run test to verify it fails because geometry does not exist**
+
+Run:
+
+```bash
+npm test -- tests/geometry.test.ts
+```
+
+Expected: FAIL with a module resolution error for `../src/core/geometry`.
+
+- [ ] **Step 6: Commit scaffold**
+
+```bash
+git add .gitignore package.json package-lock.json tsconfig.json vite.config.ts vite.preload.config.ts index.html preload/index.ts scripts/create-logo.mjs plugin.json assets/logo.png src/main.ts src/App.vue src/styles.css tests/geometry.test.ts
+git commit -m "chore: scaffold ztools screenshot plugin"
+```
+
+---
+
+### Task 2: Implement geometry primitives with tests
+
+**Files:**
+- Create: `src/core/geometry.ts`
+- Modify: `tests/geometry.test.ts`
+
+- [ ] **Step 1: Replace geometry tests with full expected behavior**
+
+Replace `tests/geometry.test.ts` with:
+
+```ts
+import { describe, expect, it } from 'vitest';
+import {
+ clampScale,
+ imageBoundsForScale,
+ isValidSelection,
+ normalizeRect,
+ outerBoundsForImage,
+ scaleFromWheelDelta,
+ translateRect,
+} from '../src/core/geometry';
+
+describe('geometry', () => {
+ it('normalizes a drag from bottom-right to top-left', () => {
+ expect(normalizeRect({ x: 30, y: 40 }, { x: 10, y: 15 })).toEqual({
+ x: 10,
+ y: 15,
+ width: 20,
+ height: 25,
+ });
+ });
+
+ it('rejects tiny selections', () => {
+ expect(isValidSelection({ x: 0, y: 0, width: 7, height: 20 })).toBe(false);
+ expect(isValidSelection({ x: 0, y: 0, width: 20, height: 7 })).toBe(false);
+ expect(isValidSelection({ x: 0, y: 0, width: 8, height: 8 })).toBe(true);
+ });
+
+ it('adds frame space around an image window', () => {
+ expect(outerBoundsForImage({ x: 100, y: 80, width: 200, height: 120 }, 6)).toEqual({
+ x: 94,
+ y: 74,
+ width: 212,
+ height: 132,
+ });
+ });
+
+ it('scales around the current image center', () => {
+ expect(imageBoundsForScale({ x: 100, y: 80, width: 200, height: 120 }, 1.5)).toEqual({
+ x: 50,
+ y: 50,
+ width: 300,
+ height: 180,
+ });
+ });
+
+ it('clamps scale and applies wheel direction', () => {
+ expect(clampScale(0.1)).toBe(0.3);
+ expect(clampScale(4)).toBe(3);
+ expect(scaleFromWheelDelta(1, -100)).toBe(1.1);
+ expect(scaleFromWheelDelta(1, 100)).toBe(0.9);
+ });
+
+ it('translates a rectangle by a delta', () => {
+ expect(translateRect({ x: 10, y: 20, width: 30, height: 40 }, 5, -8)).toEqual({
+ x: 15,
+ y: 12,
+ width: 30,
+ height: 40,
+ });
+ });
+});
+```
+
+- [ ] **Step 2: Run tests to verify failure**
+
+Run:
+
+```bash
+npm test -- tests/geometry.test.ts
+```
+
+Expected: FAIL with missing exports from `src/core/geometry.ts`.
+
+- [ ] **Step 3: Implement geometry**
+
+Create `src/core/geometry.ts`:
+
+```ts
+export interface Point {
+ x: number;
+ y: number;
+}
+
+export interface Rect {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+}
+
+export const MIN_SELECTION_SIZE = 8;
+export const MIN_SCALE = 0.3;
+export const MAX_SCALE = 3;
+export const SCALE_STEP = 0.1;
+
+const round = (value: number) => Math.round(value * 100) / 100;
+
+export function normalizeRect(start: Point, end: Point): Rect {
+ const x = Math.min(start.x, end.x);
+ const y = Math.min(start.y, end.y);
+ return {
+ x,
+ y,
+ width: Math.abs(end.x - start.x),
+ height: Math.abs(end.y - start.y),
+ };
+}
+
+export function isValidSelection(rect: Rect): boolean {
+ return rect.width >= MIN_SELECTION_SIZE && rect.height >= MIN_SELECTION_SIZE;
+}
+
+export function outerBoundsForImage(imageBounds: Rect, frameSize: number): Rect {
+ return {
+ x: Math.round(imageBounds.x - frameSize),
+ y: Math.round(imageBounds.y - frameSize),
+ width: Math.round(imageBounds.width + frameSize * 2),
+ height: Math.round(imageBounds.height + frameSize * 2),
+ };
+}
+
+export function imageBoundsForScale(currentImageBounds: Rect, nextScale: number): Rect {
+ const centerX = currentImageBounds.x + currentImageBounds.width / 2;
+ const centerY = currentImageBounds.y + currentImageBounds.height / 2;
+ const baseWidth = currentImageBounds.width;
+ const baseHeight = currentImageBounds.height;
+ const currentScale = Math.max(currentImageBounds.width / baseWidth, 1);
+ const ratio = nextScale / currentScale;
+ const width = round(currentImageBounds.width * ratio);
+ const height = round(currentImageBounds.height * ratio);
+ return {
+ x: round(centerX - width / 2),
+ y: round(centerY - height / 2),
+ width,
+ height,
+ };
+}
+
+export function imageBoundsForOriginalSize(center: Point, originalSize: Pick, scale: number): Rect {
+ const width = round(originalSize.width * scale);
+ const height = round(originalSize.height * scale);
+ return {
+ x: round(center.x - width / 2),
+ y: round(center.y - height / 2),
+ width,
+ height,
+ };
+}
+
+export function rectCenter(rect: Rect): Point {
+ return {
+ x: rect.x + rect.width / 2,
+ y: rect.y + rect.height / 2,
+ };
+}
+
+export function clampScale(scale: number): number {
+ return round(Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale)));
+}
+
+export function scaleFromWheelDelta(currentScale: number, deltaY: number): number {
+ const direction = deltaY < 0 ? 1 : -1;
+ return clampScale(currentScale + direction * SCALE_STEP);
+}
+
+export function translateRect(rect: Rect, deltaX: number, deltaY: number): Rect {
+ return {
+ x: round(rect.x + deltaX),
+ y: round(rect.y + deltaY),
+ width: rect.width,
+ height: rect.height,
+ };
+}
+```
+
+- [ ] **Step 4: Run tests to verify pass**
+
+Run:
+
+```bash
+npm test -- tests/geometry.test.ts
+```
+
+Expected: PASS.
+
+- [ ] **Step 5: Commit geometry**
+
+```bash
+git add src/core/geometry.ts tests/geometry.test.ts
+git commit -m "test: add screenshot geometry primitives"
+```
+
+---
+
+### Task 3: Implement routes and storage
+
+**Files:**
+- Create: `src/core/routes.ts`
+- Create: `src/core/storage.ts`
+- Create: `tests/routes.test.ts`
+- Create: `tests/storage.test.ts`
+
+- [ ] **Step 1: Write route tests**
+
+Create `tests/routes.test.ts`:
+
+```ts
+import { describe, expect, it } from 'vitest';
+import { buildPluginUrl, parseRoute } from '../src/core/routes';
+
+describe('routes', () => {
+ it('parses launcher route by default', () => {
+ expect(parseRoute('')).toEqual({ view: 'launcher', params: new URLSearchParams() });
+ });
+
+ it('parses capture route params', () => {
+ const route = parseRoute('#/capture?sessionId=s1&displayId=d1');
+ expect(route.view).toBe('capture');
+ expect(route.params.get('sessionId')).toBe('s1');
+ expect(route.params.get('displayId')).toBe('d1');
+ });
+
+ it('builds same-origin plugin URLs', () => {
+ const url = buildPluginUrl('pin', { id: 'abc 123' }, 'file:///D:/plugin/dist/index.html');
+ expect(url).toBe('file:///D:/plugin/dist/index.html#/pin?id=abc+123');
+ });
+});
+```
+
+Create `tests/storage.test.ts`:
+
+```ts
+import { describe, expect, it } from 'vitest';
+import type { CaptureSession, PinWindowState } from '../src/core/storage';
+import {
+ loadCaptureSession,
+ loadPinWindow,
+ markCaptureSessionCompleted,
+ saveCaptureSession,
+ savePinWindow,
+} from '../src/core/storage';
+
+function createMemoryStorage(): Storage {
+ const values = new Map();
+ return {
+ get length() {
+ return values.size;
+ },
+ clear: () => values.clear(),
+ getItem: (key: string) => values.get(key) ?? null,
+ key: (index: number) => Array.from(values.keys())[index] ?? null,
+ removeItem: (key: string) => values.delete(key),
+ setItem: (key: string, value: string) => values.set(key, value),
+ };
+}
+
+describe('storage', () => {
+ it('saves and loads capture sessions', () => {
+ const storage = createMemoryStorage();
+ const session: CaptureSession = {
+ id: 'session-1',
+ createdAt: 10,
+ completed: false,
+ displays: [
+ {
+ displayId: '1',
+ bounds: { x: 0, y: 0, width: 800, height: 600 },
+ imageDataUrl: 'data:image/png;base64,aaa',
+ scaleFactor: 1,
+ },
+ ],
+ };
+
+ saveCaptureSession(storage, session);
+ expect(loadCaptureSession(storage, 'session-1')).toEqual(session);
+
+ markCaptureSessionCompleted(storage, 'session-1');
+ expect(loadCaptureSession(storage, 'session-1')?.completed).toBe(true);
+ });
+
+ it('saves and loads pin windows', () => {
+ const storage = createMemoryStorage();
+ const state: PinWindowState = {
+ id: 'pin-1',
+ imageDataUrl: 'data:image/png;base64,bbb',
+ originalBounds: { x: 10, y: 20, width: 100, height: 80 },
+ currentBounds: { x: 10, y: 20, width: 100, height: 80 },
+ scale: 1,
+ createdAt: 100,
+ lastActiveAt: 100,
+ };
+
+ savePinWindow(storage, state);
+ expect(loadPinWindow(storage, 'pin-1')).toEqual(state);
+ });
+});
+```
+
+- [ ] **Step 2: Run tests to verify failure**
+
+Run:
+
+```bash
+npm test -- tests/routes.test.ts tests/storage.test.ts
+```
+
+Expected: FAIL with missing modules for routes and storage.
+
+- [ ] **Step 3: Implement routes**
+
+Create `src/core/routes.ts`:
+
+```ts
+export type AppView = 'launcher' | 'capture' | 'pin';
+
+export interface AppRoute {
+ view: AppView;
+ params: URLSearchParams;
+}
+
+export function parseRoute(hash: string = window.location.hash): AppRoute {
+ const cleanHash = hash.replace(/^#\/?/, '');
+ const [viewName, query = ''] = cleanHash.split('?');
+
+ if (viewName === 'capture' || viewName === 'pin') {
+ return { view: viewName, params: new URLSearchParams(query) };
+ }
+
+ return { view: 'launcher', params: new URLSearchParams() };
+}
+
+export function buildPluginUrl(view: AppView, params: Record, baseUrl = window.location.href.split('#')[0]): string {
+ const query = new URLSearchParams(params).toString();
+ return `${baseUrl}#/${view}${query ? `?${query}` : ''}`;
+}
+```
+
+- [ ] **Step 4: Implement storage**
+
+Create `src/core/storage.ts`:
+
+```ts
+import type { Rect } from './geometry';
+
+export interface DisplaySnapshot {
+ displayId: string;
+ bounds: Rect;
+ imageDataUrl: string;
+ scaleFactor: number;
+}
+
+export interface CaptureSession {
+ id: string;
+ createdAt: number;
+ completed: boolean;
+ displays: DisplaySnapshot[];
+}
+
+export interface PinWindowState {
+ id: string;
+ imageDataUrl: string;
+ originalBounds: Rect;
+ currentBounds: Rect;
+ scale: number;
+ createdAt: number;
+ lastActiveAt: number;
+}
+
+const CAPTURE_SESSION_PREFIX = 'top-screenshot:capture-session:';
+const PIN_WINDOW_PREFIX = 'top-screenshot:pin-window:';
+
+function readJson(storage: Storage, key: string): T | null {
+ const raw = storage.getItem(key);
+ if (!raw) {
+ return null;
+ }
+ return JSON.parse(raw) as T;
+}
+
+function writeJson(storage: Storage, key: string, value: T): void {
+ storage.setItem(key, JSON.stringify(value));
+}
+
+export function captureSessionKey(id: string): string {
+ return `${CAPTURE_SESSION_PREFIX}${id}`;
+}
+
+export function pinWindowKey(id: string): string {
+ return `${PIN_WINDOW_PREFIX}${id}`;
+}
+
+export function saveCaptureSession(storage: Storage, session: CaptureSession): void {
+ writeJson(storage, captureSessionKey(session.id), session);
+}
+
+export function loadCaptureSession(storage: Storage, id: string): CaptureSession | null {
+ return readJson(storage, captureSessionKey(id));
+}
+
+export function markCaptureSessionCompleted(storage: Storage, id: string): void {
+ const session = loadCaptureSession(storage, id);
+ if (!session) {
+ return;
+ }
+ saveCaptureSession(storage, { ...session, completed: true });
+}
+
+export function savePinWindow(storage: Storage, state: PinWindowState): void {
+ writeJson(storage, pinWindowKey(state.id), state);
+}
+
+export function loadPinWindow(storage: Storage, id: string): PinWindowState | null {
+ return readJson(storage, pinWindowKey(id));
+}
+```
+
+- [ ] **Step 5: Run tests to verify pass**
+
+Run:
+
+```bash
+npm test -- tests/routes.test.ts tests/storage.test.ts
+```
+
+Expected: PASS.
+
+- [ ] **Step 6: Commit routes and storage**
+
+```bash
+git add src/core/routes.ts src/core/storage.ts tests/routes.test.ts tests/storage.test.ts
+git commit -m "test: add screenshot route and storage state"
+```
+
+---
+
+### Task 4: Implement ZTools bridge
+
+**Files:**
+- Create: `src/types/ztools.ts`
+- Create: `src/core/ztoolsBridge.ts`
+- Create: `tests/ztoolsBridge.test.ts`
+- Modify: `src/main.ts`
+
+- [ ] **Step 1: Write bridge tests**
+
+Create `tests/ztoolsBridge.test.ts`:
+
+```ts
+import { describe, expect, it } from 'vitest';
+import { findSourceForDisplay, mapDisplaysToSnapshots } from '../src/core/ztoolsBridge';
+import type { DesktopCaptureSource, ZToolsDisplay } from '../src/types/ztools';
+
+function source(displayId: string, dataUrl: string): DesktopCaptureSource {
+ return {
+ id: `screen:${displayId}`,
+ name: `Screen ${displayId}`,
+ display_id: displayId,
+ thumbnail: {
+ toDataURL: () => dataUrl,
+ },
+ };
+}
+
+describe('ztoolsBridge', () => {
+ it('finds a desktop source by display id', () => {
+ expect(findSourceForDisplay({ id: 2, bounds: { x: 0, y: 0, width: 100, height: 100 }, scaleFactor: 1 }, [source('1', 'a'), source('2', 'b')])?.thumbnail.toDataURL()).toBe('b');
+ });
+
+ it('maps displays to snapshots', () => {
+ const displays: ZToolsDisplay[] = [
+ { id: 1, bounds: { x: 0, y: 0, width: 800, height: 600 }, scaleFactor: 1 },
+ ];
+ const snapshots = mapDisplaysToSnapshots(displays, [source('1', 'data:image/png;base64,screen')]);
+ expect(snapshots).toEqual([
+ {
+ displayId: '1',
+ bounds: { x: 0, y: 0, width: 800, height: 600 },
+ imageDataUrl: 'data:image/png;base64,screen',
+ scaleFactor: 1,
+ },
+ ]);
+ });
+});
+```
+
+- [ ] **Step 2: Run tests to verify failure**
+
+Run:
+
+```bash
+npm test -- tests/ztoolsBridge.test.ts
+```
+
+Expected: FAIL with missing module errors.
+
+- [ ] **Step 3: Add ZTools types**
+
+Create `src/types/ztools.ts`:
+
+```ts
+import type { Rect } from '../core/geometry';
+
+export interface ZToolsDisplay {
+ id: number | string;
+ bounds: Rect;
+ scaleFactor?: number;
+}
+
+export interface DesktopCaptureSource {
+ id: string;
+ name: string;
+ display_id?: string;
+ thumbnail: {
+ toDataURL(): string;
+ };
+}
+
+export interface BrowserWindowProxy {
+ close(): void;
+ focus?(): void;
+ show?(): void;
+}
+
+export interface BrowserWindowOptions {
+ x?: number;
+ y?: number;
+ width?: number;
+ height?: number;
+ frame?: boolean;
+ transparent?: boolean;
+ alwaysOnTop?: boolean;
+ skipTaskbar?: boolean;
+ resizable?: boolean;
+ movable?: boolean;
+ minimizable?: boolean;
+ maximizable?: boolean;
+ fullscreenable?: boolean;
+ hasShadow?: boolean;
+ backgroundColor?: string;
+}
+
+export interface ZToolsApi {
+ onPluginEnter?(callback: () => void): void;
+ onPluginReady?(callback: () => void): void;
+ hideMainWindow?(isRestorePreWindow?: boolean): void;
+ outPlugin?(isKill?: boolean): void;
+ getAllDisplays(): ZToolsDisplay[];
+ desktopCaptureSources(options: {
+ types: Array<'screen' | 'window'>;
+ thumbnailSize?: { width: number; height: number };
+ }): Promise | DesktopCaptureSource[];
+ createBrowserWindow(url: string, options: BrowserWindowOptions, callback?: () => void): BrowserWindowProxy | null;
+}
+
+declare global {
+ interface Window {
+ ztools?: ZToolsApi;
+ }
+}
+```
+
+- [ ] **Step 4: Implement bridge**
+
+Create `src/core/ztoolsBridge.ts`:
+
+```ts
+import type { DisplaySnapshot } from './storage';
+import type { BrowserWindowOptions, DesktopCaptureSource, ZToolsApi, ZToolsDisplay } from '../types/ztools';
+
+export function getZTools(): ZToolsApi | null {
+ return window.ztools ?? null;
+}
+
+export function requireZTools(): ZToolsApi {
+ const api = getZTools();
+ if (!api) {
+ throw new Error('ZTools API is not available in this window.');
+ }
+ return api;
+}
+
+export function findSourceForDisplay(display: ZToolsDisplay, sources: DesktopCaptureSource[]): DesktopCaptureSource | null {
+ const displayId = String(display.id);
+ return sources.find((source) => source.display_id === displayId) ?? sources.find((source) => source.id.includes(displayId)) ?? null;
+}
+
+export function mapDisplaysToSnapshots(displays: ZToolsDisplay[], sources: DesktopCaptureSource[]): DisplaySnapshot[] {
+ return displays.flatMap((display) => {
+ const source = findSourceForDisplay(display, sources);
+ if (!source) {
+ return [];
+ }
+
+ return [
+ {
+ displayId: String(display.id),
+ bounds: display.bounds,
+ imageDataUrl: source.thumbnail.toDataURL(),
+ scaleFactor: display.scaleFactor ?? 1,
+ },
+ ];
+ });
+}
+
+export async function getDisplaySnapshots(api: ZToolsApi): Promise {
+ const displays = api.getAllDisplays();
+ const maxWidth = Math.max(...displays.map((display) => Math.ceil(display.bounds.width * (display.scaleFactor ?? 1))));
+ const maxHeight = Math.max(...displays.map((display) => Math.ceil(display.bounds.height * (display.scaleFactor ?? 1))));
+ const sources = await api.desktopCaptureSources({
+ types: ['screen'],
+ thumbnailSize: { width: maxWidth, height: maxHeight },
+ });
+ return mapDisplaysToSnapshots(displays, sources);
+}
+
+export function createPluginWindow(api: ZToolsApi, url: string, options: BrowserWindowOptions): void {
+ api.createBrowserWindow(url, options);
+}
+```
+
+- [ ] **Step 5: Import types once**
+
+Modify `src/main.ts`:
+
+```ts
+import './types/ztools';
+import { createApp } from 'vue';
+import App from './App.vue';
+import './styles.css';
+
+createApp(App).mount('#app');
+```
+
+- [ ] **Step 6: Run tests to verify pass**
+
+Run:
+
+```bash
+npm test -- tests/ztoolsBridge.test.ts
+```
+
+Expected: PASS.
+
+- [ ] **Step 7: Commit ZTools bridge**
+
+```bash
+git add src/types/ztools.ts src/core/ztoolsBridge.ts tests/ztoolsBridge.test.ts src/main.ts
+git commit -m "test: add typed ztools bridge"
+```
+
+---
+
+### Task 5: Implement screenshot capture overlay
+
+**Files:**
+- Create: `src/core/crop.ts`
+- Create: `src/views/CaptureView.vue`
+- Modify: `src/styles.css`
+
+- [ ] **Step 1: Add image crop helper**
+
+Create `src/core/crop.ts`:
+
+```ts
+import type { Rect } from './geometry';
+
+export async function cropImageDataUrl(sourceDataUrl: string, selection: Rect, scaleFactor: number): Promise {
+ const image = await loadImage(sourceDataUrl);
+ const canvas = document.createElement('canvas');
+ const pixelX = Math.round(selection.x * scaleFactor);
+ const pixelY = Math.round(selection.y * scaleFactor);
+ const pixelWidth = Math.round(selection.width * scaleFactor);
+ const pixelHeight = Math.round(selection.height * scaleFactor);
+
+ canvas.width = pixelWidth;
+ canvas.height = pixelHeight;
+
+ const context = canvas.getContext('2d');
+ if (!context) {
+ throw new Error('Canvas 2D context is not available.');
+ }
+
+ context.drawImage(image, pixelX, pixelY, pixelWidth, pixelHeight, 0, 0, pixelWidth, pixelHeight);
+ return canvas.toDataURL('image/png');
+}
+
+function loadImage(sourceDataUrl: string): Promise {
+ return new Promise((resolve, reject) => {
+ const image = new Image();
+ image.onload = () => resolve(image);
+ image.onerror = () => reject(new Error('Failed to load screenshot image.'));
+ image.src = sourceDataUrl;
+ });
+}
+```
+
+- [ ] **Step 2: Add capture view**
+
+Create `src/views/CaptureView.vue`:
+
+```vue
+
+
+
+
+
+
+ 没有找到当前显示器截图。
+
+
+```
+
+- [ ] **Step 3: Add capture styles**
+
+Append to `src/styles.css`:
+
+```css
+.capture-view {
+ position: relative;
+ width: 100vw;
+ height: 100vh;
+ cursor: crosshair;
+ user-select: none;
+ background: rgba(0, 0, 0, 0.2);
+}
+
+.capture-image {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: fill;
+ pointer-events: none;
+}
+
+.selection-box {
+ position: absolute;
+ border: 2px solid #38bdf8;
+ background: rgba(56, 189, 248, 0.16);
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.35);
+}
+
+.capture-error {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ place-items: center;
+ color: #f9fafb;
+ background: #111827;
+}
+```
+
+- [ ] **Step 4: Run tests and type check**
+
+Run:
+
+```bash
+npm test
+npm run build
+```
+
+Expected: tests PASS and build PASS.
+
+- [ ] **Step 5: Commit capture overlay**
+
+```bash
+git add src/core/crop.ts src/views/CaptureView.vue src/styles.css
+git commit -m "feat: add screenshot capture overlay"
+```
+
+---
+
+### Task 6: Implement pin window rendering, drag, zoom, and Esc close
+
+**Files:**
+- Create: `src/views/PinView.vue`
+- Modify: `src/styles.css`
+- Modify: `src/core/storage.ts`
+- Modify: `tests/storage.test.ts`
+
+- [ ] **Step 1: Extend storage tests for active pin updates**
+
+Append this test to the `describe('storage', () => { ... })` block in `tests/storage.test.ts`:
+
+```ts
+ it('updates pin activity and current bounds', () => {
+ const storage = createMemoryStorage();
+ const state: PinWindowState = {
+ id: 'pin-2',
+ imageDataUrl: 'data:image/png;base64,ccc',
+ originalBounds: { x: 10, y: 20, width: 100, height: 80 },
+ currentBounds: { x: 10, y: 20, width: 100, height: 80 },
+ scale: 1,
+ createdAt: 100,
+ lastActiveAt: 100,
+ };
+
+ savePinWindow(storage, state);
+ savePinWindow(storage, {
+ ...state,
+ currentBounds: { x: 20, y: 30, width: 150, height: 120 },
+ scale: 1.5,
+ lastActiveAt: 200,
+ });
+
+ expect(loadPinWindow(storage, 'pin-2')).toEqual({
+ ...state,
+ currentBounds: { x: 20, y: 30, width: 150, height: 120 },
+ scale: 1.5,
+ lastActiveAt: 200,
+ });
+ });
+```
+
+- [ ] **Step 2: Run storage tests**
+
+Run:
+
+```bash
+npm test -- tests/storage.test.ts
+```
+
+Expected: PASS because `savePinWindow` already replaces the state atomically.
+
+- [ ] **Step 3: Add pin view**
+
+Create `src/views/PinView.vue`:
+
+```vue
+
+
+
+
+
+
+ 截图数据不存在
+
+```
+
+- [ ] **Step 4: Add pin styles**
+
+Append to `src/styles.css`:
+
+```css
+.pin-window {
+ display: inline-flex;
+ box-sizing: border-box;
+ width: 100vw;
+ height: 100vh;
+ padding: 6px;
+ align-items: center;
+ justify-content: center;
+ user-select: none;
+ background: transparent;
+}
+
+.pin-image {
+ display: block;
+ box-sizing: border-box;
+ border: 2px solid #38bdf8;
+ border-radius: 8px;
+ box-shadow:
+ 0 0 0 1px rgba(56, 189, 248, 0.35),
+ 0 10px 28px rgba(56, 189, 248, 0.22),
+ 0 16px 36px rgba(0, 0, 0, 0.3);
+ cursor: grab;
+ object-fit: fill;
+}
+
+.pin-window:active .pin-image {
+ cursor: grabbing;
+}
+
+.pin-window-empty {
+ display: grid;
+ place-items: center;
+ color: #f9fafb;
+ background: rgba(17, 24, 39, 0.92);
+}
+```
+
+- [ ] **Step 5: Run tests and build**
+
+Run:
+
+```bash
+npm test
+npm run build
+```
+
+Expected: tests PASS and build PASS.
+
+- [ ] **Step 6: Commit pin window**
+
+```bash
+git add src/views/PinView.vue src/styles.css tests/storage.test.ts
+git commit -m "feat: add draggable zoomable pin windows"
+```
+
+---
+
+### Task 7: Implement launcher orchestration and route rendering
+
+**Files:**
+- Create: `src/views/LauncherView.vue`
+- Modify: `src/App.vue`
+- Modify: `src/styles.css`
+
+- [ ] **Step 1: Add launcher view**
+
+Create `src/views/LauncherView.vue`:
+
+```vue
+
+
+
+
+
+
截图置顶
+
{{ status }}
+
+
+
+
+```
+
+- [ ] **Step 2: Remove unused import from launcher**
+
+Replace the imports at the top of `src/views/LauncherView.vue` with:
+
+```ts
+import { onMounted, ref } from 'vue';
+import { buildPluginUrl } from '../core/routes';
+import { saveCaptureSession, type CaptureSession, type DisplaySnapshot } from '../core/storage';
+import { getDisplaySnapshots, requireZTools } from '../core/ztoolsBridge';
+```
+
+- [ ] **Step 3: Render views by route**
+
+Replace `src/App.vue` with:
+
+```vue
+
+
+
+
+
+
+
+```
+
+- [ ] **Step 4: Add launcher styles**
+
+Append to `src/styles.css`:
+
+```css
+.launcher-view {
+ display: grid;
+ min-height: 100vh;
+ place-items: center;
+ color: #e5e7eb;
+ background: #111827;
+}
+
+.launcher-card {
+ width: min(360px, calc(100vw - 32px));
+ padding: 24px;
+ border: 1px solid rgba(148, 163, 184, 0.3);
+ border-radius: 16px;
+ background: rgba(15, 23, 42, 0.92);
+ box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
+ text-align: center;
+}
+
+.launcher-card h1 {
+ margin: 0 0 8px;
+ font-size: 24px;
+}
+
+.launcher-card p {
+ margin: 0 0 20px;
+ color: #cbd5e1;
+}
+
+.launcher-card button {
+ border: 0;
+ border-radius: 999px;
+ padding: 10px 16px;
+ color: #082f49;
+ background: #38bdf8;
+ font-weight: 700;
+ cursor: pointer;
+}
+```
+
+- [ ] **Step 5: Run tests and build**
+
+Run:
+
+```bash
+npm test
+npm run build
+```
+
+Expected: tests PASS and build PASS.
+
+- [ ] **Step 6: Commit launcher orchestration**
+
+```bash
+git add src/views/LauncherView.vue src/App.vue src/styles.css
+git commit -m "feat: launch screenshot capture from ztools entry"
+```
+
+---
+
+### Task 8: Manual verification in ZTools
+
+**Files:**
+- Modify only files required by observed failures.
+
+- [ ] **Step 1: Build the plugin**
+
+Run:
+
+```bash
+npm run build
+```
+
+Expected: `dist/index.html` and `dist/preload.cjs` exist.
+
+- [ ] **Step 2: Load plugin in ZTools**
+
+Use ZTools local plugin loading with this project root:
+
+```text
+d:/code/vue/top_screenshot
+```
+
+Expected: ZTools recognizes `plugin.json` and shows the plugin title “截图置顶”.
+
+- [ ] **Step 3: Verify launch-to-capture**
+
+In ZTools, search and run:
+
+```text
+截图置顶
+```
+
+Expected: the plugin immediately opens screenshot selection coverage on the available displays.
+
+- [ ] **Step 4: Verify pin creation**
+
+Drag a rectangle on the capture overlay.
+
+Expected: a transparent always-on-top pin window appears near the selected region. The image content keeps the selected width and height, with blue border, small radius, and glow shadow.
+
+- [ ] **Step 5: Verify multiple pins**
+
+Run “截图置顶” again and create a second pin.
+
+Expected: the first pin remains open and the second pin appears as a separate always-on-top window.
+
+- [ ] **Step 6: Verify drag, zoom, and close**
+
+Drag a pin window, scroll the mouse wheel over it, and press Esc while the pin is focused.
+
+Expected: drag moves the whole pin, wheel zooms around the image center between 30% and 300%, and Esc closes the focused pin.
+
+- [ ] **Step 7: Fix observed ZTools integration mismatch**
+
+If ZTools reports a missing entry, change `plugin.json` paths so `main` points to the built HTML file and `preload` points to the built preload file. If display screenshots do not match display IDs, adjust `findSourceForDisplay` in `src/core/ztoolsBridge.ts` to match the actual `DesktopCaptureSource` fields printed by ZTools.
+
+- [ ] **Step 8: Re-run automated checks**
+
+Run:
+
+```bash
+npm test
+npm run build
+```
+
+Expected: tests PASS and build PASS.
+
+- [ ] **Step 9: Commit verified plugin**
+
+```bash
+git add plugin.json src/core/ztoolsBridge.ts src/views/LauncherView.vue src/views/CaptureView.vue src/views/PinView.vue src/styles.css
+git commit -m "fix: verify ztools screenshot pin integration"
+```
+
+---
+
+## Self-Review
+
+- Spec coverage: startup-to-screenshot is covered by Task 7; custom screenshot bounds are covered by Task 5; multi-window pin creation is covered by Tasks 5 and 7; visual border/radius/shadow is covered by Task 6; drag, center zoom, and Esc close are covered by Task 6; TypeScript and ztools project creation are covered by Task 1.
+- Placeholder scan: this plan has no TBD markers, no TODO markers, and no undefined implementation steps.
+- Type consistency: `Rect`, `DisplaySnapshot`, `CaptureSession`, `PinWindowState`, `AppView`, and ZTools bridge types are introduced before later tasks use them.
diff --git a/plugins/top-screenshot/docs/superpowers/specs/2026-06-05-screenshot-pin-design.md b/plugins/top-screenshot/docs/superpowers/specs/2026-06-05-screenshot-pin-design.md
new file mode 100644
index 00000000..3fee1bd7
--- /dev/null
+++ b/plugins/top-screenshot/docs/superpowers/specs/2026-06-05-screenshot-pin-design.md
@@ -0,0 +1,140 @@
+# 截图置顶 ztools 插件设计
+
+## 目标
+
+创建一个名为“截图置顶”的 ztools 插件。用户在 ztools 中启动插件后立即进入截图视角,框选区域后在截图对应位置生成一张置顶图片窗口。图片初始大小和形状保持与截图区域一致,通过圆角、高亮描边和阴影凸显它是置顶截图。鼠标悬停在图片窗口上滚动滚轮时,可以按图片中心放大或缩小。
+
+## 范围
+
+本版本包含:
+
+- 从空目录创建 ztools 插件工程。
+- 使用 TypeScript 开发。
+- 插件启动后立即进入截图选择视角。
+- 截图完成后新增一张置顶图片窗口。
+- 多张置顶截图可同时存在。
+- 置顶窗口初始位置贴近截图区域,并保留少量视觉装饰空间。
+- 置顶窗口支持高亮蓝色描边、小圆角和轻发光阴影。
+- 鼠标拖动图片即可移动窗口。
+- 鼠标悬停图片窗口时滚轮缩放,缩放中心为图片中心。
+- 按 Esc 关闭当前活动或最近操作的置顶截图窗口;截图选择中按 Esc 则取消截图。
+
+本版本不包含:
+
+- 历史记录。
+- 截图保存到文件。
+- 右键菜单。
+- 配置面板。
+- 自动吸附或对齐辅助线。
+- 重启后恢复置顶截图。
+
+## 架构
+
+项目采用 Vue + TypeScript + Electron 风格的 ztools 插件结构。Vue 负责渲染截图覆盖层和置顶图片窗口;主进程负责 ztools 启动入口、窗口创建、置顶状态、多窗口生命周期、Esc 关闭逻辑和 IPC 通信。
+
+主要模块:
+
+1. 插件入口和主进程
+ - 接收 ztools 启动事件。
+ - 创建全屏截图覆盖层窗口。
+ - 接收截图结果后创建新的置顶图片窗口。
+ - 维护置顶窗口列表、最近活动窗口和 Esc 关闭目标。
+
+2. 截图覆盖层
+ - 全屏显示截图选择视角。
+ - 用户拖拽选择截图区域。
+ - 生成截图区域坐标和图片数据。
+ - 成功截图后把结果发送给主进程。
+ - 按 Esc 或无效选区时取消截图。
+
+3. 置顶图片窗口
+ - 使用透明、无边框、always-on-top 窗口。
+ - 渲染截图图片,保持原始宽高比例和初始尺寸。
+ - 外层显示蓝色描边、小圆角和轻发光阴影。
+ - 支持整张图片拖动移动。
+ - 支持滚轮缩放。
+ - 每次被点击、拖动或缩放时更新为最近活动窗口。
+
+## 数据流
+
+1. 用户通过 ztools 搜索并启动“截图置顶”。
+2. 主进程创建截图覆盖层窗口。
+3. 用户框选截图区域。
+4. 截图覆盖层返回图片数据和截图区域 bounds。
+5. 主进程关闭截图覆盖层,并创建一个新的置顶图片窗口。
+6. 置顶图片窗口按照截图区域定位,初始图片尺寸与截图区域一致。
+7. 用户可以拖动窗口、滚轮缩放或按 Esc 关闭窗口。
+
+截图结果数据结构:
+
+```ts
+interface CaptureResult {
+ imageDataUrl: string;
+ bounds: {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ };
+}
+```
+
+置顶窗口状态结构:
+
+```ts
+interface PinWindowState {
+ id: string;
+ imageDataUrl: string;
+ originalBounds: Rectangle;
+ currentBounds: Rectangle;
+ scale: number;
+ createdAt: number;
+ lastActiveAt: number;
+}
+```
+
+## 交互规则
+
+### 启动与截图
+
+插件启动后立即进入截图选择视角,不先展示面板。截图覆盖层中,拖拽形成有效选区后创建置顶截图;按 Esc 或未形成有效选区则取消。
+
+### 初始位置与视觉
+
+置顶窗口初始位置贴合截图区域,但外层预留少量视觉装饰空间,用于显示描边和阴影。图片内容不拉伸、不裁剪、不改变比例。视觉风格为高亮蓝色描边、小圆角和轻发光阴影。
+
+### 多图并存
+
+每次截图新增一张置顶图片窗口,不替换旧窗口。多个置顶窗口互相独立,分别维护自己的位置、缩放比例和活动状态。
+
+### 拖动
+
+鼠标拖动置顶图片窗口任意位置即可移动窗口。拖动过程中窗口仍保持 always-on-top。
+
+### 缩放
+
+鼠标悬停在置顶图片窗口时,滚轮向上放大,滚轮向下缩小。缩放中心固定为图片中心。建议缩放范围为 30% 到 300%,避免窗口过小不可操作或过大失控。
+
+### 关闭
+
+按 Esc 时,如果当前有活动置顶窗口,则关闭该窗口;否则关闭最近创建或最近操作的一张置顶窗口。截图覆盖层打开时按 Esc 只取消当前截图,不关闭已有置顶窗口。
+
+## 错误处理与边界
+
+- 无效选区:宽度或高度过小则取消本次截图,不创建窗口。
+- 截图失败:关闭截图覆盖层并回到空闲状态,不创建置顶窗口。
+- 图片窗口缩放到边界:继续滚轮时保持在最小或最大缩放比例。
+- 多显示器:截图 bounds 以屏幕全局坐标为准,置顶窗口按同一坐标系创建。
+- 高 DPI:截图区域坐标和图片像素尺寸需要按设备缩放因子校正,避免窗口尺寸与实际截图不一致。
+
+## 测试计划
+
+1. 启动插件后确认立即进入截图覆盖层。
+2. 框选区域后确认置顶窗口出现在截图区域附近,图片尺寸与选区一致。
+3. 确认图片不变形,圆角、蓝色描边和阴影可见。
+4. 连续截图多次,确认多张置顶窗口同时存在。
+5. 拖动任意一张置顶窗口,确认可以移动且仍置顶。
+6. 鼠标悬停窗口滚动滚轮,确认以图片中心缩放,且缩放范围受限。
+7. 按 Esc,确认关闭当前活动或最近操作的置顶窗口。
+8. 在截图覆盖层按 Esc,确认只取消截图,不影响已有置顶窗口。
+9. 如果环境支持多显示器或高 DPI,验证窗口位置和尺寸与截图区域一致。
diff --git a/plugins/top-screenshot/index.html b/plugins/top-screenshot/index.html
new file mode 100644
index 00000000..56a64b8d
--- /dev/null
+++ b/plugins/top-screenshot/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 截图置顶
+
+
+
+
+
+
diff --git a/plugins/top-screenshot/package-lock.json b/plugins/top-screenshot/package-lock.json
new file mode 100644
index 00000000..99d764ae
--- /dev/null
+++ b/plugins/top-screenshot/package-lock.json
@@ -0,0 +1,3104 @@
+{
+ "name": "top-screenshot-ztools-plugin",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "top-screenshot-ztools-plugin",
+ "version": "0.1.0",
+ "dependencies": {
+ "@vitejs/plugin-vue": "latest",
+ "@vue/test-utils": "latest",
+ "jsdom": "latest",
+ "tsx": "latest",
+ "typescript": "latest",
+ "vite": "latest",
+ "vitest": "latest",
+ "vue": "latest",
+ "vue-tsc": "latest"
+ },
+ "devDependencies": {
+ "@types/node": "latest"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
+ "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@csstools/css-calc": "^3.2.0",
+ "@csstools/css-color-parser": "^4.1.0",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
+ "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.2.1",
+ "is-potential-custom-element-name": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/generational-cache": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
+ "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "license": "MIT"
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.0.0"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
+ "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
+ "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz",
+ "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^6.0.2",
+ "@csstools/css-calc": "^3.2.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz",
+ "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
+ "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
+ "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
+ "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
+ "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
+ "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
+ "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
+ "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
+ "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
+ "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
+ "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
+ "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
+ "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
+ "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
+ "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
+ "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
+ "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
+ "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
+ "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
+ "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
+ "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
+ "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
+ "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
+ "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
+ "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
+ "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
+ "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@exodus/bytes": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
+ "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@one-ini/wasm": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
+ "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
+ "license": "MIT"
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.133.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
+ "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
+ "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
+ "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
+ "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
+ "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
+ "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
+ "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
+ "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
+ "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
+ "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
+ "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
+ "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
+ "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
+ "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
+ "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
+ "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "25.9.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
+ "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": ">=7.24.0 <7.24.7"
+ }
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "6.0.7",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
+ "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==",
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz",
+ "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.8",
+ "@vitest/utils": "4.1.8",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz",
+ "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.8",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/mocker/node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz",
+ "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==",
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz",
+ "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.8",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz",
+ "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.8",
+ "@vitest/utils": "4.1.8",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz",
+ "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz",
+ "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.8",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@volar/language-core": {
+ "version": "2.4.28",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
+ "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.28"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "2.4.28",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
+ "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
+ "license": "MIT"
+ },
+ "node_modules/@volar/typescript": {
+ "version": "2.4.28",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
+ "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.28",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.35",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz",
+ "integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.3",
+ "@vue/shared": "3.5.35",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.35",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz",
+ "integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.35",
+ "@vue/shared": "3.5.35"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.35",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz",
+ "integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.3",
+ "@vue/compiler-core": "3.5.35",
+ "@vue/compiler-dom": "3.5.35",
+ "@vue/compiler-ssr": "3.5.35",
+ "@vue/shared": "3.5.35",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.15",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.35",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz",
+ "integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.35",
+ "@vue/shared": "3.5.35"
+ }
+ },
+ "node_modules/@vue/language-core": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.3.tgz",
+ "integrity": "sha512-X6p+7nfY7vVT6dQwUJ+v0Jfq/lwIfhL2jMi91dQ3ln4hnlGXlxsDu/FNkeyHYgvYtyQy18ZX76IZy7X4diDbiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.28",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/shared": "^3.5.0",
+ "alien-signals": "^3.2.0",
+ "muggle-string": "^0.4.1",
+ "path-browserify": "^1.0.1",
+ "picomatch": "^4.0.4"
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.35",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz",
+ "integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.35"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.35",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz",
+ "integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.35",
+ "@vue/shared": "3.5.35"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.35",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz",
+ "integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.35",
+ "@vue/runtime-core": "3.5.35",
+ "@vue/shared": "3.5.35",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.35",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz",
+ "integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.35",
+ "@vue/shared": "3.5.35"
+ },
+ "peerDependencies": {
+ "vue": "3.5.35"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.35",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz",
+ "integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/test-utils": {
+ "version": "2.4.11",
+ "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.11.tgz",
+ "integrity": "sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==",
+ "license": "MIT",
+ "dependencies": {
+ "js-beautify": "^1.14.9",
+ "vue-component-type-helpers": "^3.0.0"
+ },
+ "peerDependencies": {
+ "@vue/compiler-dom": "3.x",
+ "@vue/server-renderer": "3.x",
+ "vue": "3.x"
+ },
+ "peerDependenciesMeta": {
+ "@vue/server-renderer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/abbrev": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
+ "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/alien-signals": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz",
+ "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==",
+ "license": "MIT"
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
+ "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/config-chain": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
+ "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ini": "^1.3.4",
+ "proto-list": "~1.2.1"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
+ "node_modules/editorconfig": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz",
+ "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==",
+ "license": "MIT",
+ "dependencies": {
+ "@one-ini/wasm": "0.1.1",
+ "commander": "^10.0.0",
+ "minimatch": "^9.0.1",
+ "semver": "^7.5.3"
+ },
+ "bin": {
+ "editorconfig": "bin/editorconfig"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
+ "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.0",
+ "@esbuild/android-arm": "0.28.0",
+ "@esbuild/android-arm64": "0.28.0",
+ "@esbuild/android-x64": "0.28.0",
+ "@esbuild/darwin-arm64": "0.28.0",
+ "@esbuild/darwin-x64": "0.28.0",
+ "@esbuild/freebsd-arm64": "0.28.0",
+ "@esbuild/freebsd-x64": "0.28.0",
+ "@esbuild/linux-arm": "0.28.0",
+ "@esbuild/linux-arm64": "0.28.0",
+ "@esbuild/linux-ia32": "0.28.0",
+ "@esbuild/linux-loong64": "0.28.0",
+ "@esbuild/linux-mips64el": "0.28.0",
+ "@esbuild/linux-ppc64": "0.28.0",
+ "@esbuild/linux-riscv64": "0.28.0",
+ "@esbuild/linux-s390x": "0.28.0",
+ "@esbuild/linux-x64": "0.28.0",
+ "@esbuild/netbsd-arm64": "0.28.0",
+ "@esbuild/netbsd-x64": "0.28.0",
+ "@esbuild/openbsd-arm64": "0.28.0",
+ "@esbuild/openbsd-x64": "0.28.0",
+ "@esbuild/openharmony-arm64": "0.28.0",
+ "@esbuild/sunos-x64": "0.28.0",
+ "@esbuild/win32-arm64": "0.28.0",
+ "@esbuild/win32-ia32": "0.28.0",
+ "@esbuild/win32-x64": "0.28.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/js-beautify": {
+ "version": "1.15.4",
+ "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
+ "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
+ "license": "MIT",
+ "dependencies": {
+ "config-chain": "^1.1.13",
+ "editorconfig": "^1.0.4",
+ "glob": "^10.4.2",
+ "js-cookie": "^3.0.5",
+ "nopt": "^7.2.1"
+ },
+ "bin": {
+ "css-beautify": "js/bin/css-beautify.js",
+ "html-beautify": "js/bin/html-beautify.js",
+ "js-beautify": "js/bin/js-beautify.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/js-cookie": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
+ "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
+ "license": "MIT"
+ },
+ "node_modules/jsdom": {
+ "version": "29.1.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
+ "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^5.1.11",
+ "@asamuzakjp/dom-selector": "^7.1.1",
+ "@bramus/specificity": "^2.4.2",
+ "@csstools/css-syntax-patches-for-csstree": "^1.1.3",
+ "@exodus/bytes": "^1.15.0",
+ "css-tree": "^3.2.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.3.5",
+ "parse5": "^8.0.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.1",
+ "undici": "^7.25.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.1",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "11.5.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
+ "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "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/nopt": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
+ "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^2.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz",
+ "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==",
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
+ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "license": "MIT"
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "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/proto-list": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
+ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
+ "license": "ISC"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
+ "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.133.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.3",
+ "@rolldown/binding-darwin-arm64": "1.0.3",
+ "@rolldown/binding-darwin-x64": "1.0.3",
+ "@rolldown/binding-freebsd-x64": "1.0.3",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.3",
+ "@rolldown/binding-linux-arm64-musl": "1.0.3",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.3",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-musl": "1.0.3",
+ "@rolldown/binding-openharmony-arm64": "1.0.3",
+ "@rolldown/binding-wasm32-wasi": "1.0.3",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.3",
+ "@rolldown/binding-win32-x64-msvc": "1.0.3"
+ }
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
+ "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "license": "ISC"
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+ "license": "MIT"
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "7.4.2",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz",
+ "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==",
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.4.2"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.4.2",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz",
+ "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==",
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
+ "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/tsx": {
+ "version": "4.22.4",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
+ "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.1.tgz",
+ "integrity": "sha512-UDdpiex+mzigiyrXrGbiUaF4HzTNhKbh2vRNFaTMzcqmLIPrZxaCtwo/1TMSuWoM1Xz3WiTo9KdgI3kRqYzJGg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.24.6",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
+ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "8.0.16",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
+ "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.15",
+ "rolldown": "1.0.3",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.18",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz",
+ "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.8",
+ "@vitest/mocker": "4.1.8",
+ "@vitest/pretty-format": "4.1.8",
+ "@vitest/runner": "4.1.8",
+ "@vitest/snapshot": "4.1.8",
+ "@vitest/spy": "4.1.8",
+ "@vitest/utils": "4.1.8",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.8",
+ "@vitest/browser-preview": "4.1.8",
+ "@vitest/browser-webdriverio": "4.1.8",
+ "@vitest/coverage-istanbul": "4.1.8",
+ "@vitest/coverage-v8": "4.1.8",
+ "@vitest/ui": "4.1.8",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/vscode-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+ "license": "MIT"
+ },
+ "node_modules/vue": {
+ "version": "3.5.35",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz",
+ "integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.35",
+ "@vue/compiler-sfc": "3.5.35",
+ "@vue/runtime-dom": "3.5.35",
+ "@vue/server-renderer": "3.5.35",
+ "@vue/shared": "3.5.35"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-component-type-helpers": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.3.tgz",
+ "integrity": "sha512-x4nsFpy5Pe8fqPzp/5vkTPeTTDBpAx4WVtV47Ejt0+2FQrq4pRRsJs7JmYRqMFzTu/LW+pCWEjQ3YVCkPV7f9g==",
+ "license": "MIT"
+ },
+ "node_modules/vue-tsc": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.3.tgz",
+ "integrity": "sha512-SWUEG7YRUeDJHT7Xsuhf02elYX2gxPzzAII7OxDAh4KNOr4QHQ0Lls0YfnaO5GNd560CwVa2HTfdqmA5MqvRqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "2.4.28",
+ "@vue/language-core": "3.3.3"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.0"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "license": "MIT"
+ }
+ }
+}
diff --git a/plugins/top-screenshot/package.json b/plugins/top-screenshot/package.json
new file mode 100644
index 00000000..899cb184
--- /dev/null
+++ b/plugins/top-screenshot/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "top-screenshot-ztools-plugin",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 127.0.0.1",
+ "build": "vue-tsc --noEmit && vite build && vite build --config vite.preload.config.ts",
+ "package": "npm run build && tsx scripts/package-plugin.ts",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "create:logo": "node scripts/create-logo.mjs"
+ },
+ "dependencies": {
+ "@vitejs/plugin-vue": "latest",
+ "@vue/test-utils": "latest",
+ "jsdom": "latest",
+ "tsx": "latest",
+ "typescript": "latest",
+ "vite": "latest",
+ "vitest": "latest",
+ "vue": "latest",
+ "vue-tsc": "latest"
+ },
+ "devDependencies": {
+ "@types/node": "latest"
+ }
+}
diff --git a/plugins/top-screenshot/plugin.json b/plugins/top-screenshot/plugin.json
new file mode 100644
index 00000000..21300340
--- /dev/null
+++ b/plugins/top-screenshot/plugin.json
@@ -0,0 +1,17 @@
+{
+ "name": "top-screenshot",
+ "title": "截图置顶",
+ "description": "好用的截图置顶工具,框选屏幕区域并原地置顶显示截图",
+ "version": "0.1.1",
+ "author": "Timi",
+ "main": "dist/index.html",
+ "logo": "assets/logo.png",
+ "preload": "dist/preload.cjs",
+ "features": [
+ {
+ "code": "capture-pin",
+ "explain": "截图命令",
+ "cmds": ["截图置顶", "截图", "置顶截图"]
+ }
+ ]
+}
diff --git a/plugins/top-screenshot/preload/index.ts b/plugins/top-screenshot/preload/index.ts
new file mode 100644
index 00000000..c45186ee
--- /dev/null
+++ b/plugins/top-screenshot/preload/index.ts
@@ -0,0 +1,38 @@
+const { ipcRenderer } = require('electron') as {
+ ipcRenderer: {
+ on(channel: string, callback: (event: unknown, payload: unknown) => void): void;
+ };
+};
+
+type ParentMessageCallback = (...args: unknown[]) => void;
+
+const listeners = new Map>();
+
+ipcRenderer.on('__ipc_sendto_relay__', (_event, payload) => {
+ if (!payload || typeof payload !== 'object') {
+ return;
+ }
+
+ const { channel, args } = payload as { channel?: unknown; args?: unknown };
+ if (typeof channel !== 'string' || !Array.isArray(args)) {
+ return;
+ }
+
+ listeners.get(channel)?.forEach((callback) => callback(...args));
+});
+
+const api = window.ztools;
+if (api) {
+ api.onParentMessage = (channel, callback) => {
+ const channelListeners = listeners.get(channel) ?? new Set();
+ channelListeners.add(callback);
+ listeners.set(channel, channelListeners);
+
+ return () => {
+ channelListeners.delete(callback);
+ if (channelListeners.size === 0) {
+ listeners.delete(channel);
+ }
+ };
+ };
+}
diff --git a/plugins/top-screenshot/scripts/create-logo.mjs b/plugins/top-screenshot/scripts/create-logo.mjs
new file mode 100644
index 00000000..48aaa351
--- /dev/null
+++ b/plugins/top-screenshot/scripts/create-logo.mjs
@@ -0,0 +1,5 @@
+import { mkdirSync, writeFileSync } from 'node:fs';
+
+mkdirSync('assets', { recursive: true });
+const pngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAfElEQVR4nO3QQQ0AIBDAMMC/5+ONAvZoFSzZnR1JkpyeA7g1wABggAHAAAOAAQYAAwwABhgADDAAGGAAAHAAMMAAYIABwAADgAEGAAcAAAwwABhgADDAAGGAAAHAAMMAAYIABwAADgAEGAAcAAAwwABhgADDAAGGAAcJ+UAQAA//YCrvCk7QAAAABJRU5ErkJggg==';
+writeFileSync('assets/logo.png', Buffer.from(pngBase64, 'base64'));
diff --git a/plugins/top-screenshot/scripts/package-plugin.ts b/plugins/top-screenshot/scripts/package-plugin.ts
new file mode 100644
index 00000000..869f3d0b
--- /dev/null
+++ b/plugins/top-screenshot/scripts/package-plugin.ts
@@ -0,0 +1,153 @@
+import { cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+type PluginManifest = {
+ name: string;
+ main: string;
+ preload: string;
+ logo: string;
+ [key: string]: unknown;
+};
+
+export async function packagePlugin(rootDir = process.cwd()): Promise {
+ const manifest = JSON.parse(await readFile(path.join(rootDir, 'plugin.json'), 'utf8')) as PluginManifest;
+ const outDir = path.join(rootDir, 'release', manifest.name);
+
+ await rm(outDir, { recursive: true, force: true });
+ await mkdir(outDir, { recursive: true });
+ await mkdir(path.join(outDir, 'assets'), { recursive: true });
+
+ await cp(path.join(rootDir, manifest.main), path.join(outDir, 'index.html'));
+ await cp(path.join(rootDir, manifest.preload), path.join(outDir, 'preload.cjs'));
+ await cp(path.join(rootDir, manifest.logo), path.join(outDir, 'logo.png'));
+ await cp(path.join(rootDir, 'dist', 'assets'), path.join(outDir, 'assets'), { recursive: true });
+
+ await writeFile(
+ path.join(outDir, 'plugin.json'),
+ `${JSON.stringify(
+ {
+ ...manifest,
+ main: 'index.html',
+ preload: 'preload.cjs',
+ logo: 'logo.png',
+ },
+ null,
+ 2,
+ )}\n`,
+ );
+
+ await createZpx(outDir, path.join(rootDir, 'release', `${manifest.name}.zpx`));
+
+ return outDir;
+}
+
+async function createZpx(sourceDir: string, zpxPath: string): Promise {
+ const files = await listFiles(sourceDir);
+ const chunks: Buffer[] = [];
+ const centralDirectory: Buffer[] = [];
+ let offset = 0;
+
+ for (const filePath of files) {
+ const relativePath = toZipPath(path.relative(sourceDir, filePath));
+ const data = await readFile(filePath);
+ const name = Buffer.from(relativePath, 'utf8');
+ const crc = crc32(data);
+ const localHeader = Buffer.alloc(30 + name.length);
+
+ localHeader.writeUInt32LE(0x04034b50, 0);
+ localHeader.writeUInt16LE(20, 4);
+ localHeader.writeUInt16LE(0, 6);
+ localHeader.writeUInt16LE(0, 8);
+ localHeader.writeUInt16LE(0, 10);
+ localHeader.writeUInt16LE(0, 12);
+ localHeader.writeUInt32LE(crc, 14);
+ localHeader.writeUInt32LE(data.length, 18);
+ localHeader.writeUInt32LE(data.length, 22);
+ localHeader.writeUInt16LE(name.length, 26);
+ localHeader.writeUInt16LE(0, 28);
+ name.copy(localHeader, 30);
+
+ chunks.push(localHeader, data);
+
+ const centralHeader = Buffer.alloc(46 + name.length);
+ centralHeader.writeUInt32LE(0x02014b50, 0);
+ centralHeader.writeUInt16LE(20, 4);
+ centralHeader.writeUInt16LE(20, 6);
+ centralHeader.writeUInt16LE(0, 8);
+ centralHeader.writeUInt16LE(0, 10);
+ centralHeader.writeUInt16LE(0, 12);
+ centralHeader.writeUInt16LE(0, 14);
+ centralHeader.writeUInt32LE(crc, 16);
+ centralHeader.writeUInt32LE(data.length, 20);
+ centralHeader.writeUInt32LE(data.length, 24);
+ centralHeader.writeUInt16LE(name.length, 28);
+ centralHeader.writeUInt16LE(0, 30);
+ centralHeader.writeUInt16LE(0, 32);
+ centralHeader.writeUInt16LE(0, 34);
+ centralHeader.writeUInt16LE(0, 36);
+ centralHeader.writeUInt32LE(0, 38);
+ centralHeader.writeUInt32LE(offset, 42);
+ name.copy(centralHeader, 46);
+ centralDirectory.push(centralHeader);
+
+ offset += localHeader.length + data.length;
+ }
+
+ const centralDirectorySize = centralDirectory.reduce((size, chunk) => size + chunk.length, 0);
+ const end = Buffer.alloc(22);
+ end.writeUInt32LE(0x06054b50, 0);
+ end.writeUInt16LE(0, 4);
+ end.writeUInt16LE(0, 6);
+ end.writeUInt16LE(files.length, 8);
+ end.writeUInt16LE(files.length, 10);
+ end.writeUInt32LE(centralDirectorySize, 12);
+ end.writeUInt32LE(offset, 16);
+ end.writeUInt16LE(0, 20);
+
+ await writeFile(zpxPath, Buffer.concat([...chunks, ...centralDirectory, end]));
+}
+
+async function listFiles(dir: string): Promise {
+ const entries = await readdir(dir, { withFileTypes: true });
+ const files = await Promise.all(
+ entries.map((entry) => {
+ const entryPath = path.join(dir, entry.name);
+ return entry.isDirectory() ? listFiles(entryPath) : [entryPath];
+ }),
+ );
+
+ return files.flat().sort((a, b) => toZipPath(path.relative(dir, a)).localeCompare(toZipPath(path.relative(dir, b))));
+}
+
+function toZipPath(filePath: string): string {
+ return filePath.split(path.sep).join('/');
+}
+
+function crc32(data: Buffer): number {
+ let crc = 0xffffffff;
+
+ for (const byte of data) {
+ crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ byte) & 0xff];
+ }
+
+ return (crc ^ 0xffffffff) >>> 0;
+}
+
+const CRC_TABLE = Array.from({ length: 256 }, (_, index) => {
+ let crc = index;
+
+ for (let bit = 0; bit < 8; bit += 1) {
+ crc = crc & 1 ? 0xedb88320 ^ (crc >>> 1) : crc >>> 1;
+ }
+
+ return crc >>> 0;
+});
+
+const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
+
+if (isDirectRun) {
+ packagePlugin().then((outDir) => {
+ console.log(`Packaged ZTools plugin at ${outDir}`);
+ });
+}
diff --git a/plugins/top-screenshot/src/App.vue b/plugins/top-screenshot/src/App.vue
new file mode 100644
index 00000000..82539370
--- /dev/null
+++ b/plugins/top-screenshot/src/App.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/plugins/top-screenshot/src/core/crop.ts b/plugins/top-screenshot/src/core/crop.ts
new file mode 100644
index 00000000..7452d74b
--- /dev/null
+++ b/plugins/top-screenshot/src/core/crop.ts
@@ -0,0 +1,45 @@
+import type { Rect } from './geometry';
+
+export function selectionToSourcePixels(selection: Rect, scaleFactor: number): Rect {
+ return {
+ x: Math.round(selection.x * scaleFactor),
+ y: Math.round(selection.y * scaleFactor),
+ width: Math.round(selection.width * scaleFactor),
+ height: Math.round(selection.height * scaleFactor),
+ };
+}
+
+export async function cropImageDataUrl(sourceDataUrl: string, selection: Rect, scaleFactor: number): Promise {
+ const image = await loadImage(sourceDataUrl);
+ const sourcePixels = selectionToSourcePixels(selection, scaleFactor);
+ const canvas = document.createElement('canvas');
+ canvas.width = sourcePixels.width;
+ canvas.height = sourcePixels.height;
+
+ const context = canvas.getContext('2d');
+ if (!context) {
+ throw new Error('Canvas 2D context is not available.');
+ }
+
+ context.drawImage(
+ image,
+ sourcePixels.x,
+ sourcePixels.y,
+ sourcePixels.width,
+ sourcePixels.height,
+ 0,
+ 0,
+ sourcePixels.width,
+ sourcePixels.height,
+ );
+ return canvas.toDataURL('image/png');
+}
+
+function loadImage(sourceDataUrl: string): Promise {
+ return new Promise((resolve, reject) => {
+ const image = new Image();
+ image.onload = () => resolve(image);
+ image.onerror = () => reject(new Error('Failed to load screenshot image.'));
+ image.src = sourceDataUrl;
+ });
+}
diff --git a/plugins/top-screenshot/src/core/geometry.ts b/plugins/top-screenshot/src/core/geometry.ts
new file mode 100644
index 00000000..3463e007
--- /dev/null
+++ b/plugins/top-screenshot/src/core/geometry.ts
@@ -0,0 +1,93 @@
+export type Point = {
+ x: number;
+ y: number;
+};
+
+export type Rect = {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+};
+
+const MIN_SELECTION_SIZE = 8;
+const MIN_SCALE = 0.3;
+const MAX_SCALE = 3;
+const SCALE_PER_WHEEL_DELTA = 0.001;
+
+const round = (value: number) => Math.round(value * 100) / 100;
+
+export function normalizeRect(start: Point, end: Point): Rect {
+ const x = Math.min(start.x, end.x);
+ const y = Math.min(start.y, end.y);
+
+ return {
+ x,
+ y,
+ width: Math.abs(end.x - start.x),
+ height: Math.abs(end.y - start.y),
+ };
+}
+
+export function isValidSelection(rect: Rect): boolean {
+ return rect.width >= MIN_SELECTION_SIZE && rect.height >= MIN_SELECTION_SIZE;
+}
+
+export function outerBoundsForImage(imageBounds: Rect, frameSize: number): Rect {
+ return {
+ x: Math.round(imageBounds.x - frameSize),
+ y: Math.round(imageBounds.y - frameSize),
+ width: Math.round(imageBounds.width + frameSize * 2),
+ height: Math.round(imageBounds.height + frameSize * 2),
+ };
+}
+
+export function imageBoundsForScale(currentImageBounds: Rect, nextScale: number): Rect {
+ const centerX = currentImageBounds.x + currentImageBounds.width / 2;
+ const centerY = currentImageBounds.y + currentImageBounds.height / 2;
+ const width = round(currentImageBounds.width * nextScale);
+ const height = round(currentImageBounds.height * nextScale);
+
+ return {
+ x: round(centerX - width / 2),
+ y: round(centerY - height / 2),
+ width,
+ height,
+ };
+}
+
+export function clampScale(scale: number): number {
+ return Math.round(Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale)) * 1000) / 1000;
+}
+
+export function scaleFromWheelDelta(currentScale: number, deltaY: number): number {
+ return clampScale(currentScale - deltaY * SCALE_PER_WHEEL_DELTA);
+}
+
+export function rectCenter(rect: Rect): Point {
+ return {
+ x: round(rect.x + rect.width / 2),
+ y: round(rect.y + rect.height / 2),
+ };
+}
+
+export function imageBoundsForOriginalSize(center: Point, originalSize: Pick, scale: number): Rect {
+ const width = round(originalSize.width * scale);
+ const height = round(originalSize.height * scale);
+
+ return {
+ x: round(center.x - width / 2),
+ y: round(center.y - height / 2),
+ width,
+ height,
+ };
+}
+
+export function translateRect(rect: Rect, deltaX: number, deltaY: number): Rect {
+ return {
+ x: round(rect.x + deltaX),
+ y: round(rect.y + deltaY),
+ width: rect.width,
+ height: rect.height,
+ };
+}
diff --git a/plugins/top-screenshot/src/core/launcher.ts b/plugins/top-screenshot/src/core/launcher.ts
new file mode 100644
index 00000000..8f7ba5ff
--- /dev/null
+++ b/plugins/top-screenshot/src/core/launcher.ts
@@ -0,0 +1,70 @@
+import type { BrowserWindowOptions } from '../types/ztools';
+import type { Rect } from './geometry';
+import { outerBoundsForImage } from './geometry';
+import type { CaptureSession, DisplaySnapshot } from './storage';
+
+const PIN_FRAME_SIZE = 3;
+
+export function canStartCapture(isStarting: boolean): boolean {
+ return !isStarting;
+}
+
+export function createCaptureSession(id: string, createdAt: number, displays: DisplaySnapshot[]): CaptureSession {
+ return {
+ id,
+ createdAt,
+ completed: false,
+ displays,
+ };
+}
+
+export function captureWindowOptions(snapshot: DisplaySnapshot): BrowserWindowOptions {
+ return {
+ x: snapshot.bounds.x,
+ y: snapshot.bounds.y,
+ width: snapshot.bounds.width,
+ height: snapshot.bounds.height,
+ useContentSize: true,
+ frame: false,
+ transparent: true,
+ alwaysOnTop: false,
+ skipTaskbar: true,
+ resizable: false,
+ movable: false,
+ minimizable: false,
+ maximizable: false,
+ fullscreen: true,
+ fullscreenable: false,
+ hasShadow: false,
+ backgroundColor: '#00000000',
+ webPreferences: {
+ zoomFactor: 1,
+ },
+ };
+}
+
+export function pinWindowOptions(imageBounds: Rect): BrowserWindowOptions {
+ const outerBounds = outerBoundsForImage(imageBounds, PIN_FRAME_SIZE);
+
+ return {
+ x: outerBounds.x,
+ y: outerBounds.y,
+ width: outerBounds.width,
+ height: outerBounds.height,
+ frame: false,
+ transparent: true,
+ alwaysOnTop: true,
+ skipTaskbar: true,
+ resizable: false,
+ movable: true,
+ minimizable: false,
+ maximizable: false,
+ fullscreenable: false,
+ hasShadow: false,
+ backgroundColor: '#00000000',
+ };
+}
+
+export function statusMessageForStartFailure(error: unknown): string {
+ return error instanceof Error ? error.message : '截图启动失败';
+}
diff --git a/plugins/top-screenshot/src/core/pinWindowMessages.ts b/plugins/top-screenshot/src/core/pinWindowMessages.ts
new file mode 100644
index 00000000..2b8caf32
--- /dev/null
+++ b/plugins/top-screenshot/src/core/pinWindowMessages.ts
@@ -0,0 +1,40 @@
+import type { Rect } from './geometry';
+
+export const PIN_WINDOW_BOUNDS_CHANNEL = 'top-screenshot-pin-bounds';
+export const PIN_WINDOW_CLOSED_CHANNEL = 'top-screenshot-pin-closed';
+
+export type PinWindowBoundsMessage = {
+ id: string;
+ bounds: Rect;
+};
+
+export type PinWindowClosedMessage = {
+ id: string;
+};
+
+export function isPinWindowBoundsMessage(value: unknown): value is PinWindowBoundsMessage {
+ if (!value || typeof value !== 'object') {
+ return false;
+ }
+
+ const message = value as Partial;
+ return typeof message.id === 'string' && isRect(message.bounds);
+}
+
+export function isPinWindowClosedMessage(value: unknown): value is PinWindowClosedMessage {
+ if (!value || typeof value !== 'object') {
+ return false;
+ }
+
+ const message = value as Partial;
+ return typeof message.id === 'string';
+}
+
+function isRect(value: unknown): value is Rect {
+ if (!value || typeof value !== 'object') {
+ return false;
+ }
+
+ const rect = value as Partial;
+ return typeof rect.x === 'number' && typeof rect.y === 'number' && typeof rect.width === 'number' && typeof rect.height === 'number';
+}
diff --git a/plugins/top-screenshot/src/core/routes.ts b/plugins/top-screenshot/src/core/routes.ts
new file mode 100644
index 00000000..ed9b7728
--- /dev/null
+++ b/plugins/top-screenshot/src/core/routes.ts
@@ -0,0 +1,40 @@
+export type RouteView = 'launcher' | 'capture' | 'pin';
+
+export type ParsedRoute = {
+ view: RouteView;
+ params: URLSearchParams;
+};
+
+export function parseRoute(route: string = window.location.hash): ParsedRoute {
+ if (!route) {
+ return {
+ view: 'launcher',
+ params: new URLSearchParams(),
+ };
+ }
+
+ const normalizedRoute = route.startsWith('#') ? route.slice(1) : route;
+ const [path = '', query = ''] = normalizedRoute.split('?', 2);
+ const view = path.replace(/^\//, '');
+
+ if (view === 'capture' || view === 'pin') {
+ return {
+ view,
+ params: new URLSearchParams(query),
+ };
+ }
+
+ return {
+ view: 'launcher',
+ params: new URLSearchParams(),
+ };
+}
+
+export function buildPluginUrl(
+ view: RouteView,
+ params: Record,
+ baseUrl: string,
+): string {
+ const search = new URLSearchParams(params).toString();
+ return `${baseUrl}#/${view}${search ? `?${search}` : ''}`;
+}
diff --git a/plugins/top-screenshot/src/core/storage.ts b/plugins/top-screenshot/src/core/storage.ts
new file mode 100644
index 00000000..46420564
--- /dev/null
+++ b/plugins/top-screenshot/src/core/storage.ts
@@ -0,0 +1,97 @@
+import type { Rect } from './geometry';
+
+export type DisplaySnapshot = {
+ displayId: string;
+ bounds: Rect;
+ imageDataUrl: string;
+ scaleFactor: number;
+};
+
+export type CaptureSession = {
+ id: string;
+ createdAt: number;
+ completed: boolean;
+ displays: DisplaySnapshot[];
+};
+
+export type PinWindowState = {
+ id: string;
+ imageDataUrl: string;
+ originalBounds: Rect;
+ currentBounds: Rect;
+ scale: number;
+ createdAt: number;
+ lastActiveAt: number;
+};
+
+export const captureSessionKey = (id: string) => `capture-session:${id}`;
+export const pinWindowKey = (id: string) => `pin-window:${id}`;
+export const pinWindowRequestKey = (id: string) => `pin-window-request:${id}`;
+
+function readJson(storage: Storage, key: string): T | null {
+ const value = storage.getItem(key);
+
+ if (!value) {
+ return null;
+ }
+
+ try {
+ return JSON.parse(value) as T;
+ } catch {
+ return null;
+ }
+}
+
+export function saveCaptureSession(storage: Storage, session: CaptureSession): void {
+ storage.setItem(captureSessionKey(session.id), JSON.stringify(session));
+}
+
+export function loadCaptureSession(storage: Storage, id: string): CaptureSession | null {
+ return readJson(storage, captureSessionKey(id));
+}
+
+export function markCaptureSessionCompleted(storage: Storage, id: string): void {
+ const session = loadCaptureSession(storage, id);
+
+ if (!session) {
+ return;
+ }
+
+ saveCaptureSession(storage, {
+ ...session,
+ completed: true,
+ });
+}
+
+export function finishCaptureSession(storage: Storage, id: string): void {
+ storage.removeItem(captureSessionKey(id));
+}
+
+export function isCaptureSessionFinishedEvent(event: Pick, id: string): boolean {
+ return event.key === captureSessionKey(id) && event.newValue === null;
+}
+
+export function savePinWindow(storage: Storage, state: PinWindowState): void {
+ storage.setItem(pinWindowKey(state.id), JSON.stringify(state));
+}
+
+export function loadPinWindow(storage: Storage, id: string): PinWindowState | null {
+ return readJson(storage, pinWindowKey(id));
+}
+
+export function removePinWindow(storage: Storage, id: string): void {
+ storage.removeItem(pinWindowKey(id));
+}
+
+export function savePinWindowRequest(storage: Storage, pinWindowId: string): void {
+ storage.setItem(pinWindowRequestKey(pinWindowId), JSON.stringify({ pinWindowId }));
+}
+
+export function isPinWindowRequestEvent(event: Pick): string | null {
+ if (!event.key?.startsWith('pin-window-request:') || !event.newValue) {
+ return null;
+ }
+
+ const request = JSON.parse(event.newValue) as { pinWindowId?: string };
+ return request.pinWindowId ?? null;
+}
diff --git a/plugins/top-screenshot/src/core/ztoolsBridge.ts b/plugins/top-screenshot/src/core/ztoolsBridge.ts
new file mode 100644
index 00000000..a7fe6f26
--- /dev/null
+++ b/plugins/top-screenshot/src/core/ztoolsBridge.ts
@@ -0,0 +1,67 @@
+import type { DisplaySnapshot } from './storage';
+import type { BrowserWindowOptions, DesktopCaptureSource, ZToolsApi, ZToolsDisplay } from '../types/ztools';
+
+export function getZTools(): ZToolsApi | null {
+ return window.ztools ?? null;
+}
+
+export function requireZTools(): ZToolsApi {
+ const api = getZTools();
+
+ if (!api) {
+ throw new Error('ZTools API is not available in this window.');
+ }
+
+ return api;
+}
+
+export function findSourceForDisplay(display: ZToolsDisplay, sources: DesktopCaptureSource[]): DesktopCaptureSource | null {
+ const displayId = String(display.id);
+ return sources.find((source) => source.display_id === displayId) ?? sources.find((source) => source.id.split(':').includes(displayId)) ?? null;
+}
+
+export function mapDisplaysToSnapshots(displays: ZToolsDisplay[], sources: DesktopCaptureSource[]): DisplaySnapshot[] {
+ return displays.flatMap((display) => {
+ const source = findSourceForDisplay(display, sources);
+
+ if (!source) {
+ return [];
+ }
+
+ return [
+ {
+ displayId: String(display.id),
+ bounds: display.bounds,
+ imageDataUrl: source.thumbnail.toDataURL(),
+ scaleFactor: display.scaleFactor ?? 1,
+ },
+ ];
+ });
+}
+
+export async function getDisplaySnapshots(api: ZToolsApi): Promise {
+ const displays = api.getAllDisplays();
+
+ if (displays.length === 0) {
+ return [];
+ }
+
+ const maxWidth = Math.max(...displays.map((display) => Math.ceil(display.bounds.width * (display.scaleFactor ?? 1))));
+ const maxHeight = Math.max(...displays.map((display) => Math.ceil(display.bounds.height * (display.scaleFactor ?? 1))));
+ const sources = await api.desktopCaptureSources({
+ types: ['screen'],
+ thumbnailSize: { width: maxWidth, height: maxHeight },
+ });
+
+ return mapDisplaysToSnapshots(displays, sources);
+}
+
+export function createPluginWindow(api: ZToolsApi, url: string, options: BrowserWindowOptions, onReady?: () => void) {
+ const win = api.createBrowserWindow(url, options, () => {
+ win?.setAlwaysOnTop?.(true, 'floating');
+ win?.focus?.();
+ onReady?.();
+ });
+
+ return win;
+}
diff --git a/plugins/top-screenshot/src/main.ts b/plugins/top-screenshot/src/main.ts
new file mode 100644
index 00000000..794aa85b
--- /dev/null
+++ b/plugins/top-screenshot/src/main.ts
@@ -0,0 +1,6 @@
+import './types/ztools';
+import { createApp } from 'vue';
+import App from './App.vue';
+import './styles.css';
+
+createApp(App).mount('#app');
diff --git a/plugins/top-screenshot/src/styles.css b/plugins/top-screenshot/src/styles.css
new file mode 100644
index 00000000..f62cc96f
--- /dev/null
+++ b/plugins/top-screenshot/src/styles.css
@@ -0,0 +1,134 @@
+html,
+body,
+#app {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+ font-family: "Microsoft YaHei UI", "Segoe UI", sans-serif;
+}
+
+body {
+ background: transparent;
+}
+
+.app-shell {
+ display: grid;
+ min-height: 100vh;
+ place-items: center;
+ color: #e5e7eb;
+ background: #111827;
+}
+
+.capture-view {
+ position: relative;
+ width: 100vw;
+ height: 100vh;
+ cursor: crosshair;
+ -webkit-user-select: none;
+ user-select: none;
+ background: transparent;
+}
+
+.capture-image {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: fill;
+ pointer-events: none;
+}
+
+.capture-view::after {
+ position: absolute;
+ inset: 0;
+ content: "";
+ background:
+ radial-gradient(circle at 50% 50%, rgba(15, 23, 42, 0.24), rgba(2, 6, 23, 0.46)),
+ rgba(0, 0, 0, 0.22);
+ pointer-events: none;
+}
+
+.selection-box {
+ position: absolute;
+ z-index: 1;
+ border: 1px solid rgba(14, 165, 233, 0.95);
+ background: rgba(255, 255, 255, 0.04);
+ box-shadow:
+ 0 0 0 9999px rgba(0, 0, 0, 0.42),
+ 0 0 0 1px rgba(255, 255, 255, 0.85) inset,
+ 0 0 18px rgba(14, 165, 233, 0.7);
+}
+
+.selection-box::before,
+.selection-box::after {
+ position: absolute;
+ width: 7px;
+ height: 7px;
+ border-color: #e0f2fe;
+ content: "";
+}
+
+.selection-box::before {
+ top: -3px;
+ left: -3px;
+ border-top: 2px solid;
+ border-left: 2px solid;
+}
+
+.selection-box::after {
+ right: -3px;
+ bottom: -3px;
+ border-right: 2px solid;
+ border-bottom: 2px solid;
+}
+
+.capture-error {
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ display: grid;
+ place-items: center;
+ color: #f9fafb;
+ background: #020617;
+}
+
+.pin-window {
+ display: inline-flex;
+ box-sizing: border-box;
+ width: 100vw;
+ height: 100vh;
+ padding: 3px;
+ align-items: center;
+ justify-content: center;
+ -webkit-user-select: none;
+ user-select: none;
+ background: transparent;
+}
+
+.pin-frame {
+ border: 1px solid rgba(14, 165, 233, 0.85);
+ border-radius: 4px;
+ cursor: grab;
+ overflow: hidden;
+}
+
+.pin-image {
+ display: block;
+ border: 0;
+ cursor: inherit;
+ object-fit: fill;
+ transform-origin: top left;
+ will-change: transform;
+}
+
+.pin-window:active .pin-frame {
+ cursor: grabbing;
+}
+
+.pin-window-empty {
+ display: grid;
+ place-items: center;
+ color: #f9fafb;
+ background: rgba(17, 24, 39, 0.92);
+}
diff --git a/plugins/top-screenshot/src/types/ztools.ts b/plugins/top-screenshot/src/types/ztools.ts
new file mode 100644
index 00000000..a7b6852e
--- /dev/null
+++ b/plugins/top-screenshot/src/types/ztools.ts
@@ -0,0 +1,76 @@
+import type { Rect } from '../core/geometry';
+
+export type ZToolsDisplay = {
+ id: number | string;
+ bounds: Rect;
+ scaleFactor?: number;
+};
+
+export type ScreenCaptureCallback = (imageDataUrl: string | null, bounds?: Rect) => void;
+
+export type DesktopCaptureSource = {
+ id: string;
+ name: string;
+ display_id?: string;
+ thumbnail: {
+ toDataURL(): string;
+ };
+};
+
+export type BrowserWindowProxy = {
+ close(): void;
+ focus?(): void;
+ show?(): void;
+ setAlwaysOnTop?(flag: boolean, level?: string): void;
+ setBounds?(bounds: Rect): void;
+ setPosition?(x: number, y: number): void;
+ setSize?(width: number, height: number): void;
+};
+
+export type BrowserWindowOptions = {
+ x?: number;
+ y?: number;
+ width?: number;
+ height?: number;
+ frame?: boolean;
+ transparent?: boolean;
+ alwaysOnTop?: boolean;
+ skipTaskbar?: boolean;
+ resizable?: boolean;
+ movable?: boolean;
+ minimizable?: boolean;
+ maximizable?: boolean;
+ fullscreenable?: boolean;
+ hasShadow?: boolean;
+ backgroundColor?: string;
+ fullscreen?: boolean;
+ useContentSize?: boolean;
+ webPreferences?: {
+ preload?: string;
+ zoomFactor?: number;
+ devTools?: boolean;
+ };
+};
+
+export type ZToolsApi = {
+ onPluginEnter?(callback: () => void): void;
+ onPluginReady?(callback: () => void): void;
+ hideMainWindow?(isRestorePreWindow?: boolean): void;
+ outPlugin?(isKill?: boolean): void;
+ sendToParent?(channel: string, ...args: unknown[]): void;
+ onParentMessage?(channel: string, callback: (...args: unknown[]) => void): () => void;
+ screenCapture?(callback: ScreenCaptureCallback): void;
+ screenToDipRect?(rect: Rect): Rect;
+ getAllDisplays(): ZToolsDisplay[];
+ desktopCaptureSources(options: {
+ types: Array<'screen' | 'window'>;
+ thumbnailSize?: { width: number; height: number };
+ }): Promise | DesktopCaptureSource[];
+ createBrowserWindow(url: string, options: BrowserWindowOptions, callback?: () => void): BrowserWindowProxy | null;
+};
+
+declare global {
+ interface Window {
+ ztools?: ZToolsApi;
+ }
+}
diff --git a/plugins/top-screenshot/src/views/CaptureView.vue b/plugins/top-screenshot/src/views/CaptureView.vue
new file mode 100644
index 00000000..f414ec4a
--- /dev/null
+++ b/plugins/top-screenshot/src/views/CaptureView.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
+ 没有找到当前显示器截图。
+
+
diff --git a/plugins/top-screenshot/src/views/LauncherView.vue b/plugins/top-screenshot/src/views/LauncherView.vue
new file mode 100644
index 00000000..135b89d7
--- /dev/null
+++ b/plugins/top-screenshot/src/views/LauncherView.vue
@@ -0,0 +1,174 @@
+
+
+
diff --git a/plugins/top-screenshot/src/views/PinView.vue b/plugins/top-screenshot/src/views/PinView.vue
new file mode 100644
index 00000000..ff207820
--- /dev/null
+++ b/plugins/top-screenshot/src/views/PinView.vue
@@ -0,0 +1,240 @@
+
+
+
+
+
+
![置顶截图]()
+
+
+ 截图数据不存在
+
diff --git a/plugins/top-screenshot/src/vite-env.d.ts b/plugins/top-screenshot/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/plugins/top-screenshot/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/plugins/top-screenshot/src/vue-shims.d.ts b/plugins/top-screenshot/src/vue-shims.d.ts
new file mode 100644
index 00000000..6aff71b8
--- /dev/null
+++ b/plugins/top-screenshot/src/vue-shims.d.ts
@@ -0,0 +1,6 @@
+declare module '*.vue' {
+ import type { DefineComponent } from 'vue';
+
+ const component: DefineComponent, Record, unknown>;
+ export default component;
+}
diff --git a/plugins/top-screenshot/tests/captureView.test.ts b/plugins/top-screenshot/tests/captureView.test.ts
new file mode 100644
index 00000000..bbafdeb3
--- /dev/null
+++ b/plugins/top-screenshot/tests/captureView.test.ts
@@ -0,0 +1,91 @@
+import { mount } from '@vue/test-utils';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { CaptureSession } from '../src/core/storage';
+import type { BrowserWindowOptions, ZToolsApi } from '../src/types/ztools';
+
+vi.mock('../src/core/crop', () => ({
+ cropImageDataUrl: vi.fn(async () => 'data:image/png;base64,cropped'),
+}));
+
+describe('CaptureView', () => {
+ beforeEach(() => {
+ window.location.hash = '#/capture?sessionId=session-1&displayId=display-1';
+ window.localStorage.clear();
+ vi.stubGlobal('crypto', { randomUUID: () => 'pin-1' });
+ vi.setSystemTime(new Date('2026-06-08T06:00:00.000Z'));
+ });
+
+ afterEach(() => {
+ window.localStorage.clear();
+ vi.unstubAllGlobals();
+ vi.useRealTimers();
+ delete window.ztools;
+ });
+
+ it('does not render the captured screen image behind the selection overlay', async () => {
+ const session: CaptureSession = {
+ id: 'session-1',
+ createdAt: 1780898400000,
+ completed: false,
+ displays: [
+ {
+ displayId: 'display-1',
+ bounds: { x: 100, y: 80, width: 800, height: 600 },
+ imageDataUrl: 'data:image/png;base64,screen',
+ scaleFactor: 1,
+ },
+ ],
+ };
+ window.localStorage.setItem('capture-session:session-1', JSON.stringify(session));
+
+ const { default: CaptureView } = await import('../src/views/CaptureView.vue');
+ const wrapper = mount(CaptureView);
+
+ expect(wrapper.find('.capture-image').exists()).toBe(false);
+ });
+
+ it('saves a pin request for the launcher instead of creating a child window from capture', async () => {
+ vi.useFakeTimers();
+ const session: CaptureSession = {
+ id: 'session-1',
+ createdAt: 1780898400000,
+ completed: false,
+ displays: [
+ {
+ displayId: 'display-1',
+ bounds: { x: 100, y: 80, width: 800, height: 600 },
+ imageDataUrl: 'data:image/png;base64,screen',
+ scaleFactor: 1,
+ },
+ ],
+ };
+ window.localStorage.setItem('capture-session:session-1', JSON.stringify(session));
+
+ const createdWindows: Array<{ url: string; options: BrowserWindowOptions }> = [];
+ window.ztools = {
+ getAllDisplays: () => [],
+ desktopCaptureSources: () => [],
+ createBrowserWindow: (url, options) => {
+ createdWindows.push({ url, options });
+ return {
+ close: () => {},
+ focus: () => {},
+ setAlwaysOnTop: () => {},
+ };
+ },
+ } satisfies ZToolsApi;
+ const closeSpy = vi.spyOn(window, 'close').mockImplementation(() => undefined);
+ const { default: CaptureView } = await import('../src/views/CaptureView.vue');
+ const wrapper = mount(CaptureView);
+
+ await wrapper.find('.capture-view').trigger('mousedown', { clientX: 10, clientY: 20 });
+ await wrapper.find('.capture-view').trigger('mousemove', { clientX: 110, clientY: 90 });
+ await wrapper.find('.capture-view').trigger('mouseup');
+ await vi.runAllTimersAsync();
+
+ expect(createdWindows).toEqual([]);
+ expect(JSON.parse(window.localStorage.getItem('pin-window:pin-1')!)).toMatchObject({ id: 'pin-1' });
+ expect(JSON.parse(window.localStorage.getItem('pin-window-request:pin-1')!)).toEqual({ pinWindowId: 'pin-1' });
+ expect(closeSpy).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/plugins/top-screenshot/tests/crop.test.ts b/plugins/top-screenshot/tests/crop.test.ts
new file mode 100644
index 00000000..9a0a5e60
--- /dev/null
+++ b/plugins/top-screenshot/tests/crop.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, it } from 'vitest';
+import { selectionToSourcePixels } from '../src/core/crop';
+
+describe('crop', () => {
+ it('converts selection bounds to scaled source pixels', () => {
+ expect(selectionToSourcePixels({ x: 10.2, y: 20.6, width: 100.4, height: 80.2 }, 1.5)).toEqual({
+ x: 15,
+ y: 31,
+ width: 151,
+ height: 120,
+ });
+ });
+});
diff --git a/plugins/top-screenshot/tests/geometry.test.ts b/plugins/top-screenshot/tests/geometry.test.ts
new file mode 100644
index 00000000..1c276168
--- /dev/null
+++ b/plugins/top-screenshot/tests/geometry.test.ts
@@ -0,0 +1,84 @@
+import { describe, expect, it } from 'vitest';
+import {
+ clampScale,
+ imageBoundsForScale,
+ isValidSelection,
+ normalizeRect,
+ outerBoundsForImage,
+ scaleFromWheelDelta,
+ translateRect,
+} from '../src/core/geometry';
+
+describe('geometry', () => {
+ it('normalizes a drag from bottom-right to top-left', () => {
+ expect(normalizeRect({ x: 30, y: 40 }, { x: 10, y: 15 })).toEqual({
+ x: 10,
+ y: 15,
+ width: 20,
+ height: 25,
+ });
+ });
+
+ it('rejects tiny selections', () => {
+ expect(isValidSelection({ x: 0, y: 0, width: 7, height: 20 })).toBe(false);
+ expect(isValidSelection({ x: 0, y: 0, width: 20, height: 7 })).toBe(false);
+ expect(isValidSelection({ x: 0, y: 0, width: 8, height: 8 })).toBe(true);
+ });
+
+ it('adds frame space around an image window', () => {
+ expect(outerBoundsForImage({ x: 100, y: 80, width: 200, height: 120 }, 6)).toEqual({
+ x: 94,
+ y: 74,
+ width: 212,
+ height: 132,
+ });
+ });
+
+ it('scales around the current image center', () => {
+ expect(imageBoundsForScale({ x: 100, y: 80, width: 200, height: 120 }, 1.5)).toEqual({
+ x: 50,
+ y: 50,
+ width: 300,
+ height: 180,
+ });
+ });
+
+ it('clamps scale and applies wheel direction', () => {
+ expect(clampScale(0.1)).toBe(0.3);
+ expect(clampScale(4)).toBe(3);
+ expect(scaleFromWheelDelta(1, -100)).toBe(1.1);
+ expect(scaleFromWheelDelta(1, 100)).toBe(0.9);
+ });
+
+ it('scales proportionally to wheel delta for smooth trackpad input', () => {
+ expect(scaleFromWheelDelta(1, -1)).toBe(1.001);
+ expect(scaleFromWheelDelta(1, -16)).toBe(1.016);
+ expect(scaleFromWheelDelta(1, 16)).toBe(0.984);
+ });
+
+ it('translates a rectangle by a delta', () => {
+ expect(translateRect({ x: 10, y: 20, width: 30, height: 40 }, 5, -8)).toEqual({
+ x: 15,
+ y: 12,
+ width: 30,
+ height: 40,
+ });
+ });
+
+ it('returns the rectangle center', async () => {
+ const { rectCenter } = await import('../src/core/geometry');
+
+ expect(rectCenter({ x: 10, y: 20, width: 30, height: 40 })).toEqual({ x: 25, y: 40 });
+ });
+
+ it('scales an original image size around an existing center', async () => {
+ const { imageBoundsForOriginalSize } = await import('../src/core/geometry');
+
+ expect(imageBoundsForOriginalSize({ x: 200, y: 140 }, { width: 100, height: 80 }, 1.5)).toEqual({
+ x: 125,
+ y: 80,
+ width: 150,
+ height: 120,
+ });
+ });
+});
diff --git a/plugins/top-screenshot/tests/launcher.test.ts b/plugins/top-screenshot/tests/launcher.test.ts
new file mode 100644
index 00000000..96ef3a64
--- /dev/null
+++ b/plugins/top-screenshot/tests/launcher.test.ts
@@ -0,0 +1,76 @@
+import { describe, expect, it } from 'vitest';
+import { canStartCapture, captureWindowOptions, createCaptureSession, pinWindowOptions, statusMessageForStartFailure } from '../src/core/launcher';
+import type { DisplaySnapshot } from '../src/core/storage';
+
+const display: DisplaySnapshot = {
+ displayId: '1',
+ bounds: { x: 10, y: 20, width: 800, height: 600 },
+ imageDataUrl: 'data:image/png;base64,screen',
+ scaleFactor: 1,
+};
+
+describe('launcher', () => {
+ it('creates a pending capture session from display snapshots', () => {
+ expect(createCaptureSession('capture-1', 1780660800000, [display])).toEqual({
+ id: 'capture-1',
+ createdAt: 1780660800000,
+ completed: false,
+ displays: [display],
+ });
+ });
+
+ it('creates a fullscreen capture overlay window so the real taskbar is covered', () => {
+ expect(captureWindowOptions(display)).toEqual({
+ x: 10,
+ y: 20,
+ width: 800,
+ height: 600,
+ useContentSize: true,
+ frame: false,
+ transparent: true,
+ alwaysOnTop: false,
+ skipTaskbar: true,
+ resizable: false,
+ movable: false,
+ minimizable: false,
+ maximizable: false,
+ fullscreen: true,
+ fullscreenable: false,
+ hasShadow: false,
+ backgroundColor: '#00000000',
+ webPreferences: {
+ zoomFactor: 1,
+ },
+ });
+ });
+
+ it('creates transparent always-on-top pin window options around the selected image', () => {
+ expect(pinWindowOptions({ x: 120, y: 90, width: 320, height: 180 })).toEqual({
+ x: 117,
+ y: 87,
+ width: 326,
+ height: 186,
+ frame: false,
+ transparent: true,
+ alwaysOnTop: true,
+ skipTaskbar: true,
+ resizable: false,
+ movable: true,
+ minimizable: false,
+ maximizable: false,
+ fullscreenable: false,
+ hasShadow: false,
+ backgroundColor: '#00000000',
+ });
+ });
+
+ it('only blocks starts while a capture session is currently starting', () => {
+ expect(canStartCapture(false)).toBe(true);
+ expect(canStartCapture(true)).toBe(false);
+ });
+
+ it('formats unknown start failures for display', () => {
+ expect(statusMessageForStartFailure('bad')).toBe('截图启动失败');
+ expect(statusMessageForStartFailure(new Error('missing ztools'))).toBe('missing ztools');
+ });
+});
diff --git a/plugins/top-screenshot/tests/launcherView.test.ts b/plugins/top-screenshot/tests/launcherView.test.ts
new file mode 100644
index 00000000..cbc80670
--- /dev/null
+++ b/plugins/top-screenshot/tests/launcherView.test.ts
@@ -0,0 +1,269 @@
+import { mount } from '@vue/test-utils';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import type { PinWindowState } from '../src/core/storage';
+import type { BrowserWindowOptions, ZToolsApi } from '../src/types/ztools';
+import LauncherView from '../src/views/LauncherView.vue';
+
+describe('LauncherView', () => {
+ afterEach(() => {
+ window.localStorage.clear();
+ vi.restoreAllMocks();
+ vi.unstubAllGlobals();
+ vi.useRealTimers();
+ delete window.ztools;
+ });
+
+ it('does not render a visible loading or retry interface', () => {
+ const wrapper = mount(LauncherView);
+
+ expect(wrapper.text()).toBe('');
+ expect(wrapper.find('button').exists()).toBe(false);
+ });
+
+ it('creates a pin window from the native screenshot result and exits plugin selection', async () => {
+ vi.stubGlobal('crypto', { randomUUID: () => 'pin-native' });
+ vi.setSystemTime(new Date('2026-06-08T06:00:00.000Z'));
+ const createdWindows: Array<{ url: string; options: BrowserWindowOptions }> = [];
+ const outPluginCalls: boolean[] = [];
+ window.ztools = {
+ onPluginEnter: () => {},
+ onPluginReady: () => {},
+ hideMainWindow: () => {},
+ outPlugin: (isKill?: boolean) => {
+ outPluginCalls.push(Boolean(isKill));
+ },
+ screenCapture: (callback) => {
+ callback('data:image/png;base64,native', { x: 20, y: 1488, width: 800, height: 112 });
+ },
+ screenToDipRect: (rect) => ({ x: rect.x / 2, y: rect.y / 2, width: rect.width / 2, height: rect.height / 2 }),
+ getAllDisplays: () => [],
+ desktopCaptureSources: () => [],
+ createBrowserWindow: (url, options) => {
+ createdWindows.push({ url, options });
+ return {
+ close: () => {},
+ focus: () => {},
+ setAlwaysOnTop: () => {},
+ };
+ },
+ } satisfies ZToolsApi;
+
+ mount(LauncherView);
+ await vi.dynamicImportSettled();
+
+ expect(createdWindows[0]?.url).toContain('#/pin?id=pin-native');
+ expect(createdWindows[0]?.options).toMatchObject({ x: 7, y: 741, width: 406, height: 62 });
+ expect(JSON.parse(window.localStorage.getItem('pin-window:pin-native')!)).toMatchObject({
+ imageDataUrl: 'data:image/png;base64,native',
+ originalBounds: { x: 10, y: 744, width: 400, height: 56 },
+ currentBounds: { x: 10, y: 744, width: 400, height: 56 },
+ });
+ expect(outPluginCalls).toEqual([false]);
+ });
+
+ it('opens pin windows from storage requests in the launcher process', async () => {
+ const createdWindows: Array<{ url: string; options: BrowserWindowOptions }> = [];
+ window.ztools = {
+ onPluginEnter: () => {},
+ onPluginReady: () => {},
+ getAllDisplays: () => [],
+ desktopCaptureSources: () => [],
+ createBrowserWindow: (url, options) => {
+ createdWindows.push({ url, options });
+ return {
+ close: () => {},
+ focus: () => {},
+ setAlwaysOnTop: () => {},
+ };
+ },
+ } satisfies ZToolsApi;
+ const state: PinWindowState = {
+ id: 'pin-1',
+ imageDataUrl: 'data:image/png;base64,cropped',
+ originalBounds: { x: 120, y: 90, width: 320, height: 180 },
+ currentBounds: { x: 120, y: 90, width: 320, height: 180 },
+ scale: 1,
+ createdAt: 1780898400000,
+ lastActiveAt: 1780898400000,
+ };
+ window.localStorage.setItem('pin-window:pin-1', JSON.stringify(state));
+ mount(LauncherView);
+
+ window.dispatchEvent(
+ new StorageEvent('storage', {
+ key: 'pin-window-request:pin-1',
+ newValue: JSON.stringify({ pinWindowId: 'pin-1' }),
+ }),
+ );
+
+ expect(createdWindows[0]?.url).toContain('#/pin?id=pin-1');
+ expect(createdWindows[0]?.options).toMatchObject({ x: 117, y: 87, width: 326, height: 186 });
+ });
+
+ it('keeps native screenshot bounds in DIP when ztools does not provide a converter', async () => {
+ vi.stubGlobal('crypto', { randomUUID: () => 'pin-native' });
+ vi.setSystemTime(new Date('2026-06-08T06:00:00.000Z'));
+ vi.spyOn(window, 'devicePixelRatio', 'get').mockReturnValue(2);
+ const createdWindows: Array<{ url: string; options: BrowserWindowOptions }> = [];
+ window.ztools = {
+ onPluginEnter: () => {},
+ onPluginReady: () => {},
+ hideMainWindow: () => {},
+ outPlugin: () => {},
+ screenCapture: (callback) => {
+ callback('data:image/png;base64,native', { x: 20, y: 1488, width: 800, height: 112 });
+ },
+ getAllDisplays: () => [],
+ desktopCaptureSources: () => [],
+ createBrowserWindow: (url, options) => {
+ createdWindows.push({ url, options });
+ return {
+ close: () => {},
+ focus: () => {},
+ setAlwaysOnTop: () => {},
+ };
+ },
+ } satisfies ZToolsApi;
+
+ mount(LauncherView);
+ await vi.dynamicImportSettled();
+
+ expect(createdWindows[0]?.options).toMatchObject({ x: 7, y: 741, width: 406, height: 62 });
+ expect(JSON.parse(window.localStorage.getItem('pin-window:pin-native')!)).toMatchObject({
+ originalBounds: { x: 10, y: 744, width: 400, height: 56 },
+ currentBounds: { x: 10, y: 744, width: 400, height: 56 },
+ });
+ });
+
+ it('moves an opened pin window through its BrowserWindow proxy', async () => {
+ const win = createWindowProxy();
+ const { storageHandler, parentHandlers } = mountLauncherWithParentHandlers({
+ createBrowserWindow: () => win,
+ });
+ window.localStorage.setItem('pin-window:pin-1', JSON.stringify(createPinState('pin-1')));
+
+ storageHandler!(
+ new StorageEvent('storage', {
+ key: 'pin-window-request:pin-1',
+ newValue: JSON.stringify({ pinWindowId: 'pin-1' }),
+ }),
+ );
+ parentHandlers['top-screenshot-pin-bounds']!({
+ id: 'pin-1',
+ bounds: { x: 127, y: 92, width: 326, height: 186 },
+ });
+
+ expect(win.setBounds).toHaveBeenCalledWith({ x: 127, y: 92, width: 326, height: 186 });
+ });
+
+ it('exits the plugin after the last pin window closes', async () => {
+ const outPluginCalls: boolean[] = [];
+ const windows = [createWindowProxy(), createWindowProxy()];
+ let index = 0;
+ const { storageHandler, parentHandlers } = mountLauncherWithParentHandlers({
+ outPlugin: (isKill?: boolean) => outPluginCalls.push(Boolean(isKill)),
+ createBrowserWindow: () => windows[index++],
+ });
+ window.localStorage.setItem('pin-window:pin-1', JSON.stringify(createPinState('pin-1')));
+ window.localStorage.setItem('pin-window:pin-2', JSON.stringify(createPinState('pin-2')));
+
+ storageHandler!(
+ new StorageEvent('storage', {
+ key: 'pin-window-request:pin-1',
+ newValue: JSON.stringify({ pinWindowId: 'pin-1' }),
+ }),
+ );
+ storageHandler!(
+ new StorageEvent('storage', {
+ key: 'pin-window-request:pin-2',
+ newValue: JSON.stringify({ pinWindowId: 'pin-2' }),
+ }),
+ );
+
+ parentHandlers['top-screenshot-pin-closed']!({ id: 'pin-1' });
+ expect(outPluginCalls).toEqual([]);
+
+ parentHandlers['top-screenshot-pin-closed']!({ id: 'pin-2' });
+ expect(outPluginCalls).toEqual([true]);
+ });
+
+ it('ignores close messages for untracked pin windows', async () => {
+ const outPluginCalls: boolean[] = [];
+ const { parentHandlers } = mountLauncherWithParentHandlers({
+ outPlugin: (isKill?: boolean) => outPluginCalls.push(Boolean(isKill)),
+ });
+
+ parentHandlers['top-screenshot-pin-closed']!({ id: 'missing-pin' });
+
+ expect(outPluginCalls).toEqual([]);
+ });
+
+ it('does not exit twice for duplicate close messages', async () => {
+ const outPluginCalls: boolean[] = [];
+ const { storageHandler, parentHandlers } = mountLauncherWithParentHandlers({
+ outPlugin: (isKill?: boolean) => outPluginCalls.push(Boolean(isKill)),
+ createBrowserWindow: () => createWindowProxy(),
+ });
+ window.localStorage.setItem('pin-window:pin-1', JSON.stringify(createPinState('pin-1')));
+
+ storageHandler!(
+ new StorageEvent('storage', {
+ key: 'pin-window-request:pin-1',
+ newValue: JSON.stringify({ pinWindowId: 'pin-1' }),
+ }),
+ );
+ parentHandlers['top-screenshot-pin-closed']!({ id: 'pin-1' });
+ parentHandlers['top-screenshot-pin-closed']!({ id: 'pin-1' });
+
+ expect(outPluginCalls).toEqual([true]);
+ });
+});
+
+function createPinState(id: string): PinWindowState {
+ return {
+ id,
+ imageDataUrl: 'data:image/png;base64,cropped',
+ originalBounds: { x: 120, y: 90, width: 320, height: 180 },
+ currentBounds: { x: 120, y: 90, width: 320, height: 180 },
+ scale: 1,
+ createdAt: 1780898400000,
+ lastActiveAt: 1780898400000,
+ };
+}
+
+function createWindowProxy() {
+ return {
+ close: vi.fn(),
+ focus: vi.fn(),
+ setAlwaysOnTop: vi.fn(),
+ setBounds: vi.fn(),
+ };
+}
+
+function mountLauncherWithParentHandlers(overrides: Partial) {
+ let storageHandler: ((event: StorageEvent) => void) | null = null;
+ const parentHandlers: Record void) | undefined> = {};
+ const addEventListenerSpy = vi.spyOn(window, 'addEventListener').mockImplementation((type, listener) => {
+ if (type === 'storage') {
+ storageHandler = listener as (event: StorageEvent) => void;
+ }
+ });
+
+ window.ztools = {
+ onPluginEnter: () => {},
+ onPluginReady: () => {},
+ onParentMessage: (channel, callback) => {
+ parentHandlers[channel] = callback as (message: unknown) => void;
+ return () => undefined;
+ },
+ getAllDisplays: () => [],
+ desktopCaptureSources: () => [],
+ createBrowserWindow: () => null,
+ ...overrides,
+ } satisfies ZToolsApi;
+
+ mount(LauncherView);
+ addEventListenerSpy.mockRestore();
+
+ return { storageHandler, parentHandlers };
+}
diff --git a/plugins/top-screenshot/tests/package.test.ts b/plugins/top-screenshot/tests/package.test.ts
new file mode 100644
index 00000000..0d876583
--- /dev/null
+++ b/plugins/top-screenshot/tests/package.test.ts
@@ -0,0 +1,51 @@
+import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import path from 'node:path';
+import { describe, expect, it } from 'vitest';
+import { packagePlugin } from '../scripts/package-plugin';
+
+describe('packagePlugin', () => {
+ it('creates a flat ztools plugin directory with rewritten entry paths', async () => {
+ const root = await mkdtemp(path.join(tmpdir(), 'top-screenshot-package-'));
+ await mkdir(path.join(root, 'dist', 'assets'), { recursive: true });
+ await mkdir(path.join(root, 'assets'), { recursive: true });
+ await writeFile(
+ path.join(root, 'plugin.json'),
+ JSON.stringify({
+ name: 'top-screenshot',
+ title: '截图置顶',
+ main: 'dist/index.html',
+ preload: 'dist/preload.cjs',
+ logo: 'assets/logo.png',
+ features: [],
+ }),
+ );
+ await writeFile(path.join(root, 'dist', 'index.html'), '');
+ await writeFile(path.join(root, 'dist', 'preload.cjs'), 'globalThis.preloaded = true;');
+ await writeFile(path.join(root, 'dist', 'assets', 'app.js'), 'console.log("app");');
+ await writeFile(path.join(root, 'assets', 'logo.png'), 'png');
+
+ const outDir = await packagePlugin(root);
+
+ expect(outDir).toBe(path.join(root, 'release', 'top-screenshot'));
+ await expect(readFile(path.join(outDir, 'index.html'), 'utf8')).resolves.toBe('');
+ await expect(readFile(path.join(outDir, 'preload.cjs'), 'utf8')).resolves.toBe('globalThis.preloaded = true;');
+ await expect(readFile(path.join(outDir, 'assets', 'app.js'), 'utf8')).resolves.toBe('console.log("app");');
+ await expect(readFile(path.join(outDir, 'logo.png'), 'utf8')).resolves.toBe('png');
+
+ const zpx = await readFile(path.join(root, 'release', 'top-screenshot.zpx'));
+ expect(zpx.subarray(0, 4)).toEqual(Buffer.from('PK\x03\x04', 'binary'));
+ expect(zpx.toString('utf8')).toContain('assets/app.js');
+ expect(zpx.toString('utf8')).toContain('console.log("app");');
+ expect(zpx.toString('utf8')).toContain('png');
+
+ const packagedPlugin = JSON.parse(await readFile(path.join(outDir, 'plugin.json'), 'utf8'));
+ expect(packagedPlugin).toMatchObject({
+ name: 'top-screenshot',
+ title: '截图置顶',
+ main: 'index.html',
+ preload: 'preload.cjs',
+ logo: 'logo.png',
+ });
+ });
+});
diff --git a/plugins/top-screenshot/tests/pinView.test.ts b/plugins/top-screenshot/tests/pinView.test.ts
new file mode 100644
index 00000000..fbe70f5a
--- /dev/null
+++ b/plugins/top-screenshot/tests/pinView.test.ts
@@ -0,0 +1,230 @@
+import { enableAutoUnmount, mount } from '@vue/test-utils';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import type { PinWindowState } from '../src/core/storage';
+
+enableAutoUnmount(afterEach);
+
+describe('PinView', () => {
+ afterEach(() => {
+ window.localStorage.clear();
+ vi.restoreAllMocks();
+ vi.unstubAllGlobals();
+ vi.useRealTimers();
+ vi.resetModules();
+ });
+
+ it('asks the parent BrowserWindow proxy to move the pin window', async () => {
+ vi.setSystemTime(new Date('2026-06-08T06:00:00.000Z'));
+ window.location.hash = '#/pin?id=pin-1';
+ const state: PinWindowState = createPinState();
+ window.localStorage.setItem('pin-window:pin-1', JSON.stringify(state));
+ const sendToParent = vi.fn();
+ window.ztools = createZTools(sendToParent);
+ const moveToSpy = vi.spyOn(window, 'moveTo').mockImplementation(() => undefined);
+ const resizeToSpy = vi.spyOn(window, 'resizeTo').mockImplementation(() => undefined);
+ const { default: PinView } = await import('../src/views/PinView.vue');
+ const wrapper = mount(PinView);
+
+ await wrapper.find('.pin-window').trigger('mousedown', { button: 0, screenX: 200, screenY: 100 });
+ window.dispatchEvent(new MouseEvent('mousemove', { screenX: 210, screenY: 105 }));
+
+ expect(JSON.parse(window.localStorage.getItem('pin-window:pin-1')!)).toMatchObject({
+ currentBounds: { x: 130, y: 95, width: 320, height: 180 },
+ });
+ expect(sendToParent).toHaveBeenCalledWith('top-screenshot-pin-bounds', {
+ id: 'pin-1',
+ bounds: { x: 127, y: 92, width: 326, height: 186 },
+ });
+ expect(moveToSpy).not.toHaveBeenCalled();
+ expect(resizeToSpy).not.toHaveBeenCalled();
+ });
+
+ it('continues dragging when pointer movement leaves the pin element', async () => {
+ vi.setSystemTime(new Date('2026-06-08T06:00:00.000Z'));
+ window.location.hash = '#/pin?id=pin-1';
+ window.localStorage.setItem('pin-window:pin-1', JSON.stringify(createPinState()));
+ const sendToParent = vi.fn();
+ window.ztools = createZTools(sendToParent);
+ const { default: PinView } = await import('../src/views/PinView.vue');
+ const wrapper = mount(PinView);
+
+ await wrapper.find('.pin-window').trigger('mousedown', { button: 0, screenX: 200, screenY: 100 });
+ window.dispatchEvent(new MouseEvent('mousemove', { screenX: 230, screenY: 115 }));
+
+ expect(JSON.parse(window.localStorage.getItem('pin-window:pin-1')!)).toMatchObject({
+ currentBounds: { x: 150, y: 105, width: 320, height: 180 },
+ });
+ expect(sendToParent).toHaveBeenCalledWith('top-screenshot-pin-bounds', {
+ id: 'pin-1',
+ bounds: { x: 147, y: 102, width: 326, height: 186 },
+ });
+ });
+
+ it('notifies the parent when the pin window closes', async () => {
+ window.location.hash = '#/pin?id=pin-1';
+ window.localStorage.setItem('pin-window:pin-1', JSON.stringify(createPinState()));
+ const sendToParent = vi.fn();
+ window.ztools = createZTools(sendToParent);
+ const closeSpy = vi.spyOn(window, 'close').mockImplementation(() => undefined);
+ const { default: PinView } = await import('../src/views/PinView.vue');
+ mount(PinView);
+
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
+
+ expect(window.localStorage.getItem('pin-window:pin-1')).toBeNull();
+ expect(sendToParent).toHaveBeenCalledWith('top-screenshot-pin-closed', { id: 'pin-1' });
+ expect(closeSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('sends one close notification when close triggers beforeunload', async () => {
+ window.location.hash = '#/pin?id=pin-1';
+ window.localStorage.setItem('pin-window:pin-1', JSON.stringify(createPinState()));
+ const sendToParent = vi.fn();
+ window.ztools = createZTools(sendToParent);
+ vi.spyOn(window, 'close').mockImplementation(() => {
+ window.dispatchEvent(new Event('beforeunload'));
+ });
+ const { default: PinView } = await import('../src/views/PinView.vue');
+ mount(PinView);
+
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
+
+ expect(sendToParent).toHaveBeenCalledTimes(1);
+ expect(sendToParent).toHaveBeenCalledWith('top-screenshot-pin-closed', { id: 'pin-1' });
+ });
+
+ it('uses transform for wheel zoom while resizing the BrowserWindow in the next frame', async () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2026-06-08T06:00:00.000Z'));
+ window.location.hash = '#/pin?id=pin-1';
+ const state = createPinState();
+ window.localStorage.setItem('pin-window:pin-1', JSON.stringify(state));
+ const setItemSpy = vi.spyOn(window.localStorage.__proto__, 'setItem');
+ const sendToParent = vi.fn();
+ window.ztools = createZTools(sendToParent);
+ const callbacks: FrameRequestCallback[] = [];
+ vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
+ callbacks.push(callback);
+ return callbacks.length;
+ });
+ vi.stubGlobal('cancelAnimationFrame', vi.fn());
+ const { default: PinView } = await import('../src/views/PinView.vue');
+ const wrapper = mount(PinView);
+
+ await wrapper.find('.pin-window').trigger('wheel', { deltaY: -100 });
+ await wrapper.find('.pin-window').trigger('wheel', { deltaY: -100 });
+
+ expect((wrapper.find('.pin-frame').attributes('style') ?? '')).toContain('width: 320px; height: 180px;');
+ expect(wrapper.find('.pin-image').attributes('style')).toContain('width: 320px; height: 180px;');
+ expect(wrapper.find('.pin-image').attributes('style')).toContain('transform: scale(1)');
+ expect(setItemSpy).toHaveBeenCalledTimes(0);
+ expect(sendToParent).not.toHaveBeenCalledWith('top-screenshot-pin-bounds', expect.anything());
+
+ callbacks[0](16);
+ await wrapper.vm.$nextTick();
+
+ expect((wrapper.find('.pin-frame').attributes('style') ?? '')).toContain('width: 384px; height: 216px;');
+ expect(wrapper.find('.pin-image').attributes('style')).toContain('width: 320px; height: 180px;');
+ expect(wrapper.find('.pin-image').attributes('style')).toContain('transform: scale(1.2)');
+ expect(JSON.parse(window.localStorage.getItem('pin-window:pin-1')!)).toMatchObject({
+ currentBounds: { x: 88, y: 72, width: 384, height: 216 },
+ scale: 1.2,
+ });
+ expect(setItemSpy).toHaveBeenCalledTimes(1);
+ expect(sendToParent).toHaveBeenCalledWith('top-screenshot-pin-bounds', {
+ id: 'pin-1',
+ bounds: { x: 85, y: 69, width: 390, height: 222 },
+ });
+ });
+
+ it('keeps the frame layout at the scaled size when zoomed to minimum', async () => {
+ vi.useFakeTimers();
+ window.location.hash = '#/pin?id=pin-1';
+ window.localStorage.setItem(
+ 'pin-window:pin-1',
+ JSON.stringify({
+ ...createPinState(),
+ currentBounds: { x: 230.4, y: 129.6, width: 99.2, height: 55.8 },
+ scale: 0.31,
+ }),
+ );
+ const sendToParent = vi.fn();
+ window.ztools = createZTools(sendToParent);
+ const callbacks: FrameRequestCallback[] = [];
+ vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
+ callbacks.push(callback);
+ return callbacks.length;
+ });
+ vi.stubGlobal('cancelAnimationFrame', vi.fn());
+ const { default: PinView } = await import('../src/views/PinView.vue');
+ const wrapper = mount(PinView);
+
+ await wrapper.find('.pin-window').trigger('wheel', { deltaY: 100 });
+ callbacks[0](16);
+ await wrapper.vm.$nextTick();
+
+ expect((wrapper.find('.pin-frame').attributes('style') ?? '')).toContain('width: 96px; height: 54px;');
+ expect(wrapper.find('.pin-image').attributes('style')).toContain('width: 320px; height: 180px;');
+ expect(wrapper.find('.pin-image').attributes('style')).toContain('transform: scale(0.3)');
+ expect(sendToParent).toHaveBeenCalledWith('top-screenshot-pin-bounds', {
+ id: 'pin-1',
+ bounds: { x: 229, y: 128, width: 102, height: 60 },
+ });
+ });
+
+ it('does not flush stale wheel bounds after an immediate drag move', async () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2026-06-08T06:00:00.000Z'));
+ window.location.hash = '#/pin?id=pin-1';
+ window.localStorage.setItem('pin-window:pin-1', JSON.stringify(createPinState()));
+ const sendToParent = vi.fn();
+ window.ztools = createZTools(sendToParent);
+ const callbacks: FrameRequestCallback[] = [];
+ vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
+ callbacks.push(callback);
+ return callbacks.length;
+ });
+ vi.stubGlobal('cancelAnimationFrame', vi.fn());
+ const { default: PinView } = await import('../src/views/PinView.vue');
+ const wrapper = mount(PinView);
+
+ await wrapper.find('.pin-window').trigger('wheel', { deltaY: -100 });
+ await wrapper.find('.pin-window').trigger('mousedown', { button: 0, screenX: 200, screenY: 100 });
+ window.dispatchEvent(new MouseEvent('mousemove', { screenX: 210, screenY: 105 }));
+
+ expect(sendToParent).toHaveBeenCalledTimes(1);
+ expect(sendToParent).toHaveBeenCalledWith('top-screenshot-pin-bounds', {
+ id: 'pin-1',
+ bounds: { x: 111, y: 83, width: 358, height: 204 },
+ });
+ expect(JSON.parse(window.localStorage.getItem('pin-window:pin-1')!)).toMatchObject({
+ currentBounds: { x: 114, y: 86, width: 352, height: 198 },
+ scale: 1.1,
+ });
+
+ callbacks[0](16);
+
+ expect(sendToParent).toHaveBeenCalledTimes(1);
+ });
+});
+
+function createPinState(): PinWindowState {
+ return {
+ id: 'pin-1',
+ imageDataUrl: 'data:image/png;base64,cropped',
+ originalBounds: { x: 120, y: 90, width: 320, height: 180 },
+ currentBounds: { x: 120, y: 90, width: 320, height: 180 },
+ scale: 1,
+ createdAt: 1780898400000,
+ lastActiveAt: 1780898400000,
+ };
+}
+
+function createZTools(sendToParent: (channel: string, ...args: unknown[]) => void) {
+ return {
+ sendToParent,
+ getAllDisplays: () => [],
+ desktopCaptureSources: () => [],
+ createBrowserWindow: () => null,
+ };
+}
diff --git a/plugins/top-screenshot/tests/routes.test.ts b/plugins/top-screenshot/tests/routes.test.ts
new file mode 100644
index 00000000..098b9033
--- /dev/null
+++ b/plugins/top-screenshot/tests/routes.test.ts
@@ -0,0 +1,50 @@
+import { describe, expect, it } from 'vitest';
+
+describe('routes', () => {
+ it('parses an empty route as the launcher view with no params', async () => {
+ const { parseRoute } = await import('../src/core/routes');
+
+ const route = parseRoute('');
+
+ expect(route.view).toBe('launcher');
+ expect(route.params).toBeInstanceOf(URLSearchParams);
+ expect([...route.params.entries()]).toEqual([]);
+ });
+
+ it('reads window.location.hash when no route is provided', async () => {
+ const { parseRoute } = await import('../src/core/routes');
+ window.location.hash = '#/pin?id=p1';
+
+ const route = parseRoute();
+
+ expect(route.view).toBe('pin');
+ expect(route.params.get('id')).toBe('p1');
+ });
+
+ it('parses a capture hash route and exposes its search params', async () => {
+ const { parseRoute } = await import('../src/core/routes');
+
+ const route = parseRoute('#/capture?sessionId=s1&displayId=d1');
+
+ expect(route.view).toBe('capture');
+ expect(route.params.get('sessionId')).toBe('s1');
+ expect(route.params.get('displayId')).toBe('d1');
+ });
+
+ it('falls back unknown hash routes to launcher with no params', async () => {
+ const { parseRoute } = await import('../src/core/routes');
+
+ const route = parseRoute('#/unknown?x=1');
+
+ expect(route.view).toBe('launcher');
+ expect([...route.params.entries()]).toEqual([]);
+ });
+
+ it('builds a plugin url by appending a hash route and encoded params', async () => {
+ const { buildPluginUrl } = await import('../src/core/routes');
+
+ expect(
+ buildPluginUrl('pin', { id: 'abc 123' }, 'file:///D:/plugin/dist/index.html'),
+ ).toBe('file:///D:/plugin/dist/index.html#/pin?id=abc+123');
+ });
+});
diff --git a/plugins/top-screenshot/tests/storage.test.ts b/plugins/top-screenshot/tests/storage.test.ts
new file mode 100644
index 00000000..f4942780
--- /dev/null
+++ b/plugins/top-screenshot/tests/storage.test.ts
@@ -0,0 +1,196 @@
+import { describe, expect, expectTypeOf, it } from 'vitest';
+import type { Rect } from '../src/core/geometry';
+import type { CaptureSession, DisplaySnapshot, PinWindowState } from '../src/core/storage';
+
+function createMemoryStorage(): Storage {
+ const data = new Map();
+
+ return {
+ get length() {
+ return data.size;
+ },
+ clear() {
+ data.clear();
+ },
+ getItem(key) {
+ return data.has(key) ? data.get(key)! : null;
+ },
+ key(index) {
+ return Array.from(data.keys())[index] ?? null;
+ },
+ removeItem(key) {
+ data.delete(key);
+ },
+ setItem(key, value) {
+ data.set(key, value);
+ },
+ };
+}
+
+describe('storage', () => {
+ it('defines the planned storage shapes', () => {
+ expectTypeOf().toMatchTypeOf<{
+ displayId: string;
+ bounds: Rect;
+ imageDataUrl: string;
+ scaleFactor: number;
+ }>();
+
+ expectTypeOf().toMatchTypeOf<{
+ id: string;
+ createdAt: number;
+ completed: boolean;
+ displays: DisplaySnapshot[];
+ }>();
+
+ expectTypeOf().toMatchTypeOf<{
+ id: string;
+ imageDataUrl: string;
+ originalBounds: Rect;
+ currentBounds: Rect;
+ scale: number;
+ createdAt: number;
+ lastActiveAt: number;
+ }>();
+ });
+
+ it('saves and loads capture sessions with display snapshots as json in storage', async () => {
+ const { loadCaptureSession, saveCaptureSession } = await import('../src/core/storage');
+ const storage = createMemoryStorage();
+ const bounds: Rect = { x: 10, y: 20, width: 300, height: 200 };
+ const session: CaptureSession = {
+ id: 'session-1',
+ createdAt: 1780660800000,
+ completed: false,
+ displays: [
+ {
+ displayId: 'display-1',
+ bounds,
+ imageDataUrl: 'data:image/png;base64,aaa',
+ scaleFactor: 1.25,
+ },
+ ],
+ };
+
+ saveCaptureSession(storage, session);
+
+ expect(storage.length).toBe(1);
+ expect(storage.getItem(storage.key(0)!)).toBe(JSON.stringify(session));
+ expect(loadCaptureSession(storage, 'session-1')).toEqual(session);
+ });
+
+ it('marks an existing capture session as completed', async () => {
+ const { loadCaptureSession, markCaptureSessionCompleted, saveCaptureSession } = await import('../src/core/storage');
+ const storage = createMemoryStorage();
+ const session: CaptureSession = {
+ id: 'session-1',
+ createdAt: 1780660800000,
+ completed: false,
+ displays: [
+ {
+ displayId: 'display-1',
+ bounds: { x: 0, y: 0, width: 1920, height: 1080 },
+ imageDataUrl: 'data:image/png;base64,bbb',
+ scaleFactor: 1,
+ },
+ ],
+ };
+
+ saveCaptureSession(storage, session);
+ markCaptureSessionCompleted(storage, 'session-1');
+
+ expect(loadCaptureSession(storage, 'session-1')).toEqual({
+ ...session,
+ completed: true,
+ });
+ });
+
+ it('returns null when the stored capture session value is malformed json', async () => {
+ const { loadCaptureSession } = await import('../src/core/storage');
+ const storage = createMemoryStorage();
+
+ storage.setItem('capture-session:broken-session', '{not valid json');
+
+ expect(loadCaptureSession(storage, 'broken-session')).toBeNull();
+ });
+
+ it('finishes capture sessions and identifies their storage events', async () => {
+ const { captureSessionKey, finishCaptureSession, isCaptureSessionFinishedEvent, saveCaptureSession } = await import('../src/core/storage');
+ const storage = createMemoryStorage();
+ const session: CaptureSession = {
+ id: 'session-1',
+ createdAt: 1780660800000,
+ completed: false,
+ displays: [],
+ };
+
+ saveCaptureSession(storage, session);
+ finishCaptureSession(storage, 'session-1');
+
+ expect(storage.getItem(captureSessionKey('session-1'))).toBeNull();
+ expect(isCaptureSessionFinishedEvent({ key: captureSessionKey('session-1'), newValue: null }, 'session-1')).toBe(true);
+ expect(isCaptureSessionFinishedEvent({ key: captureSessionKey('other'), newValue: null }, 'session-1')).toBe(false);
+ });
+
+ it('saves and loads pin windows as json in storage', async () => {
+ const { loadPinWindow, savePinWindow } = await import('../src/core/storage');
+ const storage = createMemoryStorage();
+ const state: PinWindowState = {
+ id: 'pin-1',
+ imageDataUrl: 'data:image/png;base64,ccc',
+ originalBounds: { x: 30, y: 40, width: 250, height: 140 },
+ currentBounds: { x: 35, y: 45, width: 375, height: 210 },
+ scale: 1.5,
+ createdAt: 1780660800000,
+ lastActiveAt: 1780661100000,
+ };
+
+ savePinWindow(storage, state);
+
+ expect(storage.length).toBe(1);
+ expect(storage.getItem(storage.key(0)!)).toBe(JSON.stringify(state));
+ expect(loadPinWindow(storage, 'pin-1')).toEqual(state);
+ });
+
+ it('returns null when the stored pin window value is malformed json', async () => {
+ const { loadPinWindow } = await import('../src/core/storage');
+ const storage = createMemoryStorage();
+
+ storage.setItem('pin-window:broken-pin', '{not valid json');
+
+ expect(loadPinWindow(storage, 'broken-pin')).toBeNull();
+ });
+
+ it('saves pin window requests and identifies their storage events', async () => {
+ const { isPinWindowRequestEvent, pinWindowRequestKey, savePinWindowRequest } = await import('../src/core/storage');
+ const storage = createMemoryStorage();
+
+ savePinWindowRequest(storage, 'pin-1');
+
+ expect(storage.getItem(pinWindowRequestKey('pin-1'))).toBe(JSON.stringify({ pinWindowId: 'pin-1' }));
+ expect(isPinWindowRequestEvent({ key: pinWindowRequestKey('pin-1'), newValue: JSON.stringify({ pinWindowId: 'pin-1' }) })).toBe(
+ 'pin-1',
+ );
+ expect(isPinWindowRequestEvent({ key: pinWindowRequestKey('pin-1'), newValue: null })).toBeNull();
+ expect(isPinWindowRequestEvent({ key: 'other', newValue: JSON.stringify({ pinWindowId: 'pin-1' }) })).toBeNull();
+ });
+
+ it('removes pin window state after it is no longer needed', async () => {
+ const { loadPinWindow, removePinWindow, savePinWindow } = await import('../src/core/storage');
+ const storage = createMemoryStorage();
+ const state: PinWindowState = {
+ id: 'pin-1',
+ imageDataUrl: 'data:image/png;base64,ccc',
+ originalBounds: { x: 30, y: 40, width: 250, height: 140 },
+ currentBounds: { x: 35, y: 45, width: 375, height: 210 },
+ scale: 1.5,
+ createdAt: 1780660800000,
+ lastActiveAt: 1780661100000,
+ };
+
+ savePinWindow(storage, state);
+ removePinWindow(storage, 'pin-1');
+
+ expect(loadPinWindow(storage, 'pin-1')).toBeNull();
+ });
+});
diff --git a/plugins/top-screenshot/tests/ztoolsBridge.test.ts b/plugins/top-screenshot/tests/ztoolsBridge.test.ts
new file mode 100644
index 00000000..3a27f36f
--- /dev/null
+++ b/plugins/top-screenshot/tests/ztoolsBridge.test.ts
@@ -0,0 +1,180 @@
+import { describe, expect, it } from 'vitest';
+import {
+ createPluginWindow,
+ findSourceForDisplay,
+ getDisplaySnapshots,
+ getZTools,
+ mapDisplaysToSnapshots,
+ requireZTools,
+} from '../src/core/ztoolsBridge';
+import type { BrowserWindowOptions, DesktopCaptureSource, ZToolsApi, ZToolsDisplay } from '../src/types/ztools';
+
+function source(displayId: string, dataUrl: string): DesktopCaptureSource {
+ return {
+ id: `screen:${displayId}`,
+ name: `Screen ${displayId}`,
+ display_id: displayId,
+ thumbnail: {
+ toDataURL: () => dataUrl,
+ },
+ };
+}
+
+describe('ztoolsBridge', () => {
+ it('finds a desktop source by display id', () => {
+ const display: ZToolsDisplay = { id: 2, bounds: { x: 0, y: 0, width: 100, height: 100 }, scaleFactor: 1 };
+
+ expect(findSourceForDisplay(display, [source('1', 'a'), source('2', 'b')])?.thumbnail.toDataURL()).toBe('b');
+ });
+
+ it('falls back to matching display id as a source id segment', () => {
+ const display: ZToolsDisplay = { id: 'fallback', bounds: { x: 0, y: 0, width: 100, height: 100 } };
+ const fallbackSource: DesktopCaptureSource = {
+ id: 'screen:fallback:0',
+ name: 'Fallback Screen',
+ thumbnail: {
+ toDataURL: () => 'fallback-data',
+ },
+ };
+
+ expect(findSourceForDisplay(display, [fallbackSource])?.thumbnail.toDataURL()).toBe('fallback-data');
+ });
+
+ it('does not match display ids as ambiguous substrings', () => {
+ const display: ZToolsDisplay = { id: 1, bounds: { x: 0, y: 0, width: 100, height: 100 } };
+ const wrongSource: DesktopCaptureSource = {
+ id: 'screen:10:0',
+ name: 'Wrong Screen',
+ thumbnail: {
+ toDataURL: () => 'wrong-data',
+ },
+ };
+
+ expect(findSourceForDisplay(display, [wrongSource])).toBeNull();
+ });
+
+ it('maps displays to snapshots', () => {
+ const displays: ZToolsDisplay[] = [
+ { id: 1, bounds: { x: 0, y: 0, width: 800, height: 600 }, scaleFactor: 1.25 },
+ { id: 2, bounds: { x: 800, y: 0, width: 1024, height: 768 } },
+ ];
+
+ expect(mapDisplaysToSnapshots(displays, [source('1', 'data:one'), source('2', 'data:two')])).toEqual([
+ {
+ displayId: '1',
+ bounds: { x: 0, y: 0, width: 800, height: 600 },
+ imageDataUrl: 'data:one',
+ scaleFactor: 1.25,
+ },
+ {
+ displayId: '2',
+ bounds: { x: 800, y: 0, width: 1024, height: 768 },
+ imageDataUrl: 'data:two',
+ scaleFactor: 1,
+ },
+ ]);
+ });
+
+ it('gets and requires the ztools api from window', () => {
+ const api = createApi();
+ window.ztools = api;
+
+ expect(getZTools()).toBe(api);
+ expect(requireZTools()).toBe(api);
+
+ delete window.ztools;
+ expect(getZTools()).toBeNull();
+ expect(() => requireZTools()).toThrow('ZTools API is not available in this window.');
+ });
+
+ it('returns no snapshots when ztools has no displays', async () => {
+ const api = createApi();
+
+ await expect(getDisplaySnapshots(api)).resolves.toEqual([]);
+ expect(api.lastDesktopCaptureOptions).toBeNull();
+ });
+
+ it('captures all displays with a thumbnail size large enough for scaled displays', async () => {
+ const api = createApi({
+ displays: [
+ { id: 1, bounds: { x: 0, y: 0, width: 800, height: 600 }, scaleFactor: 1.5 },
+ { id: 2, bounds: { x: 800, y: 0, width: 1024, height: 768 }, scaleFactor: 1 },
+ ],
+ sources: [source('1', 'data:one'), source('2', 'data:two')],
+ });
+
+ await expect(getDisplaySnapshots(api)).resolves.toEqual([
+ {
+ displayId: '1',
+ bounds: { x: 0, y: 0, width: 800, height: 600 },
+ imageDataUrl: 'data:one',
+ scaleFactor: 1.5,
+ },
+ {
+ displayId: '2',
+ bounds: { x: 800, y: 0, width: 1024, height: 768 },
+ imageDataUrl: 'data:two',
+ scaleFactor: 1,
+ },
+ ]);
+ expect(api.lastDesktopCaptureOptions).toEqual({
+ types: ['screen'],
+ thumbnailSize: { width: 1200, height: 900 },
+ });
+ });
+
+ it('delegates plugin window creation to ztools', () => {
+ const api = createApi();
+ const options: BrowserWindowOptions = { width: 100, height: 80, alwaysOnTop: true };
+
+ createPluginWindow(api, 'index.html#/pin?id=1', options);
+
+ expect(api.createdWindows).toEqual([{ url: 'index.html#/pin?id=1', options }]);
+ });
+
+ it('raises created plugin windows to the floating level and focuses them', () => {
+ const api = createApi();
+ const options: BrowserWindowOptions = { width: 100, height: 80, alwaysOnTop: false };
+
+ const win = createPluginWindow(api, 'index.html#/pin?id=1', options);
+ api.runLastCreateCallback();
+
+ expect(win?.alwaysOnTopCalls).toEqual([{ flag: true, level: 'floating' }]);
+ expect(win?.focusCalls).toBe(1);
+ });
+});
+
+function createApi(input: { displays?: ZToolsDisplay[]; sources?: DesktopCaptureSource[] } = {}): ZToolsApi & {
+ createdWindows: Array<{ url: string; options: BrowserWindowOptions }>;
+ lastDesktopCaptureOptions: unknown;
+ runLastCreateCallback(): void;
+} {
+ let lastCreateCallback: (() => void) | undefined;
+ const api = {
+ createdWindows: [] as Array<{ url: string; options: BrowserWindowOptions }>,
+ lastDesktopCaptureOptions: null as unknown,
+ runLastCreateCallback: () => lastCreateCallback?.(),
+ getAllDisplays: () => input.displays ?? [],
+ desktopCaptureSources: (options: { types: Array<'screen' | 'window'>; thumbnailSize?: { width: number; height: number } }) => {
+ api.lastDesktopCaptureOptions = options;
+ return input.sources ?? [];
+ },
+ createBrowserWindow: (url: string, options: BrowserWindowOptions, callback?: () => void) => {
+ const win = {
+ alwaysOnTopCalls: [] as Array<{ flag: boolean; level?: string }>,
+ focusCalls: 0,
+ close: () => {},
+ focus() {
+ win.focusCalls += 1;
+ },
+ setAlwaysOnTop(flag: boolean, level?: string) {
+ win.alwaysOnTopCalls.push({ flag, level });
+ },
+ };
+ api.createdWindows.push({ url, options });
+ lastCreateCallback = callback;
+ return win;
+ },
+ };
+ return api;
+}
diff --git a/plugins/top-screenshot/tsconfig.json b/plugins/top-screenshot/tsconfig.json
new file mode 100644
index 00000000..561114ca
--- /dev/null
+++ b/plugins/top-screenshot/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "jsx": "preserve",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "types": ["vitest/globals", "vite/client", "node"],
+ "skipLibCheck": true,
+ "noEmit": true
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.d.ts",
+ "src/**/*.vue",
+ "preload/**/*.ts",
+ "vite.config.ts",
+ "vite.preload.config.ts"
+ ]
+}
diff --git a/plugins/top-screenshot/vite.config.ts b/plugins/top-screenshot/vite.config.ts
new file mode 100644
index 00000000..3db3d3b3
--- /dev/null
+++ b/plugins/top-screenshot/vite.config.ts
@@ -0,0 +1,12 @@
+import vue from '@vitejs/plugin-vue';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ base: './',
+ plugins: [vue()],
+ test: {
+ environment: 'jsdom',
+ globals: true,
+ exclude: ['node_modules/**', 'dist/**', 'release/**', '.worktrees/**'],
+ },
+});
diff --git a/plugins/top-screenshot/vite.preload.config.ts b/plugins/top-screenshot/vite.preload.config.ts
new file mode 100644
index 00000000..66c4ea66
--- /dev/null
+++ b/plugins/top-screenshot/vite.preload.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ build: {
+ emptyOutDir: false,
+ lib: {
+ entry: 'preload/index.ts',
+ formats: ['cjs'],
+ fileName: () => 'preload.cjs',
+ },
+ outDir: 'dist',
+ rollupOptions: {
+ external: ['electron'],
+ },
+ },
+});