208d0cda12
真相源整固: - 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>
214 lines
16 KiB
HTML
214 lines
16 KiB
HTML
<!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/<组>/X.jsx</code>(+ .d.ts)并在本页「组件登记」加卡,再到各端实现。</div>
|
||
<div class="rule">新增<b>图标</b>:先登记 <code>icons.js</code>(Lucide 24 网格 2px 圆头)+ <code>icon-map.json</code> 跨端映射,业务代码禁止内联 <path>。</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>iOS(SwiftUI)</td><td><code>DuduTheme.*</code> 常量(codegen 直写)</td><td><code>ios/dudu/Shared/DuduTheme.swift</code></td></tr>
|
||
<tr><td>Android(Compose)</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><link href="tokens.css"></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 → text,Light/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>web(icons.js)</th><th>iOS(SF 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>
|