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>
141 lines
7.0 KiB
React
141 lines
7.0 KiB
React
// dudu 桌面端 · 系统托盘菜单 + 设置窗口
|
|
|
|
function TrayMenu() {
|
|
const { Badge } = window.DuduDesignSystem_2e0172;
|
|
const Item = ({ children, right, accent, onClick }) => {
|
|
const [hover, setHover] = React.useState(false);
|
|
return (
|
|
<div
|
|
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} onClick={onClick}
|
|
style={{
|
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 18,
|
|
padding: '7px 14px', cursor: 'default', fontSize: 'var(--text-sm)',
|
|
background: hover ? 'var(--accent)' : 'transparent',
|
|
color: hover ? 'var(--text-on-accent)' : (accent ? 'var(--text-1)' : 'var(--text-1)'),
|
|
transition: 'background var(--dur-fast) var(--ease-out)',
|
|
}}>
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, whiteSpace: 'nowrap' }}>{children}</span>
|
|
{right && <span style={{ fontSize: 'var(--text-xs)', color: hover ? 'var(--text-on-accent)' : 'var(--text-3)', opacity: hover ? 0.8 : 1, whiteSpace: 'nowrap' }}>{right}</span>}
|
|
</div>
|
|
);
|
|
};
|
|
const Sep = () => <div style={{ height: 1, background: 'var(--border-1)', margin: '4px 0' }}></div>;
|
|
return (
|
|
<div style={{
|
|
width: 248, padding: '5px 0', fontFamily: 'var(--font-sans)',
|
|
background: 'color-mix(in srgb, var(--surface-card) 96%, transparent)',
|
|
backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)',
|
|
border: '1px solid var(--border-1)', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-menu)',
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 14px 6px' }}>
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 'var(--text-sm)', fontWeight: 'var(--weight-medium)', color: 'var(--text-1)', whiteSpace: 'nowrap' }}>
|
|
<DuduLogo size={17} /> dudu 语音输入
|
|
</span>
|
|
<Badge tone="green">已就绪</Badge>
|
|
</div>
|
|
<Sep />
|
|
<Item right={<DuIcons.Check size={13} />}>启用快捷键</Item>
|
|
<Item right="⌘⇧Space">按住说话</Item>
|
|
<Item>设置…</Item>
|
|
<Item right="472 分钟">时长余额</Item>
|
|
<Sep />
|
|
<Item right="已登录">账号 wang***</Item>
|
|
<Item>检查更新</Item>
|
|
<Sep />
|
|
<Item>退出 dudu</Item>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SettingsWindow({ theme, onThemeChange }) {
|
|
const { Switch, SettingRow, HotkeyCombo, Button, Badge, ProgressBar } = window.DuduDesignSystem_2e0172;
|
|
const [autostart, setAutostart] = React.useState(true);
|
|
const [sound, setSound] = React.useState(false);
|
|
const [mic, setMic] = React.useState('MacBook Pro 麦克风');
|
|
|
|
const Section = ({ title, children }) => (
|
|
<div style={{ padding: '4px 18px 8px' }}>
|
|
<div style={{ fontSize: 'var(--text-xs)', color: 'var(--text-3)', fontWeight: 'var(--weight-medium)', margin: '10px 0 2px', letterSpacing: 'var(--tracking-wide)' }}>{title}</div>
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
const themeOptions = [['system', '跟随系统'], ['light', '浅色'], ['dark', '深色']];
|
|
|
|
return (
|
|
<MacWindow title="dudu 设置" width={440}>
|
|
<Section title="输入">
|
|
<SettingRow label="说话快捷键" description="按住说话,松开上屏">
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
|
|
<HotkeyCombo keys={['⌘', '⇧', 'Space']} />
|
|
<Button variant="ghost" size="sm">修改</Button>
|
|
</span>
|
|
</SettingRow>
|
|
<SettingRow label="麦克风">
|
|
<span style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }}>
|
|
<select value={mic} onChange={(e) => setMic(e.target.value)} style={{
|
|
appearance: 'none', WebkitAppearance: 'none', height: 'var(--control-sm)',
|
|
padding: '0 30px 0 10px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--border-1)',
|
|
background: 'var(--surface-card)', color: 'var(--text-1)', fontSize: 'var(--text-sm)', fontFamily: 'var(--font-sans)', cursor: 'pointer',
|
|
}}>
|
|
<option>MacBook Pro 麦克风</option>
|
|
<option>AirPods Pro</option>
|
|
<option>外接 USB 麦克风</option>
|
|
</select>
|
|
<span style={{ position: 'absolute', right: 8, pointerEvents: 'none', color: 'var(--text-3)', display: 'inline-flex' }}><DuIcons.ChevronDown size={13} /></span>
|
|
</span>
|
|
</SettingRow>
|
|
<SettingRow label="提示音" description="开始 / 完成时播放轻提示音">
|
|
<Switch checked={sound} onChange={setSound} />
|
|
</SettingRow>
|
|
</Section>
|
|
|
|
<div style={{ height: 1, background: 'var(--border-1)' }}></div>
|
|
|
|
<Section title="通用">
|
|
<SettingRow label="开机自启">
|
|
<Switch checked={autostart} onChange={setAutostart} />
|
|
</SettingRow>
|
|
<SettingRow label="外观">
|
|
<span style={{ display: 'inline-flex', background: 'var(--surface-2)', borderRadius: 'var(--radius-sm)', padding: 2, gap: 2 }}>
|
|
{themeOptions.map(([val, lab]) => (
|
|
<button key={val} type="button" onClick={() => onThemeChange(val)} style={{
|
|
height: 24, padding: '0 10px', borderRadius: 4, border: 'none', cursor: 'pointer',
|
|
fontSize: 'var(--text-xs)', fontFamily: 'var(--font-sans)',
|
|
background: theme === val ? 'var(--surface-card)' : 'transparent',
|
|
color: theme === val ? 'var(--text-1)' : 'var(--text-2)',
|
|
boxShadow: theme === val ? 'var(--shadow-card)' : 'none',
|
|
transition: 'background var(--dur-fast) var(--ease-out)',
|
|
}}>{lab}</button>
|
|
))}
|
|
</span>
|
|
</SettingRow>
|
|
</Section>
|
|
|
|
<div style={{ height: 1, background: 'var(--border-1)' }}></div>
|
|
|
|
<Section title="账号与时长">
|
|
<SettingRow label="登录状态" description="微信已登录">
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 'var(--text-sm)', color: 'var(--text-1)' }}>
|
|
wang*** <Badge tone="green">余额充足</Badge>
|
|
</span>
|
|
</SettingRow>
|
|
<SettingRow label="时长余额" description="时长不过期 · 每天另有 3 分钟免费试用">
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
|
|
<span style={{ fontSize: 'var(--text-base)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)', fontFamily: 'var(--font-sans)' }}>472 分钟</span>
|
|
<Button variant="secondary" size="sm">购买时长</Button>
|
|
</span>
|
|
</SettingRow>
|
|
<div style={{ padding: '10px 0 12px' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 'var(--text-xs)', color: 'var(--text-2)', marginBottom: 7 }}>
|
|
<span>今日免费试用</span><span style={{ fontFamily: 'var(--font-mono)' }}>1 / 3 分钟</span>
|
|
</div>
|
|
<ProgressBar value={1} max={3} />
|
|
</div>
|
|
</Section>
|
|
</MacWindow>
|
|
);
|
|
}
|
|
|
|
Object.assign(window, { TrayMenu, SettingsWindow });
|