From a34a86288bedb60401135ad61d219d6cc06cb9a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Wed, 24 Dec 2025 18:02:54 +0800 Subject: [PATCH] Refactor font handling and theme config, switch to CodeMirror editor Replaces Monaco editor with CodeMirror in the frontend, removing related dependencies and configuration. Refactors font management to support multiple formats (woff, woff2, ttf, otf) and dynamic font switching, including backend API and frontend theme config UI. Adds gzip compression middleware to backend. Updates theme config to allow font selection and custom font upload, and improves theme preview and color customization UI. Cleans up unused code and improves sidebar and terminal font sizing responsiveness. --- packages/napcat-core/index.ts | 2 +- packages/napcat-webui-backend/index.ts | 54 +- packages/napcat-webui-backend/package.json | 2 + packages/napcat-webui-backend/src/api/File.ts | 4 +- .../src/assets/sw_template.js | 132 ++++ .../napcat-webui-backend/src/helper/config.ts | 28 +- .../napcat-webui-backend/src/types/theme.ts | 14 +- .../src/uploader/webui_font.ts | 37 +- packages/napcat-webui-frontend/package.json | 20 +- .../src/components/code_editor.tsx | 150 +++- .../file_manage/file_edit_modal.tsx | 26 +- .../src/components/onebot/api/debug.tsx | 7 +- .../src/components/onebot/send_modal.tsx | 2 +- .../src/components/page_loading.tsx | 4 +- .../src/components/sidebar/index.tsx | 9 +- .../src/components/xterm.tsx | 20 +- packages/napcat-webui-frontend/src/main.tsx | 19 +- packages/napcat-webui-frontend/src/monaco.ts | 20 - .../src/pages/dashboard/config/theme.tsx | 405 ++++++++--- .../src/pages/dashboard/config/webui.tsx | 39 +- .../src/styles/fonts.css | 12 +- .../src/styles/globals.css | 36 +- .../src/types/server.d.ts | 317 ++++---- .../napcat-webui-frontend/src/utils/theme.ts | 78 ++ packages/napcat-webui-frontend/vite.config.ts | 55 +- pnpm-lock.yaml | 685 +++++++++++++++++- 26 files changed, 1678 insertions(+), 499 deletions(-) create mode 100644 packages/napcat-webui-backend/src/assets/sw_template.js delete mode 100644 packages/napcat-webui-frontend/src/monaco.ts diff --git a/packages/napcat-core/index.ts b/packages/napcat-core/index.ts index 57a47c2c..765491fc 100644 --- a/packages/napcat-core/index.ts +++ b/packages/napcat-core/index.ts @@ -126,7 +126,7 @@ export class NapCatCore { container.bind(TypedEventEmitter).toConstantValue(this.event); ReceiverServiceRegistry.forEach((ServiceClass, serviceName) => { container.bind(ServiceClass).toSelf(); - console.log(`Registering service handler for: ${serviceName}`); + //console.log(`Registering service handler for: ${serviceName}`); this.context.packetHandler.onCmd(serviceName, ({ seq, hex_data }) => { const serviceInstance = container.get(ServiceClass); return serviceInstance.handler(seq, hex_data); diff --git a/packages/napcat-webui-backend/index.ts b/packages/napcat-webui-backend/index.ts index f0a4e908..d39efae8 100644 --- a/packages/napcat-webui-backend/index.ts +++ b/packages/napcat-webui-backend/index.ts @@ -23,6 +23,13 @@ import { ILogWrapper } from 'napcat-common/src/log-interface'; import { ISubscription } from 'napcat-common/src/subscription-interface'; import { IStatusHelperSubscription } from '@/napcat-common/src/status-interface'; import { handleDebugWebSocket } from '@/napcat-webui-backend/src/api/Debug'; +import compression from 'compression'; +import { napCatVersion } from 'napcat-common/src/version'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); // 实例化Express const app = express(); /** @@ -143,18 +150,31 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra // ------------注册中间件------------ // 使用express的json中间件 app.use(express.json()); + // 启用gzip压缩(对所有响应启用,阈值1KB) + app.use(compression({ + level: 6, // 压缩级别 1-9,6 是性能和压缩率的平衡点 + threshold: 1024, // 只压缩大于 1KB 的响应 + filter: (req, res) => { + // 不压缩 SSE 和 WebSocket 升级请求 + if (req.headers['accept'] === 'text/event-stream') { + return false; + } + // 使用默认过滤器 + return compression.filter(req, res); + }, + })); // CORS中间件 // TODO: app.use(cors); - // 如果是webui字体文件,挂载字体文件 - app.use('/webui/fonts/AaCute.woff', async (_req, res, next) => { - const isFontExist = await WebUiConfig.CheckWebUIFontExist(); - if (isFontExist) { - res.sendFile(WebUiConfig.GetWebUIFontPath()); + // 自定义字体文件路由 - 返回用户上传的字体文件 + app.use('/webui/fonts/CustomFont.woff', async (_req, res) => { + const fontPath = await WebUiConfig.GetWebUIFontPath(); + if (fontPath) { + res.sendFile(fontPath); } else { - next(); + res.status(404).send('Custom font not found'); } }); @@ -176,6 +196,28 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra res.send(css); }); + // 动态生成 sw.js + app.get('/webui/sw.js', async (_req, res) => { + try { + // 读取模板文件 + const templatePath = resolve(__dirname, 'src/assets/sw_template.js'); + let swContent = readFileSync(templatePath, 'utf-8'); + + // 替换版本号 + // 使用 napCatVersion,如果为 alpha 则尝试加上时间戳或其他标识以避免缓存冲突,或者直接使用 + // 用户要求控制 sw.js 版本,napCatVersion 是核心控制点 + swContent = swContent.replace('{{VERSION}}', napCatVersion); + + res.header('Content-Type', 'application/javascript'); + res.header('Service-Worker-Allowed', '/webui/'); + res.header('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.send(swContent); + } catch (error) { + console.error('[NapCat] [WebUi] Error generating sw.js', error); + res.status(500).send('Error generating service worker'); + } + }); + // ------------中间件结束------------ // ------------挂载路由------------ diff --git a/packages/napcat-webui-backend/package.json b/packages/napcat-webui-backend/package.json index dbdc4fa3..b3b140b7 100644 --- a/packages/napcat-webui-backend/package.json +++ b/packages/napcat-webui-backend/package.json @@ -20,6 +20,7 @@ "@sinclair/typebox": "^0.34.38", "ajv": "^8.13.0", "compressing": "^1.10.3", + "compression": "^1.8.1", "express": "^5.0.0", "express-rate-limit": "^7.5.0", "json5": "^2.2.3", @@ -29,6 +30,7 @@ "ws": "^8.18.3" }, "devDependencies": { + "@types/compression": "^1.8.1", "@types/express": "^5.0.0", "@types/multer": "^1.4.12", "@types/node": "^22.0.1", diff --git a/packages/napcat-webui-backend/src/api/File.ts b/packages/napcat-webui-backend/src/api/File.ts index a3d8520b..42af94ee 100644 --- a/packages/napcat-webui-backend/src/api/File.ts +++ b/packages/napcat-webui-backend/src/api/File.ts @@ -640,10 +640,10 @@ export const UploadWebUIFontHandler: RequestHandler = async (req, res) => { // 删除WebUI字体文件处理方法 export const DeleteWebUIFontHandler: RequestHandler = async (_req, res) => { try { - const fontPath = WebUiConfig.GetWebUIFontPath(); + const fontPath = await WebUiConfig.GetWebUIFontPath(); const exists = await WebUiConfig.CheckWebUIFontExist(); - if (!exists) { + if (!exists || !fontPath) { return sendSuccess(res, true); } diff --git a/packages/napcat-webui-backend/src/assets/sw_template.js b/packages/napcat-webui-backend/src/assets/sw_template.js new file mode 100644 index 00000000..c861a9e8 --- /dev/null +++ b/packages/napcat-webui-backend/src/assets/sw_template.js @@ -0,0 +1,132 @@ +const CACHE_NAME = 'napcat-webui-v{{VERSION}}'; +const ASSETS_TO_CACHE = [ + '/webui/' +]; + +// 安装阶段:预缓存核心文件 +self.addEventListener('install', (event) => { + self.skipWaiting(); // 强制立即接管 + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + // 这里的资源如果加载失败不应该阻断 SW 安装 + return cache.addAll(ASSETS_TO_CACHE).catch(err => console.warn('Failed to cache core assets', err)); + }) + ); +}); + +// 激活阶段:清理旧缓存 +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName.startsWith('napcat-webui-') && cacheName !== CACHE_NAME) { + console.log('[SW] Deleting old cache:', cacheName); + return caches.delete(cacheName); + } + }) + ); + }) + ); + self.clients.claim(); // 立即控制所有客户端 +}); + +// 拦截请求 +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url); + + // 1. API 请求:仅网络 (Network Only) + if (url.pathname.startsWith('/api/') || url.pathname.includes('/socket')) { + return; + } + + // 2. 强缓存策略 (Cache First) + // - 外部 QQ 头像 (q1.qlogo.cn) + // - 静态资源 (assets, fonts) + // - 常见静态文件后缀 + const isQLogo = url.hostname === 'q1.qlogo.cn'; + const isCustomFont = url.pathname.includes('CustomFont.woff'); // 用户自定义字体,不强缓存 + const isThemeCss = url.pathname.includes('files/theme.css'); // 主题 CSS,不强缓存 + const isStaticAsset = url.pathname.includes('/webui/assets/') || + url.pathname.includes('/webui/fonts/'); + const isStaticFile = /\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|ico)$/i.test(url.pathname); + + if (!isCustomFont && !isThemeCss && (isQLogo || isStaticAsset || isStaticFile)) { + event.respondWith( + caches.match(event.request).then((response) => { + if (response) { + return response; + } + + // 跨域请求 (qlogo) 需要 mode: 'no-cors' 才能缓存 opaque response, + // 但 fetch(event.request) 默认会继承 request 的 mode。 + // 如果是 img标签发起的请求,通常 mode 是 no-cors 或 cors。 + // 对于 opaque response (status 0), cache API 允许缓存。 + return fetch(event.request).then((response) => { + // 对 qlogo 允许 status 0 (opaque) + // 对其他资源要求 status 200 + const isValidResponse = response && ( + response.status === 200 || + response.type === 'basic' || + (isQLogo && response.type === 'opaque') + ); + + if (!isValidResponse) { + return response; + } + + const responseToCache = response.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseToCache); + }); + return response; + }); + }) + ); + return; + } + + // 3. HTML 页面 / 导航请求 -> 网络优先 (Network First) + if (event.request.mode === 'navigate') { + event.respondWith( + fetch(event.request) + .then((response) => { + const responseToCache = response.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseToCache); + }); + return response; + }) + .catch(() => { + return caches.match(event.request); + }) + ); + return; + } + + // 4. 其他 Same-Origin 请求 -> Stale-While-Revalidate + // 优先返回缓存,同时后台更新缓存,保证下次访问是新的 + if (url.origin === self.location.origin) { + event.respondWith( + caches.match(event.request).then((cachedResponse) => { + const fetchPromise = fetch(event.request).then((networkResponse) => { + if (networkResponse && networkResponse.status === 200) { + const responseToCache = networkResponse.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseToCache); + }); + } + return networkResponse; + }); + // 如果有缓存,返回缓存;否则等待网络 + return cachedResponse || fetchPromise; + }) + ); + return; + } + + // 默认:网络优先 + event.respondWith( + fetch(event.request).catch(() => caches.match(event.request)) + ); +}); diff --git a/packages/napcat-webui-backend/src/helper/config.ts b/packages/napcat-webui-backend/src/helper/config.ts index 527a1671..3faa4285 100644 --- a/packages/napcat-webui-backend/src/helper/config.ts +++ b/packages/napcat-webui-backend/src/helper/config.ts @@ -176,17 +176,35 @@ export class WebUiConfigWrapper { return []; } - // 判断字体是否存在(webui.woff) + // 判断字体是否存在(支持多种格式) async CheckWebUIFontExist (): Promise { - const fontsPath = resolve(webUiPathWrapper.configPath, './fonts'); + const fontPath = await this.GetWebUIFontPath(); + if (!fontPath) return false; return await fs - .access(resolve(fontsPath, './webui.woff'), constants.F_OK) + .access(fontPath, constants.F_OK) .then(() => true) .catch(() => false); } - // 获取webui字体文件路径 - GetWebUIFontPath (): string { + // 获取webui字体文件路径(支持多种格式) + async GetWebUIFontPath (): Promise { + const fontsPath = resolve(webUiPathWrapper.configPath, './fonts'); + const extensions = ['.woff', '.woff2', '.ttf', '.otf']; + for (const ext of extensions) { + const fontPath = resolve(fontsPath, `webui${ext}`); + const exists = await fs + .access(fontPath, constants.F_OK) + .then(() => true) + .catch(() => false); + if (exists) { + return fontPath; + } + } + return null; + } + + // 同步版本,用于 multer 配置 + GetWebUIFontPathSync (): string { return resolve(webUiPathWrapper.configPath, './fonts/webui.woff'); } diff --git a/packages/napcat-webui-backend/src/types/theme.ts b/packages/napcat-webui-backend/src/types/theme.ts index 00d7a2b9..5084cef1 100644 --- a/packages/napcat-webui-backend/src/types/theme.ts +++ b/packages/napcat-webui-backend/src/types/theme.ts @@ -4,9 +4,11 @@ export const themeType = Type.Object( { dark: Type.Record(Type.String(), Type.String()), light: Type.Record(Type.String(), Type.String()), + fontMode: Type.String({ default: 'system' }), }, { default: { + fontMode: 'system', dark: { '--heroui-background': '0 0% 0%', '--heroui-foreground-50': '240 5.88% 10%', @@ -124,11 +126,11 @@ export const themeType = Type.Object( '--heroui-border-width-medium': '2px', '--heroui-border-width-large': '3px', '--heroui-box-shadow-small': - '0px 0px 5px 0px rgba(0, 0, 0, .05), 0px 2px 10px 0px rgba(0, 0, 0, .2), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)', + '0px 0px 5px 0px rgba(0, 0, 0, .05), 0px 2px 10px 0px rgba(0, 0, 0, .2), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)', '--heroui-box-shadow-medium': - '0px 0px 15px 0px rgba(0, 0, 0, .06), 0px 2px 30px 0px rgba(0, 0, 0, .22), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)', + '0px 0px 15px 0px rgba(0, 0, 0, .06), 0px 2px 30px 0px rgba(0, 0, 0, .22), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)', '--heroui-box-shadow-large': - '0px 0px 30px 0px rgba(0, 0, 0, .07), 0px 30px 60px 0px rgba(0, 0, 0, .26), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)', + '0px 0px 30px 0px rgba(0, 0, 0, .07), 0px 30px 60px 0px rgba(0, 0, 0, .26), inset 0px 0px 1px 0px hsla(0, 0%, 100%, .15)', '--heroui-hover-opacity': '.9', }, light: { @@ -248,11 +250,11 @@ export const themeType = Type.Object( '--heroui-border-width-medium': '2px', '--heroui-border-width-large': '3px', '--heroui-box-shadow-small': - '0px 0px 5px 0px rgba(0, 0, 0, .02), 0px 2px 10px 0px rgba(0, 0, 0, .06), 0px 0px 1px 0px rgba(0, 0, 0, .3)', + '0px 0px 5px 0px rgba(0, 0, 0, .02), 0px 2px 10px 0px rgba(0, 0, 0, .06), 0px 0px 1px 0px rgba(0, 0, 0, .3)', '--heroui-box-shadow-medium': - '0px 0px 15px 0px rgba(0, 0, 0, .03), 0px 2px 30px 0px rgba(0, 0, 0, .08), 0px 0px 1px 0px rgba(0, 0, 0, .3)', + '0px 0px 15px 0px rgba(0, 0, 0, .03), 0px 2px 30px 0px rgba(0, 0, 0, .08), 0px 0px 1px 0px rgba(0, 0, 0, .3)', '--heroui-box-shadow-large': - '0px 0px 30px 0px rgba(0, 0, 0, .04), 0px 30px 60px 0px rgba(0, 0, 0, .12), 0px 0px 1px 0px rgba(0, 0, 0, .3)', + '0px 0px 30px 0px rgba(0, 0, 0, .04), 0px 30px 60px 0px rgba(0, 0, 0, .12), 0px 0px 1px 0px rgba(0, 0, 0, .3)', '--heroui-hover-opacity': '.8', }, }, diff --git a/packages/napcat-webui-backend/src/uploader/webui_font.ts b/packages/napcat-webui-backend/src/uploader/webui_font.ts index 9e287aed..635ea941 100644 --- a/packages/napcat-webui-backend/src/uploader/webui_font.ts +++ b/packages/napcat-webui-backend/src/uploader/webui_font.ts @@ -4,30 +4,51 @@ import fs from 'fs'; import type { Request, Response } from 'express'; import { WebUiConfig } from '@/napcat-webui-backend/index'; +// 支持的字体格式 +const SUPPORTED_FONT_EXTENSIONS = ['.woff', '.woff2', '.ttf', '.otf']; + +// 清理旧的字体文件 +const cleanOldFontFiles = (fontsPath: string) => { + for (const ext of SUPPORTED_FONT_EXTENSIONS) { + const fontPath = path.join(fontsPath, `webui${ext}`); + try { + if (fs.existsSync(fontPath)) { + fs.unlinkSync(fontPath); + } + } catch { + // 忽略删除失败 + } + } +}; + export const webUIFontStorage = multer.diskStorage({ destination: (_, __, cb) => { try { - const fontsPath = path.dirname(WebUiConfig.GetWebUIFontPath()); + const fontsPath = path.dirname(WebUiConfig.GetWebUIFontPathSync()); // 确保字体目录存在 fs.mkdirSync(fontsPath, { recursive: true }); + // 清理旧的字体文件 + cleanOldFontFiles(fontsPath); cb(null, fontsPath); } catch (error) { // 确保错误信息被正确传递 cb(new Error(`创建字体目录失败:${(error as Error).message}`), ''); } }, - filename: (_, __, cb) => { - // 统一保存为webui.woff - cb(null, 'webui.woff'); + filename: (_, file, cb) => { + // 保留原始扩展名,统一文件名为 webui + const ext = path.extname(file.originalname).toLowerCase(); + cb(null, `webui${ext}`); }, }); export const webUIFontUpload = multer({ storage: webUIFontStorage, fileFilter: (_, file, cb) => { - // 再次验证文件类型 - if (!file.originalname.toLowerCase().endsWith('.woff')) { - cb(new Error('只支持WOFF格式的字体文件')); + // 验证文件类型 + const ext = path.extname(file.originalname).toLowerCase(); + if (!SUPPORTED_FONT_EXTENSIONS.includes(ext)) { + cb(new Error('只支持 WOFF/WOFF2/TTF/OTF 格式的字体文件')); return; } cb(null, true); @@ -41,8 +62,6 @@ const webUIFontUploader = (req: Request, res: Response) => { return new Promise((resolve, reject) => { webUIFontUpload(req, res, (error) => { if (error) { - // 错误处理 - // sendError(res, error.message, true); return reject(error); } return resolve(true); diff --git a/packages/napcat-webui-frontend/package.json b/packages/napcat-webui-frontend/package.json index b434215d..323d27ef 100644 --- a/packages/napcat-webui-frontend/package.json +++ b/packages/napcat-webui-frontend/package.json @@ -5,13 +5,19 @@ "type": "module", "scripts": { "dev": "vite --host=0.0.0.0", - "build": "tsc && vite build", + "build": "vite build", + "build:full": "tsc && vite build", "fontmin": "node scripts/fontmin.cjs", "typecheck": "tsc --noEmit", "lint": "eslint -c eslint.config.mjs ./src/**/**/*.{ts,tsx} --fix", "preview": "vite preview" }, "dependencies": { + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.39.6", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -47,10 +53,10 @@ "@heroui/theme": "2.4.6", "@heroui/tooltip": "2.2.8", "@monaco-editor/loader": "^1.4.0", - "@monaco-editor/react": "4.7.0-rc.0", "@react-aria/visually-hidden": "^3.8.19", "@reduxjs/toolkit": "^2.5.1", "@uidotdev/usehooks": "^2.4.1", + "@uiw/react-codemirror": "^4.25.4", "@xterm/addon-canvas": "^0.7.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-web-links": "^0.11.0", @@ -60,7 +66,6 @@ "clsx": "^2.1.1", "crypto-js": "^4.2.0", "event-source-polyfill": "^1.0.31", - "monaco-editor": "^0.52.2", "motion": "^12.0.6", "path-browserify": "^1.0.1", "qface": "^1.4.1", @@ -104,15 +109,18 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "5.2.3", "eslint-plugin-unused-imports": "^4.1.4", + "fontmin": "^0.9.9", + "glob": "^10.3.10", "postcss": "^8.5.1", "prettier": "^3.4.2", + "sharp": "^0.34.5", "typescript": "^5.7.3", "vite": "^6.0.5", + "vite-plugin-compression": "^0.5.1", "vite-plugin-font": "^5.1.2", + "vite-plugin-image-optimizer": "^2.0.3", "vite-plugin-static-copy": "^2.2.0", - "vite-tsconfig-paths": "^5.1.4", - "fontmin": "^0.9.9", - "glob": "^10.3.10" + "vite-tsconfig-paths": "^5.1.4" }, "overrides": { "ahooks": { diff --git a/packages/napcat-webui-frontend/src/components/code_editor.tsx b/packages/napcat-webui-frontend/src/components/code_editor.tsx index 8fc60c33..4ad5227c 100644 --- a/packages/napcat-webui-frontend/src/components/code_editor.tsx +++ b/packages/napcat-webui-frontend/src/components/code_editor.tsx @@ -1,46 +1,126 @@ -import Editor, { OnMount, loader } from '@monaco-editor/react'; - -import React from 'react'; - +import React, { useImperativeHandle, useEffect, useState } from 'react'; +import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror'; +import { json } from '@codemirror/lang-json'; +import { oneDark } from '@codemirror/theme-one-dark'; import { useTheme } from '@/hooks/use-theme'; +import { EditorView } from '@codemirror/view'; +import clsx from 'clsx'; -import monaco from '@/monaco'; +const getLanguageExtension = (lang?: string) => { + switch (lang) { + case 'json': return json(); + default: return []; + } +}; -loader.config({ - monaco, -}); - -export interface CodeEditorProps extends React.ComponentProps { - test?: string; +export interface CodeEditorProps { + value?: string; + defaultValue?: string; + language?: string; + defaultLanguage?: string; + onChange?: (value: string | undefined) => void; + height?: string; + options?: any; + onMount?: any; } -export type CodeEditorRef = monaco.editor.IStandaloneCodeEditor; +export interface CodeEditorRef { + getValue: () => string; +} -const CodeEditor = React.forwardRef( - (props, ref) => { - const { isDark } = useTheme(); +const CodeEditor = React.forwardRef((props, ref) => { + const { isDark } = useTheme(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [val, setVal] = useState(props.value || props.defaultValue || ''); + const internalRef = React.useRef(null); - const handleEditorDidMount: OnMount = (editor, monaco) => { - if (ref) { - if (typeof ref === 'function') { - ref(editor); - } else { - (ref as React.RefObject).current = editor; - } - } - if (props.onMount) { - props.onMount(editor, monaco); - } - }; + useEffect(() => { + if (props.value !== undefined) { + setVal(props.value); + } + }, [props.value]); - return ( - ({ + getValue: () => { + // Prefer getting dynamic value from view, fallback to state + return internalRef.current?.view?.state.doc.toString() || val; + } + })); + + const customTheme = EditorView.theme({ + "&": { + fontSize: "14px", + height: "100% !important", + }, + ".cm-scroller": { + fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace", + lineHeight: "1.6", + overflow: "auto !important", + height: "100% !important", + }, + ".cm-gutters": { + backgroundColor: "transparent", + borderRight: "none", + color: isDark ? "#ffffff50" : "#00000040", + }, + ".cm-gutterElement": { + paddingLeft: "12px", + paddingRight: "12px", + }, + ".cm-activeLineGutter": { + backgroundColor: "transparent", + color: isDark ? "#fff" : "#000", + }, + ".cm-content": { + caretColor: isDark ? "#fff" : "#000", + paddingTop: "12px", + paddingBottom: "12px", + }, + ".cm-activeLine": { + backgroundColor: isDark ? "#ffffff10" : "#00000008", + }, + ".cm-selectionMatch": { + backgroundColor: isDark ? "#ffffff20" : "#00000010", + }, + }); + + const extensions = [ + customTheme, + getLanguageExtension(props.language || props.defaultLanguage), + props.options?.wordWrap === 'on' ? EditorView.lineWrapping : [], + props.options?.readOnly ? EditorView.editable.of(false) : [], + ].flat(); + + return ( +
+ { + setVal(value); + props.onChange?.(value); + }} + readOnly={props.options?.readOnly} + basicSetup={{ + lineNumbers: props.options?.lineNumbers !== 'off', + foldGutter: props.options?.folding !== false, + highlightActiveLine: props.options?.renderLineHighlight !== 'none', + }} /> - ); - } -); +
+ ); +}); export default CodeEditor; diff --git a/packages/napcat-webui-frontend/src/components/file_manage/file_edit_modal.tsx b/packages/napcat-webui-frontend/src/components/file_manage/file_edit_modal.tsx index 309d8f78..00ea23aa 100644 --- a/packages/napcat-webui-frontend/src/components/file_manage/file_edit_modal.tsx +++ b/packages/napcat-webui-frontend/src/components/file_manage/file_edit_modal.tsx @@ -11,11 +11,11 @@ import { import CodeEditor from '@/components/code_editor'; interface FileEditModalProps { - isOpen: boolean - file: { path: string; content: string } | null - onClose: () => void - onSave: () => void - onContentChange: (newContent?: string) => void + isOpen: boolean; + file: { path: string; content: string; } | null; + onClose: () => void; + onSave: () => void; + onContentChange: (newContent?: string) => void; } export default function FileEditModal ({ @@ -65,12 +65,20 @@ export default function FileEditModal ({ return ( - + 编辑文件 {file?.path} +
+ 按 Ctrl/Cmd + S 保存 +
- -
+ +
{ + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault(); + onSave(); + } + }}>
- + diff --git a/packages/napcat-webui-frontend/src/components/onebot/api/debug.tsx b/packages/napcat-webui-frontend/src/components/onebot/api/debug.tsx index 3bb89b05..0125ad7b 100644 --- a/packages/napcat-webui-frontend/src/components/onebot/api/debug.tsx +++ b/packages/napcat-webui-frontend/src/components/onebot/api/debug.tsx @@ -274,8 +274,9 @@ const OneBotApiDebug: React.FC = (props) => {
{activeTab === 'request' ? ( = (props) => { {/* Response Content - Code Editor */} {responseExpanded && ( -
+
= (props) => { 构造请求 -
+
= ({ loading }) => { return (
= (props) => { const { open, items, onClose } = props; const { toggleTheme, isDark } = useTheme(); const { revokeAuth } = useAuth(); - const [b64img] = useLocalStorage(key.backgroundImage, ''); const dialog = useDialog(); const onRevokeAuth = () => { dialog.confirm({ @@ -50,9 +47,9 @@ const SideBar: React.FC = (props) => { ((props, ref) => { const { theme } = useTheme(); useEffect(() => { // 根据屏幕宽度决定字体大小,手机端使用更小的字体 - const isMobile = window.innerWidth < 768; - const fontSize = isMobile ? 11 : 14; + const width = window.innerWidth; + // 按屏幕宽度自适应字体大小 + let fontSize = 16; + if (width < 400) { + fontSize = 4; + } else if (width < 600) { + fontSize = 5; + } else if (width < 900) { + fontSize = 6; + } else if (width < 1280) { + fontSize = 12; + } // ≥1280: 16 const terminal = new Terminal({ allowTransparency: true, @@ -60,10 +70,8 @@ const XTerm = forwardRef((props, ref) => { terminal.loadAddon(fitAddon); terminal.open(domRef.current!); - // 只在非手机端使用 Canvas 渲染器,手机端使用默认 DOM 渲染器以避免渲染问题 - if (!isMobile) { - terminal.loadAddon(new CanvasAddon()); - } + // 所有端都使用 Canvas 渲染器(包括手机端) + terminal.loadAddon(new CanvasAddon()); terminal.onData((data) => { if (onInput) { onInput(data); diff --git a/packages/napcat-webui-frontend/src/main.tsx b/packages/napcat-webui-frontend/src/main.tsx index 4af74990..cc0b21d7 100644 --- a/packages/napcat-webui-frontend/src/main.tsx +++ b/packages/napcat-webui-frontend/src/main.tsx @@ -8,7 +8,7 @@ import '@/styles/globals.css'; import key from './const/key'; import WebUIManager from './controllers/webui_manager'; -import { loadTheme } from './utils/theme'; +import { initFont, loadTheme } from './utils/theme'; WebUIManager.checkWebUiLogined(); @@ -24,6 +24,7 @@ if (theme && !theme.startsWith('"')) { } loadTheme(); +initFont(); ReactDOM.createRoot(document.getElementById('root')!).render( // @@ -34,3 +35,19 @@ ReactDOM.createRoot(document.getElementById('root')!).render( // ); + +if (!import.meta.env.DEV) { + if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + const baseUrl = import.meta.env.BASE_URL; + const swUrl = `${baseUrl}sw.js`; + navigator.serviceWorker.register(swUrl, { scope: baseUrl }) + .then((registration) => { + console.log('SW registered: ', registration); + }) + .catch((registrationError) => { + console.log('SW registration failed: ', registrationError); + }); + }); + } +} diff --git a/packages/napcat-webui-frontend/src/monaco.ts b/packages/napcat-webui-frontend/src/monaco.ts deleted file mode 100644 index 97506814..00000000 --- a/packages/napcat-webui-frontend/src/monaco.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as monaco from 'monaco-editor'; -import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; -import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; - -// Monaco Environment - Only load JSON worker for performance -// Other languages will use basic editor worker (no IntelliSense, but syntax highlighting works) -self.MonacoEnvironment = { - getWorker (_: unknown, label: string) { - if (label === 'json') { - // eslint-disable-next-line new-cap - return new jsonWorker(); - } - // For all other languages, use the basic editor worker - // This provides syntax highlighting but no language-specific IntelliSense - // eslint-disable-next-line new-cap - return new editorWorker(); - }, -}; - -export default monaco; diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/config/theme.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/config/theme.tsx index dd9f6e68..cc221d32 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/config/theme.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/config/theme.tsx @@ -1,28 +1,34 @@ import { Accordion, AccordionItem } from '@heroui/accordion'; +import { Button } from '@heroui/button'; import { Card, CardBody, CardHeader } from '@heroui/card'; +import { Select, SelectItem } from '@heroui/select'; +import { Chip } from '@heroui/chip'; import { useRequest } from 'ahooks'; import clsx from 'clsx'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState, useMemo, useCallback } from 'react'; import { Controller, useForm, useWatch } from 'react-hook-form'; import toast from 'react-hot-toast'; -import { FaUserAstronaut } from 'react-icons/fa'; +import { FaFont, FaUserAstronaut, FaCheck } from 'react-icons/fa'; import { FaPaintbrush } from 'react-icons/fa6'; import { IoIosColorPalette } from 'react-icons/io'; import { MdDarkMode, MdLightMode } from 'react-icons/md'; +import { IoMdRefresh } from 'react-icons/io'; import themes from '@/const/themes'; import ColorPicker from '@/components/ColorPicker'; -import SaveButtons from '@/components/button/save_buttons'; +import FileInput from '@/components/input/file_input'; import PageLoading from '@/components/page_loading'; -import { colorKeys, generateTheme, loadTheme } from '@/utils/theme'; +import FileManager from '@/controllers/file_manager'; +import { applyFont, colorKeys, generateTheme, loadTheme, updateFontCache } from '@/utils/theme'; import WebUIManager from '@/controllers/webui_manager'; export type PreviewThemeCardProps = { theme: ThemeInfo; onPreview: () => void; + isSelected?: boolean; }; const values = [ @@ -47,7 +53,7 @@ const colors = [ 'default', ]; -function PreviewThemeCard ({ theme, onPreview }: PreviewThemeCardProps) { +function PreviewThemeCard ({ theme, onPreview, isSelected }: PreviewThemeCardProps) { const style = document.createElement('style'); style.innerHTML = generateTheme(theme.theme, theme.name); const cardRef = useRef(null); @@ -64,8 +70,19 @@ function PreviewThemeCard ({ theme, onPreview }: PreviewThemeCardProps) { radius='sm' isPressable onPress={onPreview} - className={clsx('text-primary bg-primary-50', theme.name)} + className={clsx( + 'text-primary bg-primary-50 relative transition-all', + theme.name, + isSelected && 'ring-2 ring-primary ring-offset-2' + )} > + {isSelected && ( +
+ + + +
+ )}
{theme.name} @@ -100,6 +117,29 @@ function PreviewThemeCard ({ theme, onPreview }: PreviewThemeCardProps) { ); } +// 比较两个主题配置是否相同(不比较 fontMode) +const isThemeColorsEqual = (a: ThemeConfig, b: ThemeConfig): boolean => { + if (!a || !b) return false; + const aKeys = [...Object.keys(a.light || {}), ...Object.keys(a.dark || {})]; + const bKeys = [...Object.keys(b.light || {}), ...Object.keys(b.dark || {})]; + if (aKeys.length !== bKeys.length) return false; + + for (const key of Object.keys(a.light || {})) { + if (a.light?.[key as keyof ThemeConfigItem] !== b.light?.[key as keyof ThemeConfigItem]) return false; + } + for (const key of Object.keys(a.dark || {})) { + if (a.dark?.[key as keyof ThemeConfigItem] !== b.dark?.[key as keyof ThemeConfigItem]) return false; + } + return true; +}; + +// 字体模式显示名称映射 +const fontModeNames: Record = { + 'aacute': 'Aa 偷吃可爱长大的', + 'system': '系统默认', + 'custom': '自定义字体', +}; + const ThemeConfigCard = () => { const { data, loading, error, refreshAsync } = useRequest( WebUIManager.getThemeConfig @@ -116,12 +156,17 @@ const ThemeConfigCard = () => { theme: { dark: {}, light: {}, + fontMode: 'aacute', }, }, }); + const [dataLoaded, setDataLoaded] = useState(false); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + // 使用 useRef 存储 style 标签引用 const styleTagRef = useRef(null); + const originalDataRef = useRef(null); // 在组件挂载时创建 style 标签,并在卸载时清理 useEffect(() => { @@ -137,13 +182,45 @@ const ThemeConfigCard = () => { const theme = useWatch({ control, name: 'theme' }); - const reset = () => { - if (data) setOnebotValue('theme', data); - }; + // 检测是否有未保存的更改 + useEffect(() => { + if (originalDataRef.current && dataLoaded) { + const colorsChanged = !isThemeColorsEqual(theme, originalDataRef.current); + const fontChanged = theme.fontMode !== originalDataRef.current.fontMode; + setHasUnsavedChanges(colorsChanged || fontChanged); + } + }, [theme, dataLoaded]); - const onSubmit = handleOnebotSubmit(async (data) => { + const reset = useCallback(() => { + if (data) { + setOnebotValue('theme', data); + originalDataRef.current = data; + // 应用已保存的字体设置 + if (data.fontMode) { + applyFont(data.fontMode); + } + } + setDataLoaded(true); + setHasUnsavedChanges(false); + }, [data, setOnebotValue]); + + // 实时应用字体预设(预览) + useEffect(() => { + if (dataLoaded && theme.fontMode) { + applyFont(theme.fontMode); + } + }, [theme.fontMode, dataLoaded]); + + const onSubmit = handleOnebotSubmit(async (formData) => { try { - await WebUIManager.setThemeConfig(data.theme); + await WebUIManager.setThemeConfig(formData.theme); + // 更新原始数据引用 + originalDataRef.current = formData.theme; + // 更新字体缓存 + if (formData.theme.fontMode) { + updateFontCache(formData.theme.fontMode); + } + setHasUnsavedChanges(false); toast.success('保存成功'); loadTheme(); } catch (error) { @@ -164,7 +241,7 @@ const ThemeConfigCard = () => { useEffect(() => { reset(); - }, [data]); + }, [data, reset]); useEffect(() => { if (theme && styleTagRef.current) { @@ -173,6 +250,25 @@ const ThemeConfigCard = () => { } }, [theme]); + // 找到当前选中的主题(预览中的) + const selectedThemeName = useMemo(() => { + return themes.find(t => isThemeColorsEqual(t.theme, theme))?.name; + }, [theme]); + + // 找到已保存的主题名称 + const savedThemeName = useMemo(() => { + if (!originalDataRef.current) return null; + return themes.find(t => isThemeColorsEqual(t.theme, originalDataRef.current!))?.name || '自定义'; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dataLoaded, hasUnsavedChanges]); + + // 已保存的字体模式显示名称 + const savedFontModeDisplayName = useMemo(() => { + const mode = originalDataRef.current?.fontMode || 'aacute'; + return fontModeNames[mode] || mode; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dataLoaded, hasUnsavedChanges]); + if (loading) return ; if (error) { @@ -185,96 +281,209 @@ const ThemeConfigCard = () => { <> 主题配置 - NapCat WebUI - -
实时预览,记得保存!
- - } - > -
- {themes.map((theme) => ( - { - setOnebotValue('theme', theme.theme); - }} - /> - ))} + {/* 顶部操作栏 */} +
+
+
+
+ 当前主题: + + {savedThemeName || '加载中...'} + +
+
+ 字体: + + {savedFontModeDisplayName} + +
+ {hasUnsavedChanges && ( + + 有未保存的更改 + + )}
- +
+ + + +
+
+
- } - > -
- {(['dark', 'light'] as const).map((mode) => ( -
-

- {mode === 'dark' - ? ( - - ) - : ( - - )} - {mode === 'dark' ? '夜间模式主题' : '白昼模式主题'} -

- {colorKeys.map((key) => ( -
+ + } + > +
+ ( + + )} + /> +
+
+ 上传自定义字体(仅在选择"自定义字体"时生效) +
+ { + try { + await FileManager.uploadWebUIFont(file); + toast.success('上传成功,即将刷新页面'); + setTimeout(() => { + window.location.reload(); + }, 1000); + } catch (error) { + toast.error('上传失败: ' + (error as Error).message); + } + }} + onDelete={async () => { + try { + await FileManager.deleteWebUIFont(); + toast.success('删除成功,即将刷新页面'); + setTimeout(() => { + window.location.reload(); + }, 1000); + } catch (error) { + toast.error('删除失败: ' + (error as Error).message); + } + }} + />
- ))} -
-
-
+
+ + + } + > +
+ {themes.map((t) => ( + { + setOnebotValue('theme', { ...t.theme, fontMode: theme.fontMode }); + }} + /> + ))} +
+
+ + } + > +
+ {(['light', 'dark'] as const).map((mode) => ( +
+

+ {mode === 'dark' ? : } + {mode === 'dark' ? '深色模式' : '浅色模式'} +

+
+ {colorKeys.map((colorKey) => ( +
+ { + const hslArray = value?.split(' ') ?? [0, 0, 0]; + const color = `hsl(${hslArray[0]}, ${hslArray[1]}, ${hslArray[2]})`; + return ( + { + onChange( + `${result.hsl.h} ${result.hsl.s * 100}% ${result.hsl.l * 100}%` + ); + }} + /> + ); + }} + /> + + {colorKey.replace('--heroui-', '')} + +
+ ))} +
+
+ ))} +
+
+ +
); }; diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/config/webui.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/config/webui.tsx index af18425f..28ad1702 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/config/webui.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/config/webui.tsx @@ -7,11 +7,9 @@ import toast from 'react-hot-toast'; import key from '@/const/key'; import SaveButtons from '@/components/button/save_buttons'; -import FileInput from '@/components/input/file_input'; import ImageInput from '@/components/input/image_input'; import { siteConfig } from '@/config/site'; -import FileManager from '@/controllers/file_manager'; import WebUIManager from '@/controllers/webui_manager'; // Base64URL to Uint8Array converter @@ -37,10 +35,10 @@ const WebUIConfigCard = () => { handleSubmit: handleWebuiSubmit, formState: { isSubmitting }, setValue: setWebuiValue, - } = useForm({ + } = useForm({ defaultValues: { background: '', - customIcons: {}, + customIcons: {} as Record, }, }); @@ -92,39 +90,6 @@ const WebUIConfigCard = () => { return ( <> WebUI配置 - NapCat WebUI -
-
WebUI字体
-
- 此项不需要手动保存,上传成功后需清空网页缓存并刷新 - { - try { - await FileManager.uploadWebUIFont(file); - toast.success('上传成功'); - setTimeout(() => { - window.location.reload(); - }, 1000); - } catch (error) { - toast.error('上传失败: ' + (error as Error).message); - } - }} - onDelete={async () => { - try { - await FileManager.deleteWebUIFont(); - toast.success('删除成功'); - setTimeout(() => { - window.location.reload(); - }, 1000); - } catch (error) { - toast.error('删除失败: ' + (error as Error).message); - } - }} - /> -
-
背景图
{ - code: number - data: T - message: string + code: number; + data: T; + message: string; } interface AuthResponse { - Credential: string + Credential: string; } interface LoginListItem { - uin: string - uid: string - nickName: string - faceUrl: string - facePath: string - loginType: 1 // 1是二维码登录? - isQuickLogin: boolean // 是否可以快速登录 - isAutoLogin: boolean // 是否可以自动登录 + uin: string; + uid: string; + nickName: string; + faceUrl: string; + facePath: string; + loginType: 1; // 1是二维码登录? + isQuickLogin: boolean; // 是否可以快速登录 + isAutoLogin: boolean; // 是否可以自动登录 } interface PackageInfo { - name: string - version: string - private: boolean - type: string - scripts: Record - dependencies: Record - devDependencies: Record + name: string; + version: string; + private: boolean; + type: string; + scripts: Record; + dependencies: Record; + devDependencies: Record; } interface SystemStatus { cpu: { - core: number - model: string - speed: string + core: number; + model: string; + speed: string; usage: { - system: string - qq: string - } - } + system: string; + qq: string; + }; + }; memory: { - total: string + total: string; usage: { - system: string - qq: string - } - } - arch: string + system: string; + qq: string; + }; + }; + arch: string; } interface ThemeConfigItem { - '--heroui-background': string - '--heroui-foreground-50': string - '--heroui-foreground-100': string - '--heroui-foreground-200': string - '--heroui-foreground-300': string - '--heroui-foreground-400': string - '--heroui-foreground-500': string - '--heroui-foreground-600': string - '--heroui-foreground-700': string - '--heroui-foreground-800': string - '--heroui-foreground-900': string - '--heroui-foreground': string - '--heroui-focus': string - '--heroui-overlay': string - '--heroui-divider': string - '--heroui-divider-opacity': string - '--heroui-content1': string - '--heroui-content1-foreground': string - '--heroui-content2': string - '--heroui-content2-foreground': string - '--heroui-content3': string - '--heroui-content3-foreground': string - '--heroui-content4': string - '--heroui-content4-foreground': string - '--heroui-default-50': string - '--heroui-default-100': string - '--heroui-default-200': string - '--heroui-default-300': string - '--heroui-default-400': string - '--heroui-default-500': string - '--heroui-default-600': string - '--heroui-default-700': string - '--heroui-default-800': string - '--heroui-default-900': string - '--heroui-default-foreground': string - '--heroui-default': string + '--heroui-background': string; + '--heroui-foreground-50': string; + '--heroui-foreground-100': string; + '--heroui-foreground-200': string; + '--heroui-foreground-300': string; + '--heroui-foreground-400': string; + '--heroui-foreground-500': string; + '--heroui-foreground-600': string; + '--heroui-foreground-700': string; + '--heroui-foreground-800': string; + '--heroui-foreground-900': string; + '--heroui-foreground': string; + '--heroui-focus': string; + '--heroui-overlay': string; + '--heroui-divider': string; + '--heroui-divider-opacity': string; + '--heroui-content1': string; + '--heroui-content1-foreground': string; + '--heroui-content2': string; + '--heroui-content2-foreground': string; + '--heroui-content3': string; + '--heroui-content3-foreground': string; + '--heroui-content4': string; + '--heroui-content4-foreground': string; + '--heroui-default-50': string; + '--heroui-default-100': string; + '--heroui-default-200': string; + '--heroui-default-300': string; + '--heroui-default-400': string; + '--heroui-default-500': string; + '--heroui-default-600': string; + '--heroui-default-700': string; + '--heroui-default-800': string; + '--heroui-default-900': string; + '--heroui-default-foreground': string; + '--heroui-default': string; // 新增 danger - '--heroui-danger-50': string - '--heroui-danger-100': string - '--heroui-danger-200': string - '--heroui-danger-300': string - '--heroui-danger-400': string - '--heroui-danger-500': string - '--heroui-danger-600': string - '--heroui-danger-700': string - '--heroui-danger-800': string - '--heroui-danger-900': string - '--heroui-danger-foreground': string - '--heroui-danger': string + '--heroui-danger-50': string; + '--heroui-danger-100': string; + '--heroui-danger-200': string; + '--heroui-danger-300': string; + '--heroui-danger-400': string; + '--heroui-danger-500': string; + '--heroui-danger-600': string; + '--heroui-danger-700': string; + '--heroui-danger-800': string; + '--heroui-danger-900': string; + '--heroui-danger-foreground': string; + '--heroui-danger': string; // 新增 primary - '--heroui-primary-50': string - '--heroui-primary-100': string - '--heroui-primary-200': string - '--heroui-primary-300': string - '--heroui-primary-400': string - '--heroui-primary-500': string - '--heroui-primary-600': string - '--heroui-primary-700': string - '--heroui-primary-800': string - '--heroui-primary-900': string - '--heroui-primary-foreground': string - '--heroui-primary': string + '--heroui-primary-50': string; + '--heroui-primary-100': string; + '--heroui-primary-200': string; + '--heroui-primary-300': string; + '--heroui-primary-400': string; + '--heroui-primary-500': string; + '--heroui-primary-600': string; + '--heroui-primary-700': string; + '--heroui-primary-800': string; + '--heroui-primary-900': string; + '--heroui-primary-foreground': string; + '--heroui-primary': string; // 新增 secondary - '--heroui-secondary-50': string - '--heroui-secondary-100': string - '--heroui-secondary-200': string - '--heroui-secondary-300': string - '--heroui-secondary-400': string - '--heroui-secondary-500': string - '--heroui-secondary-600': string - '--heroui-secondary-700': string - '--heroui-secondary-800': string - '--heroui-secondary-900': string - '--heroui-secondary-foreground': string - '--heroui-secondary': string + '--heroui-secondary-50': string; + '--heroui-secondary-100': string; + '--heroui-secondary-200': string; + '--heroui-secondary-300': string; + '--heroui-secondary-400': string; + '--heroui-secondary-500': string; + '--heroui-secondary-600': string; + '--heroui-secondary-700': string; + '--heroui-secondary-800': string; + '--heroui-secondary-900': string; + '--heroui-secondary-foreground': string; + '--heroui-secondary': string; // 新增 success - '--heroui-success-50': string - '--heroui-success-100': string - '--heroui-success-200': string - '--heroui-success-300': string - '--heroui-success-400': string - '--heroui-success-500': string - '--heroui-success-600': string - '--heroui-success-700': string - '--heroui-success-800': string - '--heroui-success-900': string - '--heroui-success-foreground': string - '--heroui-success': string + '--heroui-success-50': string; + '--heroui-success-100': string; + '--heroui-success-200': string; + '--heroui-success-300': string; + '--heroui-success-400': string; + '--heroui-success-500': string; + '--heroui-success-600': string; + '--heroui-success-700': string; + '--heroui-success-800': string; + '--heroui-success-900': string; + '--heroui-success-foreground': string; + '--heroui-success': string; // 新增 warning - '--heroui-warning-50': string - '--heroui-warning-100': string - '--heroui-warning-200': string - '--heroui-warning-300': string - '--heroui-warning-400': string - '--heroui-warning-500': string - '--heroui-warning-600': string - '--heroui-warning-700': string - '--heroui-warning-800': string - '--heroui-warning-900': string - '--heroui-warning-foreground': string - '--heroui-warning': string + '--heroui-warning-50': string; + '--heroui-warning-100': string; + '--heroui-warning-200': string; + '--heroui-warning-300': string; + '--heroui-warning-400': string; + '--heroui-warning-500': string; + '--heroui-warning-600': string; + '--heroui-warning-700': string; + '--heroui-warning-800': string; + '--heroui-warning-900': string; + '--heroui-warning-foreground': string; + '--heroui-warning': string; // 其它配置 - '--heroui-code-background': string - '--heroui-strong': string - '--heroui-code-mdx': string - '--heroui-divider-weight': string - '--heroui-disabled-opacity': string - '--heroui-font-size-tiny': string - '--heroui-font-size-small': string - '--heroui-font-size-medium': string - '--heroui-font-size-large': string - '--heroui-line-height-tiny': string - '--heroui-line-height-small': string - '--heroui-line-height-medium': string - '--heroui-line-height-large': string - '--heroui-radius-small': string - '--heroui-radius-medium': string - '--heroui-radius-large': string - '--heroui-border-width-small': string - '--heroui-border-width-medium': string - '--heroui-border-width-large': string - '--heroui-box-shadow-small': string - '--heroui-box-shadow-medium': string - '--heroui-box-shadow-large': string - '--heroui-hover-opacity': string + '--heroui-code-background': string; + '--heroui-strong': string; + '--heroui-code-mdx': string; + '--heroui-divider-weight': string; + '--heroui-disabled-opacity': string; + '--heroui-font-size-tiny': string; + '--heroui-font-size-small': string; + '--heroui-font-size-medium': string; + '--heroui-font-size-large': string; + '--heroui-line-height-tiny': string; + '--heroui-line-height-small': string; + '--heroui-line-height-medium': string; + '--heroui-line-height-large': string; + '--heroui-radius-small': string; + '--heroui-radius-medium': string; + '--heroui-radius-large': string; + '--heroui-border-width-small': string; + '--heroui-border-width-medium': string; + '--heroui-border-width-large': string; + '--heroui-box-shadow-small': string; + '--heroui-box-shadow-medium': string; + '--heroui-box-shadow-large': string; + '--heroui-hover-opacity': string; } interface ThemeConfig { - dark: ThemeConfigItem - light: ThemeConfigItem + dark: ThemeConfigItem; + light: ThemeConfigItem; + fontMode?: string; } interface WebUIConfig { - host: string - port: number - loginRate: number - disableWebUI: boolean - disableNonLANAccess: boolean + host: string; + port: number; + loginRate: number; + disableWebUI: boolean; + disableNonLANAccess: boolean; } diff --git a/packages/napcat-webui-frontend/src/utils/theme.ts b/packages/napcat-webui-frontend/src/utils/theme.ts index 65303c4c..8f02b31d 100644 --- a/packages/napcat-webui-frontend/src/utils/theme.ts +++ b/packages/napcat-webui-frontend/src/utils/theme.ts @@ -3,6 +3,11 @@ import { request } from './request'; const style = document.createElement('style'); document.head.appendChild(style); +// 字体样式标签 +const fontStyle = document.createElement('style'); +fontStyle.id = 'dynamic-font-style'; +document.head.appendChild(fontStyle); + export function loadTheme () { request('/files/theme.css?_t=' + Date.now()) .then((res) => res.data) @@ -14,6 +19,29 @@ export function loadTheme () { }); } +// 动态加载字体 CSS +const loadFontCSS = (mode: string) => { + let css = ''; + + if (mode === 'aacute') { + css = ` +@font-face { + font-family: 'Aa偷吃可爱长大的'; + src: url('/webui/fonts/AaCute.woff') format('woff'); + font-display: swap; +}`; + } else if (mode === 'custom') { + css = ` +@font-face { + font-family: 'CustomFont'; + src: url('/webui/fonts/CustomFont.woff') format('woff'); + font-display: swap; +}`; + } + + fontStyle.innerHTML = css; +}; + export const colorKeys = [ '--heroui-background', @@ -139,3 +167,53 @@ export const generateTheme = (theme: ThemeConfig, validField?: string) => { css += '}'; return css; }; + +export const applyFont = (mode: string) => { + const root = document.documentElement; + + // 先加载字体 CSS + loadFontCSS(mode); + + if (mode === 'aacute') { + root.style.setProperty('--font-family-base', "'Aa偷吃可爱长大的', var(--font-family-fallbacks)", 'important'); + } else if (mode === 'custom') { + root.style.setProperty('--font-family-base', "'CustomFont', var(--font-family-fallbacks)", 'important'); + } else { + // system or default - restore default + root.style.setProperty('--font-family-base', 'var(--font-family-fallbacks)', 'important'); + } +}; + +const FONT_MODE_CACHE_KEY = 'webui-font-mode-cache'; + +export const initFont = () => { + // 先从缓存读取,立即应用 + const cached = localStorage.getItem(FONT_MODE_CACHE_KEY); + if (cached) { + applyFont(cached); + } else { + // 默认使用系统字体 + applyFont('system'); + } + + // 后台拉取最新配置并更新缓存 + request('/api/base/Theme') + .then((res) => { + const data = res.data as { data: ThemeConfig; }; + const fontMode = data?.data?.fontMode || 'system'; + // 更新缓存 + localStorage.setItem(FONT_MODE_CACHE_KEY, fontMode); + // 如果与当前不同,则应用新字体 + if (fontMode !== cached) { + applyFont(fontMode); + } + }) + .catch((e) => { + console.error('Failed to fetch font config', e); + }); +}; + +// 保存时更新缓存 +export const updateFontCache = (fontMode: string) => { + localStorage.setItem(FONT_MODE_CACHE_KEY, fontMode); +}; diff --git a/packages/napcat-webui-frontend/vite.config.ts b/packages/napcat-webui-frontend/vite.config.ts index 88e1bab6..8d64323f 100644 --- a/packages/napcat-webui-frontend/vite.config.ts +++ b/packages/napcat-webui-frontend/vite.config.ts @@ -1,13 +1,9 @@ import react from '@vitejs/plugin-react'; -import path from 'node:path'; -import { defineConfig, loadEnv, normalizePath } from 'vite'; -import { viteStaticCopy } from 'vite-plugin-static-copy'; +import { defineConfig, loadEnv } from 'vite'; +import viteCompression from 'vite-plugin-compression'; +import { ViteImageOptimizer } from 'vite-plugin-image-optimizer'; import tsconfigPaths from 'vite-tsconfig-paths'; -const monacoEditorPath = normalizePath( - path.resolve(__dirname, 'node_modules/monaco-editor/min/vs') -); - // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd()); @@ -17,14 +13,7 @@ export default defineConfig(({ mode }) => { plugins: [ react(), tsconfigPaths(), - viteStaticCopy({ - targets: [ - { - src: monacoEditorPath, - dest: 'monaco-editor/min', - }, - ], - }), + ViteImageOptimizer({}), ], base: '/webui/', server: { @@ -41,19 +30,41 @@ export default defineConfig(({ mode }) => { }, '/api': backendDebugUrl, '/files': backendDebugUrl, + '/webui/fonts/CustomFont.woff': backendDebugUrl, + '/webui/sw.js': backendDebugUrl, }, }, build: { assetsInlineLimit: 0, rollupOptions: { output: { - manualChunks: { - 'monaco-editor': ['monaco-editor'], - 'react-dom': ['react-dom'], - 'react-router-dom': ['react-router-dom'], - 'react-hook-form': ['react-hook-form'], - 'react-hot-toast': ['react-hot-toast'], - qface: ['qface'], + manualChunks (id) { + if (id.includes('node_modules')) { + if (id.includes('@heroui/')) { + return 'heroui'; + } + if (id.includes('react-dom')) { + return 'react-dom'; + } + if (id.includes('react-router-dom')) { + return 'react-router-dom'; + } + if (id.includes('react-hook-form')) { + return 'react-hook-form'; + } + if (id.includes('react-hot-toast')) { + return 'react-hot-toast'; + } + if (id.includes('qface')) { + return 'qface'; + } + if (id.includes('@uiw/react-codemirror') || id.includes('@codemirror/view') || id.includes('@codemirror/theme-one-dark')) { + return 'codemirror-core'; + } + if (id.includes('@codemirror/lang-')) { + return 'codemirror-lang'; + } + } }, }, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2512e06e..9e6c7083 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -323,6 +323,9 @@ importers: compressing: specifier: ^1.10.3 version: 1.10.3 + compression: + specifier: ^1.8.1 + version: 1.8.1 express: specifier: ^5.0.0 version: 5.1.0 @@ -345,6 +348,9 @@ importers: specifier: ^8.18.3 version: 8.18.3 devDependencies: + '@types/compression': + specifier: ^1.8.1 + version: 1.8.1 '@types/express': specifier: ^5.0.0 version: 5.0.5 @@ -360,6 +366,21 @@ importers: packages/napcat-webui-frontend: dependencies: + '@codemirror/lang-css': + specifier: ^6.3.1 + version: 6.3.1 + '@codemirror/lang-javascript': + specifier: ^6.2.4 + version: 6.2.4 + '@codemirror/lang-json': + specifier: ^6.0.2 + version: 6.0.2 + '@codemirror/theme-one-dark': + specifier: ^6.1.3 + version: 6.1.3 + '@codemirror/view': + specifier: ^6.39.6 + version: 6.39.6 '@dnd-kit/core': specifier: ^6.3.1 version: 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -465,9 +486,6 @@ importers: '@monaco-editor/loader': specifier: ^1.4.0 version: 1.6.1 - '@monaco-editor/react': - specifier: 4.7.0-rc.0 - version: 4.7.0-rc.0(monaco-editor@0.52.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@react-aria/visually-hidden': specifier: ^3.8.19 version: 3.8.28(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -477,6 +495,9 @@ importers: '@uidotdev/usehooks': specifier: ^2.4.1 version: 2.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@uiw/react-codemirror': + specifier: ^4.25.4 + version: 4.25.4(@babel/runtime@7.28.4)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.3)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.6)(codemirror@6.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@xterm/addon-canvas': specifier: ^0.7.0 version: 0.7.0(@xterm/xterm@5.5.0) @@ -504,9 +525,6 @@ importers: event-source-polyfill: specifier: ^1.0.31 version: 1.0.31 - monaco-editor: - specifier: ^0.52.2 - version: 0.52.2 motion: specifier: ^12.0.6 version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -643,15 +661,24 @@ importers: prettier: specifier: ^3.4.2 version: 3.6.2 + sharp: + specifier: ^0.34.5 + version: 0.34.5 typescript: specifier: ^5.7.3 version: 5.9.3 vite: specifier: ^6.0.5 version: 6.4.1(@types/node@22.19.1)(jiti@1.21.7) + vite-plugin-compression: + specifier: ^0.5.1 + version: 0.5.1(vite@6.4.1(@types/node@22.19.1)(jiti@1.21.7)) vite-plugin-font: specifier: ^5.1.2 version: 5.1.2(encoding@0.1.13) + vite-plugin-image-optimizer: + specifier: ^2.0.3 + version: 2.0.3(sharp@0.34.5)(vite@6.4.1(@types/node@22.19.1)(jiti@1.21.7)) vite-plugin-static-copy: specifier: ^2.2.0 version: 2.3.2(vite@6.4.1(@types/node@22.19.1)(jiti@1.21.7)) @@ -764,6 +791,39 @@ packages: '@capsizecss/unpack@2.4.0': resolution: {integrity: sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q==} + '@codemirror/autocomplete@6.20.0': + resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} + + '@codemirror/commands@6.10.1': + resolution: {integrity: sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-javascript@6.2.4': + resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/language@6.12.1': + resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==} + + '@codemirror/lint@6.9.2': + resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==} + + '@codemirror/search@6.5.11': + resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==} + + '@codemirror/state@6.5.3': + resolution: {integrity: sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==} + + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + + '@codemirror/view@6.39.6': + resolution: {integrity: sha512-/N+SoP5NndJjkGInp3BwlUa3KQKD6bDo0TV6ep37ueAdQ7BVu/PqlZNywmgjCq0MQoZadZd8T+MZucSr7fktyQ==} + '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} @@ -1569,6 +1629,159 @@ packages: peerDependencies: react: '*' + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@internationalized/date@3.10.0': resolution: {integrity: sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==} @@ -1637,16 +1850,30 @@ packages: '@levischuck/tiny-cbor@0.2.11': resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} + '@lezer/common@1.5.0': + resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==} + + '@lezer/css@1.3.0': + resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.5': + resolution: {integrity: sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@monaco-editor/loader@1.6.1': resolution: {integrity: sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==} - '@monaco-editor/react@4.7.0-rc.0': - resolution: {integrity: sha512-YfjXkDK0bcwS0zo8PXptvQdCQfOPPtzGsAzmIv7PnoUGFdIohsR+NVDyjbajMddF+3cWUm/3q9NzP/DUke9a+w==} - peerDependencies: - monaco-editor: '>= 0.25.0 < 1' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -2571,6 +2798,9 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/compression@1.8.1': + resolution: {integrity: sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -2765,6 +2995,28 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' + '@uiw/codemirror-extensions-basic-setup@4.25.4': + resolution: {integrity: sha512-YzNwkm0AbPv1EXhCHYR5v0nqfemG2jEB0Z3Att4rBYqKrlG7AA9Rhjc3IyBaOzsBu18wtrp9/+uhTyu7TXSRng==} + peerDependencies: + '@codemirror/autocomplete': '>=6.0.0' + '@codemirror/commands': '>=6.0.0' + '@codemirror/language': '>=6.0.0' + '@codemirror/lint': '>=6.0.0' + '@codemirror/search': '>=6.0.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + + '@uiw/react-codemirror@4.25.4': + resolution: {integrity: sha512-ipO067oyfUw+DVaXhQCxkB0ZD9b7RnY+ByrprSYSKCHaULvJ3sqWYC/Zen6zVQ8/XC4o5EPBfatGiX20kC7XGA==} + peerDependencies: + '@babel/runtime': '>=7.11.0' + '@codemirror/state': '>=6.0.0' + '@codemirror/theme-one-dark': '>=6.0.0' + '@codemirror/view': '>=6.0.0' + codemirror: '>=6.0.0' + react: '>=17.0.0' + react-dom: '>=17.0.0' + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -2980,6 +3232,10 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -3336,6 +3592,9 @@ packages: code-points@2.0.0-1: resolution: {integrity: sha512-PuPoUdSqHY96e+CvEGe0+J9XkEqnQ4o79X+k+PJlZ84sZDoSJ2Q8/1OJT4dqYn8yL5EUxGCq/x2EcLEfvcGqaw==} + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -3400,10 +3659,18 @@ packages: resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} engines: {node: '>= 12.0.0'} + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + compressing@1.10.3: resolution: {integrity: sha512-F3RxWLU4UNfNYFVNwCK58HwQnv/5drvUW176FC//3i0pwpdahoZxMM7dkxWuA2MEafqfwDc+iudk70Sx/VMUIw==} engines: {node: '>= 4.0.0'} + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} @@ -3453,6 +3720,9 @@ packages: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-fetch@3.2.0: resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} @@ -3496,6 +3766,14 @@ packages: dayjs@1.11.19: resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -4095,6 +4373,10 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fs-extra@11.3.2: resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} engines: {node: '>=14.14'} @@ -5033,9 +5315,6 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - monaco-editor@0.52.2: - resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} - motion-dom@12.23.23: resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==} @@ -5060,6 +5339,9 @@ packages: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -5201,6 +5483,10 @@ packages: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -5830,6 +6116,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -6032,6 +6322,9 @@ packages: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + style-to-js@1.1.19: resolution: {integrity: sha512-Ev+SgeqiNGT1ufsXyVC5RrJRXdrkRJ1Gol9Qw7Pb72YCKJXrBvP0ckZhBeVSrw2m06DJpei2528uIpjMb4TsoQ==} @@ -6435,6 +6728,11 @@ packages: resolution: {integrity: sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==} engines: {node: '>= 0.10'} + vite-plugin-compression@0.5.1: + resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} + peerDependencies: + vite: '>=2.0.0' + vite-plugin-cp@6.0.3: resolution: {integrity: sha512-qxKGH3v9wPwUDbDchJf4IH4mRE1zkLWgzZSxwrl8LapwUWm48IFS7SlbRmcF4NquC/fESA5cHFge9E2Ps+woxg==} engines: {node: '>=14.18.0', vite: '>=3.1.0'} @@ -6442,6 +6740,19 @@ packages: vite-plugin-font@5.1.2: resolution: {integrity: sha512-Aec3NPRtON9Z+ro4MvZvjiTau7tE3xn6L0wQddVoDYeyMGWzm43D9MWhd5cjF8ohkU6ikGD1V04Q5Tr9O8G4FQ==} + vite-plugin-image-optimizer@2.0.3: + resolution: {integrity: sha512-1vrFOTcpSvv6DCY7h8UXab4wqMAjTJB/ndOzG/Kmj1oDOuPF6mbjkNQoGzzCEYeWGe7qU93jc8oQqvoJ57al3A==} + engines: {node: '>=18.17.0'} + peerDependencies: + sharp: '>=0.34.0' + svgo: '>=4' + vite: '>=5' + peerDependenciesMeta: + sharp: + optional: true + svgo: + optional: true + vite-plugin-static-copy@2.3.2: resolution: {integrity: sha512-iwrrf+JupY4b9stBttRWzGHzZbeMjAHBhkrn67MNACXJVjEMRpCI10Q3AkxdBkl45IHaTfw/CNVevzQhP7yTwg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -6534,6 +6845,9 @@ packages: resolution: {integrity: sha512-2HUCkqI0uwgBti1/+utRu7Hvk/I3HeowBQfRlEL3487r+LpW1w91kk6uTZbwOd6I2Sj3aAxBE0HxYNC/NLbuhA==} engines: {node: '>=14.18.0', vite: '>=3.1.0'} + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -6766,6 +7080,82 @@ snapshots: transitivePeerDependencies: - encoding + '@codemirror/autocomplete@6.20.0': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.6 + '@lezer/common': 1.5.0 + + '@codemirror/commands@6.10.1': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.6 + '@lezer/common': 1.5.0 + + '@codemirror/lang-css@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.3 + '@lezer/common': 1.5.0 + '@lezer/css': 1.3.0 + + '@codemirror/lang-javascript@6.2.4': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.12.1 + '@codemirror/lint': 6.9.2 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.6 + '@lezer/common': 1.5.0 + '@lezer/javascript': 1.5.4 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.1 + '@lezer/json': 1.0.3 + + '@codemirror/language@6.12.1': + dependencies: + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.6 + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.5 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.2': + dependencies: + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.6 + crelt: 1.0.6 + + '@codemirror/search@6.5.11': + dependencies: + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.6 + crelt: 1.0.6 + + '@codemirror/state@6.5.3': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.6 + '@lezer/highlight': 1.2.3 + + '@codemirror/view@6.39.6': + dependencies: + '@codemirror/state': 6.5.3 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@colors/colors@1.6.0': {} '@dabh/diagnostics@2.0.8': @@ -7883,6 +8273,102 @@ snapshots: dependencies: react: 19.2.0 + '@img/colour@1.0.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.7.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + '@internationalized/date@3.10.0': dependencies: '@swc/helpers': 0.5.17 @@ -7968,17 +8454,40 @@ snapshots: '@levischuck/tiny-cbor@0.2.11': {} + '@lezer/common@1.5.0': {} + + '@lezer/css@1.3.0': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.5 + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.0 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.5 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.0 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.5 + + '@lezer/lr@1.4.5': + dependencies: + '@lezer/common': 1.5.0 + + '@marijn/find-cluster-break@1.0.2': {} + '@monaco-editor/loader@1.6.1': dependencies: state-local: 1.0.7 - '@monaco-editor/react@4.7.0-rc.0(monaco-editor@0.52.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': - dependencies: - '@monaco-editor/loader': 1.6.1 - monaco-editor: 0.52.2 - react: 19.2.0 - react-dom: 19.2.0(react@19.2.0) - '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.7.1 @@ -9216,6 +9725,11 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/compression@1.8.1': + dependencies: + '@types/express': 5.0.5 + '@types/node': 22.19.1 + '@types/connect@3.4.38': dependencies: '@types/node': 22.19.1 @@ -9449,6 +9963,33 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) + '@uiw/codemirror-extensions-basic-setup@4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.1)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.3)(@codemirror/view@6.39.6)': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/commands': 6.10.1 + '@codemirror/language': 6.12.1 + '@codemirror/lint': 6.9.2 + '@codemirror/search': 6.5.11 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.6 + + '@uiw/react-codemirror@4.25.4(@babel/runtime@7.28.4)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.3)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.6)(codemirror@6.0.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@codemirror/commands': 6.10.1 + '@codemirror/state': 6.5.3 + '@codemirror/theme-one-dark': 6.1.3 + '@codemirror/view': 6.39.6 + '@uiw/codemirror-extensions-basic-setup': 4.25.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.1)(@codemirror/language@6.12.1)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.3)(@codemirror/view@6.39.6) + codemirror: 6.0.2 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + transitivePeerDependencies: + - '@codemirror/autocomplete' + - '@codemirror/language' + - '@codemirror/lint' + - '@codemirror/search' + '@ungap/structured-clone@1.3.0': {} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -9653,6 +10194,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-colors@4.1.3: {} + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -10070,6 +10613,16 @@ snapshots: dependencies: code-point: 1.1.0 + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/commands': 6.10.1 + '@codemirror/language': 6.12.1 + '@codemirror/lint': 6.9.2 + '@codemirror/search': 6.5.11 + '@codemirror/state': 6.5.3 + '@codemirror/view': 6.39.6 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -10121,6 +10674,10 @@ snapshots: comment-parser@1.4.1: {} + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + compressing@1.10.3: dependencies: '@eggjs/yauzl': 2.11.0 @@ -10133,6 +10690,18 @@ snapshots: tar-stream: 1.6.2 yazl: 2.5.1 + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + compute-scroll-into-view@3.1.1: {} concat-map@0.0.1: {} @@ -10171,6 +10740,8 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + crelt@1.0.6: {} + cross-fetch@3.2.0(encoding@0.1.13): dependencies: node-fetch: 2.7.0(encoding@0.1.13) @@ -10218,6 +10789,10 @@ snapshots: dayjs@1.11.19: {} + debug@2.6.9: + dependencies: + ms: 2.0.0 + debug@3.2.7: dependencies: ms: 2.1.3 @@ -11001,6 +11576,12 @@ snapshots: fs-constants@1.0.0: {} + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs-extra@11.3.2: dependencies: graceful-fs: 4.2.11 @@ -12185,8 +12766,6 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.1 - monaco-editor@0.52.2: {} - motion-dom@12.23.23: dependencies: motion-utils: 12.23.6 @@ -12203,6 +12782,8 @@ snapshots: mrmime@2.0.1: {} + ms@2.0.0: {} + ms@2.1.3: {} multer@2.0.2: @@ -12369,6 +12950,8 @@ snapshots: dependencies: ee-first: 1.1.1 + on-headers@1.1.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -13086,6 +13669,37 @@ snapshots: setprototypeof@1.2.0: {} + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -13312,6 +13926,8 @@ snapshots: dependencies: '@tokenizer/token': 0.3.0 + style-mod@4.1.3: {} + style-to-js@1.1.19: dependencies: style-to-object: 1.0.12 @@ -13824,6 +14440,15 @@ snapshots: remove-trailing-separator: 1.1.0 replace-ext: 1.0.1 + vite-plugin-compression@0.5.1(vite@6.4.1(@types/node@22.19.1)(jiti@1.21.7)): + dependencies: + chalk: 4.1.2 + debug: 4.4.3 + fs-extra: 10.1.0 + vite: 6.4.1(@types/node@22.19.1)(jiti@1.21.7) + transitivePeerDependencies: + - supports-color + vite-plugin-cp@6.0.3: dependencies: fs-extra: 11.3.2 @@ -13845,6 +14470,14 @@ snapshots: transitivePeerDependencies: - encoding + vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@6.4.1(@types/node@22.19.1)(jiti@1.21.7)): + dependencies: + ansi-colors: 4.1.3 + pathe: 2.0.3 + vite: 6.4.1(@types/node@22.19.1)(jiti@1.21.7) + optionalDependencies: + sharp: 0.34.5 + vite-plugin-static-copy@2.3.2(vite@6.4.1(@types/node@22.19.1)(jiti@1.21.7)): dependencies: chokidar: 3.6.0 @@ -13929,6 +14562,8 @@ snapshots: transitivePeerDependencies: - supports-color + w3c-keyname@2.2.8: {} + webidl-conversions@3.0.1: {} webpack-virtual-modules@0.6.2: {}