dudu MVP:五端语音输入法初始提交
- 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:
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/** 胶囊标签 — 状态/套餐/风险等级的小标识。 */
|
||||
export interface BadgeProps {
|
||||
/** 语义色调 @default 'neutral' */
|
||||
tone?: 'neutral' | 'blue' | 'green' | 'orange' | 'red';
|
||||
children?: any;
|
||||
}
|
||||
export declare function Badge(props: BadgeProps): any;
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
```
|
||||
Vendored
+16
@@ -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;
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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.
|
||||
Vendored
+8
@@ -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;
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
@@ -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 16–20px inside a 28–36px button.
|
||||
Vendored
+7
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
@@ -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 分钟」.
|
||||
@@ -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>
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/** 文本输入框 — 桌面 36px / 移动 44px。 */
|
||||
export interface InputProps {
|
||||
/** md=36px 桌面 / lg=44px 移动 @default 'md' */
|
||||
size?: 'md' | 'lg';
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
onChange?: (e: any) => void;
|
||||
style?: any;
|
||||
}
|
||||
export declare function Input(props: InputProps): any;
|
||||
@@ -0,0 +1,26 @@
|
||||
const ddInputCss = `
|
||||
.dd-input {
|
||||
display: block; width: 100%; box-sizing: border-box;
|
||||
height: var(--control-md); padding: 0 12px;
|
||||
font-family: var(--font-sans); font-size: var(--text-base); color: var(--text-1);
|
||||
background: var(--surface-card); border: 1px solid var(--border-2);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out);
|
||||
}
|
||||
.dd-input::placeholder { color: var(--text-3); }
|
||||
.dd-input:hover { border-color: var(--gray-400); }
|
||||
.dd-input:focus { outline: none; border-color: var(--accent); box-shadow: var(--focus-ring); }
|
||||
.dd-input[disabled] { opacity: 0.45; pointer-events: none; }
|
||||
.dd-input--lg { height: var(--control-lg); font-size: var(--text-md); border-radius: var(--radius-sm); }
|
||||
`;
|
||||
|
||||
if (typeof document !== 'undefined' && !document.getElementById('dd-input-css')) {
|
||||
const s = document.createElement('style');
|
||||
s.id = 'dd-input-css';
|
||||
s.textContent = ddInputCss;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function Input({ size = 'md', style, ...rest }) {
|
||||
return <input className={`dd-input${size === 'lg' ? ' dd-input--lg' : ''}`} style={style} {...rest} />;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Standard text field. Focus = accent border + soft ring.
|
||||
|
||||
```jsx
|
||||
const { Input } = window.DuduDesignSystem_2e0172;
|
||||
<Input placeholder="说点什么…" />
|
||||
<Input size="lg" placeholder="移动端 44px" />
|
||||
```
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/** 设置行 — 左标签(可带说明)+ 右控件,1px 分隔线;设置窗口/移动端「我的」的基本骨架。 */
|
||||
export interface SettingRowProps {
|
||||
label: string;
|
||||
/** 12px 灰色辅助说明 */
|
||||
description?: string;
|
||||
/** 右侧控件:Switch / HotkeyCombo / Button / 文本 */
|
||||
children?: any;
|
||||
}
|
||||
export declare function SettingRow(props: SettingRowProps): any;
|
||||
@@ -0,0 +1,30 @@
|
||||
const ddSettingRowCss = `
|
||||
.dd-setrow {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 16px;
|
||||
padding: 12px 0; border-bottom: 1px solid var(--border-1);
|
||||
font-family: var(--font-sans); font-size: var(--text-base);
|
||||
}
|
||||
.dd-setrow:last-child { border-bottom: none; }
|
||||
.dd-setrow__label { color: var(--text-1); }
|
||||
.dd-setrow__desc { font-size: var(--text-xs); color: var(--text-3); margin-top: 2px; }
|
||||
.dd-setrow__control { display: flex; align-items: center; gap: 8px; color: var(--text-2); font-size: var(--text-sm); }
|
||||
`;
|
||||
|
||||
if (typeof document !== 'undefined' && !document.getElementById('dd-setrow-css')) {
|
||||
const s = document.createElement('style');
|
||||
s.id = 'dd-setrow-css';
|
||||
s.textContent = ddSettingRowCss;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function SettingRow({ label, description, children, ...rest }) {
|
||||
return (
|
||||
<div className="dd-setrow" {...rest}>
|
||||
<div>
|
||||
<div className="dd-setrow__label">{label}</div>
|
||||
{description ? <div className="dd-setrow__desc">{description}</div> : null}
|
||||
</div>
|
||||
<div className="dd-setrow__control">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
Label-left / control-right list row, the skeleton of every settings surface.
|
||||
|
||||
```jsx
|
||||
const { SettingRow, Switch, HotkeyCombo } = window.DuduDesignSystem_2e0172;
|
||||
<SettingRow label="说话快捷键" description="按住说话,松开完成">
|
||||
<HotkeyCombo keys={['⌘','⇧','Space']} />
|
||||
</SettingRow>
|
||||
<SettingRow label="开机自启"><Switch checked /></SettingRow>
|
||||
```
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/** 开关 — 设置项的布尔控件(开机自启、启用快捷键…)。 */
|
||||
export interface SwitchProps {
|
||||
checked?: boolean;
|
||||
onChange?: (next: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
export declare function Switch(props: SwitchProps): any;
|
||||
@@ -0,0 +1,33 @@
|
||||
const ddSwitchCss = `
|
||||
.dd-switch {
|
||||
position: relative; display: inline-block; width: 40px; height: 24px;
|
||||
border-radius: var(--radius-full); background: var(--surface-3);
|
||||
border: none; cursor: pointer; padding: 0; flex: none;
|
||||
transition: background var(--dur-base) var(--ease-out);
|
||||
}
|
||||
.dd-switch::after {
|
||||
content: ''; position: absolute; top: 3px; left: 3px;
|
||||
width: 18px; height: 18px; border-radius: 50%;
|
||||
background: #fff; box-shadow: 0 1px 3px rgba(17,19,26,0.25);
|
||||
transition: transform var(--dur-base) var(--ease-out);
|
||||
}
|
||||
.dd-switch[aria-checked="true"] { background: var(--accent); }
|
||||
.dd-switch[aria-checked="true"]::after { transform: translateX(16px); }
|
||||
.dd-switch:focus-visible { outline: none; box-shadow: var(--focus-ring); }
|
||||
.dd-switch[disabled] { opacity: 0.45; pointer-events: none; }
|
||||
`;
|
||||
|
||||
if (typeof document !== 'undefined' && !document.getElementById('dd-switch-css')) {
|
||||
const s = document.createElement('style');
|
||||
s.id = 'dd-switch-css';
|
||||
s.textContent = ddSwitchCss;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function Switch({ checked = false, onChange, disabled = false, ...rest }) {
|
||||
return (
|
||||
<button type="button" role="switch" className="dd-switch"
|
||||
aria-checked={checked ? 'true' : 'false'} disabled={disabled}
|
||||
onClick={() => onChange && onChange(!checked)} {...rest}></button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
Boolean toggle for settings rows. 40×24, accent when on, no bounce.
|
||||
|
||||
```jsx
|
||||
const { Switch } = window.DuduDesignSystem_2e0172;
|
||||
<Switch checked={autoStart} onChange={setAutoStart} />
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
<!-- @dsCard group="Components" viewport="700x300" name="Forms 表单" subtitle="Input · Switch · SettingRow" -->
|
||||
<!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: 300px; box-sizing: border-box; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="pane" id="light"></div>
|
||||
<div class="pane" id="dark" data-theme="dark"></div>
|
||||
<script type="text/babel">
|
||||
const { Input, Switch, SettingRow, HotkeyCombo, Card } = window.DuduDesignSystem_2e0172;
|
||||
function Demo() {
|
||||
const [auto, setAuto] = React.useState(true);
|
||||
const [hotkey, setHotkey] = React.useState(true);
|
||||
return (
|
||||
<div style={{ display: 'grid', gap: 14 }}>
|
||||
<Input placeholder="说点什么…" />
|
||||
<Card padding="4px 16px">
|
||||
<SettingRow label="说话快捷键" description="按住说话,松开完成">
|
||||
<HotkeyCombo keys={['⌘', '⇧', 'Space']} />
|
||||
</SettingRow>
|
||||
<SettingRow label="麦克风">MacBook Pro 麦克风 ▾</SettingRow>
|
||||
<SettingRow label="开机自启"><Switch checked={auto} onChange={setAuto} /></SettingRow>
|
||||
<SettingRow label="启用快捷键"><Switch checked={hotkey} onChange={setHotkey} /></SettingRow>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.createRoot(document.getElementById('light')).render(<Demo />);
|
||||
ReactDOM.createRoot(document.getElementById('dark')).render(<Demo />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 麦克风条 — 移动端键盘的主操作:按住说话的大胶囊按键。
|
||||
* @startingPoint section="Components" subtitle="按住说话 · 四种状态" viewport="700x300"
|
||||
*/
|
||||
export interface MicBarProps {
|
||||
/** idle 按住说话 / recording 录音中 / disabled 未登录 / quota 超额 @default 'idle' */
|
||||
state?: 'idle' | 'recording' | 'disabled' | 'quota';
|
||||
/** 覆盖默认文案 */
|
||||
label?: string;
|
||||
onPointerDown?: (e: any) => void;
|
||||
onPointerUp?: (e: any) => void;
|
||||
}
|
||||
export declare function MicBar(props: MicBarProps): any;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Waveform } from './Waveform';
|
||||
|
||||
const ddMicBarCss = `
|
||||
.dd-micbar {
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
width: 100%; height: var(--control-xl); border: none; cursor: pointer;
|
||||
border-radius: var(--radius-full);
|
||||
font-family: var(--font-sans); font-size: var(--text-md); font-weight: var(--weight-semibold);
|
||||
user-select: none; -webkit-user-select: none; touch-action: none;
|
||||
transition: background var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
|
||||
}
|
||||
.dd-micbar--idle { background: var(--accent); color: var(--text-on-accent); }
|
||||
.dd-micbar--idle:active { background: var(--accent-press); }
|
||||
.dd-micbar--recording { background: var(--positive); color: #fff; }
|
||||
.dd-micbar--disabled { background: var(--surface-3); color: var(--text-3); cursor: default; }
|
||||
.dd-micbar--quota { background: var(--warning); color: #fff; }
|
||||
`;
|
||||
|
||||
if (typeof document !== 'undefined' && !document.getElementById('dd-micbar-css')) {
|
||||
const s = document.createElement('style');
|
||||
s.id = 'dd-micbar-css';
|
||||
s.textContent = ddMicBarCss;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function DdMicIcon({ size = 20 }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"></path>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
|
||||
<line x1="12" x2="12" y1="19" y2="22"></line>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const DD_MICBAR_LABELS = {
|
||||
idle: '按住说话',
|
||||
recording: '松开完成 · 上滑取消',
|
||||
disabled: '打开 dudu App 登录',
|
||||
quota: '今日试用已用完,去购买时长',
|
||||
};
|
||||
|
||||
export function MicBar({ state = 'idle', label, ...rest }) {
|
||||
const text = label || DD_MICBAR_LABELS[state] || DD_MICBAR_LABELS.idle;
|
||||
return (
|
||||
<button type="button" className={`dd-micbar dd-micbar--${state}`} {...rest}>
|
||||
{state === 'recording'
|
||||
? <Waveform active bars={9} height={18} color="rgba(255,255,255,0.95)" />
|
||||
: <DdMicIcon />}
|
||||
<span>{text}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
The push-to-talk capsule that anchors the mobile keyboard. 56px tall, full width.
|
||||
|
||||
```jsx
|
||||
const { MicBar } = window.DuduDesignSystem_2e0172;
|
||||
<MicBar state="idle" onPointerDown={start} onPointerUp={stop} />
|
||||
<MicBar state="recording" /> // green + animated white waveform
|
||||
<MicBar state="disabled" /> // 未登录
|
||||
<MicBar state="quota" /> // 今日试用已用完 → 引导购买时长
|
||||
```
|
||||
|
||||
Default copy is built in; override with `label`.
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 识别浮层 — 桌面端核心界面:按住快捷键时出现在光标旁的深色小窗。
|
||||
* 两种主题下都保持深色。
|
||||
* @startingPoint section="Components" subtitle="桌面识别浮层 · 聆听/流式两态" viewport="700x220"
|
||||
*/
|
||||
export interface RecognitionOverlayProps {
|
||||
/** listening 聆听中(未出字)/ streaming 流式识别中 @default 'listening' */
|
||||
state?: 'listening' | 'streaming';
|
||||
/** 已定稿文本(白色) */
|
||||
finalText?: string;
|
||||
/** 未定稿 partial 文本(灰蓝色,实时刷新) */
|
||||
partialText?: string;
|
||||
/** 右下角提示 @default '松开 ⌘⇧Space 完成输入' */
|
||||
hint?: string;
|
||||
/** @default 340 */
|
||||
width?: number;
|
||||
style?: any;
|
||||
}
|
||||
export declare function RecognitionOverlay(props: RecognitionOverlayProps): any;
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Waveform } from './Waveform';
|
||||
|
||||
const ddOverlayCss = `
|
||||
.dd-overlay {
|
||||
box-sizing: border-box; width: 340px;
|
||||
background: var(--overlay-bg); color: var(--overlay-text);
|
||||
border-radius: var(--radius-lg); padding: 14px 18px;
|
||||
box-shadow: var(--shadow-overlay);
|
||||
font-family: var(--font-sans);
|
||||
backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
.dd-overlay__status {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
color: var(--overlay-text-2); font-size: var(--text-base); margin-top: 8px;
|
||||
}
|
||||
.dd-overlay__text { font-size: var(--text-md); line-height: 1.6; margin-top: 8px; min-height: 24px; }
|
||||
.dd-overlay__text .dd-partial { color: var(--overlay-text-2); }
|
||||
.dd-overlay__hint { font-size: 11px; color: var(--overlay-text-3); margin-top: 8px; text-align: right; }
|
||||
`;
|
||||
|
||||
if (typeof document !== 'undefined' && !document.getElementById('dd-overlay-css')) {
|
||||
const s = document.createElement('style');
|
||||
s.id = 'dd-overlay-css';
|
||||
s.textContent = ddOverlayCss;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
export function RecognitionOverlay({
|
||||
state = 'listening',
|
||||
finalText = '',
|
||||
partialText = '',
|
||||
hint = '松开 ⌘⇧Space 完成输入',
|
||||
width = 340,
|
||||
style,
|
||||
}) {
|
||||
return (
|
||||
<div className="dd-overlay" style={{ width, ...style }} role="status">
|
||||
<Waveform active bars={12} height={24} color="#7C9BFF" />
|
||||
{state === 'listening' ? (
|
||||
<div className="dd-overlay__status">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"></path>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
|
||||
<line x1="12" x2="12" y1="19" y2="22"></line>
|
||||
</svg>
|
||||
<span>聆听中…</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="dd-overlay__text">
|
||||
{finalText}
|
||||
{partialText ? <span className="dd-partial">{partialText}</span> : null}
|
||||
</div>
|
||||
)}
|
||||
<div className="dd-overlay__hint">{hint}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
The desktop push-to-talk overlay: dark glass panel (in BOTH themes), waveform on top, recognized text below, hotkey hint bottom-right.
|
||||
|
||||
```jsx
|
||||
const { RecognitionOverlay } = window.DuduDesignSystem_2e0172;
|
||||
<RecognitionOverlay state="listening" />
|
||||
<RecognitionOverlay state="streaming"
|
||||
finalText="帮我把这份周报整理一下,"
|
||||
partialText="重点突出本周的" />
|
||||
```
|
||||
|
||||
finalText = settled (white); partialText = in-flight (gray-blue). Never restyle it light.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/** 声波 — 品牌母题:竖向圆头短线组成的音量波形;录音中起伏动画。 */
|
||||
export interface WaveformProps {
|
||||
/** 竖线条数 @default 12 */
|
||||
bars?: number;
|
||||
/** 录音中(起伏动画) @default false */
|
||||
active?: boolean;
|
||||
/** 最大波高 px @default 26 */
|
||||
height?: number;
|
||||
/** 波形颜色 @default 'var(--accent)' */
|
||||
color?: string;
|
||||
style?: any;
|
||||
}
|
||||
export declare function Waveform(props: WaveformProps): any;
|
||||
@@ -0,0 +1,35 @@
|
||||
const ddWaveCss = `
|
||||
.dd-wave { display: inline-flex; align-items: center; gap: 3px; }
|
||||
.dd-wave i { display: block; width: 3px; border-radius: 2px; background: currentColor; }
|
||||
.dd-wave--active i { animation: dd-wave-bounce 0.9s var(--ease-in-out) infinite alternate; }
|
||||
@keyframes dd-wave-bounce {
|
||||
from { transform: scaleY(0.35); }
|
||||
to { transform: scaleY(1); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dd-wave--active i { animation: none; }
|
||||
}
|
||||
`;
|
||||
|
||||
if (typeof document !== 'undefined' && !document.getElementById('dd-wave-css')) {
|
||||
const s = document.createElement('style');
|
||||
s.id = 'dd-wave-css';
|
||||
s.textContent = ddWaveCss;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
const DD_WAVE_PATTERN = [0.3, 0.55, 0.85, 0.45, 0.7, 1, 0.4, 0.6, 0.25, 0.8, 0.5, 0.35];
|
||||
|
||||
export function Waveform({ bars = 12, active = false, height = 26, color = 'var(--accent)', style }) {
|
||||
const items = [];
|
||||
for (let i = 0; i < bars; i++) {
|
||||
const h = Math.round(DD_WAVE_PATTERN[i % DD_WAVE_PATTERN.length] * height);
|
||||
items.push(<i key={i} style={{ height: Math.max(4, h), animationDelay: `${(i % 6) * 0.07}s` }}></i>);
|
||||
}
|
||||
return (
|
||||
<span className={`dd-wave${active ? ' dd-wave--active' : ''}`}
|
||||
style={{ height, color, ...style }} aria-hidden="true">
|
||||
{items}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
The brand's voice motif — vertical rounded bars. Animates only while `active` (recording).
|
||||
|
||||
```jsx
|
||||
const { Waveform } = window.DuduDesignSystem_2e0172;
|
||||
<Waveform active height={26} /> // 录音中,品牌蓝
|
||||
<Waveform color="#7C9BFF" active /> // 深色浮层内用亮蓝
|
||||
<Waveform color="rgba(255,255,255,.9)" bars={8} /> // 静态装饰
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
<!-- @dsCard group="Components" viewport="700x400" name="Voice 语音" subtitle="RecognitionOverlay · MicBar · Waveform" -->
|
||||
<!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; box-sizing: border-box; min-height: 400px; display: flex; flex-direction: column; gap: 14px; align-items: center; justify-content: center; }
|
||||
.left { background: var(--gray-100); }
|
||||
.right { background: var(--bg-app); }
|
||||
.caption { font-size: var(--text-xs); color: var(--text-3); align-self: flex-start; margin: -6px 0 -8px 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="pane left" id="overlays"></div>
|
||||
<div class="pane right" id="micbars" data-theme="dark"></div>
|
||||
<script type="text/babel">
|
||||
const { RecognitionOverlay, MicBar, Waveform } = window.DuduDesignSystem_2e0172;
|
||||
ReactDOM.createRoot(document.getElementById('overlays')).render(
|
||||
<React.Fragment>
|
||||
<RecognitionOverlay state="listening" width={300} />
|
||||
<RecognitionOverlay state="streaming" width={300}
|
||||
finalText="帮我把这份周报整理一下," partialText="重点突出本周的" />
|
||||
<Waveform active height={22} />
|
||||
</React.Fragment>
|
||||
);
|
||||
ReactDOM.createRoot(document.getElementById('micbars')).render(
|
||||
<div style={{ display: 'grid', gap: 12, width: '100%', maxWidth: 300 }}>
|
||||
<MicBar state="idle" />
|
||||
<MicBar state="recording" />
|
||||
<MicBar state="disabled" />
|
||||
<MicBar state="quota" />
|
||||
</div>
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user