feat(design): design/ 真相源 + 防漂移静态闸(#25)

真相源整固:
- design/index.html 登记簿:token 色板(var() 实时渲染)/ 组件登记卡 /
  图标与跨端映射 / guidelines / ui_kits 单一入口,check-ds 强制登记
- design/icons.js 图标单源(Lucide 线条)+ icon-map.json 跨端映射表
  (web/iOS/Android 三列,iOS 12 个 SF Symbol 全部收编登记)
- components/core/Icon.jsx 单源图标渲染器;MicBar/RecognitionOverlay 接入
- tokens 新增 --brand-wechat(-press)(微信官方绿)与 --overlay-wave
- fonts.css 弃 Google Fonts CDN,Outfit 可变字体自托管
  (assets/outfit-latin.woff2,40KB;中国网络下 CDN 不可用会卡字体)
- design/serve.mjs 零依赖预览服务器(登记簿与原型评审入口)

codegen 强化(export-tokens.mjs):
- 产物直写消费位置:ios/dudu/Shared/DuduTheme.swift、
  android/.../design/DuduTheme.kt——删除 design-pipeline/generated/
  中间拷贝层(此前 app 内是手工拷贝,--check 守不到真实消费文件已漂移)
- 新增 web/tokens.css 产物(custom props 透传 + Outfit data URI 内嵌)
- tokens 文件遍历排序,产物确定性

存量收编:
- desktop:三个窗口内联 SVG → Icon 组件;tray 裸 rgba → token+opacity
- Android:LoginScreen 微信绿常量 → DuduPalette.brandWechat(Press),
  Color.White → textOnAccent
- 官网 web/index.html:接入 tokens.css,34 处硬编码 hex 全部 var() 化;
  深色板块(metrics/cta/footer)改挂 data-theme="dark" 复用 dark 令牌;
  移除 Google Fonts CDN
- design 包内 8 处裸色:微信绿 token 化、装饰色 ds-ignore 白名单、
  同值色改 var() 引用

防漂移闸(零依赖 Node):
- design-pipeline/check-ds.mjs:真相源自检 7 道(dark⊆light / var 链 /
  裸色∈tokens 值集 / 组件登记 / icons 唯一 / icon-map 双向同集 / 禁 emoji)
- design-pipeline/check-code.mjs:代码侧单源(desktop 禁字面色+内联 path /
  iOS 禁字面色+SF Symbol 同集 / Android 禁 Color(0x / web 禁字面色),
  ds-ignore 行级豁免制
- .githooks/pre-commit(启用:git config core.hooksPath .githooks)
- CI design job 扩展为三道闸全绿
- 反向验证通过:注入裸色/孤儿 token/未登记组件/未登记 symbol 均被拦截

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-11 00:05:22 +08:00
parent 4aec0ef6e9
commit 208d0cda12
36 changed files with 1014 additions and 494 deletions
+28 -58
View File
@@ -5,49 +5,21 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dudu — 按住说话,文字上屏</title>
<meta name="description" content="dudu 语音输入法:大模型识别,流式实时上屏。Mac / Windows / iPhone / iPad / Android。每天免费 3 分钟。">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="tokens.css">
<style>
/* ────────────────────────────────────────────────────────────────
令牌来源 design/tokens/(宣传页独立部署,按值内联;改动需同步)
令牌层由 tokens.css 提供(design/tokens/ 经 export-tokens.mjs 生成,
含 Outfit 字体内嵌,禁外部 CDN)。本页只允许 var(--*),禁字面色值
check-code 闸拦截)。深色板块用 data-theme="dark" 复用 dark 令牌。
品牌硬规则:单一品牌蓝 · 无渐变 · 无 emoji · 波形动画仅录音中
──────────────────────────────────────────────────────────────── */
:root {
--bg: #F6F7FA;
--card: #FFFFFF;
--surface-2: #F0F1F4;
--surface-3: #E4E6EB;
--border-1: #E4E6EB;
--border-2: #D2D5DD;
--text-1: #1B1E26;
--text-2: #5A6072;
--text-3: #A6ABB8;
--accent: #4F6EF7;
--accent-hover: #3D58DB;
--accent-press: #2F44B3;
--accent-soft: #EEF1FE;
--accent-text: #3D58DB;
--positive: #16A34A;
--positive-soft: #DCFCE7;
--warning: #E8890C;
--warning-soft: #FCEFD7;
--ink: #11131A;
--ink-card: #171A22;
--ink-border: #272C38;
--ink-text-2: #9AA1B2;
--overlay-bg: rgba(24, 26, 34, 0.95);
--overlay-text: #F2F4FA;
--overlay-text-2: #9AA3BD;
--overlay-text-3: #646E8C;
--shadow-overlay: 0 12px 36px rgba(10, 12, 18, 0.38);
--shadow-card: 0 1px 3px rgba(17, 19, 26, 0.05);
--shadow-window: 0 8px 28px rgba(17, 19, 26, 0.14);
--ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
--sans: -apple-system, BlinkMacSystemFont, "PingFang SC", "MiSans",
"HarmonyOS Sans SC", "Microsoft YaHei", sans-serif;
--latin: "Outfit", -apple-system, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, monospace;
/* 页面级别名 → 设计系统 token(仅别名,禁止写字面色值) */
--bg: var(--bg-app);
--card: var(--surface-card);
--sans: var(--font-sans);
--latin: var(--font-sans);
--mono: var(--font-mono);
}
* { box-sizing: border-box; margin: 0; }
html { scroll-behavior: smooth; }
@@ -90,7 +62,7 @@ nav {
}
.btn-sm { height: 32px; padding: 0 14px; font-size: 13px; }
.btn-lg { height: 48px; padding: 0 26px; font-size: 16px; border-radius: 999px; }
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary { background: var(--accent); color: var(--text-on-accent); }
.btn-primary:hover { background: var(--accent-hover); }
.btn-primary:active { background: var(--accent-press); }
.btn-secondary { background: var(--card); border-color: var(--border-2); color: var(--text-1); }
@@ -154,7 +126,7 @@ nav {
pointer-events: none;
}
.overlay.show { opacity: 1; translate: 0 0; }
.wave { display: inline-flex; align-items: center; gap: 3px; height: 24px; color: #7C9BFF; }
.wave { display: inline-flex; align-items: center; gap: 3px; height: 24px; color: var(--overlay-wave); }
.wave i { display: block; width: 3px; border-radius: 2px; background: currentColor; }
.recording .wave i { animation: wavebounce 0.9s cubic-bezier(0.45, 0, 0.2, 1) infinite alternate; }
@keyframes wavebounce { from { transform: scaleY(0.35); } to { transform: scaleY(1); } }
@@ -168,7 +140,7 @@ nav {
.micbar {
margin-top: 64px; width: 100%; height: 56px; border: none; cursor: pointer;
border-radius: 999px; background: var(--accent); color: #fff;
border-radius: 999px; background: var(--accent); color: var(--text-on-accent);
font-family: var(--sans); font-size: 16px; font-weight: 600;
display: inline-flex; align-items: center; justify-content: center; gap: 9px;
user-select: none; -webkit-user-select: none; touch-action: none;
@@ -184,22 +156,22 @@ kbd {
}
/* ── 指标带(墨色) ── */
.metrics { background: var(--ink); color: #ECEEF4; border-bottom: 1px solid var(--ink-border); }
.metrics { background: var(--gray-950); color: var(--text-1); border-bottom: 1px solid var(--border-1); }
.metrics-grid { display: grid; grid-template-columns: repeat(3, 1fr); }
.metric {
padding: 44px 32px; text-align: center;
border-left: 1px solid var(--ink-border);
border-left: 1px solid var(--border-1);
}
.metric:first-child { border-left: none; }
.metric .num {
font-family: var(--latin); font-weight: 700;
font-size: clamp(34px, 4.4vw, 52px); line-height: 1.1;
}
.metric .num small { font-size: 0.45em; font-weight: 600; color: var(--ink-text-2); margin-left: 2px; }
.metric .label { margin-top: 8px; font-size: 14px; color: var(--ink-text-2); }
.metric .num small { font-size: 0.45em; font-weight: 600; color: var(--text-2); margin-left: 2px; }
.metric .label { margin-top: 8px; font-size: 14px; color: var(--text-2); }
@media (max-width: 720px) {
.metrics-grid { grid-template-columns: 1fr; }
.metric { border-left: none; border-top: 1px solid var(--ink-border); padding: 28px 20px; }
.metric { border-left: none; border-top: 1px solid var(--border-1); padding: 28px 20px; }
.metric:first-child { border-top: none; }
}
@@ -282,25 +254,23 @@ section { border-bottom: 1px solid var(--border-1); }
@media (max-width: 820px) { .pricing { grid-template-columns: 1fr; } }
/* ── 尾部 CTA(墨色) ── */
.cta { background: var(--ink); color: #ECEEF4; border-bottom: 1px solid var(--ink-border); }
.cta { background: var(--gray-950); color: var(--text-1); border-bottom: 1px solid var(--border-1); }
.cta-inner { padding: 110px 0; text-align: center; }
.cta .logo-big { color: #5C77E8; }
.cta .logo-big { color: var(--accent); }
.cta h2 { margin-top: 26px; font-size: clamp(34px, 5vw, 56px); font-weight: 700; letter-spacing: -0.01em; }
.cta p { margin-top: 14px; color: var(--ink-text-2); font-size: 16px; }
.cta p { margin-top: 14px; color: var(--text-2); font-size: 16px; }
.cta .btn { margin-top: 36px; }
.cta .btn-primary { background: #5C77E8; }
.cta .btn-primary:hover { background: #6E89F9; }
/* ── 页脚 ── */
footer { background: var(--ink); color: var(--ink-text-2); }
footer { background: var(--gray-950); color: var(--text-2); }
.footer-inner {
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px;
padding: 28px 0; font-size: 13px;
}
.footer-inner .brand { color: #ECEEF4; }
.footer-inner .brand svg { color: #5C77E8; }
.footer-inner .brand { color: var(--text-1); }
.footer-inner .brand svg { color: var(--accent); }
.footer-links { display: flex; gap: 22px; }
.footer-links a:hover { color: #ECEEF4; }
.footer-links a:hover { color: var(--text-1); }
</style>
</head>
<body>
@@ -367,7 +337,7 @@ footer { background: var(--ink); color: var(--ink-text-2); }
</header>
<!-- ════════ 指标带 ════════ -->
<div class="metrics">
<div class="metrics" data-theme="dark">
<div class="frame metrics-grid">
<div class="metric reveal">
<div class="num">&lt;500<small>ms</small></div>
@@ -489,7 +459,7 @@ footer { background: var(--ink); color: var(--ink-text-2); }
</section>
<!-- ════════ 尾部 CTA ════════ -->
<div class="cta">
<div class="cta" data-theme="dark">
<div class="frame cta-inner reveal">
<svg class="logo-big" width="64" height="64" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"><circle cx="24" cy="24" r="18"/><path d="M17 20.5v7"/><path d="M24 16.5v15"/><path d="M31 20.5v7"/></svg>
<h2>从下一句话开始<br>用说的</h2>
@@ -499,7 +469,7 @@ footer { background: var(--ink); color: var(--ink-text-2); }
</div>
<!-- ════════ 页脚 ════════ -->
<footer>
<footer data-theme="dark">
<div class="frame footer-inner">
<a class="brand" href="#">
<svg width="20" height="20" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"><circle cx="24" cy="24" r="18"/><path d="M17 20.5v7"/><path d="M24 16.5v15"/><path d="M31 20.5v7"/></svg>
+159
View File
File diff suppressed because one or more lines are too long