Files
dudu/design/index.html
T
wangjia 208d0cda12 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>
2026-07-11 00:05:22 +08:00

214 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dudu 设计系统 · 登记簿</title>
<link rel="stylesheet" href="./styles.css">
<style>
/* 登记簿版式(色值一律 var(--*),本页也受 check-ds 闸约束) */
body { margin: 0; background: var(--bg-app); color: var(--text-1); font-family: var(--font-sans); line-height: var(--leading-normal); }
.wrap { max-width: 1080px; margin: 0 auto; padding: 0 24px 80px; }
header.hero { padding: 40px 0 8px; display: flex; align-items: center; justify-content: space-between; }
header.hero h1 { font-size: var(--text-2xl); margin: 0; }
header.hero p { color: var(--text-2); margin: 4px 0 0; font-size: var(--text-sm); }
.theme-btn { height: var(--control-sm); padding: 0 14px; border-radius: var(--radius-sm); border: 1px solid var(--border-2); background: var(--surface-card); color: var(--text-1); cursor: pointer; font-size: var(--text-sm); }
section { background: var(--surface-card); border: 1px solid var(--border-1); border-radius: var(--radius-lg); padding: 22px 26px; margin-top: 20px; box-shadow: var(--shadow-card); }
h2 { font-size: var(--text-lg); margin: 0 0 12px; }
h3 { font-size: var(--text-base); margin: 18px 0 8px; color: var(--accent-text); }
.rule { border-left: 3px solid var(--accent); background: var(--accent-soft); padding: 8px 14px; border-radius: 0 var(--radius-sm) var(--radius-sm) 0; font-size: var(--text-sm); margin: 8px 0; }
.sw-row { display: flex; flex-wrap: wrap; gap: 8px; }
.sw { width: 88px; font-size: var(--text-xs); color: var(--text-2); }
.sw i { display: block; height: 40px; border-radius: var(--radius-sm); border: 1px solid var(--border-1); margin-bottom: 3px; }
table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); margin-top: 8px; }
th, td { border: 1px solid var(--border-1); padding: 6px 10px; text-align: left; }
th { background: var(--surface-2); }
a { color: var(--accent-text); text-decoration: none; }
a:hover { text-decoration: underline; }
code { background: var(--surface-2); padding: 1px 6px; border-radius: var(--radius-xs); font-family: var(--font-mono); font-size: var(--text-xs); }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px; }
.entry { border: 1px solid var(--border-1); border-radius: var(--radius-md); padding: 12px 14px; }
.entry b { font-size: var(--text-base); }
.entry .sub { color: var(--text-2); font-size: var(--text-xs); margin: 3px 0 6px; }
.entry .meta { font-size: var(--text-xs); color: var(--text-3); }
#icon-grid { display: flex; flex-wrap: wrap; gap: 14px; }
#icon-grid .ic { width: 96px; text-align: center; font-size: var(--text-xs); color: var(--text-2); }
#icon-grid .ic .box { height: 48px; display: flex; align-items: center; justify-content: center; border: 1px solid var(--border-1); border-radius: var(--radius-sm); margin-bottom: 4px; color: var(--text-1); }
.note { color: var(--text-3); font-size: var(--text-xs); }
</style>
</head>
<body>
<div class="wrap">
<header class="hero">
<div>
<h1>dudu 设计系统 · 登记簿</h1>
<p>单一真相源活文档 —— 所有 token / 组件 / 图标必须在本页登记(check-ds 闸强制)。评审预览:<code>node design/serve.mjs</code></p>
</div>
<button class="theme-btn" onclick="const r=document.documentElement; r.dataset.theme = r.dataset.theme==='dark'?'':'dark'">切换 Light / Dark</button>
</header>
<section>
<h2>规则速览(L1 · 设计元素单源)</h2>
<div class="rule">新增<b>颜色/字号/间距</b>:先登记 <code>tokens/*.css</code><code>node design-pipeline/export-tokens.mjs</code> 再生成各端 → 业务代码只引 token。</div>
<div class="rule">新增<b>组件</b>:先建 <code>components/&lt;&gt;/X.jsx</code>(+ .d.ts)并在本页「组件登记」加卡,再到各端实现。</div>
<div class="rule">新增<b>图标</b>:先登记 <code>icons.js</code>Lucide 24 网格 2px 圆头)+ <code>icon-map.json</code> 跨端映射,业务代码禁止内联 &lt;path&gt;</div>
<div class="rule">违规谁拦:<code>design-pipeline/check-ds.mjs</code>(真相源自检)/ <code>check-code.mjs</code>(代码侧单源)/ <code>export-tokens.mjs --check</code>(产物零 diff),pre-commit 与 CI 双挂。</div>
<h3>各端消费方式</h3>
<table>
<tr><th></th><th>消费方式</th><th>令牌文件</th></tr>
<tr><td>桌面(Tauri web 层)</td><td>vite alias <code>@dudu/design</code> 直引 CSS 变量与组件</td><td><code>design/tokens/*.css</code>(源)</td></tr>
<tr><td>iOSSwiftUI</td><td><code>DuduTheme.*</code> 常量(codegen 直写)</td><td><code>ios/dudu/Shared/DuduTheme.swift</code></td></tr>
<tr><td>AndroidCompose</td><td><code>DuduTheme.Light/Dark</code><code>DuduPalette</code> 注入</td><td><code>android/.../design/DuduTheme.kt</code></td></tr>
<tr><td>官网(web/</td><td><code>&lt;link href="tokens.css"&gt;</code>,页面只允许 var(--*)</td><td><code>web/tokens.css</code>codegen,含 Outfit 内嵌)</td></tr>
</table>
</section>
<section>
<h2>Tokens 色板</h2>
<p class="note">色块直接渲染 var(--token),与 tokens/colors.css 永远同步;右上角切主题查看 dark 值。完整定义见 <a href="./tokens/colors.css">colors.css</a> · <a href="./tokens/typography.css">typography.css</a> · <a href="./tokens/spacing.css">spacing.css</a> · <a href="./tokens/effects.css">effects.css</a> · <a href="./tokens/fonts.css">fonts.css</a></p>
<h3>品牌蓝阶梯</h3>
<div class="sw-row">
<div class="sw"><i style="background:var(--blue-50)"></i>blue-50</div>
<div class="sw"><i style="background:var(--blue-100)"></i>blue-100</div>
<div class="sw"><i style="background:var(--blue-200)"></i>blue-200</div>
<div class="sw"><i style="background:var(--blue-300)"></i>blue-300</div>
<div class="sw"><i style="background:var(--blue-400)"></i>blue-400</div>
<div class="sw"><i style="background:var(--blue-500)"></i>blue-500 ★</div>
<div class="sw"><i style="background:var(--blue-600)"></i>blue-600</div>
<div class="sw"><i style="background:var(--blue-700)"></i>blue-700</div>
<div class="sw"><i style="background:var(--blue-800)"></i>blue-800</div>
<div class="sw"><i style="background:var(--blue-900)"></i>blue-900</div>
</div>
<h3>中性阶梯(冷灰)</h3>
<div class="sw-row">
<div class="sw"><i style="background:var(--gray-0)"></i>gray-0</div>
<div class="sw"><i style="background:var(--gray-25)"></i>gray-25</div>
<div class="sw"><i style="background:var(--gray-50)"></i>gray-50</div>
<div class="sw"><i style="background:var(--gray-100)"></i>gray-100</div>
<div class="sw"><i style="background:var(--gray-200)"></i>gray-200</div>
<div class="sw"><i style="background:var(--gray-300)"></i>gray-300</div>
<div class="sw"><i style="background:var(--gray-400)"></i>gray-400</div>
<div class="sw"><i style="background:var(--gray-500)"></i>gray-500</div>
<div class="sw"><i style="background:var(--gray-600)"></i>gray-600</div>
<div class="sw"><i style="background:var(--gray-700)"></i>gray-700</div>
<div class="sw"><i style="background:var(--gray-800)"></i>gray-800</div>
<div class="sw"><i style="background:var(--gray-900)"></i>gray-900</div>
<div class="sw"><i style="background:var(--gray-950)"></i>gray-950</div>
</div>
<h3>语义与功能</h3>
<div class="sw-row">
<div class="sw"><i style="background:var(--accent)"></i>accent</div>
<div class="sw"><i style="background:var(--accent-hover)"></i>accent-hover</div>
<div class="sw"><i style="background:var(--accent-press)"></i>accent-press</div>
<div class="sw"><i style="background:var(--accent-soft)"></i>accent-soft</div>
<div class="sw"><i style="background:var(--positive)"></i>positive</div>
<div class="sw"><i style="background:var(--warning)"></i>warning</div>
<div class="sw"><i style="background:var(--danger)"></i>danger</div>
<div class="sw"><i style="background:var(--bg-app)"></i>bg-app</div>
<div class="sw"><i style="background:var(--surface-card)"></i>surface-card</div>
<div class="sw"><i style="background:var(--surface-2)"></i>surface-2</div>
<div class="sw"><i style="background:var(--surface-3)"></i>surface-3</div>
<div class="sw"><i style="background:var(--border-1)"></i>border-1</div>
</div>
<h3>浮层与第三方品牌</h3>
<div class="sw-row">
<div class="sw"><i style="background:var(--overlay-bg)"></i>overlay-bg</div>
<div class="sw"><i style="background:var(--overlay-text)"></i>overlay-text</div>
<div class="sw"><i style="background:var(--overlay-text-2)"></i>overlay-text-2</div>
<div class="sw"><i style="background:var(--overlay-text-3)"></i>overlay-text-3</div>
<div class="sw"><i style="background:var(--overlay-wave)"></i>overlay-wave</div>
<div class="sw"><i style="background:var(--brand-wechat)"></i>brand-wechat</div>
<div class="sw"><i style="background:var(--brand-wechat-press)"></i>brand-wechat-press</div>
</div>
</section>
<section>
<h2>组件登记</h2>
<p class="note">每个 <code>components/**/*.jsx</code> 组件必须在此有登记卡(check-ds 第④道)。演示卡:<a href="./components/core/core.card.html">core</a> · <a href="./components/forms/forms.card.html">forms</a> · <a href="./components/voice/voice.card.html">voice</a></p>
<h3>core</h3>
<div class="grid">
<div class="entry"><b>Button</b><div class="sub">主/次/幽灵/危险按钮,三种尺寸</div><div class="meta">variant · size · block — components/core/Button.jsx</div></div>
<div class="entry"><b>IconButton</b><div class="sub">方形图标按钮</div><div class="meta">size · label(无障碍必填)— components/core/IconButton.jsx</div></div>
<div class="entry"><b>Badge</b><div class="sub">状态徽标</div><div class="meta">tone: green/blue/orange/red/neutral — components/core/Badge.jsx</div></div>
<div class="entry"><b>Kbd</b> / <b>HotkeyCombo</b><div class="sub">键帽与快捷键组合(⌘ ⇧ Space</div><div class="meta">keys=['⌘','⇧','Space'] — components/core/Kbd.jsx</div></div>
<div class="entry"><b>Card</b><div class="sub">标准卡片容器</div><div class="meta">padding — components/core/Card.jsx</div></div>
<div class="entry"><b>ProgressBar</b><div class="sub">用量进度条,超阈自动警告色</div><div class="meta">value/max · warnAt=0.9 — components/core/ProgressBar.jsx</div></div>
<div class="entry"><b>Icon</b><div class="sub">icons.js 单源图标渲染器</div><div class="meta">name · size · sw — components/core/Icon.jsx</div></div>
</div>
<h3>forms</h3>
<div class="grid">
<div class="entry"><b>Input</b><div class="sub">文本输入框</div><div class="meta">size: md/lg — components/forms/Input.jsx</div></div>
<div class="entry"><b>Switch</b><div class="sub">开关</div><div class="meta">checked · onChange — components/forms/Switch.jsx</div></div>
<div class="entry"><b>SettingRow</b><div class="sub">设置行(标签 + 描述 + 右侧控件)</div><div class="meta">label · description — components/forms/SettingRow.jsx</div></div>
</div>
<h3>voice</h3>
<div class="grid">
<div class="entry"><b>RecognitionOverlay</b><div class="sub">桌面识别浮层 · 聆听/流式两态</div><div class="meta">state · text — components/voice/RecognitionOverlay.jsx</div></div>
<div class="entry"><b>MicBar</b><div class="sub">移动端按住说话 · 四种状态</div><div class="meta">state — components/voice/MicBar.jsx</div></div>
<div class="entry"><b>Waveform</b><div class="sub">波形条(唯一循环动画,仅录音中)</div><div class="meta">active · bars · height · color — components/voice/Waveform.jsx</div></div>
</div>
</section>
<section>
<h2>图标登记(icons.js + icon-map.json</h2>
<p class="note">Lucide 线条 · 24 网格 · 2px 圆头 · currentColor。下方由 <code>icons.js</code> 实时渲染(需经 serve.mjs 访问;file:// 打开时仅显示映射表)。</p>
<div id="icon-grid"></div>
<div id="icon-map-table"></div>
</section>
<section>
<h2>Guidelines 规范卡</h2>
<div class="grid">
<div class="entry"><b><a href="./guidelines/brand-logo.card.html">Logo</a></b><div class="sub">圆形嘴巴 + 声波线条 · 线性风格</div></div>
<div class="entry"><b><a href="./guidelines/brand-wordmark.card.html">Wordmark</a></b><div class="sub">dudu 几何线条字标</div></div>
<div class="entry"><b><a href="./guidelines/colors-brand.card.html">品牌蓝阶梯</a></b><div class="sub">blue-50 → 900</div></div>
<div class="entry"><b><a href="./guidelines/colors-neutrals.card.html">中性阶梯</a></b><div class="sub">gray-0 → 950 冷灰</div></div>
<div class="entry"><b><a href="./guidelines/colors-surfaces.card.html">表面与文字</a></b><div class="sub">bg → surface → textLight/Dark</div></div>
<div class="entry"><b><a href="./guidelines/colors-semantic.card.html">功能色</a></b><div class="sub">绿=录音/成功 · 琥珀=警告 · 红=错误</div></div>
<div class="entry"><b><a href="./guidelines/colors-overlay.card.html">识别浮层色</a></b><div class="sub">双模式深色玻璃</div></div>
<div class="entry"><b><a href="./guidelines/type-families.card.html">字体家族</a></b><div class="sub">Outfit(自托管)+ 系统中文栈</div></div>
<div class="entry"><b><a href="./guidelines/type-scale.card.html">字号阶梯</a></b><div class="sub">12 → 44</div></div>
<div class="entry"><b><a href="./guidelines/type-recognition.card.html">识别文本规范</a></b><div class="sub">final 实色 · partial 弱色</div></div>
<div class="entry"><b><a href="./guidelines/spacing-grid.card.html">间距网格</a></b><div class="sub">4px 基准</div></div>
<div class="entry"><b><a href="./guidelines/spacing-radius.card.html">圆角阶梯</a></b><div class="sub">xs 4 → full 999</div></div>
<div class="entry"><b><a href="./guidelines/spacing-controls.card.html">控件高度</a></b><div class="sub">桌面 28/36 · 移动 44/56</div></div>
<div class="entry"><b><a href="./guidelines/spacing-elevation.card.html">层级与阴影</a></b><div class="sub">card → menu → window → overlay</div></div>
<div class="entry"><b><a href="./guidelines/motion.card.html">动效</a></b><div class="sub">快入慢出 · 120/200/320ms · 无弹跳</div></div>
</div>
</section>
<section>
<h2>UI Kits(屏级原型)</h2>
<div class="grid">
<div class="entry"><b><a href="./ui_kits/desktop/index.html">桌面端 UI Kit</a></b><div class="sub">识别浮层 · 托盘菜单 · 设置 · 登录购买(可交互,Light/Dark)</div></div>
<div class="entry"><b><a href="./ui_kits/mobile/index.html">移动端 UI Kit</a></b><div class="sub">语音键盘(按住说话)+ 主 App 四屏(可交互,Light/Dark</div></div>
</div>
<p class="note">已知滞后:ui_kits 预览页仍经 unpkg CDN 加载 React/Babel,离线/无代理网络下不可用(治理项见 doc/frontend-overview.html)。</p>
</section>
<script type="module">
// 图标区:从单源渲染(serve.mjs 下可用;file:// 因模块 CORS 限制自动降级为仅映射表)
try {
const { icons } = await import('./icons.js');
const map = await (await fetch('./icon-map.json')).json();
const grid = document.getElementById('icon-grid');
grid.innerHTML = Object.entries(icons).map(([k, body]) =>
`<div class="ic"><div class="box"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</svg></div>${k}</div>`
).join('');
const rows = Object.entries(map).filter(([k]) => k !== '$schema').map(([k, v]) =>
`<tr><td><code>${k}</code></td><td>${v.web ?? '—'}</td><td>${v.ios ?? '—'}</td><td>${v.android ?? '—'}</td></tr>`
).join('');
document.getElementById('icon-map-table').innerHTML =
`<table><tr><th>语义名</th><th>webicons.js</th><th>iOSSF Symbol</th><th>Android</th></tr>${rows}</table>`;
} catch {
document.getElementById('icon-map-table').innerHTML =
'<p class="note">file:// 模式无法加载 icons.js / icon-map.json,请 <code>node design/serve.mjs</code> 后访问。</p>';
}
</script>
</div>
</body>
</html>