dudu MVP:五端语音输入法初始提交
ci / server (push) Failing after 14s
ci / design-tokens (push) Failing after 11s

- server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调
- desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导)
- android:Compose 主 App + IME(键盘内录音直传)
- ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写)
- design/design-pipeline:设计系统 + token 导出 iOS/Android 主题
- doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-12 00:38:37 +08:00
commit 40760aa884
252 changed files with 40789 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
/** 胶囊标签 — 状态/套餐/风险等级的小标识。 */
export interface BadgeProps {
/** 语义色调 @default 'neutral' */
tone?: 'neutral' | 'blue' | 'green' | 'orange' | 'red';
children?: any;
}
export declare function Badge(props: BadgeProps): any;
+23
View File
@@ -0,0 +1,23 @@
const ddBadgeCss = `
.dd-badge {
display: inline-flex; align-items: center; gap: 4px;
font-family: var(--font-sans); font-size: var(--text-xs); font-weight: var(--weight-semibold);
padding: 2px 10px; border-radius: var(--radius-full); line-height: var(--leading-normal);
}
.dd-badge--neutral { background: var(--surface-2); color: var(--text-2); }
.dd-badge--blue { background: var(--accent-soft); color: var(--accent-text); }
.dd-badge--green { background: var(--positive-soft); color: var(--positive); }
.dd-badge--orange { background: var(--warning-soft); color: var(--warning); }
.dd-badge--red { background: var(--danger-soft); color: var(--danger); }
`;
if (typeof document !== 'undefined' && !document.getElementById('dd-badge-css')) {
const s = document.createElement('style');
s.id = 'dd-badge-css';
s.textContent = ddBadgeCss;
document.head.appendChild(s);
}
export function Badge({ tone = 'neutral', children, ...rest }) {
return <span className={`dd-badge dd-badge--${tone}`} {...rest}>{children}</span>;
}
+7
View File
@@ -0,0 +1,7 @@
Capsule status label. green=余额充足/录音中, orange=时长将用完, red=错误/高风险, blue=信息, neutral=默认。
```jsx
const { Badge } = window.DuduDesignSystem_2e0172;
<Badge tone="green">余额充足</Badge>
<Badge tone="orange">时长将用完</Badge>
```
+16
View File
@@ -0,0 +1,16 @@
/**
* 按钮 — dudu 的主操作控件。
* @startingPoint section="Components" subtitle="主/次/幽灵/危险按钮,三种尺寸" viewport="700x260"
*/
export interface ButtonProps {
/** 视觉变体 @default 'primary' */
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
/** 尺寸:sm=28px 桌面紧凑 / md=36px 默认 / lg=44px 移动端 @default 'md' */
size?: 'sm' | 'md' | 'lg';
/** 占满整行(移动端主操作常用) @default false */
block?: boolean;
disabled?: boolean;
onClick?: (e: any) => void;
children?: any;
}
export declare function Button(props: ButtonProps): any;
+40
View File
@@ -0,0 +1,40 @@
const ddButtonCss = `
.dd-btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
font-family: var(--font-sans); font-weight: var(--weight-medium);
border-radius: var(--radius-sm); border: 1px solid transparent;
cursor: pointer; white-space: nowrap; user-select: none;
transition: background var(--dur-fast) var(--ease-out),
border-color var(--dur-fast) var(--ease-out),
color var(--dur-fast) var(--ease-out);
}
.dd-btn:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.dd-btn--sm { height: var(--control-sm); padding: 0 12px; font-size: var(--text-sm); }
.dd-btn--md { height: var(--control-md); padding: 0 16px; font-size: var(--text-base); }
.dd-btn--lg { height: var(--control-lg); padding: 0 20px; font-size: var(--text-md); }
.dd-btn--block { width: 100%; }
.dd-btn--primary { background: var(--accent); color: var(--text-on-accent); }
.dd-btn--primary:hover { background: var(--accent-hover); }
.dd-btn--primary:active { background: var(--accent-press); }
.dd-btn--secondary { background: var(--surface-card); border-color: var(--border-2); color: var(--text-1); }
.dd-btn--secondary:hover { background: var(--surface-2); }
.dd-btn--secondary:active { background: var(--surface-3); }
.dd-btn--ghost { background: transparent; color: var(--accent-text); }
.dd-btn--ghost:hover { background: var(--accent-soft); }
.dd-btn--ghost:active { background: var(--accent-soft-2); }
.dd-btn--danger { background: var(--danger); color: #fff; }
.dd-btn--danger:hover { filter: brightness(0.94); }
.dd-btn[disabled] { opacity: 0.45; pointer-events: none; }
`;
if (typeof document !== 'undefined' && !document.getElementById('dd-button-css')) {
const s = document.createElement('style');
s.id = 'dd-button-css';
s.textContent = ddButtonCss;
document.head.appendChild(s);
}
export function Button({ variant = 'primary', size = 'md', block = false, disabled = false, children, ...rest }) {
const cls = `dd-btn dd-btn--${variant} dd-btn--${size}${block ? ' dd-btn--block' : ''}`;
return <button type="button" className={cls} disabled={disabled} {...rest}>{children}</button>;
}
+10
View File
@@ -0,0 +1,10 @@
Primary action control. Use `primary` for the single main action per view, `secondary` for the rest, `ghost` for inline/quiet actions, `danger` for destructive ones.
```jsx
const { Button } = window.DuduDesignSystem_2e0172;
<Button variant="primary" size="md" onClick={...}>购买时长</Button>
<Button variant="secondary" size="sm">充值</Button>
<Button variant="primary" size="lg" block>微信一键登录</Button>
```
Variants: primary / secondary (bordered) / ghost (text+tint hover) / danger. Sizes sm 28px · md 36px · lg 44px (mobile). `block` fills the row. Never put two primary buttons side by side.
+8
View File
@@ -0,0 +1,8 @@
/** 卡片 — 白底(dark: surface-card+ 1px 边框 + 极轻投影的内容容器。 */
export interface CardProps {
/** 内边距 @default 'var(--space-5)' */
padding?: string | number;
style?: any;
children?: any;
}
export declare function Card(props: CardProps): any;
+17
View File
@@ -0,0 +1,17 @@
const ddCardCss = `
.dd-card {
background: var(--surface-card); border: 1px solid var(--border-1);
border-radius: var(--radius-md); box-shadow: var(--shadow-card);
}
`;
if (typeof document !== 'undefined' && !document.getElementById('dd-card-css')) {
const s = document.createElement('style');
s.id = 'dd-card-css';
s.textContent = ddCardCss;
document.head.appendChild(s);
}
export function Card({ padding = 'var(--space-5)', style, children, ...rest }) {
return <div className="dd-card" style={{ padding, ...style }} {...rest}>{children}</div>;
}
+8
View File
@@ -0,0 +1,8 @@
Surface container: border + faint shadow, 10px radius. The default grouping element on both desktop and mobile.
```jsx
const { Card } = window.DuduDesignSystem_2e0172;
<Card padding="20px"></Card>
```
Don't nest cards; use `--surface-2` wells inside a card instead.
+12
View File
@@ -0,0 +1,12 @@
/** 图标按钮 — 无边框方形热区,包裹一个线性 SVG 图标。 */
export interface IconButtonProps {
/** 方形边长 px @default 32 */
size?: number;
/** 无障碍标签(必填语义) */
label?: string;
disabled?: boolean;
onClick?: (e: any) => void;
/** 内联 SVG 图标(Lucide 风格,stroke=currentColor */
children?: any;
}
export declare function IconButton(props: IconButtonProps): any;
+28
View File
@@ -0,0 +1,28 @@
const ddIconBtnCss = `
.dd-iconbtn {
display: inline-flex; align-items: center; justify-content: center;
border: none; background: transparent; color: var(--text-2);
border-radius: var(--radius-sm); cursor: pointer;
transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.dd-iconbtn:hover { background: var(--surface-2); color: var(--text-1); }
.dd-iconbtn:active { background: var(--surface-3); }
.dd-iconbtn:focus-visible { outline: none; box-shadow: var(--focus-ring); }
.dd-iconbtn[disabled] { opacity: 0.45; pointer-events: none; }
`;
if (typeof document !== 'undefined' && !document.getElementById('dd-iconbtn-css')) {
const s = document.createElement('style');
s.id = 'dd-iconbtn-css';
s.textContent = ddIconBtnCss;
document.head.appendChild(s);
}
export function IconButton({ size = 32, label, disabled = false, children, ...rest }) {
return (
<button type="button" className="dd-iconbtn" aria-label={label} title={label}
style={{ width: size, height: size }} disabled={disabled} {...rest}>
{children}
</button>
);
}
@@ -0,0 +1,10 @@
Square borderless hit-area for a single line icon (settings gear, close, theme toggle).
```jsx
const { IconButton } = window.DuduDesignSystem_2e0172;
<IconButton label="设置" size={32} onClick={...}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"></svg>
</IconButton>
```
Icon inherits `currentColor` (text-2, darkens on hover). Keep icons 1620px inside a 2836px button.
+7
View File
@@ -0,0 +1,7 @@
/** 键帽 — 展示快捷键的单个按键。HotkeyCombo 排列一组键帽。 */
export interface KbdProps {
/** 键名:⌘ ⇧ ⌥ ⌃ Space Enter 等 */
children?: any;
}
export declare function Kbd(props: KbdProps): any;
export declare function HotkeyCombo(props: { keys: string[]; gap?: number }): any;
+28
View File
@@ -0,0 +1,28 @@
const ddKbdCss = `
.dd-kbd {
display: inline-block; background: var(--surface-2);
border: 1px solid var(--border-2); border-bottom-width: 2px;
border-radius: var(--radius-xs); padding: 1px 7px;
font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-1);
line-height: 1.5;
}
`;
if (typeof document !== 'undefined' && !document.getElementById('dd-kbd-css')) {
const s = document.createElement('style');
s.id = 'dd-kbd-css';
s.textContent = ddKbdCss;
document.head.appendChild(s);
}
export function Kbd({ children, ...rest }) {
return <kbd className="dd-kbd" {...rest}>{children}</kbd>;
}
export function HotkeyCombo({ keys = [], gap = 4 }) {
return (
<span style={{ display: 'inline-flex', gap, alignItems: 'center' }}>
{keys.map((k, i) => <Kbd key={i}>{k}</Kbd>)}
</span>
);
}
+9
View File
@@ -0,0 +1,9 @@
Keycap chip for shortcuts — core to dudu's push-to-talk identity.
```jsx
const { Kbd, HotkeyCombo } = window.DuduDesignSystem_2e0172;
<HotkeyCombo keys={['⌘', '⇧', 'Space']} /> // 按住说话快捷键
<Kbd>Enter</Kbd>
```
Use unicode modifier symbols (⌘ ⇧ ⌥ ⌃), not words.
+11
View File
@@ -0,0 +1,11 @@
/** 用量进度条 — 本月识别分钟数等配额展示;接近上限自动转橙色。 */
export interface ProgressBarProps {
value?: number;
max?: number;
/** 条高 px @default 6 */
height?: number;
/** 超过该比例转为警告色 @default 0.9 */
warnAt?: number;
style?: any;
}
export declare function ProgressBar(props: ProgressBarProps): any;
+24
View File
@@ -0,0 +1,24 @@
const ddProgressCss = `
.dd-progress { background: var(--surface-3); border-radius: var(--radius-full); overflow: hidden; }
.dd-progress i { display: block; height: 100%; border-radius: var(--radius-full);
background: var(--accent); transition: width var(--dur-slow) var(--ease-out); }
.dd-progress--warning i { background: var(--warning); }
`;
if (typeof document !== 'undefined' && !document.getElementById('dd-progress-css')) {
const s = document.createElement('style');
s.id = 'dd-progress-css';
s.textContent = ddProgressCss;
document.head.appendChild(s);
}
export function ProgressBar({ value = 0, max = 100, height = 6, warnAt = 0.9, style, ...rest }) {
const ratio = Math.min(1, max > 0 ? value / max : 0);
const warning = ratio >= warnAt;
return (
<div className={`dd-progress${warning ? ' dd-progress--warning' : ''}`}
style={{ height, ...style }} role="progressbar" aria-valuenow={value} aria-valuemax={max} {...rest}>
<i style={{ width: `${ratio * 100}%` }}></i>
</div>
);
}
@@ -0,0 +1,8 @@
Quota/usage meter (e.g. 128 / 600 分钟). Turns warning-orange past `warnAt` (default 90%).
```jsx
const { ProgressBar } = window.DuduDesignSystem_2e0172;
<ProgressBar value={128} max={600} />
```
Pair with a `13px text-2` label row above: 「本月已用 128 / 600 分钟」.
+67
View File
@@ -0,0 +1,67 @@
<!-- @dsCard group="Components" viewport="700x360" name="Core 核心控件" subtitle="Button · IconButton · Badge · Kbd · ProgressBar · Card" -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../../styles.css">
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script src="../../_ds_bundle.js"></script>
<style>
body { margin: 0; font-family: var(--font-sans); display: grid; grid-template-columns: 1fr 1fr; }
.pane { padding: 20px 22px; background: var(--bg-app); color: var(--text-1); min-height: 360px; box-sizing: border-box; }
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }
</style>
</head>
<body>
<div class="pane" id="light"></div>
<div class="pane" id="dark" data-theme="dark"></div>
<script type="text/babel">
const { Button, IconButton, Badge, Kbd, HotkeyCombo, Card, ProgressBar } = window.DuduDesignSystem_2e0172;
const GearIcon = () => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
);
function Demo() {
return (
<div>
<div className="row">
<Button variant="primary">购买时长</Button>
<Button variant="secondary">充值</Button>
<Button variant="ghost">查看用量</Button>
<Button variant="danger">退出登录</Button>
</div>
<div className="row">
<Button variant="primary" size="sm">小按钮</Button>
<Button variant="primary" size="lg">移动端 44px</Button>
<Button variant="primary" disabled>禁用</Button>
<IconButton label="设置"><GearIcon /></IconButton>
</div>
<div className="row">
<Badge tone="green">余额充足</Badge>
<Badge tone="blue">试用中</Badge>
<Badge tone="orange">时长将用完</Badge>
<Badge tone="red">连接失败</Badge>
<Badge tone="neutral">免费版</Badge>
</div>
<div className="row">
<HotkeyCombo keys={['⌘', '⇧', 'Space']} />
<span style={{ fontSize: 'var(--text-sm)', color: 'var(--text-2)' }}>按住说话</span>
</div>
<Card padding="14px 16px">
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 'var(--text-sm)', color: 'var(--text-2)', marginBottom: 8 }}>
<span>时长余额</span><span>472 / 600 分钟</span>
</div>
<ProgressBar value={472} max={600} />
</Card>
</div>
);
}
ReactDOM.createRoot(document.getElementById('light')).render(<Demo />);
ReactDOM.createRoot(document.getElementById('dark')).render(<Demo />);
</script>
</body>
</html>