diff --git a/.agents/skills/frontend-craft/SKILL.md b/.agents/skills/frontend-craft/SKILL.md new file mode 100644 index 0000000..8c90b42 --- /dev/null +++ b/.agents/skills/frontend-craft/SKILL.md @@ -0,0 +1,192 @@ +--- +name: frontend-craft +description: 生成或修改前端界面时建立并维持统一的设计系统。当用户要求做前端页面/组件/样式、抱怨界面丑或乱或不一致、做前端技术选型与设计 Token、或需要整理迁移既有 CSS(样式膨胀、重复规则、颜色字号失控)时使用。提供两阶段工作流(先建 token/原语/lint/截图验收回路,再生成界面)、设计 Token 数值基准、原语组件模板、反模式清单、提示词模板与零依赖截图方法。不适用于纯后端工作;也不替代用户做审美与参照物的最终选择。 +--- + +# 前端界面工程(frontend-craft) + +## 目标 + +让 AI 产出的前端界面**一致、完整、可维护**,并且让"漂移"从一件无人察觉的事 +变成一件会被机器发现的事。 + +**这个技能解决的不是"审美"问题,是"约束"问题。** 界面看起来乱, +绝大多数时候不是因为某个颜色选错了,而是因为**每一轮迭代都在重新发明一个值**。 +一份真实的体检报告:某项目 1313 行 CSS 里只有 31 次 `var()`(约 97% 硬编码), +产出 7 个几乎一样的绿色、21 种圆角、19 种字号、24 种 rgba、3 个互相覆盖的 `:root` 块、 +66 条重复声明。**单个按钮都不丑,叠在一起眼睛找不到节奏。** + +所以做法不是"重新画得好看一点"(那会把同一类错误再犯一次), +而是**先把约束建起来,再在约束内生成**。 + +--- + +## 诚实的能力边界(先读,避免期待错位) + +**这个技能能做到:** +- 消除颜色/字号/圆角/间距的失控(从"21 种圆角"到"3 种") +- 补齐 hover / focus / disabled / 空态 / 错态 / 加载态,不再遗漏 +- 新会话不用重新解释规范(规范在仓库里,不在对话里) +- 从 mock 切真后端时改动极小(契约是真的) +- 让漂移可被机器发现,而不是靠人眼 + +**这个技能做不到:** +- **提升审美上限。** 参照物选得差,产出依然是"整洁的差品味"。 +- **替代信息架构判断。** 把控件状态当正文写进页面,token 和 lint 都拦不住。 +- **阻止用户自己用临时 prompt 破坏体系。** 技能管不住旁边那个人。 + +> 一句话:**这个技能把"很烂"提到"整洁统一",这是真实且巨大的提升。 +> 从"整洁"到"优秀"那一步靠参照物和人的品味判断。** +> 不要向用户承诺超出这个范围的结果。 + +--- + +## 适用与不适用 + +**适用**:新项目前端从零搭建 / 既有界面整理迁移 / 用户抱怨"界面丑、乱、不一致" / +需要定义设计 Token / 需要建 lint 与验收回路 / 需要写前端相关的提示词。 + +**不适用**:纯后端或 CLI 工作 / 用户只想要一个一次性的静态页面(用不上这套)/ +用户已明确有成熟设计系统且不希望改动。 + +--- + +## 工作流 + +### 0. 先判断当前处于哪个阶段 + +| 现象 | 阶段 | 去哪 | +|---|---|---| +| 项目没有 `tokens.css` / `DESIGN.md` | **阶段一(建约束)** | `references/bootstrap.md` | +| 有约束,要做一个新页面/组件 | **阶段二(生成)** | `references/build.md` | +| 已有界面但样式乱了,要整理 | **迁移** | `tokens-baseline.md` 第 4 节 | +| 需要对照截图检查 | **验收** | `references/screenshot-and-qa.md` | + +**不要跳过阶段一直接生成界面。** 这是本技能最核心的一条。 +理由见 `bootstrap.md` 开头 —— 简单说:直接生成时 AI 要做 200 个即兴决定, +其中 190 个没有依据,而这些决定会不可逆地叠加。 + +### 1. 阶段一:建设约束基础设施 + +产出 12 项(清单见 `bootstrap.md`),核心是这四类: + +1. **具体数值** → `src/styles/tokens.css`(复制 `templates/tokens.css`, + 只调正文基准字号、主色、中性色偏色三处) +2. **视觉契约** → `docs/design/DESIGN.md`(复制 `templates/DESIGN.md`) + —— 尤其是**参照物表**和**页面×状态矩阵** +3. **可执行检查** → `stylelint.config.mjs` + `scripts/audit-css.mjs` +4. **验收回路** → `scripts/capture.mjs` + `docs/design/qa-checklist.md` + +**为什么要"可执行检查"这一层**:一句写在文档里的"禁止字面量颜色", +被违反时什么都不会发生。一条会 `exit 1` 的规则,被违反时你一定会知道。 +**规则的价值不在于被写下来,而在于被违反时会发生什么。** + +### 2. 阶段二:按契约生成界面 + +**顺序是铁律**: + +``` +灰块结构 → 空态 + 错态 → 加载态 → 正常态 → 边界态 → 截图验收 +``` + +两个最关键的顺序决定: +- **空态和错态在正常态之前** —— 反过来的话,空态需要的引导按钮和居中容器 + 在正常态布局里没有位置,只能硬塞或草草了事。 + **顺序反了 = 空态永远补不上。** 而且新用户第一次打开看到的就是空态。 +- **灰块结构在最前面** —— 先填内容再调布局,等于在移动的靶子上画画。 + +细节见 `references/build.md`。 + +### 3. 迁移既有界面 + +**不要推翻重做**(那是把同一类错误再犯一次,还丢掉已完成的交互)。 +按 `tokens-baseline.md` 第 4 节:取色 → 按频次归一 → 主色取现状中最后生效的那个 +(不新造,避免"又换风格了"的反弹)→ 用 `audit-css.mjs` 记录迁移前后分数。 + +按频次归一(19 种字号 → 7 种,21 种圆角 → 3 种)会**立刻**让界面看起来整齐 —— +因为噪音消失了,而不是因为"变漂亮了"。这是投入产出比最高的一次改动。 + +### 4. 验收(每次改完必做) + +``` +截图 → 看图挑刺 → 改 → 再截图 +``` + +- 截图:`node scripts/capture.mjs shots --widths 375,1280`(输出目录是位置参数) +- 挑刺:对照 `templates/qa-checklist.md`,按 8 组**顺序**过(对齐优先于颜色) +- **必须真的调用读图工具看图。** 声称"我看到界面很整洁"但没读图,是编造。 +- 反馈必须能直接翻译成一行 CSS 改动。**"看起来不够精致"是无效反馈**, + "工具栏三个控件高度是 28/36/32px"才是有效反馈。 +- 更有效:开一个**全新的 subagent**,只给它截图和清单、不告诉它代码是谁写的。 + 它的上下文里没有"我这么写是有道理的"这笔账。见 `prompts.md` 第 5 节。 + +--- + +## 硬规则(不可协商) + +写进每个 prompt,并让 lint 强制: + +1. 全项目**只有一个 `:root` 块**,只在 `tokens.css` 里 +2. **禁止颜色字面量**(`#xxx` / `rgb()` / `hsl()`),颜色只用 `var(--c-*)` +3. **禁止设计维度写裸 px**(间距/字号/圆角/行高),只用 `var(--space-* / --text-* / --radius-* / --leading-*)`。例外:`1px` 边框线宽 +4. **禁止 `!important`**(几乎总是重复规则打架的后遗症) +5. **禁止 `outline: none`**(焦点环是键盘用户唯一的导航手段) +6. **禁止为同一语义造第二个 token**(先 grep 找最接近的复用) +7. **改原规则,不要在文件末尾追加覆盖块** —— 这是 CSS 膨胀的头号来源。 + 真实案例:某文件 453 条规则只覆盖 387 个选择器,66 条重复声明,文件注释就是 6 段迭代史 +8. **禁止"顺便优化"** —— "发现其他问题请单独列出,不要顺手改" +9. **一次只改一个维度**(对齐 → 间距 → 灰度 → 圆角 → 颜色),改完截图再动下一个 +10. **禁止给形容词下需求** —— 给数值或给参照图。"高级感"没有可执行的落点 + +完整清单(22 条,含症状/为什么坏/正确做法)见 `templates/anti-patterns.md`。 + +--- + +## 物料索引 + +**物料比方法论值钱。** 大部分的实际效果来自下面这些具体文件和数值, +而不是上面这些流程描述。 + +### templates/ —— 复制到项目里去用的 + +| 文件 | 用途 | +|---|---| +| `templates/tokens.css` | 设计 Token 唯一真相源:11 节、含不可违反规则注释、深色模式结构预留 | +| `templates/DESIGN.md` | 视觉契约模板:定位/参照物表/组件白名单/页面×状态矩阵/变更流程 | +| `templates/anti-patterns.md` | 22 条视觉禁飞区,每条含症状·为什么坏·正确做法·机器检测方式 | +| `templates/qa-checklist.md` | 8 组视觉验收清单,全部可对着截图回答 | +| `templates/stylelint.config.mjs` | 把硬规则变成会 `exit 1` 的检查 | +| `templates/vue/Ui{Button,Input,Select,Card,Badge,EmptyState}.vue` | 6 个 Vue 3 原语,已覆盖 4 种交互状态、零颜色字面量 | + +### references/ —— 按需加载的方法说明 + +| 文件 | 什么时候读 | +|---|---| +| `references/bootstrap.md` | 阶段一:为什么必须先建约束、12 项产出清单与验收判据 | +| `references/build.md` | 阶段二:单页面实现顺序、契约层与 mock、切真后端判据 | +| `references/tokens-baseline.md` | Token 数值为什么是这些数字;三种项目类型的调参;迁移做法 | +| `references/screenshot-and-qa.md` | 零依赖截图方案(实测可用)、截不到时的降级策略、验收回路、MCP 选型 | +| `references/reference-sources.md` | 去哪找参照图、**为什么 AI 不能替用户选参照物**、常见错误 | +| `references/prompts.md` | 6 个可直接复制的提示词模板 + 好坏对照表 | + +### scripts/ —— 阶段一生成到项目里的 + +| 脚本 | 作用 | +|---|---| +| `scripts/capture.mjs` | 零依赖截图(自动探测 Chrome/Edge,多宽度,SPA 等待) | +| `scripts/audit-css.mjs` | 设计系统漂移记分卡(`:root` 块数、颜色近重复聚类、字号/圆角档位、token 覆盖率、冗余声明),任一超阈值 `exit 1`;支持 `--ignore <片段>` 排除 token 文件这类"字面量本来就合法"的文件;「同一选择器分散在多块」只提示、不参与判定 | + +--- + +## 关键提醒 + +**参照物是价值最高的一项(约占 80%),而它的选择不能由 AI 代劳。** +"好看"是用户的品味判断,不在代码里也不在任何可检索的资料里。 +AI 应该产出**候选集 + 特征分析 + 截图**,由用户做最终选择。 +详见 `references/reference-sources.md` 第 4 节。 + +**如果项目已有界面,优先取现状中最后生效的那个主色**, +而不是新造一个 —— 迁移在视觉上接近中性,阻力最小。 + +**截图能力已实测**(本环境零依赖):Chrome headless 675ms 产出 PNG, +可正常读图。不要因为"没有截图工具"而跳过验收 —— `capture.mjs` 今天就能用。 diff --git a/.agents/skills/frontend-craft/references/bootstrap.md b/.agents/skills/frontend-craft/references/bootstrap.md new file mode 100644 index 0000000..7bf54dc --- /dev/null +++ b/.agents/skills/frontend-craft/references/bootstrap.md @@ -0,0 +1,153 @@ +# bootstrap.md — 阶段一:先把"约束"建起来,再写界面 + +> **本文回答**:"先让 AI 按 skill 把仓库契约文件、可执行检查、验收回路都准备好, +> 然后再按 skill 生成前端代码,这样做出来会不会更好?" +> +> **答案是会,而且这是这套方法里最重要的一步。** 但要说清它好在哪里 —— +> 它提升的是**下限和稳定性**,不是审美上限。 + +--- + +## 为什么必须先做这一步 + +对比两种做法,同一个 AI、同一个需求: + +**做法 A(绝大多数 vibe coding)**:直接说"给我做个工作台界面"。 +AI 从零开始,每个决定都是即兴的:这个绿还是那个绿?12px 还是 13px? +圆角 8 还是 10?——每个决定单独看都合理,合起来就是 7 个绿 + 21 种圆角 + +19 种字号。**不是因为它审美差,而是因为它一共要做 200 个决定, +其中 190 个没有依据。** + +**做法 B(先建约束)**:先花一轮把 token 表定下来(7 档字号、4 档圆角、 +4 个有彩色),把原语组件写出来,把 lint 打开,把截图脚本跑通。 +然后再生成界面 —— 此时 AI 要做的是**选择**而不是**发明**:正文用 +`--text-base`,卡片用 `--radius-md`,主色用 `--c-accent`。 + +差别在于:做法 A 里"12px"和"13px"是两个同等合法的即兴创作; +做法 B 里只有一个存在,另一个在 lint 里会报错。 + +> **一句话**:这一步不产生任何业务价值,它的全部价值是让后面每一步都有依据。 +> 跳过它,后面 200 个决定就全部失去约束,而这些决定是不可逆地叠加的。 + +--- + +## 阶段一的产出清单 + +按顺序做,每一项都必须**落盘成文件**。没落盘 = 没做(下一轮会话就忘了)。 + +| # | 产出 | 路径 | 验收方式 | +|---|---|---|---| +| A1 | 目录骨架 | `src/{components/{ui,layout},styles,api,mocks,views}` | 目录存在 | +| A2 | Design Token | `src/styles/tokens.css` | `audit-css.mjs` 的 `:root` 块数 == 1 | +| A3 | 视觉契约 | `docs/design/DESIGN.md` | 参照物表非空;状态矩阵填满 | +| A4 | 负面清单 | `docs/design/anti-patterns.md` | 至少 10 条 | +| A5 | 原语组件 | `src/components/ui/Ui{Button,Input,Select,Card,Badge,EmptyState}.vue` | 每个覆盖 4 种状态;无字面量颜色 | +| A6 | 布局骨架 | `src/components/layout/AppShell.vue` | **只有灰块**,无业务内容 | +| A7 | 契约层 | `src/api/types.ts` + `client.ts` | 类型完整;唯一网络出口 | +| A8 | Mock | `src/mocks/*` | 能返回 typed 假数据 | +| A9 | lint | `stylelint.config.mjs` + `package.json` 脚本 | 在干净项目上通过 | +| A10 | 漂移审计 | `scripts/audit-css.mjs` | 能跑出报告 | +| A11 | 截图 | `scripts/capture.mjs` | 能产出非空 PNG | +| A12 | 验收清单 | `docs/design/qa-checklist.md` | 清单已按项目调整 | + +**A6 的"只有灰块"是硬要求。** 先用灰色矩形把布局占满,确认结构、间距、 +断点都对了,再往里填内容。先填内容再调布局,等于在移动的靶子上画画 —— +你会发现每次改内容都要重调布局。 + +--- + +## A2 / A3 的具体做法 + +### 定 token(这一步决定了 80% 的后续自由度) + +**不要从零发明。** 直接抄 `tokens-baseline.md` 里的数值表,只调三件事: + +1. **正文基准字号**:密集工具界面 13px;阅读/营销型 15~16px。 +2. **主色**:从参照物或品牌色取一个。 + > 如果项目已有界面,**优先取现状里最后生效的那个主色**, + > 而不是新造一个。这样迁移看起来是"整理"而不是"换风格", + > 阻力最小,也不会引起"又改配色"的反弹。 +3. **中性色偏色**:冷灰(偏蓝)显得技术、理性;暖灰(偏黄/绿)显得柔和。 + +其余档位(间距 13 档、圆角 3 档、语义色 3 个)**不要动**,它们不是审美选择, +是防膨胀的结构。 + +### 定参照物(这一步决定上限) + +见 `reference-sources.md`。核心:**AI 不能替用户选参照物** —— +因为"好看"是用户的品味判断,不是技术判断。AI 可以找到候选、 +可以描述差异、可以指出某个参照物和你的产品调性不匹配, +但最终选定必须由人做。 + +如果没有参照物,产出会稳定地落在"整洁但平庸"这个位置。 + +--- + +## A9 / A10 / A11 为什么算"阶段一"的一部分 + +因为它们不是"代码质量工具",它们是**约束的执行机构**。 + +- 一句写在文档里的"禁止字面量颜色",AI 有 5% 概率违反且你不会知道。 +- 一条会 `exit 1` 的 stylelint 规则,违反时你**一定会知道**。 + +**规则的价值不在于被写下来,而在于被违反时会发生什么。** +文字规则违反后什么都不会发生 —— 这就是为什么单靠 SKILL.md 不够。 + +同理,`capture.mjs` 让"看一眼"从一件麻烦事变成一条命令。 +如果截图很麻烦,它就不会发生;不发生的验收等于没有验收。 + +--- + +## 阶段一结束的判据 + +全部满足才算阶段一完成: + +- [ ] `npx stylelint "src/**/*.{css,vue}"` 在**新代码**上 0 error。 + 迁移期 legacy 文件用下面的 audit 跟踪收敛,不进这个硬门 —— + 一开就是 600+ 条报错时,人会习惯性忽略输出,那等于没开。 +- [ ] **token 文件本身干净**:`node scripts/audit-css.mjs src/styles/tokens.css` + 的报告里 `:root` 块 = 1、变量无重复声明、**冗余声明(同属性重复声明)= 0**。 + > 「同一选择器分散在多块」已降级为提示项,不计入这条判据 —— + > 各块属性互不重叠时它并不冗余,把它归零反而要冒合并块带来的渲染风险。 + > ⚠️ **不要拿"全绿"当这条的判据。** token 文件按定义必然让 + > `token 覆盖率` 和 `彩色 distinct` 两项 FAIL —— 它**就是**那些 + > 字面量的唯一合法来源。这两项要在「排除 token 文件之后」的主样式上量: + > `node scripts/audit-css.mjs src --ignore tokens.css`。 +- [ ] `node scripts/audit-css.mjs src --ignore tokens.css` 能跑出报告, + 并把当前分数**存档**(这就是迁移的"前"分数,阶段二每一步都要跟它比) +- [ ] `node scripts/capture.mjs <本地 URL> shots` 产出非空 PNG, + 且**你亲眼看过**那张图(没看过的截图等于没截) +- [ ] **迁移项目专用**:引入 token 文件后重截,与引入前**逐像素一致**。 + 阶段一的硬要求是"零视觉变化" —— 地基不该改变外观,只改变依据。 +- [ ] 原语组件在 `AppShell` 灰块骨架里渲染正常(**新建项目**要求; + 迁移项目跳过,不要为了这一条去重建已有的壳) +- [ ] `DESIGN.md` 的参照物表和状态矩阵已填 +- [ ] `api/types.js`(JS 项目用 JSDoc `@typedef`)或 `api/types.ts`(TS 项目) + 覆盖了所有接口的形状,且**形状来自真实响应**而不是凭印象写的。 + 把取证记录(哪个端点、HTTP 码、响应大小、观测到的枚举值)写进文件末尾。 + +**此时应该能截到一张"结构正确但内容空洞"的界面**(新建项目), +或者一张"和改造前逐像素相同、但依据已经换掉"的界面(迁移项目)。 +这张图就是后续所有页面的地基。 + +--- + +## 诚实的边界 + +做这一步**会**带来: + +- 一致性的数量级提升(不再有 7 个绿、21 种圆角) +- 空态/错态/hover/focus 不再遗漏(因为原语层就要求覆盖) +- 新会话不用重新解释规范(规范在仓库里) +- 从 mock 切真后端时改动极小(契约是真的) +- 漂移可被机器发现,而不是靠人眼 + +做这一步**不会**带来: + +- 审美上限。参照物选得差,产出依然是"整洁的差品味"。 +- 信息架构能力。把不该出现在正文里的控件状态写进正文(AP-18), + token 和 lint 都拦不住 —— 那是产品判断。 +- 阻止用户自己用临时 prompt 破坏体系。skill 管不住旁边那个人。 + +> **所以:这一步把"很烂"提到"整洁统一",这是真实且巨大的提升。 +> 但"整洁"到"优秀"之间那一步,靠的是参照物和人的品味判断。** diff --git a/.agents/skills/frontend-craft/references/build.md b/.agents/skills/frontend-craft/references/build.md new file mode 100644 index 0000000..2b740d6 --- /dev/null +++ b/.agents/skills/frontend-craft/references/build.md @@ -0,0 +1,173 @@ +# build.md — 阶段二:按契约生成界面 + +> 前置:阶段一已完成(`bootstrap.md` 的判据全部满足)。 +> 此时你有:token、原语、灰块骨架、契约+mock、lint、截图脚本、验收清单。 +> +> 本文是**单个页面/组件**的实现流程。核心是**顺序**。 + +--- + +## 铁律:顺序不能颠倒 + +``` +灰块结构 → 空态 + 错态 → 加载态 → 正常态 → 边界态 → 截图验收 +``` + +### 为什么"空态和错态要在正常态之前" + +这是最反直觉、也最有效的一条。 + +**做正常态时,你脑子里有一份"数据长什么样"的完整图景**, +于是布局自然就根据数据反推出来了。等做完正常态再补空态, +你会发现空态需要的引导按钮、提示文案、居中容器, +在正常态的布局里**没有位置** —— 于是只能硬塞,或者草草了事。 + +**反过来做**:先做空态。空态要求你回答"这个页面在没有数据时, +用户应该做什么"。这个问题会倒逼你想清楚页面的**主要目的**, +而这个理解会直接改善正常态的设计。 + +而且从用户视角看:**新用户第一次打开就是空态。** +只做正常态,等于让第一批用户看到一个半成品。 + +> 顺序反了 = 空态永远补不上。这不是推测,是几乎所有项目的实际结局。 + +### 为什么"灰块结构"要在最前面 + +先只用灰块把布局撑起来,确认:区块位置、间距、断点、滚动行为。 +此时不涉及颜色字号 —— 所以布局问题不会被视觉细节掩盖。 + +**先填内容再调布局,等于在移动的靶子上画画。** 每加一个字段, +布局就要重调一次,而且永远调不完。 + +--- + +## 单页面实现流程 + +### 第 1 步:读约束(不要跳过) + +- `DESIGN.md` 第 5 节(组件白名单)→ 你能用哪些原语 +- `DESIGN.md` 第 6 节(状态矩阵)→ 本页要覆盖哪些状态 +- `DESIGN.md` 第 2 节 + `reference/` → 参照物,以及"取什么/不取什么" +- `anti-patterns.md` → 禁止事项 + +**第 1 步的信号**:如果读完你还说不出这个页面的"主要目的是什么", +停下,去问,不要开始写。 + +### 第 2 步:灰块结构 + +只做:容器、分区、栅格、间距、断点。 +不做:颜色、字号、图标、真实文本(用等高灰条占位)。 + +**验收**:截一张图,只看结构。区块的层级关系清楚吗? +间距有节奏吗?375px 下不崩吗? + +### 第 3 步:空态 + +- 一句说明"这里是空的,因为……" +- 一个**主操作**(不是三个) +- 视觉居中,不要贴在页面顶部 +- 用 `UiEmptyState` 原语 + +### 第 4 步:错态 + +- 人能看懂的错误信息(不是 `Error: 500`) +- 一个**重试**操作 +- 保留页面框架(不要整页替换成错误页,除非是致命错误) + +### 第 5 步:加载态 + +- **优先骨架屏**,不用 spinner(骨架屏能保留布局,不跳动) +- 骨架屏的尺寸要接近真实内容,否则加载完会跳一下 +- 超过 300ms 才显示(避免闪烁) + +### 第 6 步:正常态 + +此时布局和所有状态容器都已就位,你只是在往既定框架里填内容。 +**这一步会异常顺利** —— 这就是前面顺序的价值。 + +### 第 7 步:边界态 + +- 超长标题(截断 + tooltip) +- 空字段(显示 `—`,不是留白或 `null`) +- 单条数据 / 极多数据 +- 窄屏 375px + +### 第 8 步:截图验收 + +按 `qa-checklist.md` 走一遍。**必须真的看图**,不是看代码。 + +--- + +## 契约层与 mock + +### 唯一网络出口 + +``` +src/api/types.ts ← 所有接口的形状(类型) +src/api/client.ts ← 唯一的网络调用处 +src/mocks/* ← 假数据,形状与 types.ts 一致 +``` + +**组件永远不直接 fetch。** 组件只调用 `client.ts` 里导出的 typed 函数。 + +为什么: +- 接口改字段 → 只改 client 和 types,不用改 N 个组件 +- 能在没有后端时开发界面(这是整个方法的前提) +- 错误处理、鉴权、loading 可以统一 + +### 强制切换状态做验收 + +在开发模式下支持 URL 参数强制状态: + +```ts +// src/api/client.ts +const forced = new URLSearchParams(location.search).get('state') +if (forced === 'empty') return MOCK_EMPTY +if (forced === 'error') throw new ApiError('模拟失败') +if (forced === 'loading') return new Promise(() => {}) // 永不 resolve +``` + +这样 `?state=empty` / `?state=error` 可以直接截图, +不用手工造数据。**这是让验收变得便宜的关键技巧** —— +如果造一个错误态要 5 分钟,你就不会去验收错误态。 + +### 切真后端的判据 + +> **改动应该极小。** + +切换时只应该改 `client.ts` 里 mock 与真实请求的替换,外加少量字段适配。 +如果页面代码要改 >10%,说明契约是假的 —— 你当初写的类型 +只是"看起来像"接口,实际上页面在直接依赖 mock 的具体形状。 + +**此时不要修页面,回退去修契约。** +在页面打补丁会让下一次接口变更更贵。 + +--- + +## 常见错误 + +| 错误 | 后果 | 正确做法 | +|---|---|---| +| 先做正常态 | 空态/错态永远补不上 | 空态错态先行 | +| 边写结构边定颜色 | 布局问题被视觉细节掩盖 | 灰块阶段只做结构 | +| 组件内直接 fetch | 接口一改改 20 个文件 | 唯一网络出口 | +| 一次改多个维度 | 出问题无法归因 | 一次一个,改完截图 | +| 让 AI 自评"看起来不错" | 它没有眼睛,是编造 | 截图 + 独立 agent 审查 | +| 空态放在页面顶部一行灰字 | 用户不知道要干什么 | 居中 + 主操作引导 | +| 加载态用整页 spinner | 加载完布局跳动 | 骨架屏 | +| 契约不严(用 any) | 切真后端时全崩 | 类型完整 + mock 符合类型 | + +--- + +## 完成一个页面的定义 + +- [ ] `DESIGN.md` 状态矩阵里本页那一行**全部勾上** +- [ ] 截图覆盖:正常/空/错/加载 + 375px + 1280px +- [ ] `qa-checklist.md` 8 组全部通过 +- [ ] stylelint 通过 +- [ ] `audit-css.mjs` 无新增违规(颜色/字号/圆角档位数量没涨) +- [ ] 独立 subagent 审查过(见 `prompts.md` 第 5 节) +- [ ] `DESIGN.md` 如有新决定,已回写 + +> **最后一条最容易漏。** 实现过程中产生的新决定("这个页面用 24px 顶部留白") +> 如果不回写文档,下一轮会话就不知道,于是又发明一个新值 —— 漂移就是这么开始的。 diff --git a/.agents/skills/frontend-craft/references/prompts.md b/.agents/skills/frontend-craft/references/prompts.md new file mode 100644 index 0000000..705a1f1 --- /dev/null +++ b/.agents/skills/frontend-craft/references/prompts.md @@ -0,0 +1,241 @@ +# prompts.md — 提示词模板 + +> **原理**:prompt 是**短时记忆**,仓库是**长期记忆**。 +> +> 不要试图在 prompt 里写完整规范 —— 那会随会话消失。prompt 的职责只有三件事: +> 1. **指向**仓库里的约束("先读 X") +> 2. **定义**这一轮的验收标准 +> 3. **禁止** AI 的默认行为 +> +> 维持一致性是仓库文件的职责,不是 prompt 的职责。 +> prompt 写得再好,也管不住第 20 轮。 + +--- + +## 0. 通用约束块(所有 prompt 都带上) + +复制这段作为每个 prompt 的开头,按项目替换路径: + +``` +【约束来源 — 先读,再动手】 +1. docs/design/DESIGN.md —— 视觉契约(参照物、组件白名单、状态矩阵) +2. src/styles/tokens.css —— 唯一的设计变量来源 +3. docs/design/anti-patterns.md —— 禁止事项,强制阅读 +4. docs/design/qa-checklist.md —— 验收标准 + +【硬规则 — 违反即返工】 +- 禁止字面量颜色(#xxx / rgb() / hsl());颜色只能用 var(--c-*) +- 禁止设计维度写裸 px(间距/字号/圆角/行高);只能用 var(--space-* / --text-* / --radius-* / --leading-*) + 例外:1px 边框线宽 +- 禁止新增 :root 块或任何 CSS 变量定义 +- 禁止 !important +- 禁止 outline: none +- 禁止修改已有 class 名或已有 token 值 +- 禁止新增全局 CSS 文件;样式写在组件内或既有文件里 +- 只使用 DESIGN.md 组件白名单里的原语;缺什么先告诉我,不要就地手写 +- 禁止"顺便优化":只做我要求的事。发现其他问题请**单独列出**,不要顺手改。 + +【流程】 +先给我一份改动计划(要改哪些文件、每处改什么),我确认后你再写代码。 +``` + +> **最后一句很重要**:先给计划再写代码,能在 30 秒内发现方向错误, +> 而不是等它写完 300 行才发现理解偏了。 + +--- + +## 1. 阶段一:搭基础设施 + +``` +【任务】为这个项目搭建设计系统的地基。**不要写任何业务界面。** + +范围严格限定在以下产出,一次做完,逐个告诉我完成情况: + +1. src/styles/tokens.css + —— 复制 templates/tokens.css 的内容(我将提供), + 只调整三处:正文基准字号、主色、中性色偏色。其余档位不要动。 + +2. docs/design/DESIGN.md + —— 用 templates/DESIGN.md 作骨架。 + 第 1 节"一句话定位"先留空等我填。 + 第 2 节参照物表先留空(我会提供截图)。 + 第 6 节状态矩阵:列出我下面说的这几个页面,但状态先不勾。 + +3. src/components/ui/ 下的 6 个原语 + —— 用 templates/vue/ 里的模板(我将提供)。 + 每个组件必须覆盖 default / hover / focus-visible / disabled。 + +4. src/components/layout/AppShell.vue + —— **只放灰色方块占位**,把布局骨架撑起来。不要填任何业务内容。 + +5. stylelint.config.mjs + package.json 的 lint:css 脚本 + —— 复制模板。跑一次,把现有违规列出来(先不要修)。 + +6. scripts/audit-css.mjs 和 scripts/capture.mjs + —— 复制模板。各跑一次,把输出贴给我。 + +【验收】 +- npx stylelint 能跑起来 +- node scripts/audit-css.mjs src/styles/tokens.css 输出全绿 +- node scripts/capture.mjs 能产出一张非空 PNG,并把路径给我 + +【禁止】 +- 不要创建任何 views/ 下的页面 +- 不要写任何 API 调用 +- 不要"顺便"安装 UI 组件库 +- 不要改动 ui/src/styles/main.css 的现有内容(后面单独处理迁移) +``` + +--- + +## 2. 生成原语组件 + +``` +【任务】创建 src/components/ui/UiStat.vue —— 一个展示"标签 + 数值"的小组件。 + +【约束】见前面的通用约束块。 + +【要求】 +- props: label (string, 必填), value (string|number, 必填), tone ('neutral'|'accent'|'success'|'warning'|'danger', 默认 'neutral') +- label 用 var(--text-sm) + var(--c-text-secondary) +- value 用 var(--text-md) + var(--c-text) + font-weight 600 +- 两者垂直排列,间距 var(--space-4) +- tone 只影响 value 的颜色,用现有的语义色 token +- 容器不设边框、不设背景(它是嵌入式组件,不是卡片) +- **超长文本**:value 用 overflow-wrap: anywhere 且不设死宽度 + +【验收 — 写成可核对条目】 +□ 组件内没有任何颜色字面量(我会 grep) +□ 组件内没有裸 px(1px 边框除外) +□ 5 个 tone 值都有对应样式,无遗漏 +□ 新组件没有修改任何既有文件的既有 class + +【完成后】告诉我:(1) 文件路径 (2) 用到了哪些 token (3) 有没有发现 DESIGN.md 里缺失的定义 +``` + +--- + +## 3. 生成一个页面 + +``` +【任务】实现 "会话列表" 页面(src/views/SessionListView.vue)。 + +【前置】先读: +- docs/design/DESIGN.md 第 5 节(组件白名单)和第 6 节(状态矩阵,本页那一行) +- docs/design/reference/linear-inbox.png(参照图 — 取什么/不取什么见 direction.md) + +【顺序要求 — 必须按这个顺序做,每一步做完跟我说一声】 +第 1 步:只搭结构。用 AppShell + 灰块,不定颜色不定字号,先把区块位置和间距定下来。 +第 2 步:**先做空态和错误态**。空态要有引导文案和一个主操作按钮;错误态要有重试。 +第 3 步:再做加载态(骨架屏,不用 spinner)。 +第 4 步:最后做正常态(有数据)。 +第 5 步:补超长内容态(长标题截断 + tooltip)。 + +【实现约束】 +- 数据从 src/api/client.ts 取,不要直接 fetch +- 类型来自 src/api/types.ts +- 列表项用 UiCard 或纯 div?——按参照图的密度决定,并说明你的选择理由 +- 所有间距来自 --space-*,所有文字来自 --text-* + +【验收】 +□ 上面的状态矩阵这一行,6 个状态全部有实现 +□ 每个可点击元素有 hover 和 focus-visible +□ 长标题(100 字)不溢出、不撑破容器 +□ 375px 宽度下布局不崩(侧栏折叠) +□ 页面内 0 个字面量颜色、0 个裸 px(1px 除外) + +【交付后】我会截图验收。先不要自评"看起来不错",把改动计划列出来即可。 +``` + +--- + +## 4. 修改类 prompt(最容易出 AP-10 的场景) + +``` +【任务】把主按钮的背景色改成 var(--c-accent-active)。 + +【严格范围】 +- 只改 UiButton.vue 里 primary variant 的默认态背景色,**一处** +- 不要动 hover / active / disabled 态 +- 不要动其他 variant +- 不要动圆角、间距、字号、阴影 +- **不要改 :root 里的 token 值** + +【禁止的做法】 +❌ 在文件末尾追加一段 .ui-button--primary { background: ... } 覆盖 +❌ 加 !important +❌ 新增一个变量 + +【正确的做法】 +找到定义 .ui-button--primary 背景色的那一行,直接改那个值。 + +【完成后】 +1. 贴出 diff(只应有 1 行变化) +2. 告诉我你在文件里搜到的相关规则有哪几处(确认没有遗漏的重复定义) + +【如果发现其他问题】单独列出来,不要顺手改。 +``` + +> **"贴出 diff" 是这条 prompt 的关键。** 它把"是否只改了该改的" +> 从主观判断变成了可核对的事实。 + +--- + +## 5. 视觉验收 prompt(给独立审查 agent) + +**开一个全新的 subagent**,只给它截图和清单,不要告诉它代码是谁写的: + +``` +【任务】审查一张界面截图,找出所有视觉问题。 + +【输入】截图路径:shots/home-1280.png + +【方法】 +对照 docs/design/qa-checklist.md 的 8 组检查项,**按顺序**逐条过。 +前 3 组(对齐/灰度/控件一致性)是重点。 + +【输出要求】 +对每个问题给出三要素: +1. 问题位置 —— 哪个元素(用截图里能识别的描述) +2. 具体差距 —— 数值化的,例如"工具栏三个控件高度分别约 28/36/32px" +3. 建议改法 —— 能直接翻译成一行 CSS 改动 + +【禁止】 +- 不要评价优点,只找问题 +- 不要写"看起来不够精致"这类无法执行的反馈 +- 不要一次列出 20 条:按严重程度排序,最多 8 条 + +【最终】给出一个判断:这张图的**最主要**的一个问题是什么? +``` + +> 独立 agent 为什么更有效:它的上下文里没有"我这么写是有道理的"这笔账。 +> 它只看图,所以更接近用户的视角。 + +--- + +## 6. 好不好对照表 + +| ❌ 差的 prompt | ✅ 好的 prompt | 差在哪 | +|---|---|---| +| "帮我做个好看的后台界面" | 见上面模板 3(含顺序、约束、验收) | 没有约束来源、没有顺序、没有验收标准 | +| "把界面改好看一点" | "工具栏三个控件高度统一到 --control-h-md" | 形容词 vs 数值 | +| "优化一下样式" | "只改 X 一处,贴出 diff" | 范围不封顶 = 无限改动授权 | +| "用 Tailwind 重写" | "用现有 token 和原语,不引入新依赖" | 引入新体系 = 第七种绿 | +| "顺便看看有没有其他问题" | "发现其他问题请单独列出,**不要**顺手改" | 前者授权了漂移 | +| "这里改一下,那里也调一下" | 一次一个维度,改完截图确认 | 无法归因 | +| (无)"你确认一下这样对吗" | "先复述一遍约束,再动手" | 让 AI 先暴露理解偏差 | + +--- + +## 7. 三条最容易被忽略但最有效的技巧 + +1. **让它先复述约束。** + "在写代码前,先复述一遍你从 anti-patterns.md 里读到的禁止事项。" + —— 复述出错 = 它没读或理解偏了,此时拦住成本最低。 + +2. **让它贴 diff。** + 把"是否越界"从主观判断变成可核对事实。一行 prompt 换来一次真正的范围审查。 + +3. **明确禁止"顺便优化"。** + 这是 CSS 膨胀的头号来源。用户项目里那 6 段 `/* refinement */` 追加块、 + 66 条重复声明,绝大多数来自"顺手改一下"。 diff --git a/.agents/skills/frontend-craft/references/reference-sources.md b/.agents/skills/frontend-craft/references/reference-sources.md new file mode 100644 index 0000000..954cdf8 --- /dev/null +++ b/.agents/skills/frontend-craft/references/reference-sources.md @@ -0,0 +1,154 @@ +# reference-sources.md — 参照物:去哪找、怎么用、以及 AI 的边界 + +> **本文回答**:"skill 里怎么写清楚去哪里找截图?" +> +> 更重要的:**参照物是这套方法里价值最高的一项**(约占 80% 的效果), +> 而它的选择**不能由 AI 代劳**。本文说清为什么,以及 AI 到底能帮什么。 + +--- + +## 1. 为什么参照物比方法论值钱 + +文字描述风格 → few-shot 图像 的效力差距是数量级的: + +| 说法 | AI 实际接收到的信息量 | +|---|---| +| "要简洁、克制、有工具感" | 几乎为零。"简洁"在不同实现里可以差 200px 圆角 | +| "看这张图,取它的列表密度和分割线用法" | 具体到可直接映射到行高、边框、间距 | + +**所以:一个字都别写"高级感",放一张图。** +一次给 3~5 张同类产品的截图,胜过 500 字风格描述。 + +--- + +## 2. 去哪找参照图 + +### 2.1 直接截真实产品(**首选**) + +对 AI 编码工作台这类产品,最好的参照物就是做得好的同类产品本身。 +用 `capture.mjs` 直接截: + +```bash +node scripts/capture.mjs https://linear.app docs/design/reference --widths 1440 --name linear-home +``` + +推荐来源(按本产品调性排序): + +| 产品 | 值得取什么 | +|---|---| +| **Linear** | 列表密度、hover 反馈的克制、工具栏控件高度一致性 | +| **Vercel Dashboard** | 空态设计、表格排版、灰度层级 | +| **GitHub** | 侧栏树形结构、徽标用法、信息密度 | +| **Stripe Docs** | 三栏布局、代码块样式、阅读节奏 | +| **Supabase** | 数据密集型界面、状态色用法 | +| **Resend** | 极简表单、空态引导 | +| **Notion** | 折叠交互、图标栏 | +| **Raycast** | 命令面板密度、键盘优先的焦点态 | + +> **优点**:这些界面经过真实用户 8 小时/天的检验,不是概念稿。 +> 它们的每个数值都是被使用打磨过的,直接可抄。 + +### 2.2 产品截图库 + +| 站点 | 特点 | 注意 | +|---|---|---| +| [Refero](https://refero.design) | 真实产品 UI 截图,可按组件/页面筛 | **最推荐**,都是实现而非概念 | +| [Mobbin](https://mobbin.com) | 移动 + Web 产品截图库 | 部分需付费 | +| [SaaS Interface](https://saasinterface.com) | SaaS 界面合集 | 质量参差,需筛 | +| [Page Flows](https://pageflows.com) | **用户流程录像** | 看流程交互最好用,不是静态图 | +| [Land-book](https://land-book.com) | 落地页灵感 | 偏营销页,工具界面慎用 | +| [Godly](https://godly.website) | 网页设计灵感 | 多为概念/实验性,**不要用于工具类产品** | + +### 2.3 中文源 + +| 站点 | 特点 | +|---|---| +| [站酷](https://www.zcool.com.cn) | 视觉设计为主,UI 分类下有界面稿 | +| [UI 中国](https://www.ui.cn) | 国内 UI 设计社区 | +| 各产品的官方设计规范 | 如 Ant Design / Arco / Semi 的官网 —— **本身就是可抄的参照物** | + +### 2.4 ⚠️ Dribbble 类的陷阱 + +[Dribbble](https://dribbble.com) 上大量是**概念稿(concept)**,不是实现: + +- 圆角 24px、字重极细、大量留白、渐变 —— 在 1440px 大图上漂亮, + 做成真实界面后**信息密度低到不可用** +- 常忽略 hover/focus/disabled/空态 —— 正是真实产品最需要的部分 +- 配色在真实屏幕上往往对比度不足 + +**结论**:Dribbble 可以用来找**局部灵感**(一个卡片的样子、 +一个图表的画法),**不要**用它定整体布局和信息密度。 +工具类产品尤其危险。 + +--- + +## 3. 怎么用参照物:`direction.md` + +在 `docs/design/reference/` 下建一份 `direction.md`,每个参照物两句话: + +```markdown +## linear-inbox.png (取自 linear.app/inbox) + +**取**: +- 列表行高紧凑(约 44px),行间只有 1px 分隔线,不用卡片包裹 +- hover 时只有背景变浅(--c-surface-hover),没有位移和缩放 +- 工具栏所有控件同高,图标 16px,图标与文字间距 8px + +**不取**: +- 深色主题(我们是浅色) +- 键盘快捷键徽标(我们暂无快捷键体系) +``` + +**"不取"这一栏比"取"更重要。** 因为参照图总带着一堆你不想要的特征, +不显式排除,AI 会把它们一起学过来 —— 包括深色主题这种大改。 + +--- + +## 4. AI 在"选参照物"上的边界(重要) + +### AI **不能**替你做的 + +**选定哪张图作为美的标准。** + +原因不是能力问题,是**信息问题**:好看与否是**你的品味判断**, +不在代码里、不在任何可检索的资料里。AI 说"这张更高级", +只是在复述训练数据里的流行趋势,不代表它符合你的产品。 + +如果 AI 替你选了,你会得到一个"符合平均审美"的界面 —— +而"平均"正是"平庸"的另一种写法。 + +### AI **能**替你做的 + +| 能做 | 具体做法 | +|---|---| +| 找候选 | 按"密集工具界面 / 浅色 / 中文字体友好 / 有侧栏和列表"筛选,给 5~8 个候选 | +| 分类 | "这 3 个是高密度方案,那 2 个是留白方案,它们的取舍是 X" | +| 指出调性冲突 | "这张是营销落地页,圆角和留白都偏大,用它做工具界面会显得松散" | +| 提取可移植特征 | "这张图的行高是 44px、图标 16px、无卡片包裹 —— 这三条可以直接用" | +| 对比差距 | 截图和参照图并排,列出 3 条具体差距 | +| 截图 | `capture.mjs` 对候选 URL 批量截图,铺成一张对比图给你看 | + +**所以正确的分工是**:AI 产出**候选集 + 特征分析 + 截图**, +你在候选集里**做最终选择**。这一步花 5 分钟,但它决定后面所有工作的上限。 + +--- + +## 5. 参照物的常见错误 + +| 错误 | 后果 | 正确做法 | +|---|---|---| +| 选营销落地页给工具界面做参照 | 间距过大、信息密度过低,界面显得空 | 参照物必须是**同类型**界面 | +| 选深色主题参照物 | AI 会连深色一起学 | 在"不取"里显式排除 | +| 只给 1 张 | AI 会连它的偶然特征一起复制 | 给 3~5 张,取交集 | +| 只给形容词描述 | 无信息量 | 必须给图 | +| 参照图和产品调性冲突 | 越努力越偏 | 先写 DESIGN.md 第 1 节的"一句话定位",再据此筛图 | +| 参照图不入库 | 下轮会话丢失 | 存 `docs/design/reference/`,路径写进 DESIGN.md | + +--- + +## 6. 一句话总结 + +> **参照物决定上限,token 和 lint 决定下限。** +> AI 负责守住下限(这部分它可以自动化), +> 你负责用参照物定上限(这部分只有你能做)。 +> 两边都做了,才有"统一且优质";只做前者,得到的是"统一且平庸"。 diff --git a/.agents/skills/frontend-craft/references/screenshot-and-qa.md b/.agents/skills/frontend-craft/references/screenshot-and-qa.md new file mode 100644 index 0000000..fdba696 --- /dev/null +++ b/.agents/skills/frontend-craft/references/screenshot-and-qa.md @@ -0,0 +1,207 @@ +# screenshot-and-qa.md — 截图与验收回路 + +> **本文回答**:"AI 能否为我准备截图?skill 里怎么写清楚去哪里找截图、怎么截图? +> 有没有相应的 MCP 工具?" +> +> 答:**能,而且在当前环境下已经完全实测跑通,零额外依赖。** + +--- + +## 1. 为什么截图是这套方法的核心 + +前面所有约束(token、lint、清单)都在防止"变烂"。 +只有截图能让界面**变好** —— 因为它是唯一能让 AI 察觉自己在做什么的手段。 + +**没有截图的循环是这样的**:生成代码 → 看代码 → 觉得合理 → 交付。 +问题:AI 看代码时看到的是 `padding: var(--space-12)`,而用户看到的是 +"这个按钮和旁边那个没对齐"。**代码里没有"对齐"这个信息。** + +**有截图的循环**:生成代码 → 截图 → **看图** → 挑出具体问题 → 修 → 再截图。 +只有这条回路能闭合"做 → 看 → 改"。 + +> 用户的项目里 Hero 区那行 `独立会话仓库上下文模型代号`, +> 写代码时看起来完全正常(一行文字嘛),截图看才会发现它是信息架构事故。 +> 这类问题**只能通过看图发现**。 + +--- + +## 2. 实测可用的截图方案(无依赖) + +当前环境已实测: + +- Chrome:`C:\Program Files\Google\Chrome\Application\chrome.exe` +- Edge(备用):`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe` +- 实测命令: + +```powershell +& "C:\Program Files\Google\Chrome\Application\chrome.exe" ` + --headless=new --disable-gpu --hide-scrollbars --no-sandbox ` + --window-size=800,300 ` + --screenshot="A:\path\out.png" ` + "file:///A:/path/page.html" +``` + +**实测结果:675ms 产出 13557 字节 PNG(800x300),图像内容可读。** +不需要 playwright、不需要 puppeteer、不需要任何 MCP。 + +### 关键参数 + +| 参数 | 作用 | 备注 | +|---|---|---| +| `--headless=new` | 新版无头模式 | 旧的 `--headless` 渲染差异较大,别用 | +| `--screenshot=` | 输出路径 | 必须是绝对路径或从 cwd 解析 | +| `--window-size=W,H` | 视口尺寸 | **无头模式只截视口,不截整页** | +| `--hide-scrollbars` | 隐藏滚动条 | 否则右侧多一条,干扰判断 | +| `--virtual-time-budget=5000` | 等待 JS 渲染 | **SPA 必加**,否则截到白屏 | +| `--force-device-scale-factor=2` | 2x 高清 | 需要看清 1px 细节时用 | +| `--default-background-color=FFFFFFFF` | 强制白底 | 默认透明底,PNG 里会变黑 | + +### 已知限制 + +- **只截视口**:长页面需要 `--window-size=1280,4000` 这种超高视口, + 或者分页截。 +- **需要交互才出现的状态**(hover、展开的菜单、对话框)截不到 —— + 除非做成静态 HTML fixture。见下面第 4 节。 +- **需要登录/真后端的页面**截不到 —— 除非用 mock。 + +--- + +## 3. `capture.mjs` 用法 + +(脚本由阶段一生成,放在 `scripts/capture.mjs`) + +**语法:`node capture.mjs [选项]`** —— 输出目录是**位置参数**,不是 `--out`。 + +```bash +# 基础:默认三个宽度 375 / 768 / 1440 +node scripts/capture.mjs http://localhost:5173 shots + +# 指定宽度 +node scripts/capture.mjs http://localhost:5173 shots --widths 375,1280 + +# 本地静态文件(不需要起服务) +node scripts/capture.mjs ./page.html shots --widths 1440 + +# 自定义文件名前缀 → shots/home-375.png, shots/home-1280.png +node scripts/capture.mjs http://localhost:5173 shots --widths 375,1280 --name home + +# 高清,用于看 1px 级细节 +node scripts/capture.mjs http://localhost:5173 shots --scale 2 + +# 等待 SPA 渲染(虚拟时间预算,毫秒) +node scripts/capture.mjs http://localhost:5173 shots --budget 5000 + +# 指定浏览器 +node scripts/capture.mjs http://localhost:5173 shots --chrome "D:\Chrome\chrome.exe" +``` + +输出文件名为 `-.png`,默认前缀 `shot`。 +退出码 `0` = 全部成功;`1` = 参数错误 / 未找到浏览器 / 任一张失败。 + +### 脚本必须遵守的两条工程约束 + +1. **用 `spawnSync(..., { stdio: 'ignore' })`,不要捕获子进程输出。** + 在受限沙箱(如本环境)下,管道会抛 `EPERM`。改为 + **检查输出文件是否存在且非空**来判断成功,而不是读 stdout。 +2. **自动探测浏览器**:按 `CHROME_PATH` 环境变量 → Chrome 默认路径 → + Edge 默认路径 的顺序找。找不到就报清晰的错误,不要静默失败。 + +--- + +## 4. 截不到的时候怎么办(降级策略) + +按优先级: + +1. **起本地 dev server**(Vite/Next 都有一条命令),用 `http://localhost:5173`。 + **这是最好的方式**,因为渲染的是真实组件。 +2. **静态 HTML fixture**:把组件的关键状态手写成一个独立 HTML 文件, + 直接 `file://` 截图。适合截 hover / 展开 / 错误态 —— + 因为这些在真实页面里难以复现。**这是最被低估的技巧**: + 做一个 `fixtures/states.html` 把所有状态平铺出来,一次截完。 +3. **mock 数据 + 强制状态**:在开发模式下加 URL 参数 + (`?state=empty` / `?state=error` / `?state=loading`), + 让页面能强制进入各个状态。然后批量截图。 +4. **人工截图**:确实需要登录/复杂交互时,让用户截,然后 AI 用 + `read_image` 读。**这是完全合法的路径** —— 不要因为不能自动截就跳过验收。 + +> ⚠️ 无论用哪种方式,**AI 必须真的读那张图再评论**。 +> 声称"我看到界面很整洁"但没有调用读图工具,是编造。 +> 这一点要写进 skill 的行为约束里。 + +--- + +## 5. 验收回路的四步 + +``` +截图 → 看图挑刺 → 改 → 再截图 + ↑___________________________| +``` + +### 第 1 步:截图 +用 `capture.mjs`,至少覆盖:正常态、空态、错误态、375px、1280px。 + +### 第 2 步:挑刺(关键步骤,不能跳) + +对照 `qa-checklist.md`,**按顺序**过 8 组。 +必须输出**具体**的问题,不是印象: + +| ❌ 无效反馈 | ✅ 有效反馈 | +|---|---| +| "看起来不够精致" | "工具栏三个控件高度分别是 28/36/32px,不齐" | +| "颜色有点乱" | "同行出现 #1f7667 和亮青 #2ec4b6 两个绿" | +| "间距不太对" | "卡片间距 16px,但卡片内 padding 24px,比例反了" | +| "有点空" | "空态只有一行灰字,没有引导操作" | + +**判断标准**:这条反馈能不能直接翻译成一行 CSS 改动? +不能就是无效反馈,重写。 + +### 第 3 步:改 +**一次只改一个维度。** 同时改对齐+颜色+间距,出问题时无法归因。 + +### 第 4 步:再截图 +**必须重新截图。** 改完不截图 = 没改完。 +(这一条要写死,因为跳过它的诱惑最大。) + +--- + +## 6. 独立审查者:一个被低估的技巧 + +让**生成界面的那个 agent 去审自己**,效果有限 —— 它会倾向于认为 +自己写的是对的(同一份上下文里的自我一致性偏差)。 + +更有效的做法:**开一个全新的 subagent,只给它截图和 qa-checklist, +不告诉它是谁写的**,让它独立挑刺。 + +``` +任务:这是一张界面截图(附路径)。请对照 docs/design/qa-checklist.md +逐条检查,列出所有不合格项。不要评价优点,只找问题。 +对每一条给出:问题位置(哪个元素)、具体数值差距、建议改法。 +``` + +**为什么有效**:它的上下文里没有"我刚才这么写是有道理的"这笔账。 +它只看图。 + +成本很低(一次 subagent 调用),但抓到的对齐/一致性问题的数量 +通常显著高于自审。 + +--- + +## 7. 有没有现成的 MCP 可以用 + +当前 DSH 会话**没有**注册任何浏览器/截图类 MCP。已调研的同类方案: + +| 方案 | 能做什么 | 代价 | +|---|---|---| +| [Playwright MCP](https://apify.com/nexgendata/playwright-mcp-server#1) | 真实浏览器交互、可点击后截图、可读 DOM/无障碍树 | 需要装 playwright + 配 MCP | +| [chrome-devtools-mcp](https://github.com/ChromeDevTools/chrome-devtools-mcp/blob/136da6a3/docs/cli.md?plain=1#L75-L79#1) | 直连 Chrome DevTools 协议,性能追踪 + 截图 | 需要 Node + Chrome | +| [Playwright 视觉测试实践](https://argos-ci.com/blog/playwright-mcp-visual-testing) | 截图快照基线对比(回归检测) | 适合已有稳定 UI 的项目 | + +**建议**: +- **现在就用零依赖的 `capture.mjs`** —— 它已经实测跑通,覆盖 90% 需求。 +- 需要**交互后截图**(点开菜单再截)或**读 DOM 结构**时,再上 + Playwright MCP。它比 `capture.mjs` 强的地方就是"能操作"。 +- 视觉回归(防止改 A 页面弄坏 B 页面)是另一个问题, + 属于 CI 范畴,等界面稳定了再上。 + +> 不要因为"有更好的工具"而推迟截图。`capture.mjs` 今天就能用, +> 而"等配好 Playwright 再开始验收"通常意味着永远不开始。 diff --git a/.agents/skills/frontend-craft/references/tokens-baseline.md b/.agents/skills/frontend-craft/references/tokens-baseline.md new file mode 100644 index 0000000..ae0504b --- /dev/null +++ b/.agents/skills/frontend-craft/references/tokens-baseline.md @@ -0,0 +1,222 @@ +# tokens-baseline.md — Token 数值基准与调参指南 + +> 数值的**唯一真相源**是项目里的 `src/styles/tokens.css`(由 `templates/tokens.css` 复制而来)。 +> 本文解释**这些数字为什么是这些数字**,以及不同项目类型该怎么调。 +> +> **核心原则:结构不要动,只调三件事**(正文基准字号、主色、中性色偏色)。 +> 间距档位、圆角档位、语义色数量是**防膨胀结构**,不是审美选择。 + +--- + +## 1. 完整数值表 + +### 间距 —— 2px 网格,13 档(像素命名制) + +| 档位 | 值 | 用在哪 | +|---|---|---| +| `--space-2` | 2px | 图标与紧邻文字、徽标内边距(发丝级) | +| `--space-4` | 4px | 同一行内相邻小元素 | +| `--space-6` | 6px | 紧凑行的内边距 | +| `--space-8` | 8px | 列表项内边距、按钮组间隙 | +| `--space-10` | 10px | **最常用档**:紧凑列表行内边距、卡片内边距 | +| `--space-12` | 12px | 卡片内边距、区块内元素间距 | +| `--space-14` | 14px | 稍松的行距 | +| `--space-16` | 16px | 区块之间的标准间距 | +| `--space-18` | 18px | 介于 16 与 20 之间的过渡 | +| `--space-20` | 20px | 稍大的分组间距 | +| `--space-24` | 24px | 区块之间的大间距 | +| `--space-32` | 32px | 页面级留白 | +| `--space-48` | 48px | 空状态、主标题周围 | + +**为什么用「像素命名制」**:token 名里的数字就是像素值(`--space-10` = 10px)。 +索引命名(`--space-1..N`)有两个代价:迁移时要把每个 px 查表换算成索引; +将来在中间插一档,后面所有档位的名字集体位移、所有引用都得跟着改。 +像素命名让迁移可以机械进行(按值一次替换即可),插档也不会波及已有引用 —— +**代价是"档位看起来很多",所以纪律必须写进约束里**(见本节末尾)。 + +**为什么密集工具型界面用 2px 网格 + 13 档,而不是 4/8/12/16/24 的稀疏七档**: +刻度必须跟着**真实用量分布**走,而不是跟着"常见的 4px 网格"走。密集工具型界面 +(控件小、行距紧、信息密度高)的真实间距大量落在 2px 网格上、却不在 4px 网格上 —— +实测频次形如 10px×45、8px×38、12px×33、14px×23、9px×21、2px×20、5px×20、7px×18。 +若强行套 4/8/12/16/24 七档,占 45 次的 10px 会被顶到 8px、占 23 次的 14px 会被 +顶到 16px,整页布局随之位移 —— 那是用一个尺子去改另一个尺子的活,用户看到的是 +"界面怎么变了",而不是"界面变整齐了"。 + +> **不是密集界面就别照搬这 13 档。** 阅读型 / 营销型界面间距大而疏,值本来就集中 +> 在 4px 网格上,用 4/8/12/16/24/32/48 这类稀疏刻度更不容易随手挑值。 +> 判据是"先量出这个 App 的频次表",不是"哪种刻度更流行"。 + +**档位多 ≠ 可以随手挑**:13 档是"密集界面真实用量的完整集合",不是"可选范围"。 +落地时仍然要求:同一份界面里实际出现的不同间距值越少越好(个位数为佳); +需要一个新的间距值时,先回答"现有档位为什么不行",答不上来就用现有档位。 + +### 字号 —— 7 档,相邻差 ≥2px + +| 档位 | 值 | 用途 | +|---|---|---| +| `--text-xs` | 11px | 徽标文字、极弱化标注 | +| `--text-sm` | 12px | 次级说明、时间戳、元信息 | +| `--text-base` | 13px | **正文基准**(密集界面) | +| `--text-md` | 15px | 强调正文、小标题 | +| `--text-lg` | 18px | 区块标题 | +| `--text-xl` | 24px | 页面标题 | +| `--text-2xl` | 32px | 主标题 / Hero | + +**为什么相邻差 ≥2px**:人眼分辨 12px 和 13px 很吃力(尤其在低密度屏幕上)。 +如果档位之间存在"看不出的差别",它就不构成层级,只增加噪音。 +> 用户项目里出现过 `10.5px` 和 `10px` 并存 —— 这是最典型的无效档位。 + +**为什么正文基准是 13px**:密集工具界面(每天盯 8 小时)优先信息密度。 +阅读型/营销型需要 15~16px。 + +### 圆角 —— 3 档 + 胶囊 + +| 档位 | 值 | 用于 | +|---|---|---| +| `--radius-sm` | 6px | 控件(按钮、输入框、下拉、徽标) | +| `--radius-md` | 10px | 容器(卡片、面板、弹层) | +| `--radius-lg` | 16px | 大容器(对话框、全屏面板) | +| `--radius-full` | 999px | 胶囊(开关、头像、标签) | + +**规则**:圆角是**元素尺度的函数**,不是自由参数。 +小元件配大圆角会笨拙,大容器配小圆角会廉价。 + +### 控件高度 —— 3 档 + +| 档位 | 值 | 用于 | +|---|---|---| +| `--control-h-sm` | 28px | 表格内联操作、紧凑工具栏 | +| `--control-h-md` | 36px | **默认**:表单、工具栏、主按钮 | +| `--control-h-lg` | 44px | 主行动按钮、移动端触点 | + +**这一组是被最低估的**。同一行里的下拉/输入框/按钮如果高度不同, +无论单个控件多精致,整行读起来都是"没对齐"。 +用户项目里那行 composer 底栏就是这个病。 + +### 语义色 —— 只有 3 个,有彩色总数上限 4 + +| 语义 | 主值 | 浅底 | 对比度要求 | +|---|---|---|---| +| `--c-accent`(主色) | 项目自定 | `--c-accent-subtle` | 白字对比 ≥4.5:1 | +| `--c-success` | `#2f7d5b` | `#eef7f2` | 同上 | +| `--c-warning` | `#9a6b1f` | `#fdf6e8` | 同上 | +| `--c-danger` | `#a8453f` | `#fbefee` | 同上 | + +**不设 info 蓝**:用 `--c-accent` 承担"信息/提示"。 +每多一个有彩色,界面的统一感就下降一档 —— 4 个是上限。 + +**浅底规则**:`--c-*-subtle` 只能配对应的深色文字, +**不要**在浅底上放白字(对比度不足)。 + +### 阴影 —— 3 档,单一基色 + +```css +--shadow-xs: 0 1px 2px rgba(20, 28, 25, 0.05); +--shadow-sm: 0 2px 8px rgba(20, 28, 25, 0.06); +--shadow-md: 0 8px 24px rgba(20, 28, 25, 0.09); +``` + +**唯一基色**(`20,28,25`,一个带绿调的深色)是硬要求。 +混合多个基色(黑、slate、自定义深绿)的阴影并排时会呈现不同偏色, +整体看起来"脏",而这个病极难自查。 + +### 中性色阶 —— 唯一一套灰 + +| Token | 值 | 用途 | +|---|---|---| +| `--c-bg` | `#ffffff` | 页面底 | +| `--c-bg-subtle` | `#f7f8f8` | 侧栏、代码块底 | +| `--c-surface` | `#ffffff` | 卡片表面 | +| `--c-surface-hover` | `#f2f4f3` | 表面 hover | +| `--c-border` | `#e4e7e6` | 默认边框、分隔线 | +| `--c-border-strong` | `#d3d8d6` | 强调边框 | +| `--c-text` | `#1c2321` | 正文、标题 | +| `--c-text-secondary` | `#5a6561` | 次级说明 | +| `--c-text-muted` | `#8a938f` | 弱化标注(**不可用于正文**) | +| `--c-text-inverse` | `#ffffff` | 深底上的文字 | + +**对比度速查(on `#ffffff`)**: +- `--c-text` ≈ 14.9:1 ✓ +- `--c-text-secondary` ≈ 6.4:1 ✓ +- `--c-text-muted` ≈ 3.2:1 ✗ **低于 AA 的 4.5:1** —— 只能用于 + 非关键的弱化标注(如禁用文字、图表轴标签),**不要写正文或 placeholder 的关键说明** + +**只允许一套灰**:不要同时用 Tailwind 的 slate 系和自定义灰。 +两套灰的色温不同,并排出现会让整个界面"发脏", +但单独看每个值都很正常 —— 所以这是最难自查的问题之一。 + +--- + +## 2. 三种项目类型的调整 + +只需改 3 个地方,其余保持不变。 + +### A. 密集工具型(IDE、后台、工作台、编辑器) + +```css +--text-base: 13px; /* 保持 */ +--text-md: 15px; /* 可降到 14px,但不要再低 */ +``` + +- 行高用 `--leading-snug` (1.45) 为主,长文段落用 `--leading-normal` +- 控件默认 `--control-h-sm` (28px) 或 `-md` (36px) +- 内容最大宽度可放宽到 `1280px` 以上,甚至全宽 +- **中文界面把 `--text-base` 提到 13~14px**:中文字形比拉丁字母饱满, + 12px 的中文在小屏上偏吃力 + +### B. 标准 SaaS / 通用管理后台 + +```css +--text-base: 14px; +--text-md: 16px; +--layout-content-max: 1200px; +``` + +- 控件默认 `--control-h-md` (36px) +- 行高 `--leading-normal` 为主 + +### C. 阅读 / 营销 / 文档型 + +```css +--text-base: 16px; +--text-md: 18px; +--text-lg: 20px; +--text-xl: 28px; +--text-2xl: 40px; +--leading-normal: 1.7; +--layout-content-max: 720px; /* 阅读舒适区 */ +``` + +- 控件默认 `--control-h-lg` (44px) +- 段落最大宽度控制在 `65~75` 个字符 + +--- + +## 3. 明确**不要**调的 + +| 不要动 | 为什么 | +|---|---| +| 间距档位集合(13) | 防膨胀结构。它是"该界面真实用量的完整集合",不是可选范围:加档 = 开始随手挑值 | +| 圆角档位(3) | 多一档就多一种"这里特殊"的借口 | +| 语义色数量(3) | 每多一个,统一感下降一档 | +| 阴影基色(1) | 多基色 = 阴影发脏,且极难自查 | +| `--c-text-muted` 的用途边界 | 它低于 AA,误用会让界面"看不清" | + +--- + +## 4. 从现有项目迁移的做法 + +如果项目已经有界面(很可能已经漂移了): + +1. **别推翻重做。** 重做等于把同一类错误再犯一次,还丢掉了已完成的交互。 +2. **先做一次"取色"**:把现有 CSS 里所有颜色/字号/圆角捞出来, + 按频次排序,找出**事实上在用的那套值**。 +3. **把主色定为现状中最后生效的那个品牌色**(不是新造一个)—— + 这样迁移在视觉上接近中性,不会引起"又换风格了"的反弹。 +4. **按频次归一**:最高频的 12px 归到 `--text-sm`,13px 归到 `--text-base`, + 15/16px 都归到 `--text-md`。**这一步会立刻减少大量视觉噪音。** +5. 用 `scripts/audit-css.mjs` 记录迁移前后的漂移分数,作为改进证据。 + +> 第 4 步的收益最直接:19 种字号 → 7 种,21 种圆角 → 3 种, +> 界面会**立刻**看起来整齐 —— 因为噪音消失了,而不是因为"变漂亮了"。 +> 这是投入产出比最高的一次改动。 diff --git a/.agents/skills/frontend-craft/scripts/audit-css.mjs b/.agents/skills/frontend-craft/scripts/audit-css.mjs new file mode 100644 index 0000000..5d7a3dc --- /dev/null +++ b/.agents/skills/frontend-craft/scripts/audit-css.mjs @@ -0,0 +1,1403 @@ +#!/usr/bin/env node +/** + * audit-css.mjs —— CSS「设计系统漂移」审计记分卡(零依赖 · Node 18+ · 跨平台) + * + * 用途 + * 扫一个前端项目(或单个 CSS 文件),统计颜色 / 圆角 / 字号 / 变量 / 选择器 / + * !important 的散乱程度,输出一张 ✅ / ❌ 记分卡,判断设计系统是否已经漂移。 + * + * 用法 + * node audit-css.mjs [--json] [--quiet] [--help] + * + * 选项 + * --json 输出机器可读 JSON(含全部原始数据 + pass/fail),无装饰字符 + * --quiet 只输出最后一行总结 + * -h, --help 显示本帮助 + * + * 输入 + * - 目录:递归收集所有 .css,以及所有 .vue(只取 块参与分析) + * - 文件:直接分析(.vue 同样只取 style 块) + * - 跳过目录:node_modules / dist / build / .git / coverage / vendor + * + * 指标 + * 1. :root 块数量;CSS 变量重复声明(最后一个生效,其余标为死代码) + * 2. 十六进制颜色 distinct + 频次 Top15 + 近重复聚类(RGB 欧氏距离 < 12) + * 3. rgb()/rgba() distinct + 基色(忽略 alpha)distinct + 基色近重复聚类 + * 4. border-radius 取值列表 + distinct + * 5. font-size 取值列表 + distinct + 频次分布(clamp() 整体视作一个值) + * 6. 同一选择器分散在多块(> 1 次)数量 + Top15(提示项,不参与判定) + * 7. token 覆盖率 = var(--x) 次数 /(var 次数 + 硬编码颜色数 + 硬编码间距/字号/圆角 px 数) + * 8. !important 数量 + * 9. 规则块总数 / distinct 选择器数 + * + * 评分(任一 FAIL → 进程退出码 1) + * 彩色 distinct ≤ 5 · border-radius distinct ≤ 4 · font-size distinct ≤ 7 + * 近白背景 distinct ≤ 3 · :root 块 = 1 · token 覆盖率 ≥ 90% + * rgb/rgba 基色 distinct ≤ 3 · 冗余声明(同属性重复声明)= 0 · !important = 0 + * + * 提示项(不参与判定,不影响退出码) + * 同一选择器分散在多块 —— 归零必须合并块,而合并会把声明搬到文件更靠后处, + * 改变它与其它同特异性选择器的先后关系,有渲染风险,因此只提示不判定 + * + * 退出码 + * 0 全部通过 · 1 有未通过项 / 参数或输入错误 + */ + +import fs from 'node:fs'; +import path from 'node:path'; + +// ───────────────────────────── 常量与阈值 ───────────────────────────── + +const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git', 'coverage', 'vendor']); +const NEAR_DUP_DISTANCE = 12; // RGB 欧氏距离阈值:小于它视为「近重复颜色」 +const NEAR_WHITE_MIN = 245; // 三通道全部 > 245 视为「近白」 +const NEUTRAL_SAT = 0.12; // HSL 饱和度 < 12% 视为中性色 +const NEUTRAL_CHROMA = 6; // 彩度(max-min)≤ 6 视为「彩度接近 0」→ 中性色 +const VALUE_PREVIEW = 12; // 记分卡值列表最多展示几个 +const DETAIL_PREVIEW = 24; // 详情行最多展示几个 +const TOP_N = 15; // Top 列表长度 +const MAX_LISTED_PX = 24; // JSON 里 px 明细最多列几个 + +// 评分阈值(改这里即可调整严格度) +// 注意 duplicateSelectors 与 redundantDeclarations 是两件事: +// redundantDeclarations —— 同一 (上下文, 选择器) 下同一个属性被声明了不止一次。 +// 后一条必然覆盖前一条,删掉前一条不改变任何渲染。这是真正的漂移症状,硬门 = 0。 +// duplicateSelectors(提示,不参与判定)—— 同一选择器分散在多个规则块里,但各块声明的属性互不重叠。 +// 它不是冗余:要归零就得把多个块合并,而合并会把声明搬到文件更靠后的位置, +// 从而改变它与其它同特异性选择器之间的先后关系 —— 那是有渲染风险的,不能当作硬门。 +const LIMITS = { + coloredDistinct: 5, + radiusDistinct: 4, + fontSizeDistinct: 7, + nearWhiteDistinct: 3, + rootBlocks: 1, + tokenCoverage: 90, + rgbBaseDistinct: 3, + redundantDeclarations: 0, + duplicateSelectors: 0, + importantCount: 0, +}; + +// ───────────────────────────── 基础文本工具 ───────────────────────────── + +/** 跳过一段字符串字面量,返回结束引号之后的偏移 */ +function skipString(src, i) { + const n = src.length; + const quote = src[i]; + i++; + while (i < n) { + const c = src[i]; + if (c === '\\') { + i += 2; + continue; + } + if (c === quote) return i + 1; + i++; + } + return i; +} + +/** 跳过一对配对的括号,返回右括号之后的偏移 */ +function skipBalanced(src, i, open, close) { + const n = src.length; + let depth = 0; + while (i < n) { + const c = src[i]; + if (c === '"' || c === "'") { + i = skipString(src, i); + continue; + } + if (c === open) depth++; + else if (c === close) { + depth--; + if (depth === 0) return i + 1; + } + i++; + } + return i; +} + +/** + * 剥离 /* … *\/ 注释,但保持字符串长度不变(注释字符换成空格、换行保留), + * 这样后续所有偏移量都能一对一映射回原文件,行号才不会错位。 + * 注意:只剥离注释、不动声明,所以 :root 里的变量声明不会丢。 + */ +function stripComments(src) { + const out = src.split(''); + const n = src.length; + let i = 0; + while (i < n) { + const c = src[i]; + if (c === '"' || c === "'") { + i = skipString(src, i); + continue; + } + if (c === '/' && src[i + 1] === '*') { + const end = src.indexOf('*/', i + 2); + const stop = end === -1 ? n : end + 2; + for (let k = i; k < stop; k++) { + if (out[k] !== '\n' && out[k] !== '\r') out[k] = ' '; + } + i = stop; + continue; + } + i++; + } + return out.join(''); +} + +/** + * .vue:把 diff --git a/.agents/skills/frontend-craft/templates/vue/UiButton.vue b/.agents/skills/frontend-craft/templates/vue/UiButton.vue new file mode 100644 index 0000000..f75c98f --- /dev/null +++ b/.agents/skills/frontend-craft/templates/vue/UiButton.vue @@ -0,0 +1,204 @@ + + + + + + diff --git a/.agents/skills/frontend-craft/templates/vue/UiCard.vue b/.agents/skills/frontend-craft/templates/vue/UiCard.vue new file mode 100644 index 0000000..f8f570a --- /dev/null +++ b/.agents/skills/frontend-craft/templates/vue/UiCard.vue @@ -0,0 +1,78 @@ + + + + + + diff --git a/.agents/skills/frontend-craft/templates/vue/UiEmptyState.vue b/.agents/skills/frontend-craft/templates/vue/UiEmptyState.vue new file mode 100644 index 0000000..626bffe --- /dev/null +++ b/.agents/skills/frontend-craft/templates/vue/UiEmptyState.vue @@ -0,0 +1,89 @@ + + + + + + diff --git a/.agents/skills/frontend-craft/templates/vue/UiInput.vue b/.agents/skills/frontend-craft/templates/vue/UiInput.vue new file mode 100644 index 0000000..328e6b9 --- /dev/null +++ b/.agents/skills/frontend-craft/templates/vue/UiInput.vue @@ -0,0 +1,167 @@ + + + + + + diff --git a/.agents/skills/frontend-craft/templates/vue/UiSelect.vue b/.agents/skills/frontend-craft/templates/vue/UiSelect.vue new file mode 100644 index 0000000..6dd3dc9 --- /dev/null +++ b/.agents/skills/frontend-craft/templates/vue/UiSelect.vue @@ -0,0 +1,157 @@ + + + + + + diff --git a/.gitignore b/.gitignore index 7caa647..3ea7897 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,14 @@ __pycache__/ *.egg-info/ ui/node_modules/ ui/dist/ +ui/shots/ +# 一次性脚本与截图落点:迁移过程中用完即弃,不进仓库。 +# 需要留证的产物放 docs/design/baseline/,不靠 tmp/。 +tmp/ +# 参照物截图(Cursor / Linear / GitHub / Vercel / Notion / Raycast 的产品截图)。 +# 是别人的版权素材、体积 1.8 MB、重跑 capture.mjs 就能再取一份; +# 项目自己的设计证据都在 docs/design/baseline/。 +docs/design/reference/ ui/.tanstack/tmp/ ui/tsconfig.tsbuildinfo ui/.output/ diff --git a/docs/design/DESIGN.md b/docs/design/DESIGN.md new file mode 100644 index 0000000..4d16860 --- /dev/null +++ b/docs/design/DESIGN.md @@ -0,0 +1,1371 @@ +# DESIGN.md — CODING 工作台视觉契约 + +> 本文件是**约束**,不是描述。代码与它冲突时,改代码。 +> Token 的**值**在 `ui/src/styles/tokens.css`,本文件不重复。 + +## 1. 定位 + +一个**长时间使用的开发工作台**:中性、密实、层级分明。详见 `direction.md`。 + +## 2. 参照物 + +**Cursor(主)+ Linear(辅)**,逐张的「取 / 不取」写在 `direction.md`。 +截图在 `reference/candidates/`。改动前先看那两张图。 + +## 3. Token 与契约(唯一真相源) + +| 层面 | 唯一真相源 | 强制手段 | +|---|---|---| +| 视觉值(颜色 / 间距 / 字号 / 圆角 / 阴影 / 层级) | `ui/src/styles/tokens.css` | `npm run audit`(漂移记分卡)+ `npm run lint:css` | +| 数据形状(接口返回什么) | `ui/src/api/types.js`(JSDoc `@typedef`) | 以**真实响应**为准,形状变了**先改这里** | +| 网络出口 | `ui/src/api/client.js`(axios)、`ui/src/api/sse.js`(流式) | 组件**永远不直接 fetch** | + +**任何字面量颜色、字号、间距、圆角都是 bug。** 执行命令: + +``` +npm run lint:css # 新代码(components/ui、components/layout)0 error —— 硬门 +npm run lint:css:all # 全部文件,含 legacy;迁移期仅作参考 +npm run audit # 设计系统漂移记分卡(legacy 的收敛进度看这个) +npm run shots # 截图三个宽度(需先 npm run dev) +``` + +> **迁移期策略**:`lint:css` 只卡**新代码**。legacy 的 `main.css` 曾经是一地违规, +> 一次性开满会让人习惯性忽略输出 —— 那等于没开。 +> **阶段二收敛后 `npm run audit` 已全绿**;但 `lint:css:all` 仍会报几百条格式类问题 +> (它管的是写法风格,`audit` 管的才是设计系统漂移),所以 glob 暂不扩大。 + +### 记分卡的两个指标不要混淆 + +| 指标 | 判定 | 含义 | +|---|---|---| +| **冗余声明**(同属性重复声明) | **硬门 = 0** | 该声明被更靠后、同上下文、同选择器的声明完全覆盖 → 删掉不改渲染 | +| 同一选择器分散在多块 | 提示(ℹ️) | 同一个选择器出现在多个块里,但属性**并未**重叠。要归零必须合并块,而合并会把声明搬到文件更靠后处、改变它与其它同特异性选择器之间的先后关系 → **有渲染风险,不能当硬门** | + +## 4. 布局骨架 + +单页应用,无路由。一个 CSS Grid 两列: + +``` +grid-template-columns: var(--layout-sidebar) minmax(0, 1fr) +``` + +``` +┌──────────────────┬──────────────────────────────────────────────┐ +│ Sidebar │ Header (var(--layout-header) = 52px) │ +│ var(--layout- │ ┌ thread title (可编辑) ┌ status badge │ +│ sidebar)=272px │ └ 仓库全名 (次) └ branch (次) │ +│ ├──────────────────────────────────────────────┤ +│ ┌ logo [◀] ┐ │ │ +│ │ 新聊天 ⌘K │ │ Content max-width: var(--layout-content-max)│ +│ │ 新建项目 │ │ = 960px,居中,上下留白 │ +│ ├─────────────┤ │ │ +│ │ 项目 [4]│ │ 空态 → Hero 垂直居中: │ +│ │ ▾ 分组 │ │ mark / eyebrow / h1 / desc │ +│ │ · 会话 │ │ │ +│ │ · 会话 │ │ 会话态 → 消息列表,向上滚动 │ +│ │ 展开其余 12 │ │ │ +│ ├─────────────┤ ├──────────────────────────────────────────────┤ +│ │ 工作区已就绪│ │ Composer (sticky bottom,卡片式) │ +└──┴─────────────┴──┴──────────────────────────────────────────────┘ +``` + +### 断点:**只允许这 3 个** + +| 断点 | 语义 | 侧栏行为 | +|---|---|---| +| `max-width: 860px` | 平板 | 侧栏改为浮层 + backdrop | +| `max-width: 720px` | 窄屏 | composer 底栏折行 | +| `max-width: 560px` | 手机 | 单列,字号不下调 | + +> **已收敛**:阶段二把 `600px` 那个只出现 1 次的漂移断点并进了 `560px`。 +> 现在除 `@media (hover: none)` 外只有上表 3 个宽度,各有 3 个块; +> 合并前后 375 / 768 / 1440 三个宽度的截图**逐像素一致**。 + +## 5. 组件白名单 + +**页面只能使用下表中的组件与 class。新增 class 需要先改本表。**(阶段三后生效) + +| 用途 | 组件 | 来源 | +|---|---|---| +| 按钮 | `UiButton` (primary / secondary / ghost / danger) | `@/components/ui/UiButton.vue` | +| 输入框 | `UiInput` | `@/components/ui/UiInput.vue` | +| 多行输入 | `UiTextarea`(`bare` 无框形态 + `autoResize`) | `@/components/ui/UiTextarea.vue` | +| 下拉 | `UiSelect`(`bare` 无框形态 + `prefix`/`suffix` 插槽) | `@/components/ui/UiSelect.vue` | +| 卡片/容器 | `UiCard` | `@/components/ui/UiCard.vue` | +| 状态标记 | `UiBadge` (neutral / accent / success / warning / danger) | `@/components/ui/UiBadge.vue` | +| 空态 | `UiEmptyState` | `@/components/ui/UiEmptyState.vue` | + +**业务组件**(保留,但内部必须只使用上面的白名单 + token): + +| 组件 | 文件 | 职责 | +|---|---|---| +| 侧栏 | `SessionSidebar.vue` | 项目分组树、新建、收起 | +| 工作区 | `AgentWorkspace.vue` | Header + 内容区 + composer 编排 | +| 输入区 | `ChatComposer.vue` | 模型/推理/仓库 + 发送 | +| 消息 | `ChatMessage.vue` | 单条消息渲染 | +| 消息操作 | `MessageActions.vue` | 复制/重试等 | +| 运行轨迹 | `RunActivityCard.vue` | 折叠的运行详情 | +| 运行指示 | `ActiveTaskStrip.vue` | 正在跑的任务条 | +| 待办 | `TodoPlan.vue` | 计划清单 | +| 提案 | `ProposalCard.vue` | 计划确认 | +| 人工介入 | `HumanInterventionCard.vue` | 需要用户输入 | +| 新建项目 | `CreateProjectDialog.vue` | 弹窗 | +| 删除项目 | `ConfirmProjectDeleteDialog.vue` | 弹窗 | + +**禁止**:在页面里写裸 `