From 8bc5b47914a73a61dacc29bcfc0a9f70a22e4d99 Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 30 Sep 2026 19:00:06 +0000 Subject: [PATCH 1/5] Apply Zai panel layout, mono primary buttons and type scale Why: Phase 1 of the approved frontend redesign: re-skin the shell to the Zai look (floating rounded panels on an inset base with 4px gutters), invert primary buttons to the mono scheme, and move every font size onto the --fs-ui-* scale. Also wires @ui/motion.css and replaces local keyframes with mk-* utilities. Evidence: npx tsc -b clean; npm test -> 38 passed (new host-side design-guard ratchet); npm run build succeeds. Combined Dev make test recorded in the Dev task card. Impact: styles.css fully re-tokenized (panel borders/radius, primary button color-mix hover, login brand panel in near-mono navy); inline fontSize literals in components replaced with scale tokens except one decorative glyph in the ratchet baseline. No component class names or behavior changed. Refs: Agent-System-Dev task feature/ui-tokens-v3; DESIGN.md (parent repo). --- package-lock.json | 18 ++ package.json | 1 + src/components/AgentTracker.tsx | 6 +- src/components/Conversation.tsx | 6 +- src/components/SettingsModal.tsx | 6 +- src/components/Workbench.tsx | 2 +- src/main.tsx | 1 + src/styles.css | 272 +++++++++++++++---------------- test/design-guard.test.ts | 80 +++++++++ tsconfig.json | 2 +- tsconfig.tsbuildinfo | 2 +- 11 files changed, 247 insertions(+), 149 deletions(-) create mode 100644 test/design-guard.test.ts diff --git a/package-lock.json b/package-lock.json index 3df7381..369f2e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "xlsx": "^0.18.5" }, "devDependencies": { + "@types/node": "^22.20.4", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^4.3.4", @@ -1292,6 +1293,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.20.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz", + "integrity": "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", @@ -2350,6 +2361,13 @@ "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", diff --git a/package.json b/package.json index 5b01060..9cdf907 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "xlsx": "^0.18.5" }, "devDependencies": { + "@types/node": "^22.20.4", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^4.3.4", diff --git a/src/components/AgentTracker.tsx b/src/components/AgentTracker.tsx index 1052836..868da36 100644 --- a/src/components/AgentTracker.tsx +++ b/src/components/AgentTracker.tsx @@ -51,7 +51,7 @@ export function AgentTracker() { onClick={() => setSelected(c.id)}> 🤖 {c.agentName || c.id} - {c.status} + {c.status} ))} @@ -59,11 +59,11 @@ export function AgentTracker() {
中间过程({focus === runId ? '主任务' : '子代理'})
- {focusMsgs.length === 0 &&
(无中间过程消息)
} + {focusMsgs.length === 0 &&
(无中间过程消息)
} {focusMsgs.map(m => (
{m.parts.map((p, i) => ( -
+
{p.type === 'text' && p.text} {p.type === 'thinking' && {p.text}} {p.type === 'tool_call' && 🛠 {p.toolCall?.name}} diff --git a/src/components/Conversation.tsx b/src/components/Conversation.tsx index 26da6ec..ce14cfa 100644 --- a/src/components/Conversation.tsx +++ b/src/components/Conversation.tsx @@ -184,7 +184,7 @@ function ContextObsView({ obs }: { obs: ContextObs }) { {obs.items.map((it, i) =>
  • {it}
  • )} {obs.truncations && obs.truncations.length > 0 && ( -
    +
    {obs.truncations.map((t, i) =>
    ⚠ {t}
    )}
    )} @@ -298,7 +298,7 @@ export function Conversation() {
    ⌘
    选择或创建一个会话开始
    -
    mock 演示模式可在 设置 → runtime 开启,无需模型密钥
    +
    mock 演示模式可在 设置 → runtime 开启,无需模型密钥
    ) @@ -365,7 +365,7 @@ export function Conversation() { {messages.length === 0 &&
    发送消息开始对话
    -
    mock 模式演示指令:#hitl · #agents · #doc · #error · 命令:/help
    +
    mock 模式演示指令:#hitl · #agents · #doc · #error · 命令:/help
    }
    diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 813821b..ea2740d 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -59,7 +59,7 @@ export function SettingsModal() { footer={saved ? ✓ 已保存 : 'Agent-System · 设置中心'} actions={section === 'settings' && schema.length > 0 ? ( <> - {saved && ✓ 已保存} + {saved && ✓ 已保存} 保存设置 ) : undefined} @@ -144,7 +144,7 @@ function ProfilesSection({ profiles }: { profiles: AgentProfile[] }) {
    {profiles.map(p => ( {p.name} {p.id}{p.builtin ? ' · 内置' : ''}} + label={<>{p.name} {p.id}{p.builtin ? ' · 内置' : ''}} desc={p.description}>
    { @@ -201,7 +201,7 @@ function PluginsSection() { store.refreshPlugins() store.toast('插件已热重载') }}>热重载 - 声明资产(工作流/意图/提示词/面板)热加载;装卸不影响核心 + 声明资产(工作流/意图/提示词/面板)热加载;装卸不影响核心
    {plugins.length === 0 && 暂无已装载插件
    (目录:常规 → 插件目录;示例见 Dev 仓 plugins/example)
    } {plugins.map(p => ( diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx index 2cb8c41..59ee487 100644 --- a/src/components/Workbench.tsx +++ b/src/components/Workbench.tsx @@ -121,7 +121,7 @@ export function AgentRow({ run, depth }: { run: Run; depth: number }) {
    store.openTracker(run.id)}> {label} - {run.status} + {run.status}
    ) } diff --git a/src/main.tsx b/src/main.tsx index 54e8577..58e2e09 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client' import { App } from './App' import { wirePreviewCapabilities } from './previewsetup' import '@ui/tokens.css' +import '@ui/motion.css' import '@ui/ui.css' import './styles.css' diff --git a/src/styles.css b/src/styles.css index ce2270a..1177ee8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,43 +1,44 @@ -/* 设计令牌与 UI 组件样式已迁入 Agent-System-UI(@ui/tokens.css、@ui/ui.css,main.tsx 引入)。 - R4 视觉收敛(2026-09-30):三级表面阶梯 + 发丝边框 + 圆角标尺 + 克制阴影 + 统一线性图标。 */ +/* 宿主样式(R5/Zai 复刻,2026-09-30;权威规则见父仓 DESIGN.md): + - 颜色/圆角/字号只消费 @ui/tokens.css 令牌(--fs-ui-* 标尺); + - 动效只消费 @ui/motion.css 的 mk-* 类(本文件不得出现 @keyframes,守护测试拦截); + - 布局面板化:三栏为浮在 --bg-inset 底座上的圆角面板,间隙 4px; */ * { box-sizing: border-box; } html, body, #root { height: 100%; margin: 0; } body { - font-family: var(--font); background: var(--bg); color: var(--text); - font-size: 13.5px; line-height: 1.55; + font-family: var(--font); background: var(--bg-inset); color: var(--text); + font-size: var(--fs-ui-base); line-height: 1.6; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; } button { font-family: inherit; font-size: inherit; color: inherit; } ::selection { background: var(--accent-soft-2); } :focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 2px; } -.app { display: flex; height: 100vh; overflow: hidden; } - -/* ---- 三栏布局:层次靠背景阶梯(bg-panel 侧栏 / bg 主区),边框只发丝勾边 ---- */ -.left-nav { background: var(--bg-panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; min-width: 0; flex-shrink: 0; transition: width .15s var(--ease); } +/* ---- 面板化三栏:独立圆角面板 + 4px 间隙(DESIGN.md §6) ---- */ +.app { display: flex; height: 100vh; overflow: hidden; gap: 4px; padding: 4px; background: var(--bg-inset); } +.left-nav { background: var(--bg-panel); border: 1px solid var(--border-soft); border-radius: var(--radius-lg); display: flex; flex-direction: column; min-width: 0; flex-shrink: 0; transition: width .15s var(--ease); } .left-nav.collapsed { width: 52px !important; } .divider { width: 4px; cursor: col-resize; background: transparent; flex-shrink: 0; position: relative; } -.divider::before { content: ''; position: absolute; left: 1.5px; top: 0; bottom: 0; width: 1px; background: transparent; transition: background .12s; } -.divider:hover::before, .divider.dragging::before { background: var(--accent); } +.divider::before { content: ''; position: absolute; left: 1px; top: 12px; bottom: 12px; width: 2px; border-radius: 2px; background: transparent; transition: background .12s; } +.divider:hover::before, .divider.dragging::before { background: var(--border-strong); } .divider::after { content: ''; position: absolute; inset: 0 -3px; } /* 拖拽热区加宽 */ -.main { flex: 1; min-width: 0; display: flex; flex-direction: column; background: var(--bg); } -.right-panel { background: var(--bg-panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; min-width: 0; } +.main { flex: 1; min-width: 0; display: flex; flex-direction: column; background: var(--bg); border: 1px solid var(--border-soft); border-radius: var(--radius-lg); } +.right-panel { background: var(--bg-panel); border: 1px solid var(--border-soft); border-radius: var(--radius-lg); display: flex; flex-direction: column; flex-shrink: 0; min-width: 0; } .right-panel.collapsed { width: 44px !important; } /* ---- 通用控件 ---- */ .btn { - background: var(--accent); color: var(--text-on-accent); border: none; - border-radius: var(--radius); padding: 8px 16px; cursor: pointer; - font-size: 13px; font-weight: 550; white-space: nowrap; - box-shadow: inset 0 1px 0 rgba(255,255,255,.14), 0 1px 2px rgba(8,9,12,.3); - transition: filter .12s var(--ease), transform .12s var(--ease); + background: var(--primary); color: var(--primary-foreground); border: none; + border-radius: var(--radius-sm); padding: 8px 16px; cursor: pointer; + font-size: var(--fs-ui-caption); font-weight: 550; white-space: nowrap; + box-shadow: 0 1px 2px rgba(0, 0, 0, .25); + transition: background .12s var(--ease), transform .12s var(--ease); } -.btn:hover:not(:disabled) { filter: brightness(1.09); } +.btn:hover:not(:disabled) { background: color-mix(in srgb, var(--primary) 86%, var(--bg)); } .btn:active:not(:disabled) { transform: translateY(1px); } .btn:disabled { opacity: .42; cursor: not-allowed; box-shadow: none; } .btn.ghost { background: var(--bg-raised); border: 1px solid var(--border-strong); color: var(--text-dim); box-shadow: none; } -.btn.ghost:hover:not(:disabled) { color: var(--text); border-color: var(--text-faint); filter: none; } +.btn.ghost:hover:not(:disabled) { color: var(--text); border-color: var(--text-faint); background: var(--bg-raised); } .btn.danger { background: var(--err); } .icon-btn { background: none; border: none; color: var(--text-faint); @@ -48,7 +49,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .icon-btn:hover { background: var(--bg-hover); color: var(--text); } .search-input, .cmd-menu input { background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius); - color: var(--text); padding: 7px 11px; font-family: inherit; font-size: 13px; width: 100%; + color: var(--text); padding: 7px 11px; font-family: inherit; font-size: var(--fs-ui-caption); width: 100%; transition: border-color .12s, box-shadow .12s; } .search-input:focus { border-color: var(--accent); box-shadow: var(--ring); outline: none; } @@ -57,24 +58,24 @@ button { font-family: inherit; font-size: inherit; color: inherit; } /* min-height:0 须随列向 flex 链下传(.main→.chat→.chat-body):flex 项的自动最小 高度是内容高度,缺失时消息会撑破 .chat——chat-body 永不滚动、输入栏被顶出视口。 */ .chat { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; } -.chat-header { padding: 12px 22px; border-bottom: 1px solid var(--border-soft); display: flex; align-items: center; gap: 10px; background: var(--bg); } -.chat-title { font-weight: 600; font-size: 14.5px; letter-spacing: -0.01em; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.chat-header { padding: 12px 22px; border-bottom: 1px solid var(--border-soft); display: flex; align-items: center; gap: 10px; background: var(--bg); border-radius: var(--radius-lg) var(--radius-lg) 0 0; } +.chat-title { font-weight: 600; font-size: var(--fs-ui-base); letter-spacing: -0.01em; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .chat-body { flex: 1; overflow-y: auto; min-height: 0; padding: 22px max(22px, 13%); } -.empty-state { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--text-faint); gap: 10px; font-size: 13.5px; } -.empty-state .es-mark { width: 44px; height: 44px; border-radius: 12px; background: var(--bg-raised); border: 1px solid var(--border-soft); display: flex; align-items: center; justify-content: center; color: var(--text-faint); margin-bottom: 4px; } +.empty-state { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--text-faint); gap: 10px; font-size: var(--fs-ui-caption); } +.empty-state .es-mark { width: 44px; height: 44px; border-radius: var(--radius-lg); background: var(--bg-raised); border: 1px solid var(--border-soft); display: flex; align-items: center; justify-content: center; color: var(--text-faint); margin-bottom: 4px; } .empty-state svg { opacity: .5; } .msg { margin-bottom: 20px; } -.msg-role { font-size: 12px; color: var(--text-faint); margin-bottom: 6px; display: flex; gap: 8px; align-items: center; } +.msg-role { font-size: var(--fs-ui-sm); color: var(--text-faint); margin-bottom: 6px; display: flex; gap: 8px; align-items: center; } .msg-role .role-mark { width: 18px; height: 18px; border-radius: 5px; display: inline-flex; align-items: center; justify-content: center; color: var(--text-dim); background: var(--bg-raised); border: 1px solid var(--border); } .msg.user .bubble { background: var(--accent-soft); border: 1px solid transparent; border-radius: var(--radius-lg); padding: 11px 16px; white-space: pre-wrap; } .part { margin: 4px 0; } -.part-thinking { color: var(--text-dim); font-size: 12.5px; border-left: 2px solid var(--border); padding-left: 10px; margin: 4px 0; } +.part-thinking { color: var(--text-dim); font-size: var(--fs-ui-sm); border-left: 2px solid var(--border); padding-left: 10px; margin: 4px 0; } .part-thinking summary { cursor: pointer; color: var(--text-faint); user-select: none; } .part-thinking .part-thinking-body { white-space: pre-wrap; margin-top: 4px; } -.part-tool { font-size: 12.5px; } +.part-tool { font-size: var(--fs-ui-sm); } .tool-chip { display: inline-flex; gap: 6px; align-items: center; background: var(--bg-inset); border: 1px solid var(--border-soft); border-radius: var(--radius-sm); padding: 4px 9px; margin: 2px 4px 2px 0; font-family: var(--mono); } .tool-chip .status-ok { color: var(--ok); } .tool-chip .status-error { color: var(--err); } @@ -91,8 +92,8 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .part-text :is(ul,ol) { margin: 0.4em 0; padding-left: 1.6em; } .part-text li { margin: 0.15em 0; } .part-text li > p { margin: 0.2em 0; } -.part-text pre { background: var(--bg-inset); border: 1px solid var(--border-soft); border-radius: var(--radius); padding: 11px 13px; overflow-x: auto; font-family: var(--mono); font-size: 12.5px; } -.part-text code { font-family: var(--mono); background: var(--bg-raised); border-radius: var(--radius-xs); padding: 1px 5px; font-size: 12.5px; } +.part-text pre { background: var(--bg-inset); border: 1px solid var(--border-soft); border-radius: var(--radius); padding: 11px 13px; overflow-x: auto; font-family: var(--mono); font-size: var(--fs-ui-sm); } +.part-text code { font-family: var(--mono); background: var(--bg-raised); border-radius: var(--radius-xs); padding: 1px 5px; font-size: var(--fs-ui-sm); } .part-text pre code { background: none; padding: 0; } .part-text blockquote { margin: 0.5em 0; padding: 0.1em 0 0.1em 10px; border-left: 3px solid var(--border-strong); color: var(--text-dim); } .part-text table { border-collapse: collapse; margin: 0.6em 0; font-size: 0.95em; display: block; max-width: 100%; overflow-x: auto; } @@ -101,7 +102,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .part-text hr { border: none; border-top: 1px solid var(--border); margin: 0.8em 0; } .part-text a { color: var(--accent); } .part-error { color: var(--err); background: var(--err-soft); border: 1px solid var(--err); border-radius: var(--radius-sm); padding: 8px 12px; } -.artifact-chip { display: inline-flex; align-items: stretch; background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; font-size: 13px; margin: 2px 0; } +.artifact-chip { display: inline-flex; align-items: stretch; background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; font-size: var(--fs-ui-caption); margin: 2px 0; } .artifact-chip .ac-main { display: inline-flex; align-items: center; gap: 7px; color: var(--accent); background: none; border: none; padding: 5px 10px; cursor: pointer; font: inherit; max-width: 340px; } .artifact-chip .ac-main span, .artifact-chip .ac-main { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .artifact-chip:hover { border-color: var(--accent); } @@ -112,16 +113,15 @@ button { font-family: inherit; font-size: inherit; color: inherit; } /* 过程折叠(run 终态后) */ .process-collapse { border: 1px solid var(--border-soft); border-radius: var(--radius); background: var(--bg-panel); margin: 6px 0; } -.process-collapse summary { list-style: none; cursor: pointer; padding: 7px 12px; color: var(--text-faint); font-size: 12.5px; display: flex; align-items: center; gap: 8px; user-select: none; } -.process-collapse summary::before { content: '▸'; transition: transform .12s; font-size: 10px; } +.process-collapse summary { list-style: none; cursor: pointer; padding: 7px 12px; color: var(--text-faint); font-size: var(--fs-ui-sm); display: flex; align-items: center; gap: 8px; user-select: none; } +.process-collapse summary::before { content: '▸'; transition: transform .12s; font-size: var(--fs-ui-xs); } .process-collapse[open] summary::before { transform: rotate(90deg); } .process-collapse .body { padding: 4px 12px 10px; } -/* 流式状态行 */ -.live-bar { display: flex; align-items: center; gap: 8px; color: var(--text-faint); font-size: 12.5px; margin-bottom: 6px; } -.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: pulse 1.2s infinite; } -@keyframes pulse { 0%,100% { opacity: .4 } 50% { opacity: 1 } } -.cursor-blink::after { content: '▍'; animation: pulse 1s infinite; color: var(--accent); } +/* 流式状态行(呼吸/扫光走 mk-* 类) */ +.live-bar { display: flex; align-items: center; gap: 8px; color: var(--text-faint); font-size: var(--fs-ui-sm); margin-bottom: 6px; } +.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: mk-breathe 1.2s infinite; } +.cursor-blink::after { content: '▍'; animation: mk-breathe 1s infinite; color: var(--accent); } /* HITL 卡片(确认类交互统一绿色语言) */ .hitl-card { border: 1px solid var(--ok-border); background: var(--ok-soft); border-radius: var(--radius-lg); padding: 14px; margin: 10px 0; } @@ -131,31 +131,31 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .hitl-choice { text-align: left; border: 1px solid var(--border); background: var(--bg-raised); color: var(--text); border-radius: var(--radius); padding: 10px 12px; cursor: pointer; transition: border-color .12s, background .12s; } .hitl-choice:hover { border-color: var(--accent); background: var(--accent-soft); } .hitl-choice .label { font-weight: 600; display: flex; gap: 8px; align-items: center; } -.hitl-choice .desc { color: var(--text-dim); font-size: 12.5px; margin-top: 3px; } -.hitl-choice .impact { color: var(--warn); font-size: 12px; margin-top: 3px; } -.hitl-choice .rec { font-size: 10.5px; color: var(--accent); border: 1px solid var(--accent); padding: 0 5px; border-radius: var(--radius-full); } +.hitl-choice .desc { color: var(--text-dim); font-size: var(--fs-ui-sm); margin-top: 3px; } +.hitl-choice .impact { color: var(--warn); font-size: var(--fs-ui-sm); margin-top: 3px; } +.hitl-choice .rec { font-size: var(--fs-ui-xs); color: var(--accent); border: 1px solid var(--accent); padding: 0 5px; border-radius: var(--radius-full); } .hitl-free { display: flex; gap: 8px; margin-top: 10px; } -/* 输入区:悬浮卡片式输入壳(外圆角大、聚焦发光) */ -.input-bar { padding: 8px max(16px, 6%) 16px; background: var(--bg); } +/* 输入区:悬浮卡片式输入壳(16px 大圆角 = DESIGN.md 特批例外) */ +.input-bar { padding: 8px max(16px, 6%) 16px; background: var(--bg); border-radius: 0 0 var(--radius-lg) var(--radius-lg); } .attachments { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; } -.attach-chip { display: inline-flex; gap: 6px; align-items: center; background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 3px 8px; font-size: 12px; } +.attach-chip { display: inline-flex; gap: 6px; align-items: center; background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 3px 8px; font-size: var(--fs-ui-sm); } .input-row { display: flex; gap: 8px; align-items: flex-end; background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 8px 8px 8px 14px; box-shadow: var(--shadow-md); transition: border-color .12s, box-shadow .12s; } .input-row:focus-within { border-color: var(--accent); box-shadow: var(--shadow-md), var(--ring); } -.input-row textarea { flex: 1; resize: none; background: transparent; border: none; color: var(--text); padding: 6px 0; font-family: inherit; font-size: 13.5px; max-height: 160px; outline: none; } +.input-row textarea { flex: 1; resize: none; background: transparent; border: none; color: var(--text); padding: 6px 0; font-family: inherit; font-size: var(--fs-ui-caption); max-height: 160px; outline: none; } .input-row textarea::placeholder { color: var(--text-faint); } .cmd-menu { border: 1px solid var(--border); background: var(--bg-raised); border-radius: var(--radius); margin-bottom: 6px; max-height: 180px; overflow-y: auto; box-shadow: var(--shadow-md); } -.cmd-item { padding: 7px 12px; cursor: pointer; display: flex; gap: 10px; font-size: 13px; } +.cmd-item { padding: 7px 12px; cursor: pointer; display: flex; gap: 10px; font-size: var(--fs-ui-caption); } .cmd-item:hover, .cmd-item.active { background: var(--bg-hover); } .cmd-item .name { color: var(--accent); font-family: var(--mono); } /* ---- 右栏 ---- */ .tabs { display: flex; border-bottom: 1px solid var(--border-soft); overflow-x: auto; padding: 0 8px; } -.tab { background: none; border: none; color: var(--text-faint); padding: 10px 10px; cursor: pointer; font-size: 12.5px; font-weight: 550; border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap; } +.tab { background: none; border: none; color: var(--text-faint); padding: 10px 10px; cursor: pointer; font-size: var(--fs-ui-sm); font-weight: 550; border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap; } .tab:hover { color: var(--text-dim); } .tab.active { color: var(--text); border-bottom-color: var(--accent); } .tab-body { flex: 1; overflow-y: auto; padding: 12px; } -.step { display: flex; gap: 8px; align-items: flex-start; padding: 6px 4px; font-size: 13px; } +.step { display: flex; gap: 8px; align-items: flex-start; padding: 6px 4px; font-size: var(--fs-ui-caption); } .step-icon { width: 16px; text-align: center; flex-shrink: 0; } .step.pending .step-icon { color: var(--text-faint); } .step.in_progress .step-icon { color: var(--accent); } @@ -164,8 +164,8 @@ button { font-family: inherit; font-size: inherit; color: inherit; } /* ---- 工件面板(工作区文件管理)---- */ .artifacts-panel { display: flex; flex-direction: column; gap: 4px; } .ap-toolbar { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; } -.ap-toolbar .search-input { flex: 1; min-width: 120px; height: 30px; font-size: 12.5px; } -.kind-pill { border: 1px solid var(--border); background: none; color: var(--text-dim); border-radius: var(--radius-full); padding: 3px 10px; font-size: 12px; cursor: pointer; } +.ap-toolbar .search-input { flex: 1; min-width: 120px; height: 30px; font-size: var(--fs-ui-sm); } +.kind-pill { border: 1px solid var(--border); background: none; color: var(--text-dim); border-radius: var(--radius-full); padding: 3px 10px; font-size: var(--fs-ui-sm); cursor: pointer; } .kind-pill:hover { color: var(--text); background: var(--bg-hover); } .kind-pill.on { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); } .ap-upload { cursor: pointer; } @@ -173,30 +173,28 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .artifact-row:hover { background: var(--bg-hover); } .artifact-row .ar-icon { display: flex; color: var(--text-dim); cursor: pointer; flex: none; } .artifact-row .ar-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; cursor: pointer; } -.artifact-row .name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; } -.artifact-row .meta { font-size: 11px; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.artifact-row .name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: var(--fs-ui-caption); } +.artifact-row .meta { font-size: var(--fs-ui-xs); color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .artifact-row .ar-acts { display: none; align-items: center; gap: 2px; flex: none; } .artifact-row:hover .ar-acts { display: inline-flex; } .artifact-row .ar-acts .icon-btn { width: 26px; height: 26px; } .artifact-row .ar-acts .icon-btn.danger { color: var(--err); } -.empty-note { color: var(--text-faint); font-size: 12.5px; padding: 14px 6px; } -.agent-row { display: flex; align-items: center; gap: 8px; padding: 8px; border-radius: var(--radius-sm); cursor: pointer; font-size: 13px; } +.empty-note { color: var(--text-faint); font-size: var(--fs-ui-sm); padding: 14px 6px; } +.agent-row { display: flex; align-items: center; gap: 8px; padding: 8px; border-radius: var(--radius-sm); cursor: pointer; font-size: var(--fs-ui-caption); } .agent-row:hover { background: var(--bg-hover); } .status-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } -.status-running { background: var(--accent); animation: pulse 1.2s infinite; } +.status-running { background: var(--accent); animation: mk-breathe 1.2s infinite; } .status-completed { background: var(--ok); } .status-failed, .status-cancelled, .status-interrupted { background: var(--err); } .status-pending, .status-waiting_hitl { background: var(--warn); } -/* ---- 弹窗 / 模态 ---- */ -.overlay { position: fixed; inset: 0; background: rgba(8, 9, 12, .6); backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; z-index: 50; animation: overlay-in .14s var(--ease); } -@keyframes overlay-in { from { opacity: 0 } } -.modal { background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius-xl); width: min(860px, 92vw); max-height: 86vh; display: flex; flex-direction: column; box-shadow: var(--shadow-lg); animation: modal-in .16s var(--ease); } -@keyframes modal-in { from { opacity: 0; transform: translateY(6px) scale(.99) } } -.modal-header { padding: 16px 20px; border-bottom: 1px solid var(--border-soft); display: flex; align-items: center; font-weight: 600; font-size: 14.5px; letter-spacing: -0.01em; } +/* ---- 弹窗 / 模态(动画走 mk-* 类) ---- */ +.overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, .55); backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; z-index: 50; animation: mk-overlay-in .14s var(--ease); } +.modal { background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius-xl); width: min(860px, 92vw); max-height: 86vh; display: flex; flex-direction: column; box-shadow: var(--shadow-lg); animation: mk-modal-in .16s var(--ease); } +.modal-header { padding: 16px 20px; border-bottom: 1px solid var(--border-soft); display: flex; align-items: center; font-weight: 600; font-size: var(--fs-ui-base); letter-spacing: -0.01em; } .modal-header .spacer { flex: 1; } .modal-body { flex: 1; overflow-y: auto; padding: 18px 20px; } -.subagent-tree { font-size: 13px; } +.subagent-tree { font-size: var(--fs-ui-caption); } .subagent-node { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px; margin-bottom: 8px; background: var(--bg-panel); } .subagent-node .head { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; } @@ -205,7 +203,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .ui-setting-control input, .ui-setting-control select { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--text); padding: 7px 10px; font-family: inherit; width: 100%; max-width: 340px; } .ui-setting-control input:focus, .ui-setting-control select:focus { border-color: var(--accent); box-shadow: var(--ring); outline: none; } .ui-setting-control input[type="checkbox"] { width: auto; } -.effect-badge { font-size: 10.5px; padding: 2px 7px; border-radius: var(--radius-full); border: 1px solid var(--border); color: var(--text-faint); white-space: nowrap; } +.effect-badge { font-size: var(--fs-ui-xs); padding: 2px 7px; border-radius: var(--radius-full); border: 1px solid var(--border); color: var(--text-faint); white-space: nowrap; } .effect-hot { color: var(--ok); border-color: var(--ok); } .effect-new-run { color: var(--warn); border-color: var(--warn); } @@ -213,16 +211,16 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .docv { display: flex; flex-direction: column; min-height: 0; } .docv-toolbar { display: flex; align-items: center; gap: 8px; padding: 0 0 10px; border-bottom: 1px solid var(--border-soft); margin-bottom: 10px; } .docv-toolbar .spacer { flex: 1; } -.docv-meta { font-size: 11.5px; color: var(--text-faint); text-transform: capitalize; } -.docv-dirty { color: var(--warn); font-size: 12px; } +.docv-meta { font-size: var(--fs-ui-xs); color: var(--text-faint); text-transform: capitalize; } +.docv-dirty { color: var(--warn); font-size: var(--fs-ui-sm); } .docv-dl { text-decoration: none; } .docv-body { flex: 1; min-height: 0; overflow: auto; } .docv-pdf { width: 100%; height: 100%; min-height: 60vh; border: 1px solid var(--border-soft); border-radius: var(--radius); background: var(--bg); } -.docv-editor { width: 100%; height: 56vh; font-family: var(--mono); font-size: 12.5px; resize: vertical; } -.docv-pre { font-family: var(--mono); font-size: 12.5px; white-space: pre-wrap; word-break: break-word; background: var(--bg-inset); border: 1px solid var(--border-soft); border-radius: var(--radius); padding: 12px 14px; } +.docv-editor { width: 100%; height: 56vh; font-family: var(--mono); font-size: var(--fs-ui-sm); resize: vertical; } +.docv-pre { font-family: var(--mono); font-size: var(--fs-ui-sm); white-space: pre-wrap; word-break: break-word; background: var(--bg-inset); border: 1px solid var(--border-soft); border-radius: var(--radius); padding: 12px 14px; } /* ---- 杂项 ---- */ -.toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: var(--bg-raised); border: 1px solid var(--border); color: var(--text); padding: 9px 16px; border-radius: var(--radius-full); box-shadow: var(--shadow-lg); z-index: 90; font-size: 13px; } +.toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: var(--bg-raised); border: 1px solid var(--border); color: var(--text); padding: 9px 16px; border-radius: var(--radius-full); box-shadow: var(--shadow-lg); z-index: 90; font-size: var(--fs-ui-caption); } .conn-dot { width: 8px; height: 8px; border-radius: 50%; } .conn-dot.on { background: var(--ok); } .conn-dot.off { background: var(--err); } @@ -232,49 +230,49 @@ button { font-family: inherit; font-size: inherit; color: inherit; } ::-webkit-scrollbar-thumb:hover { background: var(--border-strong); background-clip: content-box; border: 3px solid transparent; } ::-webkit-scrollbar-track { background: transparent; } -/* ---- 品牌标(侧栏 / 登录页 / 头像共用的星芒渐变底) ---- */ -.brand-mark { background: linear-gradient(135deg, #6d8dff, #9d7bff); color: #fff; display: inline-flex; align-items: center; justify-content: center; flex: none; box-shadow: inset 0 1px 0 rgba(255,255,255,.22); } +/* ---- 品牌标(侧栏 / 登录页 / 头像共用的星芒渐变底;辅色为品牌令牌 --brand-2) ---- */ +.brand-mark { background: linear-gradient(135deg, var(--accent), var(--brand-2)); color: var(--text-on-accent); display: inline-flex; align-items: center; justify-content: center; flex: none; box-shadow: inset 0 1px 0 rgba(255,255,255,.22); } -/* ---- 左栏:任务中心(方案 A;R4 重造型) ---- */ +/* ---- 左栏:任务中心(方案 A;Zai 复刻重造型) ---- */ .tsb-brand { display: flex; align-items: center; gap: 9px; padding: 14px 14px 8px; } -.tsb-logo { width: 24px; height: 24px; border-radius: 8px; background: linear-gradient(135deg, #6d8dff, #9d7bff); color: #fff; font-size: 12px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex: none; } -.tsb-name { font-weight: 650; font-size: 13.5px; letter-spacing: -0.01em; } -.tsb-iconbtn { width: 26px; height: 26px; border-radius: var(--radius-sm); background: none; border: none; color: var(--text-faint); cursor: pointer; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; position: relative; } +.tsb-logo { width: 24px; height: 24px; border-radius: var(--radius); background: linear-gradient(135deg, var(--accent), var(--brand-2)); color: var(--text-on-accent); font-size: var(--fs-ui-sm); font-weight: 700; display: flex; align-items: center; justify-content: center; flex: none; } +.tsb-name { font-weight: 650; font-size: var(--fs-ui-caption); letter-spacing: -0.01em; } +.tsb-iconbtn { width: 26px; height: 26px; border-radius: var(--radius-sm); background: none; border: none; color: var(--text-faint); cursor: pointer; font-size: var(--fs-ui-caption); display: inline-flex; align-items: center; justify-content: center; position: relative; } .tsb-iconbtn:hover { background: var(--bg-hover); color: var(--text); } .tsb-iconbtn.active { color: var(--accent); background: var(--accent-soft); } .tsb-actions { padding: 6px 12px 8px; display: flex; flex-direction: column; gap: 7px; } -.tsb-newtask { display: flex; height: 36px; border-radius: var(--radius); background: var(--accent); color: var(--text-on-accent); overflow: hidden; box-shadow: inset 0 1px 0 rgba(255,255,255,.16), 0 2px 6px rgba(8,9,12,.28); transition: filter .12s; } -.tsb-newtask:hover { filter: brightness(1.08); } -.tsb-newtask-main { flex: 1; display: flex; align-items: center; gap: 7px; padding: 0 11px; background: none; border: none; color: inherit; font-weight: 600; font-size: 13px; cursor: pointer; min-width: 0; } +.tsb-newtask { display: flex; height: 36px; border-radius: var(--radius); background: var(--primary); color: var(--primary-foreground); overflow: hidden; box-shadow: 0 1px 3px rgba(0, 0, 0, .3); transition: background .12s; } +.tsb-newtask:hover { background: color-mix(in srgb, var(--primary) 86%, var(--bg)); } +.tsb-newtask-main { flex: 1; display: flex; align-items: center; gap: 7px; padding: 0 11px; background: none; border: none; color: inherit; font-weight: 600; font-size: var(--fs-ui-caption); cursor: pointer; min-width: 0; } .tsb-newtask-main svg { flex: none; } -.tsb-newtask-proj { font-weight: 450; font-size: 11.5px; opacity: .75; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.tsb-newtask-caret { width: 30px; border: none; border-left: 1px solid rgba(255,255,255,.18); background: none; color: inherit; cursor: pointer; display: flex; align-items: center; justify-content: center; } -.tsb-newtask-caret:hover { background: rgba(255,255,255,.08); } +.tsb-newtask-proj { font-weight: 450; font-size: var(--fs-ui-xs); opacity: .75; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.tsb-newtask-caret { width: 30px; border: none; border-left: 1px solid color-mix(in srgb, var(--primary-foreground) 18%, transparent); background: none; color: inherit; cursor: pointer; display: flex; align-items: center; justify-content: center; } +.tsb-newtask-caret:hover { background: color-mix(in srgb, var(--primary-foreground) 8%, transparent); } .tsb-search { display: flex; align-items: center; gap: 7px; height: 32px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg-raised); color: var(--text-faint); padding: 0 10px; transition: border-color .12s, box-shadow .12s; } .tsb-search:focus-within { border-color: var(--accent); box-shadow: var(--ring); } .tsb-search svg { flex: none; } -.tsb-search input { flex: 1; border: none; background: none; color: var(--text); font-size: 12.5px; outline: none; min-width: 0; } +.tsb-search input { flex: 1; border: none; background: none; color: var(--text); font-size: var(--fs-ui-sm); outline: none; min-width: 0; } .tsb-search input::placeholder { color: var(--text-faint); } .tsb-orgrow { display: flex; align-items: center; gap: 6px; padding: 0 12px 8px; } .tsb-pills { flex: 1; display: flex; background: var(--bg-inset); border: 1px solid var(--border-soft); border-radius: var(--radius-sm); padding: 2px; gap: 2px; } -.tsb-pill { flex: 1; height: 22px; border: none; border-radius: var(--radius-xs); background: none; color: var(--text-faint); font-size: 12px; cursor: pointer; transition: all .12s; } +.tsb-pill { flex: 1; height: 22px; border: none; border-radius: var(--radius-xs); background: none; color: var(--text-faint); font-size: var(--fs-ui-sm); cursor: pointer; transition: all .12s; } .tsb-pill:hover { color: var(--text-dim); } -.tsb-pill.on { background: var(--bg-raised); color: var(--text); font-weight: 550; box-shadow: 0 1px 2px rgba(8,9,12,.25); } +.tsb-pill.on { background: var(--bg-raised); color: var(--text); font-weight: 550; box-shadow: 0 1px 2px rgba(0,0,0,.25); } .tsb-scroll { flex: 1; overflow-y: auto; padding: 0 10px 8px; min-height: 0; } -.tsb-seclabel { font-size: 11px; color: var(--text-faint); padding: 12px 8px 4px; letter-spacing: .05em; font-weight: 600; } -.tsb-empty { padding: 20px 14px; color: var(--text-faint); font-size: 12.5px; line-height: 1.9; text-align: center; } +.tsb-seclabel { font-size: var(--fs-ui-xs); color: var(--text-faint); padding: 12px 8px 4px; letter-spacing: .05em; font-weight: 600; } +.tsb-empty { padding: 20px 14px; color: var(--text-faint); font-size: var(--fs-ui-sm); line-height: 1.9; text-align: center; } .tsb-newproject { padding: 0 2px 8px; } .tsb-group { margin-bottom: 2px; } -.tsb-group-head { display: flex; align-items: center; gap: 6px; height: 30px; padding: 0 6px 0 4px; border-radius: var(--radius-sm); color: var(--text-dim); font-weight: 550; font-size: 13px; cursor: pointer; user-select: none; } +.tsb-group-head { display: flex; align-items: center; gap: 6px; height: 30px; padding: 0 6px 0 4px; border-radius: var(--radius-sm); color: var(--text-dim); font-weight: 550; font-size: var(--fs-ui-caption); cursor: pointer; user-select: none; } .tsb-group-head:hover { background: var(--bg-hover); } .tsb-group.closed .tsb-chev { transform: rotate(0deg); } .tsb-chev { display: inline-flex; color: var(--text-faint); transform: rotate(90deg); transition: transform .12s; } .tsb-group-name { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.tsb-group-cnt { font-size: 11px; color: var(--text-faint); font-weight: 400; font-variant-numeric: tabular-nums; } +.tsb-group-cnt { font-size: var(--fs-ui-xs); color: var(--text-faint); font-weight: 400; font-variant-numeric: tabular-nums; } .tsb-group-add { visibility: hidden; width: 22px; height: 22px; border: none; border-radius: var(--radius-xs); background: none; color: var(--text-faint); display: inline-flex; align-items: center; justify-content: center; cursor: pointer; } .tsb-group-head:hover .tsb-group-add { visibility: visible; } .tsb-group-add:hover { background: var(--bg-active); color: var(--text); } @@ -284,9 +282,9 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .tsb-task.active { background: var(--bg-active); color: var(--text); } .tsb-task.active::before { content: ''; position: absolute; left: -2px; top: 6px; bottom: 6px; width: 2.5px; border-radius: 2px; background: var(--accent); } .tsb-task.archived .tsb-task-t { text-decoration: line-through; opacity: .6; } -.tsb-task-t { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; } -.tsb-task-tm { font-size: 11px; color: var(--text-faint); font-variant-numeric: tabular-nums; } -.tsb-pbadge { font-size: 10.5px; color: var(--text-faint); border: 1px solid var(--border); border-radius: var(--radius-xs); padding: 0 5px; line-height: 1.5; white-space: nowrap; } +.tsb-task-t { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: var(--fs-ui-caption); } +.tsb-task-tm { font-size: var(--fs-ui-xs); color: var(--text-faint); font-variant-numeric: tabular-nums; } +.tsb-pbadge { font-size: var(--fs-ui-xs); color: var(--text-faint); border: 1px solid var(--border); border-radius: var(--radius-xs); padding: 0 5px; line-height: 1.5; white-space: nowrap; } .tsb-row-acts { display: none; gap: 2px; } .tsb-task:hover .tsb-row-acts { display: inline-flex; } .tsb-task:hover .tsb-task-tm { display: none; } @@ -297,92 +295,92 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .tsb-wsstrip button { border: none; background: none; color: var(--text-faint); cursor: pointer; padding: 5px; border-radius: var(--radius-sm); display: inline-flex; } .tsb-wsstrip button:hover { background: var(--bg-hover); color: var(--text); } -.tsb-footer { display: flex; align-items: center; gap: 4px; padding: 10px 12px; border-top: 1px solid var(--border-soft); } +.tsb-footer { display: flex; align-items: center; gap: 4px; padding: 10px 12px; border-top: 1px solid var(--border-soft); border-radius: 0 0 var(--radius-lg) var(--radius-lg); } .tsb-user { flex: 1; display: flex; align-items: center; gap: 9px; min-width: 0; height: 40px; padding: 0 7px; border: none; border-radius: var(--radius); background: none; cursor: pointer; text-align: left; transition: background .12s; } .tsb-user:hover { background: var(--bg-hover); } -.tsb-avatar { width: 27px; height: 27px; border-radius: 50%; background: linear-gradient(135deg, #6d8dff, #9d7bff); color: #fff; font-size: 12px; font-weight: 600; display: flex; align-items: center; justify-content: center; flex: none; box-shadow: inset 0 1px 0 rgba(255,255,255,.2); } +.tsb-avatar { width: 27px; height: 27px; border-radius: 50%; background: linear-gradient(135deg, var(--accent), var(--brand-2)); color: var(--text-on-accent); font-size: var(--fs-ui-sm); font-weight: 600; display: flex; align-items: center; justify-content: center; flex: none; box-shadow: inset 0 1px 0 rgba(255,255,255,.2); } .tsb-user-info { flex: 1; min-width: 0; display: flex; flex-direction: column; } -.tsb-user-name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.tsb-user-sub { font-size: 11px; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.tsb-user-name { font-size: var(--fs-ui-caption); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.tsb-user-sub { font-size: var(--fs-ui-xs); color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .tsb-user-chev { color: var(--text-faint); display: inline-flex; } -.tsb-menu { position: fixed; z-index: 300; min-width: 216px; background: var(--bg-raised); border: 1px solid var(--border-strong); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); padding: 5px; display: flex; flex-direction: column; animation: modal-in .12s var(--ease); } -.tsb-mi { display: flex; align-items: center; gap: 9px; min-height: 31px; padding: 4px 9px; border: none; border-radius: var(--radius-sm); background: none; color: var(--text-dim); font-size: 13px; cursor: pointer; text-align: left; } +.tsb-menu { position: fixed; z-index: 300; min-width: 216px; background: var(--bg-raised); border: 1px solid var(--border-strong); border-radius: var(--radius); box-shadow: var(--shadow-lg); padding: 5px; display: flex; flex-direction: column; animation: mk-modal-in .12s var(--ease); } +.tsb-mi { display: flex; align-items: center; gap: 9px; min-height: 31px; padding: 4px 9px; border: none; border-radius: var(--radius-sm); background: none; color: var(--text-dim); font-size: var(--fs-ui-caption); cursor: pointer; text-align: left; } .tsb-mi svg { color: var(--text-faint); flex: none; } .tsb-mi:hover { background: var(--bg-hover); color: var(--text); } .tsb-mi:hover svg { color: var(--text-dim); } .tsb-mi.danger { color: var(--err); } -.tsb-mi.danger:hover { background: var(--err); color: #fff; } -.tsb-mi.danger:hover svg { color: #fff; } -.tsb-mi kbd { margin-left: auto; font-size: 10.5px; font-family: var(--mono); color: var(--text-faint); } +.tsb-mi.danger:hover { background: var(--err); color: var(--text-on-accent); } +.tsb-mi.danger:hover svg { color: var(--text-on-accent); } +.tsb-mi kbd { margin-left: auto; font-size: var(--fs-ui-xs); font-family: var(--mono); color: var(--text-faint); } .tsb-menu-div { height: 1px; background: var(--border-soft); margin: 5px 4px; } .tsb-acc-head { display: flex; gap: 10px; align-items: center; padding: 8px 9px 6px; } -.tsb-acc-name { font-size: 13.5px; font-weight: 600; } -.tsb-acc-mail { font-size: 11.5px; color: var(--text-faint); } +.tsb-acc-name { font-size: var(--fs-ui-caption); font-weight: 600; } +.tsb-acc-mail { font-size: var(--fs-ui-xs); color: var(--text-faint); } -/* ---- 登录页与改密门(R4 重造型:宽屏右侧品牌面板) ---- */ -.lg-wrap { position: fixed; inset: 0; display: flex; align-items: stretch; background: var(--bg); z-index: 500; } -.lg-panel { flex: none; width: 46%; max-width: 620px; position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: center; padding: 64px; color: #dfe4f2; background: linear-gradient(160deg, #171e38 0%, #1d2145 55%, #251d4d 100%); } -.lg-panel::before { content: ''; position: absolute; width: 560px; height: 560px; border-radius: 50%; background: radial-gradient(closest-side, rgba(109,141,255,.22), transparent 70%); top: -140px; right: -160px; } -.lg-panel::after { content: ''; position: absolute; width: 440px; height: 440px; border-radius: 50%; background: radial-gradient(closest-side, rgba(157,123,255,.18), transparent 70%); bottom: -150px; left: -120px; } +/* ---- 登录页与改密门(Zai 复刻:近单色深底 + 深海军品牌面;品牌面板固定深底浅字) ---- */ +.lg-wrap { position: fixed; inset: 0; display: flex; align-items: stretch; background: var(--bg-inset); z-index: 500; } +.lg-panel { flex: none; width: 46%; max-width: 620px; position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: center; padding: 64px; color: #dfe4f2; background: linear-gradient(160deg, #0e1116 0%, #101826 55%, #0b1f38 100%); } +.lg-panel::before { content: ''; position: absolute; width: 560px; height: 560px; border-radius: 50%; background: radial-gradient(closest-side, rgba(64,153,255,.16), transparent 70%); top: -140px; right: -160px; } +.lg-panel::after { content: ''; position: absolute; width: 440px; height: 440px; border-radius: 50%; background: radial-gradient(closest-side, rgba(123,92,229,.12), transparent 70%); bottom: -150px; left: -120px; } .lg-panel h2 { font-size: 26px; font-weight: 700; letter-spacing: -0.02em; line-height: 1.35; margin: 0 0 12px; position: relative; } -.lg-panel p { font-size: 14px; opacity: .72; line-height: 1.75; margin: 0; position: relative; } +.lg-panel p { font-size: var(--fs-ui-base); opacity: .72; line-height: 1.75; margin: 0; position: relative; } .lg-panel ul { margin: 28px 0 0; list-style: none; display: flex; flex-direction: column; gap: 13px; position: relative; } -.lg-panel li { display: flex; gap: 10px; align-items: flex-start; font-size: 13.5px; opacity: .88; } -.lg-panel li svg { margin-top: 2px; color: #8fa8ff; flex: none; } -.lg-panel .lg-foot { position: absolute; bottom: 28px; left: 64px; right: 64px; display: flex; justify-content: space-between; font-size: 11.5px; opacity: .45; } +.lg-panel li { display: flex; gap: 10px; align-items: flex-start; font-size: var(--fs-ui-caption); opacity: .88; } +.lg-panel li svg { margin-top: 2px; color: var(--accent-strong); flex: none; } +.lg-panel .lg-foot { position: absolute; bottom: 28px; left: 64px; right: 64px; display: flex; justify-content: space-between; font-size: var(--fs-ui-xs); opacity: .45; } @media (max-width: 960px) { .lg-panel { display: none; } } .lg-formside { flex: 1; display: flex; align-items: center; justify-content: center; padding: 24px; min-width: 0; } .lg-card { width: 392px; max-width: 100%; } -.lg-brand { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 15.5px; letter-spacing: -0.01em; margin-bottom: 26px; } -.lg-title { margin: 0 0 4px; font-size: 22px; font-weight: 700; letter-spacing: -0.02em; } -.lg-sub { margin: 0 0 18px; font-size: 12.5px; color: var(--text-faint); line-height: 1.6; } +.lg-brand { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: var(--fs-ui-base); letter-spacing: -0.01em; margin-bottom: 26px; } +.lg-title { margin: 0 0 4px; font-size: var(--fs-ui-xl); font-weight: 700; letter-spacing: -0.02em; } +.lg-sub { margin: 0 0 18px; font-size: var(--fs-ui-sm); color: var(--text-faint); line-height: 1.6; } .lg-tabs { display: flex; background: var(--bg-inset); border: 1px solid var(--border-soft); border-radius: var(--radius); padding: 3px; gap: 3px; margin-bottom: 16px; } -.lg-tab { flex: 1; height: 30px; border: none; border-radius: var(--radius-xs); background: none; color: var(--text-faint); font-size: 12.5px; cursor: pointer; transition: all .12s; } +.lg-tab { flex: 1; height: 30px; border: none; border-radius: var(--radius-xs); background: none; color: var(--text-faint); font-size: var(--fs-ui-sm); cursor: pointer; transition: all .12s; } .lg-tab:hover { color: var(--text-dim); } -.lg-tab.on { background: var(--bg-raised); color: var(--text); font-weight: 550; box-shadow: 0 1px 2px rgba(8,9,12,.25); } -.lg-label { display: block; font-size: 12px; font-weight: 550; color: var(--text-dim); margin: 12px 0 6px; } -.lg-input { width: 100%; height: 38px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg-raised); color: var(--text); padding: 0 12px; font-size: 13.5px; transition: border-color .12s, box-shadow .12s; } +.lg-tab.on { background: var(--bg-raised); color: var(--text); font-weight: 550; box-shadow: 0 1px 2px rgba(0,0,0,.25); } +.lg-label { display: block; font-size: var(--fs-ui-sm); font-weight: 550; color: var(--text-dim); margin: 12px 0 6px; } +.lg-input { width: 100%; height: 38px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg-raised); color: var(--text); padding: 0 12px; font-size: var(--fs-ui-caption); transition: border-color .12s, box-shadow .12s; } .lg-input:focus { border-color: var(--accent); box-shadow: var(--ring); outline: none; } -.lg-mono { font-family: var(--mono); font-size: 12.5px; } -.lg-err { margin-top: 12px; font-size: 12.5px; color: var(--err); background: var(--err-soft); border: 1px solid color-mix(in srgb, var(--err) 35%, transparent); border-radius: var(--radius); padding: 8px 11px; display: flex; gap: 7px; align-items: flex-start; } +.lg-mono { font-family: var(--mono); font-size: var(--fs-ui-sm); } +.lg-err { margin-top: 12px; font-size: var(--fs-ui-sm); color: var(--err); background: var(--err-soft); border: 1px solid color-mix(in srgb, var(--err) 35%, transparent); border-radius: var(--radius); padding: 8px 11px; display: flex; gap: 7px; align-items: flex-start; } .lg-err svg { flex: none; margin-top: 1px; } .lg-submit { width: 100%; height: 38px; margin-top: 16px; border: none; border-radius: var(--radius); - background: var(--accent); color: var(--text-on-accent); font-weight: 600; font-size: 13.5px; cursor: pointer; - box-shadow: inset 0 1px 0 rgba(255,255,255,.16), 0 2px 8px var(--accent-glow); - transition: filter .12s, transform .12s; + background: var(--primary); color: var(--primary-foreground); font-weight: 600; font-size: var(--fs-ui-caption); cursor: pointer; + box-shadow: 0 2px 8px rgba(0, 0, 0, .3); + transition: background .12s, transform .12s; } .lg-submit:disabled { opacity: .45; cursor: default; box-shadow: none; } -.lg-submit:not(:disabled):hover { filter: brightness(1.09); } +.lg-submit:not(:disabled):hover { background: color-mix(in srgb, var(--primary) 86%, var(--bg)); } .lg-submit:not(:disabled):active { transform: translateY(1px); } -.lg-cancel { width: 100%; margin-top: 8px; border: none; background: none; color: var(--text-faint); font-size: 12.5px; cursor: pointer; } +.lg-cancel { width: 100%; margin-top: 8px; border: none; background: none; color: var(--text-faint); font-size: var(--fs-ui-sm); cursor: pointer; } .lg-cancel:hover { color: var(--text-dim); text-decoration: underline; } -.lg-hint { margin: 14px 0 0; font-size: 11.5px; color: var(--text-faint); line-height: 1.7; } -.lg-hint code { font-family: var(--mono); font-size: 10.5px; background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius-xs); padding: 1px 5px; } +.lg-hint { margin: 14px 0 0; font-size: var(--fs-ui-xs); color: var(--text-faint); line-height: 1.7; } +.lg-hint code { font-family: var(--mono); font-size: var(--fs-ui-xs); background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius-xs); padding: 1px 5px; } /* ---- 用户管理 / 改密弹层 ---- */ .ad-body { padding: 16px 20px 20px; display: flex; flex-direction: column; gap: 12px; max-height: 70vh; overflow-y: auto; } .ad-create { border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 14px; display: flex; flex-direction: column; gap: 8px; background: var(--bg-panel); } .ad-row { display: flex; gap: 8px; align-items: center; } .ad-row .lg-input { flex: 1; min-width: 0; height: 34px; } -.ad-check { display: flex; align-items: center; gap: 5px; font-size: 12.5px; color: var(--text-dim); white-space: nowrap; } -.ad-create-btn { width: auto; padding: 0 14px; margin-top: 0; height: 34px; flex: none; box-shadow: inset 0 1px 0 rgba(255,255,255,.14); } -.ad-mini { border: 1px solid var(--border); background: var(--bg-raised); color: var(--text-dim); border-radius: var(--radius-sm); padding: 4px 9px; font-size: 12px; cursor: pointer; white-space: nowrap; display: inline-flex; align-items: center; gap: 4px; } +.ad-check { display: flex; align-items: center; gap: 5px; font-size: var(--fs-ui-sm); color: var(--text-dim); white-space: nowrap; } +.ad-create-btn { width: auto; padding: 0 14px; margin-top: 0; height: 34px; flex: none; } +.ad-mini { border: 1px solid var(--border); background: var(--bg-raised); color: var(--text-dim); border-radius: var(--radius-sm); padding: 4px 9px; font-size: var(--fs-ui-sm); cursor: pointer; white-space: nowrap; display: inline-flex; align-items: center; gap: 4px; } .ad-mini:hover { color: var(--text); border-color: var(--accent); } -.ad-hint { margin: 0; font-size: 11.5px; color: var(--text-faint); } -.ad-notice { font-size: 12.5px; color: var(--ok); background: var(--ok-soft); border: 1px solid var(--ok-border); border-radius: var(--radius); padding: 8px 11px; display: flex; gap: 7px; align-items: flex-start; } +.ad-hint { margin: 0; font-size: var(--fs-ui-xs); color: var(--text-faint); } +.ad-notice { font-size: var(--fs-ui-sm); color: var(--ok); background: var(--ok-soft); border: 1px solid var(--ok-border); border-radius: var(--radius); padding: 8px 11px; display: flex; gap: 7px; align-items: flex-start; } .ad-notice svg { flex: none; margin-top: 1px; } -.ad-table { width: 100%; border-collapse: collapse; font-size: 12.5px; } -.ad-table th { text-align: left; font-weight: 600; color: var(--text-faint); font-size: 11px; text-transform: uppercase; letter-spacing: .06em; padding: 8px; border-bottom: 1px solid var(--border); } +.ad-table { width: 100%; border-collapse: collapse; font-size: var(--fs-ui-sm); } +.ad-table th { text-align: left; font-weight: 600; color: var(--text-faint); font-size: var(--fs-ui-xs); text-transform: uppercase; letter-spacing: .06em; padding: 8px; border-bottom: 1px solid var(--border); } .ad-table td { padding: 9px 8px; border-bottom: 1px solid var(--border-soft); } .ad-table tr:last-child td { border-bottom: none; } -.ad-mono { font-family: var(--mono); font-size: 12px; } +.ad-mono { font-family: var(--mono); font-size: var(--fs-ui-sm); } .ad-dim { color: var(--text-faint); } -.ad-chip { font-size: 10px; border-radius: var(--radius-xs); padding: 2px 7px; font-weight: 700; letter-spacing: .5px; background: var(--bg-hover); color: var(--text-faint); } +.ad-chip { font-size: var(--fs-ui-xs); border-radius: var(--radius-xs); padding: 2px 7px; font-weight: 700; letter-spacing: .5px; background: var(--bg-hover); color: var(--text-faint); } .ad-chip-admin { background: var(--accent-soft); color: var(--accent); } .ad-chip-warn { background: var(--warn-soft); color: var(--warn); } -.ad-reset { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent); background: var(--warn-soft); border-radius: var(--radius-lg); padding: 12px; font-size: 12.5px; } +.ad-reset { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent); background: var(--warn-soft); border-radius: var(--radius-lg); padding: 12px; font-size: var(--fs-ui-sm); } .ad-reset .lg-input { flex: 1; min-width: 160px; height: 32px; } .ad-changepw { display: flex; flex-direction: column; gap: 2px; max-width: 340px; } @@ -390,7 +388,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .icon-only { display: flex; flex-direction: column; align-items: center; gap: 6px; padding-top: 12px; } .icon-only button { background: none; border: none; color: var(--text-faint); cursor: pointer; padding: 0; width: 34px; height: 34px; border-radius: var(--radius); display: inline-flex; align-items: center; justify-content: center; position: relative; } .icon-only button:hover { background: var(--bg-hover); color: var(--text); } -.rail-badge { position: absolute; top: 3px; right: 3px; min-width: 14px; height: 14px; border-radius: 7px; background: var(--warn); color: #fff; font-size: 9.5px; font-weight: 700; display: flex; align-items: center; justify-content: center; padding: 0 4px; } +.rail-badge { position: absolute; top: 3px; right: 3px; min-width: 14px; height: 14px; border-radius: 7px; background: var(--warn); color: var(--text-on-accent); font-size: var(--fs-ui-xs); font-weight: 700; display: flex; align-items: center; justify-content: center; padding: 0 4px; } /* ---- UI 能力平台消费(消息头像列 / 设置外壳 / 管理弹层头像) ---- */ .role-mark { width: 20px; height: 20px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; color: var(--text-dim); background: var(--bg-raised); border: 1px solid var(--border); } @@ -400,7 +398,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; } /* ---- 右栏轻预览(裁定 D4:右栏 + 弹窗双落位)---- */ .rail-preview-head { display: flex; align-items: center; gap: 6px; padding: 0 2px 8px; border-bottom: 1px solid var(--border-soft); margin-bottom: 8px; } -.rail-preview-head .rp-name { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 55%; } +.rail-preview-head .rp-name { font-size: var(--fs-ui-caption); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 55%; } .tab-body[data-rail-preview] { display: flex; flex-direction: column; } .tab-body[data-rail-preview] .ui-fp { flex: 1; } .artifact-chip .ac-act { border: none; background: none; } diff --git a/test/design-guard.test.ts b/test/design-guard.test.ts new file mode 100644 index 0000000..e6a343e --- /dev/null +++ b/test/design-guard.test.ts @@ -0,0 +1,80 @@ +// 设计系统守护测试(父仓 DESIGN.md §11 禁止清单 · 宿主侧,2026-09-30)。 +// 棘轮基线:现存豁免登记在下方;新增同类违规直接失败。收紧 = 修掉违规后删基线条目。 +// 范围:src/styles.css 与 src/**/*.tsx。@ui 组件库侧由 Agent-System-UI 的守护测试覆盖。 +import { describe, it, expect } from 'vitest' +import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs' +import { join, relative, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const HERE = dirname(fileURLToPath(import.meta.url)) // test/ +const FE_ROOT = join(HERE, '..') +const SRC = join(FE_ROOT, 'src') +const DEV_ROOT = join(FE_ROOT, '..', '..') // Dev 父仓根(DESIGN.md;单独检出本仓时不存在) + +function walk(dir: string, out: string[] = []): string[] { + for (const name of readdirSync(dir)) { + const p = join(dir, name) + if (statSync(p).isDirectory()) walk(p, out) + else out.push(p) + } + return out +} +const read = (p: string) => readFileSync(p, 'utf8') +const stripCssComments = (t: string) => t.replace(/\/\*[\s\S]*?\*\//g, '') +const collectHex = (t: string) => [...t.matchAll(/#[0-9a-fA-F]{3,8}\b/g)].map(m => m[0].toLowerCase()) + +// ---- 1. 宪法在位(组合检出时) ---- +describe('DESIGN.md 宪法在位', () => { + it('DESIGN.md 存在且 AGENTS.md 带强制阅读门禁', () => { + const designPath = join(DEV_ROOT, 'DESIGN.md') + if (!existsSync(designPath)) { console.warn('(单独检出 Frontend 仓:跳过宪法检查)'); return } + expect(read(designPath)).toContain('Agent-System-Dev 设计系统') + const agents = read(join(DEV_ROOT, 'AGENTS.md')) + expect(agents).toContain('DESIGN.md') + }) +}) + +// ---- 2. 动效唯一所有者 ---- +describe('动效唯一所有者', () => { + it('styles.css 无 @keyframes(全部走 @ui/motion.css 的 mk-* 类)', () => { + expect(stripCssComments(read(join(SRC, 'styles.css')))).not.toContain('@keyframes') + }) +}) + +// ---- 3. 裸色值棘轮 ---- +const HEX_BASELINE_STYLES = [ + '#0e1116', '#101826', '#0b1f38', // 登录品牌面板固定深底渐变(面板自带深底,主题无关) + '#dfe4f2', // 同上面板的固定浅字 +] as const +describe('裸色值棘轮', () => { + it('styles.css 不引入基线外裸色值', () => { + const found = collectHex(read(join(SRC, 'styles.css'))) + const allow = new Set(HEX_BASELINE_STYLES) + const extra = found.filter(h => !allow.has(h)) + expect(extra, `新增裸色值 ${[...new Set(extra)].join(', ')}——改用 tokens 令牌;确需豁免更新 HEX_BASELINE_STYLES 并写明理由`).toEqual([]) + }) + it('组件 TSX 无裸色值', () => { + const extra: string[] = [] + for (const p of walk(SRC).filter(p => /\.(tsx|ts)$/.test(p) && !p.endsWith('.d.ts'))) { + for (const h of collectHex(read(p))) extra.push(`${relative(SRC, p)}: ${h}`) + } + expect(extra, '组件里出现裸色值——走 tokens 令牌').toEqual([]) + }) +}) + +// ---- 4. 行内字号棘轮(--fs-ui-* 标尺) ---- +const FONTSIZE_LITERAL_BASELINE = ['40'] as const // Conversation 空态装饰字形(⌘ 展示字号,非 UI 层级) +describe('行内字号棘轮', () => { + it('组件 TSX 无基线外 fontSize 数值字面量', () => { + const allow = new Set(FONTSIZE_LITERAL_BASELINE) + const extra: string[] = [] + for (const p of walk(SRC).filter(p => p.endsWith('.tsx'))) { + const lines = read(p).split('\n') + lines.forEach((l: string, i: number) => { + const m = l.match(/fontSize:\s*['"`]?([\d.]+)/) + if (m && !allow.has(m[1])) extra.push(`${relative(SRC, p)}:${i + 1} (${m[1]})`) + }) + } + expect(extra, '行内 fontSize 数值——改用 var(--fs-ui-*);确需豁免更新基线并写明理由').toEqual([]) + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 9e04070..ffdd18d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "skipLibCheck": true, "isolatedModules": true, "noEmit": true, - "types": ["vite/client"], + "types": ["vite/client", "node"], "baseUrl": ".", "paths": { "@ui": ["../ui/src/index.ts"], diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index 7eda14f..3fb0733 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/api.ts","./src/auth.ts","./src/contracts.ts","./src/live.ts","./src/main.tsx","./src/messageparts.ts","./src/passwords.ts","./src/previewsetup.ts","./src/store.ts","./src/tasklist.ts","./src/components/AdminUsersModal.tsx","./src/components/AgentTracker.tsx","./src/components/Conversation.tsx","./src/components/DocViewer.tsx","./src/components/LoginPage.tsx","./src/components/ProjectSettingsModal.tsx","./src/components/SettingsModal.tsx","./src/components/TaskSidebar.tsx","./src/components/Workbench.tsx","./src/components/Workspaces.tsx","./test/auth.test.ts","./test/contracts.test.ts","./test/live.test.ts","./test/messageparts.test.ts","./test/tasklist.test.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/api.ts","./src/auth.ts","./src/contracts.ts","./src/live.ts","./src/main.tsx","./src/messageparts.ts","./src/passwords.ts","./src/previewsetup.ts","./src/store.ts","./src/tasklist.ts","./src/components/AdminUsersModal.tsx","./src/components/AgentTracker.tsx","./src/components/Conversation.tsx","./src/components/DocViewer.tsx","./src/components/LoginPage.tsx","./src/components/ProjectSettingsModal.tsx","./src/components/SettingsModal.tsx","./src/components/TaskSidebar.tsx","./src/components/Workbench.tsx","./src/components/Workspaces.tsx","./test/auth.test.ts","./test/contracts.test.ts","./test/design-guard.test.ts","./test/live.test.ts","./test/messageparts.test.ts","./test/tasklist.test.ts"],"version":"5.9.3"} \ No newline at end of file -- 2.54.0 From 48a14fb4d26b8699694f7d739823f0f944d7e271 Mon Sep 17 00:00:00 2001 From: weichen Date: Wed, 30 Sep 2026 19:11:20 +0000 Subject: [PATCH 2/5] Move system capabilities into an admin-only full-screen zone Why: Approved redesign phase 2 (IA): normal users run projects entirely through conversation; system capabilities (user management, agent profiles, plugin management, plugin-declared workspaces) belong behind a dedicated admin entry, not in the user-facing sidebar or settings. The sidebar workspace strip is removed; artifacts stay reachable via the right-rail tab and conversation chips. Evidence: npx tsc -b clean; npm test -> 38 passed (design-guard ratchet still green). Full combined run recorded in the Dev task card. Impact: new AdminZone full-screen view with a declarative section registry (users/profiles/plugins/plugin-workspaces via UIRenderer); sections moved out of SettingsModal (now personal preferences only) and AdminUsersModal (now password change only); store ui.adminUsersOpen renamed adminZoneOpen; workspaceEntries narrowed by role so plugin entries no longer take Cmd1-9 for regular users; theme toggle moved into the account menu. Refs: Agent-System-Dev task feature/ui-tokens-v3 (phase 2). --- src/App.tsx | 5 +- src/components/AdminUsersModal.tsx | 106 +------------ src/components/AdminZone.tsx | 66 ++++++++ src/components/SettingsModal.tsx | 114 +------------- src/components/TaskSidebar.tsx | 20 +-- src/components/Workspaces.tsx | 14 +- src/components/adminSections.tsx | 233 +++++++++++++++++++++++++++++ src/store.ts | 5 +- src/styles.css | 21 ++- tsconfig.tsbuildinfo | 2 +- 10 files changed, 351 insertions(+), 235 deletions(-) create mode 100644 src/components/AdminZone.tsx create mode 100644 src/components/adminSections.tsx diff --git a/src/App.tsx b/src/App.tsx index 5f8b686..bea6c91 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,8 @@ import { useEffect, useRef, useState } from 'react' import { auth } from './auth' import { api } from './api' import { LoginPage, ChangePasswordGate } from './components/LoginPage' -import { AdminUsersModal, ChangePasswordModal } from './components/AdminUsersModal' +import { ChangePasswordModal } from './components/AdminUsersModal' +import { AdminZone } from './components/AdminZone' import { Icon } from '@ui' import { store, useStore } from './store' import { TaskSidebar } from './components/TaskSidebar' @@ -148,7 +149,7 @@ export function App() { {trackerRunId && } {ui.settingsOpen && } {ui.projectSettingsId && } - {ui.adminUsersOpen && store.setUI({ adminUsersOpen: false })} />} + {ui.adminZoneOpen && } {ui.changePwOpen && store.setUI({ changePwOpen: false })} />} {viewingArtifact && } {toast &&
    {toast}
    } diff --git a/src/components/AdminUsersModal.tsx b/src/components/AdminUsersModal.tsx index da8c592..a195626 100644 --- a/src/components/AdminUsersModal.tsx +++ b/src/components/AdminUsersModal.tsx @@ -1,13 +1,9 @@ -// 管理员用户管理弹层 + 主动改密弹窗(R2 管理员发放制)。 -// AdminUsersModal:建号(用户名/显示名/初始密码/管理员位)、重置密码、列表状态。 -// ChangePasswordModal:登录者主动改密(与强制改密共用 API)。 -import { useEffect, useState } from 'react' +// 管理员用户管理已迁「系统管理」分区(components/adminSections.tsx UsersPanel, +// 重设计方案 阶段②)。本文件只保留登录者主动改密弹窗(与强制改密共用 API)。 +import { useState } from 'react' import { api } from '../api' import { store, useStore } from '../store' -import { Icon, Avatar } from '@ui' -import { randomPassword } from '../passwords' - -interface AdminUserRow { owner: string; name: string; isAdmin: boolean; mustChangePassword: boolean; hasPassword: boolean; passwordSetAt?: string; createdAt: string } +import { Icon } from '@ui' function Shell({ title, onClose, children }: { title: string; onClose: () => void; children: React.ReactNode }) { return ( @@ -24,100 +20,6 @@ function Shell({ title, onClose, children }: { title: string; onClose: () => voi ) } -export function AdminUsersModal({ onClose }: { onClose: () => void }) { - const me = useStore(s => s.me) - const [users, setUsers] = useState(null) - const [err, setErr] = useState('') - const [notice, setNotice] = useState('') - const [owner, setOwner] = useState('') - const [name, setName] = useState('') - const [pw, setPw] = useState('') - const [asAdmin, setAsAdmin] = useState(false) - const [busy, setBusy] = useState(false) - const [resetFor, setResetFor] = useState(null) - const [resetPw, setResetPw] = useState('') - - const refresh = async () => { - try { setUsers(await api.listAdminUsers()) } catch (e) { setErr(String(e)) } - } - useEffect(() => { refresh() }, []) - - const create = async () => { - setErr(''); setNotice('') - if (pw.length < 8) { setErr('初始密码至少 8 位'); return } - setBusy(true) - try { - await api.createAdminUser(owner.trim(), name.trim(), pw, asAdmin) - setNotice(`已创建 ${owner.trim()}:首次登录须改密`) - setOwner(''); setName(''); setPw('') - await refresh() - } catch (e) { setErr(String(e).includes('409') ? '用户名已存在' : String(e)) } finally { setBusy(false) } - } - - const reset = async () => { - if (!resetFor) return - setErr('') - if (resetPw.length < 8) { setErr('新密码至少 8 位'); return } - setBusy(true) - try { - await api.resetAdminUserPassword(resetFor, resetPw) - setNotice(`已重置 ${resetFor} 的密码:其下次登录须改密(现有会话已吊销)`) - setResetFor(null); setResetPw('') - await refresh() - } catch (e) { setErr(String(e)) } finally { setBusy(false) } - } - - return ( - - {me?.isAdmin && ( -
    -
    - setOwner(e.target.value)} /> - setName(e.target.value)} /> -
    -
    - setPw(e.target.value)} /> - - - -
    -

    创建后该用户首次登录必须修改此初始密码。

    -
    - )} - {notice &&
    {notice}
    } - {err &&
    {err}
    } - {resetFor ? ( -
    - 重置 {resetFor} 的密码: - setResetPw(e.target.value)} /> - - - -
    - ) : ( - - - - {(users ?? []).map(u => ( - - - - - - - - - ))} - {users !== null && users.length === 0 && } - -
    用户名显示名角色状态密码设置于
    {u.owner}{u.name || '—'}{u.isAdmin ? ADMIN : USER}{u.mustChangePassword ? 待改密 : u.hasPassword ? '正常' : '未设密码'}{u.passwordSetAt ? new Date(u.passwordSetAt).toLocaleString() : '—'}{me?.isAdmin && ( - - )}
    暂无账号
    - )} -
    - ) -} - /** 主动改密(账户菜单入口):当前密码 + 新密码 + 确认。 */ export function ChangePasswordModal({ onClose }: { onClose: () => void }) { const me = useStore(s => s.me) diff --git a/src/components/AdminZone.tsx b/src/components/AdminZone.tsx new file mode 100644 index 0000000..bc83811 --- /dev/null +++ b/src/components/AdminZone.tsx @@ -0,0 +1,66 @@ +// 系统管理(重设计方案 阶段②):管理员专属的全屏独立视图(带返回,非弹窗)。 +// 用户决策:系统管理 = 全屏独立视图;普通用户界面不暴露任何系统能力。 +// 分区注册表:新增管理页只改 SECTIONS,不改 AdminZone 本体(声明式注册)。 +import { useState } from 'react' +import { store, useStore } from '../store' +import { Icon, type IconName } from '@ui' +import { UsersPanel, ProfilesSection, PluginsSection, PluginWorkspacesPanel } from './adminSections' + +interface AdminSectionDef { + key: string + label: string + icon: IconName + desc: string + /** 懒渲染:进入分区才挂载(每个面板自带数据加载)。 */ + render: () => React.ReactNode +} + +function ProfilesPanelWrapper() { + const profiles = useStore(s => s.profiles) + return +} + +const SECTIONS: AdminSectionDef[] = [ + { key: 'users', label: '用户管理', icon: 'users', desc: '建号、发放初始密码、重置密码(管理员发放制)', render: () => }, + { key: 'profiles', label: '智能体画像', icon: 'bot', desc: '系统级画像:系统提示、工具允许/确认/拒绝清单', render: () => }, + { key: 'plugins', label: '插件管理', icon: 'puzzle', desc: '已装载插件清单与热重载(装卸不影响核心)', render: () => }, + { key: 'workspaces', label: '插件工作区', icon: 'grid', desc: '插件声明面板与服务端声明工作区', render: () => }, +] + +export function AdminZone() { + const me = useStore(s => s.me) + const [sectionKey, setSectionKey] = useState('users') + // 双保险:入口已在菜单层门控(me?.isAdmin),此处再拦一次(纵深防御)。 + if (!me?.isAdmin) return null + const section = SECTIONS.find(s => s.key === sectionKey) ?? SECTIONS[0] + return ( +
    +
    + + 系统管理 + 仅管理员可见 · 变更即时生效 + + {me.owner} +
    +
    + +
    +
    +

    {section.label}

    +

    {section.desc}

    +
    + {section.render()} +
    +
    +
    + ) +} diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index ea2740d..78bd2ab 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -1,12 +1,14 @@ // 设置中心(4.9 → UI 能力平台迁移):@ui SettingsShell + SettingRow 统一行语义, // 保存按钮进固定 actions 底栏(不随内容滚动);schema 驱动 + 生效时机标注 + 密钥掩码。 +// 重设计方案 阶段②(IA):设置瘦身——只留个人偏好(常规);智能体画像/插件管理/ +// 插件工作区迁「系统管理」(AdminZone,管理员专属全屏视图)。 import { useEffect, useMemo, useState } from 'react' import { api } from '../api' -import { store, useStore } from '../store' -import type { AgentProfile, SettingDesc } from '../contracts' -import { SettingsShell, SettingRow, SettingsGroup, Btn, EmptyState } from '@ui' +import { store } from '../store' +import type { SettingDesc } from '../contracts' +import { SettingsShell, SettingRow, SettingsGroup, Btn } from '@ui' -type Section = 'settings' | 'profiles' | 'plugins' +type Section = 'settings' // 分组显示名(项目设置面板共用)。 export const groupNames: Record = { @@ -15,8 +17,7 @@ export const groupNames: Record = { } export function SettingsModal() { - const profiles = useStore(s => s.profiles) - const [section, setSection] = useState
    ('settings') + const [section] = useState
    ('settings') const [schema, setSchema] = useState([]) const [, setUser] = useState>({}) const [draft, setDraft] = useState>({}) @@ -49,12 +50,10 @@ export function SettingsModal() { return ( setSection(k as Section)} + title="设置" active={section} onSelect={() => undefined} onClose={() => store.openSettings(false)} sections={[ { key: 'settings', label: '常规', icon: 'settings' }, - { key: 'profiles', label: '智能体画像', icon: 'bot', badge: profiles.length || undefined }, - { key: 'plugins', label: '插件', icon: 'puzzle' }, ]} footer={saved ? ✓ 已保存 : 'Agent-System · 设置中心'} actions={section === 'settings' && schema.length > 0 ? ( @@ -73,8 +72,6 @@ export function SettingsModal() { ))} ))} - {section === 'profiles' && } - {section === 'plugins' && } ) } @@ -119,98 +116,3 @@ export function unflatten(flatMap: Record): Record { return out } -// ---- 画像管理(数据化:导入导出 + 编辑)---- -function ProfilesSection({ profiles }: { profiles: AgentProfile[] }) { - const [editing, setEditing] = useState(null) - const [text, setText] = useState('') - return ( -
    -
    - { setEditing(newProfile()); setText('') }}>新建画像 - -
    - {profiles.map(p => ( - {p.name} {p.id}{p.builtin ? ' · 内置' : ''}} - desc={p.description}> -
    - { - const blob = new Blob([JSON.stringify(p, null, 2)], { type: 'application/json' }) - const a = document.createElement('a') - a.href = URL.createObjectURL(blob); a.download = `${p.id}.json`; a.click() - }}>导出 - {!p.builtin && { setEditing(p); setText(p.systemPrompt) }}>编辑} - {!p.builtin && { await api.deleteProfile(p.id); store.refreshProfiles() }}>删除} -
    -
    - ))} - {editing && ( - -
    - -