From 95eeb69ff86746b027cc143804f47c1cd44127c3 Mon Sep 17 00:00:00 2001 From: Jz0ojiang <0ojixueseno0@gmail.com> Date: Fri, 4 Sep 2026 15:42:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=AA=A8=E6=9E=B6=E2=80=94=E2=80=94Vit?= =?UTF-8?q?e=20+=20Vue3=20+=20Fabric=20v7=EF=BC=8C=E5=B7=A6=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8C=BA=E5=8F=B3=E4=BE=A7=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 共享外壳与类型契约,四条并行开发线在此之上各自填空: - types.ts 是唯一的跨线接口,并行期间冻结 - editor/ 画布单例、选中态、尺寸预设与比例锁 - Sidebar 按选中对象切换面板,五个面板位已接好 - fonts/ filters/ sources/ io/ 留桩,各线只改自己目录 导出定 WebP:实测 540x287 PNG 134KB vs 1200x630 WebP 23KB。 字体接口挂在 Vite 中间件上,工具只在 dev 下完整可用。 --- .gitignore | 11 + README.md | 67 +++++ bun.lock | 376 ++++++++++++++++++++++++++ index.html | 12 + package.json | 23 ++ src/App.vue | 21 ++ src/components/Sidebar.vue | 82 ++++++ src/components/Workspace.vue | 62 +++++ src/components/panels/CanvasPanel.vue | 74 +++++ src/components/panels/ExportPanel.vue | 19 ++ src/components/panels/FilterPanel.vue | 22 ++ src/components/panels/SourcePanel.vue | 22 ++ src/components/panels/TextPanel.vue | 21 ++ src/editor/canvasSize.ts | 36 +++ src/editor/filters/crystallize.ts | 19 ++ src/editor/filters/index.ts | 13 + src/editor/useEditor.ts | 150 ++++++++++ src/fonts/useFonts.ts | 23 ++ src/fonts/vite-plugin-fonts.ts | 28 ++ src/io/index.ts | 25 ++ src/main.ts | 5 + src/sources/index.ts | 18 ++ src/style.css | 74 +++++ src/types.ts | 142 ++++++++++ tsconfig.json | 24 ++ vite.config.ts | 21 ++ 26 files changed, 1390 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bun.lock create mode 100644 index.html create mode 100644 package.json create mode 100644 src/App.vue create mode 100644 src/components/Sidebar.vue create mode 100644 src/components/Workspace.vue create mode 100644 src/components/panels/CanvasPanel.vue create mode 100644 src/components/panels/ExportPanel.vue create mode 100644 src/components/panels/FilterPanel.vue create mode 100644 src/components/panels/SourcePanel.vue create mode 100644 src/components/panels/TextPanel.vue create mode 100644 src/editor/canvasSize.ts create mode 100644 src/editor/filters/crystallize.ts create mode 100644 src/editor/filters/index.ts create mode 100644 src/editor/useEditor.ts create mode 100644 src/fonts/useFonts.ts create mode 100644 src/fonts/vite-plugin-fonts.ts create mode 100644 src/io/index.ts create mode 100644 src/main.ts create mode 100644 src/sources/index.ts create mode 100644 src/style.css create mode 100644 src/types.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08a5d9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +*.log +.DS_Store + +# 本地密钥:Unsplash Access Key 等 +.env +.env.local + +# 并行开发的 worktree +/.worktrees/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..831ceae --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# cover-studio + +im0o 博客的封面 / 头图生成器。自用工具。 + +替代「为了做一张封面专门开一次 Photoshop」这个动作。产出两类图: + +- **封面(cover)** — 带字的索引卡。列表里只有 224px 宽,必须一眼认出是哪篇,所以要有字。 + 形态是一张相关截图或照片,高斯模糊 + 压暗,居中压 1–2 行标题。 +- **头图(top_img)** — 无字的原图。文章页里 780px 宽,读者已经知道在读什么,字是冗余。 + 这个模式下不模糊不压暗,只裁到目标比例。 + +## 跑起来 + +```bash +bun install +bun dev # http://localhost:5180 +``` + +**这个工具只在 dev 下完整可用。** 系统字体的枚举与读取是挂在 Vite 开发服务器上的中间件, +`vite build` 出的静态产物里没有那两个接口。这是刻意的:为「列一下本机字体」单起一个后端进程不值得。 + +## 架构 + +``` +src/ +├─ types.ts 全仓共享的类型契约。并行开发期间不要动它 +├─ style.css 设计 token。面板用这里的变量,不要自带一套颜色 +├─ App.vue 外壳:左工作区 + 右侧栏 +├─ editor/ +│ ├─ useEditor.ts Fabric 画布单例、选中态、尺寸、装图装字 +│ ├─ canvasSize.ts 尺寸预设与比例锁 +│ └─ filters/ 滤镜注册表与自定义滤镜 +├─ fonts/ +│ ├─ vite-plugin-fonts.ts Vite 中间件:枚举并读取本机字体 +│ └─ useFonts.ts 前端:取列表、@font-face 挂载 +├─ sources/ 背景来源:上传 / 预设几何 / Unsplash +├─ io/ 导出(WebP/PNG 下载)与模板(localStorage) +└─ components/ + ├─ Workspace.vue 左:画布预览 + ├─ Sidebar.vue 右:面板容器,按选中对象切换 + └─ panels/ 五个面板,各归各的开发线 +``` + +**画布是逻辑尺寸**(导出像素,如 1200×630),显示时按容器缩放。 +Fabric 的 backstore 尺寸和 CSS 尺寸分开设置,所以预览缩放不影响导出分辨率。 + +**右栏面板的显示与否由画布当前选中对象决定。** 这是 Fabric 对象模型白送的, +不需要自己做状态机:选中图片出滤镜面板,选中文字出文字面板。 + +## 技术选型的理由 + +- **Fabric.js v7** — 选它不是为了滤镜,是为了对象模型:拖拽 / 缩放 / 裁剪 / 文字框 / 层级全都白送。 + 而且 `canvas.toJSON()` / `loadFromJSON()` 直接就是模板的序列化格式,不用自己设计。 +- **导出用 WebP** — 实测同一张图,`540×287 PNG = 134 KB`,`1200×630 WebP = 23 KB`。 + 尺寸翻倍还小 5 倍。旧封面存 PNG 是历史失误:模糊背景 + 少量文字正好是 PNG 最烂的场景。 +- **无独立后端** — 字体接口写成 Vite 插件的 `configureServer` 中间件,`bun dev` 一个命令, + 没有第二个进程、没有端口要配。 + +## 明确不做 + +选文章 / 回填 frontmatter、自动上传图床、多尺寸派生(只出一份)、内置预设包(模板全靠用户自己存)。 + +## 仓库位置 + +独立 git 仓,远端在 Gitea。**不是 blog-workspace 的 submodule**——它和博客之间没有代码依赖, +产物是手动拿走的图片文件。它在 blog-workspace 目录下但被 gitignore, +和 `blog-v3-draft/` 是同一个处理方式。 diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..f440426 --- /dev/null +++ b/bun.lock @@ -0,0 +1,376 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "cover-studio", + "dependencies": { + "fabric": "^7.4.0", + "vue": "^3.5.13", + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.0", + "typescript": "^5.7.3", + "vite": "^7.0.0", + "vue-tsc": "^2.2.0", + }, + }, + }, + "packages": { + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], + + "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.2", "", { "os": "android", "cpu": "x64" }, "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.2", "", { "os": "linux", "cpu": "arm" }, "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.2", "", { "os": "linux", "cpu": "none" }, "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.2", "", { "os": "none", "cpu": "x64" }, "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.2", "", { "os": "none", "cpu": "arm64" }, "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.1", "", { "os": "android", "cpu": "arm" }, "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.1", "", { "os": "android", "cpu": "arm64" }, "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.1", "", { "os": "none", "cpu": "arm64" }, "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.8", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew=="], + + "@volar/language-core": ["@volar/language-core@2.4.15", "", { "dependencies": { "@volar/source-map": "2.4.15" } }, "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA=="], + + "@volar/source-map": ["@volar/source-map@2.4.15", "", {}, "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg=="], + + "@volar/typescript": ["@volar/typescript@2.4.15", "", { "dependencies": { "@volar/language-core": "2.4.15", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg=="], + + "@vue/compiler-core": ["@vue/compiler-core@3.5.42", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/shared": "3.5.42", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ=="], + + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.42", "", { "dependencies": { "@vue/compiler-core": "3.5.42", "@vue/shared": "3.5.42" } }, "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA=="], + + "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.42", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/compiler-core": "3.5.42", "@vue/compiler-dom": "3.5.42", "@vue/compiler-ssr": "3.5.42", "@vue/shared": "3.5.42", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.19", "source-map-js": "^1.2.1" } }, "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw=="], + + "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.42", "", { "dependencies": { "@vue/compiler-dom": "3.5.42", "@vue/shared": "3.5.42" } }, "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA=="], + + "@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="], + + "@vue/language-core": ["@vue/language-core@2.2.12", "", { "dependencies": { "@volar/language-core": "2.4.15", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA=="], + + "@vue/reactivity": ["@vue/reactivity@3.5.42", "", { "dependencies": { "@vue/shared": "3.5.42" } }, "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ=="], + + "@vue/runtime-core": ["@vue/runtime-core@3.5.42", "", { "dependencies": { "@vue/reactivity": "3.5.42", "@vue/shared": "3.5.42" } }, "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA=="], + + "@vue/runtime-dom": ["@vue/runtime-dom@3.5.42", "", { "dependencies": { "@vue/reactivity": "3.5.42", "@vue/runtime-core": "3.5.42", "@vue/shared": "3.5.42", "csstype": "^3.2.3" } }, "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g=="], + + "@vue/server-renderer": ["@vue/server-renderer@3.5.42", "", { "dependencies": { "@vue/compiler-ssr": "3.5.42", "@vue/runtime-dom": "3.5.42", "@vue/shared": "3.5.42" } }, "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ=="], + + "@vue/shared": ["@vue/shared@3.5.42", "", {}, "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "alien-signals": ["alien-signals@1.0.13", "", {}, "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "canvas": ["canvas@3.2.3", "", { "dependencies": { "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.3" } }, "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw=="], + + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], + + "de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], + + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + + "fabric": ["fabric@7.4.0", "", { "optionalDependencies": { "canvas": "^3.2.0", "jsdom": "^26.1.0" } }, "sha512-NalYDc3eifTl1C33zryQwpH6+XA/2ClxQrH9vkASkZw3tbkRmorpikhYMmxhUTmi7O3e9ODz0vOT8qfaCh9IVA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "jsdom": ["jsdom@26.1.0", "", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], + + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + + "node-abi": ["node-abi@3.96.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg=="], + + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + + "nwsapi": ["nwsapi@2.2.27", "", {}, "sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], + + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "rollup": ["rollup@4.63.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.1", "@rollup/rollup-android-arm64": "4.63.1", "@rollup/rollup-darwin-arm64": "4.63.1", "@rollup/rollup-darwin-x64": "4.63.1", "@rollup/rollup-freebsd-arm64": "4.63.1", "@rollup/rollup-freebsd-x64": "4.63.1", "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", "@rollup/rollup-linux-arm-musleabihf": "4.63.1", "@rollup/rollup-linux-arm64-gnu": "4.63.1", "@rollup/rollup-linux-arm64-musl": "4.63.1", "@rollup/rollup-linux-loong64-gnu": "4.63.1", "@rollup/rollup-linux-loong64-musl": "4.63.1", "@rollup/rollup-linux-ppc64-gnu": "4.63.1", "@rollup/rollup-linux-ppc64-musl": "4.63.1", "@rollup/rollup-linux-riscv64-gnu": "4.63.1", "@rollup/rollup-linux-riscv64-musl": "4.63.1", "@rollup/rollup-linux-s390x-gnu": "4.63.1", "@rollup/rollup-linux-x64-gnu": "4.63.1", "@rollup/rollup-linux-x64-musl": "4.63.1", "@rollup/rollup-openbsd-x64": "4.63.1", "@rollup/rollup-openharmony-arm64": "4.63.1", "@rollup/rollup-win32-arm64-msvc": "4.63.1", "@rollup/rollup-win32-ia32-msvc": "4.63.1", "@rollup/rollup-win32-x64-gnu": "4.63.1", "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg=="], + + "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], + + "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], + + "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], + + "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vite": ["vite@7.3.6", "", { "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg=="], + + "vscode-uri": ["vscode-uri@3.2.0", "", {}, "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg=="], + + "vue": ["vue@3.5.42", "", { "dependencies": { "@vue/compiler-dom": "3.5.42", "@vue/compiler-sfc": "3.5.42", "@vue/runtime-dom": "3.5.42", "@vue/server-renderer": "3.5.42", "@vue/shared": "3.5.42" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA=="], + + "vue-tsc": ["vue-tsc@2.2.12", "", { "dependencies": { "@volar/typescript": "2.4.15", "@vue/language-core": "2.2.12" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..2399c35 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Cover Studio + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..f7ae51e --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "cover-studio", + "private": true, + "type": "module", + "version": "0.1.0", + "description": "im0o 博客的封面 / 头图生成器。自用工具,只在 dev 下完整可用。", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "typecheck": "vue-tsc --noEmit", + "preview": "vite preview" + }, + "dependencies": { + "fabric": "^7.4.0", + "vue": "^3.5.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.0", + "typescript": "^5.7.3", + "vite": "^7.0.0", + "vue-tsc": "^2.2.0" + } +} diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..4eddcdf --- /dev/null +++ b/src/App.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue new file mode 100644 index 0000000..96558be --- /dev/null +++ b/src/components/Sidebar.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/src/components/Workspace.vue b/src/components/Workspace.vue new file mode 100644 index 0000000..f4ef202 --- /dev/null +++ b/src/components/Workspace.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/components/panels/CanvasPanel.vue b/src/components/panels/CanvasPanel.vue new file mode 100644 index 0000000..3f57abc --- /dev/null +++ b/src/components/panels/CanvasPanel.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/src/components/panels/ExportPanel.vue b/src/components/panels/ExportPanel.vue new file mode 100644 index 0000000..f89e45b --- /dev/null +++ b/src/components/panels/ExportPanel.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/src/components/panels/FilterPanel.vue b/src/components/panels/FilterPanel.vue new file mode 100644 index 0000000..1290725 --- /dev/null +++ b/src/components/panels/FilterPanel.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/src/components/panels/SourcePanel.vue b/src/components/panels/SourcePanel.vue new file mode 100644 index 0000000..99f78fd --- /dev/null +++ b/src/components/panels/SourcePanel.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/src/components/panels/TextPanel.vue b/src/components/panels/TextPanel.vue new file mode 100644 index 0000000..ab5ceeb --- /dev/null +++ b/src/components/panels/TextPanel.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/src/editor/canvasSize.ts b/src/editor/canvasSize.ts new file mode 100644 index 0000000..0ab9e16 --- /dev/null +++ b/src/editor/canvasSize.ts @@ -0,0 +1,36 @@ +import type { CanvasSizePreset } from "@/types"; + +/** + * 画布尺寸预设。宽高是导出像素。 + * + * 前两个是博客的真实消费框(v3 的列表卡走 OG 那张,文章顶部图带是 780×260, + * 这里给的是它的 @2x),第三个是方图。选「自定义」时这张表不参与。 + */ +export const SIZE_PRESETS: CanvasSizePreset[] = [ + { id: "og", label: "封面 / OG", width: 1200, height: 630 }, + { id: "band", label: "文章图带 @2x", width: 1560, height: 520 }, + { id: "square", label: "方图", width: 1080, height: 1080 }, +]; + +export const DEFAULT_SIZE = SIZE_PRESETS[0]; + +/** 常用比例,锁定后改一边自动算另一边。 */ +export const RATIOS = [ + { id: "free", label: "自由", value: 0 }, + { id: "1.91", label: "1.91:1(OG)", value: 1200 / 630 }, + { id: "3", label: "3:1", value: 3 }, + { id: "16-9", label: "16:9", value: 16 / 9 }, + { id: "1", label: "1:1", value: 1 }, +]; + +/** 按锁定的比例,从一边推另一边。`ratio <= 0` 表示不锁。 */ +export function applyRatio( + size: { width: number; height: number }, + ratio: number, + changed: "width" | "height", +): { width: number; height: number } { + if (ratio <= 0) return size; + return changed === "width" + ? { width: size.width, height: Math.round(size.width / ratio) } + : { width: Math.round(size.height * ratio), height: size.height }; +} diff --git a/src/editor/filters/crystallize.ts b/src/editor/filters/crystallize.ts new file mode 100644 index 0000000..90b4cba --- /dev/null +++ b/src/editor/filters/crystallize.ts @@ -0,0 +1,19 @@ +/** + * 【T3 待实现 — 晶格化滤镜】 + * + * 目标是视觉上接近 Photoshop 的「晶格化」:把画面切成多边形色块,每块填一个纯色。 + * 做法用抖动网格 Voronoi——把画面划成规则网格,每格的种子点在格内随机偏移, + * 每个像素取最近的种子点,填该种子点处的颜色。视觉上和真 Voronoi 难分辨, + * 但只需要查邻近 9 格,复杂度可控。 + * + * 实现要继承 fabric 的 `filters.BaseFilter`,模板见 + * `node_modules/fabric/dist/src/filters/Boilerplate.d.ts`。 + * + * **必须同时实现两条路径**(fabric 会按后端能力二选一,只写一条会在另一条上静默不生效): + * - `getFragmentSource()` + `static uniformLocations` + `sendUniformData()` → WebGL + * - `applyTo2d(options)` → Canvas2D 降级 + * + * 参数至少要有格子大小(cellSize),注册进 ../index.ts 的 FILTERS。 + */ + +export {}; diff --git a/src/editor/filters/index.ts b/src/editor/filters/index.ts new file mode 100644 index 0000000..8a40e97 --- /dev/null +++ b/src/editor/filters/index.ts @@ -0,0 +1,13 @@ +import type { FilterDef } from "@/types"; + +/** + * 【T3 待实现 — 滤镜注册表】 + * + * 往这个数组里注册滤镜,FilterPanel 会按 `params` 自动渲染滑杆, + * 不需要为每个滤镜单独写界面。 + * + * 第一版要有:高斯模糊、亮度、对比度、饱和度、像素化(都是 fabric 内建的 + * `filters.Blur / Brightness / Contrast / Saturation / Pixelate`), + * 外加自定义的晶格化(见 ./crystallize)。 + */ +export const FILTERS: FilterDef[] = []; diff --git a/src/editor/useEditor.ts b/src/editor/useEditor.ts new file mode 100644 index 0000000..f4143cc --- /dev/null +++ b/src/editor/useEditor.ts @@ -0,0 +1,150 @@ +import { Canvas, FabricImage, Textbox, type FabricObject } from "fabric"; +import { ref, shallowRef } from "vue"; +import type { CanvasSize, SelectionKind } from "@/types"; +import { DEFAULT_SIZE } from "./canvasSize"; + +/** + * 编辑器核心:一个模块级单例。 + * + * 画布是**逻辑尺寸**(导出像素,如 1200×630),显示时按容器缩放—— + * Fabric 的 backstore 尺寸和 CSS 尺寸分开设置,所以预览缩放不影响导出分辨率。 + * + * 各条开发线通过 `getCanvas()` 拿到 Fabric 画布实例。**不要另外 new 一个 Canvas。** + */ + +const canvas = shallowRef(null); +const activeObject = shallowRef(null); +const selectionKind = ref("none"); +const size = ref({ width: DEFAULT_SIZE.width, height: DEFAULT_SIZE.height }); +/** 预览区相对逻辑尺寸的缩放比,只影响显示。 */ +const displayScale = ref(1); + +function kindOf(obj: FabricObject | null | undefined): SelectionKind { + if (!obj) return "none"; + if (obj instanceof FabricImage) return "image"; + if (obj instanceof Textbox) return "text"; + return "other"; +} + +function syncSelection() { + const c = canvas.value; + const obj = c?.getActiveObject() ?? null; + activeObject.value = obj; + selectionKind.value = kindOf(obj); +} + +/** 挂载画布。只在 Workspace.vue 里调一次。 */ +function mount(el: HTMLCanvasElement): Canvas { + const c = new Canvas(el, { + width: size.value.width, + height: size.value.height, + backgroundColor: "#1b1b1f", + preserveObjectStacking: true, + }); + c.on("selection:created", syncSelection); + c.on("selection:updated", syncSelection); + c.on("selection:cleared", syncSelection); + canvas.value = c; + return c; +} + +function unmount() { + canvas.value?.dispose(); + canvas.value = null; + activeObject.value = null; + selectionKind.value = "none"; +} + +/** 改逻辑尺寸(导出像素)。显示缩放由 `fitTo` 单独算。 */ +function setSize(next: CanvasSize) { + size.value = next; + const c = canvas.value; + if (!c) return; + c.setDimensions({ width: next.width, height: next.height }); + c.requestRenderAll(); +} + +/** + * 把画布缩放到容器里显示。改的是 CSS 尺寸,backstore 不动, + * 所以导出的仍然是逻辑尺寸的全分辨率。 + */ +function fitTo(containerWidth: number, containerHeight: number, padding = 32) { + const c = canvas.value; + if (!c) return; + const scale = Math.min( + (containerWidth - padding * 2) / size.value.width, + (containerHeight - padding * 2) / size.value.height, + 1, + ); + displayScale.value = Math.max(scale, 0.05); + c.setDimensions( + { + width: `${size.value.width * displayScale.value}px`, + height: `${size.value.height * displayScale.value}px`, + }, + { cssOnly: true }, + ); +} + +/** 装一张图进画布并铺满(object-fit: cover 的等效)。 */ +async function addImage(src: string): Promise { + const c = canvas.value; + if (!c) throw new Error("画布还没挂载"); + const img = await FabricImage.fromURL(src, { crossOrigin: "anonymous" }); + const scale = Math.max(size.value.width / img.width, size.value.height / img.height); + img.set({ + scaleX: scale, + scaleY: scale, + left: size.value.width / 2, + top: size.value.height / 2, + originX: "center", + originY: "center", + }); + c.add(img); + c.setActiveObject(img); + c.requestRenderAll(); + return img; +} + +/** 加一个文本框,默认居中。 */ +function addText(text = "标题"): Textbox { + const c = canvas.value; + if (!c) throw new Error("画布还没挂载"); + const box = new Textbox(text, { + left: size.value.width / 2, + top: size.value.height / 2, + originX: "center", + originY: "center", + width: size.value.width * 0.8, + fontSize: Math.round(size.value.height * 0.12), + fill: "#ffffff", + textAlign: "center", + }); + c.add(box); + c.setActiveObject(box); + c.requestRenderAll(); + return box; +} + +function getCanvas(): Canvas { + const c = canvas.value; + if (!c) throw new Error("画布还没挂载"); + return c; +} + +export function useEditor() { + return { + canvas, + activeObject, + selectionKind, + size, + displayScale, + mount, + unmount, + setSize, + fitTo, + addImage, + addText, + getCanvas, + }; +} diff --git a/src/fonts/useFonts.ts b/src/fonts/useFonts.ts new file mode 100644 index 0000000..32523f4 --- /dev/null +++ b/src/fonts/useFonts.ts @@ -0,0 +1,23 @@ +import { ref } from "vue"; +import type { FontEntry } from "@/types"; + +/** + * 【T2 待实现 — 字体子系统】 + * + * 前端这一半:向 `/__fonts` 取列表,选中某款时向 `/__fonts/:id` 取文件, + * 用 `FontFace` API 动态挂载到 `document.fonts`,之后 Fabric 的 Textbox 就能用 + * `fontFamily: entry.family` 渲染。 + * + * 注意 Fabric 在字体真正 ready 之前渲染会退到 fallback 字形, + * 挂载后要 `await document.fonts.load(...)` 再 `canvas.requestRenderAll()`。 + */ +export const fonts = ref([]); +export const loadingFonts = ref(false); + +export async function listFonts(): Promise { + throw new Error("T2 未实现:listFonts"); +} + +export async function ensureFontLoaded(_entry: FontEntry): Promise { + throw new Error("T2 未实现:ensureFontLoaded"); +} diff --git a/src/fonts/vite-plugin-fonts.ts b/src/fonts/vite-plugin-fonts.ts new file mode 100644 index 0000000..e29d775 --- /dev/null +++ b/src/fonts/vite-plugin-fonts.ts @@ -0,0 +1,28 @@ +import type { Plugin } from "vite"; + +/** + * 【T2 待实现 — 字体子系统】 + * + * Vite 开发服务器中间件,给前端提供本机安装的字体。两个接口: + * + * GET /__fonts -> FontListResponse(见 src/types.ts) + * GET /__fonts/:id -> 字体文件二进制,Content-Type 按 ext 给 + * + * 枚举来源是 Windows 注册表的两个键(本机实测共 651 条): + * HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts 系统字体,值是文件名,目录 C:\Windows\Fonts + * HKCU\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts 用户字体,值多为绝对路径 + * + * 注意 `.ttc` 是合集包(如 msyh.ttc 里有微软雅黑的多个 face),浏览器不能直接当 webfont 用, + * 需要抽出单个 face 再返回;`FontEntry.needsExtraction` 标记这种情况。 + * + * 只在 `apply: "serve"` 下生效,构建产物里不存在这两个接口。 + */ +export function fontsPlugin(): Plugin { + return { + name: "cover-studio:fonts", + apply: "serve", + configureServer() { + // T2 在这里挂 server.middlewares.use(...) + }, + }; +} diff --git a/src/io/index.ts b/src/io/index.ts new file mode 100644 index 0000000..1be4113 --- /dev/null +++ b/src/io/index.ts @@ -0,0 +1,25 @@ +import type { ExportOptions, Template } from "@/types"; + +/** + * 【T5 待实现 — 导出与模板】 + * + * export.ts: + * 画布导出成 WebP(默认,带质量滑杆)或 PNG,走浏览器下载到下载文件夹。 + * **不上传图床、不写 frontmatter**——这两条明确不做。 + * 导出必须是画布的逻辑尺寸(全分辨率),不受预览缩放影响; + * `useEditor().displayScale` 只改 CSS 尺寸,所以 `toDataURL` 默认就是对的, + * 但要注意 multiplier 别被重复乘一次。 + * + * templates.ts: + * 模板 = `canvas.toJSON()` + 画布尺寸,存 localStorage(见 types.ts 的 Template)。 + * 没有内置预设包,全靠用户自己存。加载走 `canvas.loadFromJSON()`。 + * 存之前扫一遍文本对象的 fontFamily 填进 `usedFonts`——用了本机字体的模板 + * 换台机器复现不出来,加载时要提示。 + */ +export async function exportCanvas(_options: ExportOptions): Promise { + throw new Error("T5 未实现:exportCanvas"); +} + +export function listTemplates(): Template[] { + throw new Error("T5 未实现:listTemplates"); +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..de275e7 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import "./style.css"; + +createApp(App).mount("#app"); diff --git a/src/sources/index.ts b/src/sources/index.ts new file mode 100644 index 0000000..ea45b9e --- /dev/null +++ b/src/sources/index.ts @@ -0,0 +1,18 @@ +import type { BackgroundAsset } from "@/types"; + +/** + * 【T4 待实现 — 背景来源】 + * + * 三个来源各写一个模块,都返回 BackgroundAsset: + * - upload.ts 本地文件 -> object URL + * - geometry.ts 预设几何底纹,程序生成(画到离屏 canvas 再 toDataURL) + * - unsplash.ts Unsplash 搜索。Access Key 从 import.meta.env.VITE_UNSPLASH_KEY 读, + * 放 .env.local(已 gitignore)。条款强制署名,返回时填 attribution。 + */ +export async function fromUpload(_file: File): Promise { + throw new Error("T4 未实现:fromUpload"); +} + +export async function searchUnsplash(_query: string): Promise { + throw new Error("T4 未实现:searchUnsplash"); +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..7cc8b82 --- /dev/null +++ b/src/style.css @@ -0,0 +1,74 @@ +/* 全局样式与设计 token。并行开发期间**只有主控改这个文件**, + * 各面板用这里的变量,不要自带一套颜色。 */ + +:root { + --bg: #17171a; + --bg2: #1f1f23; + --bg3: #27272c; + --fg: #e8e8ea; + --fg-dim: #9a9aa2; + --line: #33333a; + --accent: #6ea8fe; + --radius: 6px; + --sidebar-w: 320px; + + color-scheme: dark; + font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; + font-size: 13px; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--bg); + color: var(--fg); +} + +button { + font: inherit; + color: var(--fg); + background: var(--bg3); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 5px 10px; + cursor: pointer; +} + +button:hover { + border-color: var(--accent); +} + +input, +select { + font: inherit; + color: var(--fg); + background: var(--bg); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 4px 6px; + width: 100%; +} + +input[type="range"] { + padding: 0; +} + +label { + display: block; + color: var(--fg-dim); + margin-bottom: 4px; +} + +/* 面板通用:一行「标签 + 控件」 */ +.field { + margin-bottom: 10px; +} + +.row { + display: flex; + gap: 8px; + align-items: center; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..cc12ead --- /dev/null +++ b/src/types.ts @@ -0,0 +1,142 @@ +/** + * 全仓共享的类型契约。 + * + * **这个文件是四条并行开发线之间唯一的接口。** 每条线只实现自己目录下的文件, + * 通过这里的类型互相衔接,因此: + * + * - 并行开发期间**不要改动本文件**。需要新增字段先在任务书里提出,由主控统一改。 + * - 每条线的实现必须满足这里声明的形状,不要在自己的目录里另立一套同义类型。 + */ + +/* ---------------------------------------------------------------- 画布 */ + +/** 画布尺寸预设。宽高是导出像素,不是显示尺寸。 */ +export interface CanvasSizePreset { + id: string; + label: string; + width: number; + height: number; +} + +export interface CanvasSize { + width: number; + height: number; +} + +/* ---------------------------------------------------------------- 字体(T2) */ + +/** + * 一款可用字体。由 Vite 中间件 `GET /__fonts` 枚举本机安装的字体后返回。 + * `id` 是稳定标识,前端用它向 `GET /__fonts/:id` 取字体文件并 `@font-face` 挂载。 + */ +export interface FontEntry { + /** 稳定 id,用于取文件;不要假设它等于文件名。 */ + id: string; + /** 用于 CSS font-family 和界面显示的字族名,如 "阿里巴巴普惠体-H"。 */ + family: string; + /** 字体文件绝对路径(仅供调试与配方记录,前端不直接读)。 */ + path: string; + /** 文件后缀,小写不带点:ttf / otf / ttc / woff2 … */ + ext: string; + /** + * `.ttc` 是合集包,一个文件里多个 face,不能直接当 webfont。 + * 中间件负责抽出单个 face 再吐给前端;这个字段标记它需要抽取, + * 前端据此可以在下拉里给个提示。 + */ + needsExtraction: boolean; +} + +/** `GET /__fonts` 的响应体。 */ +export interface FontListResponse { + fonts: FontEntry[]; +} + +/* ---------------------------------------------------------------- 滤镜(T3) */ + +/** 一个滤镜参数的取值范围,用来在侧栏自动生成滑杆。 */ +export interface FilterParam { + key: string; + label: string; + min: number; + max: number; + step: number; + default: number; +} + +/** + * 滤镜定义。注册进 `src/editor/filters/index.ts` 的注册表后, + * FilterPanel 会自动按 `params` 渲染滑杆,不需要为每个滤镜写界面。 + * + * `build` 返回一个 Fabric 滤镜实例(`fabric.filters.*` 或自定义的 BaseFilter 子类)。 + * 类型故意留成 unknown,避免这个契约文件依赖 fabric 的内部类型。 + */ +export interface FilterDef { + id: string; + label: string; + params: FilterParam[]; + build: (values: Record) => unknown; +} + +/* ---------------------------------------------------------------- 背景来源(T4) */ + +export type BackgroundSourceKind = "upload" | "geometry" | "unsplash"; + +/** + * 素材署名。**不画进图里**——由界面输出成可复制的文本,用户自己贴进文章。 + * Unsplash 的 API 条款强制要求署名,`geometry` 和 `upload` 通常没有。 + */ +export interface Attribution { + /** 直接给用户复制的成品文本,如 "Photo by X on Unsplash"。 */ + text: string; + /** 带 utm_source 的原图链接,没有就省略。 */ + url?: string; +} + +/** 一份背景素材。`objectURL` 或 `dataURL` 二选一,交给编辑器装进画布。 */ +export interface BackgroundAsset { + kind: BackgroundSourceKind; + /** 可直接喂给 fabric 的图片地址(object URL / data URL / 跨域已处理的 http URL)。 */ + src: string; + width: number; + height: number; + attribution?: Attribution; +} + +/* ---------------------------------------------------------------- 导出与模板(T5) */ + +export type ExportFormat = "webp" | "png"; + +export interface ExportOptions { + format: ExportFormat; + /** 0–1,只对 webp 生效。 */ + quality: number; + /** 不含扩展名。 */ + filename: string; +} + +/** + * 一份模板 = 一份 Fabric 画布 JSON + 它的画布尺寸。 + * 用户自己保存,存在 localStorage;没有内置预设包。 + */ +export interface Template { + id: string; + name: string; + size: CanvasSize; + /** `canvas.toJSON()` 的产物,原样存原样喂给 `loadFromJSON`。 */ + canvasJSON: unknown; + /** ISO 8601。 */ + createdAt: string; + /** + * 这份模板引用的本机字体 family 列表。 + * 用了本机字体的模板换台机器复现不出来,界面据此给出警告。 + */ + usedFonts: string[]; +} + +/* ---------------------------------------------------------------- 侧栏面板 */ + +/** 右侧栏面板。选中对象的类型决定显示哪几个。 */ +export type PanelId = "canvas" | "source" | "filter" | "text" | "export"; + +/** 画布当前选中了什么,用来决定右栏显示哪些面板。 */ +export type SelectionKind = "none" | "image" | "text" | "other"; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ea8e2c7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client", "node"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "jsx": "preserve", + "noEmit": true, + "resolveJsonModule": true, + "paths": { + "@/*": ["./src/*"] + }, + "baseUrl": "." + }, + "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..af9d182 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,21 @@ +import { fileURLToPath, URL } from "node:url"; +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import { fontsPlugin } from "./src/fonts/vite-plugin-fonts"; + +/** + * 这个工具**只在 dev 下完整可用**:系统字体的枚举与读取走 `fontsPlugin` 挂在 Vite + * 开发服务器上的中间件,`vite build` 出的静态产物里没有那两个接口。 + * 这是刻意的——自用工具不值得为此单起一个后端进程。 + */ +export default defineConfig({ + plugins: [vue(), fontsPlugin()], + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, + server: { + port: 5180, + }, +});