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: {}