From 52cee8411a381029e119a9b089122ca6e2374a3e Mon Sep 17 00:00:00 2001 From: mylesy Date: Wed, 7 Oct 2026 19:27:06 +0800 Subject: [PATCH] =?UTF-8?q?Update=20plugin=20=E9=A2=9C=E8=89=B2=E5=8A=A9?= =?UTF-8?q?=E6=89=8B=20v1.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 颜色助手:颜色、AI 配色、图片色卡、UI 色卡、传统色、渐变色、收藏颜色 - fix:gemini-code-assist code review - fix: 修复类型检查失效及多项逻辑缺陷,引入测试基建,升级构建链 - chord:chroma-js 升到 3,MUI 升到 7(补装 `@emotion/cache`、`@emotion/css`),React 升到 19 - chord: - 对齐ztools api - 修复 AI 默认硬编码模型ID - 修复 AI 配色提示词输入输出不匹配 - 图片色卡四个版式重做,色值内嵌色块、字号按槽位自适应 - fix:修复提示条收藏与跨页状态旧化,清理全库注释 - chore: 忽略本地工作文档目录 docs/ - chore: CHANGELOG 1.1.0 标题补发布日期 - chore: 合并官方仓库的 CHANGELOG 标准化改动 --- plugins/color-helper/.gitignore | 3 + plugins/color-helper/App.tsx | 187 +- plugins/color-helper/CHANGELOG.md | 41 +- plugins/color-helper/README.md | 67 +- plugins/color-helper/context/AppContext.tsx | 131 -- plugins/color-helper/index.css | 385 +-- plugins/color-helper/main.tsx | 1 - plugins/color-helper/package-lock.json | 2073 ----------------- plugins/color-helper/package.json | 33 +- plugins/color-helper/pages/AIPalettePage.tsx | 73 +- .../color-helper/pages/CollectColorsPage.tsx | 52 +- plugins/color-helper/pages/ColorPage.tsx | 107 +- plugins/color-helper/pages/GradientsPage.tsx | 47 +- .../color-helper/pages/ImagePalettePage.tsx | 760 +++--- .../pages/TraditionalColorsPage.tsx | 69 +- plugins/color-helper/pages/UIPalettesPage.tsx | 54 +- plugins/color-helper/pnpm-lock.yaml | 1685 ++++++++++++++ plugins/color-helper/public/plugin.json | 19 +- plugins/color-helper/tests/collect.test.ts | 97 + .../color-helper/tests/color-parsing.test.ts | 128 + plugins/color-helper/tests/color.test.ts | 99 + .../color-helper/tests/data-integrity.test.ts | 176 ++ .../tests/gradient-geometry.test.ts | 84 + plugins/color-helper/tests/platform.test.ts | 324 +++ .../color-helper/tests/plugin-json.test.ts | 204 ++ plugins/color-helper/tests/quantize.test.ts | 82 + .../tests/traditional-color.test.ts | 87 + plugins/color-helper/tsconfig.json | 2 +- plugins/color-helper/tsconfig.node.json | 5 +- plugins/color-helper/types/index.ts | 163 +- plugins/color-helper/utils/collect.ts | 66 + plugins/color-helper/utils/color.ts | 302 +-- plugins/color-helper/utils/gradient.ts | 27 + plugins/color-helper/utils/platform.ts | 131 +- plugins/color-helper/utils/quantize.ts | 37 +- plugins/color-helper/utils/storage.ts | 64 - plugins/color-helper/vite-env.d.ts | 1 + plugins/color-helper/vite.config.ts | 2 +- plugins/color-helper/vitest.config.ts | 12 + 39 files changed, 4198 insertions(+), 3682 deletions(-) delete mode 100644 plugins/color-helper/context/AppContext.tsx delete mode 100644 plugins/color-helper/package-lock.json create mode 100644 plugins/color-helper/pnpm-lock.yaml create mode 100644 plugins/color-helper/tests/collect.test.ts create mode 100644 plugins/color-helper/tests/color-parsing.test.ts create mode 100644 plugins/color-helper/tests/color.test.ts create mode 100644 plugins/color-helper/tests/data-integrity.test.ts create mode 100644 plugins/color-helper/tests/gradient-geometry.test.ts create mode 100644 plugins/color-helper/tests/platform.test.ts create mode 100644 plugins/color-helper/tests/plugin-json.test.ts create mode 100644 plugins/color-helper/tests/quantize.test.ts create mode 100644 plugins/color-helper/tests/traditional-color.test.ts create mode 100644 plugins/color-helper/utils/collect.ts create mode 100644 plugins/color-helper/utils/gradient.ts delete mode 100644 plugins/color-helper/utils/storage.ts create mode 100644 plugins/color-helper/vite-env.d.ts create mode 100644 plugins/color-helper/vitest.config.ts diff --git a/plugins/color-helper/.gitignore b/plugins/color-helper/.gitignore index 86826b558..21dd52227 100644 --- a/plugins/color-helper/.gitignore +++ b/plugins/color-helper/.gitignore @@ -24,3 +24,6 @@ npm-debug.log* # TypeScript *.tsbuildinfo + +# 本地工作文档(不入库) +docs/ diff --git a/plugins/color-helper/App.tsx b/plugins/color-helper/App.tsx index 6513820d9..209a16467 100644 --- a/plugins/color-helper/App.tsx +++ b/plugins/color-helper/App.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { Component, lazy, Suspense } from 'react'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemButton from '@mui/material/ListItemButton'; @@ -16,16 +16,24 @@ import ViewCompactIcon from '@mui/icons-material/ViewCompact'; import BrushIcon from '@mui/icons-material/Brush'; import GradientIcon from '@mui/icons-material/Gradient'; import StarIcon from '@mui/icons-material/Star'; +import StarBorderIcon from '@mui/icons-material/StarBorder'; import ColorizeIcon from '@mui/icons-material/Colorize'; import chroma from 'chroma-js'; import ColorPage from './pages/ColorPage'; import UIPalettesPage from './pages/UIPalettesPage'; import TraditionalColorsPage from './pages/TraditionalColorsPage'; -import GradientsPage from './pages/GradientsPage'; -import ImagePalettePage from './pages/ImagePalettePage'; import CollectColorsPage from './pages/CollectColorsPage'; -import AIPalettePage from './pages/AIPalettePage'; -import { copyText, dbStorage, screenColorPick, onPluginEnter, onPluginOut, isPlatform } from './utils/platform'; +import { copyText, db, dbStorage, screenColorPick, onPluginEnter, onPluginOut, isPlatform } from './utils/platform'; +import { collectDocId, saveCollectedColor } from './utils/collect'; + +// 代码分割试点: 只拆「单次使用 + 自带大块资源」的页面 +// - GradientsPage: 渐变数据 4.05 kB gzip + 代码 17.71 kB +// - ImagePalettePage: 4 张封面图 107 kB + quantize.ts +// - AIPalettePage: 色轮 UI(iro 仅剩样式与 ui 常量, 逻辑已在首屏共用 chunk) +// 其余页面体积小且首屏常用, 保持静态导入 +const GradientsPage = lazy(() => import('./pages/GradientsPage')); +const ImagePalettePage = lazy(() => import('./pages/ImagePalettePage')); +const AIPalettePage = lazy(() => import('./pages/AIPalettePage')); /** * App - 核心架构: 状态驱动导航(this.state.nav) + 平台生命周期 @@ -48,34 +56,40 @@ function colorFromBackgroundColor(bgColor: string, setting: boolean): string | n } } -// 导航项 -const navItems = [ +/** + * 侧边栏导航项 + */ +export const navItems = [ { key: "color", label: "颜色", icon: }, - { key: "ai", label: "AI 配色", icon: }, { key: "ui", label: "UI 色卡", icon: }, - { key: "image", label: "图片色卡", icon: }, { key: "traditional", label: "传统色", icon: }, { key: "gradient", label: "渐变色", icon: }, + { key: "image", label: "图片色卡", icon: }, { key: "collect", label: "收藏颜色", icon: }, + { key: "ai", label: "AI 配色", icon: }, ]; interface AppState { nav: string; colorValue: (string | null)[]; openMessage: boolean; - messageData: { key: number; color: string; body?: string }; + messageData: { key: number; color: string; text: string }; setting: boolean; + imagePayload: string | null; + /** 收藏页在 constructor 里一次性读库, 从提示条收藏成功后递增它重挂载刷新列表 */ + collectVersion: number; } class App extends Component<{}, AppState> { - private imageEnterPayload: any = null; state: AppState = { nav: "", colorValue: [], openMessage: false, - messageData: { key: 0, color: "" }, + messageData: { key: 0, color: "", text: "" }, setting: false, + imagePayload: null, + collectVersion: 0, }; handleNavChange = (nav: string) => () => { @@ -83,27 +97,63 @@ class App extends Component<{}, AppState> { this.setState({ nav }); }; - handleColorClick = (eventOrColor: any) => { + /** + * 复制色值并弹出带操作的 snackbar + * + * `copied` 是真正写进剪贴板、也是提示条要原样回显的文本; `color` 是同一个颜色 + * 解析出来的 hex, 只用于定位 收藏 / 查看 / 配色 三个操作指向哪个颜色。 + * + * 颜色页各格式的复制按钮传 (原文, hex): rgb/hsl/hsv/hsi/cmyk/lab 串 chroma + * 解析不了, hex 必须由调用方一并给出。其余各页色块只传 DOM 事件或色值串。 + */ + handleColorClick = (eventOrColor: any, knownHex?: string) => { let color: string | null = null; + let copied: string | null = null; - if (eventOrColor?.currentTarget?.style) { + if (knownHex && typeof eventOrColor === 'string') { + // 颜色页各格式的复制: 原文 chroma 解析不了, hex 由调用方给出 + color = knownHex; + copied = eventOrColor; + } else if (eventOrColor?.currentTarget?.style) { color = colorFromBackgroundColor(eventOrColor.currentTarget.style.backgroundColor, this.state.setting); + copied = color; } else if (typeof eventOrColor === 'string') { try { const c = chroma(eventOrColor); - color = this.state.setting ? c.hex().substring(1) : c.hex(); + color = c.hex(); + copied = eventOrColor; } catch {} } - if (color) { - copyText(color); + // 「色值去 #」只作用于复制出去的文本: 各格式串里没有 #, 去掉是空操作 + if (copied && this.state.setting) copied = copied.replace(/#/g, ''); + + if (color && copied) { + copyText(copied); this.setState({ - messageData: { color, key: Date.now() }, + messageData: { color, key: Date.now(), text: `已复制 "${copied}"` }, openMessage: true, }); } }; + handleCollectColor = () => { + const { color } = this.state.messageData; + if (!color || !!db.get(collectDocId(color))) return; + if (!saveCollectedColor(color)) { + // 走到这里说明色值非法或 db 写入失败(已收藏的情况上面已拦下): + // 保留提示条上的操作按钮, 只换提示文本 + this.setState({ messageData: { ...this.state.messageData, text: "收藏失败,请重试" } }); + return; + } + this.setState({ collectVersion: this.state.collectVersion + 1 }); + this.showMessage(`已收藏 "${color}",可在「收藏颜色」中查看`); + }; + + handleViewCollectedColors = () => { + this.setState({ nav: "collect", openMessage: false }); + }; + handleViewColorInfo = () => { if (this.state.messageData.color) { this.setState({ nav: "color", colorValue: [this.state.messageData.color], openMessage: false }); @@ -122,7 +172,7 @@ class App extends Component<{}, AppState> { }; showMessage = (msg: string) => { - this.setState({ messageData: { key: Date.now(), color: "", body: msg }, openMessage: true }); + this.setState({ messageData: { key: Date.now(), color: "", text: msg }, openMessage: true }); }; handleSettingCheckboxChange = (e: any) => { @@ -140,28 +190,25 @@ class App extends Component<{}, AppState> { const setting = !!dbStorage.getItem("setting"); if (code === "image") { + let imagePayload: string | null = null; if (type === "img") { - this.imageEnterPayload = payload; + imagePayload = payload; } else if (type === "files") { - this.imageEnterPayload = payload?.[0]?.path; - } else { - this.imageEnterPayload = null; - } - } else { - if (code === "pickercolor") { - screenColorPick(({ hex }) => { - this.setState({ nav: "color", colorValue: [hex], setting }); - }); - return; - } - if (code === "color" && type === "regex") { - this.setState({ nav: "color", colorValue: [payload], setting }); - return; - } - if (code === "ai" && type === "regex") { - this.setState({ nav: "ai", colorValue: [payload], setting }); - return; + imagePayload = payload?.[0]?.path || null; } + this.setState({ nav: "image", imagePayload, setting }); + return; + } + + if (code === "pickercolor") { + screenColorPick(({ hex }) => { + this.setState({ nav: "color", colorValue: [hex], setting }); + }); + return; + } + if (code === "color" && type === "regex") { + this.setState({ nav: "color", colorValue: [payload], setting }); + return; } this.setState({ nav: code, setting }); @@ -183,7 +230,9 @@ class App extends Component<{}, AppState> { } render() { - const { nav, colorValue, openMessage, messageData, setting } = this.state; + const { nav, colorValue, openMessage, messageData, setting, imagePayload, collectVersion } = this.state; + // 星标状态以 db 实时为准: 提示条跨页面存活, 期间收藏可能被增删 + const favorited = !!messageData.color && !!db.get(collectDocId(messageData.color)); // 页面内容 let pageContent: React.ReactNode; @@ -198,13 +247,13 @@ class App extends Component<{}, AppState> { pageContent = ; break; case "gradient": - pageContent = ; + pageContent = ; break; case "image": - pageContent = ; + pageContent = ; break; case "collect": - pageContent = ; + pageContent = ; break; case "ai": pageContent = ; @@ -246,16 +295,54 @@ class App extends Component<{}, AppState> {
- {pageContent} + }> + {pageContent} +
{messageData.text}} action={messageData.color ? ( <> + {/* 「查看」的去处是颜色页, 已经在颜色页上时点下去只会把同一个色 + 再追加成色板里的重复色块, 所以不显示。按当前页面判断: + 提示条跨页面存活, 弹出时所在的页面说明不了现在 */} + {nav !== "color" && ( + + )} + + + - ) : undefined} key={messageData.key} diff --git a/plugins/color-helper/CHANGELOG.md b/plugins/color-helper/CHANGELOG.md index d5f127b46..088f8a04b 100644 --- a/plugins/color-helper/CHANGELOG.md +++ b/plugins/color-helper/CHANGELOG.md @@ -1,12 +1,45 @@ # Changelog +## 1.1.0 - 2026-10-07 -## 1.0.0 - 2026-06-09 +### 修复 + +- 修复 `tsconfig.json` 的 `include` 路径,让 `tsc` 真正检查代码 +- 修复渐变导出图片与预览不一致(渐变线长度算错) +- 统一明暗判定判据(原先混用三套阈值),修复传统色部分色块文字对比度不足 +- 修复颜色输入识别:正则误判普通单词为颜色,主输入框里大写色值不匹配 +- 修复 AI 调用硬编码了宿主解析不出的模型 ID,导致总是报「未找到 AI 模型配置」 +- 修复图片色卡无法通过 `img` / `files` 命令带入图片 +- 修复收藏颜色删除后残留索引、单个文档损坏导致整批丢失 +- 修复剪贴板回退逻辑从未生效(接不住 Promise reject) +- 修复两处整页白屏:懒加载页面加载失败、图标库在 vite 8 下不兼容 +- 修复提示条收藏:半透明色(8 位 hex)不再被误判为非法色值、失败时给出反馈、在收藏页上收藏后列表立即刷新;「查看」按钮与收藏星标不再跨页面显示旧状态 +- 颜色页各格式复制按钮的 Tooltip 与实际复制的完整色值一致 + +### 变更 + +- 移除 AI 配色的颜色正则:与「颜色」完全重复,粘色值时只会多出冗余候选,只留「AI 配色」文本命令 +- 侧边栏顺序与 `plugin.json` 的 features 对齐,AI 配色移到末位 +- 颜色页各格式复制按钮改为走统一提示条,原样回显复制文本、剪贴板为该格式完整色值(原先一律变回 hex);在颜色页上不显示「查看」 +- 渐变页复制增加反馈,浏览器下降级为下载 PNG +- 渐变色 / 图片色卡 / AI 配色改为懒加载 +- 图片色卡四个版式重做,色值内嵌色块、字号按槽位自适应 +- 构建链与依赖升级:vite 8 + vitest 4、Node 22、chroma-js 3、MUI 7、React 19,锁定 pnpm 版本,`pnpm audit` 归零 +- 全库注释清理:删除上游残留、复述与叙事式、过期注释,为非直观处补「为什么」说明 ### 新增 -- 颜色助手:颜色、AI 配色、图片色卡、UI 色卡、传统色、渐变色、收藏颜色 +- 提示条上增加「收藏」(已收藏显示星标,点击跳到收藏页) +- 引入 vitest 测试基建(`pnpm test` / `pnpm run test:run`) +- 抽出 `utils/gradient.ts`、`utils/collect.ts`(统一收藏颜色的文档形状与写入,供提示条与收藏页共用) +- `window.platform` 类型声明逐条对齐官方 `ztools.api.d.ts` + +### 移除 -### 根据提交记录补充 +- 移除死代码与无用文件:`package-lock.json`、`react-router-dom`、`utils/storage.ts`、`context/AppContext.tsx`、`plugin.json` 的 `pluginName` 字段、quantize 死条件 `maxColors > 256`、`App.tsx` 无入口的 AI 配色 regex 分支与透传复制回调 -- 修复代码审查反馈的问题。 +## 1.0.0 - 2026-06-09 + +### 新增 + +- 颜色助手:颜色、AI 配色、图片色卡、UI 色卡、传统色、渐变色、收藏颜色 diff --git a/plugins/color-helper/README.md b/plugins/color-helper/README.md index 8a154931c..01be93994 100644 --- a/plugins/color-helper/README.md +++ b/plugins/color-helper/README.md @@ -19,31 +19,49 @@ ### 环境要求 -- Node.js 16+ -- npm 或 yarn +- **Node.js 22+**(vite 8 要求 `^20.19.0 || >=22.12.0`;Node 21 是已 EOL 的奇数版,不在支持范围) +- **pnpm 10.33.0** —— 已写入 `package.json` 的 `packageManager` 字段, + 用 corepack 可自动锁定:`corepack enable pnpm` + +> ⚠️ 本项目使用 pnpm 管理依赖,**请勿使用 npm 安装**。 +> 仓库根目录的 `pnpm-lock.yaml` 为唯一锁文件(lockfileVersion 9.0); +> npm 6 等旧版本既无法解析该 lockfile 格式,其 `prepare` 流程还会破坏 pnpm 的 +> `node_modules` 软链布局。原有的过期 `package-lock.json`(lockfileVersion 3, +> 且残留 `konva`/`react-konva` 等已移除依赖)已删除。 + +> ⚠️ 请确认 `pnpm -v` 输出为 **10.33.0**。某些 Node 发行版会自带更高主版本的 pnpm, +> 其内置的供应链策略(`minimumReleaseAge`)会拒绝安装锁文件中较新的包, +> 且不同主版本会使用不同的虚拟 store 目录,导致 `node_modules` 被重建。 ### 安装与运行 ```bash # 安装依赖 -npm install +pnpm install # 开发模式 -npm run dev +pnpm run dev -# 构建生产版本 -npm run build +# 构建生产版本(会先执行 tsc 类型检查) +pnpm run build # 预览构建结果 -npm run preview +pnpm run preview + +# 运行测试(8 个文件 / 405 条断言) +pnpm run test:run ``` +### 类型检查 + +`pnpm run build` 中的 `tsc` 会检查 `App.tsx` / `main.tsx` / `pages` / `utils` / `types` / +`data` / `tests`(见 `tsconfig.json` 的 `include`)。`pages/ImagePalettePage.tsx` 的 +`*.jpg` 导入依赖根目录的 `vite-env.d.ts`(`/// `),请勿删除。 + ## 项目结构 ``` ├── assets/ # 静态资源(色卡封面图) -├── context/ -│ └── AppContext.tsx # 全局上下文 ├── data/ # 颜色数据 │ ├── gradients-*.json # 渐变色数据 │ ├── traditional-*.json # 传统色数据 @@ -63,16 +81,21 @@ npm run preview ├── types/ │ └── index.ts # 类型定义 ├── utils/ # 工具函数 -│ ├── color.ts # 颜色处理 +│ ├── color.ts # 深浅判定与文字配色(WCAG 判据) +│ ├── collect.ts # 收藏颜色的文档形状与写入(提示条 / 收藏页共用) +│ ├── gradient.ts # 渐变端点几何(Canvas 导出用) │ ├── platform.ts # 平台 API 适配层 -│ ├── quantize.ts # 颜色量化算法 -│ └── storage.ts # 存储工具 +│ └── quantize.ts # 颜色量化算法(Median Cut) ├── App.tsx # 应用入口组件 ├── main.tsx # 渲染入口 ├── index.html # HTML 模板 ├── index.css # 全局样式 ├── vite.config.ts # Vite 配置 +├── vitest.config.ts # Vitest 配置 +├── vite-env.d.ts # Vite 客户端类型引用 +├── tests/ # 测试(8 个文件 / 405 条断言) ├── tsconfig.json # TypeScript 配置 +├── tsconfig.node.json # Node 侧 TS 配置 └── package.json # 项目配置 ``` @@ -80,13 +103,13 @@ npm run preview | 技术 | 用途 | |------|------| -| React 18 | UI 框架 | -| TypeScript | 类型安全 | -| Vite 4 | 构建工具 | -| MUI 5 | UI 组件库 | -| chroma-js | 颜色空间转换与计算 | +| React 19 | UI 框架 | +| TypeScript 5 | 类型安全 | +| Vite 8 | 构建工具(rolldown + oxc + lightningcss) | +| Vitest 4 | 测试框架(happy-dom 环境) | +| MUI 7 | UI 组件库(Emotion 引擎,`@emotion/cache` / `@emotion/css` 为显式 peer) | +| chroma-js 3 | 颜色空间转换与计算 | | iro.js | 色轮选择器 | - ## 核心功能说明 ### 颜色转换 @@ -99,7 +122,7 @@ npm run preview ### 图片色卡 -从图片中提取主要颜色,支持截图、本地图片导入,使用中位切分(Median Cut)量化算法进行颜色聚类,可生成色卡图片并复制导出。 +从图片中提取主要颜色,支持截图、本地图片导入,使用中位切分(Median Cut)量化算法进行颜色聚类,可生成色卡图片并复制导出。四种色卡版式在 `pages/ImagePalettePage.tsx` 的 `renderColorCard` 里绘制,改动后需同步该函数上方的版式说明。 ### UI 色卡 @@ -113,6 +136,12 @@ npm run preview 提供多组精选渐变色方案,支持一键复制 CSS 渐变代码。 +### 收藏颜色 + +点击任意色块复制色值后,提示条上可直接「收藏」当前颜色(星标点亮后点击进入收藏页);收藏页支持命名、取色器录入与删除。颜色页 HEX/RGB 等各格式的复制按钮也走同一条提示条(提示条不显示无去处的「查看」)。两个入口写入同一份 db 文档(`utils/collect.ts`),改动字段形状需同时兼顾两边。 + ### 平台适配 通过 `utils/platform.ts` 统一适配层,支持插件平台 API(屏幕取色、截图、剪贴板、AI、存储等),同时在浏览器环境中提供 fallback 实现,确保独立运行时功能可用。 + +`window.platform` 的类型声明在 `types/index.ts`,逐条对齐官方 `ztools.api.d.ts`(`@ztools-center/ztools-api-types`)。**改动适配层前请先核对官方 d.ts**,不要凭印象改签名。 diff --git a/plugins/color-helper/context/AppContext.tsx b/plugins/color-helper/context/AppContext.tsx deleted file mode 100644 index f436db4d8..000000000 --- a/plugins/color-helper/context/AppContext.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import React, { createContext, useContext, useState, useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { dbStorage, copyText } from '../utils/platform'; - -/** - * AppContext - 全局状态管理 - * - * 提供全局功能: - * - setting: 色值去 "#" 的设置状态 - * - onColorClick: 点击颜色后的全局处理(复制+Snackbar+操作按钮) - * - formatCopiedColor: 根据setting格式化复制的颜色值 - */ - -interface ColorMessageData { - color: string; - key: number; - body?: string; -} - -interface AppContextValue { - setting: boolean; - setSetting: (value: boolean) => void; - onColorClick: (eventOrColor: any) => void; - showMessage: (msg: string) => void; - colorMessage: ColorMessageData | null; - openMessage: boolean; - setOpenMessage: (value: boolean) => void; -} - -const AppContext = createContext(null); - -export const useAppContext = () => { - const ctx = useContext(AppContext); - if (!ctx) throw new Error('useAppContext must be used within AppContextProvider'); - return ctx; -}; - -/** - * 从 rgb(r, g, b) 格式的backgroundColor解析出HEX颜色值 - * setting=true时去掉#前缀,setting=false时保留#前缀 - */ -function colorFromBackgroundColor(backgroundColor: string, setting: boolean): string { - const match = backgroundColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); - if (!match) return backgroundColor; - const hex = match.slice(1).map((n: string) => parseInt(n, 10).toString(16).padStart(2, "0")).join(""); - return `${setting ? "" : "#"}${hex}`; -} - -/** - * 将HEX颜色转为 rgb(r, g, b) 格式 - */ -export function hexToRgbString(hex: string): string { - hex = hex.slice(1); - const re = new RegExp(`.{1,${hex.length >= 6 ? 2 : 1}}`, "g"); - let result = hex.match(re); - if (result && result[0].length === 1) { - result = result.map((o: string) => o + o); - } - if (!result) return ""; - return `rgb${result.length === 4 ? "a" : ""}(${result.map((o: string, s: number) => - s < 3 ? parseInt(o, 16) : Math.round(parseInt(o, 16) / 255 * 1000) / 1000 - ).join(", ")})`; -} - -export const AppContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const navigate = useNavigate(); - const [setting, setSettingState] = useState(() => { - return !!dbStorage.getItem('setting'); - }); - const [colorMessage, setColorMessage] = useState(null); - const [openMessage, setOpenMessage] = useState(false); - - // 全局颜色点击处理 - // 兼容两种调用方式: 1) 传入DOM event对象 2) 传入颜色字符串 - const onColorClick = useCallback((eventOrColor: any) => { - let color: string; - if (eventOrColor && eventOrColor.currentTarget && eventOrColor.currentTarget.style) { - // 从DOM event的backgroundColor提取 - color = colorFromBackgroundColor(eventOrColor.currentTarget.style.backgroundColor, setting); - } else if (typeof eventOrColor === 'string') { - // 字符串方式: 直接使用 - color = eventOrColor; - } else { - return; - } - - copyText(color); - setColorMessage({ color, key: Date.now() }); - setOpenMessage(true); - }, [setting]); - - // showMessage - const showMessage = useCallback((msg: string) => { - setColorMessage({ key: Date.now(), body: msg, color: '' }); - setOpenMessage(true); - }, []); - - // 处理设置checkbox变更 - const handleSettingChange = useCallback((checked: boolean) => { - if (checked) { - dbStorage.setItem('setting', true); - } else { - dbStorage.removeItem('setting'); - } - setSettingState(checked); - }, []); - - // 暴露全局路由函数给main.tsx的onPluginEnter使用 - React.useEffect(() => { - window.__platformNavigate = (path: string, state?: any) => { - navigate(path, { state }); - }; - return () => { window.__platformNavigate = null; }; - }, [navigate]); - - return ( - - {children} - - ); -}; - -export default AppContext; diff --git a/plugins/color-helper/index.css b/plugins/color-helper/index.css index 2c126fc1c..1554a3ca8 100644 --- a/plugins/color-helper/index.css +++ b/plugins/color-helper/index.css @@ -10,6 +10,19 @@ html, body { height: 100%; } +/* 兜底: 懒加载 chunk 取不到时 React 会卸载整棵树, 用户只看到纯白页面。 + 此时 #root 为空, 用伪元素给出可读提示, 避免"点了没反应还退不回来" */ +#root:empty::after { + content: "页面加载失败,请重新进入插件重试"; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + color: #888; + font-size: 14px; +} + /* ============ App 布局 ============ */ .app-body { width: 100%; @@ -38,6 +51,11 @@ html, body { border-top-right-radius: 16px; } +/* 懒加载页面的占位, 避免切页瞬间出现白屏 */ +.app-body .app-content > .page-loading { + height: 100%; +} + .app-body .app-setting { position: fixed; left: 22px; @@ -91,15 +109,6 @@ html, body { border-radius: 10px; } -.color-content .color-value .color-title { - padding-top: 0; - font-weight: 700; - color: #212121; - display: flex; - align-items: center; - justify-content: space-between; -} - .color-content .color-value .color-hub { display: flex; align-items: center; @@ -114,7 +123,7 @@ html, body { .color-content .color-value .color-hub .color-hub-wrapper .color-hub-value { display: flex; - margin-right: 8px; + margin-right: 6px; width: 36px; height: 36px; border-radius: 4px; @@ -327,31 +336,6 @@ html, body { } /* Open Color / Ant Design / Material Design 通用 */ -.ui-colors { - width: 100%; - display: flex; - padding-bottom: 10px; -} - -.ui-colors > div { - flex: 1; - height: 36px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; -} - -.ui-colors > div span { - color: #fff; - display: none; - font-size: 12px; -} - -.ui-colors > div:hover span { - display: block; -} - .ui-color-row, .ui-color-header { display: flex; @@ -446,6 +430,7 @@ html, body { padding: 12px 20px; box-sizing: border-box; align-items: center; + border-bottom: 1px solid #e6e6e6; } .traditional-header > div:first-child { @@ -927,336 +912,6 @@ html, body { margin-right: 9px; } -/* ============ TraditionalColorsPage 新布局 ============ */ -.traditional-body { - display: flex; - width: 100%; - height: 100%; - flex-direction: column; -} - -.traditional-header { - display: flex; - padding: 12px 20px; - box-sizing: border-box; - align-items: center; - border-bottom: 1px solid #e6e6e6; -} - -.traditional-tabs { - display: flex; - gap: 16px; -} - -.traditional-tab { - cursor: pointer; - padding: 6px 16px; - border-radius: 6px; - font-size: 14px; - color: #666; - transition: all .2s; -} - -.traditional-tab:hover { - background-color: #f0f0f0; -} - -.traditional-tab.traditional-tab-active { - background-color: #e0e0e0; - color: #333; - font-weight: 500; -} - -.traditional-search { - margin-left: auto !important; - width: 200px; -} - -.traditional-colors { - flex: 1; - min-height: 0; - overflow-y: auto; - box-sizing: border-box; - display: flex; - flex-wrap: wrap; - color: #fff; - padding: 0 20px 20px; -} - -.traditional-china .traditional-color-item, -.traditional-japan .traditional-color-item { - cursor: pointer; - position: relative; -} - -.traditional-china .traditional-color-item { - width: 25%; -} - -.traditional-japan .traditional-color-item { - width: 20%; -} - -.traditional-color-item:before { - content: ""; - padding-top: 50%; - float: left; -} - -.traditional-color-item > div { - font-size: 10px; - padding: 0 5px 2px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.traditional-color-item > div:first-child { - padding-top: 5px; - font-size: 14px; -} - -/* ============ UIPalettesPage 新布局 ============ */ -.ui-palette-body { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - -webkit-user-select: none; - user-select: none; -} - -.ui-palette-tabs { - display: flex; - gap: 8px; - padding: 12px 20px; - box-sizing: border-box; - border-bottom: 1px solid #e6e6e6; -} - -.ui-palette-tab { - cursor: pointer; - padding: 6px 16px; - border-radius: 6px; - font-size: 14px; - color: #666; - transition: all .2s; -} - -.ui-palette-tab:hover { - background-color: #f0f0f0; -} - -.ui-palette-tab.ui-palette-tab-active { - background-color: #e0e0e0; - color: #333; - font-weight: 500; -} - -.ui-palette-content { - flex: 1; - min-height: 0; - overflow-y: auto; - padding: 0 20px 20px; - box-sizing: border-box; -} - -.ui-palette-flat { - display: flex; - flex-wrap: wrap; -} - -.ui-palette-flat-item { - width: 20%; - cursor: pointer; - position: relative; -} - -.ui-palette-flat-item:before { - content: ""; - padding-top: 75%; - float: left; -} - -.ui-palette-flat-item > div { - font-size: 10px; - padding: 0 8px 2px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.ui-palette-flat-item > div:first-child { - padding-top: 8px; - font-size: 14px; -} - -.ui-palette-grouped { - display: flex; - flex-direction: column; -} - -.ui-palette-group { - display: flex; - margin: 8px 0; -} - -.ui-palette-group-name { - color: #666; - width: 100px; - display: flex; - align-items: center; - white-space: pre-wrap; - line-height: 20px; - font-size: 13px; -} - -.ui-palette-group-colors { - flex: 1; - display: flex; -} - -.ui-palette-group-item { - flex: 1; - cursor: pointer; - position: relative; - box-sizing: border-box; - padding: .5px; -} - -.ui-palette-group-item:before { - content: ""; - padding-top: 100%; - float: left; -} - -.ui-palette-group-item > span { - display: none; - font-size: 12px; -} - -.ui-palette-group-item:hover { - border-radius: 50%; -} - -/* ============ ImagePalettePage 新布局 ============ */ -.image-palette-body { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - padding: 20px; - box-sizing: border-box; -} - -.image-palette-upload { - width: 100%; - height: 300px; - border: 2px dashed #ccc; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - background-size: cover; - background-position: center; - transition: border-color .2s; -} - -.image-palette-upload:hover { - border-color: #999; -} - -.image-palette-upload-hint { - text-align: center; - color: #999; -} - -.image-palette-upload-hint > div { - padding: 4px 0; -} - -.image-palette-loading { - text-align: center; - padding: 20px; - color: #666; -} - -.image-palette-colors { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 16px; -} - -.image-palette-color-item { - width: 80px; - height: 80px; - cursor: pointer; - border-radius: 4px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: 11px; -} - -.image-palette-color-item > div { - padding: 1px 0; -} - -/* ============ AIPalettePage 新布局 ============ */ -.ai-palette-body { - width: 100%; - height: 100%; - padding: 20px; - box-sizing: border-box; -} - -.ai-palette-controls { - display: flex; - align-items: center; - padding-bottom: 16px; -} - -.ai-palette-error { - color: #d32f2f; - padding: 8px 0; -} - -.ai-palette-result { - margin-top: 16px; -} - -.ai-palette-preview { - width: 100%; - height: 120px; - border-radius: 8px; - margin-bottom: 16px; -} - -.ai-palette-colors { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.ai-palette-color-item { - width: 100px; - height: 80px; - cursor: pointer; - border-radius: 4px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: 12px; -} - -.ai-palette-color-item > div { - padding: 1px 0; -} - /* ============ CollectColorsPage 新布局补充 ============ */ .collect-grid { display: flex; diff --git a/plugins/color-helper/main.tsx b/plugins/color-helper/main.tsx index 31c0372ea..29419e6cc 100644 --- a/plugins/color-helper/main.tsx +++ b/plugins/color-helper/main.tsx @@ -5,7 +5,6 @@ import { ThemeProvider, createTheme } from '@mui/material/styles'; import CssBaseline from '@mui/material/CssBaseline'; import './index.css'; -// 创建主题配置 const theme = createTheme({ palette: { primary: { diff --git a/plugins/color-helper/package-lock.json b/plugins/color-helper/package-lock.json deleted file mode 100644 index 6338a3369..000000000 --- a/plugins/color-helper/package-lock.json +++ /dev/null @@ -1,2073 +0,0 @@ -{ - "name": "color-helper", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "color-helper", - "version": "1.0.0", - "dependencies": { - "@emotion/react": "^11.11.1", - "@emotion/styled": "^11.11.0", - "@jaames/iro": "^5.5.2", - "@mui/icons-material": "^5.14.0", - "@mui/material": "^5.14.0", - "chroma-js": "^2.4.2", - "konva": "^9.2.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-konva": "^18.2.10", - "react-router-dom": "^6.15.0" - }, - "devDependencies": { - "@types/chroma-js": "^2.4.0", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@vitejs/plugin-react": "^4.0.4", - "typescript": "^5.1.6", - "vite": "^4.4.9" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "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==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "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==", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", - "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", - "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emotion/babel-plugin": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", - "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.3.3", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/cache": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", - "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", - "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" - }, - "node_modules/@emotion/react": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", - "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", - "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", - "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.2", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" - }, - "node_modules/@emotion/styled": { - "version": "11.14.1", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", - "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/unitless": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", - "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" - }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@irojs/iro-core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@irojs/iro-core/-/iro-core-1.2.1.tgz", - "integrity": "sha512-p2OvsBSSmidsDsTSkID6jEyXDF7lcyxPrkh3qBzasBZFpjkYd6kZ3yMWai3MlAaQ3F7li/Et7rSJVV09Fpei+A==" - }, - "node_modules/@jaames/iro": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/@jaames/iro/-/iro-5.5.2.tgz", - "integrity": "sha512-Fbi5U4Vdkw6UsF+R3oMlPONqkvUDMkwzh+mX718gQsDFt3+1r1jvGsrfCbedmXAAy0WsjDHOrefK0BkDk99TQg==", - "dependencies": { - "@irojs/iro-core": "^1.2.1", - "preact": "^10.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mui/core-downloads-tracker": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.18.0.tgz", - "integrity": "sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/icons-material": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.18.0.tgz", - "integrity": "sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg==", - "dependencies": { - "@babel/runtime": "^7.23.9" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@mui/material": "^5.0.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.18.0.tgz", - "integrity": "sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/core-downloads-tracker": "^5.18.0", - "@mui/system": "^5.18.0", - "@mui/types": "~7.2.15", - "@mui/utils": "^5.17.1", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.10", - "clsx": "^2.1.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1", - "react-is": "^19.0.0", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/private-theming": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz", - "integrity": "sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.17.1", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/styled-engine": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.18.0.tgz", - "integrity": "sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/system": { - "version": "5.18.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.18.0.tgz", - "integrity": "sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.17.1", - "@mui/styled-engine": "^5.18.0", - "@mui/types": "~7.2.15", - "@mui/utils": "^5.17.1", - "clsx": "^2.1.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/types": { - "version": "7.2.24", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", - "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.17.1.tgz", - "integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/types": "~7.2.15", - "@types/prop-types": "^15.7.12", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@remix-run/router": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", - "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/chroma-js": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.5.tgz", - "integrity": "sha512-6ISjhzJViaPCy2q2e6PgK+8HcHQDQ0V2LDiKmYAh+jJlLqDa6HbwDh0wOevHY0kHHUx0iZwjSRbVD47WOUx5EQ==", - "dev": true - }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==" - }, - "node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", - "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/react-reconciler": { - "version": "0.28.9", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", - "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", - "peerDependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/react-transition-group": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", - "peerDependencies": { - "@types/react": "*" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.34", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz", - "integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==", - "dev": true, - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001797", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", - "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chroma-js": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz", - "integrity": "sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==" - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "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==" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.368", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", - "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==", - "dev": true - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/its-fine": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", - "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", - "dependencies": { - "@types/react-reconciler": "^0.28.0" - }, - "peerDependencies": { - "react": ">=18.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/konva": { - "version": "9.3.22", - "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.22.tgz", - "integrity": "sha512-yQI5d1bmELlD/fowuyfOp9ff+oamg26WOCkyqUyc+nczD/lhRa3EvD2MZOoc4c1293TAubW9n34fSQLgSeEgSw==", - "funding": [ - { - "type": "patreon", - "url": "https://www.patreon.com/lavrton" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/konva" - }, - { - "type": "github", - "url": "https://github.com/sponsors/lavrton" - } - ] - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "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==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.47", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", - "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" - }, - "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/preact": { - "version": "10.29.2", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", - "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-is": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", - "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==" - }, - "node_modules/react-konva": { - "version": "18.2.16", - "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.16.tgz", - "integrity": "sha512-bYs5TuTpaSSxBTZ79btaSXjDvLaQIZOVgBEg2ITMyc2p3jwbdIJDh7kOuK4ivxY8uZHWxmQP32bLxlwA17EgXQ==", - "funding": [ - { - "type": "patreon", - "url": "https://www.patreon.com/lavrton" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/konva" - }, - { - "type": "github", - "url": "https://github.com/sponsors/lavrton" - } - ], - "dependencies": { - "@types/react-reconciler": "^0.28.2", - "its-fine": "^1.1.1", - "react-reconciler": "~0.29.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "konva": "^8.0.1 || ^7.2.5 || ^9.0.0 || ^10.0.0", - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/react-reconciler": { - "version": "0.29.2", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", - "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.30.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", - "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", - "dependencies": { - "@remix-run/router": "1.23.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.30.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", - "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", - "dependencies": { - "@remix-run/router": "1.23.3", - "react-router": "6.30.4" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "engines": { - "node": ">=4" - } - }, - "node_modules/rollup": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz", - "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/vite": { - "version": "4.5.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", - "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", - "dev": true, - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "engines": { - "node": ">= 6" - } - } - } -} diff --git a/plugins/color-helper/package.json b/plugins/color-helper/package.json index 66117ab38..bf57c11d7 100644 --- a/plugins/color-helper/package.json +++ b/plugins/color-helper/package.json @@ -1,30 +1,37 @@ { "name": "color-helper", - "version": "1.0.0", + "version": "1.1.0", "description": "颜色助手", "type": "module", + "packageManager": "pnpm@10.33.0", "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest", + "test:run": "vitest run" }, "dependencies": { + "@emotion/cache": "^11.14.0", + "@emotion/css": "^11.13.5", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@jaames/iro": "^5.5.2", - "@mui/icons-material": "^5.14.0", - "@mui/material": "^5.14.0", - "chroma-js": "^2.4.2", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-router-dom": "^6.15.0" + "@mui/icons-material": "^7.3.11", + "@mui/material": "^7.3.11", + "chroma-js": "^3.2.0", + "react": "^19.3.0", + "react-dom": "^19.3.0" }, "devDependencies": { - "@types/chroma-js": "^2.4.0", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@vitejs/plugin-react": "^4.0.4", + "@types/chroma-js": "^3.1.2", + "@types/node": "^26.6.4", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@vitejs/plugin-react": "^6.1.1", + "happy-dom": "^20.14.5", "typescript": "^5.1.6", - "vite": "^4.4.9" + "vite": "^8.3.2", + "vitest": "4.1.11" } } diff --git a/plugins/color-helper/pages/AIPalettePage.tsx b/plugins/color-helper/pages/AIPalettePage.tsx index ef3cc9916..b209c0132 100644 --- a/plugins/color-helper/pages/AIPalettePage.tsx +++ b/plugins/color-helper/pages/AIPalettePage.tsx @@ -1,6 +1,5 @@ -import React, { useState, useEffect, useMemo, useCallback, useRef, memo } from 'react'; +import { useState, useEffect, useMemo, useCallback, useRef, memo } from 'react'; import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; import Button from '@mui/material/Button'; import Card from '@mui/material/Card'; import Dialog from '@mui/material/Dialog'; @@ -37,7 +36,10 @@ interface AIPaletteItem { description: string; } -/** 解析 AI 返回的 JSON 字符串 */ +/** + * 解析 AI 返回的 JSON 字符串。 + * 提示词已要求裸 JSON, 但模型仍可能带上 ```json 围栏, 这里兜底剥掉 + */ function parseAIResponse(content: string): AIPaletteItem[] { if (content.startsWith("{")) return JSON.parse(content); return JSON.parse(content.replace(/^```json\n/, "").replace(/\n```$/, "")); @@ -171,7 +173,7 @@ function IroPicker({ color, onChange }: IroPickerProps) { if (c) pickerRef.current.color.hexString = c.hex(); return; } - pickerRef.current = new iro.ColorPicker(containerRef.current, { + pickerRef.current = iro.ColorPicker(containerRef.current, { width: 190, borderWidth: 1, handleSvg: "#colorHandle", @@ -262,7 +264,8 @@ function ColorPickerDialog({ color, open, onClose, onSubmit }: ColorPickerDialog ); } -/** AI配色主页面 - 上次生成结果缓存 */ +// 模块级缓存: 页面随导航切换卸载、useState 清零, 用它保活上次的主色与生成结果; +// 主色取随机初值是为了首次进入页面时也有非空的主色 let lastMainColor = chroma.random().hex(); let lastPalettes: AIPaletteItem[] = []; @@ -309,38 +312,34 @@ const AIPalettePage = memo(function AIPalettePage(props: AIPaletteProps) { try { const result = await aiChat([ - { - role: "system", - content: ` -# Role -你是一个颜色配色大师 -## Skills -- 读取用户输入的颜色值 -- 给读取的颜色选择合适的配色方案,共需要给用户提供 ${count} 个颜色, 配色中不要有 ${mainColor} -- 生成的配色方案必须符合 ${style} 风格 -- 生成的配色方案必须符合 ${theme} 色系 -- 你给出的每个配色方案必须包含颜色hex值、颜色名称以及颜色描述 -- 返回的配色方案以 JSON 数组格式返回 -## Actions -- 根据用户输入的颜色值生成合适的配色方案 -- 你直接输出 JSON 数组,不再额外输出其他内容 -## Example -[{ - "hex": "#E797B7", - "name": "芭比粉", - "description": "比主色调更浅淡一些,粉色调更明显,与主色调搭配能营造出柔和温暖且具有现代感的氛围。" -}] -## Input -输入:{颜色值} -## Output -{JSON} + { + role: "system", + content: ` +# 角色 +你是颜色配色大师,围绕用户给出的基准色设计可搭配的颜色。 + +# 任务 +生成恰好 ${count} 个颜色,每个都满足: +- 符合${style}风格、属于${theme}色系 +- 不是基准色本身 +- 与基准色以及彼此之间都有明显的色相或明度差别 + +# 输出格式 +只输出一个 JSON 数组,不要输出解释、标题或代码块标记。 +数组长度恰好 ${count},每个元素包含三个字段: +- hex:以 # 开头的六位十六进制颜色值,字母大写,例如 #E797B7 +- name:颜色名称 +- description:这个颜色与基准色如何搭配,以及它带来什么氛围 + +# 示例 +下面只演示字段写法,实际数组长度以上面的任务要求为准: +[{"hex":"#E797B7","name":"芭比粉","description":"比基准色更浅淡,粉调更明显,搭配起来柔和温暖,能冲淡基准色偏冷的感觉。"}] `, - }, - { role: "user", content: `输入: ${[mainColor]}` }, - ] - ); + }, + { role: "user", content: `基准色: ${mainColor}` }, + ]); - const parsed = parseAIResponse(result.content); + const parsed = parseAIResponse(result.content ?? ''); if (!Array.isArray(parsed)) { props.showMessage("AI 生成配色失败"); return; @@ -499,8 +498,8 @@ const AIPalettePage = memo(function AIPalettePage(props: AIPaletteProps) { ); }); -// 解析颜色输入 -function parseColorInput(input: string | null): chroma.Color | null { +// 解析颜色输入(宽松实现, 直接交给 chroma; 导出以供回归测试) +export function parseColorInput(input: string | null): chroma.Color | null { if (!input) return null; try { return chroma(input); diff --git a/plugins/color-helper/pages/CollectColorsPage.tsx b/plugins/color-helper/pages/CollectColorsPage.tsx index a131af7f4..60a81835a 100644 --- a/plugins/color-helper/pages/CollectColorsPage.tsx +++ b/plugins/color-helper/pages/CollectColorsPage.tsx @@ -15,24 +15,17 @@ import EditIcon from '@mui/icons-material/Edit'; import DeleteIcon from '@mui/icons-material/Delete'; import AddIcon from '@mui/icons-material/Add'; import ColorizeIcon from '@mui/icons-material/Colorize'; -import chroma from 'chroma-js'; import { db, dbStorage, screenColorPick } from '../utils/platform'; +import { isDarkColor } from '../utils/color'; +import { COLLECT_SORT_KEY, type CollectColor, collectDocId, normalizeHex } from '../utils/collect'; /** * CollectColorsPage - 收藏颜色管理页面 - * + * * 管理用户收藏的颜色,支持分组、拖拽排序、添加/删除 * 数据持久化到 dbStorage */ -interface CollectColor { - _id: string; - _rev?: string; - name: string; - color: string; - dark?: boolean; -} - // 编辑表单对话框 interface FormDialogProps { formData: CollectColor | null; @@ -67,20 +60,19 @@ class CollectFormDialog extends Component { this.props.formData!.name = name; const isEdit = !!this.props.formData!._id; if (!isEdit) { - const parsed = chroma.valid(color) ? chroma(color) : null; - if (!parsed) { + const hex = normalizeHex(color); + if (!hex) { this.setState({ error: "错误的色值" }); return; } - const hex = parsed.hex(); - const docId = "color/" + hex.substring(1).toLowerCase(); + const docId = collectDocId(hex); if (db.get(docId)) { this.setState({ error: "该颜色已收藏!" }); return; } this.props.formData!._id = docId; - this.props.formData!.color = hex.toUpperCase(); - this.props.formData!.dark = parsed.get("lab.l") < 70; + this.props.formData!.color = hex; + this.props.formData!.dark = isDarkColor(hex); } const result = db.put(this.props.formData!); if (result?.error) { @@ -239,10 +231,15 @@ class CollectColorsPage extends Component<{ onColorClick: (e: any) => void }, Co let markerDoc: any = null; const allDocs = db.allDocs(); + if (!Array.isArray(allDocs)) { + this.state = { colors: [], colorForm: null, openDeleteData: null }; + return; + } allDocs.forEach((doc: any) => { + if (!doc || typeof doc._id !== "string") return; if (doc._id.startsWith("color/")) { colors.push(doc); - } else if (doc._id === "collectsort") { + } else if (doc._id === COLLECT_SORT_KEY) { sortDoc = doc; } else if (doc._id === "markercolor") { markerDoc = doc; @@ -250,14 +247,14 @@ class CollectColorsPage extends Component<{ onColorClick: (e: any) => void }, Co }); // 迁移markercolor旧数据 - if (markerDoc) { + if (markerDoc && Array.isArray(markerDoc.colors)) { markerDoc.colors.forEach((color: string) => { + if (typeof color !== "string") return; const upper = color.toUpperCase(); if (!/^#[A-F0-9]{6}$/.test(upper) || colors.find(c => c.color === upper)) return; - const dark = chroma(color).get("lab.l") < 70; - const newDoc: CollectColor = { _id: "color/" + color.substring(1).toLowerCase(), name: "", color: upper, dark }; + const newDoc: CollectColor = { _id: "color/" + color.substring(1).toLowerCase(), name: "", color: upper, dark: isDarkColor(upper) }; const result = db.put(newDoc); - if (result.ok) { + if (result && result.ok) { newDoc._rev = result.rev; colors.push(newDoc); } @@ -306,6 +303,8 @@ class CollectColorsPage extends Component<{ onColorClick: (e: any) => void }, Co } const index = colors.findIndex(c => c._id === openDeleteData._id); if (index !== -1) colors.splice(index, 1); + // 同步排序索引, 否则 collectsort 会永久残留僵尸 id + dbStorage.setItem("collectsort", colors.map(c => c._id)); this.setState({ openDeleteData: null }); }; @@ -335,7 +334,18 @@ class CollectColorsPage extends Component<{ onColorClick: (e: any) => void }, Co this.props.onColorClick(c.color)} + onKeyDown={(e) => { + // 仅响应落在本元素上的按键, 避免内部编辑/删除按钮的回车冒泡触发复制 + if (e.target !== e.currentTarget) return; + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.props.onColorClick(c.color); + } + }} sx={{ backgroundColor: c.color, color: c.dark ? "#fff" : "#333", diff --git a/plugins/color-helper/pages/ColorPage.tsx b/plugins/color-helper/pages/ColorPage.tsx index ab164af73..672222a42 100644 --- a/plugins/color-helper/pages/ColorPage.tsx +++ b/plugins/color-helper/pages/ColorPage.tsx @@ -1,9 +1,8 @@ import React, { Component, PureComponent } from 'react'; -import Box from '@mui/material/Box'; import Card from '@mui/material/Card'; import Typography from '@mui/material/Typography'; import IconButton from '@mui/material/IconButton'; -import { copyText as platformCopyText, screenColorPick } from '../utils/platform'; +import { screenColorPick } from '../utils/platform'; import Tooltip from '@mui/material/Tooltip'; import Divider from '@mui/material/Divider'; import CheckIcon from '@mui/icons-material/Check'; @@ -12,6 +11,7 @@ import ColorizeIcon from '@mui/icons-material/Colorize'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import chroma from 'chroma-js'; import iro from '@jaames/iro'; +import { isDarkColor } from '../utils/color'; /** * ColorPage - 颜色转换页面 @@ -97,8 +97,9 @@ function convertStateColor(color: chroma.Color): ColorState { }; } -/** 按指定格式解析颜色字符串,返回 chroma.Color 或 null */ -function parseColorByType(type: string, value: string): chroma.Color | null { +/** 按指定格式解析颜色字符串,返回 chroma.Color 或 null + * 导出以供回归测试(另一处同名实现在 AIPalettePage, 行为不同, 见 tests/color-parsing.test.ts) */ +export function parseColorByType(type: string, value: string): chroma.Color | null { if (type === "hex") { if (/^[0-9a-fA-F]{6}$/.test(value)) return chroma("#" + value); return null; @@ -140,8 +141,8 @@ function parseColorByType(type: string, value: string): chroma.Color | null { return null; } -/** 自动识别颜色格式并解析输入字符串 */ -function parseColorInput(input: string): chroma.Color | null { +/** 自动识别颜色格式并解析输入字符串(导出以供回归测试) */ +export function parseColorInput(input: string): chroma.Color | null { if (!input || input.length < 4) return null; let type: string; let value = input; @@ -174,7 +175,7 @@ class IroColorPicker extends PureComponent { componentDidMount() { if (!this.containerRef.current) return; - this.colorPicker = new iro.ColorPicker(this.containerRef.current, { + this.colorPicker = iro.ColorPicker(this.containerRef.current, { width: 300, borderWidth: 1, handleSvg: "#handle", @@ -226,7 +227,8 @@ class IroColorPicker extends PureComponent { interface ColorPageProps { value: (string | null)[]; - onColorClick: (e: any) => void; + /** (复制出去的原文, 该色的 hex) —— 原文用于复制与提示条回显, hex 用于定位操作对象 */ + onColorClick: (copied: string, hex: string) => void; setting: boolean; showMessage: (msg: string) => void; } @@ -236,21 +238,18 @@ interface ColorPageState { activeIndex: number; } -/** 配色方案标签映射 */ -const schemeLabels: Record = { - complementary: "互补色", - triadic: "对比色", - analogous: "类似色", - tetradic: "中差色", -}; +/** + * 色板槽位数。 + * 扣掉导航栏 136 与色卡内边距后, 一行放得下 6 个 36px 色块 + 间距 + 加号按钮。 + * 填空规则: 未满追加到尾部, 已满挤掉最早的(见 setNewColor)。 + */ +export const MAX_COLOR_HUB_SLOTS = 6; class ColorPage extends Component { - private _currentChromaColor: chroma.Color | null = null; - - /** 添加新颜色到色板(最多8个,超出时移除最早的) */ + /** 添加新颜色到色板: 未满追加, 已满挤掉最早的 */ setNewColor = (color: ColorState) => { const colors = [...this.state.colors, color]; - if (colors.length > 8) colors.shift(); + if (colors.length > MAX_COLOR_HUB_SLOTS) colors.shift(); this.setState({ colors, activeIndex: colors.length - 1 }); }; @@ -272,28 +271,36 @@ class ColorPage extends Component { this.setState({ colors: [...this.state.colors], activeIndex: index }); }; - /** 复制指定格式的颜色值到剪贴板 */ + /** + * 复制当前色值并弹提示条 + * + * 原文(chroma 解析不了 rgb/hsl/hsv/hsi/cmyk/lab)与当前色的 hex 一起交给 + * props.onColorClick —— 原文用于复制与提示条回显, hex 用于定位 + * 收藏 / 查看 / 配色 三个操作。颜色页上该提示条不显示「查看」(见 App.tsx)。 + */ handleCssCodeCopy = (format: string) => () => { - const color = this.state.colors[this.state.activeIndex]; - let copyText: string; - if (this.props.setting) { - if (format === "hex") copyText = color.hex.substring(1); - else if (format === "rgb") copyText = color.rgb; - else if (format === "cmyk") copyText = color.cmyk; - else if (["hsl", "hsi", "hsv"].includes(format)) copyText = (color as any)[format]; - else if (format === "lab") copyText = color.lab; - else return; - } else { - if (format === "hex") copyText = color.hex; - else if (format === "rgb") copyText = "rgb(" + color.rgb + ")"; - else if (format === "cmyk") copyText = "cmyk(" + color.cmyk + ")"; - else if (["hsl", "hsi", "hsv"].includes(format)) copyText = format + "(" + (color as any)[format] + ")"; - else if (format === "lab") copyText = "lab(" + color.lab + ")"; - else return; - } - platformCopyText(copyText); + const text = this.getCssCode(format); + if (!text) return; + this.props.onColorClick(text, this.state.colors[this.state.activeIndex].hex); }; + /** + * 按格式拼出当前色的完整色值串, 未知格式返回空串 + * + * 一律带标识(hex 带 "#", 其余带 "rgb(...)" 等): 复制出去的内容由 App 按 + * "色值去 #" 设置处理 —— 该设置只对 hex 有意义, 各格式串里没有 #。 + * 这里跟着 setting 变的话, 同一格式会产出两种文本, 反而看不出复制了什么。 + */ + getCssCode(format: string): string { + const color = this.state.colors[this.state.activeIndex]; + if (format === "hex") return color.hex; + if (format === "rgb") return "rgb(" + color.rgb + ")"; + if (format === "cmyk") return "cmyk(" + color.cmyk + ")"; + if (format === "lab") return "lab(" + color.lab + ")"; + if (["hsl", "hsi", "hsv"].includes(format)) return format + "(" + (color as any)[format] + ")"; + return ""; + } + /** 颜色值输入变更: 解析输入并更新颜色状态,解析失败则保留输入草稿 */ handleValueChange = (format: string) => (e: React.ChangeEvent) => { const value = e.target.value; @@ -325,6 +332,7 @@ class ColorPage extends Component { if (props.value?.[0]) { const parsed = parseColorInput(props.value[0]!); + // 一次性载荷, 消费即清空: 防止组件重挂载时同一个色值再次入色板 props.value[0] = null; if (parsed) initialColor = convertStateColor(parsed); } @@ -332,7 +340,7 @@ class ColorPage extends Component { if (cachedState) { if (initialColor) { cachedState.colors.push(initialColor); - if (cachedState.colors.length > 8) cachedState.colors.shift(); + if (cachedState.colors.length > MAX_COLOR_HUB_SLOTS) cachedState.colors.shift(); cachedState.activeIndex = cachedState.colors.length - 1; } this.state = cachedState; @@ -411,7 +419,7 @@ class ColorPage extends Component { key={i} data-key={color} className="color-extend-value" - onClick={() => onColorClick({ currentTarget: { style: { backgroundColor: hex } } })} + onClick={() => onColorClick(hex, hex)} style={{ backgroundColor: color, boxSizing: 'border-box', @@ -457,7 +465,7 @@ class ColorPage extends Component { />
- + @@ -476,7 +484,7 @@ class ColorPage extends Component { />
- + @@ -495,7 +503,7 @@ class ColorPage extends Component { />
- + @@ -514,7 +522,7 @@ class ColorPage extends Component { />
- + @@ -533,7 +541,7 @@ class ColorPage extends Component { />
- + @@ -552,7 +560,7 @@ class ColorPage extends Component { />
- + @@ -564,7 +572,7 @@ class ColorPage extends Component {
{colors.map((color, index) => { - const isDark = chroma(color.hex).get('lab.l') < 70; + const isDark = isDarkColor(color.hex); return ( { ); })}
- + = MAX_COLOR_HUB_SLOTS ? `已满 ${MAX_COLOR_HUB_SLOTS} 个,将替换最早的颜色` : "新建随机颜色"} + > { - (group as string[][]).forEach(colors => { + (group as unknown as GradientPreset[]).forEach(colors => { allGradients!.push({ colors, tags: colors.map(c => classifyColor(c)), @@ -71,18 +73,8 @@ function getGradients(): GradientItem[] { return allGradients; } -/** 计算渐变线的起止坐标(用于 Canvas 渲染) */ -function calculateGradientLine(width: number, height: number, angle: number) { - const rad = angle * Math.PI / 180 + Math.PI / 2; - const len = Math.sqrt(width / 2 * width / 2 + height / 2 * height / 2); - const tx = Math.cos(rad) * len + width / 2; - const ty = Math.sin(rad) * len + height / 2; - return { tx, ty, bx: width / 2, by: height / 2 }; -} - -/** 默认渐变角度和尺寸 */ +/** 默认渐变角度 */ const DEFAULT_ANGLE = 135; -const DEFAULT_SIZE = 500; /** 方向编辑器 - 可视化调整渐变角度的圆形控件 */ interface DirectionEditorProps { @@ -160,6 +152,8 @@ class DirectionEditor extends PureComponent { /** 渐变色详情弹窗 - 角度/尺寸调整、CSS代码复制、导出图片 */ interface GradientDialogProps { gradientData: string[] | null; + /** 浏览器模式下的操作反馈(平台模式靠窗口隐藏反馈, 不需要) */ + showMessage: (msg: string) => void; } interface GradientDialogState { @@ -202,14 +196,15 @@ class GradientDialog extends Component handleCssCodeCopy = () => { const cssCode = `linear-gradient(${this.state.angle}deg,${this.props.gradientData!.join(",").toLowerCase()})`; copyText(cssCode); - hideMainWindow(); + if (isPlatform) hideMainWindow(); + else this.props.showMessage(`已复制 ${cssCode}`); }; handleImageExport = () => { const { angle, imageWidth, imageHeight } = this.state; const w = parseInt(imageWidth); const h = parseInt(imageHeight); - if (w < 1 || h < 1) return; + if (!Number.isFinite(w) || !Number.isFinite(h) || w < 1 || h < 1) return; const canvas = document.createElement("canvas"); canvas.width = w; @@ -237,9 +232,10 @@ class GradientDialog extends Component ctx.fillStyle = gradient; ctx.fillRect(0, 0, w, h); - copyImage(canvas.toDataURL()); + copyImage(canvas.toDataURL(), `gradient-${w}x${h}.png`); canvas.remove(); - hideMainWindow(); + if (isPlatform) hideMainWindow(); + else this.props.showMessage(`已下载图片 ${w}×${h}`); }; componentDidUpdate(prev: GradientDialogProps) { @@ -333,10 +329,15 @@ interface GradientsPageState { gradientData: string[] | null; } -class GradientsPage extends Component<{ onColorClick: (e: any) => void }, GradientsPageState> { +interface GradientsPageProps { + onColorClick: (e: any) => void; + showMessage: (msg: string) => void; +} + +class GradientsPage extends Component { private gradientContentRef: HTMLDivElement | null = null; - constructor(props: { onColorClick: (e: any) => void }) { + constructor(props: GradientsPageProps) { super(props); this.state = { gradients: getGradients(), @@ -387,7 +388,7 @@ class GradientsPage extends Component<{ onColorClick: (e: any) => void }, Gradie
{gradient.colors.map((color, j) => ( @@ -402,7 +403,7 @@ class GradientsPage extends Component<{ onColorClick: (e: any) => void }, Gradie ))}
- +
); } diff --git a/plugins/color-helper/pages/ImagePalettePage.tsx b/plugins/color-helper/pages/ImagePalettePage.tsx index 4f69b4548..59f397746 100644 --- a/plugins/color-helper/pages/ImagePalettePage.tsx +++ b/plugins/color-helper/pages/ImagePalettePage.tsx @@ -9,8 +9,6 @@ import Dialog from '@mui/material/Dialog'; import IconButton from '@mui/material/IconButton'; import TextField from '@mui/material/TextField'; import CircularProgress from '@mui/material/CircularProgress'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; import CloseIcon from '@mui/icons-material/Close'; import RefreshIcon from '@mui/icons-material/Refresh'; import ClearIcon from '@mui/icons-material/Clear'; @@ -18,9 +16,9 @@ import FolderOpenIcon from '@mui/icons-material/FolderOpen'; import CropIcon from '@mui/icons-material/Crop'; import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; import chroma from 'chroma-js'; +import { isDarkColor } from '../utils/color'; import { quantize } from '../utils/quantize'; -import { isDarkColor, textColorFor, dimColor } from '../utils/color'; -import { screenCapture, copyText, hideMainWindow, aiChat, isAIAvailable } from '../utils/platform'; +import { screenCapture, aiChat, isAIAvailable } from '../utils/platform'; // 导入模板封面图 import cover01 from '../assets/color-card-01.jpg'; @@ -89,7 +87,6 @@ function getColor(img: HTMLImageElement, quality: number = 10): number[] | null * @param quality 采样间隔,默认10 */ function getPalette(img: HTMLImageElement, colorCount?: number, quality?: number): number[][] | null { - // 参数校验 - let count = colorCount; let q = quality; if (count !== undefined && Number.isInteger(count)) { @@ -106,19 +103,18 @@ function getPalette(img: HTMLImageElement, colorCount?: number, quality?: number const imageData = getImageData(img); const pixels = filterPixels(imageData.data, imageData.width * imageData.height, q); const cmap = quantize(pixels, count); - return cmap && cmap !== false ? cmap.palette() : null; + return cmap ? cmap.palette() : null; } /** * 从图片中提取主色和配色 - * 主色: getColor 提取5色中的第一个 - * 配色: getPalette 提取10色,排除主色后的列表 + * 主色: 取样粗一些(quality 10)取 5 色的首个, 作为整图的代表色 + * 配色: 取样细一些取 10 色, 排除主色后的列表 */ -// 原始: const o = new Uu; s = a4(o.getColor(n.target)); c = o.getPalette(n.target).map(u => a4(u)) function extractColorsFromImage(img: HTMLImageElement): { mainColor: string; paletteColors: string[] } | null { try { - const mainRgb = getColor(img, 10); // getColor(target, 10) → getPalette(target, 5, 10)[0] - const paletteRgbs = getPalette(img); // getPalette(target) → colorCount=10, quality=10 + const mainRgb = getColor(img, 10); + const paletteRgbs = getPalette(img); if (!mainRgb) return null; @@ -133,6 +129,9 @@ function extractColorsFromImage(img: HTMLImageElement): { mainColor: string; pal } } +/** AI 名称缓存,避免重复调用 */ +const nameCache = new Map(); + /** * AI 色卡名称生成 Hook * 调用 AI 根据颜色值生成文艺名称,支持缓存和自动生成 @@ -140,13 +139,16 @@ function extractColorsFromImage(img: HTMLImageElement): { mainColor: string; pal * @param style 命名风格,默认"文艺优雅" * @param nameLength 名称字数,默认4 * @param autoGenerate 是否在挂载时自动生成 + * + * status 表示名字是否已经有定论: idle 是"还没开始", loading 中, + * done / error 都算"有定论了" —— 调用方要等它落定再开始画图, + * 否则会先画一版占位名。 */ -/** AI 名称缓存,避免重复调用 */ -const nameCache = new Map(); +type NameStatus = 'idle' | 'loading' | 'done' | 'error'; function useGenerateName(color: string, style: string = '文艺优雅', nameLength: number = 4, autoGenerate: boolean = false) { const [name, setName] = useState(''); - const [loading, setLoading] = useState(false); + const [status, setStatus] = useState('idle'); const [error, setError] = useState(''); const cacheKey = useMemo(() => color + style + nameLength, [color, style, nameLength]); @@ -161,129 +163,83 @@ function useGenerateName(color: string, style: string = '文艺优雅', nameLeng }; }, [cacheKey]); - const generateName = useCallback((targetCacheKey: string, excludeName?: string) => { + const generateName = useCallback((excludeName?: string) => { const ai = isAIAvailable(); if (!ai) { setError('当前版本不支持 AI 功能'); - return null; + setStatus('error'); + return; } const systemPrompt = ` -# Role -你是一个颜色命名大师 -## Skills -- 读取用户输入的颜色值 -- 给读取的颜色起一个好听的名字,名字风格${style} -## Actions -- 根据用户输入的颜色起一个名字 -- 你直接输出名字,不再额外输出其他内容 -- 名字必须是${nameLength}个汉字组成 -${excludeName ? `- 名字不能是${excludeName}` : ''} -## Input -输入:{颜色值} -## 输出 -{名字} +# 角色 +你是颜色命名大师,给颜色起好听的名字。 + +# 任务 +给用户给出的颜色起一个名字,要求: +- 名字恰好 ${nameLength} 个汉字 +- 风格${style} +${excludeName ? `- 不能是${excludeName}\n` : ''} +# 输出格式 +只输出名字本身,不要引号、标点、解释或代码块标记。 + +# 示例 +暮色青岚 `; - setLoading(true); + // 记下发起时的 key, 回调里对比它判断这次结果是否已经过期 + const target = cacheKey; + setStatus('loading'); setName(''); setError(''); - const promise = aiChat([ + aiChat([ { role: 'system', content: systemPrompt }, - { role: 'user', content: `输入: ${color}` }, - ]); - - promise.then(result => { - if (!isMounted.current || targetCacheKey !== latestCacheKey.current) return; - setLoading(false); - setName(result.content); - nameCache.set(targetCacheKey, result.content); + { role: 'user', content: `颜色: ${color}` }, + ]).then(result => { + if (!isMounted.current || target !== latestCacheKey.current) return; + // ai() 返回的 content 是可选字段, 缺失时按空串处理 + const next = result.content ?? ''; + setName(next); + setStatus('done'); + // 只在非"重新生成"时写缓存, 否则换过的名字会污染下次打开弹窗的命中结果 + if (!excludeName) nameCache.set(target, next); }).catch(() => { - if (!isMounted.current || targetCacheKey !== latestCacheKey.current) return; - setLoading(false); + if (!isMounted.current || target !== latestCacheKey.current) return; setError('AI 调用异常'); + setStatus('error'); }); - - return promise; - }, [style, nameLength, color]); + }, [style, nameLength, color, cacheKey]); // 自动生成 useEffect(() => { if (!autoGenerate || !color) return; - const key = color + style + nameLength; - if (nameCache.has(key)) { - setName(nameCache.get(key)!); + const cached = nameCache.get(cacheKey); + if (cached !== undefined) { + setName(cached); + setStatus('done'); return; } - generateName(key); - }, [color, style, nameLength, generateName, autoGenerate]); + generateName(); + }, [cacheKey, color, autoGenerate, generateName]); - return [name, { loading, error, generateName, cacheKey }] as const; + return { name, status, error, generateName }; } -/** 画面场景选项(用于 AI 色卡描述) */ -const SCENE_OPTIONS = ["明星影视", "游戏动漫", "生活家居", "文旅出行", "时尚美妆", "体育赛事", "科技数码", "汽车交通", "艺术设计"]; - -/** 画面场景选择器 - 下拉菜单选择场景标签 */ -interface SceneSelectorProps { - data: string[]; - value: string | null; - disabled?: boolean; - onChange: (value: string | null) => void; -} - -function SceneSelector({ data, value, disabled, onChange }: SceneSelectorProps) { - const [anchorEl, setAnchorEl] = useState(null); - - if (value) { - return ( - - {value} - { if (!disabled) onChange(null); }} /> - - ); - } - - return ( - <> - { if (!disabled) setAnchorEl(e.currentTarget); }}> - - 画面 - - setAnchorEl(null)} - MenuListProps={{ dense: true, disablePadding: true }}> - {data.map(item => ( - { onChange(item); setAnchorEl(null); }}> - {item} - - ))} - - - ); -} - -/** 色卡模板定义: id/尺寸/封面图/名称字数 */ +/** 色卡模板定义: id/尺寸/封面图 */ interface TemplateDef { id: string; sizes: number | [number, number]; cover: string; - nameLength: number; } const TEMPLATES: TemplateDef[] = [ - { id: '01', sizes: 1200, cover: cover01, nameLength: 4 }, - { id: '02', sizes: 1200, cover: cover02, nameLength: 4 }, - { id: '03', sizes: [1200 * 3 / 4, 1200], cover: cover03, nameLength: 4 }, - { id: '04', sizes: [1800 * 9 / 16, 1800], cover: cover04, nameLength: 4 }, + { id: '01', sizes: 1200, cover: cover01 }, + { id: '02', sizes: 1200, cover: cover02 }, + { id: '03', sizes: [1200 * 3 / 4, 1200], cover: cover03 }, + { id: '04', sizes: [1800 * 9 / 16, 1800], cover: cover04 }, ]; /** 获取模板的画布尺寸 [宽, 高] */ @@ -297,8 +253,11 @@ function getTemplateSize(template: TemplateDef): [number, number] { /** * 色卡 Canvas 渲染 - * 根据模板类型在 Canvas 上绘制色卡: 背景色/图片 + 名称 + 配色色块 - * 支持4种模板: 01(渐变遮罩)/02(模糊大背景)/03(cover填充)/04(底部横条) + * 四个模板分别对应一种版式: 01 右下横向色块 / 02 底部全宽等分 / 03 左上竖排圆形 / 04 白底内缩留白, 上 3/4 图片 + 下方居左名称与色块 + * 背景图一律 cover: 01-03 铺满整张卡片, 04 只铺内容区上 3/4 + * 主色铺底兜住没有背景图的情况, 04 用白底 + * + * withText=false 时只画背景与色块(供 UI 里的占位预览用), 不画任何文字 */ function renderColorCard( canvas: HTMLCanvasElement, @@ -308,195 +267,297 @@ function renderColorCard( paletteColors: string[]; name: string; templateId: string; + withText?: boolean; } ) { - const { bgImage, primaryColor, paletteColors, name, templateId } = options; + const { bgImage, primaryColor, paletteColors, name, templateId, withText = true } = options; const template = TEMPLATES.find(t => t.id === templateId) || TEMPLATES[0]; const [w, h] = getTemplateSize(template); canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d')!; - const dark = isDarkColor(primaryColor); - const textCol = dark ? '#fff' : '#000'; - const dimCol = dimColor(primaryColor, 0.3); - const semiCol = dimColor(primaryColor, 0.6); - // 通用渲染:背景色填充 - ctx.fillStyle = primaryColor; + // 所有尺寸按卡片宽度的百分比取(q = 1cqw), 换画布比例时元素占比不变 + const q = (v: number) => (w * v) / 100; + + /** 把背景图 cover 画进指定矩形, 超出部分裁掉 */ + const drawCover = (x: number, y: number, boxW: number, boxH: number) => { + if (!bgImage) return; + const scale = Math.max(boxW / bgImage.width, boxH / bgImage.height); + const dw = bgImage.width * scale; + const dh = bgImage.height * scale; + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, boxW, boxH); + ctx.clip(); + ctx.drawImage(bgImage, x + (boxW - dw) / 2, y + (boxH - dh) / 2, dw, dh); + ctx.restore(); + }; + + // 通用渲染:背景色填充, 04 是白底 + ctx.fillStyle = templateId === '04' ? '#fff' : primaryColor; ctx.fillRect(0, 0, w, h); // 背景图(如果有) if (bgImage) { - const coverFit = (cw: number, ch: number) => { - const scale = Math.max(cw / bgImage.width, ch / bgImage.height) * 1.8; - const dw = bgImage.width * scale; - const dh = bgImage.height * scale; - return { dx: (cw - dw) / 2, dy: 0, dw, dh }; - }; - if (templateId === '01') { - // 01: 图片居中上半部分,下半渐变遮罩 - const scale = Math.min(w / bgImage.width, h / bgImage.height); + // 01: 图片整幅 cover 铺满 + const scale = Math.max(w / bgImage.width, h / bgImage.height); const dw = bgImage.width * scale; const dh = bgImage.height * scale; - ctx.drawImage(bgImage, (w - dw) / 2, 0, dw, dh); + ctx.drawImage(bgImage, (w - dw) / 2, (h - dh) / 2, dw, dh); - // 下半渐变遮罩 - const grad = ctx.createLinearGradient(0, h * 0.5, 0, h * 0.65); + // 底部 1/4 纯色阻挡, 上缘一小段渐变过渡 + const bandTop = (h * 3) / 4; + const grad = ctx.createLinearGradient(0, bandTop, 0, bandTop + h * 0.08); grad.addColorStop(0, chroma(primaryColor).alpha(0.01).hex()); grad.addColorStop(0.1, chroma(primaryColor).alpha(0.5).hex()); grad.addColorStop(0.25, primaryColor); grad.addColorStop(1, primaryColor); ctx.fillStyle = grad; - ctx.fillRect(0, h * 0.5, w, h * 0.5); + ctx.fillRect(0, bandTop, w, h - bandTop); } else if (templateId === '02') { - // 002: 图片模糊大背景 + 居中裁剪 - const { dx, dy, dw, dh } = coverFit(w, h); - ctx.globalAlpha = 0.3; - ctx.filter = 'blur(40px)'; - ctx.drawImage(bgImage, dx, dy, dw, dh); - ctx.filter = 'none'; - ctx.globalAlpha = 1; - - // 居中裁剪图 - const scale2 = Math.min(w * 0.95 / bgImage.width, h * 0.95 / bgImage.height); - const dw2 = bgImage.width * scale2; - const dh2 = bgImage.height * scale2; - ctx.drawImage(bgImage, (w - dw2) / 2, (h - dh2) / 2, dw2, dh2); - } else if (templateId === '03') { - // 003: 图片 cover 填充 + // 02: 图片整幅 cover 铺满 const scale = Math.max(w / bgImage.width, h / bgImage.height); const dw = bgImage.width * scale; const dh = bgImage.height * scale; ctx.drawImage(bgImage, (w - dw) / 2, (h - dh) / 2, dw, dh); - } else if (templateId === '04') { - // 004: 图片 cover 填充 + } else if (templateId === '03') { + // 图片 cover 填充 const scale = Math.max(w / bgImage.width, h / bgImage.height); const dw = bgImage.width * scale; const dh = bgImage.height * scale; ctx.drawImage(bgImage, (w - dw) / 2, (h - dh) / 2, dw, dh); + } else if (templateId === '04') { + // 04: 内容整体内缩留白, 图片铺满内容区上方 3/4 + const inset = q(6); + const contentW = w - inset * 2; + const contentH = h - inset * 2; + drawCover(inset, inset, contentW, contentH * 0.75); } } - // 文字和配色色块 + // 配色色块。四个模板版式不同, 文字统一走 label(), + // 占位预览(withText=false)时只出色块 + const label = (value: string, x: number, y: number) => { + if (withText) ctx.fillText(value, x, y); + }; + /** 该模板要显示的色: 主色在前, 再补配色; 配色为空时只显示主色 */ + const pick = (n: number) => [primaryColor, ...paletteColors].slice(0, n); + + /** 色块内色值的颜色: 深色块用白字, 浅色块用深字 */ + const hexOn = (color: string) => (isDarkColor(color) ? 'rgba(255,255,255,0.92)' : 'rgba(0,0,0,0.75)'); + + // 字号一律由它要放的槽位反推, 保证"字号变大 + 槽位固定"时文字还能缩回去 + // 文本框按 'M' 量, 比按平均字宽估更保守 + let measureCtx: CanvasRenderingContext2D | null = null; + const textW = (text: string, font: string): number => { + if (!measureCtx) measureCtx = document.createElement('canvas').getContext('2d')!; + measureCtx.font = font; + return measureCtx.measureText(text).width; + }; + /** 在 [min,max] 内找能塞进 maxW 的最大字号; 连 min 都放不下时返回 0, 由调用方跳过不写 */ + const fitFont = (text: string, weight: string, maxW: number, min: number, max: number): number => { + if (maxW <= 0) return 0; + const fontAt = (size: number) => weight ? `${weight} ${size}px sans-serif` : `${size}px sans-serif`; + if (textW(text, fontAt(max)) <= maxW) return max; + let lo = 0; + let hi = max; + for (let i = 0; i < 12; i++) { + const mid = (lo + hi) / 2; + if (textW(text, fontAt(mid)) <= maxW) lo = mid; + else hi = mid; + } + return lo >= min ? lo : 0; + }; + + const shadowOn = () => { + ctx.shadowColor = 'rgba(0,0,0,0.5)'; + ctx.shadowBlur = q(1); + }; + const shadowOff = () => { ctx.shadowBlur = 0; }; + if (templateId === '01') { - // 01 模板:下半部分文字 + 配色 - const baseY = h * 2.1 / 3; - ctx.fillStyle = semiCol; - ctx.font = `bold ${w / 9}px serif`; - ctx.textAlign = 'left'; - ctx.fillText(name || '配色方案', w / 20, baseY); - - // 主色圆 + 色值 - const circleR = w / 50; - const circleY = baseY + h / 6; - ctx.beginPath(); - ctx.arc(w / 20 + circleR, circleY, circleR, 0, Math.PI * 2); - ctx.fillStyle = semiCol; - ctx.fill(); - ctx.fillStyle = semiCol; - ctx.font = `bold ${w / 30}px sans-serif`; - ctx.textAlign = 'left'; - ctx.fillText(primaryColor, w / 20 + circleR * 2 + w / 30, circleY + w / 80); - - // 配色色块 - const colors = paletteColors.slice(0, 4); - const blockSize = w / 7; - const startX = w / 2; + // 01 右下角矩形色块组, 整组对齐右下角 + // 名称在左侧、顶部与色块组齐平 + const colors = pick(5); + const gap = q(1); + const blockH = q(16); + const margin = q(4); + const widths = colors.map((_, i) => (i === 0 ? q(12) : q(9))); + const totalW = widths.reduce((a, b) => a + b, 0) + gap * (colors.length - 1); + const groupX = w - margin - totalW; + const groupY = h - margin - blockH; + + let x = groupX; colors.forEach((color, i) => { - const y = circleY + w / 60 + i * (blockSize + w / 60); ctx.fillStyle = color; - ctx.beginPath(); - ctx.roundRect(startX, y, blockSize, blockSize, 8); - ctx.fill(); - ctx.fillStyle = textColorFor(color); - ctx.font = `${w / 38}px sans-serif`; - ctx.textAlign = 'center'; - ctx.fillText(color, startX + blockSize / 2, y + blockSize / 2 + w / 76); + ctx.fillRect(x, groupY, widths[i], blockH); + // 主色块不写色号, 只让配色块带自己的值 + if (i > 0) { + const text = color.toUpperCase(); + const size = fitFont(text, '', widths[i] - q(1.2), q(0.5), q(2.5)); + if (size > 0) { + ctx.fillStyle = hexOn(color); + ctx.font = `${size}px sans-serif`; + ctx.textAlign = 'center'; + label(text, x + widths[i] / 2, groupY + blockH / 2 + size / 3); + } + } + x += widths[i] + gap; }); - // 签名 - // ctx.fillStyle = dimCol; - // ctx.font = `${w / 32}px sans-serif`; - // ctx.textAlign = 'left'; - // ctx.fillText('色卡', w / 20, baseY + h / 6 + w / 20); + if (withText) { + const nameText = name || '配色方案'; + const nameMax = groupX - margin - q(2); + const nameSize = fitFont(nameText, 'bold', nameMax, q(1.6), q(3.2)); + if (nameSize > 0) { + shadowOn(); + ctx.textAlign = 'left'; + ctx.fillStyle = '#fff'; + ctx.font = `bold ${nameSize}px sans-serif`; + ctx.fillText(nameText, margin, groupY + nameSize * 0.8); + + // 主色号跟在名称下面 + const hexSize = fitFont(primaryColor.toUpperCase(), '', nameMax, q(0.8), nameSize * 0.5); + if (hexSize > 0) { + ctx.fillStyle = 'rgba(255,255,255,0.75)'; + ctx.font = `${hexSize}px sans-serif`; + ctx.fillText(primaryColor.toUpperCase(), margin, groupY + nameSize * 0.8 + hexSize * 1.6); + } + shadowOff(); + } + } } else if (templateId === '02') { - // 002 模板:底部配色横排 - const allColors = [primaryColor, ...paletteColors.slice(0, 5)]; - const blockSize = w / 6; - const startX = w / 20; - const startY = h - h / 4; - - allColors.forEach((color, i) => { - const x = startX + i * (blockSize + w / 60); + // 02 底部全宽等分矩形: 色值内嵌居中, 名称左上角 + const colors = pick(6); + const blockH = q(10); + const blockW = w / colors.length; + const blockY = h - blockH; + + colors.forEach((color, i) => { + const text = color.toUpperCase(); + const x = i * blockW; + const size = fitFont(text, '', blockW - q(1.2), q(0.5), q(2.5)); ctx.fillStyle = color; - ctx.beginPath(); - ctx.roundRect(x, startY, blockSize, blockSize, 8); - ctx.fill(); - ctx.fillStyle = textColorFor(color); - ctx.font = `${w / 38}px sans-serif`; - ctx.textAlign = 'center'; - ctx.fillText(color, x + blockSize / 2, startY + blockSize / 2 + w / 76); + ctx.fillRect(x, blockY, blockW, blockH); + if (size > 0) { + ctx.fillStyle = '#fff'; + ctx.shadowColor = 'rgba(0,0,0,0.3)'; + ctx.shadowBlur = q(0.2); + ctx.font = `${size}px sans-serif`; + ctx.textAlign = 'center'; + label(text, x + blockW / 2, blockY + blockH / 2 + size / 3); + shadowOff(); + } }); - // 签名 - ctx.fillStyle = dimColor(primaryColor, 0.6); - ctx.font = `${w / 35}px sans-serif`; - ctx.textAlign = 'left'; - ctx.fillText(name || '配色方案', startX, startY + blockSize + w / 20); + if (withText) { + const nameText = name || '配色方案'; + const nameSize = fitFont(nameText, 'bold', w - q(8), q(1.4), q(2.5)); + shadowOn(); + ctx.fillStyle = '#fff'; + if (nameSize > 0) { + ctx.font = `bold ${nameSize}px sans-serif`; + ctx.textAlign = 'left'; + ctx.fillText(nameText, q(4), q(4) + nameSize); + } + shadowOff(); + } } else if (templateId === '03') { - // 003 模板:左上圆环 + 右下标题 - const circleR = w / 12; - const allColors = [primaryColor, ...paletteColors.slice(0, 2)]; - allColors.forEach((color, i) => { - const y = w / 18 + i * (circleR * 2 + w / 20); + // 03 左上竖排圆形: 色值内嵌圆中, 名称在左下角 + // 圆组的顶部距离与名称的底部距离取同一个值, 上下对称 + const colors = pick(3); + const gap = q(3); + const d = q(9.6); + const step = d + gap; + const margin = q(6); + const cx = q(4) + d / 2; + const hexSize = q(2); + + colors.forEach((color, i) => { + const cy = margin + d / 2 + i * step; ctx.beginPath(); - ctx.arc(w / 20 + circleR, y + circleR, circleR, 0, Math.PI * 2); + ctx.arc(cx, cy, d / 2, 0, Math.PI * 2); ctx.fillStyle = color; ctx.fill(); - ctx.strokeStyle = textCol; - ctx.lineWidth = w / 250; - ctx.stroke(); - ctx.fillStyle = textColorFor(color); - ctx.font = `${w / 38}px sans-serif`; - ctx.textAlign = 'center'; - ctx.fillText(color, w / 20 + circleR, y + circleR + w / 76); + // 圆内一行, 宽度按圆心处的弦长收一点余量 + const text = color.toUpperCase(); + const size = fitFont(text, '', d * 0.8, q(0.6), hexSize); + if (size > 0) { + ctx.fillStyle = hexOn(color); + ctx.font = `${size}px sans-serif`; + ctx.textAlign = 'center'; + label(text, cx, cy + size / 3); + } }); - // 标题 - ctx.fillStyle = textCol; - ctx.font = `bold ${w / 10}px serif`; - ctx.textAlign = 'left'; - ctx.fillText(name || '配色方案', w / 20, h - h / 4.8); - - // ctx.fillStyle = dimColor(primaryColor, 0.5); - // ctx.font = `${w / 32}px sans-serif`; - // ctx.fillText('色卡', w / 20, h - h / 4.8 + w / 7.5); + if (withText) { + const nameText = name || '配色方案'; + const nameSize = fitFont(nameText, 'bold', w - q(8), q(1.8), q(3)); + if (nameSize > 0) { + shadowOn(); + ctx.fillStyle = '#fff'; + ctx.font = `bold ${nameSize}px sans-serif`; + ctx.textAlign = 'left'; + ctx.fillText(nameText, q(4), h - margin); + shadowOff(); + } + } } else if (templateId === '04') { - // 004 模板:底部横条配色 - const barH = h * 0.18; - const barY = h - barH - h * 0.05; - const barX = w * 0.1; - const barW = w * 0.8; - const allColors = [primaryColor, ...paletteColors.slice(0, 5)]; - const colW = barW / allColors.length; - - allColors.forEach((color, i) => { - ctx.fillStyle = color; + // 04 白底卡片: 内容整体内缩留出白边, 上 3/4 图片, 下方名称居左、色块横排 + const inset = q(6); + const contentW = w - inset * 2; + const contentH = h - inset * 2; + const imageH = contentH * 0.75; + const bottom = inset + contentH; + const colors = pick(5); + const gap = q(1.5); + const d = q(12); + const step = d + gap; + const hexSize = q(2.2); + const gapLabel = q(1); + + const zoneLeft = inset; + const zoneRight = inset + contentW; + + const nameText = name || '配色方案'; + const nameSize = fitFont(nameText, 'bold', zoneRight - zoneLeft, q(2), q(3.6)); + + // 名称行 + 色块行整体在图片下缘与内容区下缘之间居中 + const gapV = q(3); + const blockH = nameSize + gapV + d + gapLabel + hexSize; + const top = inset + imageH + Math.max(q(2), (bottom - inset - imageH - blockH) / 2); + + if (withText && nameSize > 0) { + ctx.fillStyle = '#212121'; + ctx.font = `bold ${nameSize}px sans-serif`; + ctx.textAlign = 'left'; + ctx.fillText(nameText, zoneLeft, top + nameSize); + } + + const cy = top + nameSize + gapV + d / 2; + const totalW = colors.length * step - gap; + const startX = (zoneRight - zoneLeft > totalW + ? zoneLeft + (zoneRight - zoneLeft - totalW) / 2 + : zoneLeft) + d / 2; + + colors.forEach((color, i) => { + const cx = startX + i * step; ctx.beginPath(); - ctx.roundRect(barX + i * colW + 2, barY, colW - 4, barH, 8); + ctx.arc(cx, cy, d / 2, 0, Math.PI * 2); + ctx.fillStyle = color; ctx.fill(); - ctx.fillStyle = textColorFor(color); - ctx.font = `${w / 30}px sans-serif`; - ctx.textAlign = 'center'; - ctx.fillText(color, barX + i * colW + colW / 2, barY + barH / 2 + w / 60); + const text = color.toUpperCase(); + const maxW = Math.min(step * 1.6, (zoneRight - cx) * 2); + const size = fitFont(text, '', maxW, q(0.6), hexSize); + if (size > 0) { + ctx.fillStyle = '#666'; + ctx.font = `${size}px sans-serif`; + ctx.textAlign = 'center'; + label(text, cx, cy + d / 2 + gapLabel + size * 0.8); + } }); - - // 标题 - ctx.fillStyle = textCol; - ctx.font = `bold ${w / 8}px serif`; - ctx.textAlign = 'left'; - ctx.fillText(name || '配色方案', w / 20, barY - h * 0.05); } } @@ -507,27 +568,27 @@ interface ColorCardDialogProps { bgImage: string; primaryColor: string; paletteColors: string[]; - showMessage: (msg: string) => void; } -function ColorCardDialog({ open, onClose, bgImage, primaryColor, paletteColors, showMessage }: ColorCardDialogProps) { +function ColorCardDialog({ open, onClose, bgImage, primaryColor, paletteColors }: ColorCardDialogProps) { const [previewUrl, setPreviewUrl] = useState(''); - const [generating, setGenerating] = useState(false); const [description, setDescription] = useState(''); - const [descriptionDraft, setDescriptionDraft] = useState(''); - const [scene, setScene] = useState(null); const [templateId, setTemplateId] = useState('01'); - const [rerender, setRerender] = useState(false); const canvasRef = useRef(null); const bgImgRef = useRef(null); + const drawSeq = useRef(0); // AI 名称生成 - const [aiName, { loading: aiLoading, error: aiError, generateName }] = useGenerateName( + const { name: aiName, status: nameStatus, error: nameError, generateName } = useGenerateName( primaryColor, '文艺优雅', 4, open ); - // 色卡名称: AI 生成的名称优先,用户可手动修改 - const cardName = description || aiName || '配色方案'; + // 色卡名称: 用户输入的描述优先, 其次 AI 名称, 都还没有时留空 + const cardName = description || aiName; + + // 名字没落定之前不画图 —— 否则会先用占位名画一版, + // 用户在这期间导出拿到的是占位名色卡 + const nameSettled = nameStatus === 'done' || nameStatus === 'error'; // 预加载背景图 useEffect(() => { @@ -539,69 +600,64 @@ function ColorCardDialog({ open, onClose, bgImage, primaryColor, paletteColors, } }, [bgImage]); - // 生成色卡预览 - const generatePreview = useCallback(() => { - setGenerating(true); - setTimeout(() => { - const canvas = canvasRef.current; - if (!canvas) { setGenerating(false); return; } - - renderColorCard(canvas, { - bgImage: bgImgRef.current, - primaryColor, - paletteColors, - name: cardName, - templateId, - }); - - setPreviewUrl(canvas.toDataURL('image/png')); - setGenerating(false); - }, 500); + // 画一版到 canvas 并刷新预览。withText=false 先只出色块占位, + // 等 AI 名称落定后再画带名称的完整版 + const drawCard = useCallback((withText: boolean) => { + const canvas = canvasRef.current; + if (!canvas) return; + renderColorCard(canvas, { + bgImage: bgImgRef.current, + primaryColor, + paletteColors, + name: withText ? cardName : '', + templateId, + withText, + }); + setPreviewUrl(canvas.toDataURL('image/png')); }, [primaryColor, paletteColors, cardName, templateId]); - // 打开时自动生成 + // 重绘的唯一入口: 打开 / 模板 / 描述 / 名称是否落定 变化都会走到这里。 + // 不在这里 setPreviewUrl(''), 否则每敲一个字都会闪一次 loading。 useEffect(() => { - if (open) { - generatePreview(); - } else { + if (!open) { setPreviewUrl(''); - setTemplateId('01'); - setDescription(''); - setDescriptionDraft(''); - setScene(null); + return; } - }, [open]); - // 模板/AI名称变化时重新生成 - useEffect(() => { - if (open) { - generatePreview(); - } - }, [templateId, aiName]); + const seq = ++drawSeq.current; + const timer = setTimeout(() => { + if (seq !== drawSeq.current) return; + drawCard(nameSettled); + }, 300); - // 用户手动修改描述时重新生成 - useEffect(() => { - if (open && description !== undefined) { - generatePreview(); - } - }, [description]); + return () => { clearTimeout(timer); }; + }, [open, nameSettled, drawCard]); - // 切换模板 - const handleTemplateChange = (id: string) => { - if (id !== templateId) { - setTemplateId(id); - setRerender(true); - } - }; + // 关闭时复位 + useEffect(() => { + if (open) return; + setPreviewUrl(''); + setDescription(''); + setTemplateId('01'); + }, [open]); - // 导出色卡 + // 导出色卡: 按当前参数重画一版带名称的, 不复用可能还是占位版的预览 const handleExport = () => { const canvas = canvasRef.current; - if (!canvas) return; + if (!canvas || !nameSettled) return; + renderColorCard(canvas, { + bgImage: bgImgRef.current, + primaryColor, + paletteColors, + name: cardName || '配色方案', + templateId, + }); const dataUrl = canvas.toDataURL('image/png'); if (window.services?.saveColorCard) { - const buffer = Uint8Array.from(atob(dataUrl.split(',')[1]), c => c.charCodeAt(0)); - window.services.saveColorCard(Array.from(buffer)); + const binary = atob(dataUrl.split(',')[1]); + const buffer = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) buffer[i] = binary.charCodeAt(i); + window.services.saveColorCard(buffer.buffer); } else { const a = document.createElement('a'); a.href = dataUrl; @@ -610,14 +666,12 @@ function ColorCardDialog({ open, onClose, bgImage, primaryColor, paletteColors, } }; - // 预览图尺寸计算 - const previewSize = useMemo((): [string, string] => { + // 预览尺寸交给 flex 算: 预览区是 flex 列, 由它自己的高宽反推出"能占多少", + // 不用 vw 换算 —— 换不换算得出准确值, 也就不用担心描述框/右侧面板把它挤小 + const previewRatio = useMemo((): number => { const template = TEMPLATES.find(t => t.id === templateId) || TEMPLATES[0]; const [tw, th] = getTemplateSize(template); - if (tw === th) return ['50vw', '50vw']; - const ratio = tw / th; - if (ratio > 1) return ['50vw', `${50 / ratio}vw`]; - return [`${50 * ratio}vw`, '50vw']; + return tw / th; }, [templateId]); return ( @@ -634,59 +688,50 @@ function ColorCardDialog({ open, onClose, bgImage, primaryColor, paletteColors,
{/* 中间区域:色卡预览 + 描述输入 */} - - {/* 色卡预览图 */} - - {previewUrl && !generating ? ( + + {/* 色卡预览: 撑满剩余空间, 宽高出多少就缩多少, 不用 vw 硬算 */} + + {previewUrl ? ( preview ) : ( - + + {!nameSettled && AI 正在起名} )} - {/* 描述输入 + 画面选择 + 重新生成 */} - + {/* 描述输入 + 重新生成 */} + setDescriptionDraft(e.target.value)} - onBlur={() => { if (descriptionDraft !== description) setDescription(descriptionDraft); }} + placeholder={aiName || '描述画面主体,可调整色卡名称'} + value={description} + onChange={(e) => setDescription(e.target.value)} variant="standard" fullWidth sx={{ '& .MuiInput-root': { fontSize: 14, paddingRight: 0 } }} InputProps={{ - endAdornment: descriptionDraft ? ( - { setDescription(''); setDescriptionDraft(''); }} sx={{ p: 0.3 }}> + endAdornment: description ? ( + setDescription('')} sx={{ p: 0.3 }}> ) : null, }} /> - + {nameError ? ( + {nameError} + ) : } { - // 重新调用 AI 生成名称(排除当前名称) - const key = primaryColor + '文艺优雅' + 4; - generateName(key, aiName || undefined); - generatePreview(); - }} + disabled={nameStatus === 'loading'} + onClick={() => { generateName(aiName || undefined); }} > @@ -709,7 +754,7 @@ function ColorCardDialog({ open, onClose, bgImage, primaryColor, paletteColors, boxSizing="border-box" overflow="hidden" position="relative" - onClick={() => handleTemplateChange(t.id)} + onClick={() => setTemplateId(t.id)} sx={{ cursor: 'pointer' }} > cover @@ -724,7 +769,7 @@ function ColorCardDialog({ open, onClose, bgImage, primaryColor, paletteColors,