diff --git a/src/frontend/package.json b/src/frontend/package.json index 130b415f..55d68340 100755 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -5,8 +5,9 @@ "type": "module", "scripts": { "dev": "vite", - "build": "node scripts/check-i18n.mjs && tsc -b && vite build", + "build": "node scripts/check-i18n.mjs && node scripts/check-dark-mode.mjs && tsc -b && vite build", "check:i18n": "node scripts/check-i18n.mjs", + "check:dark": "node scripts/check-dark-mode.mjs", "test:chat-stream": "esbuild scripts/test-chat-stream-segments.ts --bundle --platform=node --format=esm --outfile=node_modules/.tmp/test-chat-stream-segments.mjs && node node_modules/.tmp/test-chat-stream-segments.mjs", "lint": "eslint .", "preview": "vite preview" diff --git a/src/frontend/scripts/check-dark-mode.mjs b/src/frontend/scripts/check-dark-mode.mjs new file mode 100644 index 00000000..c389c068 --- /dev/null +++ b/src/frontend/scripts/check-dark-mode.mjs @@ -0,0 +1,406 @@ +#!/usr/bin/env node +/** + * 深色模式门禁(构建前执行,与 check-i18n 同级)。 + * + * 背景:深色模式靠 `` 切换 variables.css 的令牌覆盖块实现。 + * 只要新写的样式**绕过令牌**直接写死颜色,浅色下看着正常、深色下就是一块白斑—— + * 而且没人会在提交前逐个组件切两次主题。历史上踩过的四类盲区: + * 1. CSS 声明里直接写 #fff / rgba(0,0,0,.06); + * 2. 自定义属性里嵌硬编码色(`--jx-chip-bg:#fff`)——批量替换脚本只扫真实声明,扫不到它; + * 3. TSX 内联 style / SVG 属性写死颜色(`style={{ background:'#ffffff' }}`); + * 4. `var(--never-defined, #fff)`——兜底永远生效,等于硬编码。 + * 外加一条纪律:深色专项覆盖一律用 `:root[data-theme='dark']`,禁止 `@media (prefers-color-scheme:dark)` + * (会和手动三档打架)。 + * + * 门禁形态是**棘轮**而不是一刀切:`dark-mode-baseline.json` 冻结存量债, + * 新增违规直接挂构建,修掉存量后跑 `npm run check:dark -- --update` 把基线收紧。 + * 存量只能变少、不能变多。 + * + * 逃生舱:确实要写死颜色(暗色代码台、品牌插画、第三方主题对象)在同一行或上一行标 + * `dark-ok: 原因`(CSS 用 `/* dark-ok: … *\/`,TS 用 `// dark-ok: …`)即豁免。 + * + * 用法: + * node scripts/check-dark-mode.mjs # 门禁,超基线即退出码 1 + * node scripts/check-dark-mode.mjs --list # 打印全部违规明细(含基线内的) + * node scripts/check-dark-mode.mjs --update # 以当前实测重写基线 + */ +import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { join, dirname, relative, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const frontendRoot = join(scriptDir, '..'); +const srcRoot = join(frontendRoot, 'src'); +const baselinePath = join(scriptDir, 'dark-mode-baseline.json'); + +/* CE 派生树的 overlay:`ce/overlay/src/frontend/src/**` 会在 build_ce 时**整文件替换** + 同名的 `src/frontend/src/**`。不一起扫,overlay 里的硬编码色在本仓库就完全没有反馈—— + 要等派生出去之后、在另一个仓库跑构建才炸,那正是本门禁要消灭的「无反馈回路」。 + 仓库根 = frontendRoot 的上两级(src/frontend → repo root)。 */ +const repoRoot = join(frontendRoot, '..', '..'); +const ceOverlayRoot = join(repoRoot, 'ce', 'overlay', 'src', 'frontend', 'src'); + +const args = process.argv.slice(2); +const UPDATE = args.includes('--update'); +const LIST = args.includes('--list'); + +/* ── 允许写死颜色的文件:调色板真源与成对主题定义 ────────────────────── + 这些文件**本身**就是「浅色一套 / 深色一套」的定义处,写死颜色是它们的职责。 */ +const ALLOWED_FILES = new Set([ + 'src/styles/variables.css', // 令牌调色板真源(:root 与 :root[data-theme=dark] 两块) + 'src/styles/code-highlight.css', // 代码高亮双主题(github / github-dark 两套 token 色) + 'src/theme.ts', // antd ConfigProvider 的成对主题对象 +]); + +/* 目录级跳过 */ +const SKIP_DIRS = new Set(['node_modules', 'dist', 'assets', 'i18n']); + +/* ── 颜色字面量识别 ─────────────────────────────────────────────── + 命名色只收「会造成明暗事故」的那批(白/黑/灰系);rebeccapurple 之流不管。 */ +const NAMED_COLORS = + /\b(white|black|whitesmoke|snow|ivory|floralwhite|ghostwhite|gainsboro|lightgray|lightgrey|silver|darkgray|darkgrey|dimgray|dimgrey|gray|grey)\b/i; +const HEX = /#[0-9a-fA-F]{3,8}\b/; +const FUNC_COLOR = /\b(rgba?|hsla?|hwb|lab|lch|oklab|oklch)\s*\(/i; + +/** `var(--已定义令牌, #fff)` 的兜底永远走不到,是死代码不是深色事故——整段摘掉别报。 + * 令牌未定义的那种由 undefined-var-fallback 规则单独抓。 */ +function stripSafeVarFallbacks(value) { + return value.replace( + /var\(\s*(--[A-Za-z0-9_-]+)\s*,([^()]*(?:\([^()]*\)[^()]*)*)\)/g, + (whole, name) => (definedVars.has(name) ? ' ' : whole), + ); +} + +function hasColorLiteral(value) { + // 先摘掉令牌名本身:--color-text-white / --color-bg-gray 里的 white/gray 是名字不是颜色。 + // color-mix(in srgb, var(--x) 8%, transparent) 是推荐写法,摘完就不含任何字面色。 + const v = stripSafeVarFallbacks(value).replace(/--[A-Za-z0-9_-]+/g, ''); + if (HEX.test(v)) return true; + if (FUNC_COLOR.test(v)) return true; + if (NAMED_COLORS.test(v)) return true; + return false; +} + +/* 承载颜色的 CSS 属性(含自定义属性,单独判断) */ +const COLOR_PROPS = + /^(color|background|background-color|background-image|border|border-(top|right|bottom|left)?-?color|border(-(top|right|bottom|left))?|outline|outline-color|box-shadow|text-shadow|fill|stroke|caret-color|accent-color|text-decoration-color|column-rule-color|text-emphasis-color|scrollbar-color|filter|backdrop-filter|stop-color|flood-color|lighting-color)$/; + +/* TSX 内联 style 的同类属性(camelCase) */ +const JS_COLOR_PROPS = new Set([ + 'color', 'background', 'backgroundColor', 'backgroundImage', 'border', 'borderColor', + 'borderTop', 'borderRight', 'borderBottom', 'borderLeft', 'borderTopColor', + 'borderRightColor', 'borderBottomColor', 'borderLeftColor', 'outline', 'outlineColor', + 'boxShadow', 'textShadow', 'fill', 'stroke', 'caretColor', 'accentColor', + 'textDecorationColor', 'columnRuleColor', 'scrollbarColor', 'filter', 'backdropFilter', + 'stopColor', 'floodColor', 'lightingColor', +]); + +/* 规则 id 与它的修法写在一处——分成两份列表迟早会对不上(改了 id 忘了改帮助文案)。 */ +const RULE_SPECS = { + CSS_LITERAL: { + id: 'css-literal-color', // 盲区 1 + fix: '颜色改用令牌 var(--color-*);半透明面用\n' + + ' color-mix(in srgb, var(--color-bg-container) 60%, transparent)', + }, + CSS_VAR_DEF: { + id: 'css-var-literal-color', // 盲区 2 + fix: '自定义属性的值本身也要引令牌,别把 #fff 藏进 --jx-xxx', + }, + JS_INLINE: { + id: 'js-inline-color', // 盲区 3 + fix: '内联 style 改 var(--color-*),或把样式挪进 CSS 类', + }, + VAR_FALLBACK: { + id: 'undefined-var-fallback', // 盲区 4 + fix: 'var(--x, #fff) 的兜底永远生效;补上 --x 定义或直接引已有令牌', + }, + MEDIA_SCHEME: { + id: 'prefers-color-scheme', // 纪律:禁止绕开手动三档 + fix: "深色覆盖统一写 :root[data-theme='dark'] 选择器,别用媒体查询\n" + + ' (媒体查询会和手动 light/dark 三档打架)', + }, +}; +const RULES = Object.fromEntries(Object.entries(RULE_SPECS).map(([k, v]) => [k, v.id])); +const FIX_HINTS = Object.values(RULE_SPECS); +const RULE_ID_WIDTH = Math.max(...FIX_HINTS.map((r) => r.id.length)); + +const findings = []; +function report(rule, file, line, snippet) { + findings.push({ rule, file, line, snippet: snippet.replace(/\s+/g, ' ').trim().slice(0, 160) }); +} + +/* ── 文件遍历 ─────────────────────────────────────────────────── */ +/** 一次读盘读完:definedVars 与扫描两遍都用同一份内容,别把每个文件读两次。 */ +const sources = new Map(); // 展示用相对路径 → 文件内容 +function collect(root, relTo) { + if (!existsSync(root)) return; + (function walk(dir) { + for (const e of readdirSync(dir, { withFileTypes: true })) { + if (e.name.startsWith('.')) continue; + const p = join(dir, e.name); + if (e.isDirectory()) { + if (SKIP_DIRS.has(e.name)) continue; + walk(p); + } else if (/\.(css|ts|tsx)$/.test(e.name)) { + sources.set(relative(relTo, p).split(sep).join('/'), readFileSync(p, 'utf-8')); + } + } + })(root); +} +collect(srcRoot, frontendRoot); +// overlay 以仓库根为基准展示(ce/overlay/src/frontend/src/…),与主树同名文件区分开: +// 两边都要扫——FULL 用主树那份,CE 派生后用 overlay 那份,谁都不能漏。 +collect(ceOverlayRoot, repoRoot); + +/* ── 全仓自定义属性定义集合(供盲区 4 取差集)───────────────────── */ +const definedVars = new Set(); +for (const src of sources.values()) { + for (const m of src.matchAll(/(--[A-Za-z0-9_-]+)\s*:/g)) definedVars.add(m[1]); + // JS 侧 setProperty('--x', …) 也算定义 + for (const m of src.matchAll(/setProperty\(\s*['"`](--[A-Za-z0-9_-]+)/g)) definedVars.add(m[1]); +} + +/** 豁免标记,三种写法: + * 1. 本行带 `dark-ok`; + * 2. 紧邻其上的注释块里带 `dark-ok`(注释可跨多行); + * 3. 整段包在 `dark-ok-begin` / `dark-ok-end` 之间——用于生成独立文档的大段模板 + * (导出 PDF/HTML、邮件正文),那种整块都不该跟随界面主题。 */ +const COMMENT_LINE = /^\s*(\/\*|\*|\/\/)|\*\/\s*$/; +const regionCache = new WeakMap(); +function exemptRegions(lines) { + let flags = regionCache.get(lines); + if (flags) return flags; + flags = new Array(lines.length).fill(false); + let open = false; + lines.forEach((ln, i) => { + if (/dark-ok-begin/.test(ln)) open = true; + flags[i] = open; + if (/dark-ok-end/.test(ln)) open = false; + }); + regionCache.set(lines, flags); + return flags; +} +function exempt(lines, idx) { + if (exemptRegions(lines)[idx]) return true; + if (/dark-ok/.test(lines[idx] || '')) return true; + for (let i = idx - 1; i >= 0 && idx - i <= 8; i--) { + const ln = lines[i] || ''; + if (/dark-ok/.test(ln)) return true; + if (!COMMENT_LINE.test(ln) && ln.trim() !== '') return false; + } + return false; +} + +/* 未定义令牌 + 含颜色兜底:`var(--x, #fff)` */ +function scanVarFallback(text, lines, file) { + for (const m of text.matchAll(/var\(\s*(--[A-Za-z0-9_-]+)\s*,([^()]*(?:\([^()]*\)[^()]*)*)\)/g)) { + const [, name, fallback] = m; + if (definedVars.has(name)) continue; + if (!hasColorLiteral(fallback)) continue; + const line = text.slice(0, m.index).split('\n').length; + if (exempt(lines, line - 1)) continue; + report(RULES.VAR_FALLBACK, file, line, m[0]); + } +} + +/* ── CSS 扫描 ─────────────────────────────────────────────────── */ +function scanCss(file, raw) { + const lines = raw.split('\n'); + // 注释掏空但保留换行,行号才不漂 + const text = raw.replace(/\/\*[\s\S]*?\*\//g, (c) => c.replace(/[^\n]/g, ' ')); + + for (const m of text.matchAll(/@media[^{]*prefers-color-scheme/g)) { + const line = text.slice(0, m.index).split('\n').length; + if (exempt(lines, line - 1)) continue; + report(RULES.MEDIA_SCHEME, file, line, m[0]); + } + + scanVarFallback(text, lines, file); + if (ALLOWED_FILES.has(file)) return; + + // 声明扫描:跟踪 selector 栈,`:root[data-theme` 块内允许写死(那就是深色覆盖块本身) + const selStack = []; + let buf = ''; + let pos = 0; + // 声明可能跨多行(多层 gradient)。行号一律报**起始行**,dark-ok 注释才好写在它正上方。 + let declStart = 0; + const flushDecl = () => { + const decl = buf.trim(); + const startIdx = declStart; + buf = ''; + if (!decl || !decl.includes(':')) return; + const ci = decl.indexOf(':'); + const prop = decl.slice(0, ci).trim().toLowerCase(); + const value = decl.slice(ci + 1); + const isVarDef = prop.startsWith('--'); + if (!isVarDef && !COLOR_PROPS.test(prop)) return; + if (!hasColorLiteral(value)) return; + // 深色覆盖块内部写死颜色是允许的(它只在 data-theme=dark 下生效) + if (selStack.some((s) => /\[data-theme/.test(s))) return; + const line = text.slice(0, startIdx).split('\n').length; + if (exempt(lines, line - 1)) return; + report(isVarDef ? RULES.CSS_VAR_DEF : RULES.CSS_LITERAL, file, line, decl); + }; + + for (; pos < text.length; pos++) { + const ch = text[pos]; + if (!buf.trim() && !/\s/.test(ch)) declStart = pos; + if (ch === '{') { + selStack.push(buf.trim()); + buf = ''; + } else if (ch === '}') { + flushDecl(); + selStack.pop(); + } else if (ch === ';') { + if (selStack.length > 0) flushDecl(); + else buf = ''; // 块外的 @import / @charset 之类,不是声明 + } else { + buf += ch; + } + } +} + +/* ── TS/TSX 扫描 ──────────────────────────────────────────────── */ +/** 从 `prop:` 后取一段值:到同层 `,` 或 `}` 为止,跨过嵌套括号与字符串。 */ +function readValue(src, start) { + let i = start; + let depth = 0; + let quote = ''; + for (; i < src.length; i++) { + const c = src[i]; + if (quote) { + if (c === '\\') { i++; continue; } + if (c === quote) quote = ''; + continue; + } + if (c === '"' || c === "'" || c === '`') { quote = c; continue; } + if ('([{'.includes(c)) { depth++; continue; } + if (')]}'.includes(c)) { + if (depth === 0) break; + depth--; + continue; + } + if (c === ',' && depth === 0) break; + if (c === '\n' && depth === 0 && /[,;}]\s*$/.test(src.slice(start, i))) break; + } + return src.slice(start, i); +} + +function scanJs(file, raw) { + if (ALLOWED_FILES.has(file)) return; + const lines = raw.split('\n'); + // 行注释/块注释掏空,避免注释里的示例色误报 + const text = raw + .replace(/\/\*[\s\S]*?\*\//g, (c) => c.replace(/[^\n]/g, ' ')) + .replace(/(^|[^:'"`\\])\/\/[^\n]*/g, (m, p1) => p1 + ' '.repeat(m.length - p1.length)); + + scanVarFallback(text, lines, file); + + // 对象字面量属性:{ color: …, background: … } + const propRe = /(?:^|[{,(\s])([A-Za-z]+)\s*:/g; + for (const m of text.matchAll(propRe)) { + const prop = m[1]; + if (!JS_COLOR_PROPS.has(prop)) continue; + const value = readValue(text, m.index + m[0].length); + if (!hasColorLiteral(value)) continue; + const line = text.slice(0, m.index).split('\n').length; + if (exempt(lines, line - 1)) continue; + report(RULES.JS_INLINE, file, line, `${prop}:${value}`); + } + + // JSX 属性: + const attrRe = /\b(fill|stroke|stopColor|color|bgColor)\s*=\s*(["'])([^"']*)\2/g; + for (const m of text.matchAll(attrRe)) { + if (!hasColorLiteral(m[3])) continue; + const line = text.slice(0, m.index).split('\n').length; + if (exempt(lines, line - 1)) continue; + report(RULES.JS_INLINE, file, line, m[0]); + } +} + +for (const [file, raw] of sources) { + if (file.endsWith('.css')) scanCss(file, raw); + else scanJs(file, raw); +} + +/* ── 棘轮比对 ─────────────────────────────────────────────────── */ +/** 基线按 file → rule → 计数聚合:行号会漂,快照文本又太脆,计数最稳。 + * 已知取舍:同一 file+rule 桶内「删一处、又新增一处」计数不变,门禁不会报—— + * 但那种改动必然出现在 diff 里、由 review 兜住,而棘轮要保证的「存量只减不增」不受影响。 + * 真要收严,可改成对 snippet 内容做哈希集合(对行号漂移免疫),代价是基线体积与噪声。 */ +function tally(list) { + const out = {}; + for (const f of list) { + (out[f.file] ||= {}); + out[f.file][f.rule] = (out[f.file][f.rule] || 0) + 1; + } + return out; +} + +const current = tally(findings); + +if (UPDATE) { + const sorted = {}; + for (const file of Object.keys(current).sort()) { + sorted[file] = Object.fromEntries(Object.entries(current[file]).sort()); + } + writeFileSync(baselinePath, `${JSON.stringify(sorted, null, 2)}\n`, 'utf-8'); + console.log(`[check-dark-mode] 基线已更新:${Object.keys(sorted).length} 个文件 / ${findings.length} 处存量`); + process.exit(0); +} + +const baseline = existsSync(baselinePath) ? JSON.parse(readFileSync(baselinePath, 'utf-8')) : {}; + +if (LIST) { + for (const f of findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)) { + console.log(`${f.file}:${f.line} [${f.rule}] ${f.snippet}`); + } + console.log(`\n合计 ${findings.length} 处`); +} + +/* 取 current 与 baseline 的键并集再逐条比:只走 current 会漏掉「已清零」的桶(它不再有键), + 只走 baseline 会漏掉新出现的文件。一次遍历同时得出超标与改善。 */ +const buckets = new Map(); // file → Set(rule);用嵌套结构而不是拼接字符串当键 +for (const source of [current, baseline]) { + for (const [file, rules] of Object.entries(source)) { + const set = buckets.get(file) ?? new Set(); + for (const rule of Object.keys(rules)) set.add(rule); + buckets.set(file, set); + } +} + +const regressions = []; +let improved = 0; +for (const [file, rules] of buckets) { + for (const rule of rules) { + const count = current[file]?.[rule] || 0; + const allowed = baseline[file]?.[rule] || 0; + if (count > allowed) regressions.push({ file, rule, count, allowed }); + else if (count < allowed) improved += allowed - count; + } +} + +if (regressions.length) { + console.error('\n[check-dark-mode] 深色模式门禁未通过——下列位置绕过了颜色令牌:\n'); + for (const r of regressions) { + console.error(` ${r.file} [${r.rule}] ${r.count} 处(基线允许 ${r.allowed} 处)`); + for (const f of findings.filter((x) => x.file === r.file && x.rule === r.rule).slice(0, 12)) { + console.error(` ${f.file}:${f.line} ${f.snippet}`); + } + } + const hints = FIX_HINTS.map((r) => ` ${r.id.padEnd(RULE_ID_WIDTH)} \u2192 ${r.fix}`).join('\n'); + console.error(` +\u4fee\u6cd5\uff08\u6309\u76f2\u533a\u5206\u7c7b\uff09\uff1a +${hints} +\u786e\u5b9e\u9700\u8981\u5199\u6b7b\u989c\u8272\uff08\u6697\u8272\u4ee3\u7801\u53f0\u3001\u54c1\u724c\u63d2\u753b\u3001\u6210\u5bf9\u4e3b\u9898\u5bf9\u8c61\uff09\u2192 \u8be5\u884c\u52a0 dark-ok: \u539f\u56e0 \u8c41\u514d\uff1b +\u6574\u6bb5\u6a21\u677f\u7528 dark-ok-begin / dark-ok-end \u5305\u8d77\u6765\u3002 +\u660e\u7ec6\uff1anpm run check:dark -- --list +`); + process.exit(1); +} + +if (improved > 0) { + console.log(`[check-dark-mode] 通过(存量比基线少 ${improved} 处,可跑 npm run check:dark -- --update 收紧基线)`); +} else { + console.log(`[check-dark-mode] 通过(存量 ${findings.length} 处,全部在基线内)`); +} diff --git a/src/frontend/scripts/dark-mode-baseline.json b/src/frontend/scripts/dark-mode-baseline.json new file mode 100644 index 00000000..90696954 --- /dev/null +++ b/src/frontend/scripts/dark-mode-baseline.json @@ -0,0 +1,262 @@ +{ + "src/AdminApp.tsx": { + "js-inline-color": 9 + }, + "src/ApiDocApp.tsx": { + "js-inline-color": 1 + }, + "src/ConfigApp.tsx": { + "js-inline-color": 1 + }, + "src/components/admin/AdminAgentManager.tsx": { + "js-inline-color": 1 + }, + "src/components/admin/AppConfigEditor.tsx": { + "js-inline-color": 1 + }, + "src/components/admin/DataSourceManager.tsx": { + "js-inline-color": 1 + }, + "src/components/admin/IconPicker.tsx": { + "js-inline-color": 2 + }, + "src/components/admin/KnowledgeBaseManager.tsx": { + "js-inline-color": 10 + }, + "src/components/admin/LoginView.tsx": { + "js-inline-color": 1 + }, + "src/components/admin/ManualEditor.tsx": { + "js-inline-color": 2 + }, + "src/components/admin/McpServersEditor.tsx": { + "js-inline-color": 4 + }, + "src/components/admin/ModelsEditor.tsx": { + "js-inline-color": 5 + }, + "src/components/admin/PageConfigEditor.tsx": { + "js-inline-color": 3 + }, + "src/components/admin/PluginsEditor.tsx": { + "js-inline-color": 2 + }, + "src/components/admin/PromptVersionsEditor.tsx": { + "js-inline-color": 1 + }, + "src/components/admin/PromptsEditor.tsx": { + "js-inline-color": 2 + }, + "src/components/admin/SandboxDepsManager.tsx": { + "js-inline-color": 6 + }, + "src/components/admin/ServiceConfigsEditor.tsx": { + "js-inline-color": 2 + }, + "src/components/admin/SkillsEditor.tsx": { + "js-inline-color": 4 + }, + "src/components/agent/AgentFormFields.tsx": { + "js-inline-color": 1 + }, + "src/components/agent/AgentMarketplaceModal.tsx": { + "js-inline-color": 1 + }, + "src/components/agent/AgentPanel.tsx": { + "js-inline-color": 1 + }, + "src/components/apidoc/ApiDocPanel.tsx": { + "js-inline-color": 6 + }, + "src/components/batch/BatchProgressPanel.tsx": { + "js-inline-color": 1 + }, + "src/components/catalog/CatalogPanel.tsx": { + "js-inline-color": 2 + }, + "src/components/catalog/LarkAppInitCard.tsx": { + "js-inline-color": 1 + }, + "src/components/catalog/McpPage.tsx": { + "js-inline-color": 2 + }, + "src/components/catalog/PluginIconPicker.tsx": { + "js-inline-color": 1 + }, + "src/components/catalog/PluginsPage.tsx": { + "js-inline-color": 11 + }, + "src/components/catalog/SkillMarketplaceModal.tsx": { + "js-inline-color": 1 + }, + "src/components/catalog/SkillsPage.tsx": { + "js-inline-color": 2 + }, + "src/components/chat/PromptHubPanel.tsx": { + "js-inline-color": 1 + }, + "src/components/common/DshIcons.tsx": { + "js-inline-color": 4 + }, + "src/components/common/MarketPickerModal.tsx": { + "js-inline-color": 1 + }, + "src/components/config/ChatHistoryPanel.tsx": { + "js-inline-color": 5 + }, + "src/components/config/ConfigLoginView.tsx": { + "js-inline-color": 1 + }, + "src/components/config/SandboxInstanceManager.tsx": { + "js-inline-color": 1 + }, + "src/components/config/SecurityAuditLogsPanel.tsx": { + "js-inline-color": 2 + }, + "src/components/config/SkillCallLogsPanel.tsx": { + "js-inline-color": 1 + }, + "src/components/config/SubAgentLogsPanel.tsx": { + "js-inline-color": 1 + }, + "src/components/config/SystemHealthPanel.tsx": { + "js-inline-color": 4 + }, + "src/components/config/UsageLogsPanel.tsx": { + "js-inline-color": 1 + }, + "src/components/kb/ConceptGraph.tsx": { + "js-inline-color": 2 + }, + "src/components/kb/ReindexModal.tsx": { + "js-inline-color": 1 + }, + "src/components/kb/wikiGraphTheme.ts": { + "js-inline-color": 14 + }, + "src/components/lab/AutomationDetailPage.tsx": { + "js-inline-color": 3 + }, + "src/components/loop/loop.css": { + "css-literal-color": 4 + }, + "src/components/memory/FactsList.tsx": { + "js-inline-color": 4 + }, + "src/components/onboarding/FirstRunSetup.tsx": { + "js-inline-color": 1 + }, + "src/components/ontology/OntologyManager.tsx": { + "js-inline-color": 1 + }, + "src/components/projects/ProjectMemoriesModal.tsx": { + "js-inline-color": 1 + }, + "src/components/settings/SettingsModal.tsx": { + "js-inline-color": 5 + }, + "src/components/share/ShareRecordsPage.tsx": { + "js-inline-color": 1 + }, + "src/components/sidebar/SearchModal.tsx": { + "js-inline-color": 1 + }, + "src/components/sidebar/Sidebar.tsx": { + "js-inline-color": 2 + }, + "src/components/tool/ToolOutputRenderer.tsx": { + "js-inline-color": 8 + }, + "src/components/tool/ToolResultPanel.tsx": { + "js-inline-color": 2 + }, + "src/components/tool/renderers/KBRenderer.tsx": { + "js-inline-color": 1 + }, + "src/components/tool/renderers/MySpaceRenderer.tsx": { + "js-inline-color": 4 + }, + "src/components/tool/renderers/SearchRenderer.tsx": { + "js-inline-color": 1 + }, + "src/edition-ee/myspace/ImageGrid.tsx": { + "js-inline-color": 2 + }, + "src/edition-ee/myspace/TeamScopeTree.tsx": { + "js-inline-color": 1 + }, + "src/edition-ee/settings/TeamsSection.tsx": { + "js-inline-color": 1 + }, + "src/stores/authStore.ts": { + "js-inline-color": 1 + }, + "src/styles/admin.css": { + "css-literal-color": 1 + }, + "src/styles/automation-timeline.css": { + "css-literal-color": 6 + }, + "src/styles/automation.css": { + "css-literal-color": 8 + }, + "src/styles/canvas.css": { + "css-literal-color": 16 + }, + "src/styles/catalog.css": { + "css-literal-color": 115 + }, + "src/styles/chat.css": { + "css-literal-color": 89 + }, + "src/styles/common.css": { + "css-literal-color": 8 + }, + "src/styles/evolution.css": { + "css-literal-color": 7 + }, + "src/styles/kb-wiki.css": { + "css-literal-color": 5 + }, + "src/styles/mcp.css": { + "css-literal-color": 3 + }, + "src/styles/mobile.css": { + "css-literal-color": 34, + "css-var-literal-color": 1 + }, + "src/styles/myspace.css": { + "css-literal-color": 37 + }, + "src/styles/onboarding.css": { + "css-literal-color": 35 + }, + "src/styles/ontology.css": { + "css-literal-color": 36 + }, + "src/styles/plan.css": { + "css-literal-color": 9 + }, + "src/styles/search-modal.css": { + "css-literal-color": 4 + }, + "src/styles/settings.css": { + "css-literal-color": 22 + }, + "src/styles/sidebar.css": { + "css-literal-color": 34 + }, + "src/styles/sites.css": { + "css-literal-color": 1 + }, + "src/styles/skill-distill.css": { + "css-literal-color": 2 + }, + "src/styles/team-folder.css": { + "css-literal-color": 10 + }, + "src/styles/tool.css": { + "css-literal-color": 47, + "css-var-literal-color": 5 + } +} diff --git a/src/frontend/src/ApiDocApp.tsx b/src/frontend/src/ApiDocApp.tsx index 28cacba0..376d5818 100644 --- a/src/frontend/src/ApiDocApp.tsx +++ b/src/frontend/src/ApiDocApp.tsx @@ -23,7 +23,7 @@ export default function ApiDocApp() { return (
- <ProfileOutlined style={{ marginRight: 8, color: '#126DFF' }} /> + <ProfileOutlined style={{ marginRight: 8, color: 'var(--color-primary)' }} /> {docTitle} @@ -69,7 +69,7 @@ export default function ApiDocApp() {
- +
diff --git a/src/frontend/src/components/apidoc/ApiDocPanel.tsx b/src/frontend/src/components/apidoc/ApiDocPanel.tsx index 2caede26..cd75c957 100644 --- a/src/frontend/src/components/apidoc/ApiDocPanel.tsx +++ b/src/frontend/src/components/apidoc/ApiDocPanel.tsx @@ -670,17 +670,17 @@ function AuthGuide() { return ( - + {t('接入指南 · 认证与调用约定')} ), children: ( -
+
所有 /v1/* 接口都需要身份认证。支持两种方式,二者皆无(或无效)时返回 401绝不会匿名放行。 @@ -867,7 +867,7 @@ export function ApiDocPanel({ onReloadRef }: ApiDocPanelProps = {}) { const components = spec?.components || {}; return ( -
+
{/* Access guide: authentication and call conventions */} @@ -878,7 +878,7 @@ export function ApiDocPanel({ onReloadRef }: ApiDocPanelProps = {}) { gap: 12, padding: '12px 16px', borderBottom: '1px solid #E3E6EA', - background: '#fff', + background: 'var(--color-bg-container)', flexShrink: 0, }}> {groups.map(g => ( @@ -943,7 +943,7 @@ export function ApiDocPanel({ onReloadRef }: ApiDocPanelProps = {}) { width: 420, borderRight: '1px solid #E3E6EA', overflow: 'auto', - background: '#fff', + background: 'var(--color-bg-container)', flexShrink: 0, }}> {groupEndpoints.map(ep => ( @@ -974,7 +974,7 @@ export function ApiDocPanel({ onReloadRef }: ApiDocPanelProps = {}) {
{/* Right: detail */} -
+
{activeEndpoint ? ( /* Detail-switch keyed enter: animation plays only once on the outer container */ diff --git a/src/frontend/src/components/batch/BatchProgressPanel.tsx b/src/frontend/src/components/batch/BatchProgressPanel.tsx index 5f3dbcfa..1155cb77 100644 --- a/src/frontend/src/components/batch/BatchProgressPanel.tsx +++ b/src/frontend/src/components/batch/BatchProgressPanel.tsx @@ -77,7 +77,7 @@ function BatchItemBubble({ item }: { {item.status === 'success' ? ( ) : ( - + )} {t('第 {n} 项', { n: item.index + 1 })} {item.retry_count > 0 && ( @@ -114,7 +114,7 @@ function BatchItemBubble({ item }: { )} ) : ( -
{item.error || t('执行失败')}
+
{item.error || t('执行失败')}
)} ); @@ -242,7 +242,7 @@ export function BatchProgressPanel({ planId }: Props) { {t('{done}/{total}(成功 {success},跳过 {failed})', { done, total, success, failed })} @@ -261,7 +261,7 @@ export function BatchProgressPanel({ planId }: Props) { /> {plan.errorMsg && ( -
+
{t('错误:{msg}', { msg: plan.errorMsg })}
)} diff --git a/src/frontend/src/components/canvas/UniverSpreadsheet.tsx b/src/frontend/src/components/canvas/UniverSpreadsheet.tsx index 7e5ef32d..429089f3 100644 --- a/src/frontend/src/components/canvas/UniverSpreadsheet.tsx +++ b/src/frontend/src/components/canvas/UniverSpreadsheet.tsx @@ -255,7 +255,7 @@ export const UniverSpreadsheet = forwardRef
{t('正在加载电子表格...')} diff --git a/src/frontend/src/components/catalog/CatalogPanel.tsx b/src/frontend/src/components/catalog/CatalogPanel.tsx index b9a81bf9..825ad666 100644 --- a/src/frontend/src/components/catalog/CatalogPanel.tsx +++ b/src/frontend/src/components/catalog/CatalogPanel.tsx @@ -1021,8 +1021,8 @@ export function CatalogPanel({ embedded = false }: CatalogPanelProps = {}) { {item.enabled ? t('已启用') : t('未启用')} @@ -1067,8 +1067,8 @@ export function CatalogPanel({ embedded = false }: CatalogPanelProps = {}) { {selectedItem.enabled ? t('已启用') : t('未启用')} @@ -1646,11 +1646,11 @@ export function CatalogPanel({ embedded = false }: CatalogPanelProps = {}) { style={{ flex: 1, fontSize: 12, - color: '#595959', + color: 'var(--color-text-secondary)', whiteSpace: 'pre-wrap', wordBreak: 'break-word', - background: '#FAFBFC', - border: '1px solid #EEF1F4', + background: 'var(--color-bg-gray)', + border: '1px solid var(--color-border)', borderRadius: 6, padding: '6px 8px', lineHeight: 1.6, diff --git a/src/frontend/src/components/catalog/LarkAppInitCard.tsx b/src/frontend/src/components/catalog/LarkAppInitCard.tsx index 94e3280d..836263a6 100644 --- a/src/frontend/src/components/catalog/LarkAppInitCard.tsx +++ b/src/frontend/src/components/catalog/LarkAppInitCard.tsx @@ -105,7 +105,7 @@ export function LarkAppInitCard() { {t('飞书应用配置二维码')} )} diff --git a/src/frontend/src/components/catalog/McpPage.tsx b/src/frontend/src/components/catalog/McpPage.tsx index fa27d3a2..9b91d3d9 100644 --- a/src/frontend/src/components/catalog/McpPage.tsx +++ b/src/frontend/src/components/catalog/McpPage.tsx @@ -253,8 +253,8 @@ export function McpPage({ embedded = false }: { embedded?: boolean }) { {selectedItem.name} {selectedItem.enabled ? t('已启用') : t('未启用')} @@ -352,13 +352,13 @@ export function McpPage({ embedded = false }: { embedded?: boolean }) { {item.name} {item.enabled ? t('已启用') : t('未启用')} {item.owner === 'self' && ( - {t('我的')} + {t('我的')} )} {item.owner === 'self' && (() => { const submission = submissionByServer.get(item.id); diff --git a/src/frontend/src/components/catalog/PluginIconPicker.tsx b/src/frontend/src/components/catalog/PluginIconPicker.tsx index d3831a7d..5fdcb74c 100644 --- a/src/frontend/src/components/catalog/PluginIconPicker.tsx +++ b/src/frontend/src/components/catalog/PluginIconPicker.tsx @@ -13,12 +13,12 @@ export function PluginAvatar({ icon, size = 36 }: { icon?: string | null; size?: if (value) { return ( + style={{ borderRadius: 8, objectFit: 'contain', display: 'block', background: 'var(--color-bg-container)' }} /> ); } return (
- +
); } diff --git a/src/frontend/src/components/catalog/PluginMarketplaceModal.tsx b/src/frontend/src/components/catalog/PluginMarketplaceModal.tsx index d0579625..875a687e 100644 --- a/src/frontend/src/components/catalog/PluginMarketplaceModal.tsx +++ b/src/frontend/src/components/catalog/PluginMarketplaceModal.tsx @@ -128,7 +128,7 @@ export function PluginMarketplaceModal({
- +
{plugin.name} diff --git a/src/frontend/src/components/catalog/PluginsPage.tsx b/src/frontend/src/components/catalog/PluginsPage.tsx index 3eadae33..4451a356 100644 --- a/src/frontend/src/components/catalog/PluginsPage.tsx +++ b/src/frontend/src/components/catalog/PluginsPage.tsx @@ -286,8 +286,8 @@ export function PluginsPage() {
{name} + ? { background: 'var(--color-primary-bg)', color: 'var(--color-primary)', border: 'none' } + : { background: 'var(--color-bg-gray)', color: 'var(--color-text-placeholder)', border: 'none' }}> {component.data.enabled ? t('已启用') : t('未启用')}
@@ -311,7 +311,7 @@ export function PluginsPage() { {sk.files.length > 0 && (

{t('附带文件')}

-
    +
      {sk.files.map((f) =>
    • {f}
    • )}
@@ -446,8 +446,8 @@ export function PluginsPage() { onClick={() => openComponent({ kind: 'skill', data: s })} tags={isInstalled && ( + ? { background: 'var(--color-primary-bg)', color: 'var(--color-primary)', border: 'none' } + : { background: 'var(--color-bg-gray)', color: 'var(--color-text-placeholder)', border: 'none' }) }}> {s.enabled ? t('已启用') : t('未启用')} )} @@ -683,7 +683,7 @@ function ImportReportView({ report }: { report: PluginImportReport }) { rows={(report.imported || []).map((x) => `${x.type === 'skill' ? t('技能') : 'MCP'}:${x.name}`)} /> } title={t('已适配(降级)')} rows={(report.adapted || []).map((x) => `${x.name}:${x.note || t('已装上但默认禁用')}`)} /> - } title={t('已丢弃(本平台不支持)')} + } title={t('已丢弃(本平台不支持)')} rows={(report.dropped || []).map((x) => `${x.type}/${x.name}:${x.reason}`)} />
); @@ -694,7 +694,7 @@ function ReportGroup({ icon, title, rows }: { icon: React.ReactNode; title: stri return (
{icon}{title}({rows.length})
-
    +
      {rows.map((r, i) =>
    • {r}
    • )}
diff --git a/src/frontend/src/components/catalog/SkillsPage.tsx b/src/frontend/src/components/catalog/SkillsPage.tsx index aa86cfdc..3a91f015 100644 --- a/src/frontend/src/components/catalog/SkillsPage.tsx +++ b/src/frontend/src/components/catalog/SkillsPage.tsx @@ -637,7 +637,7 @@ export function SkillsPage({ embedded = false }: { embedded?: boolean }) { {t('已启用')} )} {item.owner === 'self' && ( - {t('我的')} + {t('我的')} )} {item.owner === 'self' && (() => { const sub = subBySkill.get(item.id); diff --git a/src/frontend/src/components/catalog/skillIcons.tsx b/src/frontend/src/components/catalog/skillIcons.tsx index 2db5cb44..a5a4ea74 100644 --- a/src/frontend/src/components/catalog/skillIcons.tsx +++ b/src/frontend/src/components/catalog/skillIcons.tsx @@ -22,6 +22,9 @@ export interface PresetIcon { paths: ReactNode; } +/* dark-ok-begin: 下面是每类技能的**身份色**(一个类别一个固定色相,属于品牌资产), + 不是主题色。深浅两档的适配在 catalog.css 的 .jx-skillAvatar 规则里用 color-mix 完成, + 组件只把色值当自定义属性传下去。 */ export const PRESETS: Record = { doc: { key: 'doc', label: t('文档'), color: '#2F6BFF', paths: (<>) }, pen: { key: 'pen', label: t('写作'), color: '#7C4DFF', paths: (<>) }, @@ -66,6 +69,7 @@ export const PRESETS: Record = { shield_check: { key: 'shield_check', label: t('合规'), color: '#1E88E5', paths: (<>) }, bell: { key: 'bell', label: t('通知'), color: '#F57C00', paths: (<>) }, }; +/* dark-ok-end */ export const PRESET_LIST: PresetIcon[] = Object.values(PRESETS); @@ -92,13 +96,6 @@ function hashStr(s: string): number { return Math.abs(h); } -function tint(hex: string): string { - // Convert the accent color into a light background at 12% opacity - const n = hex.replace('#', ''); - const r = parseInt(n.slice(0, 2), 16), g = parseInt(n.slice(2, 4), 16), b = parseInt(n.slice(4, 6), 16); - return `rgba(${r},${g},${b},0.12)`; -} - function resolvePresetKey(icon: string | undefined, seed: string): string { if (icon && icon.startsWith('preset:')) { const k = icon.slice(7); @@ -121,8 +118,13 @@ export function SkillAvatar({ return ; } const p = PRESETS[resolvePresetKey(icon, seed || name || '?')]; + // 身份色只作为**输入**交给 CSS(自定义属性),底色与字色由 catalog.css 里的 + // .jx-skillAvatar 规则用 color-mix 推导——深色档在那儿统一提亮,组件不必知道当前主题。 return ( -
+
{p.paths}
); diff --git a/src/frontend/src/components/chat/ContextGauge.tsx b/src/frontend/src/components/chat/ContextGauge.tsx index 7ba6945a..a04857fe 100644 --- a/src/frontend/src/components/chat/ContextGauge.tsx +++ b/src/frontend/src/components/chat/ContextGauge.tsx @@ -23,12 +23,12 @@ function levelOf(ratio: number): Level { // Category → display label + accent colour (aligned with design tokens). const CATEGORIES: Array<{ key: keyof ContextBreakdown; label: string; color: string }> = [ - { key: 'messages', label: '对话消息', color: '#126DFF' }, - { key: 'tools', label: '工具调用', color: '#8B5CF6' }, - { key: 'thinking', label: '思考过程', color: '#02B589' }, - { key: 'files', label: '文件', color: '#F8AB42' }, - { key: 'input', label: '当前输入', color: '#22C55E' }, - { key: 'system', label: '系统提示词与工具定义', color: '#808080' }, + { key: 'messages', label: '对话消息', color: 'var(--color-primary)' }, + { key: 'tools', label: '工具调用', color: 'var(--color-tint-purple)' }, + { key: 'thinking', label: '思考过程', color: 'var(--color-success)' }, + { key: 'files', label: '文件', color: 'var(--color-warning)' }, + { key: 'input', label: '当前输入', color: 'var(--color-tint-green)' }, + { key: 'system', label: '系统提示词与工具定义', color: 'var(--color-text-tertiary)' }, ]; interface GaugeContentProps { diff --git a/src/frontend/src/components/common/MarketPickerModal.tsx b/src/frontend/src/components/common/MarketPickerModal.tsx index d4b511ca..d7a90437 100644 --- a/src/frontend/src/components/common/MarketPickerModal.tsx +++ b/src/frontend/src/components/common/MarketPickerModal.tsx @@ -76,7 +76,7 @@ export function MarketPickerModal({ > } + prefix={} placeholder={t('搜索名称、简介或标识…')} value={q} onChange={(e) => setQ(e.target.value)} @@ -104,12 +104,12 @@ export function MarketPickerModal({ }} >
-
+
{item.name || item.id} {already && {t('已添加')}}
@@ -130,7 +130,7 @@ export function MarketPickerModal({ ); })}
-
+
{t('选中的项会在保存模式时自动安装。')}
diff --git a/src/frontend/src/components/docs/AppCenterPanel.tsx b/src/frontend/src/components/docs/AppCenterPanel.tsx index 9e8e01d7..9d68cb74 100644 --- a/src/frontend/src/components/docs/AppCenterPanel.tsx +++ b/src/frontend/src/components/docs/AppCenterPanel.tsx @@ -114,7 +114,7 @@ export default function AppCenterPanel() { width: 28, height: 28, borderRadius: '50%', - background: '#E3E6EA', + background: 'var(--color-border)', }} /> )} diff --git a/src/frontend/src/components/loop/loop.css b/src/frontend/src/components/loop/loop.css index 0324233d..daaa8345 100644 --- a/src/frontend/src/components/loop/loop.css +++ b/src/frontend/src/components/loop/loop.css @@ -61,11 +61,11 @@ .loop-list-item.active { border-color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 6%, transparent); } .loop-title { font-weight: 600; font-size: 13px; } .loop-status { display: inline-block; font-size: 11px; padding: 1px 7px; border-radius: 10px; margin: 3px 0; background: var(--color-fill-hover); } -.loop-status.s-completed { background: #dcfce7; color: #166534; } -.loop-status.s-running { background: #dbeafe; color: #1e40af; } -.loop-status.s-budget_exhausted { background: #fef9c3; color: #854d0e; } -.loop-status.s-failed { background: #fee2e2; color: #991b1b; } -.loop-status.s-awaiting_human { background: #fae8ff; color: #86198f; } +.loop-status.s-completed { background: var(--color-tint-green-bg); color: var(--color-tint-green); } +.loop-status.s-running { background: var(--color-tint-blue-bg); color: var(--color-tint-blue); } +.loop-status.s-budget_exhausted { background: var(--color-tint-amber-bg); color: var(--color-tint-amber); } +.loop-status.s-failed { background: var(--color-tint-red-bg); color: var(--color-tint-red); } +.loop-status.s-awaiting_human { background: var(--color-tint-purple-bg); color: var(--color-tint-purple); } .loop-meta { font-size: 11px; color: var(--color-text-tertiary); } .loop-main { flex: 1; padding: 20px; overflow-y: auto; } @@ -73,15 +73,17 @@ .loop-header { display: flex; justify-content: space-between; align-items: center; } .loop-goal { background: var(--color-bg-layout); border-radius: 8px; padding: 10px 12px; margin: 10px 0; } .loop-budget { font-size: 12px; color: var(--color-text-secondary); margin-top: 4px; } +/* dark-ok: 事件流是刻意的暗色终端台,两档主题下都保持深底浅字 */ .loop-stream { background: #0d1117; color: #c9d1d9; border-radius: 8px; padding: 12px; font-family: ui-monospace, monospace; font-size: 12.5px; max-height: 300px; overflow-y: auto; } .loop-ev { padding: 2px 0; } -.loop-reason { color: #8b949e; padding-left: 16px; font-size: 12px; } -.loop-warn { color: #d29922; } -.loop-done { color: #3fb950; font-weight: 600; } -.loop-err { color: #f85149; } +/* 下面四条是 .loop-stream 暗色终端台内的语法色,跟随终端底色而非主题 */ +.loop-reason { color: #8b949e; padding-left: 16px; font-size: 12px; } /* dark-ok */ +.loop-warn { color: #d29922; } /* dark-ok */ +.loop-done { color: #3fb950; font-weight: 600; } /* dark-ok */ +.loop-err { color: #f85149; } /* dark-ok */ .loop-table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 12.5px; } .loop-table th, .loop-table td { border: 1px solid var(--color-border); padding: 5px 8px; text-align: left; } .loop-table th { background: var(--color-bg-layout); } @@ -196,9 +198,9 @@ padding: 1px 8px; border-radius: 20px; background: var(--color-primary-light); color: var(--color-primary); } -.loop-status-completed { background: #dcfce7; color: #16a34a; } -.loop-status-budget_exhausted { background: #fef3c7; color: #b45309; } -.loop-status-cancelled, .loop-status-failed { background: #fee2e2; color: #dc2626; } +.loop-status-completed { background: var(--color-tint-green-bg); color: var(--color-tint-green); } +.loop-status-budget_exhausted { background: var(--color-tint-amber-bg); color: var(--color-tint-amber); } +.loop-status-cancelled, .loop-status-failed { background: var(--color-tint-red-bg); color: var(--color-tint-red); } .loop-planbar-caret { flex-shrink: 0; color: var(--color-text-placeholder); font-size: 11px; } .loop-planbar-continue { flex-shrink: 0; border: none; cursor: pointer; white-space: nowrap; @@ -225,7 +227,7 @@ } .loop-planbar-mark { flex-shrink: 0; width: 14px; text-align: center; font-size: 12px; } .loop-planbar-desc { color: var(--color-text); } -.loop-item-done .loop-planbar-mark { color: #16a34a; } +.loop-item-done .loop-planbar-mark { color: var(--color-success); } .loop-item-done .loop-planbar-desc { color: var(--color-text-placeholder); text-decoration: line-through; } .loop-item-active .loop-planbar-mark { color: var(--color-primary); } .loop-item-active .loop-planbar-desc { font-weight: 600; } @@ -237,7 +239,7 @@ } @keyframes loop-review-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.45; } } .loop-planbar-hint { padding: 4px 12px 10px; color: var(--color-text-placeholder); } -.loop-status-interrupted { background: #fef3c7; color: #b45309; } +.loop-status-interrupted { background: var(--color-tint-amber-bg); color: var(--color-tint-amber); } .loop-planbar-steer { display: flex; align-items: center; gap: 6px; padding: 4px 12px 10px; } diff --git a/src/frontend/src/components/myspace/ImageGrid.tsx b/src/frontend/src/components/myspace/ImageGrid.tsx index 883b2526..f3bca78f 100644 --- a/src/frontend/src/components/myspace/ImageGrid.tsx +++ b/src/frontend/src/components/myspace/ImageGrid.tsx @@ -74,7 +74,7 @@ export function ImageGrid({ return (
onEnterFolder?.(folder.folder_id)}>
- +
{folder.name}
{menuItems.length > 0 && ( diff --git a/src/frontend/src/components/projects/ProjectCard.tsx b/src/frontend/src/components/projects/ProjectCard.tsx index 57ab61cc..a852e822 100644 --- a/src/frontend/src/components/projects/ProjectCard.tsx +++ b/src/frontend/src/components/projects/ProjectCard.tsx @@ -44,7 +44,7 @@ export default function ProjectCard({ project, onOpen, onToggleFavorite, stagger animate={project.favorite ? { scale: [1, 1.25, 1] } : { scale: 1 }} transition={{ duration: 0.35, ease: EASE.brandOut }} > - {project.favorite ? : } + {project.favorite ? : }
{project.description &&
{project.description}
} diff --git a/src/frontend/src/components/projects/ProjectDetailPanel.tsx b/src/frontend/src/components/projects/ProjectDetailPanel.tsx index 6c99ec35..7f858061 100644 --- a/src/frontend/src/components/projects/ProjectDetailPanel.tsx +++ b/src/frontend/src/components/projects/ProjectDetailPanel.tsx @@ -140,7 +140,7 @@ export default function ProjectDetailPanel({ projectId, onBack, handleFileSelect
{bot.status === 'error' && bot.last_error && ( -
{bot.last_error}
+
{bot.last_error}
)} {canObserve && (
diff --git a/src/frontend/src/components/settings/MyLogsPanel.tsx b/src/frontend/src/components/settings/MyLogsPanel.tsx index 3cd5fc9a..7a58dc4b 100644 --- a/src/frontend/src/components/settings/MyLogsPanel.tsx +++ b/src/frontend/src/components/settings/MyLogsPanel.tsx @@ -383,7 +383,7 @@ export function MyLogsPanel() { {stepItem.tool_calls_count ?? 0} {t('工具')} {stepItem.output_content && ( - + {stepItem.output_content.slice(0, 400)} {stepItem.output_content.length > 400 ? '…' : ''} diff --git a/src/frontend/src/components/settings/SettingsModal.tsx b/src/frontend/src/components/settings/SettingsModal.tsx index ed0aab41..6d759e00 100644 --- a/src/frontend/src/components/settings/SettingsModal.tsx +++ b/src/frontend/src/components/settings/SettingsModal.tsx @@ -1166,7 +1166,7 @@ function renderProfileTab(profile: import('../../types').MemoryProfile | null) {
- + {getShareExpiryLabel(record)}