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>
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: dudu-design
|
||||
description: Use this skill to generate well-branded interfaces and assets for dudu (嘟嘟语音输入法), either for production or throwaway prototypes/mocks/etc. Contains essential design guidelines, colors, type, fonts, assets, and UI kit components for prototyping.
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
Read the README.md file within this skill, and explore the other available files.
|
||||
|
||||
Key facts: dudu is a paid voice input method (语音输入法) for Mac/Windows/iOS/Android — push-to-talk dictation with streaming recognition. Minimal aesthetic, light/dark themes via `data-theme="dark"`, single accent blue, Lucide line icons, circle-mouth line logo. Link `styles.css` for all tokens; reusable React components live in `components/` (Button, Switch, MicBar, RecognitionOverlay, …); full-screen interactive recreations in `ui_kits/desktop` and `ui_kits/mobile`.
|
||||
|
||||
If creating visual artifacts (slides, mocks, throwaway prototypes, etc), copy assets out and create static HTML files for the user to view. If working on production code, you can copy assets and read the rules here to become an expert in designing with this brand.
|
||||
|
||||
If the user invokes this skill without any other guidance, ask them what they want to build or design, ask some questions, and act as an expert designer who outputs HTML artifacts _or_ production code, depending on the need.
|
||||
|
||||
Hard rules: 一屏一个蓝色主按钮;无渐变;无 emoji;浮层永远深色玻璃;文案短、用"你"、无句号。
|
||||
@@ -0,0 +1,387 @@
|
||||
{
|
||||
"plugins": [
|
||||
"react",
|
||||
"import"
|
||||
],
|
||||
"rules": {
|
||||
"react/forbid-elements": [
|
||||
"warn",
|
||||
{
|
||||
"forbid": []
|
||||
}
|
||||
],
|
||||
"no-restricted-imports": [
|
||||
"warn",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": [
|
||||
"components/core/**",
|
||||
"components/forms/**",
|
||||
"components/voice/**",
|
||||
"ui_kits/desktop/**",
|
||||
"ui_kits/mobile/**"
|
||||
],
|
||||
"message": "Import design-system components from 'index.js', not component internals."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-restricted-syntax": [
|
||||
"warn",
|
||||
{
|
||||
"selector": "Literal[value=/#[0-9a-fA-F]{3,8}\\b/]",
|
||||
"message": "Raw hex color — use a design-system color token via var()."
|
||||
},
|
||||
{
|
||||
"selector": "Literal[value=/\\b\\d+px\\b/]",
|
||||
"message": "Raw px value — use a design-system spacing token via var()."
|
||||
},
|
||||
{
|
||||
"selector": "Literal[value=/font-family\\s*:\\s*(?!['\\\"]?(?:Outfit))/i]",
|
||||
"message": "Font not provided by the design system. Available: Outfit."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Badge'] > JSXAttribute > JSXIdentifier[name!=/^(?:tone|children|key|ref|className|style|children)$/]",
|
||||
"message": "<Badge> doesn't accept that prop. Declared props: tone, children."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Badge'] > JSXAttribute[name.name='tone'] > Literal[value!=/^(?:neutral|blue|green|orange|red)$/]",
|
||||
"message": "<Badge> tone must be one of 'neutral' | 'blue' | 'green' | 'orange' | 'red'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute > JSXIdentifier[name!=/^(?:variant|size|block|disabled|onClick|children|key|ref|className|style|children)$/]",
|
||||
"message": "<Button> doesn't accept that prop. Declared props: variant, size, block, disabled, onClick, children."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute[name.name='variant'] > Literal[value!=/^(?:primary|secondary|ghost|danger)$/]",
|
||||
"message": "<Button> variant must be one of 'primary' | 'secondary' | 'ghost' | 'danger'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute[name.name='size'] > Literal[value!=/^(?:sm|md|lg)$/]",
|
||||
"message": "<Button> size must be one of 'sm' | 'md' | 'lg'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Card'] > JSXAttribute > JSXIdentifier[name!=/^(?:padding|style|children|key|ref|className|style|children)$/]",
|
||||
"message": "<Card> doesn't accept that prop. Declared props: padding, style, children."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='IconButton'] > JSXAttribute > JSXIdentifier[name!=/^(?:size|label|disabled|onClick|children|key|ref|className|style|children)$/]",
|
||||
"message": "<IconButton> doesn't accept that prop. Declared props: size, label, disabled, onClick, children."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Input'] > JSXAttribute > JSXIdentifier[name!=/^(?:size|placeholder|value|disabled|onChange|style|key|ref|className|style|children)$/]",
|
||||
"message": "<Input> doesn't accept that prop. Declared props: size, placeholder, value, disabled, onChange, style."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Input'] > JSXAttribute[name.name='size'] > Literal[value!=/^(?:md|lg)$/]",
|
||||
"message": "<Input> size must be one of 'md' | 'lg'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Kbd'] > JSXAttribute > JSXIdentifier[name!=/^(?:children|key|ref|className|style|children)$/]",
|
||||
"message": "<Kbd> doesn't accept that prop. Declared props: children."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='MicBar'] > JSXAttribute > JSXIdentifier[name!=/^(?:state|label|onPointerDown|onPointerUp|key|ref|className|style|children)$/]",
|
||||
"message": "<MicBar> doesn't accept that prop. Declared props: state, label, onPointerDown, onPointerUp."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='MicBar'] > JSXAttribute[name.name='state'] > Literal[value!=/^(?:idle|recording|disabled|quota)$/]",
|
||||
"message": "<MicBar> state must be one of 'idle' | 'recording' | 'disabled' | 'quota'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='ProgressBar'] > JSXAttribute > JSXIdentifier[name!=/^(?:value|max|height|warnAt|style|key|ref|className|style|children)$/]",
|
||||
"message": "<ProgressBar> doesn't accept that prop. Declared props: value, max, height, warnAt, style."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='RecognitionOverlay'] > JSXAttribute > JSXIdentifier[name!=/^(?:state|finalText|partialText|hint|width|style|key|ref|className|style|children)$/]",
|
||||
"message": "<RecognitionOverlay> doesn't accept that prop. Declared props: state, finalText, partialText, hint, width, style."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='RecognitionOverlay'] > JSXAttribute[name.name='state'] > Literal[value!=/^(?:listening|streaming)$/]",
|
||||
"message": "<RecognitionOverlay> state must be one of 'listening' | 'streaming'."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='SettingRow'] > JSXAttribute > JSXIdentifier[name!=/^(?:label|description|children|key|ref|className|style|children)$/]",
|
||||
"message": "<SettingRow> doesn't accept that prop. Declared props: label, description, children."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Switch'] > JSXAttribute > JSXIdentifier[name!=/^(?:checked|onChange|disabled|key|ref|className|style|children)$/]",
|
||||
"message": "<Switch> doesn't accept that prop. Declared props: checked, onChange, disabled."
|
||||
},
|
||||
{
|
||||
"selector": "JSXOpeningElement[name.name='Waveform'] > JSXAttribute > JSXIdentifier[name!=/^(?:bars|active|height|color|style|key|ref|className|style|children)$/]",
|
||||
"message": "<Waveform> doesn't accept that prop. Declared props: bars, active, height, color, style."
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/index.js"
|
||||
],
|
||||
"rules": {
|
||||
"no-restricted-imports": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-omelette": {
|
||||
"components": {
|
||||
"Badge": {
|
||||
"replaces": []
|
||||
},
|
||||
"Button": {
|
||||
"replaces": []
|
||||
},
|
||||
"Card": {
|
||||
"replaces": []
|
||||
},
|
||||
"IconButton": {
|
||||
"replaces": []
|
||||
},
|
||||
"Input": {
|
||||
"replaces": []
|
||||
},
|
||||
"Kbd": {
|
||||
"replaces": []
|
||||
},
|
||||
"MicBar": {
|
||||
"replaces": []
|
||||
},
|
||||
"ProgressBar": {
|
||||
"replaces": []
|
||||
},
|
||||
"RecognitionOverlay": {
|
||||
"replaces": []
|
||||
},
|
||||
"SettingRow": {
|
||||
"replaces": []
|
||||
},
|
||||
"Switch": {
|
||||
"replaces": []
|
||||
},
|
||||
"Waveform": {
|
||||
"replaces": []
|
||||
}
|
||||
},
|
||||
"tokens": [
|
||||
"--accent",
|
||||
"--accent-hover",
|
||||
"--accent-press",
|
||||
"--accent-soft",
|
||||
"--accent-soft-2",
|
||||
"--accent-text",
|
||||
"--amber-100",
|
||||
"--amber-500",
|
||||
"--bg-app",
|
||||
"--blue-100",
|
||||
"--blue-200",
|
||||
"--blue-300",
|
||||
"--blue-400",
|
||||
"--blue-50",
|
||||
"--blue-500",
|
||||
"--blue-600",
|
||||
"--blue-700",
|
||||
"--blue-800",
|
||||
"--blue-900",
|
||||
"--border-1",
|
||||
"--border-2",
|
||||
"--control-lg",
|
||||
"--control-md",
|
||||
"--control-sm",
|
||||
"--control-xl",
|
||||
"--danger",
|
||||
"--danger-soft",
|
||||
"--dur-base",
|
||||
"--dur-fast",
|
||||
"--dur-slow",
|
||||
"--ease-in-out",
|
||||
"--ease-out",
|
||||
"--focus-ring",
|
||||
"--font-mono",
|
||||
"--font-sans",
|
||||
"--gray-0",
|
||||
"--gray-100",
|
||||
"--gray-200",
|
||||
"--gray-25",
|
||||
"--gray-300",
|
||||
"--gray-400",
|
||||
"--gray-50",
|
||||
"--gray-500",
|
||||
"--gray-600",
|
||||
"--gray-700",
|
||||
"--gray-800",
|
||||
"--gray-900",
|
||||
"--gray-950",
|
||||
"--green-100",
|
||||
"--green-500",
|
||||
"--leading-loose",
|
||||
"--leading-normal",
|
||||
"--leading-tight",
|
||||
"--overlay-bg",
|
||||
"--overlay-text",
|
||||
"--overlay-text-2",
|
||||
"--overlay-text-3",
|
||||
"--positive",
|
||||
"--positive-soft",
|
||||
"--radius-full",
|
||||
"--radius-lg",
|
||||
"--radius-md",
|
||||
"--radius-sm",
|
||||
"--radius-xl",
|
||||
"--radius-xs",
|
||||
"--red-100",
|
||||
"--red-500",
|
||||
"--shadow-card",
|
||||
"--shadow-key",
|
||||
"--shadow-menu",
|
||||
"--shadow-overlay",
|
||||
"--shadow-window",
|
||||
"--space-1",
|
||||
"--space-10",
|
||||
"--space-12",
|
||||
"--space-16",
|
||||
"--space-2",
|
||||
"--space-3",
|
||||
"--space-4",
|
||||
"--space-5",
|
||||
"--space-6",
|
||||
"--space-8",
|
||||
"--surface-2",
|
||||
"--surface-3",
|
||||
"--surface-card",
|
||||
"--text-1",
|
||||
"--text-2",
|
||||
"--text-2xl",
|
||||
"--text-3",
|
||||
"--text-3xl",
|
||||
"--text-4xl",
|
||||
"--text-base",
|
||||
"--text-lg",
|
||||
"--text-md",
|
||||
"--text-on-accent",
|
||||
"--text-sm",
|
||||
"--text-xl",
|
||||
"--text-xs",
|
||||
"--tracking-normal",
|
||||
"--tracking-wide",
|
||||
"--warning",
|
||||
"--warning-soft",
|
||||
"--weight-bold",
|
||||
"--weight-medium",
|
||||
"--weight-regular",
|
||||
"--weight-semibold"
|
||||
],
|
||||
"tokenKinds": {
|
||||
"--blue-50": "color",
|
||||
"--blue-100": "color",
|
||||
"--blue-200": "color",
|
||||
"--blue-300": "color",
|
||||
"--blue-400": "color",
|
||||
"--blue-500": "color",
|
||||
"--blue-600": "color",
|
||||
"--blue-700": "color",
|
||||
"--blue-800": "color",
|
||||
"--blue-900": "color",
|
||||
"--gray-0": "color",
|
||||
"--gray-25": "color",
|
||||
"--gray-50": "color",
|
||||
"--gray-100": "color",
|
||||
"--gray-200": "color",
|
||||
"--gray-300": "color",
|
||||
"--gray-400": "color",
|
||||
"--gray-500": "color",
|
||||
"--gray-600": "color",
|
||||
"--gray-700": "color",
|
||||
"--gray-800": "color",
|
||||
"--gray-900": "color",
|
||||
"--gray-950": "color",
|
||||
"--green-500": "color",
|
||||
"--green-100": "color",
|
||||
"--amber-500": "color",
|
||||
"--amber-100": "color",
|
||||
"--red-500": "color",
|
||||
"--red-100": "color",
|
||||
"--bg-app": "color",
|
||||
"--surface-card": "color",
|
||||
"--surface-2": "color",
|
||||
"--surface-3": "color",
|
||||
"--border-1": "color",
|
||||
"--border-2": "color",
|
||||
"--text-1": "font",
|
||||
"--text-2": "font",
|
||||
"--text-3": "font",
|
||||
"--text-on-accent": "font",
|
||||
"--accent": "color",
|
||||
"--accent-hover": "color",
|
||||
"--accent-press": "color",
|
||||
"--accent-soft": "color",
|
||||
"--accent-soft-2": "color",
|
||||
"--accent-text": "font",
|
||||
"--positive": "color",
|
||||
"--positive-soft": "color",
|
||||
"--warning": "color",
|
||||
"--warning-soft": "color",
|
||||
"--danger": "color",
|
||||
"--danger-soft": "color",
|
||||
"--overlay-bg": "color",
|
||||
"--overlay-text": "font",
|
||||
"--overlay-text-2": "font",
|
||||
"--overlay-text-3": "font",
|
||||
"--focus-ring": "color",
|
||||
"--font-sans": "font",
|
||||
"--font-mono": "font",
|
||||
"--text-xs": "font",
|
||||
"--text-sm": "font",
|
||||
"--text-base": "font",
|
||||
"--text-md": "font",
|
||||
"--text-lg": "font",
|
||||
"--text-xl": "font",
|
||||
"--text-2xl": "font",
|
||||
"--text-3xl": "font",
|
||||
"--text-4xl": "font",
|
||||
"--weight-regular": "font",
|
||||
"--weight-medium": "font",
|
||||
"--weight-semibold": "font",
|
||||
"--weight-bold": "font",
|
||||
"--leading-tight": "font",
|
||||
"--leading-normal": "font",
|
||||
"--leading-loose": "font",
|
||||
"--tracking-normal": "font",
|
||||
"--tracking-wide": "font",
|
||||
"--space-1": "spacing",
|
||||
"--space-2": "spacing",
|
||||
"--space-3": "spacing",
|
||||
"--space-4": "spacing",
|
||||
"--space-5": "spacing",
|
||||
"--space-6": "spacing",
|
||||
"--space-8": "spacing",
|
||||
"--space-10": "spacing",
|
||||
"--space-12": "spacing",
|
||||
"--space-16": "spacing",
|
||||
"--radius-xs": "radius",
|
||||
"--radius-sm": "radius",
|
||||
"--radius-md": "radius",
|
||||
"--radius-lg": "radius",
|
||||
"--radius-xl": "radius",
|
||||
"--radius-full": "radius",
|
||||
"--control-sm": "spacing",
|
||||
"--control-md": "spacing",
|
||||
"--control-lg": "spacing",
|
||||
"--control-xl": "spacing",
|
||||
"--shadow-card": "shadow",
|
||||
"--shadow-menu": "shadow",
|
||||
"--shadow-window": "shadow",
|
||||
"--shadow-overlay": "shadow",
|
||||
"--shadow-key": "shadow",
|
||||
"--ease-out": "other",
|
||||
"--ease-in-out": "other",
|
||||
"--dur-fast": "other",
|
||||
"--dur-base": "other",
|
||||
"--dur-slow": "other"
|
||||
},
|
||||
"fontFamilies": [
|
||||
"Outfit"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96" fill="none">
|
||||
<rect width="96" height="96" rx="22" fill="#11131A"></rect>
|
||||
<g stroke="#7590F8" stroke-width="7" stroke-linecap="round">
|
||||
<circle cx="48" cy="48" r="33"></circle>
|
||||
<path d="M35 41.5v13"></path>
|
||||
<path d="M48 34v28"></path>
|
||||
<path d="M61 41.5v13"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 379 B |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96" fill="none">
|
||||
<rect width="96" height="96" rx="22" fill="#4F6EF7"></rect>
|
||||
<g stroke="#FFFFFF" stroke-width="7" stroke-linecap="round">
|
||||
<circle cx="48" cy="48" r="33"></circle>
|
||||
<path d="M35 41.5v13"></path>
|
||||
<path d="M48 34v28"></path>
|
||||
<path d="M61 41.5v13"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 379 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" stroke="#4F6EF7" stroke-width="3.5" stroke-linecap="round">
|
||||
<circle cx="24" cy="24" r="18"></circle>
|
||||
<path d="M17 20.5v7"></path>
|
||||
<path d="M24 16.5v15"></path>
|
||||
<path d="M31 20.5v7"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" stroke="#1B1E26" stroke-width="3.5" stroke-linecap="round">
|
||||
<circle cx="24" cy="24" r="18"></circle>
|
||||
<path d="M17 20.5v7"></path>
|
||||
<path d="M24 16.5v15"></path>
|
||||
<path d="M31 20.5v7"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" stroke="#FFFFFF" stroke-width="3.5" stroke-linecap="round">
|
||||
<circle cx="24" cy="24" r="18"></circle>
|
||||
<path d="M17 20.5v7"></path>
|
||||
<path d="M24 16.5v15"></path>
|
||||
<path d="M31 20.5v7"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round">
|
||||
<circle cx="24" cy="24" r="18"></circle>
|
||||
<path d="M17 20.5v7"></path>
|
||||
<path d="M24 16.5v15"></path>
|
||||
<path d="M31 20.5v7"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 280 B |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 152 52" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round">
|
||||
|
||||
<circle cx="18" cy="32" r="14"></circle>
|
||||
<path d="M32 6v40"></path>
|
||||
|
||||
<path d="M42 18v14a14 14 0 0 0 28 0V18"></path>
|
||||
|
||||
<circle cx="94" cy="32" r="14"></circle>
|
||||
<path d="M108 6v40"></path>
|
||||
|
||||
<path d="M118 18v14a14 14 0 0 0 28 0V18"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 400 B |
@@ -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>
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
```
|
||||
@@ -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>
|
||||
```
|
||||
@@ -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>
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!-- @dsCard group="Brand" viewport="700x190" name="Logo" subtitle="圆形嘴巴 + 声波线条 · 线性风格" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; }
|
||||
.cell { height: 190px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; }
|
||||
.cell img { display: block; }
|
||||
.light { background: var(--gray-0); }
|
||||
.tint { background: var(--gray-50); }
|
||||
.dark { background: #11131A; }
|
||||
.blue { background: #4F6EF7; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="cell light"><img src="../assets/logo-mark-ink.svg" width="72" height="72" alt="dudu logo"></div>
|
||||
<div class="cell tint"><img src="../assets/logo-mark-blue.svg" width="72" height="72" alt="dudu logo blue"></div>
|
||||
<div class="cell dark"><img src="../assets/logo-mark-white.svg" width="72" height="72" alt="dudu logo white"></div>
|
||||
<div class="cell blue"><img src="../assets/app-icon.svg" width="76" height="76" alt="dudu app icon" style="border-radius: 17px; box-shadow: 0 4px 14px rgba(0,0,0,.25);"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!-- @dsCard group="Brand" viewport="700x170" name="Wordmark" subtitle="dudu — 几何线条字标,与 logo 同一笔画语言" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.cell { height: 170px; display: flex; align-items: center; justify-content: center; gap: 18px; }
|
||||
.light { background: var(--gray-0); color: #1B1E26; }
|
||||
.dark { background: #11131A; color: #ECEEF4; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="cell light">
|
||||
<svg viewBox="0 0 48 48" width="50" height="50" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"><circle cx="24" cy="24" r="18"></circle><path d="M17 20.5v7"></path><path d="M24 16.5v15"></path><path d="M31 20.5v7"></path></svg>
|
||||
<svg viewBox="0 0 152 52" width="140" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round"><circle cx="18" cy="32" r="14"></circle><path d="M32 6v40"></path><path d="M42 18v14a14 14 0 0 0 28 0V18"></path><circle cx="94" cy="32" r="14"></circle><path d="M108 6v40"></path><path d="M118 18v14a14 14 0 0 0 28 0V18"></path></svg>
|
||||
</div>
|
||||
<div class="cell dark">
|
||||
<svg viewBox="0 0 48 48" width="50" height="50" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"><circle cx="24" cy="24" r="18"></circle><path d="M17 20.5v7"></path><path d="M24 16.5v15"></path><path d="M31 20.5v7"></path></svg>
|
||||
<svg viewBox="0 0 152 52" width="140" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round"><circle cx="18" cy="32" r="14"></circle><path d="M32 6v40"></path><path d="M42 18v14a14 14 0 0 0 28 0V18"></path><circle cx="94" cy="32" r="14"></circle><path d="M108 6v40"></path><path d="M118 18v14a14 14 0 0 0 28 0V18"></path></svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<!-- @dsCard group="Colors" viewport="700x120" name="品牌蓝阶梯" subtitle="--blue-50 → 900 · 主色 500(light)/ 400 系(dark)" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); background: var(--gray-0); }
|
||||
.row { display: grid; grid-template-columns: repeat(10, 1fr); height: 120px; }
|
||||
.sw { display: flex; flex-direction: column; justify-content: flex-end; padding: 8px; box-sizing: border-box; }
|
||||
.sw span { font-size: 10px; font-family: var(--font-mono); opacity: 0.85; }
|
||||
.sw.dk span { color: #fff; }
|
||||
.sw.lt span { color: var(--gray-700); }
|
||||
.sw.main { outline: 2px solid var(--gray-900); outline-offset: -2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="row">
|
||||
<div class="sw lt" style="background:var(--blue-50)"><span>50</span></div>
|
||||
<div class="sw lt" style="background:var(--blue-100)"><span>100</span></div>
|
||||
<div class="sw lt" style="background:var(--blue-200)"><span>200</span></div>
|
||||
<div class="sw lt" style="background:var(--blue-300)"><span>300</span></div>
|
||||
<div class="sw dk" style="background:var(--blue-400)"><span>400</span></div>
|
||||
<div class="sw dk main" style="background:var(--blue-500)"><span>500 ★</span></div>
|
||||
<div class="sw dk" style="background:var(--blue-600)"><span>600</span></div>
|
||||
<div class="sw dk" style="background:var(--blue-700)"><span>700</span></div>
|
||||
<div class="sw dk" style="background:var(--blue-800)"><span>800</span></div>
|
||||
<div class="sw dk" style="background:var(--blue-900)"><span>900</span></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
<!-- @dsCard group="Colors" viewport="700x120" name="中性阶梯" subtitle="--gray-0 → 950 · 冷灰 · 界面的主体" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); background: var(--gray-0); }
|
||||
.row { display: grid; grid-template-columns: repeat(13, 1fr); height: 120px; }
|
||||
.sw { display: flex; flex-direction: column; justify-content: flex-end; padding: 7px 6px; box-sizing: border-box; }
|
||||
.sw span { font-size: 9.5px; font-family: var(--font-mono); }
|
||||
.lt span { color: var(--gray-600); }
|
||||
.dk span { color: rgba(255,255,255,0.75); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="row">
|
||||
<div class="sw lt" style="background:var(--gray-0); box-shadow: inset -1px 0 0 var(--gray-100)"><span>0</span></div>
|
||||
<div class="sw lt" style="background:var(--gray-25)"><span>25</span></div>
|
||||
<div class="sw lt" style="background:var(--gray-50)"><span>50</span></div>
|
||||
<div class="sw lt" style="background:var(--gray-100)"><span>100</span></div>
|
||||
<div class="sw lt" style="background:var(--gray-200)"><span>200</span></div>
|
||||
<div class="sw lt" style="background:var(--gray-300)"><span>300</span></div>
|
||||
<div class="sw dk" style="background:var(--gray-400)"><span>400</span></div>
|
||||
<div class="sw dk" style="background:var(--gray-500)"><span>500</span></div>
|
||||
<div class="sw dk" style="background:var(--gray-600)"><span>600</span></div>
|
||||
<div class="sw dk" style="background:var(--gray-700)"><span>700</span></div>
|
||||
<div class="sw dk" style="background:var(--gray-800)"><span>800</span></div>
|
||||
<div class="sw dk" style="background:var(--gray-900)"><span>900</span></div>
|
||||
<div class="sw dk" style="background:var(--gray-950)"><span>950</span></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!-- @dsCard group="Colors" viewport="700x130" name="识别浮层色" subtitle="两种模式均为深色玻璃 · final 亮 / partial 灰 / hint 暗" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.pane { background: var(--bg-app); display: flex; align-items: center; justify-content: center; height: 130px; }
|
||||
.ov { width: 280px; background: var(--overlay-bg); border-radius: var(--radius-lg); padding: 12px 16px; box-shadow: var(--shadow-overlay); }
|
||||
.ov .txt { color: var(--overlay-text); font-size: var(--text-md); line-height: 1.6; }
|
||||
.ov .txt .partial { color: var(--overlay-text-2); }
|
||||
.ov .hint { color: var(--overlay-text-3); font-size: 11px; text-align: right; margin-top: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="pane">
|
||||
<div class="ov">
|
||||
<div class="txt">帮我把这份周报整理一下,<span class="partial">重点突出本周的</span></div>
|
||||
<div class="hint">松开 ⌘⇧Space 完成输入</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pane" data-theme="dark">
|
||||
<div class="ov">
|
||||
<div class="txt">帮我把这份周报整理一下,<span class="partial">重点突出本周的</span></div>
|
||||
<div class="hint">松开 ⌘⇧Space 完成输入</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
<!-- @dsCard group="Colors" viewport="700x150" name="功能色" subtitle="绿=录音/成功 · 琥珀=超额/警告 · 红=错误 · soft 底 + 实色字" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.pane { padding: 18px 20px; background: var(--bg-app); display: flex; flex-direction: column; gap: 10px; height: 150px; box-sizing: border-box; }
|
||||
.chip-row { display: flex; gap: 8px; align-items: center; }
|
||||
.chip { font-size: 12px; padding: 4px 12px; border-radius: var(--radius-full); font-weight: var(--weight-medium); }
|
||||
.dot { width: 14px; height: 14px; border-radius: 50%; }
|
||||
.lbl { font-size: 11px; color: var(--text-3); font-family: var(--font-mono); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="pane">
|
||||
<div class="chip-row"><span class="dot" style="background:var(--positive)"></span><span class="chip" style="background:var(--positive-soft);color:var(--positive)">录音中 · 余额充足</span><span class="lbl">--positive</span></div>
|
||||
<div class="chip-row"><span class="dot" style="background:var(--warning)"></span><span class="chip" style="background:var(--warning-soft);color:var(--warning)">时长将用完</span><span class="lbl">--warning</span></div>
|
||||
<div class="chip-row"><span class="dot" style="background:var(--danger)"></span><span class="chip" style="background:var(--danger-soft);color:var(--danger)">连接失败</span><span class="lbl">--danger</span></div>
|
||||
</div>
|
||||
<div class="pane" data-theme="dark">
|
||||
<div class="chip-row"><span class="dot" style="background:var(--positive)"></span><span class="chip" style="background:var(--positive-soft);color:var(--positive)">录音中 · 余额充足</span><span class="lbl">--positive</span></div>
|
||||
<div class="chip-row"><span class="dot" style="background:var(--warning)"></span><span class="chip" style="background:var(--warning-soft);color:var(--warning)">时长将用完</span><span class="lbl">--warning</span></div>
|
||||
<div class="chip-row"><span class="dot" style="background:var(--danger)"></span><span class="chip" style="background:var(--danger-soft);color:var(--danger)">连接失败</span><span class="lbl">--danger</span></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
<!-- @dsCard group="Colors" viewport="700x170" name="表面与文字 · Light / Dark" subtitle="bg-app → surface-card → surface-2/3 · text-1/2/3" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.pane { background: var(--bg-app); padding: 16px 18px; height: 170px; box-sizing: border-box; }
|
||||
.card { background: var(--surface-card); border: 1px solid var(--border-1); border-radius: var(--radius-lg); padding: 14px 16px; box-shadow: var(--shadow-card); }
|
||||
.t1 { color: var(--text-1); font-size: var(--text-base); font-weight: var(--weight-medium); }
|
||||
.t2 { color: var(--text-2); font-size: var(--text-sm); margin-top: 2px; }
|
||||
.t3 { color: var(--text-3); font-size: var(--text-xs); margin-top: 2px; }
|
||||
.chips { display: flex; gap: 6px; margin-top: 10px; }
|
||||
.chip { flex: 1; height: 26px; border-radius: var(--radius-sm); border: 1px solid var(--border-1); display: flex; align-items: center; justify-content: center; font-size: 10px; font-family: var(--font-mono); color: var(--text-3); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="pane">
|
||||
<div class="card">
|
||||
<div class="t1">text-1 · 主要文字</div>
|
||||
<div class="t2">text-2 · 次要说明</div>
|
||||
<div class="t3">text-3 · 弱提示</div>
|
||||
<div class="chips">
|
||||
<div class="chip" style="background:var(--bg-app)">bg-app</div>
|
||||
<div class="chip" style="background:var(--surface-2)">surface-2</div>
|
||||
<div class="chip" style="background:var(--surface-3)">surface-3</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pane" data-theme="dark">
|
||||
<div class="card">
|
||||
<div class="t1">text-1 · 主要文字</div>
|
||||
<div class="t2">text-2 · 次要说明</div>
|
||||
<div class="t3">text-3 · 弱提示</div>
|
||||
<div class="chips">
|
||||
<div class="chip" style="background:var(--bg-app)">bg-app</div>
|
||||
<div class="chip" style="background:var(--surface-2)">surface-2</div>
|
||||
<div class="chip" style="background:var(--surface-3)">surface-3</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!-- @dsCard group="Spacing" viewport="700x130" name="动效" subtitle="快入慢出 · 120/200/320ms · 无弹跳(悬停小球查看)" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); background: var(--surface-card); padding: 18px 22px; display: flex; flex-direction: column; gap: 12px; height: 130px; box-sizing: border-box; justify-content: center; }
|
||||
.row { display: flex; align-items: center; gap: 14px; }
|
||||
.tok { width: 150px; font-family: var(--font-mono); font-size: 10.5px; color: var(--text-3); flex: none; }
|
||||
.track { flex: 1; height: 22px; background: var(--surface-2); border-radius: var(--radius-full); position: relative; }
|
||||
.ball { position: absolute; left: 2px; top: 2px; width: 18px; height: 18px; border-radius: 50%; background: var(--accent); }
|
||||
.track:hover .f { transition: left var(--dur-fast) var(--ease-out); left: calc(100% - 20px); }
|
||||
.track:hover .b { transition: left var(--dur-base) var(--ease-out); left: calc(100% - 20px); }
|
||||
.track:hover .s { transition: left var(--dur-slow) var(--ease-out); left: calc(100% - 20px); }
|
||||
.f, .b, .s { transition: left var(--dur-base) var(--ease-out); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="row"><span class="tok">--dur-fast · 120ms</span><div class="track"><div class="ball f"></div></div><span class="tok" style="width:90px">hover · 按压</span></div>
|
||||
<div class="row"><span class="tok">--dur-base · 200ms</span><div class="track"><div class="ball b"></div></div><span class="tok" style="width:90px">浮层 · 开关</span></div>
|
||||
<div class="row"><span class="tok">--dur-slow · 320ms</span><div class="track"><div class="ball s"></div></div><span class="tok" style="width:90px">屏幕切换</span></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
<!-- @dsCard group="Spacing" viewport="700x130" name="控件高度" subtitle="桌面 28/36 · 移动触控 44/56" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); background: var(--surface-card); padding: 18px 22px; display: flex; gap: 14px; align-items: flex-end; height: 130px; box-sizing: border-box; }
|
||||
.ctl { display: flex; align-items: center; justify-content: center; padding: 0 18px; border-radius: var(--radius-sm); background: var(--surface-2); border: 1px solid var(--border-1); color: var(--text-2); font-size: var(--text-sm); }
|
||||
.item { display: flex; flex-direction: column; gap: 6px; align-items: center; }
|
||||
.item span { font-size: 10px; font-family: var(--font-mono); color: var(--text-3); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="item"><div class="ctl" style="height:var(--control-sm)">紧凑 28</div><span>--control-sm</span></div>
|
||||
<div class="item"><div class="ctl" style="height:var(--control-md)">桌面默认 36</div><span>--control-md</span></div>
|
||||
<div class="item"><div class="ctl" style="height:var(--control-lg)">移动最小触控 44</div><span>--control-lg</span></div>
|
||||
<div class="item"><div class="ctl" style="height:var(--control-xl);border-radius:var(--radius-full);background:var(--accent);border:none;color:var(--text-on-accent);font-weight:var(--weight-semibold)">主操作 / 麦克风条 56</div><span>--control-xl</span></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
<!-- @dsCard group="Spacing" viewport="700x160" name="层级与阴影" subtitle="card → menu → window → overlay · dark 模式靠边框提层级" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.pane { background: var(--bg-app); display: flex; gap: 14px; align-items: center; justify-content: center; height: 160px; }
|
||||
.lv { width: 110px; height: 84px; background: var(--surface-card); border: 1px solid var(--border-1); border-radius: var(--radius-lg); display: flex; align-items: flex-end; padding: 8px 10px; box-sizing: border-box; }
|
||||
.lv span { font-size: 10px; font-family: var(--font-mono); color: var(--text-3); }
|
||||
.ov { background: var(--overlay-bg); border: none; }
|
||||
.ov span { color: var(--overlay-text-3); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="pane">
|
||||
<div class="lv" style="box-shadow:var(--shadow-card)"><span>card</span></div>
|
||||
<div class="lv" style="box-shadow:var(--shadow-menu)"><span>menu</span></div>
|
||||
<div class="lv" style="box-shadow:var(--shadow-window)"><span>window</span></div>
|
||||
<div class="lv ov" style="box-shadow:var(--shadow-overlay)"><span>overlay</span></div>
|
||||
</div>
|
||||
<div class="pane" data-theme="dark">
|
||||
<div class="lv" style="box-shadow:var(--shadow-card)"><span>card</span></div>
|
||||
<div class="lv" style="box-shadow:var(--shadow-menu)"><span>menu</span></div>
|
||||
<div class="lv" style="box-shadow:var(--shadow-window)"><span>window</span></div>
|
||||
<div class="lv ov" style="box-shadow:var(--shadow-overlay)"><span>overlay</span></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
<!-- @dsCard group="Spacing" viewport="700x130" name="间距网格" subtitle="4px 基准 · --space-1 → 16" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); background: var(--surface-card); padding: 18px 22px; display: flex; align-items: flex-end; gap: 18px; height: 130px; box-sizing: border-box; }
|
||||
.item { display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
||||
.bar { width: 22px; background: var(--accent-soft-2); border: 1px solid var(--accent); border-radius: 3px; box-sizing: border-box; }
|
||||
.item span { font-size: 10px; font-family: var(--font-mono); color: var(--text-3); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="item"><div class="bar" style="height:4px"></div><span>1·4</span></div>
|
||||
<div class="item"><div class="bar" style="height:8px"></div><span>2·8</span></div>
|
||||
<div class="item"><div class="bar" style="height:12px"></div><span>3·12</span></div>
|
||||
<div class="item"><div class="bar" style="height:16px"></div><span>4·16</span></div>
|
||||
<div class="item"><div class="bar" style="height:20px"></div><span>5·20</span></div>
|
||||
<div class="item"><div class="bar" style="height:24px"></div><span>6·24</span></div>
|
||||
<div class="item"><div class="bar" style="height:32px"></div><span>8·32</span></div>
|
||||
<div class="item"><div class="bar" style="height:40px"></div><span>10·40</span></div>
|
||||
<div class="item"><div class="bar" style="height:48px"></div><span>12·48</span></div>
|
||||
<div class="item"><div class="bar" style="height:64px"></div><span>16·64</span></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!-- @dsCard group="Spacing" viewport="700x140" name="圆角阶梯" subtitle="xs 4 → xl 20 → full · 卡片/浮层 14 · 胶囊 999" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); background: var(--bg-app); padding: 20px 22px; display: flex; gap: 16px; align-items: center; height: 140px; box-sizing: border-box; }
|
||||
.item { display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
||||
.box { width: 76px; height: 60px; background: var(--surface-card); border: 1.5px solid var(--border-2); }
|
||||
.pill { width: 110px; height: 36px; background: var(--accent); }
|
||||
.item span { font-size: 10.5px; font-family: var(--font-mono); color: var(--text-3); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="item"><div class="box" style="border-radius:var(--radius-xs)"></div><span>xs · 4 · kbd</span></div>
|
||||
<div class="item"><div class="box" style="border-radius:var(--radius-sm)"></div><span>sm · 6 · 按钮</span></div>
|
||||
<div class="item"><div class="box" style="border-radius:var(--radius-md)"></div><span>md · 10 · 菜单</span></div>
|
||||
<div class="item"><div class="box" style="border-radius:var(--radius-lg)"></div><span>lg · 14 · 卡片/浮层</span></div>
|
||||
<div class="item"><div class="box" style="border-radius:var(--radius-xl)"></div><span>xl · 20 · 大容器</span></div>
|
||||
<div class="item"><div class="pill" style="border-radius:var(--radius-full)"></div><span>full · 麦克风条</span></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<!-- @dsCard group="Type" viewport="700x170" name="字体家族" subtitle="Outfit(品牌拉丁/数字)· 系统中文栈 · 等宽" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); background: var(--surface-card); color: var(--text-1); padding: 16px 22px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.row { display: flex; align-items: baseline; gap: 16px; }
|
||||
.tok { width: 110px; font-family: var(--font-mono); font-size: 10.5px; color: var(--text-3); flex: none; }
|
||||
.note { font-size: 11px; color: var(--text-3); margin-left: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="row">
|
||||
<span class="tok">--font-sans</span>
|
||||
<span style="font-size:26px;font-weight:var(--weight-semibold);letter-spacing:var(--tracking-wide)">dudu 0123456789</span>
|
||||
<span class="note">Outfit · 字标与数字</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="tok">--font-sans</span>
|
||||
<span style="font-size:17px">嘟嘟语音输入,说话即上屏</span>
|
||||
<span class="note">中文走系统栈 PingFang SC / MiSans</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="tok">--font-mono</span>
|
||||
<span style="font-family:var(--font-mono);font-size:14px">/v1/asr/stream · 00:04</span>
|
||||
<span class="note">代码 · 计时</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
<!-- @dsCard group="Type" viewport="700x120" name="识别文本规范" subtitle="final 实色 · partial 弱色实时刷新 · 永不闪跳" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.pane { padding: 18px 22px; height: 120px; box-sizing: border-box; display: flex; flex-direction: column; gap: 8px; justify-content: center; }
|
||||
.lt { background: var(--surface-card); }
|
||||
.dkv { background: var(--overlay-bg); }
|
||||
.txt { font-size: var(--text-md); line-height: 1.6; }
|
||||
.lt .txt { color: var(--text-1); }
|
||||
.lt .txt .partial { color: var(--text-3); }
|
||||
.dkv .txt { color: var(--overlay-text); }
|
||||
.dkv .txt .partial { color: var(--overlay-text-2); }
|
||||
.lbl { font-size: 11px; }
|
||||
.lt .lbl { color: var(--text-3); }
|
||||
.dkv .lbl { color: var(--overlay-text-3); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="pane lt">
|
||||
<div class="txt">好的,我马上把文件发给你,<span class="partial">大概十分钟之</span></div>
|
||||
<div class="lbl">键盘 partial 文本条:final 用 text-1,partial 用 text-3</div>
|
||||
</div>
|
||||
<div class="pane dkv">
|
||||
<div class="txt">好的,我马上把文件发给你,<span class="partial">大概十分钟之</span></div>
|
||||
<div class="lbl">浮层:final 用 overlay-text,partial 用 overlay-text-2</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!-- @dsCard group="Type" viewport="700x230" name="字号阶梯" subtitle="12 → 44 · 桌面正文 14 / 移动与浮层 15" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../styles.css">
|
||||
<style>
|
||||
body { margin: 0; font-family: var(--font-sans); background: var(--surface-card); color: var(--text-1); padding: 16px 22px; }
|
||||
.row { display: flex; align-items: baseline; gap: 14px; padding: 3px 0; }
|
||||
.tok { width: 110px; font-family: var(--font-mono); font-size: 10.5px; color: var(--text-3); flex: none; }
|
||||
.sp { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="row"><span class="tok">--text-xs 12</span><span class="sp" style="font-size:var(--text-xs)">角标 · 键帽 · 浮层提示</span></div>
|
||||
<div class="row"><span class="tok">--text-sm 13</span><span class="sp" style="font-size:var(--text-sm)">次要信息 · 托盘菜单</span></div>
|
||||
<div class="row"><span class="tok">--text-base 14</span><span class="sp" style="font-size:var(--text-base)">桌面正文 · 设置项</span></div>
|
||||
<div class="row"><span class="tok">--text-md 15</span><span class="sp" style="font-size:var(--text-md)">浮层识别文本 · 移动正文</span></div>
|
||||
<div class="row"><span class="tok">--text-lg 17</span><span class="sp" style="font-size:var(--text-lg);font-weight:var(--weight-medium)">区块标题 · 移动列表</span></div>
|
||||
<div class="row"><span class="tok">--text-xl 20</span><span class="sp" style="font-size:var(--text-xl);font-weight:var(--weight-semibold)">窗口标题</span></div>
|
||||
<div class="row"><span class="tok">--text-2xl 24</span><span class="sp" style="font-size:var(--text-2xl);font-weight:var(--weight-semibold)">页面大标题</span></div>
|
||||
<div class="row"><span class="tok">--text-3xl 32</span><span class="sp" style="font-size:var(--text-3xl);font-weight:var(--weight-bold)">¥ 128 / 年</span></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,101 @@
|
||||
# dudu 语音输入法 · 设计系统
|
||||
|
||||
**dudu(嘟嘟)** 是一款付费语音输入法,核心卖点:大模型识别准确度 + 流式实时上屏 + 极简交互。全平台覆盖(Mac / Windows / iPhone / iPad / Android),桌面端按住全局快捷键说话、移动端键盘按住大麦克风说话,微信登录 + 微信支付按识别时长购买(时长包,不过期),每天免费试用 3 分钟。
|
||||
|
||||
**设计基调:简约、克制、快。** 输入法是"消失在使用中"的工具——界面越少越好,反馈越快越好。支持 Light / Dark 双模式,桌面与移动端共用同一套令牌。
|
||||
|
||||
## 来源
|
||||
|
||||
- 本地代码库 `dudu/`(File System Access 挂载)— 目前仅含 `dudu/doc/plan/design.html`:MVP 前后端设计方案 v0.1(2026-06-11),内含全部界面清单与粗略 mockup。本设计系统的页面设计以该文档第八章为需求来源。
|
||||
- 技术栈(影响设计决策):桌面 Tauri 2 + Rust(浮层不抢焦点)、移动端原生键盘扩展(iOS/Android)、后端 Go。
|
||||
|
||||
## 产品表面
|
||||
|
||||
| 表面 | 界面 |
|
||||
|---|---|
|
||||
| 桌面端(Mac/Win) | ① 识别浮层(核心,两态)② 系统托盘菜单 ③ 设置窗口 ④ 登录/购买窗口 |
|
||||
| 移动端键盘扩展 | ① 语音为主键盘(大麦克风)② partial 文本条 ③ 未登录/超额提示态 |
|
||||
| 移动端主 App | ① 启用引导 ② 微信登录 ③ 购买时长 ④ 我的(余额/试用/权限) |
|
||||
|
||||
## 品牌标识
|
||||
|
||||
"嘟嘟"= 撅起嘴发出的声音。Logo 以**圆形嘴巴**为母题:一个线条圆环(嘟起的嘴)+ 内部三条声波竖线(说话中)。纯线条、3.5px 圆头笔画、单色 `currentColor`。字标 "dudu" 用同一笔画语言手绘(圆 + 竖线 = d/u)。
|
||||
|
||||
- `assets/logo-mark.svg` — 标准线条 mark(currentColor,随前景色)
|
||||
- `assets/logo-mark-ink.svg` / `-blue.svg` / `-white.svg` — 固定色变体
|
||||
- `assets/wordmark.svg` — "dudu" 字标(currentColor)
|
||||
- `assets/app-icon.svg` / `app-icon-dark.svg` — 圆角方形应用图标(22/96 圆角比)
|
||||
- 中文标准写法:"dudu 语音输入"或"嘟嘟语音输入";品牌名永远小写 **dudu**
|
||||
|
||||
## CONTENT FUNDAMENTALS(文案基调)
|
||||
|
||||
- **语言**:简体中文为主;品牌名小写 dudu。技术词(partial/final)不出现在用户界面,界面上只说"实时识别""上屏"。
|
||||
- **人称**:对用户用"你",不用"您"(简约、平等、工具感)。
|
||||
- **句式**:短句、动宾结构、无营销腔。"按住说话""松开完成""上滑取消"——三到六个字说清一个动作。
|
||||
- **标点**:界面微文案不带句号;提示语用"·"分隔("松开完成 · 上滑取消")。
|
||||
- **数字**:半角数字 + 空格:"472 / 600 分钟""00:04"。
|
||||
- **emoji**:界面文案不用 emoji(规划稿 mockup 里的 🎙️ 在正式设计中为线条 mic 图标)。
|
||||
- **示例语料**(演示识别文本用):日常办公对话——"帮我把这份周报整理一下,重点突出本周的进展""好的,我马上把文件发给你"。
|
||||
|
||||
## VISUAL FOUNDATIONS(视觉基础)
|
||||
|
||||
- **色彩**:单一品牌蓝 `--accent`(light #4F6EF7 / dark #6E89F9)+ 冷灰中性阶梯。绿=录音中/成功,琥珀=超额/警告,红=错误。大面积界面是中性色,蓝色只出现在主操作与品牌处——**一屏最多一个蓝色主按钮**。**禁用渐变**(规划稿的蓝紫渐变弃用),纯色填充。
|
||||
- **双模式**:light 页面底 `#F6F7FA` + 白卡片;dark 页面底 `#0F1116` + `#171A22` 卡片。识别浮层在两种模式下都是深色玻璃(`--overlay-bg` 95% 不透明 + blur(12px))——它叠在任意用户界面上,深色最不打扰。
|
||||
- **字体**:中文 UI 走系统字体(PingFang SC / MiSans / HarmonyOS Sans / 雅黑);Outfit 仅用于 "dudu" 字标、数字与价格。正文 14px(桌面)/ 15px(移动与浮层),微文案 12–13px。字重 400/500/600(700 仅大数字)。
|
||||
- **间距**:4px 网格;卡片内边距 16–24px;控件高度 28/36px(桌面)、44/56px(移动触控)。
|
||||
- **圆角**:呼应圆嘴 logo——按钮/输入框 6px、菜单 10px、卡片/浮层 14px、大容器 20px、麦克风条/胶囊 999px。
|
||||
- **边框与阴影**:1px 实线边框(`--border-1`)是主要分隔手段;页面内卡片仅 `--shadow-card`(极轻);浮起元素用 `--shadow-menu/window/overlay` 三档。dark 模式阴影加深、靠边框提层级。无内阴影。
|
||||
- **背景**:纯色,无图片、无纹理、无插画。空状态用 logo 线条图形 + 一句话。
|
||||
- **动效**:快入慢出 `--ease-out`,120/200/320ms 三档;浮层淡入+上移 4px;波形条高度动画是唯一循环动画(仅录音中)。**无弹跳、无旋转、无视差**。
|
||||
- **Hover**:背景加一层(surface-2/3);主按钮加深为 `--accent-hover`。**按压**:再深一档 `--accent-press`,桌面无缩放;移动端键帽按压可缩 0.97。
|
||||
- **焦点**:3px 柔和蓝环 `--focus-ring`,仅键盘聚焦显示。
|
||||
- **透明与模糊**:仅两处——识别浮层与托盘菜单(95–98% 不透明 + blur)。普通卡片不透明。
|
||||
- **图片色调**:产品几乎无摄影图;若营销需要,冷色调、低饱和、留白多。
|
||||
- **卡片范式**:`--surface-card` + 1px `--border-1` + 14px 圆角 + `--shadow-card`;浮起卡片才用更重阴影。
|
||||
|
||||
## ICONOGRAPHY(图标)
|
||||
|
||||
- **图标系统**:[Lucide](https://lucide.dev)(ISC 许可)线条图标——24 网格、2px 圆头描边,与线条 logo 同语言。组件内已内嵌所需 Lucide 路径(mic、settings 等),新界面可从 lucide.dev 复制同风格路径或经 CDN `unpkg.com/lucide-static` 引用。
|
||||
- 描边统一 2px,颜色跟随 `currentColor`。
|
||||
- **不用 emoji 作图标**;不用 unicode 符号作图标。例外:键帽符号 ⌘ ⇧ ⌫ ↵ ——它们是键盘标准字符,出现在 `Kbd` / `HotkeyCombo` 组件中。
|
||||
- 无 png 图标,全部 SVG。
|
||||
|
||||
## 索引
|
||||
|
||||
```
|
||||
styles.css 全局入口(@import 全部令牌)
|
||||
tokens/ colors / typography / spacing / effects / fonts
|
||||
assets/ logo-mark(.ink/.blue/.white) · wordmark · app-icon(±dark)
|
||||
guidelines/ 基础规范卡片(品牌、色板、字体、间距、动效)
|
||||
components/core/ Button · IconButton · Badge · Kbd · HotkeyCombo · Card · ProgressBar
|
||||
components/forms/ Input · Switch · SettingRow
|
||||
components/voice/ Waveform · MicBar · RecognitionOverlay
|
||||
ui_kits/desktop/ 桌面端:识别浮层 / 托盘菜单 / 设置 / 登录购买(交互演示)
|
||||
ui_kits/mobile/ 移动端:语音键盘(多态)+ 主 App(引导 / 购买 / 我的)
|
||||
SKILL.md 供 Claude Code 等代理使用的技能入口
|
||||
```
|
||||
|
||||
### 组件速查
|
||||
|
||||
| 组件 | 关键 props |
|
||||
|---|---|
|
||||
| Button | `variant: primary/secondary/ghost/danger` · `size: sm/md/lg` · `block` |
|
||||
| IconButton | `size` · `label`(无障碍必填)|
|
||||
| Badge | `tone: green/blue/orange/red/neutral` |
|
||||
| Kbd / HotkeyCombo | `keys={['⌘','⇧','Space']}` |
|
||||
| Card | `padding` |
|
||||
| ProgressBar | `value/max` · `warnAt=0.9`(超阈值自动变警告色)|
|
||||
| Input | `size: md/lg` |
|
||||
| Switch | `checked` · `onChange` |
|
||||
| SettingRow | `label` · `description` · children 放右侧控件 |
|
||||
| Waveform | `bars` · `active` · `height` · `color` |
|
||||
| MicBar | `state: idle/recording/disabled/quota`(文案内置)|
|
||||
| RecognitionOverlay | `state: listening/text` · `finalText` · `partialText` · `hint` |
|
||||
|
||||
### 使用规则速查
|
||||
|
||||
1. 先链 `styles.css`;dark 模式给容器加 `data-theme="dark"`。
|
||||
2. 一屏一个蓝色主按钮;其余操作 secondary/ghost。
|
||||
3. 浮层永远深色玻璃;键盘/设置跟随系统模式。
|
||||
4. 文案:短、用"你"、无句号、无 emoji。
|
||||
5. 不画渐变,不加无意义阴影,不引入第二品牌色。
|
||||
@@ -0,0 +1,5 @@
|
||||
@import "./tokens/fonts.css";
|
||||
@import "./tokens/colors.css";
|
||||
@import "./tokens/typography.css";
|
||||
@import "./tokens/spacing.css";
|
||||
@import "./tokens/effects.css";
|
||||
@@ -0,0 +1,113 @@
|
||||
/* ============================================================
|
||||
dudu 语音输入法 · 颜色令牌
|
||||
Light 为默认(:root),Dark 通过 [data-theme="dark"] 切换。
|
||||
简约原则:单一品牌蓝 + 冷灰中性阶梯;绿=录音/成功,
|
||||
琥珀=超额/警告,红=错误。识别浮层两种模式均为深色玻璃。
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
/* ---- 品牌蓝阶梯 ---- */
|
||||
--blue-50: #EEF1FE;
|
||||
--blue-100: #DFE5FD;
|
||||
--blue-200: #C4CFFB;
|
||||
--blue-300: #9FB1F9;
|
||||
--blue-400: #7590F8;
|
||||
--blue-500: #4F6EF7;
|
||||
--blue-600: #3D58DB;
|
||||
--blue-700: #2F44B3;
|
||||
--blue-800: #25368C;
|
||||
--blue-900: #1E2B6B;
|
||||
|
||||
/* ---- 中性阶梯(冷灰)---- */
|
||||
--gray-0: #FFFFFF;
|
||||
--gray-25: #FCFCFD;
|
||||
--gray-50: #F6F7FA;
|
||||
--gray-100: #F0F1F4;
|
||||
--gray-200: #E4E6EB;
|
||||
--gray-300: #D2D5DD;
|
||||
--gray-400: #A6ABB8;
|
||||
--gray-500: #7A8090;
|
||||
--gray-600: #5A6072;
|
||||
--gray-700: #434957;
|
||||
--gray-800: #2B2F3A;
|
||||
--gray-900: #1B1E26;
|
||||
--gray-950: #11131A;
|
||||
|
||||
/* ---- 功能色 ---- */
|
||||
--green-500: #16A34A;
|
||||
--green-100: #DCFCE7;
|
||||
--amber-500: #E8890C;
|
||||
--amber-100: #FCEFD7;
|
||||
--red-500: #DC2626;
|
||||
--red-100: #FEE2E2;
|
||||
|
||||
/* ============ 语义别名 · Light ============ */
|
||||
--bg-app: var(--gray-50);
|
||||
--surface-card: var(--gray-0);
|
||||
--surface-2: var(--gray-100);
|
||||
--surface-3: var(--gray-200);
|
||||
--border-1: var(--gray-200);
|
||||
--border-2: var(--gray-300);
|
||||
|
||||
--text-1: var(--gray-900);
|
||||
--text-2: var(--gray-600);
|
||||
--text-3: var(--gray-400);
|
||||
--text-on-accent: #FFFFFF;
|
||||
|
||||
--accent: var(--blue-500);
|
||||
--accent-hover: var(--blue-600);
|
||||
--accent-press: var(--blue-700);
|
||||
--accent-soft: var(--blue-50);
|
||||
--accent-soft-2: var(--blue-100);
|
||||
--accent-text: var(--blue-600);
|
||||
|
||||
--positive: var(--green-500);
|
||||
--positive-soft: var(--green-100);
|
||||
--warning: var(--amber-500);
|
||||
--warning-soft: var(--amber-100);
|
||||
--danger: var(--red-500);
|
||||
--danger-soft: var(--red-100);
|
||||
|
||||
/* 识别浮层(两种模式均为深色玻璃)*/
|
||||
--overlay-bg: rgba(24, 26, 34, 0.95);
|
||||
--overlay-text: #F2F4FA;
|
||||
--overlay-text-2: #9AA3BD;
|
||||
--overlay-text-3: #646E8C;
|
||||
|
||||
--focus-ring: 0 0 0 3px var(--blue-200);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg-app: #0F1116;
|
||||
--surface-card: #171A22;
|
||||
--surface-2: #1E222C;
|
||||
--surface-3: #262B37;
|
||||
--border-1: #272C38;
|
||||
--border-2: #343A49;
|
||||
|
||||
--text-1: #ECEEF4;
|
||||
--text-2: #9AA1B2;
|
||||
--text-3: #5F6678;
|
||||
--text-on-accent: #FFFFFF;
|
||||
|
||||
--accent: #5C77E8;
|
||||
--accent-hover: #6E89F9;
|
||||
--accent-press: #4A63D6;
|
||||
--accent-soft: rgba(92, 119, 232, 0.16);
|
||||
--accent-soft-2: rgba(92, 119, 232, 0.26);
|
||||
--accent-text: #7D95F7;
|
||||
|
||||
--positive: #34C46A;
|
||||
--positive-soft: rgba(52, 196, 106, 0.16);
|
||||
--warning: #F2A33C;
|
||||
--warning-soft: rgba(242, 163, 60, 0.16);
|
||||
--danger: #F05B5B;
|
||||
--danger-soft: rgba(240, 91, 91, 0.16);
|
||||
|
||||
--overlay-bg: rgba(30, 33, 43, 0.96);
|
||||
--overlay-text: #F2F4FA;
|
||||
--overlay-text-2: #9AA3BD;
|
||||
--overlay-text-3: #646E8C;
|
||||
|
||||
--focus-ring: 0 0 0 3px rgba(92, 119, 232, 0.4);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/* ============================================================
|
||||
dudu · elevation / motion tokens
|
||||
阴影极轻——简约风靠边框和层级背景色区分层次;
|
||||
只有浮动元素(浮层、菜单、弹窗)有明显投影。
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
/* elevation */
|
||||
--shadow-card: 0 1px 3px rgba(17, 19, 26, 0.05);
|
||||
--shadow-menu: 0 4px 16px rgba(17, 19, 26, 0.10), 0 1px 3px rgba(17, 19, 26, 0.06);
|
||||
--shadow-window: 0 8px 28px rgba(17, 19, 26, 0.14);
|
||||
--shadow-overlay: 0 12px 36px rgba(10, 12, 18, 0.38);
|
||||
--shadow-key: 0 1px 0 rgba(17, 19, 26, 0.22); /* 键帽底边 */
|
||||
|
||||
/* motion — 快入慢出,无弹跳 */
|
||||
--ease-out: cubic-bezier(0.2, 0.8, 0.2, 1); /* @kind other */
|
||||
--ease-in-out: cubic-bezier(0.45, 0, 0.2, 1); /* @kind other */
|
||||
--dur-fast: 120ms; /* @kind other */ /* hover、按压 */
|
||||
--dur-base: 200ms; /* @kind other */ /* 浮层进出、开关 */
|
||||
--dur-slow: 320ms; /* @kind other */ /* 页面/屏幕切换 */
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
--shadow-menu: 0 4px 16px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
--shadow-window: 0 8px 28px rgba(0, 0, 0, 0.5);
|
||||
--shadow-overlay: 0 12px 36px rgba(0, 0, 0, 0.6);
|
||||
--shadow-key: 0 1px 0 rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/* dudu · 品牌字体
|
||||
Outfit(Google Fonts CDN)— 圆润几何无衬线,用于品牌拉丁字
|
||||
"dudu"、数字与价格;中文 UI 文本走系统字体栈(PingFang SC /
|
||||
MiSans / HarmonyOS Sans / 雅黑)。
|
||||
注:暂以 CDN 引入;如有自托管字体文件请替换为 @font-face。 */
|
||||
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap");
|
||||
@@ -0,0 +1,33 @@
|
||||
/* ============================================================
|
||||
dudu · 间距 / 圆角 / 控件尺寸令牌
|
||||
4px 基准网格;圆角偏圆润(呼应圆嘴 logo);
|
||||
控件高度:桌面紧凑 28/36,移动触控 44/56。
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
/* 间距(4px 网格)*/
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
--space-16: 64px;
|
||||
|
||||
/* 圆角 */
|
||||
--radius-xs: 4px; /* kbd、小标签 */
|
||||
--radius-sm: 6px; /* 按钮、输入框、键帽 */
|
||||
--radius-md: 10px; /* 菜单、小卡片 */
|
||||
--radius-lg: 14px; /* 卡片、浮层 */
|
||||
--radius-xl: 20px; /* 大容器、手机屏 */
|
||||
--radius-full: 999px;
|
||||
|
||||
/* 控件高度 */
|
||||
--control-sm: 28px; /* 桌面紧凑 */
|
||||
--control-md: 36px; /* 桌面默认 */
|
||||
--control-lg: 44px; /* 移动最小触控 */
|
||||
--control-xl: 56px; /* 移动主操作 / 麦克风条 */
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/* ============================================================
|
||||
dudu · 字体与排版令牌
|
||||
UI 中文走系统字体(输入法 = 原生应用,跟随系统最自然);
|
||||
Outfit(圆润几何无衬线)用于拉丁字 "dudu"、数字与价格,
|
||||
与圆嘴 logo 的线条语言呼应。字重只用 400/500/600/700。
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
--font-sans: 'Outfit', -apple-system, BlinkMacSystemFont, 'PingFang SC',
|
||||
'MiSans', 'HarmonyOS Sans SC', 'Microsoft YaHei', 'Segoe UI', sans-serif;
|
||||
--font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
||||
|
||||
/* 字号阶梯 */
|
||||
--text-xs: 12px; /* 角标、键帽 */
|
||||
--text-sm: 13px; /* 次要信息、菜单 */
|
||||
--text-base: 14px; /* 桌面正文 */
|
||||
--text-md: 15px; /* 浮层识别文本、移动正文 */
|
||||
--text-lg: 17px; /* 区块标题、移动列表 */
|
||||
--text-xl: 20px; /* 窗口标题 */
|
||||
--text-2xl: 24px; /* 页面大标题 */
|
||||
--text-3xl: 32px; /* 价格、大数字 */
|
||||
--text-4xl: 44px; /* 营销大字 */
|
||||
|
||||
/* 字重 */
|
||||
--weight-regular: 400;
|
||||
--weight-medium: 500;
|
||||
--weight-semibold: 600;
|
||||
--weight-bold: 700;
|
||||
|
||||
/* 行高 */
|
||||
--leading-tight: 1.25;
|
||||
--leading-normal: 1.5;
|
||||
--leading-loose: 1.75; /* 中文长文本 */
|
||||
|
||||
/* 字距 */
|
||||
--tracking-normal: 0;
|
||||
--tracking-wide: 0.02em; /* 品牌字 dudu */
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// dudu 桌面端 UI kit · 共享元素:Lucide 图标子集、mac 窗口框、伪二维码
|
||||
// 通过 window 共享给其余 text/babel 脚本
|
||||
|
||||
function DuduIcon({ d, size = 16, sw = 2, children, ...rest }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...rest}>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const DuIcons = {
|
||||
Mic: (p) => (
|
||||
<DuduIcon {...p}>
|
||||
<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>
|
||||
</DuduIcon>
|
||||
),
|
||||
Settings: (p) => (
|
||||
<DuduIcon {...p}>
|
||||
<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>
|
||||
</DuduIcon>
|
||||
),
|
||||
Check: (p) => <DuduIcon {...p}><path d="M20 6 9 17l-5-5"></path></DuduIcon>,
|
||||
X: (p) => <DuduIcon {...p}><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></DuduIcon>,
|
||||
ChevronDown: (p) => <DuduIcon {...p}><path d="m6 9 6 6 6-6"></path></DuduIcon>,
|
||||
ChevronRight: (p) => <DuduIcon {...p}><path d="m9 18 6-6-6-6"></path></DuduIcon>,
|
||||
Sun: (p) => (
|
||||
<DuduIcon {...p}>
|
||||
<circle cx="12" cy="12" r="4"></circle>
|
||||
<path d="M12 2v2"></path><path d="M12 20v2"></path><path d="m4.93 4.93 1.41 1.41"></path>
|
||||
<path d="m17.66 17.66 1.41 1.41"></path><path d="M2 12h2"></path><path d="M20 12h2"></path>
|
||||
<path d="m6.34 17.66-1.41 1.41"></path><path d="m19.07 4.93-1.41 1.41"></path>
|
||||
</DuduIcon>
|
||||
),
|
||||
Moon: (p) => <DuduIcon {...p}><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path></DuduIcon>,
|
||||
AudioLines: (p) => (
|
||||
<DuduIcon {...p}>
|
||||
<path d="M2 10v3"></path><path d="M6 6v11"></path><path d="M10 3v18"></path>
|
||||
<path d="M14 8v7"></path><path d="M18 5v13"></path><path d="M22 10v3"></path>
|
||||
</DuduIcon>
|
||||
),
|
||||
User: (p) => (
|
||||
<DuduIcon {...p}>
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</DuduIcon>
|
||||
),
|
||||
LogOut: (p) => (
|
||||
<DuduIcon {...p}>
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<polyline points="16 17 21 12 16 7"></polyline>
|
||||
<line x1="21" x2="9" y1="12" y2="12"></line>
|
||||
</DuduIcon>
|
||||
),
|
||||
};
|
||||
|
||||
// dudu 圆嘴 logo(currentColor 线条版)
|
||||
function DuduLogo({ size = 20, sw = 3.5 }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 48 48" fill="none" stroke="currentColor"
|
||||
strokeWidth={sw} strokeLinecap="round" aria-hidden="true">
|
||||
<circle cx="24" cy="24" r="18"></circle>
|
||||
<path d="M17 20.5v7"></path>
|
||||
<path d="M24 16.5v15"></path>
|
||||
<path d="M31 20.5v7"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// mac 风格窗口(红绿灯 + 标题)
|
||||
function MacWindow({ title, width = 460, children, style }) {
|
||||
return (
|
||||
<div style={{
|
||||
width, background: 'var(--surface-card)', borderRadius: 'var(--radius-md)',
|
||||
border: '1px solid var(--border-1)', boxShadow: 'var(--shadow-window)',
|
||||
overflow: 'hidden', fontFamily: 'var(--font-sans)', ...style,
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 7, padding: '10px 14px',
|
||||
background: 'var(--surface-2)', borderBottom: '1px solid var(--border-1)',
|
||||
}}>
|
||||
<span style={{ width: 11, height: 11, borderRadius: '50%', background: '#FF5F57' }}></span>
|
||||
<span style={{ width: 11, height: 11, borderRadius: '50%', background: '#FEBC2E' }}></span>
|
||||
<span style={{ width: 11, height: 11, borderRadius: '50%', background: '#28C840' }}></span>
|
||||
<span style={{ marginLeft: 10, fontSize: 'var(--text-xs)', color: 'var(--text-2)', whiteSpace: 'nowrap' }}>{title}</span>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 伪二维码占位(确定性图案,仅作占位 — 接入后端 /v1/auth/qr 后替换)
|
||||
function FakeQr({ size = 148, seed = 7 }) {
|
||||
const n = 17;
|
||||
const cells = [];
|
||||
let s = seed;
|
||||
const rnd = () => { s = (s * 16807) % 2147483647; return s / 2147483647; };
|
||||
for (let y = 0; y < n; y++) {
|
||||
for (let x = 0; x < n; x++) {
|
||||
const corner = (x < 5 && y < 5) || (x > n - 6 && y < 5) || (x < 5 && y > n - 6);
|
||||
let on;
|
||||
if (corner) {
|
||||
const cx = x > n - 6 ? x - (n - 5) : x, cy = y > n - 6 ? y - (n - 5) : y;
|
||||
on = cx === 0 || cx === 4 || cy === 0 || cy === 4 || (cx > 1 && cx < 3 + 1 && cy > 1 && cy < 3 + 1);
|
||||
} else {
|
||||
on = rnd() > 0.52;
|
||||
}
|
||||
if (on) cells.push(<span key={x + '-' + y} style={{ gridColumn: x + 1, gridRow: y + 1, background: '#1B1E26', borderRadius: 1 }}></span>);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div style={{
|
||||
width: size, height: size, padding: 10, boxSizing: 'border-box', background: '#fff',
|
||||
borderRadius: 'var(--radius-md)', border: '1px solid var(--border-1)',
|
||||
display: 'grid', gridTemplateColumns: `repeat(${n}, 1fr)`, gridTemplateRows: `repeat(${n}, 1fr)`, gap: 1,
|
||||
}} aria-label="二维码占位">{cells}</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { DuduIcon, DuIcons, DuduLogo, MacWindow, FakeQr });
|
||||
@@ -0,0 +1,108 @@
|
||||
// dudu 桌面端 · 登录 / 购买窗口(点击走完整流程:扫码登录 → 选时长包 → 支付 → 完成)
|
||||
|
||||
function LoginPurchaseWindow() {
|
||||
const { Button, Badge } = window.DuduDesignSystem_2e0172;
|
||||
const [step, setStep] = React.useState('login'); // login | plans | pay | done
|
||||
const [plan, setPlan] = React.useState('m');
|
||||
|
||||
const PACKS = {
|
||||
s: { label: '100 分钟', price: 9, desc: '约 ¥0.09 / 分钟', tag: '' },
|
||||
m: { label: '500 分钟', price: 39, desc: '约 ¥0.078 / 分钟', tag: '省 13%' },
|
||||
l: { label: '2000 分钟', price: 129, desc: '约 ¥0.065 / 分钟', tag: '省 28%' },
|
||||
};
|
||||
|
||||
const Center = ({ children }) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 14, padding: '26px 24px 30px', fontFamily: 'var(--font-sans)' }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<MacWindow title={step === 'login' ? 'dudu — 登录' : 'dudu — 购买时长'} width={400}>
|
||||
{step === 'login' && (
|
||||
<Center>
|
||||
<span style={{ color: 'var(--accent)' }}><DuduLogo size={40} /></span>
|
||||
<div style={{ fontSize: 'var(--text-lg)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)' }}>微信扫码登录</div>
|
||||
<FakeQr seed={7} />
|
||||
<div style={{ fontSize: 'var(--text-sm)', color: 'var(--text-2)', textAlign: 'center', lineHeight: 'var(--leading-normal)' }}>
|
||||
打开微信扫一扫,确认后自动登录
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => setStep('plans')}>模拟:手机已确认 →</Button>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{step === 'plans' && (
|
||||
<Center>
|
||||
<div style={{ alignSelf: 'stretch', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 'var(--text-lg)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)' }}>购买时长</span>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 'var(--text-sm)', color: 'var(--text-2)' }}>
|
||||
wang*** <Badge tone="blue">试用中</Badge>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ alignSelf: 'stretch', display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{Object.entries(PACKS).map(([key, p]) => {
|
||||
const active = plan === key;
|
||||
return (
|
||||
<button key={key} type="button" onClick={() => setPlan(key)} style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center', textAlign: 'left',
|
||||
padding: '13px 16px', borderRadius: 'var(--radius-md)', cursor: 'pointer',
|
||||
border: active ? '1.5px solid var(--accent)' : '1px solid var(--border-1)',
|
||||
background: active ? 'var(--accent-soft)' : 'var(--surface-card)',
|
||||
fontFamily: 'var(--font-sans)', transition: 'border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out)',
|
||||
}}>
|
||||
<span>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontSize: 'var(--text-base)', fontWeight: 'var(--weight-medium)', color: 'var(--text-1)' }}>{p.label}</span>
|
||||
{p.tag && <span style={{ fontSize: 10, padding: '1px 7px', borderRadius: 'var(--radius-full)', background: 'var(--warning-soft)', color: 'var(--warning)', fontWeight: 'var(--weight-medium)' }}>{p.tag}</span>}
|
||||
</span>
|
||||
<span style={{ display: 'block', fontSize: 'var(--text-xs)', color: 'var(--text-2)', marginTop: 3 }}>{p.desc}</span>
|
||||
</span>
|
||||
<span style={{ color: active ? 'var(--accent-text)' : 'var(--text-1)', whiteSpace: 'nowrap' }}>
|
||||
<span style={{ fontSize: 'var(--text-2xl)', fontWeight: 'var(--weight-bold)' }}>¥{p.price}</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ alignSelf: 'stretch', fontSize: 'var(--text-xs)', color: 'var(--text-3)', lineHeight: 'var(--leading-normal)' }}>
|
||||
时长不过期 · 每天另有 3 分钟免费试用
|
||||
</div>
|
||||
<Button variant="primary" block onClick={() => setStep('pay')}>微信支付 ¥{PACKS[plan].price}</Button>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{step === 'pay' && (
|
||||
<Center>
|
||||
<div style={{ fontSize: 'var(--text-lg)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)' }}>微信扫码支付</div>
|
||||
<div style={{ fontSize: 'var(--text-3xl)', fontWeight: 'var(--weight-bold)', color: 'var(--text-1)', fontFamily: 'var(--font-sans)' }}>
|
||||
¥{PACKS[plan].price}<span style={{ fontSize: 'var(--text-sm)', fontWeight: 'var(--weight-regular)', color: 'var(--text-3)' }}> · {PACKS[plan].label}时长包</span>
|
||||
</div>
|
||||
<FakeQr seed={23} />
|
||||
<div style={{ fontSize: 'var(--text-sm)', color: 'var(--text-2)' }}>支付完成后自动开通</div>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
<Button variant="secondary" size="sm" onClick={() => setStep('plans')}>返回</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setStep('done')}>模拟:支付成功 →</Button>
|
||||
</div>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{step === 'done' && (
|
||||
<Center>
|
||||
<span style={{
|
||||
width: 56, height: 56, borderRadius: '50%', background: 'var(--positive-soft)',
|
||||
color: 'var(--positive)', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}><DuIcons.Check size={28} sw={2.5} /></span>
|
||||
<div style={{ fontSize: 'var(--text-lg)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)' }}>已到账 {PACKS[plan].label}</div>
|
||||
<div style={{ fontSize: 'var(--text-sm)', color: 'var(--text-2)', textAlign: 'center' }}>
|
||||
时长余额 472 分钟 · 不过期 · 现在就按住 ⌘⇧Space 试试
|
||||
</div>
|
||||
<Button variant="primary" onClick={() => setStep('login')}>开始使用</Button>
|
||||
</Center>
|
||||
)}
|
||||
</MacWindow>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { LoginPurchaseWindow });
|
||||
@@ -0,0 +1,108 @@
|
||||
// dudu 桌面端 · 识别浮层演示
|
||||
// 在仿真备忘录窗口上方演示完整 push-to-talk 流程:
|
||||
// 按住按钮(= 按住 ⌘⇧Space)→ 聆听中 → partial 流式 → 松开 → final 注入输入框
|
||||
|
||||
const DEMO_SENTENCES = [
|
||||
'帮我把这份周报整理一下,重点突出本周的进展和下周的计划。',
|
||||
'好的,我马上把文件发给你,大概十分钟之内。',
|
||||
'明天上午十点的会改到下午三点,记得同步给项目组。',
|
||||
];
|
||||
|
||||
function OverlayDemo() {
|
||||
const { RecognitionOverlay } = window.DuduDesignSystem_2e0172;
|
||||
const [phase, setPhase] = React.useState('idle'); // idle | listening | streaming
|
||||
const [finalText, setFinalText] = React.useState('');
|
||||
const [partialText, setPartialText] = React.useState('');
|
||||
const [docText, setDocText] = React.useState('');
|
||||
const [sentenceIdx, setSentenceIdx] = React.useState(0);
|
||||
const timerRef = React.useRef(null);
|
||||
const stateRef = React.useRef({});
|
||||
stateRef.current = { finalText, partialText, sentenceIdx };
|
||||
|
||||
const stop = React.useCallback(() => {
|
||||
clearInterval(timerRef.current);
|
||||
const { finalText: f, partialText: p, sentenceIdx: idx } = stateRef.current;
|
||||
const committed = f + p;
|
||||
if (committed) {
|
||||
setDocText((d) => d + committed);
|
||||
setSentenceIdx((idx + 1) % DEMO_SENTENCES.length);
|
||||
}
|
||||
setFinalText(''); setPartialText('');
|
||||
setPhase('idle');
|
||||
}, []);
|
||||
|
||||
const start = React.useCallback(() => {
|
||||
if (timerRef.current) clearInterval(timerRef.current);
|
||||
setPhase('listening'); setFinalText(''); setPartialText('');
|
||||
const sentence = DEMO_SENTENCES[stateRef.current.sentenceIdx];
|
||||
let i = 0;
|
||||
setTimeout(() => setPhase((ph) => (ph === 'listening' ? 'streaming' : ph)), 420);
|
||||
timerRef.current = setInterval(() => {
|
||||
i += 1;
|
||||
if (i >= sentence.length) { clearInterval(timerRef.current); setFinalText(sentence); setPartialText(''); return; }
|
||||
// 模拟流式:句读处定稿(final),其余为 partial
|
||||
const lastPunct = Math.max(sentence.lastIndexOf(',', i), sentence.lastIndexOf('。', i));
|
||||
setFinalText(sentence.slice(0, lastPunct + 1));
|
||||
setPartialText(sentence.slice(lastPunct + 1, i));
|
||||
}, 90);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => () => clearInterval(timerRef.current), []);
|
||||
|
||||
const overlayState = phase === 'streaming' && (finalText || partialText) ? 'text' : 'listening';
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 28 }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<MacWindow title="备忘录 — 周报草稿" width={560}>
|
||||
<div style={{ padding: '18px 20px', minHeight: 150 }}>
|
||||
<div style={{ fontSize: 'var(--text-md)', lineHeight: 'var(--leading-loose)', color: 'var(--text-1)', minHeight: 96 }}>
|
||||
{docText || <span style={{ color: 'var(--text-3)' }}>光标在这里 — 在任意应用中按住快捷键即可语音输入</span>}
|
||||
<span style={{ color: 'var(--accent)', animation: 'dd-caret 1.1s steps(1) infinite' }}>▏</span>
|
||||
</div>
|
||||
</div>
|
||||
</MacWindow>
|
||||
|
||||
{phase !== 'idle' && (
|
||||
<div style={{ position: 'absolute', left: '50%', top: '100%', transform: 'translate(-50%, -22px)', animation: 'dd-rise var(--dur-base) var(--ease-out)' }}>
|
||||
<RecognitionOverlay
|
||||
state={overlayState}
|
||||
finalText={finalText}
|
||||
partialText={partialText}
|
||||
hint="松开 ⌘⇧Space 完成输入"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginTop: phase === 'idle' ? 0 : 40 }}>
|
||||
<button
|
||||
type="button"
|
||||
onPointerDown={start}
|
||||
onPointerUp={stop}
|
||||
onPointerLeave={() => phase !== 'idle' && stop()}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8, height: 'var(--control-lg)',
|
||||
padding: '0 22px', borderRadius: 'var(--radius-full)', border: 'none', cursor: 'pointer',
|
||||
background: phase === 'idle' ? 'var(--accent)' : 'var(--positive)',
|
||||
color: 'var(--text-on-accent)', fontSize: 'var(--text-base)', fontWeight: 'var(--weight-semibold)',
|
||||
fontFamily: 'var(--font-sans)', userSelect: 'none', WebkitUserSelect: 'none',
|
||||
transition: 'background var(--dur-fast) var(--ease-out)',
|
||||
}}
|
||||
>
|
||||
<DuIcons.Mic size={17} />
|
||||
{phase === 'idle' ? '按住试一试(模拟按住 ⌘⇧Space)' : '松开完成输入'}
|
||||
</button>
|
||||
{docText && (
|
||||
<button type="button" onClick={() => setDocText('')} style={{
|
||||
height: 'var(--control-lg)', padding: '0 16px', borderRadius: 'var(--radius-full)',
|
||||
border: '1px solid var(--border-1)', background: 'var(--surface-card)', color: 'var(--text-2)',
|
||||
fontSize: 'var(--text-sm)', cursor: 'pointer', fontFamily: 'var(--font-sans)',
|
||||
}}>清空</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { OverlayDemo });
|
||||
@@ -0,0 +1,27 @@
|
||||
# dudu 桌面端 UI Kit
|
||||
|
||||
桌面端(Tauri 2,Mac / Windows)四个界面的高保真交互演示,对应 MVP 设计方案 §8.1:
|
||||
|
||||
| Tab | 界面 | 交互 |
|
||||
|---|---|---|
|
||||
| 识别浮层 | 仿真备忘录窗口 + RecognitionOverlay | 按住按钮模拟 ⌘⇧Space:聆听中 → partial 流式 → 松开 final 注入 |
|
||||
| 托盘菜单 | 常驻菜单栏下拉 | hover 高亮(accent 反白)|
|
||||
| 设置 | 单页设置窗口 | Switch / 麦克风下拉 / 外观切换(驱动整页 Light/Dark)|
|
||||
| 登录 / 购买 | 微信扫码 → 选时长包 → 支付 → 完成 | 点击走完整流程 |
|
||||
|
||||
右上角月亮/太阳切换 Light / Dark;设置中"外观"同样生效(含跟随系统)。
|
||||
|
||||
## 设计要点
|
||||
|
||||
- 浮层:`--overlay-bg` 深色玻璃(两种模式不变),淡入 + 上移 4px(`--dur-base`),final 亮 / partial 灰。
|
||||
- 一屏一个蓝色主按钮;托盘 hover 用 accent 反白是系统菜单惯例。
|
||||
- 设置窗口宽 440,行高紧凑(`--control-sm` 控件),分组标题 12px。
|
||||
- 二维码为占位图案(`FakeQr`),接入后端 `/v1/auth/qr` 后替换。
|
||||
|
||||
## 文件
|
||||
|
||||
- `index.html` — 入口(tabs + 主题状态)
|
||||
- `DesktopShared.jsx` — Lucide 图标子集 · DuduLogo · MacWindow · FakeQr
|
||||
- `OverlayDemo.jsx` — push-to-talk 全流程模拟
|
||||
- `SettingsTray.jsx` — TrayMenu + SettingsWindow
|
||||
- `LoginPurchase.jsx` — 登录/时长包/支付/完成 四步
|
||||
@@ -0,0 +1,140 @@
|
||||
// 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 ? 'rgba(255,255,255,0.8)' : 'var(--text-3)', 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 });
|
||||
@@ -0,0 +1,110 @@
|
||||
<!-- @dsCard group="桌面端" viewport="1100x640" name="桌面端 UI Kit" subtitle="识别浮层 · 托盘菜单 · 设置 · 登录购买 — 可交互,Light/Dark" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>dudu 桌面端 UI Kit</title>
|
||||
<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>
|
||||
html, body { margin: 0; height: 100%; }
|
||||
body { font-family: var(--font-sans); }
|
||||
#root { height: 100%; }
|
||||
@keyframes dd-caret { 0%, 55% { opacity: 1; } 56%, 100% { opacity: 0; } }
|
||||
@keyframes dd-rise { from { opacity: 0; transform: translate(-50%, -14px); } to { opacity: 1; transform: translate(-50%, -22px); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel" src="./DesktopShared.jsx"></script>
|
||||
<script type="text/babel" src="./OverlayDemo.jsx"></script>
|
||||
<script type="text/babel" src="./SettingsTray.jsx"></script>
|
||||
<script type="text/babel" src="./LoginPurchase.jsx"></script>
|
||||
<script type="text/babel">
|
||||
const TABS = [
|
||||
['overlay', '识别浮层'],
|
||||
['tray', '托盘菜单'],
|
||||
['settings', '设置'],
|
||||
['account', '登录 / 购买'],
|
||||
];
|
||||
|
||||
function DesktopKit() {
|
||||
const [tab, setTab] = React.useState('overlay');
|
||||
const [theme, setTheme] = React.useState('system'); // system | light | dark
|
||||
const [systemDark, setSystemDark] = React.useState(
|
||||
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
);
|
||||
React.useEffect(() => {
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const fn = (e) => setSystemDark(e.matches);
|
||||
mq.addEventListener('change', fn);
|
||||
return () => mq.removeEventListener('change', fn);
|
||||
}, []);
|
||||
const dark = theme === 'dark' || (theme === 'system' && systemDark);
|
||||
|
||||
return (
|
||||
<div data-theme={dark ? 'dark' : undefined} style={{
|
||||
height: '100%', display: 'flex', flexDirection: 'column',
|
||||
background: 'var(--bg-app)', transition: 'background var(--dur-base) var(--ease-out)',
|
||||
}}>
|
||||
{/* 顶栏 */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 16, padding: '12px 20px',
|
||||
borderBottom: '1px solid var(--border-1)', background: 'var(--surface-card)',
|
||||
}}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 9, color: 'var(--text-1)' }}>
|
||||
<span style={{ color: 'var(--accent)', display: 'inline-flex' }}><DuduLogo size={22} /></span>
|
||||
<span style={{ fontWeight: 'var(--weight-semibold)', fontSize: 'var(--text-base)', letterSpacing: 'var(--tracking-wide)' }}>dudu</span>
|
||||
<span style={{ fontSize: 'var(--text-sm)', color: 'var(--text-3)', whiteSpace: 'nowrap' }}>桌面端 · Mac / Windows</span>
|
||||
</span>
|
||||
|
||||
<div style={{ display: 'flex', gap: 4, marginLeft: 18 }}>
|
||||
{TABS.map(([key, label]) => (
|
||||
<button key={key} type="button" onClick={() => setTab(key)} style={{
|
||||
height: 30, padding: '0 14px', borderRadius: 'var(--radius-full)', border: 'none', cursor: 'pointer', whiteSpace: 'nowrap',
|
||||
fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)',
|
||||
fontWeight: tab === key ? 'var(--weight-medium)' : 'var(--weight-regular)',
|
||||
background: tab === key ? 'var(--accent-soft)' : 'transparent',
|
||||
color: tab === key ? 'var(--accent-text)' : 'var(--text-2)',
|
||||
transition: 'background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out)',
|
||||
}}>{label}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button type="button" onClick={() => setTheme(dark ? 'light' : 'dark')} aria-label="切换深浅模式" style={{
|
||||
marginLeft: 'auto', width: 32, height: 32, borderRadius: 'var(--radius-sm)',
|
||||
border: '1px solid var(--border-1)', background: 'var(--surface-card)', color: 'var(--text-2)',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
|
||||
}}>
|
||||
{dark ? <DuIcons.Sun size={15} /> : <DuIcons.Moon size={15} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 舞台 */}
|
||||
<div style={{ flex: 1, display: 'flex', padding: 28, overflow: 'auto' }}>
|
||||
<div style={{ margin: 'auto' }}>
|
||||
{tab === 'overlay' && <OverlayDemo />}
|
||||
{tab === 'tray' && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 10 }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: 'var(--text-2)', fontSize: 'var(--text-sm)', paddingRight: 12, whiteSpace: 'nowrap' }}>
|
||||
<DuduLogo size={15} /> 菜单栏图标 ▾
|
||||
</span>
|
||||
<TrayMenu />
|
||||
</div>
|
||||
)}
|
||||
{tab === 'settings' && <SettingsWindow theme={theme} onThemeChange={setTheme} />}
|
||||
{tab === 'account' && <LoginPurchaseWindow />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<DesktopKit />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,187 @@
|
||||
// dudu 移动端 · 主 App 四屏:启用引导 / 登录 / 购买时长 / 我的
|
||||
|
||||
function MAppHeader({ title }) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', fontSize: 'var(--text-md)', fontWeight: 'var(--weight-medium)', color: 'var(--text-1)', padding: '4px 0 10px' }}>{title}</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- ① 启用引导 ---------- */
|
||||
function OnboardingScreen() {
|
||||
const { Button } = window.DuduDesignSystem_2e0172;
|
||||
const [step, setStep] = React.useState(0);
|
||||
const steps = [
|
||||
{ title: '添加键盘', desc: '设置 → 通用 → 键盘 → 添加新键盘,选择 dudu', icon: <DuMIcons.Keyboard size={26} /> },
|
||||
{ title: '开启完全访问', desc: '语音识别需要联网,请允许完全访问', icon: <DuMIcons.Shield size={26} /> },
|
||||
{ title: '试一试', desc: '在下面的输入框按住麦克风说一句话', icon: <DuMIcons.Mic size={26} /> },
|
||||
];
|
||||
return (
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', padding: '4px 18px 18px' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, padding: '18px 0 6px' }}>
|
||||
<span style={{ color: 'var(--accent)' }}><DuduMLogo size={44} /></span>
|
||||
<div style={{ fontSize: 'var(--text-xl)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)', whiteSpace: 'nowrap' }}>三步开启 dudu</div>
|
||||
<div style={{ fontSize: 'var(--text-sm)', color: 'var(--text-2)' }}>按住说话,松开上屏</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 14 }}>
|
||||
{steps.map((s, i) => {
|
||||
const state = i < step ? 'done' : i === step ? 'active' : 'todo';
|
||||
return (
|
||||
<div key={i} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 12, padding: '13px 14px',
|
||||
borderRadius: 'var(--radius-lg)', background: 'var(--surface-card)',
|
||||
border: state === 'active' ? '1.5px solid var(--accent)' : '1px solid var(--border-1)',
|
||||
opacity: state === 'todo' ? 0.62 : 1,
|
||||
transition: 'border-color var(--dur-fast) var(--ease-out), opacity var(--dur-fast) var(--ease-out)',
|
||||
}}>
|
||||
<span style={{
|
||||
width: 40, height: 40, borderRadius: 'var(--radius-md)', flex: 'none',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: state === 'done' ? 'var(--positive-soft)' : 'var(--accent-soft)',
|
||||
color: state === 'done' ? 'var(--positive)' : 'var(--accent-text)',
|
||||
}}>{state === 'done' ? <DuMIcons.Check size={20} /> : s.icon}</span>
|
||||
<span style={{ minWidth: 0 }}>
|
||||
<span style={{ display: 'block', fontSize: 'var(--text-base)', fontWeight: 'var(--weight-medium)', color: 'var(--text-1)' }}>{i + 1}. {s.title}</span>
|
||||
<span style={{ display: 'block', fontSize: 'var(--text-xs)', color: 'var(--text-2)', marginTop: 2, lineHeight: 'var(--leading-normal)' }}>{s.desc}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 'auto' }}>
|
||||
<Button variant="primary" size="lg" block onClick={() => setStep((s) => (s + 1) % 4)}>
|
||||
{step >= 3 ? '完成' : step === 2 ? '我说完了' : '去设置'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- ② 登录 ---------- */
|
||||
function LoginScreen() {
|
||||
return (
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', padding: '4px 22px 22px' }}>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12 }}>
|
||||
<span style={{ color: 'var(--accent)' }}><DuduMLogo size={64} sw={3.2} /></span>
|
||||
<div style={{ fontSize: 'var(--text-2xl)', fontWeight: 'var(--weight-semibold)', color: 'var(--text-1)', letterSpacing: 'var(--tracking-wide)' }}>dudu</div>
|
||||
<div style={{ fontSize: 'var(--text-sm)', color: 'var(--text-2)' }}>大模型语音输入 · 说话即上屏</div>
|
||||
</div>
|
||||
<button type="button" style={{
|
||||
height: 'var(--control-xl)', borderRadius: 'var(--radius-full)', border: 'none', cursor: 'pointer',
|
||||
background: '#07C160', color: '#fff', fontSize: 'var(--text-md)', fontWeight: 'var(--weight-semibold)',
|
||||
fontFamily: 'var(--font-sans)', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, width: '100%',
|
||||
}}>
|
||||
<DuMIcons.Wechat size={19} /> 微信一键登录
|
||||
</button>
|
||||
<div style={{ textAlign: 'center', fontSize: 'var(--text-xs)', color: 'var(--text-3)', marginTop: 12, lineHeight: 'var(--leading-normal)' }}>
|
||||
登录即同意《用户协议》与《隐私政策》
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- ③ 购买时长 ---------- */
|
||||
function PaywallScreen() {
|
||||
const { Button } = window.DuduDesignSystem_2e0172;
|
||||
const [pack, setPack] = React.useState('m');
|
||||
const PACKS = [
|
||||
['s', '100 分钟', 9, '约 ¥0.09 / 分钟', ''],
|
||||
['m', '500 分钟', 39, '约 ¥0.078 / 分钟', '省 13%'],
|
||||
['l', '2000 分钟', 129, '约 ¥0.065 / 分钟', '省 28%'],
|
||||
];
|
||||
const price = PACKS.find((p) => p[0] === pack)[2];
|
||||
return (
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', padding: '4px 16px 16px', minHeight: 0, overflow: 'auto' }}>
|
||||
<MAppHeader title="购买时长" />
|
||||
|
||||
{/* 余额卡 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 16px', background: 'var(--surface-card)', border: '1px solid var(--border-1)', borderRadius: 'var(--radius-lg)', flex: 'none' }}>
|
||||
<span>
|
||||
<span style={{ display: 'block', fontSize: 'var(--text-xs)', color: 'var(--text-2)' }}>时长余额</span>
|
||||
<span style={{ display: 'block', marginTop: 2, fontSize: 'var(--text-2xl)', fontWeight: 'var(--weight-bold)', color: 'var(--text-1)', fontFamily: 'var(--font-sans)' }}>472 <span style={{ fontSize: 'var(--text-xs)', fontWeight: 'var(--weight-regular)', color: 'var(--text-3)' }}>分钟</span></span>
|
||||
</span>
|
||||
<span style={{ fontSize: 'var(--text-xs)', color: 'var(--text-3)', textAlign: 'right', lineHeight: 'var(--leading-normal)' }}>时长不过期<br />每天另有 3 分钟免费试用</span>
|
||||
</div>
|
||||
|
||||
{/* 时长包 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 12 }}>
|
||||
{PACKS.map(([key, lab, p, desc, tag]) => {
|
||||
const active = pack === key;
|
||||
return (
|
||||
<button key={key} type="button" onClick={() => setPack(key)} style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center', textAlign: 'left',
|
||||
padding: '12px 14px', cursor: 'pointer', borderRadius: 'var(--radius-lg)', fontFamily: 'var(--font-sans)',
|
||||
border: active ? '1.5px solid var(--accent)' : '1px solid var(--border-1)',
|
||||
background: active ? 'var(--accent-soft)' : 'var(--surface-card)',
|
||||
transition: 'border-color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out)',
|
||||
}}>
|
||||
<span>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
|
||||
<span style={{ fontSize: 'var(--text-base)', fontWeight: 'var(--weight-medium)', color: 'var(--text-1)' }}>{lab}</span>
|
||||
{tag && <span style={{ fontSize: 10, padding: '1px 7px', borderRadius: 'var(--radius-full)', background: 'var(--warning-soft)', color: 'var(--warning)', fontWeight: 'var(--weight-medium)', whiteSpace: 'nowrap' }}>{tag}</span>}
|
||||
</span>
|
||||
<span style={{ display: 'block', fontSize: 'var(--text-xs)', color: 'var(--text-2)', marginTop: 2 }}>{desc}</span>
|
||||
</span>
|
||||
<span style={{ whiteSpace: 'nowrap', fontSize: 'var(--text-xl)', fontWeight: 'var(--weight-bold)', color: active ? 'var(--accent-text)' : 'var(--text-1)' }}>¥{p}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 'auto', paddingTop: 14 }}>
|
||||
<Button variant="primary" size="lg" block>微信支付 ¥{price}</Button>
|
||||
<div style={{ textAlign: 'center', fontSize: 'var(--text-xs)', color: 'var(--text-3)', marginTop: 8 }}>支付后立即到账 · 时长不过期</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- ④ 我的 ---------- */
|
||||
function ProfileScreen() {
|
||||
const { Badge, ProgressBar } = window.DuduDesignSystem_2e0172;
|
||||
const perms = [
|
||||
{ label: '麦克风权限', ok: true },
|
||||
{ label: '键盘已添加', ok: true },
|
||||
{ label: '完全访问', ok: false, hint: '去开启' },
|
||||
];
|
||||
return (
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 12, padding: '4px 16px 16px', minHeight: 0, overflow: 'auto' }}>
|
||||
<MAppHeader title="我的" />
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '14px 14px', background: 'var(--surface-card)', border: '1px solid var(--border-1)', borderRadius: 'var(--radius-lg)' }}>
|
||||
<span style={{ width: 46, height: 46, borderRadius: '50%', background: 'var(--accent-soft)', color: 'var(--accent-text)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }}>
|
||||
<DuMIcons.User size={22} />
|
||||
</span>
|
||||
<span style={{ minWidth: 0, flex: 1 }}>
|
||||
<span style={{ display: 'block', fontSize: 'var(--text-md)', fontWeight: 'var(--weight-medium)', color: 'var(--text-1)' }}>wang***</span>
|
||||
<span style={{ display: 'block', fontSize: 'var(--text-xs)', color: 'var(--text-2)', marginTop: 2 }}>时长余额 472 分钟 · 不过期</span>
|
||||
</span>
|
||||
<Badge tone="green">余额充足</Badge>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '14px 14px 16px', background: 'var(--surface-card)', border: '1px solid var(--border-1)', borderRadius: 'var(--radius-lg)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 'var(--text-xs)', color: 'var(--text-2)', marginBottom: 8 }}>
|
||||
<span>今日免费试用</span><span style={{ fontFamily: 'var(--font-mono)' }}>1 / 3 分钟</span>
|
||||
</div>
|
||||
<ProgressBar value={1} max={3} />
|
||||
</div>
|
||||
|
||||
<div style={{ background: 'var(--surface-card)', border: '1px solid var(--border-1)', borderRadius: 'var(--radius-lg)', overflow: 'hidden' }}>
|
||||
{perms.map((p, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 14px', borderBottom: i < perms.length - 1 ? '1px solid var(--border-2)' : 'none', fontSize: 'var(--text-base)', color: 'var(--text-1)' }}>
|
||||
<span>{p.label}</span>
|
||||
{p.ok ? (
|
||||
<span style={{ color: 'var(--positive)', display: 'inline-flex' }}><DuMIcons.Check size={16} /></span>
|
||||
) : (
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 2, fontSize: 'var(--text-sm)', color: 'var(--danger)' }}>
|
||||
{p.hint} <DuMIcons.ChevronRight size={14} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { OnboardingScreen, LoginScreen, PaywallScreen, ProfileScreen });
|
||||
@@ -0,0 +1,140 @@
|
||||
// dudu 移动端 · 语音键盘(键盘扩展)
|
||||
// 微信聊天上下文 + 以语音为主的键盘:工具条 / partial 文本条 / 功能键行 / 大麦克风条
|
||||
// 按住麦克风条:partial 流式显示 → 松开 final 上屏到宿主输入框
|
||||
|
||||
const KB_SENTENCES = [
|
||||
'好的,我马上把文件发给你,大概十分钟之内。',
|
||||
'今晚的聚餐改到八点,地址我发你定位。',
|
||||
];
|
||||
|
||||
function KeyboardScreen({ account = 'ok' }) {
|
||||
// account: ok(有余额) | trial(试用中) | guest(未登录) | quota(今日试用已用完)
|
||||
const { MicBar } = window.DuduDesignSystem_2e0172;
|
||||
const [recording, setRecording] = React.useState(false);
|
||||
const [finalText, setFinalText] = React.useState('');
|
||||
const [partialText, setPartialText] = React.useState('');
|
||||
const [inputText, setInputText] = React.useState('');
|
||||
const [seconds, setSeconds] = React.useState(0);
|
||||
const [idx, setIdx] = React.useState(0);
|
||||
const timers = React.useRef({});
|
||||
const ref = React.useRef({});
|
||||
ref.current = { finalText, partialText, idx };
|
||||
|
||||
const stop = () => {
|
||||
clearInterval(timers.current.t); clearInterval(timers.current.s);
|
||||
const { finalText: f, partialText: p, idx: i } = ref.current;
|
||||
const committed = f + p;
|
||||
if (committed) { setInputText((v) => v + committed); setIdx((i + 1) % KB_SENTENCES.length); }
|
||||
setFinalText(''); setPartialText(''); setSeconds(0);
|
||||
setRecording(false);
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
if (account === 'guest' || account === 'quota') return;
|
||||
setRecording(true); setFinalText(''); setPartialText(''); setSeconds(0);
|
||||
const sentence = KB_SENTENCES[ref.current.idx];
|
||||
let i = 0;
|
||||
timers.current.t = setInterval(() => {
|
||||
i += 1;
|
||||
if (i >= sentence.length) { clearInterval(timers.current.t); setFinalText(sentence); setPartialText(''); return; }
|
||||
const lastPunct = Math.max(sentence.lastIndexOf(',', i), sentence.lastIndexOf('。', i));
|
||||
setFinalText(sentence.slice(0, lastPunct + 1));
|
||||
setPartialText(sentence.slice(lastPunct + 1, i));
|
||||
}, 95);
|
||||
timers.current.s = setInterval(() => setSeconds((s) => s + 1), 1000);
|
||||
};
|
||||
React.useEffect(() => () => { clearInterval(timers.current.t); clearInterval(timers.current.s); }, []);
|
||||
|
||||
const micState = account === 'guest' ? 'disabled' : account === 'quota' ? 'quota' : recording ? 'recording' : 'idle';
|
||||
const fmt = (s) => `00:${String(s).padStart(2, '0')}`;
|
||||
|
||||
const FnKey = ({ children, label, flex = 1, onClick }) => (
|
||||
<button type="button" onClick={onClick} aria-label={label} style={{
|
||||
flex, height: 40, border: 'none', borderRadius: 'var(--radius-sm)', cursor: 'pointer',
|
||||
background: 'var(--surface-card)', color: 'var(--text-1)', boxShadow: 'var(--shadow-key)',
|
||||
fontFamily: 'var(--font-sans)', fontSize: 'var(--text-md)',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 4,
|
||||
}}>{children}</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* 宿主 App:微信聊天 */}
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||
<div style={{ textAlign: 'center', fontSize: 'var(--text-sm)', color: 'var(--text-2)', padding: '4px 0 8px', borderBottom: '1px solid var(--border-1)' }}>张三</div>
|
||||
<div style={{ flex: 1, padding: '12px 12px 8px', display: 'flex', flexDirection: 'column', gap: 8, overflow: 'hidden', justifyContent: 'flex-end' }}>
|
||||
<div style={{ alignSelf: 'flex-start', maxWidth: '78%', background: 'var(--surface-card)', border: '1px solid var(--border-1)', borderRadius: '4px 12px 12px 12px', padding: '8px 11px', fontSize: 'var(--text-base)', color: 'var(--text-1)', lineHeight: 'var(--leading-normal)' }}>
|
||||
上次说的那份材料方便发我一下吗
|
||||
</div>
|
||||
<div style={{ alignSelf: 'flex-end', maxWidth: '78%', background: 'var(--accent)', color: 'var(--text-on-accent)', borderRadius: '12px 4px 12px 12px', padding: '8px 11px', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-normal)' }}>
|
||||
稍等,我看一下
|
||||
</div>
|
||||
</div>
|
||||
{/* 宿主输入框 */}
|
||||
<div style={{ padding: '0 10px 10px' }}>
|
||||
<div style={{
|
||||
minHeight: 38, boxSizing: 'border-box', padding: '8px 12px', borderRadius: 'var(--radius-md)',
|
||||
background: 'var(--surface-card)', border: recording ? '1.5px solid var(--accent)' : '1px solid var(--border-1)',
|
||||
fontSize: 'var(--text-base)', color: inputText ? 'var(--text-1)' : 'var(--text-3)', lineHeight: 'var(--leading-normal)',
|
||||
}}>
|
||||
{inputText || '发消息…'}
|
||||
<span style={{ color: 'var(--accent)', animation: 'dd-caret 1.1s steps(1) infinite' }}>▏</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== 键盘扩展 ===== */}
|
||||
<div style={{ flex: 'none', background: 'var(--surface-2)', borderTop: '1px solid var(--border-1)', padding: '6px 8px 10px' }}>
|
||||
{/* 工具条 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '2px 6px 8px' }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 'var(--text-xs)', color: 'var(--text-2)' }}>
|
||||
<DuduMLogo size={13} /> dudu 语音输入
|
||||
</span>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 'var(--text-xs)', fontFamily: recording ? 'var(--font-mono)' : 'var(--font-sans)', color: recording ? 'var(--positive)' : account === 'quota' ? 'var(--warning)' : 'var(--text-3)' }}>
|
||||
{account === 'guest' ? '未登录'
|
||||
: account === 'quota' ? '今日试用已用完'
|
||||
: recording ? fmt(seconds)
|
||||
: account === 'trial' ? '试用 · 今日剩 2 分钟'
|
||||
: <React.Fragment>余额 472 分钟 <DuMIcons.Check size={11} /></React.Fragment>}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* partial 文本条(录音中替代功能键行) */}
|
||||
{recording ? (
|
||||
<div style={{
|
||||
margin: '0 2px 8px', minHeight: 40, boxSizing: 'border-box', padding: '9px 12px',
|
||||
background: 'var(--surface-card)', border: '1px solid var(--border-1)', borderRadius: 'var(--radius-md)',
|
||||
fontSize: 'var(--text-base)', lineHeight: 'var(--leading-normal)', color: 'var(--text-1)',
|
||||
}}>
|
||||
{finalText}<span style={{ color: 'var(--text-3)' }}>{partialText}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', gap: 6, margin: '0 2px 8px' }}>
|
||||
<FnKey label="切换键盘" flex={1.3}><DuMIcons.Globe size={17} /></FnKey>
|
||||
<FnKey label="逗号" onClick={() => setInputText((v) => v + ',')}>,</FnKey>
|
||||
<FnKey label="句号" onClick={() => setInputText((v) => v + '。')}>。</FnKey>
|
||||
<FnKey label="退格" flex={1.3} onClick={() => setInputText((v) => v.slice(0, -1))}><DuMIcons.Backspace size={17} /></FnKey>
|
||||
<FnKey label="发送" flex={1.3} onClick={() => setInputText('')}><DuMIcons.Enter size={16} /></FnKey>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 大麦克风条 */}
|
||||
<div style={{ padding: '0 2px' }}>
|
||||
<MicBar
|
||||
state={micState}
|
||||
onPointerDown={start}
|
||||
onPointerUp={stop}
|
||||
onPointerLeave={() => recording && stop()}
|
||||
/>
|
||||
</div>
|
||||
{recording && (
|
||||
<div style={{ textAlign: 'center', fontSize: 'var(--text-xs)', color: 'var(--text-3)', paddingTop: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4 }}>
|
||||
<DuMIcons.ArrowUp size={11} /> 上滑取消
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { KeyboardScreen });
|
||||
@@ -0,0 +1,103 @@
|
||||
// dudu 移动端 UI kit · 共享:Lucide 图标子集、DuduLogo、手机框
|
||||
|
||||
function DuMIcon({ size = 16, sw = 2, children, ...rest }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...rest}>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const DuMIcons = {
|
||||
Mic: (p) => (
|
||||
<DuMIcon {...p}>
|
||||
<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>
|
||||
</DuMIcon>
|
||||
),
|
||||
Globe: (p) => (
|
||||
<DuMIcon {...p}>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"></path>
|
||||
<path d="M2 12h20"></path>
|
||||
</DuMIcon>
|
||||
),
|
||||
Backspace: (p) => (
|
||||
<DuMIcon {...p}>
|
||||
<path d="M20 5H9l-7 7 7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2Z"></path>
|
||||
<line x1="18" x2="12" y1="9" y2="15"></line>
|
||||
<line x1="12" x2="18" y1="9" y2="15"></line>
|
||||
</DuMIcon>
|
||||
),
|
||||
Enter: (p) => (
|
||||
<DuMIcon {...p}>
|
||||
<polyline points="9 10 4 15 9 20"></polyline>
|
||||
<path d="M20 4v7a4 4 0 0 1-4 4H4"></path>
|
||||
</DuMIcon>
|
||||
),
|
||||
Check: (p) => <DuMIcon {...p}><path d="M20 6 9 17l-5-5"></path></DuMIcon>,
|
||||
ChevronRight: (p) => <DuMIcon {...p}><path d="m9 18 6-6-6-6"></path></DuMIcon>,
|
||||
User: (p) => (
|
||||
<DuMIcon {...p}>
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</DuMIcon>
|
||||
),
|
||||
Keyboard: (p) => (
|
||||
<DuMIcon {...p}>
|
||||
<rect width="20" height="16" x="2" y="4" rx="2"></rect>
|
||||
<path d="M6 8h.01"></path><path d="M10 8h.01"></path><path d="M14 8h.01"></path><path d="M18 8h.01"></path>
|
||||
<path d="M6 12h.01"></path><path d="M10 12h.01"></path><path d="M14 12h.01"></path><path d="M18 12h.01"></path>
|
||||
<path d="M7 16h10"></path>
|
||||
</DuMIcon>
|
||||
),
|
||||
Shield: (p) => (
|
||||
<DuMIcon {...p}>
|
||||
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"></path>
|
||||
</DuMIcon>
|
||||
),
|
||||
ArrowUp: (p) => <DuMIcon {...p}><path d="m5 12 7-7 7 7"></path><path d="M12 19V5"></path></DuMIcon>,
|
||||
Wechat: (p) => (
|
||||
/* 简化对话气泡(代微信图标占位,正式包内用官方资源)*/
|
||||
<DuMIcon {...p}>
|
||||
<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"></path>
|
||||
</DuMIcon>
|
||||
),
|
||||
};
|
||||
|
||||
function DuduMLogo({ size = 20, sw = 3.5 }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 48 48" fill="none" stroke="currentColor"
|
||||
strokeWidth={sw} strokeLinecap="round" aria-hidden="true">
|
||||
<circle cx="24" cy="24" r="18"></circle>
|
||||
<path d="M17 20.5v7"></path>
|
||||
<path d="M24 16.5v15"></path>
|
||||
<path d="M31 20.5v7"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// 中性手机框:深色边框 + 打孔/刘海,内容区由调用方填充
|
||||
function PhoneFrame({ width = 312, height = 596, children }) {
|
||||
return (
|
||||
<div style={{
|
||||
width, height, boxSizing: 'border-box', background: '#0B0C10', borderRadius: 38,
|
||||
padding: 9, boxShadow: 'var(--shadow-window)', flex: 'none',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '100%', height: '100%', boxSizing: 'border-box', background: 'var(--bg-app)',
|
||||
borderRadius: 30, overflow: 'hidden', display: 'flex', flexDirection: 'column', position: 'relative',
|
||||
}}>
|
||||
{/* 状态栏 */}
|
||||
<div style={{ height: 30, flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ width: 84, height: 18, background: '#0B0C10', borderRadius: 10 }}></div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { DuMIcon, DuMIcons, DuduMLogo, PhoneFrame });
|
||||
@@ -0,0 +1,30 @@
|
||||
# dudu 移动端 UI Kit
|
||||
|
||||
移动端(iOS / Android 原生键盘扩展 + 主 App)高保真交互演示,对应 MVP 设计方案 §8.2–8.3。
|
||||
|
||||
## 左:键盘扩展
|
||||
|
||||
微信聊天宿主上下文 + 以语音为主的键盘:
|
||||
|
||||
- 工具条(dudu 标识 + 余额/试用/计时状态)
|
||||
- 功能键行:切换键盘 / ,/ 。/ 退格 / 发送(语音为主,不做 26 键)
|
||||
- 大麦克风条(`MicBar`,56px 胶囊):**按住说话** → partial 文本条流式刷新 → **松开** final 上屏到宿主输入框;录音中显示"上滑取消"
|
||||
- 顶部状态 chips 切换四态:有余额 / 试用中(每天免费 3 分钟)/ 未登录(按键置灰)/ 试用已用完(橙色引导购买时长)
|
||||
|
||||
## 右:主 App 四屏
|
||||
|
||||
- **启用引导**:三步卡片(添加键盘 → 完全访问 → 试一试),点主按钮推进
|
||||
- **登录**:logo + 微信一键登录(微信品牌绿 #07C160)
|
||||
- **购买时长**:时长余额卡 + 时长包(100/500/2000 分钟,不过期)+ 微信支付
|
||||
- **我的**:账号卡(时长余额)+ 今日免费试用 ProgressBar + 权限自检(异常红色"去开启")
|
||||
|
||||
右上角切换 Light / Dark。键盘面板与 App 均随主题;键帽用 `--surface-card` + `--shadow-key` 底边。
|
||||
|
||||
## 文件
|
||||
|
||||
- `index.html` — 入口(两台手机并排 + 状态 chips + 主题)
|
||||
- `MobileShared.jsx` — Lucide 图标子集 · DuduMLogo · PhoneFrame
|
||||
- `KeyboardScreen.jsx` — 键盘扩展全态
|
||||
- `AppScreens.jsx` — Onboarding / Login / Paywall / Profile
|
||||
|
||||
注:微信图标为简化气泡占位,正式包请使用微信官方资源。
|
||||
@@ -0,0 +1,131 @@
|
||||
<!-- @dsCard group="移动端" viewport="1100x720" name="移动端 UI Kit" subtitle="语音键盘(按住说话)+ 主 App 四屏 — 可交互,Light/Dark" -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>dudu 移动端 UI Kit</title>
|
||||
<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>
|
||||
html, body { margin: 0; height: 100%; }
|
||||
body { font-family: var(--font-sans); }
|
||||
#root { height: 100%; }
|
||||
@keyframes dd-caret { 0%, 55% { opacity: 1; } 56%, 100% { opacity: 0; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel" src="./MobileShared.jsx"></script>
|
||||
<script type="text/babel" src="./KeyboardScreen.jsx"></script>
|
||||
<script type="text/babel" src="./AppScreens.jsx"></script>
|
||||
<script type="text/babel">
|
||||
function Chip({ active, onClick, children }) {
|
||||
return (
|
||||
<button type="button" onClick={onClick} style={{
|
||||
height: 28, padding: '0 13px', borderRadius: 'var(--radius-full)', cursor: 'pointer', whiteSpace: 'nowrap',
|
||||
border: active ? '1px solid transparent' : '1px solid var(--border-1)',
|
||||
background: active ? 'var(--accent-soft)' : 'var(--surface-card)',
|
||||
color: active ? 'var(--accent-text)' : 'var(--text-2)',
|
||||
fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)',
|
||||
fontWeight: active ? 'var(--weight-medium)' : 'var(--weight-regular)',
|
||||
transition: 'background var(--dur-fast) var(--ease-out)',
|
||||
}}>{children}</button>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileKit() {
|
||||
const [override, setOverride] = React.useState(null); // null = 跟随系统
|
||||
const [systemDark, setSystemDark] = React.useState(
|
||||
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
);
|
||||
React.useEffect(() => {
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const fn = (e) => setSystemDark(e.matches);
|
||||
mq.addEventListener('change', fn);
|
||||
return () => mq.removeEventListener('change', fn);
|
||||
}, []);
|
||||
const dark = override === null ? systemDark : override;
|
||||
const [account, setAccount] = React.useState('ok'); // 键盘状态
|
||||
const [appScreen, setAppScreen] = React.useState('onboarding');
|
||||
|
||||
const APP_SCREENS = [
|
||||
['onboarding', '启用引导'],
|
||||
['login', '登录'],
|
||||
['paywall', '购买时长'],
|
||||
['profile', '我的'],
|
||||
];
|
||||
|
||||
return (
|
||||
<div data-theme={dark ? 'dark' : undefined} style={{
|
||||
height: '100%', display: 'flex', flexDirection: 'column',
|
||||
background: 'var(--bg-app)', transition: 'background var(--dur-base) var(--ease-out)',
|
||||
}}>
|
||||
{/* 顶栏 */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 14, padding: '12px 20px',
|
||||
borderBottom: '1px solid var(--border-1)', background: 'var(--surface-card)',
|
||||
}}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 9, color: 'var(--text-1)', whiteSpace: 'nowrap' }}>
|
||||
<span style={{ color: 'var(--accent)', display: 'inline-flex' }}><DuduMLogo size={22} /></span>
|
||||
<span style={{ fontWeight: 'var(--weight-semibold)', fontSize: 'var(--text-base)', letterSpacing: 'var(--tracking-wide)' }}>dudu</span>
|
||||
<span style={{ fontSize: 'var(--text-sm)', color: 'var(--text-3)' }}>移动端 · iOS / Android</span>
|
||||
</span>
|
||||
<button type="button" onClick={() => setOverride(!dark)} aria-label="切换深浅模式" style={{
|
||||
marginLeft: 'auto', width: 32, height: 32, borderRadius: 'var(--radius-sm)',
|
||||
border: '1px solid var(--border-1)', background: 'var(--surface-card)', color: 'var(--text-2)',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
|
||||
}}>
|
||||
{dark ? (
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2"></path><path d="M12 20v2"></path><path d="m4.93 4.93 1.41 1.41"></path><path d="m17.66 17.66 1.41 1.41"></path><path d="M2 12h2"></path><path d="M20 12h2"></path><path d="m6.34 17.66-1.41 1.41"></path><path d="m19.07 4.93-1.41 1.41"></path></svg>
|
||||
) : (
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path></svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 舞台:键盘 + 主 App 并排 */}
|
||||
<div style={{ flex: 1, display: 'flex', overflow: 'auto' }}>
|
||||
<div style={{ margin: 'auto', display: 'flex', gap: 56, alignItems: 'flex-start', padding: 28 }}>
|
||||
{/* 键盘扩展 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<Chip active={account === 'ok'} onClick={() => setAccount('ok')}>有余额</Chip>
|
||||
<Chip active={account === 'trial'} onClick={() => setAccount('trial')}>试用中</Chip>
|
||||
<Chip active={account === 'guest'} onClick={() => setAccount('guest')}>未登录</Chip>
|
||||
<Chip active={account === 'quota'} onClick={() => setAccount('quota')}>试用已用完</Chip>
|
||||
</div>
|
||||
<PhoneFrame>
|
||||
<KeyboardScreen account={account} key={account} />
|
||||
</PhoneFrame>
|
||||
<span style={{ fontSize: 'var(--text-xs)', color: 'var(--text-3)' }}>键盘扩展 — 按住麦克风条说话</span>
|
||||
</div>
|
||||
|
||||
{/* 主 App */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
{APP_SCREENS.map(([key, label]) => (
|
||||
<Chip key={key} active={appScreen === key} onClick={() => setAppScreen(key)}>{label}</Chip>
|
||||
))}
|
||||
</div>
|
||||
<PhoneFrame>
|
||||
{appScreen === 'onboarding' && <OnboardingScreen />}
|
||||
{appScreen === 'login' && <LoginScreen />}
|
||||
{appScreen === 'paywall' && <PaywallScreen />}
|
||||
{appScreen === 'profile' && <ProfileScreen />}
|
||||
</PhoneFrame>
|
||||
<span style={{ fontSize: 'var(--text-xs)', color: 'var(--text-3)' }}>主 App — 登录 / 购买 / 用量</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<MobileKit />);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||