Compare commits

..

4 Commits

Author SHA1 Message Date
wangjia 4d274aad7a docs(plan): 前端设计系统治理重构(ds-flow 全端)实现计划
用 ds-flow 把 Flutter 五端 + 官网 + 用户中心收口到「设计单源·代码镜像·
静态闸拦漂移·golden/fidelity 双级像素验收兜底」。

摸底结论:pangolin 非从零 bootstrap,已约 65% 达标(token 单源含暗色、
Flutter codegen+drift 闸、golden+CI 闸、pre-commit 写好未启用)。本计划是
补缺口 + Web 共享原子层去重,非推倒重来。主题保持 light/dark。

6 阶段:CLAUDE.md → 原型三件套(atoms/icons/index.html,收敛 ui_kits) →
Web token 同源闸 → Web 原子层去重(各自实现+同源闸) → Flutter 收尾+golden
补齐 → 静态闸挂满+启用 pre-commit+fidelity 体检。

真相源(含 checkbox)+ HTML 阅读版 + 登记 docs/index.html;todo #19 tier-1。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:41:50 +08:00
wangjia 84448b3064 fix(server/cors): 放行 X-Refresh-Token 头(修用户中心刷新掉线)
Deploy Server / deploy-server (push) Successful in 3m3s
用户中心用 header token 方案:access token 只存内存、refresh token 存
localStorage,页面刷新后靠 POST /v1/auth/refresh 带 X-Refresh-Token 头静默续期
(web/usercenter/lib/api/http.ts)。

但 CORS Allow-Headers 只放行 Content-Type/Authorization,浏览器预检发现
X-Refresh-Token 不在白名单 → 拦截真正的 refresh 请求 → api.refresh() 抛网络错
→ setAuthed(false) → 甩回登录页。表现:登录能成功(不带该头),但一刷新就掉线,
用量/续费等需登录态的页面全打不开。

修:Allow-Headers 加 X-Refresh-Token。加回归断言(cors_test)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:03:39 +08:00
wangjia 386e0bce90 fix(web): 语言下拉换自定义菜单(原生 select 弹层被系统定位/漂移)
Deploy Site / deploy-site (push) Successful in 4m31s
原生 <select> 的选项弹层由 OS 定位(macOS 锚在选中项上、整体偏移=漂移),CSS 控
不了。改成自控下拉:按钮(复用药丸样式)+ 绝对定位菜单(top:100%,right:0)+ 点外/
Esc 关闭 + 选中高亮。用户中心 LangSeg 与官网 Header 同款;官网选项用 <a href> 导航
(SEO 友好)、用户中心用 setLang。两端 build 通过。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-07 22:42:00 +08:00
wangjia b584188ce2 fix(ci/site): 用 ! Content-Security-Policy 删官网 CSP(CF _headers 叠加,浏览器取交集)
Deploy Site / deploy-site (push) Successful in 4m36s
上一版判断错了:CF Pages _headers 不是'首个匹配生效',而是【叠加下发】——/* 和
/user/* 都匹配 /user/,两条 CSP 都发,浏览器对多条 CSP 取交集(最严)→ 官网严格
CSP 的 style-src(无 unsafe-inline)/connect-src('self')赢,用户中心内联样式被拦、
连 API 被拦 → 裸奔 + 登录失败。正解:/user/* 里  先删掉
/* 继承的官网 CSP,再设用户中心自己的(排 /* 之后,先加后删)。其余安全头沿用 /*。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-07 22:25:36 +08:00
10 changed files with 672 additions and 78 deletions
+163
View File
@@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>前端设计系统治理重构(ds-flow)· 实现计划</title>
<style>
:root{
--bg:#0f1117; --panel:#171a22; --panel2:#1d2129; --fg:#e6e8ee; --fg2:#a8afbd;
--accent:#e0884f; --accent2:#5fb0c9; --ok:#5ec27a; --bad:#e06a6a; --warn:#e0b84f;
--border:#272c36; --mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
--sans:-apple-system,"PingFang SC","Helvetica Neue",Arial,sans-serif;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
.wrap{max-width:920px;margin:0 auto;padding:48px 24px 96px}
h1{font-size:29px;line-height:1.3;margin:0 0 8px;letter-spacing:-.01em}
.sub{color:var(--fg2);font-size:15px;margin:0 0 28px}
h2{font-size:20px;margin:38px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border)}
h3{font-size:16px;margin:22px 0 8px;color:var(--accent)}
p{margin:10px 0}
code{font-family:var(--mono);font-size:.86em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
a{color:var(--accent2);text-decoration:none} a:hover{text-decoration:underline}
.lead{background:linear-gradient(180deg,rgba(224,136,79,.10),transparent);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:0 0 20px}
.small{color:var(--fg2);font-size:13px}
.phase{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px 20px;margin:14px 0}
.phase h2{margin-top:4px;border:none;padding:0;font-size:18px}
ul{margin:8px 0;padding-left:22px} li{margin:5px 0}
.box{overflow-x:auto;margin:14px 0}
table{border-collapse:collapse;width:100%;font-size:13.5px;min-width:560px}
th,td{border:1px solid var(--border);padding:7px 10px;text-align:left;vertical-align:top}
th{background:var(--panel2);color:var(--fg)}
.ok{color:var(--ok);font-weight:700} .bad{color:var(--bad);font-weight:700} .warn{color:var(--warn);font-weight:700}
.pill{display:inline-block;font-size:11px;font-weight:700;padding:1px 8px;border-radius:999px;margin-left:6px}
.pill.big{background:rgba(224,106,106,.16);color:var(--bad)}
.tag{display:inline-block;font-size:11px;font-weight:700;padding:1px 8px;border-radius:999px;background:rgba(94,194,122,.14);color:var(--ok);margin-left:8px;vertical-align:middle}
.back{color:var(--fg2);font-size:13px}
</style>
</head>
<body>
<div class="wrap">
<p class="back"><a href="index.html">← 文档索引</a></p>
<h1>前端设计系统治理重构 <span class="tag">ds-flow</span></h1>
<p class="sub">用 ds-flow 把 Flutter 五端 + 官网 + 用户中心收口到「设计单源 · 代码镜像 · 静态闸拦漂移 · 双级像素验收兜底」</p>
<div class="lead">
<strong>阅读版</strong>;执行真相源 <code>docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md</code>(含 checkbox)。<br>
<strong>关键前提</strong>pangolin <b>不是从零 bootstrap,已约 65% 达标</b>——token 单源(含暗色)、Flutter codegen + drift 闸、golden + CI 闸、pre-commit(写好未启用)都在。本计划是<b>补缺口 + Web 共享原子层去重</b>,非推倒重来。主题保持 <b>light / dark 双主题</b>
</div>
<h2>现状盘点</h2>
<div class="box">
<table>
<tr><th>维度</th><th>现状</th><th>缺口</th></tr>
<tr><td>Token 单源</td><td class="ok">✓ colors_and_type.css(含 [data-theme=dark]</td><td>迁为 ds-flow 原型结构</td></tr>
<tr><td>Flutter codegen / 主题层</td><td class="ok">✓ gen 层/实现层分离 + drift 闸</td><td></td></tr>
<tr><td>Flutter UI 硬编码</td><td class="ok">✓ 零裸 hex(唯 1 处裸色 adaptive_menu</td><td>清 1 处</td></tr>
<tr><td>Flutter golden</td><td class="ok">✓ 36 张 + harness + 真字体 + CI</td><td class="warn">6 张 failure;缺 CJK 测试字体;覆盖不全</td></tr>
<tr><td>website / usercenter</td><td class="ok">✓ 179 / 171 处 var(--token)</td><td class="bad">零前端测试</td></tr>
<tr><td>跨端共享组件</td><td class="bad">✗ 下拉/按钮/卡片两端各写一遍</td><td>抽 atoms 对齐</td></tr>
<tr><td>原型三件套</td><td class="warn">⚠ 有 _ds_manifest/preview/ui_kits</td><td>缺 atoms.css / icons.js / index.html</td></tr>
<tr><td>静态闸</td><td class="ok">✓ redline / analyze+test / codegen-drift / golden</td><td class="bad">✗ 硬编码色扫描 / fidelity / Web 同源</td></tr>
<tr><td>pre-commit</td><td class="warn">⚠ .githooks 写好</td><td class="bad">默认未启用</td></tr>
</table>
</div>
<h2>已定决策</h2>
<ul>
<li><b>Web 去重</b>:两端<b>各自实现 + 同源闸</b>(不建跨端组件包;成本低、风险小,符合 jiu 取舍)</li>
<li><b>ui_kits</b>jsx/css 端原型<b>收敛为纯 HTML 原型</b>并删副本(消除与「禁向 design/ 提组件代码副本」的冲突)</li>
<li><b>节奏</b><b>先定稿本计划,再逐刀执行</b>(每刀 committier-1 大改走确认闸)</li>
</ul>
<h2>执行阶段(6 阶段)</h2>
<div class="phase">
<h2>Phase 0 — 更新 CLAUDE.md + 计划落库</h2>
<ul>
<li>CLAUDE.md 补「前端设计系统治理(ds-flow)」章节:原型单源位置、codegen 命令、L1/L2/L3 三层规则、四道静态闸 +「违规谁拦」对照表、golden/fidelity 双闸定位</li>
<li>本计划 .md 定稿 + HTML 阅读版 + 登记 docs/index.html</li>
<li>/todo 建 tier-1 条目 + 6 子任务</li>
</ul>
</div>
<div class="phase">
<h2>Phase 1 — 原型单源三件套(design/prototype/</h2>
<p class="small">把散在 ui_kits / preview / _ds_manifest.json 的东西收敛为 ds-flow 标准三件套。</p>
<ul>
<li><code>serve.mjs</code> 照搬 jiu(零依赖热重载)</li>
<li><code>tokens.css</code>:现有 token <b>数值不变</b>,重排为「基础 :root 标量 + [data-theme=dark] 颜色覆盖」结构</li>
<li><code>atoms.css</code>:按钮/卡片/输入/<b>语言下拉</b>/徽章/状态药丸公用原子(只引 var(--token)</li>
<li><code>icons.js</code>SVG sprite 单源,收敛 website / usercenter / Flutter 三处图标集</li>
<li><code>index.html</code>:活登记页——light/dark 切换 + 声明式色板 + 全组件/图标展示卡(每 atom 必登记)</li>
<li>ui_kits jsx 副本提炼后<b>删除</b>;屏级布局参考迁 <code>prototype/screens/</code></li>
</ul>
</div>
<div class="phase">
<h2>Phase 2 — Web token 升为一等公民 + 同源闸</h2>
<ul>
<li>两端 token 落点统一指向原型 tokens.csswebsite→tokens.gen.css / usercenter→public/colors_and_type.css</li>
<li><code>tools/check-l1-sync.mjs</code>(照搬 jiu 裁剪):website / usercenter token 值逐值同源 + icons 同集 + Web hex 白名单扫描</li>
<li>build-tokens 幂等:重跑零 diff(纳入 CI)</li>
</ul>
</div>
<div class="phase">
<h2>Phase 3 — Web 共享原子层对齐 <span class="pill big">工作量最大</span></h2>
<p class="small">各自实现 + 同源闸:两端对齐同一 atoms.css,靠闸防漂移。</p>
<ul>
<li>抽公共原子:langsel / button / card / input / badge / pill → atoms.css canonical</li>
<li>websitewebsite.css/site-extra.css 对齐 atoms 语义,非白/黑/logo 硬编码清零</li>
<li>usercentershared.tsx 的 card/input/LangSeg 对齐 atoms 语义,13 处硬编码核对</li>
<li>两端 langsel 一致性纳入登记;更新 CONTRACT.md(Web 原子清单 + 屏级三态台账)</li>
</ul>
</div>
<div class="phase">
<h2>Phase 4 — Flutter 收尾 + golden 补齐</h2>
<ul>
<li>清 adaptive_menu.dart 唯 1 处裸色 → token</li>
<li>测试字体补 CJK 子集(make-cjk-subset.sh → client/test/fonts + flutter_test_config 注册)</li>
<li>处理现存 6 张 failure diff,逐张确认后 --update-goldens 重录</li>
<li>golden 覆盖扩容:desktop/tablet/mobile 全屏 × light/dark 双主题矩阵</li>
<li>harness 对齐 jiu:多主题循环 + 钉死 viewport/dpr + ProviderScope 固定数据</li>
</ul>
</div>
<div class="phase">
<h2>Phase 5 — 静态闸挂满 + 启用 pre-commit + fidelity 体检</h2>
<ul>
<li>硬编码色扫描:Flutter <code>check_ds_code.mjs</code>(含 --changed+ Web hex 并入 check-l1-sync</li>
<li>原型校验 <code>check-ds.mjs</code>(照搬 jiu 12 道,按 pangolin 断点/主题裁剪)</li>
<li>CI 串起来:原型校验 → 跨端同源 → 代码色单源 → codegen 零 diff(已有)→ 测试含 golden(补 mobile+主题)</li>
<li>启用 pre-commitinstall-hooks 纳入文档,增挂 check-ds --changed(条件触发)</li>
<li>fidelity 像素闸(本地体检,不进 CI):screens.mjs + fidelity.mjs,逐屏阈值=实测残差+2pp</li>
<li>全景文档 docs/frontend-overview.html(照搬 jiu 十节)+ 登记索引</li>
</ul>
</div>
<h2>Verification(端到端)</h2>
<ul>
<li><b>原型</b>serve.mjs 逐屏目检 light/darkcheck-ds 12 道全绿</li>
<li><b>同源</b>check-l1-sync 全绿(tokens 逐值 / icons 同集 / Web hex 白名单)</li>
<li><b>Flutter</b>analyze + testgolden ×双主题);check_ds_code 绿;codegen 重跑零 diff</li>
<li><b>Web</b>:两端 build 通过;token 同源绿;langsel/button/card 对齐 atoms</li>
<li><b>fidelity</b>:逐屏残差在阈内(首次校准记录实测值)</li>
<li><b>闸生效</b>install-hooks 后改一处硬编码色/未登记组件 → pre-commit 或 CI 拦下</li>
</ul>
<h2>不在本轮</h2>
<ul>
<li>新功能 / 新屏开发(本轮是治理重构)</li>
<li>iOS/iPad 专属布局深度优化(响应式已覆盖)</li>
<li>三主题扩展(保持 light/dark</li>
</ul>
<p class="small" style="margin-top:32px">真相源(含 checkbox 执行跟踪):<code>docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md</code></p>
</div>
</body>
</html>
+5
View File
@@ -86,6 +86,11 @@
</a>
<h2>实现计划 / Plans</h2>
<a class="doc" href="frontend-ds-refactor-plan.html">
<div class="t">前端设计系统治理重构(ds-flow 全端)<span class="tag html">HTML</span></div>
<div class="d">阅读版;执行真相源 <code>docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md</code>(含 checkbox)。用 ds-flow 把 Flutter 五端 + 官网 + 用户中心收口到「设计单源·代码镜像·静态闸拦漂移·golden/fidelity 双级像素验收兜底」。<b>非从零 bootstrap(已约 65% 达标)</b>:补原型三件套(atoms.css/icons.js/index.html 登记页)+ Web 共享原子层去重(各自实现+同源闸)+ 硬编码色/fidelity 闸 + 启用 pre-commit。6 阶段:CLAUDE.md → 原型单源 → Web token 同源 → Web 原子对齐 → Flutter golden 补齐 → 闸挂满。主题保持 light/dark。</div>
<div class="path">docs/frontend-ds-refactor-plan.html · 真相源 docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md</div>
</a>
<a class="doc" href="control-plane-tls-tunnel.html">
<div class="t">控制面 TLS(Cloudflare Tunnel 前置)实现计划 <span class="tag html">HTML</span></div>
<div class="d">阅读版;执行真相源 <code>docs/superpowers/plans/2026-07-06-control-plane-tls-tunnel.md</code>(含 checkbox)。把控制面 API 从明文 <code>http://103.119.13.48:8080</code> 迁到 <code>https://api.yanmeiai.com</code>cloudflared 出站隧道前置,源站仅绑 127.0.0.1,数据面 sing-box REALITY :443 全程不动)。6 任务:CF Tunnel 供给 → 客户端切 https/Android 去明文 → CI 守护禁明文 → PANGOLIN_PUBLIC_URL 切 https → 退役明文口(收 loopback,带上线顺序闸)→ 文档。</div>
@@ -0,0 +1,132 @@
# 前端设计系统治理重构(ds-flow 落地全端)
> 用 ds-flow 方法论把 pangolin 全部前端(Flutter 五端 + 官网 website + 用户中心 usercenter
> 收口到「设计只有一个出生地(原型单源),代码永远是镜像;漂移由静态闸在提交/CI 前拦截,
> 走样由 golden/fidelity 双级像素验收兜底」。
>
> **关键前提(摸底结论)**pangolin 不是从零 bootstrap,已约 65% 达标——
> token 单源(`design/colors_and_type.css` 含 `[data-theme=dark]`)、Flutter codegen + drift 闸、
> golden + CI 闸、pre-commit(写好未启用)都在。本计划是**补缺口 + Web 共享原子层去重**,
> 不是推倒重来。
>
> 主题模型:pangolin 用 **light / dark 两主题**(非 jiu 的 a/b/c 三主题),全程保持。
>
> 已定决策:① Web 两端**各自实现 + 同源闸**(不建跨端共享组件包);
> ② `design/ui_kits/` 的 jsx/css 端原型**收敛为纯 HTML 原型**并删副本;
> ③ **先定稿本计划,再逐刀执行**(每刀 commit)。
>
> 参考样板:`~/code/jiu``design/prototype/` + `tools/` + `client/lib/core/theme/` + `docs/frontend-overview.html`)。
---
## Phase 0 — 更新 CLAUDE.md + 计划落库
- [ ] 0.1 CLAUDE.md 新增「## 前端设计系统治理(ds-flow)」章节:
- 原型单源位置(`design/prototype/`tokens/atoms/icons/index.html 登记簿)+ 只读约定
- codegen 命令(Flutter `gen_flutter_tokens.mjs`Web `build-tokens.mjs` 同源)
- 三层治理 L1/L2/L3 规则速查
- 四道静态闸清单 + 「违规谁拦」对照表(原型校验 / 跨端同源 / 代码色单源 / codegen 零 diff
- golden(多主题回归自比)/ fidelity(对原型 pixelmatch,本地体检不进 CI)双闸定位
- [ ] 0.2 本 `.md` 定稿 + 生成 HTML 阅读版 `docs/frontend-ds-refactor-plan.html`,登记进 `docs/index.html`「实现计划」
- [ ] 0.3 `/todo` 建 tier-1 条目跟踪本重构,拆 6 个子任务(对应 Phase 1-5 + 收尾)
---
## Phase 1 — 原型单源三件套(design/prototype/
把散在 `ui_kits/`6 端 jsx/css 原型)+ `preview/`20 规格 HTML+ `_ds_manifest.json`(登记簿)
的东西收敛成 ds-flow 标准三件套。
- [ ] 1.1 建 `design/prototype/` 目录;`serve.mjs` 照搬 jiu(零依赖热重载,默认端口按 jiu)
- [ ] 1.2 `design/prototype/tokens.css`:从现有 `colors_and_type.css` 迁移/规整为
「基础 `:root`(主题无关标量:间距/圆角/字号/字体/阴影/动效)+ `[data-theme=dark]` 颜色覆盖块」结构。
**保持数值不变**,只重排为 ds-flow 结构;`colors_and_type.css` 作为兼容别名或迁移为薄封装(不破坏现有 codegen)
- [ ] 1.3 `design/prototype/atoms.css`:把按钮/卡片/输入/**语言下拉**/徽章/状态药丸等公用原子类沉淀为
只引 `var(--token)` 的 CSS(镜像 `design/preview/` 现有规格 + client widgets 实现语义)
- [ ] 1.4 `design/prototype/icons.js`SVG sprite 单源(`<symbol id="i-*">`),
收敛现有分散图标(website Icon.astro / usercenter icons.tsx / Flutter pangolin_icons.dart 三处的图标集)
- [ ] 1.5 `design/prototype/index.html`:活登记页——三…两主题(light/dark)切换 + `data-swatches` 声明式色板 +
字号梯度 + 圆角/间距/阴影 + 全部公用组件原子展示卡 + 图标库全展示。**每个 atom 必须在此登记**
- [ ] 1.6 `design/ui_kits/` 的 jsx/css 端原型:提炼进 prototype 后**删除 jsx 组件副本**(消除与
「禁向 design/ 提组件代码副本」的冲突 + 漂移源);保留必要的屏级 HTML 布局参考迁进 `prototype/screens/`
- [ ] 1.7 更新/退役 `_ds_manifest.json` + `_ds_bundle.js`:登记簿职责交给 `index.html`
manifest 若仍被消费则保留为派生产物(记清谁是真源)
---
## Phase 2 — Web token 升为一等公民 + 同源闸
现状:Web 的 `build-tokens.mjs` 只是「原样拷 css,删 Google Fonts 行」。升级为受闸守护的同源关系。
- [ ] 2.1 确认两端 token 落点与生成链:website→`src/styles/tokens.gen.css`
usercenter→`public/colors_and_type.css`;源统一指向 `design/prototype/tokens.css`Phase 1 后)
- [ ] 2.2 建 `tools/check-l1-sync.mjs`(照搬 jiu 裁剪):
- ① website `tokens.gen.css` token 值 ≡ 原型 tokens.css(逐值)
- ② usercenter `public/colors_and_type.css` ≡ 原型(逐值)
- ③ icons 同源:website / usercenter / Flutter 三处图标集 ⊆ 原型 icons.js sprite
- ④ Web 硬编码色扫描(白名单 `#fff/#000/logo 固定色`,其余报警)
- [ ] 2.3 codegen 幂等:重跑 `build-tokens.mjs``git diff` 零差异(纳入 CI,见 Phase 5)
---
## Phase 3 — Web 共享原子层对齐(各自实现 + 同源闸)★工作量最大
不建跨端组件包;两端各自实现,但都对齐 `design/prototype/atoms.css`,靠闸保证不漂移。
- [ ] 3.1 抽公共原子清单:langsel(语言下拉,刚修的两套合规范)/ button / card / input / badge / pill。
对每个原子在 `atoms.css` 定义 canonical 样式
- [ ] 3.2 website`website.css` + `site-extra.css` 里的按钮/卡片/下拉 class 对齐 atoms.css 语义,
残留 `#fff/#000`/logo 外的硬编码色清零(当前业务硬编码 ~30 处,多为可保留的白/黑/logo)
- [ ] 3.3 usercenter`shared.tsx``card/input/LangSeg` 内联对象对齐 atoms.css 语义;
残留 13 处硬编码(基本 `#fff`)核对,非白/黑/logo 的清零
- [ ] 3.4 两端 langsel 行为/样式一致性核对(此前刚统一为自定义下拉,纳入 atoms 登记)
- [ ] 3.5 更新 `design/CONTRACT.md`:Web 原子清单 + 屏级台账(同步/快照/代码先行三态)
---
## Phase 4 — Flutter 收尾 + golden 补齐
Flutter 已很干净(UI 层零裸 hex),只需收尾。
- [ ] 4.1 清 `client/lib/widgets/adaptive_menu.dart` 唯 1 处裸 Material 色 → 走 token
- [ ] 4.2 测试字体补 CJK 子集:用 `tools/fonts/make-cjk-subset.sh` 生成 Noto Sans SC 子集放
`client/test/fonts/``flutter_test_config.dart` 注册——消除 golden 中文与生产渲染差异
- [ ] 4.3 处理现存 6 张 `client/test/golden/failures/` diff:逐张确认「原型对得上」后 `--update-goldens` 重录入库
- [ ] 4.4 golden 覆盖扩容:desktop/tablet/mobile 全屏 × light/dark 双主题矩阵
(现有 `desktop_pages/tablet_pages/components/auth` → 补 mobile + 主题维度)
- [ ] 4.5 `client/test/helpers/harness.dart` 对齐 jiu `golden_harness.dart` 手法:
多主题循环辅助 + 钉死 viewport/dpr + ProviderScope 固定数据(防动态值翻车)
---
## Phase 5 — 静态闸挂满 + 启用 pre-commit + fidelity 体检
- [ ] 5.1 硬编码色扫描闸:
- Flutter `client/tool/check_ds_code.mjs`(照搬 jiu,含 `--changed` 供 pre-commit):禁 `Color(0x..)`/裸 `Colors.x`
- Web hex 扫描并入 `check-l1-sync.mjs`
- [ ] 5.2 原型校验闸 `design/prototype/tools/check-ds.mjs`(照搬 jiu 12 道,按 pangolin 断点/主题裁剪)
- [ ] 5.3 CI 串起来(`.gitea/workflows/ci.yml` 增补):
原型校验 → 跨端同源 → 代码色单源 → codegen 零 diff(已有)→ 测试含 golden(已有,补 mobile+主题)
- [ ] 5.4 启用 pre-commit`ci/install-hooks.sh` 纳入 onboarding 文档 + CLAUDE.md
`.githooks/pre-commit` 增挂 `check-ds --changed`(只在动了 `design/prototype/` 时跑,轻量条件触发)
- [ ] 5.5 fidelity 像素闸(本地体检,不进 CI):`tools/screens.mjs` 屏注册表 + `tools/fidelity.mjs`
(原型 Chromium 截图 vs Flutter golden pixelmatch,逐屏阈值=实测残差+2pp,两边统一注入 CJK 字体)
- [ ] 5.6 全景文档 `docs/frontend-overview.html`(照搬 jiu 十节):一次 UI 改动标准路径 + 目录地图 +
三层分治 + 闸全景 + 像素验收体系 + 响应式范式 + 规则速查,登记进 docs/index.html
---
## Verification(端到端)
- **原型**`node design/prototype/serve.mjs` 起服务,浏览器逐屏目检 light/dark;`check-ds.mjs` 12 道全绿
- **同源**`node tools/check-l1-sync.mjs` 全绿(tokens 逐值 / icons 同集 / Web hex 白名单)
- **Flutter**`flutter analyze` + `flutter test`(含 golden ×双主题);`check_ds_code.mjs` 全绿;codegen 重跑零 diff
- **Web**:两端 `npm run build` 通过;token 同源闸绿;langsel/button/card 对齐 atoms
- **fidelity**`node tools/fidelity.mjs` 逐屏残差在阈内(首次校准记录各屏实测值)
- **闸生效**`ci/install-hooks.sh` 后改一处硬编码色/未登记组件 → pre-commit 或 CI 拦下
## 不在本轮
- 新功能/新屏开发(本轮是治理重构,不加业务)
- iOS/iPad 专属布局深度优化(响应式已覆盖,超阈再单独立项)
- 三主题扩展(保持 light/dark 双主题)
+11 -13
View File
@@ -22,22 +22,20 @@ rm -rf "${DIST}/user"
mkdir -p "${DIST}/user"
cp -R "${UC}/." "${DIST}/user/"
# 2) 合并 _headers:用户中心路径规则前缀 /user,置于官网规则【之前】。
# ⚠️ CF Pages _headers 对同一 header 取【首个匹配规则】的值(实测:若 /user/* 排
# 在官网 /* 之后,/user/ 页会命中在前的 /* 官网严格 CSP → 内联样式/连 API 被拦、
# 页面裸奔)。故 /user/* 必须在 /* 之前才能让用户中心 CSP 生效。
# 删掉并入产物里那份会被 CF 忽略的 /user/_headers,避免误导
# 2) 合并 _headers 的 CSP。⚠️ CF Pages _headers 是【叠加】的:/* 与 /user/* 都匹配
# /user/,两条 CSP 都会下发,浏览器对多条 CSP 取【交集(最严)】→ 官网严格 CSP
# 的「style-src 无 unsafe-inline / connect-src 'self'」赢,用户中心的内联样式被拦、
# 连 API 被拦 → 页面裸奔且登录失败。故 /user/* 里用 `! Content-Security-Policy`
# 先【删掉】/* 继承来的官网 CSP,再设用户中心自己的(须排在 /* 之后:先加后删)
# 其余安全头(HSTS/X-Frame/…)/* 与用户中心一致,沿用 /* 即可,不重复。
rm -f "${DIST}/user/_headers"
# 行首 '/'(路径规则)前缀 /user;'#' 注释与缩进的 header 行不动。
sed -E 's#^/#/user/#' "${UC}/_headers" > "${DIST}/_uc_headers.tmp"
{
echo "# ==== 用户中心(/user/*)独立 CSP,须置于官网 /* 之前(CF Pages 首个匹配 header 生效)===="
cat "${DIST}/_uc_headers.tmp"
echo ""
cat "${DIST}/_headers"
} > "${DIST}/_headers.new"
mv "${DIST}/_headers.new" "${DIST}/_headers"
rm -f "${DIST}/_uc_headers.tmp"
echo "# ==== 用户中心(/user/*):删掉官网 /* 继承的 CSP,换用用户中心自己的 ===="
echo "/user/*"
echo " ! Content-Security-Policy"
grep -iE '^[[:space:]]*Content-Security-Policy:' "${UC}/_headers"
} >> "${DIST}/_headers"
echo "==> combine-site: done — 用户中心并入 ${DIST}/user/,_headers 已按 /user/* 分域合并"
ls -la "${DIST}/user" | head
+4 -1
View File
@@ -35,7 +35,10 @@ func NewCORS() func(http.Handler) http.Handler {
h.Set("Access-Control-Allow-Origin", origin)
h.Add("Vary", "Origin")
h.Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
h.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
// X-Refresh-Token:静默续期/登出把 refresh token 放在此自定义头里
// (header token 方案,见 web/usercenter/lib/api/http.ts)。不放行则浏览器
// 预检拦截 /v1/auth/refresh → 登录后一刷新即掉线(login 不带此头故不受影响)。
h.Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Refresh-Token")
h.Set("Access-Control-Max-Age", "600")
}
// 预检请求直接 204(不落到业务路由,避免 /v1/... 的 OPTIONS 404)。
+6
View File
@@ -3,6 +3,7 @@ package httpapi
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
@@ -35,6 +36,11 @@ func TestCORS(t *testing.T) {
if w.Header().Get("Access-Control-Allow-Methods") == "" {
t.Fatalf("preflight missing Allow-Methods")
}
// 静默续期/登出把 refresh token 放 X-Refresh-Token 头,必须在允许头里,
// 否则浏览器预检拦截 /v1/auth/refresh → 登录后一刷新即掉线。
if ah := w.Header().Get("Access-Control-Allow-Headers"); !strings.Contains(ah, "X-Refresh-Token") {
t.Fatalf("preflight Allow-Headers must include X-Refresh-Token, got %q", ah)
}
// 未白名单 Origin:不补 Allow-Origin。
r = httptest.NewRequest("POST", "/v1/auth/login", nil)
+138 -6
View File
@@ -100,6 +100,8 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
.t-tag:hover { background: #d9e8fb; color: #1d4ed8; }
.ver-badge { font-weight: 700; color: #1f7a44; }
.sub-progress-badge { background: #f0f4ff; color: #3730a3; }
.owner-user{background:rgba(232,192,122,.16);border:1px solid #e8c07a;color:#e8c07a}
.owner-agent{background:rgba(126,224,162,.14);border:1px solid #7ee0a2;color:#7ee0a2}
/* 状态徽章 */
.status-badge { font-size: 11.5px; }
@@ -246,6 +248,78 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
.modal-cmd-code { font-family: "JetBrains Mono", "Fira Code", monospace; font-size: 13px; color: #86efac; word-break: break-all; display: block; }
.modal-copy-btn { margin-top: 10px; padding: 5px 14px; border-radius: 6px; background: #334155; color: #e2e8f0; border: none; font-size: 12px; cursor: pointer; }
.modal-copy-btn:hover { background: #475569; }
/* ════ 家族深色主题覆盖层(2026-07-06 对齐 brain/docs 家族风格;后置覆盖上方浅色基础样式) ════ */
:root{--bg:#0d1117;--card:#161b22;--card-2:#1c2330;--border:#283041;--fg:#e6edf3;--fg-soft:#aeb9c7;--muted:#7d8896;--accent:#58a6ff;--green:#3fb950;--amber:#d29922;--red:#f85149}
body{color:var(--fg);background:radial-gradient(1200px 600px at 80% -10%,rgba(88,166,255,.08),transparent 60%),var(--bg)}
header{background:linear-gradient(135deg,#101a2c 0%,#15294a 100%);border-bottom:1px solid var(--border)}
header .header-meta{color:var(--fg-soft)}
.stat-pill{background:rgba(88,166,255,.12);border:1px solid var(--border)}
.filter-bar{background:rgba(13,17,23,.92);backdrop-filter:blur(6px);border-bottom:1px solid var(--border)}
.filter-label{color:var(--muted)}
.filter-chip,.filter-btn{background:var(--card);border-color:var(--border);color:var(--fg-soft)}
.filter-chip:hover,.filter-btn:hover{border-color:var(--accent);color:var(--accent)}
.filter-chip.active,.filter-btn.active{background:var(--accent);border-color:var(--accent);color:#0d1117}
.filter-sep{background:var(--border)}
.section-title .s-count{background:rgba(255,255,255,.08)}
.st-open {background:rgba(88,166,255,.10);color:#79b8ff;border-left-color:var(--accent)}
.st-doing {background:rgba(210,153,34,.10);color:#e3b341;border-left-color:var(--amber)}
.st-done {background:rgba(63,185,80,.08);color:#7ee787;border-left-color:var(--green)}
.st-accepted{background:rgba(63,185,80,.12);color:#7ee787;border-left-color:var(--green)}
.todo-card{background:var(--card);border-color:var(--border)}
.todo-card.s-doing{border-left-color:var(--amber)}
.todo-card.s-done{border-left-color:var(--green)}
.todo-card.s-accepted{background:rgba(63,185,80,.06);border-left-color:var(--green)}
.item-id{color:var(--accent);background:#0b1020;border-color:var(--border)}
.item-title{color:var(--fg)}
.todo-card.s-accepted .item-title{color:var(--muted)}
.item-desc{color:var(--fg-soft)}
.item-desc code{background:#0b1020;border:1px solid var(--border);color:#c9d6e6}
.item-meta{color:var(--muted)}
.t-block{background:rgba(248,81,73,.15);color:#ff9492}
.t-high{background:rgba(210,153,34,.15);color:#e3b341}
.t-low{background:rgba(88,166,255,.12);color:#79b8ff}
.t-tag{background:var(--card-2);color:var(--fg-soft)}
.t-tag:hover{background:rgba(88,166,255,.15);color:var(--accent)}
.ver-badge{color:var(--green)}
.sub-progress-badge{background:rgba(88,166,255,.12);color:#79b8ff}
.s-open{background:rgba(88,166,255,.15);color:#79b8ff}
.s-doing{background:rgba(210,153,34,.18);color:#e3b341}
.s-done{background:rgba(63,185,80,.15);color:#7ee787}
.s-accepted{background:rgba(63,185,80,.18);color:#7ee787}
.empty-tip{color:var(--muted)}
.tier-1{background:rgba(163,113,247,.15);color:#d2a8ff}
.tier-2{background:rgba(57,197,187,.12);color:#76e3db}
.tier-3{background:var(--card-2);color:var(--fg-soft)}
.reject-btn{background:transparent;border-color:var(--red);color:#ff9492}
.reject-btn:hover{background:var(--red);color:#0d1117}
.reject-note{background:rgba(248,81,73,.08);border-color:rgba(248,81,73,.4);color:#ff9492}
.reject-date{color:rgba(248,81,73,.6)}
.gate-pending{background:rgba(210,153,34,.08);border-color:rgba(210,153,34,.4)}
.gate-granted{background:rgba(63,185,80,.08);border-color:rgba(63,185,80,.4)}
.gate-info{background:var(--card-2);border-color:var(--border)}
.gate-kind,.gate-note,.gate-ref{color:var(--fg-soft)}
.gate-date{color:var(--muted)}
.gate-note code,.gate-ref code{background:#0b1020;border:1px solid var(--border);color:#c9d6e6}
.subtask-block{background:var(--card-2);border-color:var(--border)}
.subtask-label{color:var(--muted)}
.subtask-progress-text{color:var(--fg-soft)}
.subtask-progress-bar{background:var(--border)}
.subtask-item{background:var(--card);border-color:var(--border)}
.subtask-item.s-done,.subtask-item.s-accepted{background:rgba(63,185,80,.06);border-color:rgba(63,185,80,.3)}
.subtask-item.s-doing{background:rgba(210,153,34,.06);border-color:rgba(210,153,34,.3)}
.sub-icon.s-open{color:var(--muted)}
.sub-sid{color:var(--muted)}
.subtask-item.s-done .sub-title,.subtask-item.s-accepted .sub-title{color:var(--muted)}
.dep-label{color:var(--muted)}
.dep-done{background:rgba(63,185,80,.15);color:#7ee787}
.dep-pending{background:rgba(248,81,73,.15);color:#ff9492}
.modal-overlay{background:rgba(0,0,0,.6)}
.modal-box{background:var(--card);border:1px solid var(--border)}
.modal-box h3{color:var(--fg)}
.modal-subtitle{color:var(--fg-soft)}
.modal-label{color:var(--fg-soft)}
.modal-textarea{background:#0b1020;border-color:var(--border);color:var(--fg)}
.modal-btn-cancel{background:var(--card-2);color:var(--fg-soft)}
</style>
</head>
<body>
@@ -253,14 +327,14 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<header>
<div class="wrap">
<h1>feature+windows — 项目 TODO</h1>
<div class="header-meta">生成于 2026-07-01 · 真相源 todo/todo.json</div>
<div class="header-meta">生成于 2026-07-07 · 真相源 todo/todo.json</div>
<div class="stats">
<div class="stat-pill"><strong>17</strong>全部</div>
<div class="stat-pill"><strong>8</strong>待开始</div>
<div class="stat-pill"><strong>18</strong>全部</div>
<div class="stat-pill"><strong>9</strong>待开始</div>
<div class="stat-pill"><strong>0</strong>开发中</div>
<div class="stat-pill"><strong>1</strong>待验收</div>
<div class="stat-pill"><strong>8</strong>已验收</div>
<div class="stat-pill gate-stat"><strong>1</strong>待确认</div>
</div>
</div>
</header>
@@ -293,7 +367,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="filter-sep"></div>
<div class="filter-group">
<span class="filter-label">平台 / 标签</span>
<button class="filter-chip" data-filter-tag="Android">Android</button><button class="filter-chip" data-filter-tag="Windows">Windows</button><button class="filter-chip" data-filter-tag="gRPC">gRPC</button><button class="filter-chip" data-filter-tag="iOS">iOS</button><button class="filter-chip" data-filter-tag="mac">mac</button><button class="filter-chip" data-filter-tag="前端">前端</button><button class="filter-chip" data-filter-tag="后端">后端</button><button class="filter-chip" data-filter-tag="运维">运维</button>
<button class="filter-chip" data-filter-tag="Android">Android</button><button class="filter-chip" data-filter-tag="CI/CD">CI/CD</button><button class="filter-chip" data-filter-tag="Web">Web</button><button class="filter-chip" data-filter-tag="Windows">Windows</button><button class="filter-chip" data-filter-tag="gRPC">gRPC</button><button class="filter-chip" data-filter-tag="iOS">iOS</button><button class="filter-chip" data-filter-tag="mac">mac</button><button class="filter-chip" data-filter-tag="前端">前端</button><button class="filter-chip" data-filter-tag="后端">后端</button><button class="filter-chip" data-filter-tag="运维">运维</button>
</div>
</div>
@@ -302,7 +376,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="section-block" id="section-open">
<div class="section-title st-open" data-toggle="open">
📋 待开始 <span class="s-count">8</span>
📋 待开始 <span class="s-count">9</span>
<span class="s-arrow">▴ 收起</span>
</div>
<div class="section-list-wrap " id="list-wrap-open">
@@ -332,6 +406,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="mac">mac</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-22</span>
</div>
@@ -362,6 +437,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="Android">Android</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-22</span>
</div>
@@ -392,6 +468,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="运维">运维</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-28</span>
</div>
@@ -422,6 +499,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-28</span>
</div>
@@ -452,6 +530,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-28</span>
</div>
@@ -482,6 +561,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="Android">Android</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-29</span>
</div>
@@ -512,12 +592,54 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="前端">前端</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-30</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="19"
data-level="mid"
data-status="open"
data-tier="1"
data-tags="前端,Web,mac,iOS,Android,Windows,CI/CD">
<div class="card-header">
<span class="item-id">#19</span>
<span class="item-title">前端设计系统治理重构(ds-flow 全端)</span>
<div class="card-badges">
<span class="tag status-badge s-open">待开始</span>
<span class="tag t-high">重要</span>
<span class="tag tier-1">一级</span>
</div>
</div>
<div class="item-desc">用 ds-flow 把 Flutter 五端+官网+用户中心收口到设计单源。补原型三件套(atoms.css/icons.js/index.html)+Web共享原子层去重(各自实现+同源闸)+硬编码色/fidelity闸+启用pre-commit。6阶段。计划见 docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md</div>
<div class="gate-block gate-pending">
<div class="gate-head"><span class="gate-badge pending">⏸ 待确认</span>
<span class="gate-kind">一级方案规划 · 确认后方可继续开发</span></div>
<div class="gate-note">6 阶段 ds-flow 治理重构:Phase0 CLAUDE.md+计划落库 / Phase1 原型三件套(atoms.css/icons.js/index.html,收敛ui_kits) / Phase2 Web token同源闸 / Phase3 Web共享原子层去重(各自实现+同源闸,★工作量最大) / Phase4 Flutter收尾+golden补齐 / Phase5 静态闸挂满+启用pre-commit+fidelity体检。已定决策:Web各自实现+同源闸、ui_kits收敛纯HTML、先定稿再逐刀。主题保持light/dark。</div><div class="gate-ref">📄 详见 <code>docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md</code></div>
<button class="approve-btn" data-id="19">确认执行 ✓</button>
<div class="approve-cmd" id="approve-cmd-19" style="display:none">
<div class="approve-cmd-label">在终端运行:</div>
<code class="approve-cmd-code">/todo approve 19</code>
<button class="approve-copy-btn" data-cmd="/todo approve 19">复制命令</button>
</div>
</div>
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="Web">Web</span> <span class="tag t-tag" data-tag="mac">mac</span> <span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="Android">Android</span> <span class="tag t-tag" data-tag="Windows">Windows</span> <span class="tag t-tag" data-tag="CI/CD">CI/CD</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-07-07</span>
</div>
</div>
</li>
<li class="todo-card s-open"
data-id="3"
data-level="low"
@@ -542,6 +664,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="Windows">Windows</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-22</span>
</div>
@@ -593,6 +716,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="mac">mac</span> <span class="tag t-tag" data-tag="前端">前端</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-07-01</span>
</div>
@@ -633,6 +757,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-28</span>
<span class="meta-date">✅ 验收 2026-06-30</span>
</div>
@@ -663,6 +788,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-28</span>
<span class="meta-date">✅ 验收 2026-06-30</span>
</div>
@@ -693,6 +819,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-28</span>
<span class="meta-date">✅ 验收 2026-06-30</span>
</div>
@@ -723,6 +850,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="iOS">iOS</span> <span class="tag t-tag" data-tag="前端">前端</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-30</span>
<span class="meta-date">✅ 验收 2026-06-30</span>
</div>
@@ -753,6 +881,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-28</span>
<span class="meta-date">✅ 验收 2026-06-30</span>
</div>
@@ -783,6 +912,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span> <span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="gRPC">gRPC</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-29</span>
<span class="meta-date">✅ 验收 2026-06-30</span>
</div>
@@ -813,6 +943,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="前端">前端</span> <span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-28</span>
<span class="meta-date">✅ 验收 2026-06-28</span>
</div>
@@ -843,6 +974,7 @@ ul.todo-list { list-style: none; margin: 0; padding: 0; }
<div class="card-footer">
<div class="tag-row"><span class="tag t-tag" data-tag="后端">后端</span></div>
<div class="item-meta">
<span class="tag owner-agent">🤖 agent</span>
<span class="meta-date">🕐 2026-06-28</span>
<span class="meta-date">✅ 验收 2026-06-28</span>
</div>
+66 -19
View File
@@ -1,9 +1,9 @@
{
"meta": {
"title": "feature+windows — 项目 TODO",
"updated_at": "2026-06-30T23:10:43.458Z"
"updated_at": "2026-07-07T15:41:36.537Z"
},
"seq": 18,
"seq": 19,
"items": [
{
"id": 1,
@@ -18,7 +18,8 @@
"created_at": "2026-06-22T09:32:42.340Z",
"done": false,
"completed_at": null,
"version": null
"version": null,
"owner": "agent"
},
{
"id": 2,
@@ -33,7 +34,8 @@
"created_at": "2026-06-22T09:32:56.098Z",
"done": false,
"completed_at": null,
"version": null
"version": null,
"owner": "agent"
},
{
"id": 3,
@@ -48,7 +50,8 @@
"created_at": "2026-06-22T09:32:56.169Z",
"done": false,
"completed_at": null,
"version": null
"version": null,
"owner": "agent"
},
{
"id": 4,
@@ -64,7 +67,8 @@
"created_at": "2026-06-27T16:39:31.549Z",
"done": false,
"completed_at": null,
"version": null
"version": null,
"owner": "agent"
},
{
"id": 5,
@@ -79,7 +83,8 @@
"created_at": "2026-06-27T23:37:24.980Z",
"done": true,
"completed_at": "2026-06-28T13:19:24.207Z",
"version": null
"version": null,
"owner": "agent"
},
{
"id": 6,
@@ -95,7 +100,8 @@
"created_at": "2026-06-28T00:21:30.990Z",
"done": true,
"completed_at": "2026-06-28T13:19:24.299Z",
"version": null
"version": null,
"owner": "agent"
},
{
"id": 7,
@@ -110,7 +116,8 @@
"created_at": "2026-06-28T00:26:44.498Z",
"done": true,
"completed_at": "2026-06-30T14:44:35.082Z",
"version": null
"version": null,
"owner": "agent"
},
{
"id": 8,
@@ -125,7 +132,8 @@
"created_at": "2026-06-28T00:38:22.413Z",
"done": false,
"completed_at": null,
"version": null
"version": null,
"owner": "agent"
},
{
"id": 9,
@@ -141,7 +149,8 @@
"created_at": "2026-06-28T09:32:26.008Z",
"done": true,
"completed_at": "2026-06-30T14:44:34.810Z",
"version": null
"version": null,
"owner": "agent"
},
{
"id": 10,
@@ -156,7 +165,8 @@
"created_at": "2026-06-28T09:32:26.080Z",
"done": true,
"completed_at": "2026-06-30T14:44:34.991Z",
"version": null
"version": null,
"owner": "agent"
},
{
"id": 11,
@@ -171,7 +181,8 @@
"created_at": "2026-06-28T11:27:56.637Z",
"done": true,
"completed_at": "2026-06-30T14:47:39.343Z",
"version": null
"version": null,
"owner": "agent"
},
{
"id": 12,
@@ -187,7 +198,8 @@
"created_at": "2026-06-28T11:27:56.708Z",
"done": false,
"completed_at": null,
"version": null
"version": null,
"owner": "agent"
},
{
"id": 13,
@@ -204,7 +216,8 @@
"created_at": "2026-06-29T04:55:46.733Z",
"done": true,
"completed_at": "2026-06-29T16:59:39.926Z",
"version": null
"version": null,
"owner": "agent"
},
{
"id": 14,
@@ -221,7 +234,8 @@
"created_at": "2026-06-29T09:36:14.004Z",
"done": false,
"completed_at": null,
"version": null
"version": null,
"owner": "agent"
},
{
"id": 15,
@@ -237,7 +251,8 @@
"created_at": "2026-06-30T10:56:48.894Z",
"done": true,
"completed_at": "2026-06-30T14:44:34.900Z",
"version": null
"version": null,
"owner": "agent"
},
{
"id": 16,
@@ -253,7 +268,8 @@
"created_at": "2026-06-30T14:33:48.996Z",
"done": false,
"completed_at": null,
"version": null
"version": null,
"owner": "agent"
},
{
"id": 18,
@@ -269,7 +285,38 @@
"created_at": "2026-06-30T23:00:25.193Z",
"done": false,
"completed_at": null,
"version": null
"version": null,
"owner": "agent"
},
{
"id": 19,
"title": "前端设计系统治理重构(ds-flow 全端)",
"desc": "用 ds-flow 把 Flutter 五端+官网+用户中心收口到设计单源。补原型三件套(atoms.css/icons.js/index.html)+Web共享原子层去重(各自实现+同源闸)+硬编码色/fidelity闸+启用pre-commit。6阶段。计划见 docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md",
"level": "mid",
"tier": 1,
"tags": [
"前端",
"Web",
"mac",
"iOS",
"Android",
"Windows",
"CI/CD"
],
"owner": "agent",
"status": "open",
"created_at": "2026-07-07T15:41:25.760Z",
"done": false,
"completed_at": null,
"version": null,
"gate": {
"kind": "plan",
"note": "6 阶段 ds-flow 治理重构:Phase0 CLAUDE.md+计划落库 / Phase1 原型三件套(atoms.css/icons.js/index.html,收敛ui_kits) / Phase2 Web token同源闸 / Phase3 Web共享原子层去重(各自实现+同源闸,★工作量最大) / Phase4 Flutter收尾+golden补齐 / Phase5 静态闸挂满+启用pre-commit+fidelity体检。已定决策:Web各自实现+同源闸、ui_kits收敛纯HTML、先定稿再逐刀。主题保持light/dark。",
"ref": "docs/superpowers/plans/2026-07-07-frontend-ds-refactor.md",
"approval": "pending",
"proposed_at": "2026-07-07T15:41:36.536Z",
"approved_at": null
}
}
]
}
+105 -28
View File
@@ -1,5 +1,6 @@
// shared.tsx — 公共样式常量与原子(承袭 ucapp.jsx 的 ucCard / ucInput / UCLang
import React from 'react';
'use client';
import React, { useEffect, useRef, useState } from 'react';
import type { Lang } from '../lib/i18n';
export const card: React.CSSProperties = {
@@ -22,34 +23,110 @@ export const input: React.CSSProperties = {
boxSizing: 'border-box',
};
const LANGS: [Lang, string][] = [
['en', 'English'], ['zh', '中文'], ['ja', '日本語'],
['ko', '한국어'], ['ru', 'Русский'], ['es', 'Español'],
];
// 自定义下拉:原生 <select> 的弹层由系统定位(macOS 锚在选中项、会"漂移"),
// 改成自控菜单(按钮 + 绝对定位列表),不漂移、样式统一、跨端一致。
export function LangSeg({ lang, setLang }: { lang: Lang; setLang: (l: Lang) => void }) {
// 6 语言用原生下拉(段控横排会挤)。默认 en。
const langs: [Lang, string][] = [
['en', 'English'], ['zh', '中文'], ['ja', '日本語'],
['ko', '한국어'], ['ru', 'Русский'], ['es', 'Español'],
];
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!open) return;
const onDoc = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); };
document.addEventListener('mousedown', onDoc);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onDoc);
document.removeEventListener('keydown', onKey);
};
}, [open]);
const current = LANGS.find(([v]) => v === lang)?.[1] ?? String(lang);
return (
<select
value={lang}
onChange={(e) => setLang(e.target.value as Lang)}
aria-label="Language"
style={{
// 圆角矩形 + 柔和边框(原纯 --border 太生硬)。
border: '1px solid color-mix(in oklab, var(--border), transparent 55%)',
cursor: 'pointer',
borderRadius: 10,
padding: '5px 10px',
fontFamily: 'var(--font-sans)',
fontSize: 12.5,
fontWeight: 700,
background: 'var(--bg-subtle)',
color: 'var(--fg2)',
boxShadow: '0 1px 2px rgba(45, 30, 20, 0.05)',
}}
>
{langs.map(([v, l]) => (
<option key={v} value={v}>{l}</option>
))}
</select>
<div ref={ref} style={{ position: 'relative', display: 'inline-block' }}>
<button
type="button"
onClick={() => setOpen((o) => !o)}
aria-haspopup="listbox"
aria-expanded={open}
aria-label="Language"
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 6,
border: '1px solid color-mix(in oklab, var(--border), transparent 55%)',
cursor: 'pointer',
borderRadius: 10,
padding: '5px 10px',
fontFamily: 'var(--font-sans)',
fontSize: 12.5,
fontWeight: 700,
background: 'var(--bg-subtle)',
color: 'var(--fg2)',
boxShadow: '0 1px 2px rgba(45, 30, 20, 0.05)',
}}
>
<span>{current}</span>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" aria-hidden="true"
style={{ transform: open ? 'rotate(180deg)' : 'none', transition: 'transform 140ms' }}>
<path d="M6 9l6 6 6-6" stroke="currentColor" strokeWidth="2.4"
strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
{open && (
<div
role="listbox"
style={{
position: 'absolute',
top: 'calc(100% + 6px)',
right: 0,
minWidth: 132,
background: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: 12,
boxShadow: '0 10px 30px rgba(20, 12, 6, 0.22)',
padding: 5,
zIndex: 60,
display: 'flex',
flexDirection: 'column',
gap: 1,
}}
>
{LANGS.map(([v, l]) => {
const on = v === lang;
return (
<button
key={v}
type="button"
role="option"
aria-selected={on}
onClick={() => { setLang(v); setOpen(false); }}
style={{
textAlign: 'left',
border: 'none',
cursor: 'pointer',
background: on ? 'var(--accent-subtle, var(--bg-subtle))' : 'transparent',
color: on ? 'var(--accent)' : 'var(--fg1)',
fontWeight: on ? 700 : 500,
fontFamily: 'var(--font-sans)',
fontSize: 13,
padding: '8px 11px',
borderRadius: 8,
whiteSpace: 'nowrap',
}}
>
{l}
</button>
);
})}
</div>
)}
</div>
);
}
+42 -11
View File
@@ -3,7 +3,7 @@
* 迁自 design/ui_kits/website/index.html 的 <header> + site.js 的菜单/滚动毛玻璃逻辑。
* 语言切换由原型的 JS 文本替换改为「路由跳转」(zh=/, en=/en/),单显不并排(铁律 6)。
*/
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Download, Menu } from 'lucide-react';
import { SITE } from '../config/site';
@@ -29,6 +29,17 @@ function Mark() {
export default function Header({ lang = 'zh', t = {} }) {
const [open, setOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const [langOpen, setLangOpen] = useState(false);
const langRef = useRef(null);
useEffect(() => {
if (!langOpen) return;
const onDoc = (e) => { if (langRef.current && !langRef.current.contains(e.target)) setLangOpen(false); };
const onKey = (e) => { if (e.key === 'Escape') setLangOpen(false); };
document.addEventListener('mousedown', onDoc);
document.addEventListener('keydown', onKey);
return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
}, [langOpen]);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
@@ -64,16 +75,36 @@ export default function Header({ lang = 'zh', t = {} }) {
))}
</nav>
<div class="right">
<select
class="langsel"
value={lang}
onChange={(e) => { window.location.href = langHref(e.target.value); }}
aria-label="Language"
>
{langs.map(([code, label]) => (
<option key={code} value={code}>{label}</option>
))}
</select>
<div ref={langRef} style={{ position: 'relative', display: 'inline-block' }}>
<button
type="button"
class="langsel"
onClick={() => setLangOpen((o) => !o)}
aria-haspopup="listbox"
aria-expanded={langOpen}
aria-label="Language"
style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}
>
<span>{(langs.find(([c]) => c === lang) || ['', 'English'])[1]}</span>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" aria-hidden="true"
style={{ transform: langOpen ? 'rotate(180deg)' : 'none', transition: 'transform 140ms' }}>
<path d="M6 9l6 6 6-6" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
{langOpen && (
<div role="listbox" style={{ position: 'absolute', top: 'calc(100% + 6px)', right: 0, minWidth: 132, background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 12, boxShadow: '0 10px 30px rgba(20, 12, 6, 0.22)', padding: 5, zIndex: 60, display: 'flex', flexDirection: 'column', gap: 1 }}>
{langs.map(([code, label]) => {
const on = code === lang;
return (
<a key={code} role="option" aria-selected={on} href={langHref(code)}
style={{ textAlign: 'left', textDecoration: 'none', background: on ? 'var(--accent-subtle, var(--bg-subtle))' : 'transparent', color: on ? 'var(--accent)' : 'var(--fg1)', fontWeight: on ? 700 : 500, fontFamily: 'var(--font-sans)', fontSize: 13, padding: '8px 11px', borderRadius: 8, whiteSpace: 'nowrap' }}>
{label}
</a>
);
})}
</div>
)}
</div>
<a class="linklogin" href={SITE.usercenter}>{t.login}</a>
<a class="btn btn-primary" href="#download">
<Download />