From a4856c5aa17a538298d21c10d2b7b55ce888fb3e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Jan 2026 07:46:13 +0000 Subject: [PATCH 1/7] feat(design): add W3C DTCG 2025.10 compliant design tokens system - Add design-tokens.tokens.json following W3C specification - Add component-tokens.ts with MD3 and Liquid Glass patterns - Add tokens.css with full CSS Variables implementation - Add 006_design_modernization_spec.md with ASCII layouts Token categories: - Color: primitives + semantic + accent variants - Spacing: 4px base unit scale (0-24) - Typography: font families, sizes, presets - Radius: sm to full scale - Shadow: sm to 2xl + glass-glow - Glass: blur, opacity, border, surface presets - Motion: duration + easing - Navigation: MD3 compliant sizes Responsive layouts designed for: - Mobile: Bottom Tab Bar (56px, 3-5 items) - Tablet: Nav Rail (80px) + Split View - Desktop: Sidebar (256px) + Multi-column References: - W3C Design Tokens 2025.10 - Apple Liquid Glass iOS 26 - Material Design 3 Navigation --- apps/ratewise/src/config/component-tokens.ts | 779 ++++++++++++++++++ .../src/config/design-tokens.tokens.json | 522 ++++++++++++ apps/ratewise/src/styles/tokens.css | 586 +++++++++++++ docs/dev/006_design_modernization_spec.md | 672 +++++++++++++++ 4 files changed, 2559 insertions(+) create mode 100644 apps/ratewise/src/config/component-tokens.ts create mode 100644 apps/ratewise/src/config/design-tokens.tokens.json create mode 100644 apps/ratewise/src/styles/tokens.css create mode 100644 docs/dev/006_design_modernization_spec.md diff --git a/apps/ratewise/src/config/component-tokens.ts b/apps/ratewise/src/config/component-tokens.ts new file mode 100644 index 00000000..9842e4b2 --- /dev/null +++ b/apps/ratewise/src/config/component-tokens.ts @@ -0,0 +1,779 @@ +/** + * Component Tokens - Ratewise Design System + * + * @description 元件層級的 Token 定義,基於 W3C DTCG 2025.10 規範 + * @see https://www.designtokens.org/tr/drafts/format/ + * @see https://m3.material.io/components - Material Design 3 Guidelines + * @see https://developer.apple.com/documentation/TechnologyOverviews/liquid-glass - Apple Liquid Glass + * + * @version 2.0.0 + * @updated 2025-01-17 + */ + +// ============================================================ +// 1. 導覽元件 Tokens (Material Design 3 + Apple HIG) +// ============================================================ + +/** + * 底部導覽列 - Mobile (Material Design 3 Navigation Bar) + * @see https://m3.material.io/components/navigation-bar/guidelines + */ +export const bottomTabBar = { + // 容器 + container: { + height: '56px', // MD3 標準高度 + paddingX: '8px', + paddingY: '0px', + background: 'var(--glass-surface-base)', + backdropBlur: 'var(--glass-blur-lg)', + borderTop: '1px solid var(--glass-border-light)', + }, + // 導覽項目 + item: { + minWidth: '64px', // MD3 最小寬度 + maxWidth: '96px', // MD3 最大寬度 + height: '100%', + padding: '4px 0', + gap: '4px', + }, + // 圖示 + icon: { + size: '24px', + activeColor: 'var(--color-accent-primary)', + inactiveColor: 'var(--color-text-tertiary)', + }, + // 標籤 + label: { + fontSize: 'var(--font-size-xs)', + fontWeight: '500', + activeColor: 'var(--color-accent-primary)', + inactiveColor: 'var(--color-text-tertiary)', + }, + // 指示器 (MD3 Pill Indicator) + indicator: { + height: '32px', + borderRadius: 'var(--radius-full)', + background: 'var(--color-accent-primary)', + opacity: '0.12', + }, +} as const; + +/** + * 側邊導覽軌 - Tablet (Material Design 3 Navigation Rail) + * @see https://m3.material.io/components/navigation-rail/guidelines + */ +export const navRail = { + container: { + width: '80px', // MD3 標準寬度 + paddingY: '12px', + background: 'var(--glass-surface-base)', + backdropBlur: 'var(--glass-blur-lg)', + borderRight: '1px solid var(--glass-border-light)', + }, + item: { + width: '56px', + height: '56px', + borderRadius: 'var(--radius-xl)', + marginBottom: '4px', + }, + icon: { + size: '24px', + }, + label: { + fontSize: 'var(--font-size-xs)', + marginTop: '4px', + }, +} as const; + +/** + * 側邊欄 - Desktop + */ +export const sidebar = { + container: { + width: '256px', + collapsedWidth: '72px', + paddingX: '12px', + paddingY: '16px', + background: 'var(--glass-surface-base)', + backdropBlur: 'var(--glass-blur-lg)', + borderRight: '1px solid var(--glass-border-light)', + }, + section: { + marginBottom: '24px', + }, + sectionTitle: { + fontSize: 'var(--font-size-xs)', + fontWeight: '600', + color: 'var(--color-text-tertiary)', + textTransform: 'uppercase' as const, + letterSpacing: '0.05em', + marginBottom: '8px', + paddingX: '12px', + }, + item: { + height: '40px', + paddingX: '12px', + borderRadius: 'var(--radius-lg)', + gap: '12px', + }, +} as const; + +/** + * 頂部工具列 - Desktop + */ +export const topBar = { + container: { + height: '64px', + paddingX: '24px', + background: 'var(--glass-surface-base)', + backdropBlur: 'var(--glass-blur-lg)', + borderBottom: '1px solid var(--glass-border-light)', + }, + logo: { + height: '32px', + }, + search: { + width: '320px', + height: '40px', + }, + actions: { + gap: '8px', + }, +} as const; + +// ============================================================ +// 2. 卡片元件 Tokens (Liquid Glass) +// ============================================================ + +/** + * 玻璃卡片 - 基礎表面 + */ +export const glassCard = { + base: { + background: 'rgba(255, 255, 255, 0.08)', + backdropFilter: 'blur(16px)', + border: '1px solid rgba(255, 255, 255, 0.18)', + borderRadius: 'var(--radius-xl)', + boxShadow: 'var(--shadow-md)', + padding: 'var(--spacing-4)', + }, + elevated: { + background: 'rgba(255, 255, 255, 0.12)', + backdropFilter: 'blur(24px)', + border: '1px solid rgba(255, 255, 255, 0.25)', + borderRadius: 'var(--radius-xl)', + boxShadow: 'var(--shadow-lg)', + padding: 'var(--spacing-4)', + }, + interactive: { + transition: 'all var(--duration-normal) var(--easing-ease-out)', + hover: { + background: 'rgba(255, 255, 255, 0.15)', + transform: 'translateY(-2px)', + boxShadow: 'var(--shadow-xl)', + }, + active: { + transform: 'translateY(0)', + boxShadow: 'var(--shadow-md)', + }, + }, +} as const; + +/** + * 匯率卡片 - 專用於 Ratewise + */ +export const rateCard = { + container: { + ...glassCard.base, + padding: 'var(--spacing-4)', + minHeight: '120px', + }, + header: { + marginBottom: 'var(--spacing-3)', + }, + currencyPair: { + fontSize: 'var(--font-size-lg)', + fontWeight: '600', + color: 'var(--color-text-primary)', + }, + rateDisplay: { + fontSize: 'var(--font-size-3xl)', + fontWeight: '700', + fontFamily: 'var(--font-mono)', + color: 'var(--color-text-primary)', + letterSpacing: '-0.02em', + }, + change: { + fontSize: 'var(--font-size-sm)', + fontWeight: '500', + positive: 'var(--color-status-success)', + negative: 'var(--color-status-error)', + neutral: 'var(--color-text-tertiary)', + }, + timestamp: { + fontSize: 'var(--font-size-xs)', + color: 'var(--color-text-tertiary)', + }, +} as const; + +// ============================================================ +// 3. 按鈕元件 Tokens +// ============================================================ + +export const button = { + // 尺寸變體 + size: { + sm: { + height: '32px', + paddingX: '12px', + fontSize: 'var(--font-size-sm)', + borderRadius: 'var(--radius-md)', + iconSize: '16px', + gap: '6px', + }, + md: { + height: '40px', + paddingX: '16px', + fontSize: 'var(--font-size-sm)', + borderRadius: 'var(--radius-lg)', + iconSize: '20px', + gap: '8px', + }, + lg: { + height: '48px', + paddingX: '24px', + fontSize: 'var(--font-size-base)', + borderRadius: 'var(--radius-lg)', + iconSize: '20px', + gap: '8px', + }, + }, + // 樣式變體 + variant: { + primary: { + background: 'var(--color-accent-primary)', + color: 'var(--color-text-inverse)', + border: 'none', + hover: { + background: 'var(--color-accent-primary-hover)', + }, + active: { + background: 'var(--color-accent-primary-active)', + }, + }, + secondary: { + background: 'transparent', + color: 'var(--color-accent-primary)', + border: '1px solid var(--color-accent-primary)', + hover: { + background: 'rgba(124, 58, 237, 0.08)', + }, + }, + ghost: { + background: 'transparent', + color: 'var(--color-text-secondary)', + border: 'none', + hover: { + background: 'var(--color-background-secondary)', + }, + }, + glass: { + background: 'rgba(255, 255, 255, 0.1)', + color: 'var(--color-text-primary)', + border: '1px solid rgba(255, 255, 255, 0.2)', + backdropFilter: 'blur(8px)', + hover: { + background: 'rgba(255, 255, 255, 0.15)', + }, + }, + }, + // 共用屬性 + common: { + fontWeight: '500', + transition: 'all var(--duration-fast) var(--easing-ease-out)', + focusRing: '2px solid var(--color-border-focus)', + focusRingOffset: '2px', + disabled: { + opacity: '0.5', + cursor: 'not-allowed', + }, + }, +} as const; + +// ============================================================ +// 4. 輸入元件 Tokens +// ============================================================ + +export const input = { + container: { + height: '48px', + paddingX: 'var(--spacing-4)', + background: 'var(--color-surface-default)', + border: '1px solid var(--color-border-default)', + borderRadius: 'var(--radius-lg)', + fontSize: 'var(--font-size-base)', + transition: 'all var(--duration-fast) var(--easing-ease-out)', + }, + states: { + hover: { + borderColor: 'var(--color-border-strong)', + }, + focus: { + borderColor: 'var(--color-border-focus)', + boxShadow: '0 0 0 3px rgba(124, 58, 237, 0.15)', + }, + error: { + borderColor: 'var(--color-status-error)', + boxShadow: '0 0 0 3px rgba(239, 68, 68, 0.15)', + }, + disabled: { + background: 'var(--color-background-secondary)', + opacity: '0.6', + }, + }, + label: { + fontSize: 'var(--font-size-sm)', + fontWeight: '500', + color: 'var(--color-text-secondary)', + marginBottom: 'var(--spacing-2)', + }, + helper: { + fontSize: 'var(--font-size-xs)', + color: 'var(--color-text-tertiary)', + marginTop: 'var(--spacing-1)', + }, + error: { + fontSize: 'var(--font-size-xs)', + color: 'var(--color-status-error)', + marginTop: 'var(--spacing-1)', + }, +} as const; + +/** + * 貨幣輸入框 - Ratewise 專用 + */ +export const currencyInput = { + container: { + ...input.container, + height: '56px', + paddingLeft: 'var(--spacing-4)', + paddingRight: '80px', // 為貨幣選擇器預留空間 + }, + value: { + fontSize: 'var(--font-size-xl)', + fontWeight: '600', + fontFamily: 'var(--font-mono)', + }, + currencySelector: { + position: 'absolute' as const, + right: '4px', + top: '50%', + transform: 'translateY(-50%)', + height: '40px', + paddingX: 'var(--spacing-3)', + borderRadius: 'var(--radius-md)', + background: 'var(--color-background-secondary)', + }, +} as const; + +// ============================================================ +// 5. 模態框與覆蓋層 Tokens +// ============================================================ + +export const modal = { + backdrop: { + background: 'rgba(0, 0, 0, 0.5)', + backdropFilter: 'blur(4px)', + }, + container: { + background: 'rgba(255, 255, 255, 0.95)', + backdropFilter: 'blur(32px)', + borderRadius: 'var(--radius-2xl)', + boxShadow: 'var(--shadow-xl)', + maxWidth: '480px', + width: '90%', + maxHeight: '85vh', + }, + header: { + padding: 'var(--spacing-4) var(--spacing-4) var(--spacing-3)', + borderBottom: '1px solid var(--color-border-default)', + }, + body: { + padding: 'var(--spacing-4)', + overflowY: 'auto' as const, + }, + footer: { + padding: 'var(--spacing-3) var(--spacing-4) var(--spacing-4)', + borderTop: '1px solid var(--color-border-default)', + gap: 'var(--spacing-3)', + }, +} as const; + +export const drawer = { + backdrop: modal.backdrop, + container: { + background: 'rgba(255, 255, 255, 0.95)', + backdropFilter: 'blur(32px)', + boxShadow: 'var(--shadow-xl)', + }, + // 方向變體 + direction: { + right: { + width: '320px', + maxWidth: '90%', + height: '100%', + borderRadius: 'var(--radius-2xl) 0 0 var(--radius-2xl)', + }, + bottom: { + width: '100%', + maxHeight: '85vh', + borderRadius: 'var(--radius-2xl) var(--radius-2xl) 0 0', + }, + }, +} as const; + +// ============================================================ +// 6. 圖表元件 Tokens +// ============================================================ + +export const chart = { + container: { + ...glassCard.base, + padding: 'var(--spacing-4)', + minHeight: '200px', + }, + header: { + marginBottom: 'var(--spacing-3)', + }, + title: { + fontSize: 'var(--font-size-base)', + fontWeight: '600', + color: 'var(--color-text-primary)', + }, + subtitle: { + fontSize: 'var(--font-size-sm)', + color: 'var(--color-text-tertiary)', + }, + // 時間範圍選擇器 + timeRange: { + gap: 'var(--spacing-1)', + button: { + height: '28px', + paddingX: 'var(--spacing-2)', + fontSize: 'var(--font-size-xs)', + fontWeight: '500', + borderRadius: 'var(--radius-md)', + active: { + background: 'var(--color-accent-primary)', + color: 'var(--color-text-inverse)', + }, + inactive: { + background: 'transparent', + color: 'var(--color-text-tertiary)', + }, + }, + }, + // 圖表顏色 + colors: { + primary: 'var(--color-accent-primary)', + secondary: 'var(--color-accent-secondary)', + positive: 'var(--color-status-success)', + negative: 'var(--color-status-error)', + grid: 'var(--color-border-default)', + axis: 'var(--color-text-tertiary)', + }, +} as const; + +// ============================================================ +// 7. 列表元件 Tokens +// ============================================================ + +export const list = { + container: { + ...glassCard.base, + padding: '0', + overflow: 'hidden', + }, + item: { + padding: 'var(--spacing-3) var(--spacing-4)', + borderBottom: '1px solid var(--color-border-default)', + transition: 'background var(--duration-fast) var(--easing-ease-out)', + hover: { + background: 'rgba(0, 0, 0, 0.02)', + }, + active: { + background: 'rgba(124, 58, 237, 0.08)', + }, + }, + // 貨幣列表項 + currencyItem: { + height: '64px', + padding: 'var(--spacing-3) var(--spacing-4)', + gap: 'var(--spacing-3)', + }, + currencyIcon: { + size: '40px', + borderRadius: 'var(--radius-full)', + background: 'var(--color-background-secondary)', + }, + currencyName: { + fontSize: 'var(--font-size-base)', + fontWeight: '500', + color: 'var(--color-text-primary)', + }, + currencyCode: { + fontSize: 'var(--font-size-sm)', + color: 'var(--color-text-tertiary)', + }, + currencyRate: { + fontSize: 'var(--font-size-base)', + fontWeight: '600', + fontFamily: 'var(--font-mono)', + textAlign: 'right' as const, + }, +} as const; + +// ============================================================ +// 8. 標籤與徽章 Tokens +// ============================================================ + +export const chip = { + size: { + sm: { + height: '24px', + paddingX: 'var(--spacing-2)', + fontSize: 'var(--font-size-xs)', + borderRadius: 'var(--radius-md)', + }, + md: { + height: '32px', + paddingX: 'var(--spacing-3)', + fontSize: 'var(--font-size-sm)', + borderRadius: 'var(--radius-lg)', + }, + }, + variant: { + filled: { + background: 'var(--color-background-secondary)', + color: 'var(--color-text-secondary)', + }, + outlined: { + background: 'transparent', + border: '1px solid var(--color-border-default)', + color: 'var(--color-text-secondary)', + }, + tonal: { + background: 'rgba(124, 58, 237, 0.12)', + color: 'var(--color-accent-primary)', + }, + }, +} as const; + +export const badge = { + size: { + sm: { + minWidth: '16px', + height: '16px', + fontSize: '10px', + }, + md: { + minWidth: '20px', + height: '20px', + fontSize: '12px', + }, + }, + variant: { + default: { + background: 'var(--color-accent-primary)', + color: 'var(--color-text-inverse)', + }, + success: { + background: 'var(--color-status-success)', + color: 'var(--color-text-inverse)', + }, + warning: { + background: 'var(--color-status-warning)', + color: 'var(--color-text-inverse)', + }, + error: { + background: 'var(--color-status-error)', + color: 'var(--color-text-inverse)', + }, + }, + common: { + fontWeight: '600', + borderRadius: 'var(--radius-full)', + padding: '0 var(--spacing-1)', + }, +} as const; + +// ============================================================ +// 9. 設定頁元件 Tokens +// ============================================================ + +export const settings = { + section: { + marginBottom: 'var(--spacing-6)', + }, + sectionTitle: { + fontSize: 'var(--font-size-sm)', + fontWeight: '600', + color: 'var(--color-text-tertiary)', + textTransform: 'uppercase' as const, + letterSpacing: '0.05em', + marginBottom: 'var(--spacing-3)', + paddingX: 'var(--spacing-4)', + }, + group: { + ...glassCard.base, + padding: '0', + overflow: 'hidden', + }, + item: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: 'var(--spacing-4)', + borderBottom: '1px solid var(--color-border-default)', + minHeight: '56px', + }, + itemLabel: { + fontSize: 'var(--font-size-base)', + color: 'var(--color-text-primary)', + }, + itemValue: { + fontSize: 'var(--font-size-sm)', + color: 'var(--color-text-tertiary)', + }, + itemIcon: { + size: '20px', + color: 'var(--color-text-tertiary)', + }, +} as const; + +/** + * 主題選擇器 + */ +export const themePicker = { + grid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(100px, 1fr))', + gap: 'var(--spacing-3)', + padding: 'var(--spacing-4)', + }, + option: { + aspectRatio: '1', + borderRadius: 'var(--radius-xl)', + border: '2px solid transparent', + overflow: 'hidden', + cursor: 'pointer', + transition: 'all var(--duration-normal) var(--easing-ease-out)', + selected: { + borderColor: 'var(--color-accent-primary)', + boxShadow: '0 0 0 2px var(--color-accent-primary)', + }, + }, + preview: { + width: '100%', + height: '70%', + borderRadius: 'var(--radius-lg) var(--radius-lg) 0 0', + }, + label: { + height: '30%', + fontSize: 'var(--font-size-xs)', + fontWeight: '500', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, +} as const; + +/** + * 顏色選擇器 + */ +export const colorPicker = { + container: { + display: 'flex', + gap: 'var(--spacing-3)', + padding: 'var(--spacing-4)', + }, + swatch: { + width: '40px', + height: '40px', + borderRadius: 'var(--radius-full)', + border: '2px solid transparent', + cursor: 'pointer', + transition: 'all var(--duration-fast) var(--easing-ease-out)', + selected: { + borderColor: 'var(--color-text-primary)', + transform: 'scale(1.1)', + }, + }, +} as const; + +// ============================================================ +// 10. Tailwind 類別生成器 +// ============================================================ + +/** + * 生成 Tailwind 類別字串 + */ +export const tw = { + // 玻璃卡片 + glassCard: { + base: 'bg-white/[0.08] backdrop-blur-md border border-white/[0.18] rounded-xl shadow-md', + elevated: 'bg-white/[0.12] backdrop-blur-lg border border-white/[0.25] rounded-xl shadow-lg', + interactive: + 'transition-all duration-200 ease-out hover:bg-white/[0.15] hover:-translate-y-0.5 hover:shadow-xl active:translate-y-0 active:shadow-md', + }, + // 按鈕 + button: { + base: 'inline-flex items-center justify-center font-medium transition-all duration-100 ease-out focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed', + primary: + 'bg-violet-600 text-white hover:bg-violet-700 active:bg-violet-800 focus:ring-violet-500', + secondary: + 'bg-transparent text-violet-600 border border-violet-600 hover:bg-violet-50 focus:ring-violet-500', + ghost: 'bg-transparent text-slate-600 hover:bg-slate-100 focus:ring-slate-400', + glass: + 'bg-white/10 text-slate-900 border border-white/20 backdrop-blur-sm hover:bg-white/15 focus:ring-white/50', + sm: 'h-8 px-3 text-sm rounded-md gap-1.5', + md: 'h-10 px-4 text-sm rounded-lg gap-2', + lg: 'h-12 px-6 text-base rounded-lg gap-2', + }, + // 輸入框 + input: { + base: 'w-full h-12 px-4 bg-white border border-slate-200 rounded-lg text-base transition-all duration-100 ease-out placeholder:text-slate-400 focus:outline-none focus:border-violet-500 focus:ring-3 focus:ring-violet-500/15 disabled:bg-slate-100 disabled:opacity-60', + error: 'border-red-500 focus:border-red-500 focus:ring-red-500/15', + }, + // 標籤 + chip: { + base: 'inline-flex items-center font-medium', + filled: 'bg-slate-100 text-slate-600', + outlined: 'bg-transparent border border-slate-200 text-slate-600', + tonal: 'bg-violet-600/12 text-violet-600', + sm: 'h-6 px-2 text-xs rounded-md', + md: 'h-8 px-3 text-sm rounded-lg', + }, +} as const; + +export default { + bottomTabBar, + navRail, + sidebar, + topBar, + glassCard, + rateCard, + button, + input, + currencyInput, + modal, + drawer, + chart, + list, + chip, + badge, + settings, + themePicker, + colorPicker, + tw, +}; diff --git a/apps/ratewise/src/config/design-tokens.tokens.json b/apps/ratewise/src/config/design-tokens.tokens.json new file mode 100644 index 00000000..861342ba --- /dev/null +++ b/apps/ratewise/src/config/design-tokens.tokens.json @@ -0,0 +1,522 @@ +{ + "$schema": "https://www.designtokens.org/schemas/2025.10/tokens.schema.json", + "$description": "Ratewise Design Tokens - W3C DTCG 2025.10 Specification Compliant", + "$version": "2.0.0", + + "color": { + "$description": "Color Tokens - Primitive Layer", + + "primitive": { + "$description": "Raw color values - do not use directly in components", + + "violet": { + "50": { "$type": "color", "$value": "#f5f3ff" }, + "100": { "$type": "color", "$value": "#ede9fe" }, + "200": { "$type": "color", "$value": "#ddd6fe" }, + "300": { "$type": "color", "$value": "#c4b5fd" }, + "400": { "$type": "color", "$value": "#a78bfa" }, + "500": { "$type": "color", "$value": "#8b5cf6" }, + "600": { "$type": "color", "$value": "#7c3aed" }, + "700": { "$type": "color", "$value": "#6d28d9" }, + "800": { "$type": "color", "$value": "#5b21b6" }, + "900": { "$type": "color", "$value": "#4c1d95" }, + "950": { "$type": "color", "$value": "#2e1065" } + }, + + "slate": { + "50": { "$type": "color", "$value": "#f8fafc" }, + "100": { "$type": "color", "$value": "#f1f5f9" }, + "200": { "$type": "color", "$value": "#e2e8f0" }, + "300": { "$type": "color", "$value": "#cbd5e1" }, + "400": { "$type": "color", "$value": "#94a3b8" }, + "500": { "$type": "color", "$value": "#64748b" }, + "600": { "$type": "color", "$value": "#475569" }, + "700": { "$type": "color", "$value": "#334155" }, + "800": { "$type": "color", "$value": "#1e293b" }, + "900": { "$type": "color", "$value": "#0f172a" }, + "950": { "$type": "color", "$value": "#020617" } + }, + + "cyan": { + "400": { "$type": "color", "$value": "#22d3ee" }, + "500": { "$type": "color", "$value": "#06b6d4" }, + "600": { "$type": "color", "$value": "#0891b2" } + }, + + "emerald": { + "400": { "$type": "color", "$value": "#34d399" }, + "500": { "$type": "color", "$value": "#10b981" }, + "600": { "$type": "color", "$value": "#059669" } + }, + + "red": { + "400": { "$type": "color", "$value": "#f87171" }, + "500": { "$type": "color", "$value": "#ef4444" }, + "600": { "$type": "color", "$value": "#dc2626" } + }, + + "amber": { + "400": { "$type": "color", "$value": "#fbbf24" }, + "500": { "$type": "color", "$value": "#f59e0b" }, + "600": { "$type": "color", "$value": "#d97706" } + }, + + "white": { "$type": "color", "$value": "#ffffff" }, + "black": { "$type": "color", "$value": "#000000" }, + "transparent": { "$type": "color", "$value": "transparent" } + }, + + "semantic": { + "$description": "Semantic color tokens - use these in components", + + "background": { + "primary": { "$type": "color", "$value": "{color.primitive.slate.50}" }, + "secondary": { "$type": "color", "$value": "{color.primitive.slate.100}" }, + "tertiary": { "$type": "color", "$value": "{color.primitive.slate.200}" }, + "inverse": { "$type": "color", "$value": "{color.primitive.slate.900}" } + }, + + "surface": { + "default": { "$type": "color", "$value": "{color.primitive.white}" }, + "elevated": { "$type": "color", "$value": "{color.primitive.white}" }, + "overlay": { "$type": "color", "$value": "rgba(255, 255, 255, 0.85)" } + }, + + "text": { + "primary": { "$type": "color", "$value": "{color.primitive.slate.900}" }, + "secondary": { "$type": "color", "$value": "{color.primitive.slate.600}" }, + "tertiary": { "$type": "color", "$value": "{color.primitive.slate.500}" }, + "disabled": { "$type": "color", "$value": "{color.primitive.slate.400}" }, + "inverse": { "$type": "color", "$value": "{color.primitive.white}" }, + "link": { "$type": "color", "$value": "{color.primitive.violet.600}" }, + "link-hover": { "$type": "color", "$value": "{color.primitive.violet.700}" } + }, + + "border": { + "default": { "$type": "color", "$value": "{color.primitive.slate.200}" }, + "strong": { "$type": "color", "$value": "{color.primitive.slate.300}" }, + "focus": { "$type": "color", "$value": "{color.primitive.violet.500}" } + }, + + "accent": { + "primary": { "$type": "color", "$value": "{color.primitive.violet.600}" }, + "primary-hover": { "$type": "color", "$value": "{color.primitive.violet.700}" }, + "primary-active": { "$type": "color", "$value": "{color.primitive.violet.800}" }, + "secondary": { "$type": "color", "$value": "{color.primitive.cyan.500}" } + }, + + "status": { + "success": { "$type": "color", "$value": "{color.primitive.emerald.500}" }, + "success-bg": { "$type": "color", "$value": "#ecfdf5" }, + "warning": { "$type": "color", "$value": "{color.primitive.amber.500}" }, + "warning-bg": { "$type": "color", "$value": "#fffbeb" }, + "error": { "$type": "color", "$value": "{color.primitive.red.500}" }, + "error-bg": { "$type": "color", "$value": "#fef2f2" }, + "info": { "$type": "color", "$value": "{color.primitive.cyan.500}" }, + "info-bg": { "$type": "color", "$value": "#ecfeff" } + } + } + }, + + "spacing": { + "$description": "Spacing scale based on 4px base unit", + "0": { "$type": "dimension", "$value": "0px" }, + "1": { "$type": "dimension", "$value": "4px" }, + "2": { "$type": "dimension", "$value": "8px" }, + "3": { "$type": "dimension", "$value": "12px" }, + "4": { "$type": "dimension", "$value": "16px" }, + "5": { "$type": "dimension", "$value": "20px" }, + "6": { "$type": "dimension", "$value": "24px" }, + "8": { "$type": "dimension", "$value": "32px" }, + "10": { "$type": "dimension", "$value": "40px" }, + "12": { "$type": "dimension", "$value": "48px" }, + "16": { "$type": "dimension", "$value": "64px" }, + "20": { "$type": "dimension", "$value": "80px" }, + "24": { "$type": "dimension", "$value": "96px" } + }, + + "size": { + "$description": "Component size tokens", + "icon": { + "xs": { "$type": "dimension", "$value": "12px" }, + "sm": { "$type": "dimension", "$value": "16px" }, + "md": { "$type": "dimension", "$value": "20px" }, + "lg": { "$type": "dimension", "$value": "24px" }, + "xl": { "$type": "dimension", "$value": "32px" } + }, + "touch-target": { + "min": { "$type": "dimension", "$value": "44px" }, + "comfortable": { "$type": "dimension", "$value": "48px" } + }, + "navigation": { + "tab-bar-height": { "$type": "dimension", "$value": "56px" }, + "nav-rail-width": { "$type": "dimension", "$value": "80px" }, + "sidebar-width": { "$type": "dimension", "$value": "256px" }, + "sidebar-collapsed": { "$type": "dimension", "$value": "72px" }, + "top-bar-height": { "$type": "dimension", "$value": "64px" } + } + }, + + "radius": { + "$description": "Border radius scale", + "none": { "$type": "dimension", "$value": "0px" }, + "sm": { "$type": "dimension", "$value": "4px" }, + "md": { "$type": "dimension", "$value": "8px" }, + "lg": { "$type": "dimension", "$value": "12px" }, + "xl": { "$type": "dimension", "$value": "16px" }, + "2xl": { "$type": "dimension", "$value": "24px" }, + "3xl": { "$type": "dimension", "$value": "32px" }, + "full": { "$type": "dimension", "$value": "9999px" } + }, + + "typography": { + "$description": "Typography tokens following W3C composite type", + + "font-family": { + "sans": { "$type": "fontFamily", "$value": ["Noto Sans TC", "system-ui", "sans-serif"] }, + "mono": { "$type": "fontFamily", "$value": ["SF Mono", "Consolas", "monospace"] } + }, + + "font-size": { + "xs": { "$type": "dimension", "$value": "12px" }, + "sm": { "$type": "dimension", "$value": "14px" }, + "base": { "$type": "dimension", "$value": "16px" }, + "lg": { "$type": "dimension", "$value": "18px" }, + "xl": { "$type": "dimension", "$value": "20px" }, + "2xl": { "$type": "dimension", "$value": "24px" }, + "3xl": { "$type": "dimension", "$value": "30px" }, + "4xl": { "$type": "dimension", "$value": "36px" }, + "5xl": { "$type": "dimension", "$value": "48px" } + }, + + "font-weight": { + "normal": { "$type": "fontWeight", "$value": 400 }, + "medium": { "$type": "fontWeight", "$value": 500 }, + "semibold": { "$type": "fontWeight", "$value": 600 }, + "bold": { "$type": "fontWeight", "$value": 700 } + }, + + "line-height": { + "tight": { "$type": "number", "$value": 1.25 }, + "snug": { "$type": "number", "$value": 1.375 }, + "normal": { "$type": "number", "$value": 1.5 }, + "relaxed": { "$type": "number", "$value": 1.625 } + }, + + "letter-spacing": { + "tighter": { "$type": "dimension", "$value": "-0.05em" }, + "tight": { "$type": "dimension", "$value": "-0.025em" }, + "normal": { "$type": "dimension", "$value": "0em" }, + "wide": { "$type": "dimension", "$value": "0.025em" } + }, + + "preset": { + "$description": "Composite typography presets", + "display-lg": { + "$type": "typography", + "$value": { + "fontFamily": "{typography.font-family.sans}", + "fontSize": "{typography.font-size.5xl}", + "fontWeight": "{typography.font-weight.bold}", + "lineHeight": "{typography.line-height.tight}", + "letterSpacing": "{typography.letter-spacing.tight}" + } + }, + "display-md": { + "$type": "typography", + "$value": { + "fontFamily": "{typography.font-family.sans}", + "fontSize": "{typography.font-size.4xl}", + "fontWeight": "{typography.font-weight.bold}", + "lineHeight": "{typography.line-height.tight}", + "letterSpacing": "{typography.letter-spacing.tight}" + } + }, + "heading-lg": { + "$type": "typography", + "$value": { + "fontFamily": "{typography.font-family.sans}", + "fontSize": "{typography.font-size.2xl}", + "fontWeight": "{typography.font-weight.semibold}", + "lineHeight": "{typography.line-height.snug}" + } + }, + "heading-md": { + "$type": "typography", + "$value": { + "fontFamily": "{typography.font-family.sans}", + "fontSize": "{typography.font-size.xl}", + "fontWeight": "{typography.font-weight.semibold}", + "lineHeight": "{typography.line-height.snug}" + } + }, + "heading-sm": { + "$type": "typography", + "$value": { + "fontFamily": "{typography.font-family.sans}", + "fontSize": "{typography.font-size.lg}", + "fontWeight": "{typography.font-weight.semibold}", + "lineHeight": "{typography.line-height.snug}" + } + }, + "body-lg": { + "$type": "typography", + "$value": { + "fontFamily": "{typography.font-family.sans}", + "fontSize": "{typography.font-size.lg}", + "fontWeight": "{typography.font-weight.normal}", + "lineHeight": "{typography.line-height.relaxed}" + } + }, + "body-md": { + "$type": "typography", + "$value": { + "fontFamily": "{typography.font-family.sans}", + "fontSize": "{typography.font-size.base}", + "fontWeight": "{typography.font-weight.normal}", + "lineHeight": "{typography.line-height.normal}" + } + }, + "body-sm": { + "$type": "typography", + "$value": { + "fontFamily": "{typography.font-family.sans}", + "fontSize": "{typography.font-size.sm}", + "fontWeight": "{typography.font-weight.normal}", + "lineHeight": "{typography.line-height.normal}" + } + }, + "caption": { + "$type": "typography", + "$value": { + "fontFamily": "{typography.font-family.sans}", + "fontSize": "{typography.font-size.xs}", + "fontWeight": "{typography.font-weight.normal}", + "lineHeight": "{typography.line-height.normal}" + } + }, + "label": { + "$type": "typography", + "$value": { + "fontFamily": "{typography.font-family.sans}", + "fontSize": "{typography.font-size.sm}", + "fontWeight": "{typography.font-weight.medium}", + "lineHeight": "{typography.line-height.tight}" + } + }, + "rate-display": { + "$type": "typography", + "$value": { + "fontFamily": "{typography.font-family.mono}", + "fontSize": "{typography.font-size.3xl}", + "fontWeight": "{typography.font-weight.bold}", + "lineHeight": "{typography.line-height.tight}", + "letterSpacing": "{typography.letter-spacing.tighter}" + } + } + } + }, + + "shadow": { + "$description": "Shadow tokens following W3C composite type", + "none": { + "$type": "shadow", + "$value": { + "color": "transparent", + "offsetX": "0px", + "offsetY": "0px", + "blur": "0px", + "spread": "0px" + } + }, + "sm": { + "$type": "shadow", + "$value": { + "color": "rgba(0, 0, 0, 0.05)", + "offsetX": "0px", + "offsetY": "1px", + "blur": "2px", + "spread": "0px" + } + }, + "md": { + "$type": "shadow", + "$value": [ + { + "color": "rgba(0, 0, 0, 0.1)", + "offsetX": "0px", + "offsetY": "4px", + "blur": "6px", + "spread": "-1px" + }, + { + "color": "rgba(0, 0, 0, 0.06)", + "offsetX": "0px", + "offsetY": "2px", + "blur": "4px", + "spread": "-1px" + } + ] + }, + "lg": { + "$type": "shadow", + "$value": [ + { + "color": "rgba(0, 0, 0, 0.1)", + "offsetX": "0px", + "offsetY": "10px", + "blur": "15px", + "spread": "-3px" + }, + { + "color": "rgba(0, 0, 0, 0.05)", + "offsetX": "0px", + "offsetY": "4px", + "blur": "6px", + "spread": "-2px" + } + ] + }, + "xl": { + "$type": "shadow", + "$value": [ + { + "color": "rgba(0, 0, 0, 0.1)", + "offsetX": "0px", + "offsetY": "20px", + "blur": "25px", + "spread": "-5px" + }, + { + "color": "rgba(0, 0, 0, 0.04)", + "offsetX": "0px", + "offsetY": "8px", + "blur": "10px", + "spread": "-6px" + } + ] + }, + "glass-glow": { + "$type": "shadow", + "$value": { + "color": "rgba(124, 58, 237, 0.15)", + "offsetX": "0px", + "offsetY": "0px", + "blur": "40px", + "spread": "0px" + } + }, + "inner": { + "$type": "shadow", + "$value": { + "color": "rgba(0, 0, 0, 0.05)", + "offsetX": "0px", + "offsetY": "2px", + "blur": "4px", + "spread": "0px", + "inset": true + } + } + }, + + "glass": { + "$description": "Liquid Glass effect tokens - Apple iOS 26 inspired", + + "blur": { + "none": { "$type": "dimension", "$value": "0px" }, + "sm": { "$type": "dimension", "$value": "8px" }, + "md": { "$type": "dimension", "$value": "16px" }, + "lg": { "$type": "dimension", "$value": "24px" }, + "xl": { "$type": "dimension", "$value": "32px" }, + "2xl": { "$type": "dimension", "$value": "40px" } + }, + + "opacity": { + "subtle": { "$type": "number", "$value": 0.05 }, + "light": { "$type": "number", "$value": 0.08 }, + "medium": { "$type": "number", "$value": 0.12 }, + "heavy": { "$type": "number", "$value": 0.18 }, + "solid": { "$type": "number", "$value": 0.85 } + }, + + "border": { + "light": { "$type": "color", "$value": "rgba(255, 255, 255, 0.18)" }, + "medium": { "$type": "color", "$value": "rgba(255, 255, 255, 0.25)" }, + "strong": { "$type": "color", "$value": "rgba(255, 255, 255, 0.35)" } + }, + + "surface": { + "$description": "Pre-composed glass surface configurations", + "base": { + "$type": "object", + "$value": { + "blur": "{glass.blur.md}", + "opacity": "{glass.opacity.light}", + "border": "{glass.border.light}", + "background": "rgba(255, 255, 255, 0.08)" + } + }, + "elevated": { + "$type": "object", + "$value": { + "blur": "{glass.blur.lg}", + "opacity": "{glass.opacity.medium}", + "border": "{glass.border.medium}", + "background": "rgba(255, 255, 255, 0.12)" + } + }, + "overlay": { + "$type": "object", + "$value": { + "blur": "{glass.blur.xl}", + "opacity": "{glass.opacity.heavy}", + "border": "{glass.border.strong}", + "background": "rgba(255, 255, 255, 0.18)" + } + } + } + }, + + "motion": { + "$description": "Animation and transition tokens", + + "duration": { + "instant": { "$type": "duration", "$value": "0ms" }, + "fast": { "$type": "duration", "$value": "100ms" }, + "normal": { "$type": "duration", "$value": "200ms" }, + "slow": { "$type": "duration", "$value": "300ms" }, + "slower": { "$type": "duration", "$value": "500ms" } + }, + + "easing": { + "linear": { "$type": "cubicBezier", "$value": [0, 0, 1, 1] }, + "ease-out": { "$type": "cubicBezier", "$value": [0, 0, 0.2, 1] }, + "ease-in": { "$type": "cubicBezier", "$value": [0.4, 0, 1, 1] }, + "ease-in-out": { "$type": "cubicBezier", "$value": [0.4, 0, 0.2, 1] }, + "spring": { "$type": "cubicBezier", "$value": [0.34, 1.56, 0.64, 1] } + } + }, + + "breakpoint": { + "$description": "Responsive breakpoints - Mobile First", + "xs": { "$type": "dimension", "$value": "0px" }, + "sm": { "$type": "dimension", "$value": "375px" }, + "md": { "$type": "dimension", "$value": "768px" }, + "lg": { "$type": "dimension", "$value": "1024px" }, + "xl": { "$type": "dimension", "$value": "1280px" }, + "2xl": { "$type": "dimension", "$value": "1536px" } + }, + + "z-index": { + "$description": "Z-index layering system", + "base": { "$type": "number", "$value": 0 }, + "dropdown": { "$type": "number", "$value": 1000 }, + "sticky": { "$type": "number", "$value": 1100 }, + "fixed": { "$type": "number", "$value": 1200 }, + "drawer": { "$type": "number", "$value": 1300 }, + "modal": { "$type": "number", "$value": 1400 }, + "popover": { "$type": "number", "$value": 1500 }, + "toast": { "$type": "number", "$value": 1600 }, + "tooltip": { "$type": "number", "$value": 1700 } + } +} diff --git a/apps/ratewise/src/styles/tokens.css b/apps/ratewise/src/styles/tokens.css new file mode 100644 index 00000000..59ae1ed9 --- /dev/null +++ b/apps/ratewise/src/styles/tokens.css @@ -0,0 +1,586 @@ +/** + * Ratewise Design Tokens - CSS Variables Implementation + * + * @description 基於 W3C DTCG 2025.10 規範的 CSS Variables + * @see https://www.designtokens.org/tr/drafts/format/ + * @see https://developer.apple.com/documentation/TechnologyOverviews/liquid-glass + * @see https://m3.material.io/ + * + * @version 2.0.0 + * @updated 2025-01-17 + * + * 結構: + * 1. Primitive Tokens (原始值) + * 2. Semantic Tokens (語意值) + * 3. Glass Effect Tokens (玻璃效果) + * 4. Component Tokens (元件值) + * 5. Dark Theme Overrides + * 6. Accent Color Variants + * 7. Accessibility Fallbacks + */ + +/* ============================================================ + 1. PRIMITIVE TOKENS - 原始值(不直接使用於元件) + ============================================================ */ + +:root { + /* -------------------------------------------------- + 1.1 Color Primitives + -------------------------------------------------- */ + + /* Violet (Primary Brand) */ + --primitive-violet-50: 245 243 255; + --primitive-violet-100: 237 233 254; + --primitive-violet-200: 221 214 254; + --primitive-violet-300: 196 181 253; + --primitive-violet-400: 167 139 250; + --primitive-violet-500: 139 92 246; + --primitive-violet-600: 124 58 237; + --primitive-violet-700: 109 40 217; + --primitive-violet-800: 91 33 182; + --primitive-violet-900: 76 29 149; + --primitive-violet-950: 46 16 101; + + /* Slate (Neutral) */ + --primitive-slate-50: 248 250 252; + --primitive-slate-100: 241 245 249; + --primitive-slate-200: 226 232 240; + --primitive-slate-300: 203 213 225; + --primitive-slate-400: 148 163 184; + --primitive-slate-500: 100 116 139; + --primitive-slate-600: 71 85 105; + --primitive-slate-700: 51 65 85; + --primitive-slate-800: 30 41 59; + --primitive-slate-900: 15 23 42; + --primitive-slate-950: 2 6 23; + + /* Cyan (Secondary Accent) */ + --primitive-cyan-400: 34 211 238; + --primitive-cyan-500: 6 182 212; + --primitive-cyan-600: 8 145 178; + + /* Emerald (Success) */ + --primitive-emerald-50: 236 253 245; + --primitive-emerald-400: 52 211 153; + --primitive-emerald-500: 16 185 129; + --primitive-emerald-600: 5 150 105; + + /* Red (Error) */ + --primitive-red-50: 254 242 242; + --primitive-red-400: 248 113 113; + --primitive-red-500: 239 68 68; + --primitive-red-600: 220 38 38; + + /* Amber (Warning) */ + --primitive-amber-50: 255 251 235; + --primitive-amber-400: 251 191 36; + --primitive-amber-500: 245 158 11; + --primitive-amber-600: 217 119 6; + + /* -------------------------------------------------- + 1.2 Spacing Primitives (4px base unit) + -------------------------------------------------- */ + --spacing-0: 0px; + --spacing-1: 4px; + --spacing-2: 8px; + --spacing-3: 12px; + --spacing-4: 16px; + --spacing-5: 20px; + --spacing-6: 24px; + --spacing-8: 32px; + --spacing-10: 40px; + --spacing-12: 48px; + --spacing-16: 64px; + --spacing-20: 80px; + --spacing-24: 96px; + + /* -------------------------------------------------- + 1.3 Typography Primitives + -------------------------------------------------- */ + + /* Font Families */ + --font-sans: 'Noto Sans TC', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-mono: 'SF Mono', ui-monospace, 'Cascadia Code', 'Roboto Mono', Consolas, monospace; + + /* Font Sizes */ + --font-size-xs: 12px; + --font-size-sm: 14px; + --font-size-base: 16px; + --font-size-lg: 18px; + --font-size-xl: 20px; + --font-size-2xl: 24px; + --font-size-3xl: 30px; + --font-size-4xl: 36px; + --font-size-5xl: 48px; + + /* Font Weights */ + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + /* Line Heights */ + --line-height-tight: 1.25; + --line-height-snug: 1.375; + --line-height-normal: 1.5; + --line-height-relaxed: 1.625; + + /* Letter Spacing */ + --letter-spacing-tighter: -0.05em; + --letter-spacing-tight: -0.025em; + --letter-spacing-normal: 0em; + --letter-spacing-wide: 0.025em; + + /* -------------------------------------------------- + 1.4 Border Radius Primitives + -------------------------------------------------- */ + --radius-none: 0px; + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-2xl: 24px; + --radius-3xl: 32px; + --radius-full: 9999px; + + /* -------------------------------------------------- + 1.5 Shadow Primitives + -------------------------------------------------- */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.04); + --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.05); + + /* -------------------------------------------------- + 1.6 Motion Primitives + -------------------------------------------------- */ + --duration-instant: 0ms; + --duration-fast: 100ms; + --duration-normal: 200ms; + --duration-slow: 300ms; + --duration-slower: 500ms; + + --easing-linear: linear; + --easing-ease-out: cubic-bezier(0, 0, 0.2, 1); + --easing-ease-in: cubic-bezier(0.4, 0, 1, 1); + --easing-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --easing-spring: cubic-bezier(0.34, 1.56, 0.64, 1); + + /* -------------------------------------------------- + 1.7 Z-Index Scale + -------------------------------------------------- */ + --z-base: 0; + --z-dropdown: 1000; + --z-sticky: 1100; + --z-fixed: 1200; + --z-drawer: 1300; + --z-modal: 1400; + --z-popover: 1500; + --z-toast: 1600; + --z-tooltip: 1700; +} + +/* ============================================================ + 2. SEMANTIC TOKENS - 語意值(在元件中使用) + ============================================================ */ + +:root { + /* -------------------------------------------------- + 2.1 Background Colors + -------------------------------------------------- */ + --color-bg-primary: rgb(var(--primitive-slate-50)); + --color-bg-secondary: rgb(var(--primitive-slate-100)); + --color-bg-tertiary: rgb(var(--primitive-slate-200)); + --color-bg-inverse: rgb(var(--primitive-slate-900)); + + /* -------------------------------------------------- + 2.2 Surface Colors + -------------------------------------------------- */ + --color-surface-default: rgb(255 255 255); + --color-surface-elevated: rgb(255 255 255); + --color-surface-overlay: rgba(255, 255, 255, 0.95); + + /* -------------------------------------------------- + 2.3 Text Colors + -------------------------------------------------- */ + --color-text-primary: rgb(var(--primitive-slate-900)); + --color-text-secondary: rgb(var(--primitive-slate-600)); + --color-text-tertiary: rgb(var(--primitive-slate-500)); + --color-text-disabled: rgb(var(--primitive-slate-400)); + --color-text-inverse: rgb(255 255 255); + --color-text-link: rgb(var(--primitive-violet-600)); + --color-text-link-hover: rgb(var(--primitive-violet-700)); + + /* -------------------------------------------------- + 2.4 Border Colors + -------------------------------------------------- */ + --color-border-default: rgb(var(--primitive-slate-200)); + --color-border-strong: rgb(var(--primitive-slate-300)); + --color-border-focus: rgb(var(--primitive-violet-500)); + + /* -------------------------------------------------- + 2.5 Accent Colors + -------------------------------------------------- */ + --color-accent-primary: rgb(var(--primitive-violet-600)); + --color-accent-primary-hover: rgb(var(--primitive-violet-700)); + --color-accent-primary-active: rgb(var(--primitive-violet-800)); + --color-accent-primary-subtle: rgba(124, 58, 237, 0.12); + --color-accent-secondary: rgb(var(--primitive-cyan-500)); + + /* -------------------------------------------------- + 2.6 Status Colors + -------------------------------------------------- */ + --color-status-success: rgb(var(--primitive-emerald-500)); + --color-status-success-bg: rgb(var(--primitive-emerald-50)); + --color-status-warning: rgb(var(--primitive-amber-500)); + --color-status-warning-bg: rgb(var(--primitive-amber-50)); + --color-status-error: rgb(var(--primitive-red-500)); + --color-status-error-bg: rgb(var(--primitive-red-50)); + --color-status-info: rgb(var(--primitive-cyan-500)); + --color-status-info-bg: rgba(6, 182, 212, 0.1); +} + +/* ============================================================ + 3. GLASS EFFECT TOKENS - 液態玻璃效果 + ============================================================ */ + +:root { + /* -------------------------------------------------- + 3.1 Blur Values + -------------------------------------------------- */ + --glass-blur-none: 0px; + --glass-blur-sm: 8px; + --glass-blur-md: 16px; + --glass-blur-lg: 24px; + --glass-blur-xl: 32px; + --glass-blur-2xl: 40px; + + /* -------------------------------------------------- + 3.2 Opacity Values + -------------------------------------------------- */ + --glass-opacity-subtle: 0.05; + --glass-opacity-light: 0.08; + --glass-opacity-medium: 0.12; + --glass-opacity-heavy: 0.18; + --glass-opacity-solid: 0.85; + + /* -------------------------------------------------- + 3.3 Border Colors (Highlight) + -------------------------------------------------- */ + --glass-border-light: rgba(255, 255, 255, 0.18); + --glass-border-medium: rgba(255, 255, 255, 0.25); + --glass-border-strong: rgba(255, 255, 255, 0.35); + + /* -------------------------------------------------- + 3.4 Pre-composed Glass Surfaces + -------------------------------------------------- */ + --glass-surface-base: rgba(255, 255, 255, 0.08); + --glass-surface-elevated: rgba(255, 255, 255, 0.12); + --glass-surface-overlay: rgba(255, 255, 255, 0.18); + + /* -------------------------------------------------- + 3.5 Glass Shadows + -------------------------------------------------- */ + --glass-shadow-glow: 0 0 40px rgba(124, 58, 237, 0.15); + --glass-shadow-inner: inset 0 1px 1px rgba(255, 255, 255, 0.1); +} + +/* ============================================================ + 4. COMPONENT TOKENS - 元件專用 + ============================================================ */ + +:root { + /* -------------------------------------------------- + 4.1 Navigation Sizes (Material Design 3) + -------------------------------------------------- */ + --nav-tab-bar-height: 56px; + --nav-rail-width: 80px; + --nav-sidebar-width: 256px; + --nav-sidebar-collapsed: 72px; + --nav-top-bar-height: 64px; + + /* -------------------------------------------------- + 4.2 Touch Targets (WCAG 2.2) + -------------------------------------------------- */ + --touch-target-min: 44px; + --touch-target-comfortable: 48px; + + /* -------------------------------------------------- + 4.3 Icon Sizes + -------------------------------------------------- */ + --icon-xs: 12px; + --icon-sm: 16px; + --icon-md: 20px; + --icon-lg: 24px; + --icon-xl: 32px; + + /* -------------------------------------------------- + 4.4 Button Heights + -------------------------------------------------- */ + --button-height-sm: 32px; + --button-height-md: 40px; + --button-height-lg: 48px; + + /* -------------------------------------------------- + 4.5 Input Heights + -------------------------------------------------- */ + --input-height-sm: 36px; + --input-height-md: 44px; + --input-height-lg: 52px; +} + +/* ============================================================ + 5. DARK THEME - 深色主題覆蓋 + ============================================================ */ + +[data-theme='dark'] { + /* Background */ + --color-bg-primary: rgb(var(--primitive-slate-950)); + --color-bg-secondary: rgb(var(--primitive-slate-900)); + --color-bg-tertiary: rgb(var(--primitive-slate-800)); + --color-bg-inverse: rgb(var(--primitive-slate-50)); + + /* Surface */ + --color-surface-default: rgb(var(--primitive-slate-900)); + --color-surface-elevated: rgb(var(--primitive-slate-800)); + --color-surface-overlay: rgba(30, 41, 59, 0.95); + + /* Text */ + --color-text-primary: rgb(var(--primitive-slate-50)); + --color-text-secondary: rgb(var(--primitive-slate-300)); + --color-text-tertiary: rgb(var(--primitive-slate-400)); + --color-text-disabled: rgb(var(--primitive-slate-500)); + --color-text-inverse: rgb(var(--primitive-slate-900)); + --color-text-link: rgb(var(--primitive-violet-400)); + --color-text-link-hover: rgb(var(--primitive-violet-300)); + + /* Border */ + --color-border-default: rgb(var(--primitive-slate-700)); + --color-border-strong: rgb(var(--primitive-slate-600)); + --color-border-focus: rgb(var(--primitive-violet-400)); + + /* Accent */ + --color-accent-primary: rgb(var(--primitive-violet-400)); + --color-accent-primary-hover: rgb(var(--primitive-violet-300)); + --color-accent-primary-active: rgb(var(--primitive-violet-200)); + --color-accent-primary-subtle: rgba(167, 139, 250, 0.15); + + /* Status */ + --color-status-success: rgb(var(--primitive-emerald-400)); + --color-status-success-bg: rgba(52, 211, 153, 0.15); + --color-status-warning: rgb(var(--primitive-amber-400)); + --color-status-warning-bg: rgba(251, 191, 36, 0.15); + --color-status-error: rgb(var(--primitive-red-400)); + --color-status-error-bg: rgba(248, 113, 113, 0.15); + --color-status-info: rgb(var(--primitive-cyan-400)); + --color-status-info-bg: rgba(34, 211, 238, 0.15); + + /* Glass (Dark Mode) */ + --glass-border-light: rgba(255, 255, 255, 0.1); + --glass-border-medium: rgba(255, 255, 255, 0.15); + --glass-border-strong: rgba(255, 255, 255, 0.2); + --glass-surface-base: rgba(255, 255, 255, 0.05); + --glass-surface-elevated: rgba(255, 255, 255, 0.08); + --glass-surface-overlay: rgba(255, 255, 255, 0.12); + --glass-shadow-glow: 0 0 40px rgba(167, 139, 250, 0.2); + + /* Shadows (Dark) */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.3); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3); +} + +/* ============================================================ + 6. ACCENT COLOR VARIANTS - 配色方案 + ============================================================ */ + +/* Ocean Blue */ +[data-accent='ocean'] { + --primitive-accent-400: 56 189 248; + --primitive-accent-500: 14 165 233; + --primitive-accent-600: 2 132 199; + --primitive-accent-700: 3 105 161; + --primitive-accent-800: 7 89 133; + + --color-accent-primary: rgb(var(--primitive-accent-600)); + --color-accent-primary-hover: rgb(var(--primitive-accent-700)); + --color-accent-primary-active: rgb(var(--primitive-accent-800)); + --color-accent-primary-subtle: rgba(14, 165, 233, 0.12); + --glass-shadow-glow: 0 0 40px rgba(14, 165, 233, 0.15); +} + +[data-theme='dark'][data-accent='ocean'] { + --color-accent-primary: rgb(var(--primitive-accent-400)); + --color-accent-primary-hover: rgb(56 189 248); + --color-accent-primary-active: rgb(125 211 252); + --color-accent-primary-subtle: rgba(56, 189, 248, 0.15); + --glass-shadow-glow: 0 0 40px rgba(56, 189, 248, 0.2); +} + +/* Forest Green */ +[data-accent='forest'] { + --primitive-accent-400: 74 222 128; + --primitive-accent-500: 34 197 94; + --primitive-accent-600: 22 163 74; + --primitive-accent-700: 21 128 61; + --primitive-accent-800: 22 101 52; + + --color-accent-primary: rgb(var(--primitive-accent-600)); + --color-accent-primary-hover: rgb(var(--primitive-accent-700)); + --color-accent-primary-active: rgb(var(--primitive-accent-800)); + --color-accent-primary-subtle: rgba(34, 197, 94, 0.12); + --glass-shadow-glow: 0 0 40px rgba(34, 197, 94, 0.15); +} + +[data-theme='dark'][data-accent='forest'] { + --color-accent-primary: rgb(var(--primitive-accent-400)); + --color-accent-primary-hover: rgb(134 239 172); + --color-accent-primary-active: rgb(187 247 208); + --color-accent-primary-subtle: rgba(74, 222, 128, 0.15); + --glass-shadow-glow: 0 0 40px rgba(74, 222, 128, 0.2); +} + +/* Sunset Orange */ +[data-accent='sunset'] { + --primitive-accent-400: 251 146 60; + --primitive-accent-500: 249 115 22; + --primitive-accent-600: 234 88 12; + --primitive-accent-700: 194 65 12; + --primitive-accent-800: 154 52 18; + + --color-accent-primary: rgb(var(--primitive-accent-600)); + --color-accent-primary-hover: rgb(var(--primitive-accent-700)); + --color-accent-primary-active: rgb(var(--primitive-accent-800)); + --color-accent-primary-subtle: rgba(249, 115, 22, 0.12); + --glass-shadow-glow: 0 0 40px rgba(249, 115, 22, 0.15); +} + +[data-theme='dark'][data-accent='sunset'] { + --color-accent-primary: rgb(var(--primitive-accent-400)); + --color-accent-primary-hover: rgb(253 186 116); + --color-accent-primary-active: rgb(254 215 170); + --color-accent-primary-subtle: rgba(251, 146, 60, 0.15); + --glass-shadow-glow: 0 0 40px rgba(251, 146, 60, 0.2); +} + +/* ============================================================ + 7. ACCESSIBILITY FALLBACKS - 無障礙降級 + ============================================================ */ + +/* Reduced Transparency - 降低透明度 */ +@media (prefers-reduced-transparency: reduce) { + :root { + --glass-blur-sm: 0px; + --glass-blur-md: 0px; + --glass-blur-lg: 0px; + --glass-blur-xl: 0px; + --glass-blur-2xl: 0px; + + --glass-surface-base: var(--color-surface-default); + --glass-surface-elevated: var(--color-surface-elevated); + --glass-surface-overlay: var(--color-surface-overlay); + + --glass-border-light: var(--color-border-default); + --glass-border-medium: var(--color-border-strong); + --glass-border-strong: var(--color-border-strong); + } +} + +/* High Contrast Mode - 高對比模式 */ +[data-contrast='high'] { + --color-text-primary: rgb(0 0 0); + --color-text-secondary: rgb(0 0 0); + --color-border-default: rgb(0 0 0); + --color-border-strong: rgb(0 0 0); + --color-accent-primary: rgb(0 0 139); + + --glass-surface-base: rgb(255 255 255); + --glass-surface-elevated: rgb(255 255 255); + --glass-border-light: rgb(0 0 0); + --glass-border-medium: rgb(0 0 0); +} + +[data-theme='dark'][data-contrast='high'] { + --color-text-primary: rgb(255 255 255); + --color-text-secondary: rgb(255 255 255); + --color-border-default: rgb(255 255 255); + --color-border-strong: rgb(255 255 255); + --color-accent-primary: rgb(173 216 230); + + --glass-surface-base: rgb(0 0 0); + --glass-surface-elevated: rgb(0 0 0); + --glass-border-light: rgb(255 255 255); + --glass-border-medium: rgb(255 255 255); +} + +/* Reduced Motion - 減少動態效果 */ +@media (prefers-reduced-motion: reduce) { + :root { + --duration-instant: 0ms; + --duration-fast: 0ms; + --duration-normal: 0ms; + --duration-slow: 0ms; + --duration-slower: 0ms; + } + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* ============================================================ + 8. UTILITY CLASSES - 工具類別 + ============================================================ */ + +/* Glass Card Utilities */ +.glass-base { + background: var(--glass-surface-base); + backdrop-filter: blur(var(--glass-blur-md)); + -webkit-backdrop-filter: blur(var(--glass-blur-md)); + border: 1px solid var(--glass-border-light); + border-radius: var(--radius-xl); +} + +.glass-elevated { + background: var(--glass-surface-elevated); + backdrop-filter: blur(var(--glass-blur-lg)); + -webkit-backdrop-filter: blur(var(--glass-blur-lg)); + border: 1px solid var(--glass-border-medium); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); +} + +.glass-overlay { + background: var(--glass-surface-overlay); + backdrop-filter: blur(var(--glass-blur-xl)); + -webkit-backdrop-filter: blur(var(--glass-blur-xl)); + border: 1px solid var(--glass-border-strong); + border-radius: var(--radius-2xl); + box-shadow: var(--shadow-xl); +} + +/* Focus Ring Utility */ +.focus-ring { + outline: none; +} + +.focus-ring:focus-visible { + outline: 2px solid var(--color-border-focus); + outline-offset: 2px; +} + +/* Safe Area Insets (for notched devices) */ +.safe-area-inset-bottom { + padding-bottom: env(safe-area-inset-bottom, 0px); +} + +.safe-area-inset-top { + padding-top: env(safe-area-inset-top, 0px); +} diff --git a/docs/dev/006_design_modernization_spec.md b/docs/dev/006_design_modernization_spec.md new file mode 100644 index 00000000..21830e9e --- /dev/null +++ b/docs/dev/006_design_modernization_spec.md @@ -0,0 +1,672 @@ +# Ratewise Design Modernization Specification + +> **版本**: 2.0.0 +> **建立日期**: 2025-01-17 +> **狀態**: 📋 規劃中 +> **依據**: W3C DTCG 2025.10, Apple Liquid Glass, Material Design 3 + +--- + +## 目錄 + +1. [設計標準引用](#1-設計標準引用) +2. [Token 架構總覽](#2-token-架構總覽) +3. [色彩系統](#3-色彩系統) +4. [液態玻璃規格](#4-液態玻璃規格) +5. [排版系統](#5-排版系統) +6. [間距與尺寸](#6-間距與尺寸) +7. [三端最佳實踐排版](#7-三端最佳實踐排版) +8. [元件規格](#8-元件規格) +9. [無障礙設計](#9-無障礙設計) +10. [實作檢查清單](#10-實作檢查清單) + +--- + +## 1. 設計標準引用 + +本規格基於以下權威來源: + +| 標準 | 版本 | 用途 | +|------|------|------| +| [W3C Design Tokens](https://www.designtokens.org/tr/drafts/format/) | 2025.10 | Token JSON 格式規範 | +| [Apple Liquid Glass](https://developer.apple.com/documentation/TechnologyOverviews/liquid-glass) | iOS 26 | 玻璃效果視覺語言 | +| [Material Design 3](https://m3.material.io/) | 2025 | 導覽元件與互動模式 | +| [WCAG 2.2](https://www.w3.org/WAI/WCAG22/quickref/) | Level AA | 無障礙標準 | +| [Tailwind CSS](https://tailwindcss.com/docs/responsive-design) | v4 | 響應式斷點 | + +--- + +## 2. Token 架構總覽 + +### 2.1 三層架構 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Component Tokens │ +│ (button, card, navigation, input, modal, chart) │ +├─────────────────────────────────────────────────────────┤ +│ Semantic Tokens │ +│ (bg, surface, text, border, accent, status) │ +├─────────────────────────────────────────────────────────┤ +│ Primitive Tokens │ +│ (violet-600, slate-900, 16px, 400, etc.) │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 檔案結構 + +``` +apps/ratewise/src/ +├── config/ +│ ├── design-tokens.tokens.json # W3C DTCG 格式 (SSOT) +│ ├── design-tokens.ts # TypeScript 定義 (現有) +│ └── component-tokens.ts # 元件層級 tokens (新增) +├── styles/ +│ └── tokens.css # CSS Variables 實作 (新增) +└── index.css # Tailwind 進入點 +``` + +--- + +## 3. 色彩系統 + +### 3.1 主色與強調色 + +| Token | Light Mode | Dark Mode | 用途 | +|-------|------------|-----------|------| +| `accent-primary` | Violet 600 `#7c3aed` | Violet 400 `#a78bfa` | CTA、連結、焦點 | +| `accent-secondary` | Cyan 500 `#06b6d4` | Cyan 400 `#22d3ee` | 次要強調 | + +### 3.2 配色方案(可切換) + +| 方案 | Primary | 描述 | +|------|---------|------| +| `violet` (預設) | `#7c3aed` | 品牌紫羅蘭 | +| `ocean` | `#0ea5e9` | 海洋藍 | +| `forest` | `#16a34a` | 森林綠 | +| `sunset` | `#ea580c` | 日落橙 | + +### 3.3 狀態色 + +| 狀態 | 顏色 | 背景 | 用途 | +|------|------|------|------| +| Success | Emerald 500 | Emerald 50 | 正向漲幅、成功訊息 | +| Warning | Amber 500 | Amber 50 | 警告提示 | +| Error | Red 500 | Red 50 | 負向跌幅、錯誤訊息 | +| Info | Cyan 500 | Cyan 50 | 資訊提示 | + +--- + +## 4. 液態玻璃規格 + +### 4.1 Apple Liquid Glass 原則 + +> "Content First - 將最重要的內容置於視覺焦點" +> "Use Color Sparingly - 節制使用色彩以確保可讀性" +> "Avoid Overuse - 克制地應用效果" + +### 4.2 玻璃表面層級 + +| 層級 | Blur | Opacity | Border | 用途 | +|------|------|---------|--------|------| +| `base` | 16px | 8% | 18% white | 卡片、列表容器 | +| `elevated` | 24px | 12% | 25% white | 浮動元素、強調卡片 | +| `overlay` | 32px | 18% | 35% white | Modal、Drawer | + +### 4.3 CSS 實作 + +```css +.glass-base { + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 16px; +} +``` + +### 4.4 降級策略 + +```css +/* 偏好降低透明度的使用者 */ +@media (prefers-reduced-transparency: reduce) { + .glass-base { + background: var(--color-surface-default); + backdrop-filter: none; + border: 1px solid var(--color-border-default); + } +} +``` + +--- + +## 5. 排版系統 + +### 5.1 字體堆疊 + +```css +--font-sans: 'Noto Sans TC', system-ui, sans-serif; +--font-mono: 'SF Mono', ui-monospace, monospace; +``` + +### 5.2 字級比例 + +| Token | Size | Weight | Line Height | 用途 | +|-------|------|--------|-------------|------| +| `display-lg` | 48px | Bold | 1.25 | 大標題 | +| `display-md` | 36px | Bold | 1.25 | 頁面標題 | +| `heading-lg` | 24px | Semibold | 1.375 | 區塊標題 | +| `heading-md` | 20px | Semibold | 1.375 | 卡片標題 | +| `heading-sm` | 18px | Semibold | 1.375 | 小標題 | +| `body-lg` | 18px | Normal | 1.625 | 大段落 | +| `body-md` | 16px | Normal | 1.5 | 一般內文 | +| `body-sm` | 14px | Normal | 1.5 | 小內文 | +| `caption` | 12px | Normal | 1.5 | 說明文字 | +| `rate-display` | 30px | Bold (Mono) | 1.25 | 匯率數字 | + +--- + +## 6. 間距與尺寸 + +### 6.1 間距比例(4px 基準) + +| Token | Value | 常見用途 | +|-------|-------|----------| +| `spacing-1` | 4px | 緊密間距 | +| `spacing-2` | 8px | 元素內間距 | +| `spacing-3` | 12px | 小區塊間距 | +| `spacing-4` | 16px | 標準間距 | +| `spacing-6` | 24px | 區塊間距 | +| `spacing-8` | 32px | 大區塊間距 | +| `spacing-12` | 48px | 頁面間距 | + +### 6.2 圓角比例 + +| Token | Value | 用途 | +|-------|-------|------| +| `radius-sm` | 4px | 小元素、標籤 | +| `radius-md` | 8px | 按鈕、輸入框 | +| `radius-lg` | 12px | 卡片 | +| `radius-xl` | 16px | 大卡片、導覽 | +| `radius-2xl` | 24px | Modal | +| `radius-full` | 9999px | 圓形 | + +### 6.3 導覽尺寸(Material Design 3) + +| 元件 | 尺寸 | 來源 | +|------|------|------| +| Bottom Tab Bar 高度 | 56px | MD3 Navigation Bar | +| Tab 項目最小寬度 | 64px | MD3 Guidelines | +| Tab 項目最大寬度 | 96px | MD3 Guidelines | +| Nav Rail 寬度 | 80px | MD3 Navigation Rail | +| Sidebar 寬度 | 256px | 自訂 | +| Top Bar 高度 | 64px | 自訂 | + +--- + +## 7. 三端最佳實踐排版 + +### 7.1 響應式斷點 + +| 斷點 | 寬度 | 裝置 | 導覽模式 | +|------|------|------|----------| +| `xs` | 0-374px | 小型手機 | Bottom Tab | +| `sm` | 375-767px | 手機 | Bottom Tab | +| `md` | 768-1023px | 平板 | Nav Rail | +| `lg` | 1024-1279px | 小桌面 | Sidebar | +| `xl` | 1280px+ | 大桌面 | Sidebar | + +--- + +### 7.2 Mobile 排版(Bottom Tab Bar) + +#### Layout M1: 匯率首頁 - 卡片化 KPI + +``` ++---------------------------+ +| [StatusBar] 🌙 ⚙ | ← Safe Area Top +|---------------------------| +| (Glass) 主匯率卡片 | +| TWD → USD | +| ┌─────────────────────┐ | +| │ 31.2500 │ | ← rate-display token +| │ ▲ +0.12% today │ | +| └─────────────────────┘ | +|---------------------------| +| (Glass) 快速轉換 | +| ┌──────────┐ → ┌──────┐ | +| │ 1,000 │ │31.25 │ | +| │ TWD ▼ │ │ USD │ | +| └──────────┘ └──────┘ | +|---------------------------| +| (Glass) 收藏貨幣 | +| [JPY] [EUR] [GBP] [+] | ← chip tokens +|---------------------------| +| (Glass) 7日趨勢 | +| ╭──────────────────────╮ | +| │ 📈 Mini Chart │ | +| ╰──────────────────────╯ | +|---------------------------| +| (空間) | ++---------------------------+ +| [Tab] 首頁 | 列表 | 設定 | ← 56px height ++---------------------------+ +| [SafeArea] | ← env(safe-area-inset-bottom) ++---------------------------+ +``` + +**設計決策**: +- ✅ Material Design 3: 3-5 個 Tab 項目最佳 +- ✅ 主要 KPI 置頂,符合「Content First」原則 +- ✅ 快速轉換區提供即時互動 +- ✅ Safe Area 處理 iPhone notch + +--- + +#### Layout M2: 貨幣列表頁 - 搜尋 + Chips 篩選 + +``` ++---------------------------+ +| [Top] 貨幣列表 🔍 | +| ┌───────────────────────┐ | +| │ 搜尋貨幣... │ | ← input token +| └───────────────────────┘ | +| [全部] [收藏★] [熱門🔥] | ← chip tonal variant +|---------------------------| +| (Glass) List Container | +| ┌─────────────────────┐ | +| │ 🇺🇸 USD 美元 │ | +| │ 31.25 TWD ▲0.12%│ | ← list.currencyItem +| ├─────────────────────┤ | +| │ 🇪🇺 EUR 歐元 │ | +| │ 34.18 TWD ▼0.05%│ | +| ├─────────────────────┤ | +| │ 🇯🇵 JPY 日圓 │ | +| │ 0.21 TWD ▲0.08%│ | +| ├─────────────────────┤ | +| │ ... 更多 ... │ | +| └─────────────────────┘ | ++---------------------------+ +| [Tab] 首頁 | 列表 | 設定 | ++---------------------------+ +``` + +**設計決策**: +- ✅ 搜尋框置頂,快速找到貨幣 +- ✅ Chips 篩選取代複雜下拉選單 +- ✅ 列表項高度 64px 符合觸控目標 44px+ + +--- + +#### Layout M3: 匯率詳情頁 - 沉浸式圖表 + +``` ++---------------------------+ +| [Top] < USD/TWD ★ ⋯ | ← 返回 + 收藏 + 更多 +|---------------------------| +| (Glass) Header Card | +| 美元 / 新台幣 | +| ┌─────────────────────┐ | +| │ 31.2500 │ | ← rate-display 48px +| │ ▲ +0.0375 (+0.12%) │ | +| │ 更新於 14:30 │ | +| └─────────────────────┘ | +|---------------------------| +| (Glass) Chart Container | +| ┌─────────────────────┐ | +| │ │ | +| │ Interactive │ | ← chart.container +| │ Chart │ | +| │ │ | +| └─────────────────────┘ | +| [1D] [1W] [1M] [3M] [1Y] | ← chart.timeRange +|---------------------------| +| (Glass) Statistics | +| 最高 31.50 │ 最低 30.80 | +| 平均 31.15 │ 波動 2.3% | +|---------------------------| +| | ++---------------------------+ +| [Action Bar] | +| [ 設為預設 ] [ 換算 ] | ← 固定底部操作 ++---------------------------+ +``` + +**設計決策**: +- ✅ 圖表佔主視覺,支援手勢縮放 +- ✅ 時間範圍選擇器內嵌 +- ✅ 底部操作列固定,不需捲動找按鈕 + +--- + +#### Layout M4: 設定頁 - 分群列表 + +``` ++---------------------------+ +| [Top] 設定 | +|---------------------------| +| 外觀 | ← settings.sectionTitle +| (Glass) Group | +| ┌─────────────────────┐ | +| │ 主題風格 Glass >│ | ← settings.item +| ├─────────────────────┤ | +| │ 配色方案 紫羅蘭 >│ | +| ├─────────────────────┤ | +| │ 玻璃強度 75% >│ | +| ├─────────────────────┤ | +| │ 深色模式 ○ │ | ← Toggle +| └─────────────────────┘ | +|---------------------------| +| 匯率偏好 | +| (Glass) Group | +| ┌─────────────────────┐ | +| │ 預設匯率類型 即期 >│ | +| ├─────────────────────┤ | +| │ 預設來源幣 TWD >│ | +| └─────────────────────┘ | +|---------------------------| +| 資料 | +| (Glass) Group | +| ┌─────────────────────┐ | +| │ 匯出資料 >│ | +| ├─────────────────────┤ | +| │ 清除快取 >│ | +| └─────────────────────┘ | ++---------------------------+ +| [Tab] 首頁 | 列表 | 設定 | ++---------------------------+ +``` + +**設計決策**: +- ✅ 分群清晰,iOS 設定頁風格 +- ✅ 每項右側顯示當前值 +- ✅ 深色模式使用 Toggle 而非進入子頁 + +--- + +### 7.3 Tablet 排版(Nav Rail + Split View) + +#### Layout T1: Master-Detail 雙欄 + +``` ++--------------------------------------------------------+ +| [Top Bar] RateWise 🔍 🔔 👤 ⚙ | ++--------+-----------------------------------------------+ +| [Rail] | (Glass) Master List | (Glass) Detail Panel | +| | | | +| 🏠 | 搜尋... | USD / TWD | +| 首頁 | ─────────────────── | ════════════════ | +| | ┌──────────────────┐ | | +| 📋 | │ USD 31.25 ▲ │ ← | 31.2500 | +| 列表 | ├──────────────────┤ | ▲ +0.12% | +| | │ EUR 34.18 ▼ │ | | +| ⭐ | ├──────────────────┤ | ╭────────────────╮ | +| 收藏 | │ JPY 0.21 ▲ │ | │ │ | +| | ├──────────────────┤ | │ 趨勢圖表 │ | +| 📊 | │ GBP 39.82 ─ │ | │ │ | +| 趨勢 | └──────────────────┘ | ╰────────────────╯ | +| | | | +| ── | | [1D] [1W] [1M] [1Y] | +| | | | +| ⚙ | | 最高 31.50 最低 30.80| +| 設定 | | | +| | | [設為預設] [換算] | ++--------+------------------------+----------------------+ + 80px 40% 60% +``` + +**設計決策**: +- ✅ MD3 Nav Rail 80px 標準寬度 +- ✅ Master-Detail 模式減少跳頁 +- ✅ 點擊左欄項目,右欄即時更新 + +--- + +#### Layout T2: Dashboard 網格 + +``` ++--------------------------------------------------------+ +| [Top Bar] RateWise 🔍 🔔 👤 ⚙ | +| [Tabs] 總覽 | 趨勢 | 比較 | 提醒 | ++--------+-----------------------------------------------+ +| [Rail] | (Glass) KPI Card 1 | (Glass) KPI Card 2 | +| | USD 31.25 ▲ +0.12% | EUR 34.18 ▼ -0.05% | +| 🏠 |───────────────────────────────────────────────| +| | (Glass) Main Chart | +| 📋 | ╭──────────────────────────────────────────╮ | +| | │ │ | +| ⭐ | │ 多幣別比較圖表 │ | +| | │ │ | +| 📊 | ╰──────────────────────────────────────────╯ | +| |───────────────────────────────────────────────| +| ⚙ | (Glass) Favorites | (Glass) Recent | +| | JPY EUR GBP CNY + | TWD→USD 1000 | +| | | TWD→JPY 5000 | ++--------+-----------------------------------------------+ +``` + +**設計決策**: +- ✅ 頂部 Tabs 切換工作區 +- ✅ 網格布局最大化資訊密度 +- ✅ KPI 卡片並排方便比較 + +--- + +### 7.4 Desktop 排版(Sidebar + Multi-Column) + +#### Layout D1: 三欄資訊密度型 + +``` ++------------------------------------------------------------------------+ +| [Top Bar] 🔍 搜尋貨幣... 通知 🔔 帳號 👤 設定 ⚙ | ++-----------+-------------------------+----------------------------------+ +| [Sidebar] | (Glass) Currency List | (Glass) Detail Panel | +| | | | +| 📊 儀表板 | 搜尋... | USD / TWD | +| ─────────|───────────────────────── | ════════════════════════════ | +| 💱 換算 | ┌───────────────────┐ | | +| | │ USD 31.25 ▲ ★ │ | 31.2500 | +| 📋 列表 | ├───────────────────┤ | ▲ +0.0375 (+0.12%) | +| | │ EUR 34.18 ▼ │ | 更新於 2025/01/17 14:30 | +| ⭐ 收藏 | ├───────────────────┤ | | +| | │ JPY 0.21 ▲ ★ │ | ╭──────────────────────────────╮ | +| 📰 新聞 | ├───────────────────┤ | │ │ | +| | │ GBP 39.82 ─ │ | │ 互動式趨勢圖表 │ | +| 🔔 提醒 | ├───────────────────┤ | │ │ | +| | │ CNY 4.35 ▲ │ | ╰──────────────────────────────╯ | +| ─────────| │ ... │ | [1D] [1W] [1M] [3M] [1Y] | +| | └───────────────────┘ | | +| ⚙ 設定 | | ┌──────────────────────────────┐ | +| | | │ 開盤 31.20 │ 收盤 31.25 │ | +| | | │ 最高 31.50 │ 最低 31.10 │ | +| | | └──────────────────────────────┘ | +| | | | +| | | [設為預設] [加入收藏] [換算] | ++-----------+-------------------------+----------------------------------+ + 256px 320px 剩餘空間 +``` + +**設計決策**: +- ✅ 三欄設計:導覽、列表、詳情同時可見 +- ✅ Sidebar 可收合至 72px +- ✅ 鍵盤快捷鍵支援(↑↓ 切換、Enter 選擇) + +--- + +#### Layout D2: 設定頁 - 左側導覽 + 右側內容 + +``` ++------------------------------------------------------------------------+ +| [Top Bar] ← 返回 設定 | ++-----------+------------------------------------------------------------+ +| [Side] | (Glass) 外觀設定 | +| | | +| 外觀 > | 主題風格 | +| 通知 | ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ | +| 匯率偏好 | │ Light │ │ Dark │ │ Neon │ │Contrast │ | +| 資料 | │ ☀️ │ │ 🌙 │ │ ⚡ │ │ 👁️ │ | +| 關於 | └────●────┘ └─────────┘ └─────────┘ └─────────┘ | +| | | +| | 配色方案 | +| | ┌────┐ ┌────┐ ┌────┐ ┌────┐ | +| | │ ● │ │ ○ │ │ ○ │ │ ○ │ | +| | │紫 │ │藍 │ │綠 │ │橙 │ | +| | └────┘ └────┘ └────┘ └────┘ | +| | | +| | 玻璃效果強度 | +| | 關閉 ────────●──────────────────── 最強 | +| | | +| | (Glass) 即時預覽 | +| | ╭────────────────────────────────────────────────────────╮| +| | │ │| +| | │ 這是預覽卡片,會隨著上方設定即時變化 │| +| | │ USD / TWD 31.2500 ▲ +0.12% │| +| | │ │| +| | ╰────────────────────────────────────────────────────────╯| ++-----------+------------------------------------------------------------+ +``` + +**設計決策**: +- ✅ 視覺化主題選擇器 +- ✅ 即時預覽區域 +- ✅ 滑桿控制玻璃效果強度 + +--- + +## 8. 元件規格 + +### 8.1 Bottom Tab Bar + +| 屬性 | 值 | 來源 | +|------|------|------| +| 高度 | 56px | MD3 | +| 背景 | `glass-surface-base` | Liquid Glass | +| Blur | 24px | 自訂 | +| Item 最小寬度 | 64px | MD3 | +| Item 最大寬度 | 96px | MD3 | +| Icon 大小 | 24px | MD3 | +| Label 字級 | 12px | MD3 | +| Active Indicator | Pill 形狀, 32px 高 | MD3 | + +### 8.2 Glass Card + +| 屬性 | Base | Elevated | Overlay | +|------|------|----------|---------| +| Background | `rgba(255,255,255,0.08)` | `0.12` | `0.18` | +| Blur | 16px | 24px | 32px | +| Border | `rgba(255,255,255,0.18)` | `0.25` | `0.35` | +| Radius | 16px | 16px | 24px | +| Shadow | `md` | `lg` | `xl` | + +### 8.3 Button + +| Size | Height | Padding X | Font Size | Radius | +|------|--------|-----------|-----------|--------| +| `sm` | 32px | 12px | 14px | 8px | +| `md` | 40px | 16px | 14px | 12px | +| `lg` | 48px | 24px | 16px | 12px | + +| Variant | Background | Text | Border | +|---------|------------|------|--------| +| `primary` | `accent-primary` | `text-inverse` | none | +| `secondary` | transparent | `accent-primary` | `accent-primary` | +| `ghost` | transparent | `text-secondary` | none | +| `glass` | `rgba(255,255,255,0.1)` | `text-primary` | `rgba(255,255,255,0.2)` | + +--- + +## 9. 無障礙設計 + +### 9.1 對比度要求(WCAG 2.2 AA) + +| 元素 | 最小對比度 | 驗證 | +|------|------------|------| +| 正文文字 | 4.5:1 | ✅ slate-900 on slate-50 = 12.6:1 | +| 大標題 (18px+) | 3:1 | ✅ | +| 互動元素 | 3:1 | ✅ violet-600 on white = 4.5:1 | +| 玻璃上文字 | 4.5:1 | ⚠️ 需要測試不同背景 | + +### 9.2 觸控目標 + +| 規範 | 最小尺寸 | +|------|----------| +| WCAG 2.2 | 24×24px | +| Apple HIG | 44×44px | +| 本系統採用 | 44×44px (min), 48×48px (comfortable) | + +### 9.3 降級支援 + +```css +/* 偏好降低透明度 */ +@media (prefers-reduced-transparency: reduce) { ... } + +/* 偏好減少動態 */ +@media (prefers-reduced-motion: reduce) { ... } + +/* 高對比模式 */ +[data-contrast="high"] { ... } +``` + +--- + +## 10. 實作檢查清單 + +### Phase 1: Token 基礎建設 ✅ + +- [x] 建立 W3C DTCG 格式 JSON (`design-tokens.tokens.json`) +- [x] 建立 CSS Variables (`tokens.css`) +- [x] 建立元件 Tokens (`component-tokens.ts`) +- [ ] 更新 Tailwind 設定引用新 tokens +- [ ] 遷移現有硬編碼值 + +### Phase 2: 導覽重構 + +- [ ] 實作 `BottomTabBar` 元件 (Mobile) +- [ ] 實作 `NavRail` 元件 (Tablet) +- [ ] 實作 `Sidebar` 元件 (Desktop) +- [ ] 實作響應式導覽切換邏輯 +- [ ] 加入路由高亮狀態 + +### Phase 3: 玻璃效果 + +- [ ] 實作 `GlassCard` 元件 +- [ ] 實作 `GlassSurface` 元件 +- [ ] 加入降級策略 +- [ ] 效能測試與優化 + +### Phase 4: 設定頁 + +- [ ] 建立 `/settings` 路由 +- [ ] 實作 `ThemePicker` 元件 +- [ ] 實作 `ColorPicker` 元件 +- [ ] 實作即時預覽功能 +- [ ] localStorage 持久化 + +### Phase 5: 驗收 + +- [ ] 三端響應式測試 +- [ ] 無障礙測試 (axe-core) +- [ ] 效能測試 (Lighthouse) +- [ ] 對比度驗證 + +--- + +## 參考資源 + +### 權威來源 + +- [W3C Design Tokens Format Module 2025.10](https://www.designtokens.org/tr/drafts/format/) +- [Apple Liquid Glass Documentation](https://developer.apple.com/documentation/TechnologyOverviews/liquid-glass) +- [Material Design 3 Navigation Bar](https://m3.material.io/components/navigation-bar/guidelines) +- [WCAG 2.2 Quick Reference](https://www.w3.org/WAI/WCAG22/quickref/) + +### 研究文章 + +- [Glassmorphism in 2025: Apple's Liquid Glass](https://www.everydayux.net/glassmorphism-apple-liquid-glass-interface-design/) +- [Bottom Navigation Bar Design Best Practices](https://uxdworld.com/bottom-tab-bar-navigation-design-best-practices/) +- [Responsive Design Breakpoints 2025](https://www.browserstack.com/guide/responsive-design-breakpoints) + +--- + +**最後更新**: 2025-01-17 +**維護者**: Claude Code From 0bf3b6d3853207046b4ebb886232cfe51f080c87 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Jan 2026 10:42:07 +0000 Subject: [PATCH 2/7] docs(design): expand spec with grid, forms, animations, states Add comprehensive UI/UX specifications: Grid System (Section 7): - 4px base unit (iOS 8pt, Android 4dp compatible) - Mobile 4-col, Tablet 8-col, Desktop 12-col grids - Margins 16-24px, Gutters 16-24px - Android Window Size Classes Card Design (Section 9.4): - Shadow elevation system (sm/md/lg/xl) - Hover animation specs (200ms, translateY) - Border radius consistency rules Form Design (Section 10): - Input height 44-52px - Validation: Reward Early, Punish Late - Currency input 56px with embedded selector Micro-interactions (Section 11): - 3-second rule for animations - Button 100-150ms, Modal 300-400ms - Ripple effect for mobile - prefers-reduced-motion support Skeleton Loading (Section 12): - Shimmer effect 1.5s cycle - Layout matching principle - Cross-fade transitions Empty/Error States (Section 13): - 5 empty state types - Ratewise-specific templates - CTA requirement Gestures (Section 14): - Touch targets 44x44px min - Pull-to-refresh 60px threshold - Swipe actions with fallbacks Financial Visualization (Section 15): - Green/Red for trends - Multi-currency chart colors - Number formatting standards Sources: - Android Developers, Material Design 3 - NN/g, LogRocket UX Design - Mobbin, UXPin --- docs/dev/006_design_modernization_spec.md | 611 +++++++++++++++++++++- 1 file changed, 598 insertions(+), 13 deletions(-) diff --git a/docs/dev/006_design_modernization_spec.md b/docs/dev/006_design_modernization_spec.md index 21830e9e..0a068b64 100644 --- a/docs/dev/006_design_modernization_spec.md +++ b/docs/dev/006_design_modernization_spec.md @@ -1,9 +1,10 @@ # Ratewise Design Modernization Specification -> **版本**: 2.0.0 +> **版本**: 2.1.0 > **建立日期**: 2025-01-17 +> **最後更新**: 2025-01-17 > **狀態**: 📋 規劃中 -> **依據**: W3C DTCG 2025.10, Apple Liquid Glass, Material Design 3 +> **依據**: W3C DTCG 2025.10, Apple Liquid Glass, Material Design 3, Android/iOS HIG --- @@ -15,10 +16,17 @@ 4. [液態玻璃規格](#4-液態玻璃規格) 5. [排版系統](#5-排版系統) 6. [間距與尺寸](#6-間距與尺寸) -7. [三端最佳實踐排版](#7-三端最佳實踐排版) -8. [元件規格](#8-元件規格) -9. [無障礙設計](#9-無障礙設計) -10. [實作檢查清單](#10-實作檢查清單) +7. [Grid 系統與 Layout](#7-grid-系統與-layout) +8. [三端最佳實踐排版](#8-三端最佳實踐排版) +9. [元件規格](#9-元件規格) +10. [表單與輸入設計](#10-表單與輸入設計) +11. [Micro-interactions 與動效](#11-micro-interactions-與動效) +12. [載入狀態與 Skeleton](#12-載入狀態與-skeleton) +13. [Empty States 與 Error States](#13-empty-states-與-error-states) +14. [手勢與觸控互動](#14-手勢與觸控互動) +15. [財務數據視覺化](#15-財務數據視覺化) +16. [無障礙設計](#16-無障礙設計) +17. [實作檢查清單](#17-實作檢查清單) --- @@ -204,9 +212,83 @@ apps/ratewise/src/ --- -## 7. 三端最佳實踐排版 +## 7. Grid 系統與 Layout -### 7.1 響應式斷點 +> 參考來源: [Android Grids and Units](https://developer.android.com/design/ui/mobile/guides/layout-and-content/grids-and-units), [The 4-Point Grid System](https://www.thedesignership.com/blog/the-ultimate-spacing-guide-for-ui-designers) + +### 7.1 基準網格 + +| 平台 | 基準單位 | 說明 | +|------|----------|------| +| iOS (Apple HIG) | 8pt | 間距應為 8 的倍數 | +| Android (MD3) | 4dp | 小元素用 4dp,一般用 8dp | +| Web (本系統) | 4px | 相容兩平台,靈活度高 | + +### 7.2 Mobile Grid 規格 + +``` ++---------------------------+ +|←16px→| 內容區域 |←16px→| ← Margins: 16-24px +| | | | +| 4-column grid | ← Columns: 4 +| | | | +| ←16px gutter→ | ← Gutters: 16px (≤655px) ++---------------------------+ +``` + +| 屬性 | 值 | 說明 | +|------|------|------| +| Columns | 4 | 簡單布局適合 | +| Margins | 16-24px | 邊距 | +| Gutters | 16px | 欄間距 | +| Content Width | 100% - margins | 自適應 | + +### 7.3 Tablet Grid 規格 + +``` ++-----------------------------------------------+ +|←24px→| 內容區域 (8 columns) |←24px→| +| | col col col col ... | | +| | ←16px→ ←16px→ ←16px→ ←16px→ | | ++-----------------------------------------------+ +``` + +| 屬性 | 值 | 說明 | +|------|------|------| +| Columns | 8 | 複雜布局 | +| Margins | 24px | 邊距 | +| Gutters | 16-24px | 欄間距 | + +### 7.4 Desktop Grid 規格 + +``` ++------------------------------------------------------------------+ +|←auto→| 內容區域 (12 columns, max-width) |←auto→| +| | col col col col col col col col col ... | | +| |←24px→←24px→←24px→←24px→ | | ++------------------------------------------------------------------+ +``` + +| 屬性 | 值 | 說明 | +|------|------|------| +| Columns | 12 | 標準網格 | +| Max Width | 1280px | 內容最大寬度 | +| Margins | auto (居中) | 自動居中 | +| Gutters | 24px | 欄間距 | + +### 7.5 Android Window Size Classes + +| 類別 | 寬度範圍 | 典型裝置 | +|------|----------|----------| +| Compact | < 600dp | 手機直立 | +| Medium | 600-839dp | 平板直立、折疊手機展開 | +| Expanded | ≥ 840dp | 平板橫放、桌面 | + +--- + +## 8. 三端最佳實踐排版 + +### 8.1 響應式斷點 | 斷點 | 寬度 | 裝置 | 導覽模式 | |------|------|------|----------| @@ -573,11 +655,514 @@ apps/ratewise/src/ | `ghost` | transparent | `text-secondary` | none | | `glass` | `rgba(255,255,255,0.1)` | `text-primary` | `rgba(255,255,255,0.2)` | +### 9.4 Card 設計最佳實踐 + +> 參考來源: [Card UI Design Examples](https://bricxlabs.com/blogs/card-ui-design-examples), [Shadows in UI Design](https://blog.logrocket.com/ux-design/shadows-ui-design-tips-best-practices/) + +#### Shadow 層級系統 + +| 層級 | 用途 | CSS Shadow | +|------|------|------------| +| `shadow-sm` | 靜態卡片 | `0 1px 2px rgba(0,0,0,0.05)` | +| `shadow-md` | 互動卡片 | `0 4px 6px -1px rgba(0,0,0,0.1)` | +| `shadow-lg` | Hover 狀態 | `0 10px 15px -3px rgba(0,0,0,0.1)` | +| `shadow-xl` | 浮動元素 | `0 20px 25px -5px rgba(0,0,0,0.1)` | + +#### Hover 動效規格 + +```css +/* 卡片 Hover 效果 */ +.card { + transition: transform 200ms ease-out, box-shadow 200ms ease-out; +} + +.card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +/* 效能優化:避免 box-shadow 動畫,改用 transform */ +.card-optimized::after { + content: ''; + position: absolute; + inset: 0; + box-shadow: var(--shadow-lg); + opacity: 0; + transition: opacity 200ms ease-out; +} + +.card-optimized:hover::after { + opacity: 1; +} +``` + +#### Border Radius 一致性原則 + +``` +┌─────────────────────────────────────┐ +│ Card Container (radius: 16px) │ +│ ┌─────────────────────────────────┐ │ +│ │ Image (radius: 12px) │ │ ← 內層 radius = 外層 - padding +│ └─────────────────────────────────┘ │ +│ ┌───────────┐ │ +│ │ Button │ (radius: 8px) │ ← 更小的內層元素 +│ └───────────┘ │ +└─────────────────────────────────────┘ +``` + +| 元素 | Radius | 規則 | +|------|--------|------| +| Card 外層 | 16px | 基準 | +| Card 內圖片 | 12px | 外層 - 4px | +| Card 內按鈕 | 8px | 保持比例 | +| Nested Card | 12px | 外層 - 4px | + +--- + +## 10. 表單與輸入設計 + +> 參考來源: [Form UI/UX Best Practices](https://www.designstudiouiux.com/blog/form-ux-design-best-practices/), [Inline Validation UX](https://smart-interface-design-patterns.com/articles/inline-validation-ux/) + +### 10.1 輸入框規格 + +``` ++------------------------------------------+ +| Label Text 必填 * | ++------------------------------------------+ +| ┌──────────────────────────────────────┐ | +| │ Placeholder text... │ | ← 48px height +| └──────────────────────────────────────┘ | +| Helper text or error message | ++------------------------------------------+ +``` + +| 屬性 | 值 | 說明 | +|------|------|------| +| Height | 44-52px | 符合觸控目標 | +| Padding | 12-16px | 內間距 | +| Label 字級 | 14px | 清晰可讀 | +| Placeholder | 14-16px | 比 label 稍淡 | +| Helper/Error | 12px | 輔助說明 | +| Border Radius | 8-12px | 與系統一致 | + +### 10.2 驗證策略 + +| 策略 | 時機 | 適用場景 | +|------|------|----------| +| **Inline (即時)** | 離開欄位時 | Email、密碼強度 | +| **On Submit** | 送出時 | 複雜多欄位表單 | +| **Hybrid** | 關鍵欄位即時 + 整體送出 | 最佳實踐 | + +#### Reward Early, Punish Late 原則 + +``` +輸入中 → 不顯示錯誤 (避免干擾) +離開欄位 → 如果正確,顯示 ✓ + → 如果錯誤,暫不顯示 +再次修正後 → 即時顯示驗證結果 +送出時 → 顯示所有錯誤 +``` + +### 10.3 狀態視覺 + +| 狀態 | Border | Background | Icon | +|------|--------|------------|------| +| Default | `slate-200` | `white` | - | +| Focus | `violet-500` + ring | `white` | - | +| Valid | `emerald-500` | `emerald-50` | ✓ | +| Error | `red-500` | `red-50` | ✗ | +| Disabled | `slate-200` | `slate-100` | - | + +### 10.4 貨幣輸入框(Ratewise 專用) + +``` ++------------------------------------------+ +| 金額 | +| ┌────────────────────────────┬─────────┐ | +| │ 1,000.00 │ TWD ▼ │ | ← 56px height +| └────────────────────────────┴─────────┘ | +| | +| ┌────────────────────────────┬─────────┐ | +| │ 31.25 │ USD ▼ │ | +| └────────────────────────────┴─────────┘ | ++------------------------------------------+ +``` + +| 屬性 | 值 | +|------|------| +| Height | 56px | +| Font | Monospace, 20-24px | +| Currency Selector | 內嵌右側,80px 寬 | +| Thousand Separator | 自動格式化 | + +--- + +## 11. Micro-interactions 與動效 + +> 參考來源: [Micro Interactions 2025](https://www.stan.vision/journal/micro-interactions-2025-in-web-design), [Motion UI Trends](https://www.betasofttechnology.com/motion-ui-trends-and-micro-interactions/) + +### 11.1 動效時間規格 + +| 類型 | Duration | Easing | 用途 | +|------|----------|--------|------| +| **Instant** | 0-100ms | - | 即時回饋 | +| **Fast** | 100-150ms | ease-out | 按鈕、小元素 | +| **Normal** | 200-300ms | ease-in-out | 卡片、面板 | +| **Slow** | 300-500ms | ease-in-out | Modal、頁面轉場 | + +### 11.2 3 秒法則 + +> "Your animation budget sits in that 0-3s window. Use it wisely." + +| 情境 | 建議時間 | +|------|----------| +| 按鈕點擊回饋 | 100-150ms | +| Hover 效果 | 200ms | +| Toast 出現 | 300ms | +| Modal 開啟 | 300-400ms | +| 頁面轉場 | 400-500ms | +| Loading 動畫 | 持續,1.5-2s 循環 | + +### 11.3 Hover 與 Active 狀態 + +```css +/* 按鈕互動 */ +.button { + transition: all 150ms ease-out; +} + +.button:hover { + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.button:active { + transform: translateY(0); + box-shadow: var(--shadow-sm); +} + +/* Mobile Ripple Effect */ +.button-ripple::after { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, transparent 70%); + transform: scale(0); + opacity: 0; +} + +.button-ripple:active::after { + animation: ripple 400ms ease-out; +} + +@keyframes ripple { + to { + transform: scale(2.5); + opacity: 0; + } +} +``` + +### 11.4 Reduced Motion 支援 + +```css +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} +``` + +--- + +## 12. 載入狀態與 Skeleton + +> 參考來源: [Skeleton Loading Screen Design](https://blog.logrocket.com/ux-design/skeleton-loading-screen-design/), [NN/g Skeleton Screens](https://www.nngroup.com/articles/skeleton-screens/) + +### 12.1 Skeleton 設計原則 + +| 原則 | 說明 | +|------|------| +| **匹配佈局** | Skeleton 結構應與最終內容一致 | +| **使用 Shimmer** | 動態效果減少感知等待時間 20-30% | +| **平滑過渡** | 內容載入時使用 cross-fade | +| **避免空白** | 不使用只有 header/footer 的 skeleton | + +### 12.2 Shimmer 效果實作 + +```css +.skeleton { + background: linear-gradient( + 90deg, + var(--color-bg-secondary) 0%, + var(--color-bg-tertiary) 50%, + var(--color-bg-secondary) 100% + ); + background-size: 200% 100%; + animation: shimmer 1.5s ease-in-out infinite; + border-radius: var(--radius-md); +} + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} +``` + +### 12.3 Ratewise Skeleton 範例 + +``` +匯率卡片 Skeleton: ++---------------------------+ +| ┌─────┐ ████████████ | ← 貨幣 icon + 名稱 +| └─────┘ ████████ | +| | +| ████████████████████ | ← 匯率數字 +| ████████ | ← 漲跌幅 +| | +| ╭──────────────────╮ | +| │ ░░░░░░░░░░░░░░░░ │ | ← 圖表區域 +| ╰──────────────────╯ | ++---------------------------+ + +列表項 Skeleton: +┌─────────────────────────┐ +│ ┌───┐ ████████ ██████ │ +│ └───┘ ████ ████ │ +├─────────────────────────┤ +│ ┌───┐ ████████ ██████ │ +│ └───┘ ████ ████ │ +└─────────────────────────┘ +``` + +### 12.4 載入狀態類型 + +| 類型 | 時機 | 實作 | +|------|------|------| +| **Skeleton** | 首次載入、結構已知 | 灰色區塊 + shimmer | +| **Spinner** | 短暫操作 (<2s) | 居中旋轉圖標 | +| **Progress Bar** | 長操作、進度可知 | 線性進度條 | +| **Pull-to-refresh** | 手動刷新 | 頂部 spinner | + +--- + +## 13. Empty States 與 Error States + +> 參考來源: [Empty State UX Best Practices](https://www.uxpin.com/studio/blog/ux-best-practices-designing-the-overlooked-empty-states/), [Empty State UI Pattern](https://mobbin.com/glossary/empty-state) + +### 13.1 Empty State 類型 + +| 類型 | 情境 | 設計重點 | +|------|------|----------| +| **First Use** | 新用戶首次使用 | 引導 + CTA | +| **User Cleared** | 完成所有任務 | 正面鼓勵 | +| **No Results** | 搜尋無結果 | 建議 + 替代方案 | +| **No Data** | 系統無資料 | 說明 + 下一步 | +| **Error** | 發生錯誤 | 清楚說明 + 解決方案 | + +### 13.2 Empty State 結構 + +``` ++----------------------------------+ +| | +| ┌──────────┐ | +| │ 插圖 │ | ← 可選,匹配品牌調性 +| └──────────┘ | +| | +| 沒有找到符合的貨幣 | ← Headline +| | +| 試著搜尋其他關鍵字,或瀏覽 | ← Secondary text +| 所有可用的貨幣 | +| | +| [ 瀏覽全部貨幣 ] | ← CTA (必須) +| | ++----------------------------------+ +``` + +### 13.3 Error State 規格 + +| 錯誤類型 | 顏色 | Icon | 訊息範例 | +|----------|------|------|----------| +| **網路錯誤** | Neutral | 📶 | 無法連線,請檢查網路 | +| **載入失敗** | Warning | ⚠️ | 載入失敗,點擊重試 | +| **找不到** | Neutral | 🔍 | 找不到此頁面 | +| **伺服器錯誤** | Error | ⚠️ | 系統忙碌中,請稍後再試 | +| **權限錯誤** | Warning | 🔒 | 您沒有權限存取此內容 | + +### 13.4 Ratewise 專用 Empty States + +``` +收藏列表空白: ++----------------------------------+ +| ⭐ | +| | +| 還沒有收藏的貨幣 | +| | +| 點擊貨幣旁的星號即可加入收藏 | +| | +| [ 瀏覽全部貨幣 ] | ++----------------------------------+ + +匯率載入失敗: ++----------------------------------+ +| ⚠️ | +| | +| 無法取得最新匯率 | +| | +| 請檢查網路連線,或稍後再試 | +| | +| [ 重試 ] [ 使用快取 ] | ++----------------------------------+ +``` + +--- + +## 14. 手勢與觸控互動 + +> 參考來源: [Material Design Gestures](https://m1.material.io/patterns/gestures.html), [Pull-to-Refresh Pattern](https://ui-patterns.com/patterns/pull-to-refresh) + +### 14.1 觸控目標規格 + +| 規範 | 最小尺寸 | 間距 | +|------|----------|------| +| WCAG 2.2 | 24×24px | - | +| Apple HIG | 44×44pt | - | +| Android MD3 | 48×48dp | 8dp | +| 本系統 | 44×44px | 8px | + +### 14.2 Pull-to-Refresh 規格 + +``` +下拉刷新流程: + ┌───────────┐ + │ ↓ 60px │ ← Trigger threshold + └───────────┘ + ↓ +┌──────────────────────────────────┐ +│ ◠ 正在更新... │ ← Spinner + text +├──────────────────────────────────┤ +│ │ +│ 內容區域 │ +│ │ +└──────────────────────────────────┘ +``` + +| 階段 | 視覺回饋 | +|------|----------| +| 下拉中 | 顯示 spinner,opacity 隨距離增加 | +| 觸發 (≥60px) | Spinner 開始旋轉 | +| 載入中 | Spinner 持續旋轉 | +| 完成 | Spinner 收起,內容更新 | + +### 14.3 Swipe 手勢 + +| 手勢 | 動作 | 視覺回饋 | +|------|------|----------| +| Swipe Left | 刪除/封存 | 紅色背景 + 垃圾桶 icon | +| Swipe Right | 收藏/標記 | 黃色背景 + 星星 icon | +| Long Press | 多選/編輯 | 放大 + 震動 | + +``` +Swipe 刪除動效: +┌─────────────────────────────┐ +│ ←←← 🗑️ │ Item Content │ +│ │ │ ← 紅色背景漸顯 +└─────────────────────────────┘ +``` + +### 14.4 無障礙替代方案 + +| 手勢 | 替代操作 | +|------|----------| +| Swipe | 長按顯示選單 | +| Pull-to-refresh | 頂部刷新按鈕 | +| Pinch-to-zoom | +/- 按鈕 | +| Double-tap | 明確按鈕 | + +--- + +## 15. 財務數據視覺化 + +> 參考來源: [Financial Data Visualization](https://blog.coupler.io/financial-data-visualization/), [Color Theory in Finance Dashboard](https://medium.com/@extej/the-role-of-color-theory-in-finance-dashboard-design-d2942aec9fff) + +### 15.1 匯率顏色規範 + +| 狀態 | 顏色 | 用途 | +|------|------|------| +| **上漲** | Emerald 500 `#10b981` | 正向變動 | +| **下跌** | Red 500 `#ef4444` | 負向變動 | +| **持平** | Slate 400 `#94a3b8` | 無變動 | +| **主要數據** | Slate 900 `#0f172a` | 匯率數字 | +| **次要數據** | Slate 500 `#64748b` | 時間戳記 | + +### 15.2 圖表顏色系統 + +| 用途 | Light Mode | Dark Mode | +|------|------------|-----------| +| Primary Line | Violet 600 | Violet 400 | +| Secondary Line | Cyan 500 | Cyan 400 | +| Positive Area | Emerald 500/20 | Emerald 400/20 | +| Negative Area | Red 500/20 | Red 400/20 | +| Grid Lines | Slate 200 | Slate 700 | +| Axis Labels | Slate 500 | Slate 400 | + +### 15.3 多幣別比較圖 + +``` +顏色分配 (最多 6 條線): +┌──────────────────────────────┐ +│ USD ━━━ Violet │ +│ EUR ─── Cyan │ +│ JPY ··· Emerald │ +│ GBP ─·─ Amber │ +│ CNY ─── Rose │ +│ KRW ··· Sky │ +└──────────────────────────────┘ +``` + +### 15.4 數字格式化 + +| 類型 | 格式 | 範例 | +|------|------|------| +| 匯率 (主要) | 4 位小數 | 31.2500 | +| 匯率 (日圓) | 4 位小數 | 0.2185 | +| 漲跌幅 | ± 百分比 | +0.12% | +| 漲跌值 | ± 數值 | +0.0375 | +| 金額 | 千分位 | 1,000.00 | +| 時間 | HH:mm | 14:30 | +| 日期 | YYYY/MM/DD | 2025/01/17 | + +### 15.5 KPI 卡片規格 + +``` ++---------------------------+ +| USD / TWD ⭐ | ← 貨幣對 + 收藏 +| | +| 31.2500 | ← 匯率 (rate-display) +| ▲ +0.0375 (+0.12%) | ← 漲跌 (綠色) +| | +| 更新於 14:30 | ← 時間戳記 ++---------------------------+ + +規格: +- 匯率字級: 30-36px, Monospace, Bold +- 漲跌字級: 14px, Medium +- 時間戳記: 12px, Slate-500 +``` + --- -## 9. 無障礙設計 +## 16. 無障礙設計 -### 9.1 對比度要求(WCAG 2.2 AA) +### 16.1 對比度要求(WCAG 2.2 AA) | 元素 | 最小對比度 | 驗證 | |------|------------|------| @@ -586,7 +1171,7 @@ apps/ratewise/src/ | 互動元素 | 3:1 | ✅ violet-600 on white = 4.5:1 | | 玻璃上文字 | 4.5:1 | ⚠️ 需要測試不同背景 | -### 9.2 觸控目標 +### 16.2 觸控目標 | 規範 | 最小尺寸 | |------|----------| @@ -594,7 +1179,7 @@ apps/ratewise/src/ | Apple HIG | 44×44px | | 本系統採用 | 44×44px (min), 48×48px (comfortable) | -### 9.3 降級支援 +### 16.3 降級支援 ```css /* 偏好降低透明度 */ @@ -609,7 +1194,7 @@ apps/ratewise/src/ --- -## 10. 實作檢查清單 +## 17. 實作檢查清單 ### Phase 1: Token 基礎建設 ✅ From 2b87c8167810b4d52f2345d63a992d1017c3d122 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Jan 2026 01:18:38 +0000 Subject: [PATCH 3/7] feat(design): integrate spacing, radius, shadow, typography tokens to Tailwind - Add spacingTokens mapping to CSS Variables (--spacing-*) - Add borderRadiusTokens mapping to CSS Variables (--radius-*) - Add boxShadowTokens with glass-specific shadows (--shadow-*, --glass-shadow-*) - Add fontSizeTokens with line-height configuration - Add transitionDurationTokens and transitionTimingTokens - Add zIndexTokens with semantic levels (dropdown, modal, toast, etc.) - Update generateTailwindThemeExtension() to include all token categories --- apps/ratewise/src/config/design-tokens.ts | 142 +++++- docs/dev/006_design_modernization_spec.md | 528 +++++++++++----------- 2 files changed, 410 insertions(+), 260 deletions(-) diff --git a/apps/ratewise/src/config/design-tokens.ts b/apps/ratewise/src/config/design-tokens.ts index 5754ae91..e2221002 100644 --- a/apps/ratewise/src/config/design-tokens.ts +++ b/apps/ratewise/src/config/design-tokens.ts @@ -352,6 +352,133 @@ export const darkTheme = { }, } as const; +/** + * Spacing Tokens - 使用 CSS Variables + * + * @description 基於 4px base unit 的間距系統 + * @reference Material Design spacing: https://m3.material.io/foundations/layout/spacing + */ +export const spacingTokens = { + 0: 'var(--spacing-0)', + 1: 'var(--spacing-1)', + 2: 'var(--spacing-2)', + 3: 'var(--spacing-3)', + 4: 'var(--spacing-4)', + 5: 'var(--spacing-5)', + 6: 'var(--spacing-6)', + 8: 'var(--spacing-8)', + 10: 'var(--spacing-10)', + 12: 'var(--spacing-12)', + 16: 'var(--spacing-16)', + 20: 'var(--spacing-20)', + 24: 'var(--spacing-24)', +} as const; + +/** + * Border Radius Tokens - 使用 CSS Variables + * + * @description 統一圓角系統 + */ +export const borderRadiusTokens = { + none: 'var(--radius-none)', + sm: 'var(--radius-sm)', + DEFAULT: 'var(--radius-md)', + md: 'var(--radius-md)', + lg: 'var(--radius-lg)', + xl: 'var(--radius-xl)', + '2xl': 'var(--radius-2xl)', + '3xl': 'var(--radius-3xl)', + full: 'var(--radius-full)', +} as const; + +/** + * Box Shadow Tokens - 使用 CSS Variables + * + * @description 統一陰影系統 + */ +export const boxShadowTokens = { + sm: 'var(--shadow-sm)', + DEFAULT: 'var(--shadow-md)', + md: 'var(--shadow-md)', + lg: 'var(--shadow-lg)', + xl: 'var(--shadow-xl)', + '2xl': 'var(--shadow-2xl)', + inner: 'var(--shadow-inner)', + none: 'none', + // Glass-specific shadows + 'glass-glow': 'var(--glass-shadow-glow)', + 'glass-inner': 'var(--glass-shadow-inner)', +} as const; + +/** + * Font Size Tokens - 使用 CSS Variables + * + * @description 統一字級系統 + */ +export const fontSizeTokens: Record = { + xs: ['var(--font-size-xs)', { lineHeight: 'var(--line-height-normal)' }], + sm: ['var(--font-size-sm)', { lineHeight: 'var(--line-height-normal)' }], + base: ['var(--font-size-base)', { lineHeight: 'var(--line-height-normal)' }], + lg: ['var(--font-size-lg)', { lineHeight: 'var(--line-height-snug)' }], + xl: ['var(--font-size-xl)', { lineHeight: 'var(--line-height-snug)' }], + '2xl': ['var(--font-size-2xl)', { lineHeight: 'var(--line-height-tight)' }], + '3xl': ['var(--font-size-3xl)', { lineHeight: 'var(--line-height-tight)' }], + '4xl': ['var(--font-size-4xl)', { lineHeight: 'var(--line-height-tight)' }], + '5xl': ['var(--font-size-5xl)', { lineHeight: 'var(--line-height-tight)' }], +}; + +/** + * Transition Duration Tokens - 使用 CSS Variables + * + * @description 動畫時長系統 + */ +export const transitionDurationTokens = { + 0: 'var(--duration-instant)', + 75: '75ms', + 100: 'var(--duration-fast)', + 150: '150ms', + 200: 'var(--duration-normal)', + 300: 'var(--duration-slow)', + 500: 'var(--duration-slower)', + 700: '700ms', + 1000: '1000ms', +} as const; + +/** + * Transition Timing Function Tokens - 使用 CSS Variables + * + * @description 緩動函數系統 + */ +export const transitionTimingTokens = { + linear: 'var(--easing-linear)', + in: 'var(--easing-ease-in)', + out: 'var(--easing-ease-out)', + 'in-out': 'var(--easing-ease-in-out)', + spring: 'var(--easing-spring)', +} as const; + +/** + * Z-Index Tokens - 使用 CSS Variables + * + * @description 層級系統 + */ +export const zIndexTokens = { + 0: 'var(--z-base)', + 10: '10', + 20: '20', + 30: '30', + 40: '40', + 50: '50', + dropdown: 'var(--z-dropdown)', + sticky: 'var(--z-sticky)', + fixed: 'var(--z-fixed)', + drawer: 'var(--z-drawer)', + modal: 'var(--z-modal)', + popover: 'var(--z-popover)', + toast: 'var(--z-toast)', + tooltip: 'var(--z-tooltip)', +} as const; + /** * 取得 Design Token 配置 * @@ -365,7 +492,13 @@ export const darkTheme = { * ``` */ export function getDesignTokens() { - return { colors: semanticColors }; + return { + colors: semanticColors, + spacing: spacingTokens, + borderRadius: borderRadiusTokens, + boxShadow: boxShadowTokens, + fontSize: fontSizeTokens, + }; } /** @@ -394,6 +527,13 @@ export function generateTailwindThemeExtension(): Config['theme'] { return { extend: { colors: semanticColors, + spacing: spacingTokens, + borderRadius: borderRadiusTokens, + boxShadow: boxShadowTokens, + fontSize: fontSizeTokens, + transitionDuration: transitionDurationTokens, + transitionTimingFunction: transitionTimingTokens, + zIndex: zIndexTokens, }, }; } diff --git a/docs/dev/006_design_modernization_spec.md b/docs/dev/006_design_modernization_spec.md index 0a068b64..3ff98ab0 100644 --- a/docs/dev/006_design_modernization_spec.md +++ b/docs/dev/006_design_modernization_spec.md @@ -34,13 +34,13 @@ 本規格基於以下權威來源: -| 標準 | 版本 | 用途 | -|------|------|------| -| [W3C Design Tokens](https://www.designtokens.org/tr/drafts/format/) | 2025.10 | Token JSON 格式規範 | -| [Apple Liquid Glass](https://developer.apple.com/documentation/TechnologyOverviews/liquid-glass) | iOS 26 | 玻璃效果視覺語言 | -| [Material Design 3](https://m3.material.io/) | 2025 | 導覽元件與互動模式 | -| [WCAG 2.2](https://www.w3.org/WAI/WCAG22/quickref/) | Level AA | 無障礙標準 | -| [Tailwind CSS](https://tailwindcss.com/docs/responsive-design) | v4 | 響應式斷點 | +| 標準 | 版本 | 用途 | +| ------------------------------------------------------------------------------------------------ | -------- | ------------------- | +| [W3C Design Tokens](https://www.designtokens.org/tr/drafts/format/) | 2025.10 | Token JSON 格式規範 | +| [Apple Liquid Glass](https://developer.apple.com/documentation/TechnologyOverviews/liquid-glass) | iOS 26 | 玻璃效果視覺語言 | +| [Material Design 3](https://m3.material.io/) | 2025 | 導覽元件與互動模式 | +| [WCAG 2.2](https://www.w3.org/WAI/WCAG22/quickref/) | Level AA | 無障礙標準 | +| [Tailwind CSS](https://tailwindcss.com/docs/responsive-design) | v4 | 響應式斷點 | --- @@ -80,28 +80,28 @@ apps/ratewise/src/ ### 3.1 主色與強調色 -| Token | Light Mode | Dark Mode | 用途 | -|-------|------------|-----------|------| -| `accent-primary` | Violet 600 `#7c3aed` | Violet 400 `#a78bfa` | CTA、連結、焦點 | -| `accent-secondary` | Cyan 500 `#06b6d4` | Cyan 400 `#22d3ee` | 次要強調 | +| Token | Light Mode | Dark Mode | 用途 | +| ------------------ | -------------------- | -------------------- | --------------- | +| `accent-primary` | Violet 600 `#7c3aed` | Violet 400 `#a78bfa` | CTA、連結、焦點 | +| `accent-secondary` | Cyan 500 `#06b6d4` | Cyan 400 `#22d3ee` | 次要強調 | ### 3.2 配色方案(可切換) -| 方案 | Primary | 描述 | -|------|---------|------| +| 方案 | Primary | 描述 | +| --------------- | --------- | ---------- | | `violet` (預設) | `#7c3aed` | 品牌紫羅蘭 | -| `ocean` | `#0ea5e9` | 海洋藍 | -| `forest` | `#16a34a` | 森林綠 | -| `sunset` | `#ea580c` | 日落橙 | +| `ocean` | `#0ea5e9` | 海洋藍 | +| `forest` | `#16a34a` | 森林綠 | +| `sunset` | `#ea580c` | 日落橙 | ### 3.3 狀態色 -| 狀態 | 顏色 | 背景 | 用途 | -|------|------|------|------| +| 狀態 | 顏色 | 背景 | 用途 | +| ------- | ----------- | ---------- | ------------------ | | Success | Emerald 500 | Emerald 50 | 正向漲幅、成功訊息 | -| Warning | Amber 500 | Amber 50 | 警告提示 | -| Error | Red 500 | Red 50 | 負向跌幅、錯誤訊息 | -| Info | Cyan 500 | Cyan 50 | 資訊提示 | +| Warning | Amber 500 | Amber 50 | 警告提示 | +| Error | Red 500 | Red 50 | 負向跌幅、錯誤訊息 | +| Info | Cyan 500 | Cyan 50 | 資訊提示 | --- @@ -115,11 +115,11 @@ apps/ratewise/src/ ### 4.2 玻璃表面層級 -| 層級 | Blur | Opacity | Border | 用途 | -|------|------|---------|--------|------| -| `base` | 16px | 8% | 18% white | 卡片、列表容器 | -| `elevated` | 24px | 12% | 25% white | 浮動元素、強調卡片 | -| `overlay` | 32px | 18% | 35% white | Modal、Drawer | +| 層級 | Blur | Opacity | Border | 用途 | +| ---------- | ---- | ------- | --------- | ------------------ | +| `base` | 16px | 8% | 18% white | 卡片、列表容器 | +| `elevated` | 24px | 12% | 25% white | 浮動元素、強調卡片 | +| `overlay` | 32px | 18% | 35% white | Modal、Drawer | ### 4.3 CSS 實作 @@ -159,18 +159,18 @@ apps/ratewise/src/ ### 5.2 字級比例 -| Token | Size | Weight | Line Height | 用途 | -|-------|------|--------|-------------|------| -| `display-lg` | 48px | Bold | 1.25 | 大標題 | -| `display-md` | 36px | Bold | 1.25 | 頁面標題 | -| `heading-lg` | 24px | Semibold | 1.375 | 區塊標題 | -| `heading-md` | 20px | Semibold | 1.375 | 卡片標題 | -| `heading-sm` | 18px | Semibold | 1.375 | 小標題 | -| `body-lg` | 18px | Normal | 1.625 | 大段落 | -| `body-md` | 16px | Normal | 1.5 | 一般內文 | -| `body-sm` | 14px | Normal | 1.5 | 小內文 | -| `caption` | 12px | Normal | 1.5 | 說明文字 | -| `rate-display` | 30px | Bold (Mono) | 1.25 | 匯率數字 | +| Token | Size | Weight | Line Height | 用途 | +| -------------- | ---- | ----------- | ----------- | -------- | +| `display-lg` | 48px | Bold | 1.25 | 大標題 | +| `display-md` | 36px | Bold | 1.25 | 頁面標題 | +| `heading-lg` | 24px | Semibold | 1.375 | 區塊標題 | +| `heading-md` | 20px | Semibold | 1.375 | 卡片標題 | +| `heading-sm` | 18px | Semibold | 1.375 | 小標題 | +| `body-lg` | 18px | Normal | 1.625 | 大段落 | +| `body-md` | 16px | Normal | 1.5 | 一般內文 | +| `body-sm` | 14px | Normal | 1.5 | 小內文 | +| `caption` | 12px | Normal | 1.5 | 說明文字 | +| `rate-display` | 30px | Bold (Mono) | 1.25 | 匯率數字 | --- @@ -178,37 +178,37 @@ apps/ratewise/src/ ### 6.1 間距比例(4px 基準) -| Token | Value | 常見用途 | -|-------|-------|----------| -| `spacing-1` | 4px | 緊密間距 | -| `spacing-2` | 8px | 元素內間距 | -| `spacing-3` | 12px | 小區塊間距 | -| `spacing-4` | 16px | 標準間距 | -| `spacing-6` | 24px | 區塊間距 | -| `spacing-8` | 32px | 大區塊間距 | -| `spacing-12` | 48px | 頁面間距 | +| Token | Value | 常見用途 | +| ------------ | ----- | ---------- | +| `spacing-1` | 4px | 緊密間距 | +| `spacing-2` | 8px | 元素內間距 | +| `spacing-3` | 12px | 小區塊間距 | +| `spacing-4` | 16px | 標準間距 | +| `spacing-6` | 24px | 區塊間距 | +| `spacing-8` | 32px | 大區塊間距 | +| `spacing-12` | 48px | 頁面間距 | ### 6.2 圓角比例 -| Token | Value | 用途 | -|-------|-------|------| -| `radius-sm` | 4px | 小元素、標籤 | -| `radius-md` | 8px | 按鈕、輸入框 | -| `radius-lg` | 12px | 卡片 | -| `radius-xl` | 16px | 大卡片、導覽 | -| `radius-2xl` | 24px | Modal | -| `radius-full` | 9999px | 圓形 | +| Token | Value | 用途 | +| ------------- | ------ | ------------ | +| `radius-sm` | 4px | 小元素、標籤 | +| `radius-md` | 8px | 按鈕、輸入框 | +| `radius-lg` | 12px | 卡片 | +| `radius-xl` | 16px | 大卡片、導覽 | +| `radius-2xl` | 24px | Modal | +| `radius-full` | 9999px | 圓形 | ### 6.3 導覽尺寸(Material Design 3) -| 元件 | 尺寸 | 來源 | -|------|------|------| -| Bottom Tab Bar 高度 | 56px | MD3 Navigation Bar | -| Tab 項目最小寬度 | 64px | MD3 Guidelines | -| Tab 項目最大寬度 | 96px | MD3 Guidelines | -| Nav Rail 寬度 | 80px | MD3 Navigation Rail | -| Sidebar 寬度 | 256px | 自訂 | -| Top Bar 高度 | 64px | 自訂 | +| 元件 | 尺寸 | 來源 | +| ------------------- | ----- | ------------------- | +| Bottom Tab Bar 高度 | 56px | MD3 Navigation Bar | +| Tab 項目最小寬度 | 64px | MD3 Guidelines | +| Tab 項目最大寬度 | 96px | MD3 Guidelines | +| Nav Rail 寬度 | 80px | MD3 Navigation Rail | +| Sidebar 寬度 | 256px | 自訂 | +| Top Bar 高度 | 64px | 自訂 | --- @@ -218,11 +218,11 @@ apps/ratewise/src/ ### 7.1 基準網格 -| 平台 | 基準單位 | 說明 | -|------|----------|------| -| iOS (Apple HIG) | 8pt | 間距應為 8 的倍數 | -| Android (MD3) | 4dp | 小元素用 4dp,一般用 8dp | -| Web (本系統) | 4px | 相容兩平台,靈活度高 | +| 平台 | 基準單位 | 說明 | +| --------------- | -------- | ------------------------ | +| iOS (Apple HIG) | 8pt | 間距應為 8 的倍數 | +| Android (MD3) | 4dp | 小元素用 4dp,一般用 8dp | +| Web (本系統) | 4px | 相容兩平台,靈活度高 | ### 7.2 Mobile Grid 規格 @@ -236,12 +236,12 @@ apps/ratewise/src/ +---------------------------+ ``` -| 屬性 | 值 | 說明 | -|------|------|------| -| Columns | 4 | 簡單布局適合 | -| Margins | 16-24px | 邊距 | -| Gutters | 16px | 欄間距 | -| Content Width | 100% - margins | 自適應 | +| 屬性 | 值 | 說明 | +| ------------- | -------------- | ------------ | +| Columns | 4 | 簡單布局適合 | +| Margins | 16-24px | 邊距 | +| Gutters | 16px | 欄間距 | +| Content Width | 100% - margins | 自適應 | ### 7.3 Tablet Grid 規格 @@ -253,11 +253,11 @@ apps/ratewise/src/ +-----------------------------------------------+ ``` -| 屬性 | 值 | 說明 | -|------|------|------| -| Columns | 8 | 複雜布局 | -| Margins | 24px | 邊距 | -| Gutters | 16-24px | 欄間距 | +| 屬性 | 值 | 說明 | +| ------- | ------- | -------- | +| Columns | 8 | 複雜布局 | +| Margins | 24px | 邊距 | +| Gutters | 16-24px | 欄間距 | ### 7.4 Desktop Grid 規格 @@ -269,20 +269,20 @@ apps/ratewise/src/ +------------------------------------------------------------------+ ``` -| 屬性 | 值 | 說明 | -|------|------|------| -| Columns | 12 | 標準網格 | -| Max Width | 1280px | 內容最大寬度 | -| Margins | auto (居中) | 自動居中 | -| Gutters | 24px | 欄間距 | +| 屬性 | 值 | 說明 | +| --------- | ----------- | ------------ | +| Columns | 12 | 標準網格 | +| Max Width | 1280px | 內容最大寬度 | +| Margins | auto (居中) | 自動居中 | +| Gutters | 24px | 欄間距 | ### 7.5 Android Window Size Classes -| 類別 | 寬度範圍 | 典型裝置 | -|------|----------|----------| -| Compact | < 600dp | 手機直立 | -| Medium | 600-839dp | 平板直立、折疊手機展開 | -| Expanded | ≥ 840dp | 平板橫放、桌面 | +| 類別 | 寬度範圍 | 典型裝置 | +| -------- | --------- | ---------------------- | +| Compact | < 600dp | 手機直立 | +| Medium | 600-839dp | 平板直立、折疊手機展開 | +| Expanded | ≥ 840dp | 平板橫放、桌面 | --- @@ -290,13 +290,13 @@ apps/ratewise/src/ ### 8.1 響應式斷點 -| 斷點 | 寬度 | 裝置 | 導覽模式 | -|------|------|------|----------| -| `xs` | 0-374px | 小型手機 | Bottom Tab | -| `sm` | 375-767px | 手機 | Bottom Tab | -| `md` | 768-1023px | 平板 | Nav Rail | -| `lg` | 1024-1279px | 小桌面 | Sidebar | -| `xl` | 1280px+ | 大桌面 | Sidebar | +| 斷點 | 寬度 | 裝置 | 導覽模式 | +| ---- | ----------- | -------- | ---------- | +| `xs` | 0-374px | 小型手機 | Bottom Tab | +| `sm` | 375-767px | 手機 | Bottom Tab | +| `md` | 768-1023px | 平板 | Nav Rail | +| `lg` | 1024-1279px | 小桌面 | Sidebar | +| `xl` | 1280px+ | 大桌面 | Sidebar | --- @@ -338,6 +338,7 @@ apps/ratewise/src/ ``` **設計決策**: + - ✅ Material Design 3: 3-5 個 Tab 項目最佳 - ✅ 主要 KPI 置頂,符合「Content First」原則 - ✅ 快速轉換區提供即時互動 @@ -374,6 +375,7 @@ apps/ratewise/src/ ``` **設計決策**: + - ✅ 搜尋框置頂,快速找到貨幣 - ✅ Chips 篩選取代複雜下拉選單 - ✅ 列表項高度 64px 符合觸控目標 44px+ @@ -415,6 +417,7 @@ apps/ratewise/src/ ``` **設計決策**: + - ✅ 圖表佔主視覺,支援手勢縮放 - ✅ 時間範圍選擇器內嵌 - ✅ 底部操作列固定,不需捲動找按鈕 @@ -460,6 +463,7 @@ apps/ratewise/src/ ``` **設計決策**: + - ✅ 分群清晰,iOS 設定頁風格 - ✅ 每項右側顯示當前值 - ✅ 深色模式使用 Toggle 而非進入子頁 @@ -498,6 +502,7 @@ apps/ratewise/src/ ``` **設計決策**: + - ✅ MD3 Nav Rail 80px 標準寬度 - ✅ Master-Detail 模式減少跳頁 - ✅ 點擊左欄項目,右欄即時更新 @@ -528,6 +533,7 @@ apps/ratewise/src/ ``` **設計決策**: + - ✅ 頂部 Tabs 切換工作區 - ✅ 網格布局最大化資訊密度 - ✅ KPI 卡片並排方便比較 @@ -569,6 +575,7 @@ apps/ratewise/src/ ``` **設計決策**: + - ✅ 三欄設計:導覽、列表、詳情同時可見 - ✅ Sidebar 可收合至 72px - ✅ 鍵盤快捷鍵支援(↑↓ 切換、Enter 選擇) @@ -609,6 +616,7 @@ apps/ratewise/src/ ``` **設計決策**: + - ✅ 視覺化主題選擇器 - ✅ 即時預覽區域 - ✅ 滑桿控制玻璃效果強度 @@ -619,41 +627,41 @@ apps/ratewise/src/ ### 8.1 Bottom Tab Bar -| 屬性 | 值 | 來源 | -|------|------|------| -| 高度 | 56px | MD3 | -| 背景 | `glass-surface-base` | Liquid Glass | -| Blur | 24px | 自訂 | -| Item 最小寬度 | 64px | MD3 | -| Item 最大寬度 | 96px | MD3 | -| Icon 大小 | 24px | MD3 | -| Label 字級 | 12px | MD3 | -| Active Indicator | Pill 形狀, 32px 高 | MD3 | +| 屬性 | 值 | 來源 | +| ---------------- | -------------------- | ------------ | +| 高度 | 56px | MD3 | +| 背景 | `glass-surface-base` | Liquid Glass | +| Blur | 24px | 自訂 | +| Item 最小寬度 | 64px | MD3 | +| Item 最大寬度 | 96px | MD3 | +| Icon 大小 | 24px | MD3 | +| Label 字級 | 12px | MD3 | +| Active Indicator | Pill 形狀, 32px 高 | MD3 | ### 8.2 Glass Card -| 屬性 | Base | Elevated | Overlay | -|------|------|----------|---------| -| Background | `rgba(255,255,255,0.08)` | `0.12` | `0.18` | -| Blur | 16px | 24px | 32px | -| Border | `rgba(255,255,255,0.18)` | `0.25` | `0.35` | -| Radius | 16px | 16px | 24px | -| Shadow | `md` | `lg` | `xl` | +| 屬性 | Base | Elevated | Overlay | +| ---------- | ------------------------ | -------- | ------- | +| Background | `rgba(255,255,255,0.08)` | `0.12` | `0.18` | +| Blur | 16px | 24px | 32px | +| Border | `rgba(255,255,255,0.18)` | `0.25` | `0.35` | +| Radius | 16px | 16px | 24px | +| Shadow | `md` | `lg` | `xl` | ### 8.3 Button | Size | Height | Padding X | Font Size | Radius | -|------|--------|-----------|-----------|--------| -| `sm` | 32px | 12px | 14px | 8px | -| `md` | 40px | 16px | 14px | 12px | -| `lg` | 48px | 24px | 16px | 12px | - -| Variant | Background | Text | Border | -|---------|------------|------|--------| -| `primary` | `accent-primary` | `text-inverse` | none | -| `secondary` | transparent | `accent-primary` | `accent-primary` | -| `ghost` | transparent | `text-secondary` | none | -| `glass` | `rgba(255,255,255,0.1)` | `text-primary` | `rgba(255,255,255,0.2)` | +| ---- | ------ | --------- | --------- | ------ | +| `sm` | 32px | 12px | 14px | 8px | +| `md` | 40px | 16px | 14px | 12px | +| `lg` | 48px | 24px | 16px | 12px | + +| Variant | Background | Text | Border | +| ----------- | ----------------------- | ---------------- | ----------------------- | +| `primary` | `accent-primary` | `text-inverse` | none | +| `secondary` | transparent | `accent-primary` | `accent-primary` | +| `ghost` | transparent | `text-secondary` | none | +| `glass` | `rgba(255,255,255,0.1)` | `text-primary` | `rgba(255,255,255,0.2)` | ### 9.4 Card 設計最佳實踐 @@ -661,19 +669,21 @@ apps/ratewise/src/ #### Shadow 層級系統 -| 層級 | 用途 | CSS Shadow | -|------|------|------------| -| `shadow-sm` | 靜態卡片 | `0 1px 2px rgba(0,0,0,0.05)` | -| `shadow-md` | 互動卡片 | `0 4px 6px -1px rgba(0,0,0,0.1)` | +| 層級 | 用途 | CSS Shadow | +| ----------- | ---------- | ---------------------------------- | +| `shadow-sm` | 靜態卡片 | `0 1px 2px rgba(0,0,0,0.05)` | +| `shadow-md` | 互動卡片 | `0 4px 6px -1px rgba(0,0,0,0.1)` | | `shadow-lg` | Hover 狀態 | `0 10px 15px -3px rgba(0,0,0,0.1)` | -| `shadow-xl` | 浮動元素 | `0 20px 25px -5px rgba(0,0,0,0.1)` | +| `shadow-xl` | 浮動元素 | `0 20px 25px -5px rgba(0,0,0,0.1)` | #### Hover 動效規格 ```css /* 卡片 Hover 效果 */ .card { - transition: transform 200ms ease-out, box-shadow 200ms ease-out; + transition: + transform 200ms ease-out, + box-shadow 200ms ease-out; } .card:hover { @@ -710,12 +720,12 @@ apps/ratewise/src/ └─────────────────────────────────────┘ ``` -| 元素 | Radius | 規則 | -|------|--------|------| -| Card 外層 | 16px | 基準 | -| Card 內圖片 | 12px | 外層 - 4px | -| Card 內按鈕 | 8px | 保持比例 | -| Nested Card | 12px | 外層 - 4px | +| 元素 | Radius | 規則 | +| ----------- | ------ | ---------- | +| Card 外層 | 16px | 基準 | +| Card 內圖片 | 12px | 外層 - 4px | +| Card 內按鈕 | 8px | 保持比例 | +| Nested Card | 12px | 外層 - 4px | --- @@ -736,22 +746,22 @@ apps/ratewise/src/ +------------------------------------------+ ``` -| 屬性 | 值 | 說明 | -|------|------|------| -| Height | 44-52px | 符合觸控目標 | -| Padding | 12-16px | 內間距 | -| Label 字級 | 14px | 清晰可讀 | -| Placeholder | 14-16px | 比 label 稍淡 | -| Helper/Error | 12px | 輔助說明 | -| Border Radius | 8-12px | 與系統一致 | +| 屬性 | 值 | 說明 | +| ------------- | ------- | ------------- | +| Height | 44-52px | 符合觸控目標 | +| Padding | 12-16px | 內間距 | +| Label 字級 | 14px | 清晰可讀 | +| Placeholder | 14-16px | 比 label 稍淡 | +| Helper/Error | 12px | 輔助說明 | +| Border Radius | 8-12px | 與系統一致 | ### 10.2 驗證策略 -| 策略 | 時機 | 適用場景 | -|------|------|----------| -| **Inline (即時)** | 離開欄位時 | Email、密碼強度 | -| **On Submit** | 送出時 | 複雜多欄位表單 | -| **Hybrid** | 關鍵欄位即時 + 整體送出 | 最佳實踐 | +| 策略 | 時機 | 適用場景 | +| ----------------- | ----------------------- | --------------- | +| **Inline (即時)** | 離開欄位時 | Email、密碼強度 | +| **On Submit** | 送出時 | 複雜多欄位表單 | +| **Hybrid** | 關鍵欄位即時 + 整體送出 | 最佳實踐 | #### Reward Early, Punish Late 原則 @@ -765,13 +775,13 @@ apps/ratewise/src/ ### 10.3 狀態視覺 -| 狀態 | Border | Background | Icon | -|------|--------|------------|------| -| Default | `slate-200` | `white` | - | -| Focus | `violet-500` + ring | `white` | - | -| Valid | `emerald-500` | `emerald-50` | ✓ | -| Error | `red-500` | `red-50` | ✗ | -| Disabled | `slate-200` | `slate-100` | - | +| 狀態 | Border | Background | Icon | +| -------- | ------------------- | ------------ | ---- | +| Default | `slate-200` | `white` | - | +| Focus | `violet-500` + ring | `white` | - | +| Valid | `emerald-500` | `emerald-50` | ✓ | +| Error | `red-500` | `red-50` | ✗ | +| Disabled | `slate-200` | `slate-100` | - | ### 10.4 貨幣輸入框(Ratewise 專用) @@ -788,12 +798,12 @@ apps/ratewise/src/ +------------------------------------------+ ``` -| 屬性 | 值 | -|------|------| -| Height | 56px | -| Font | Monospace, 20-24px | -| Currency Selector | 內嵌右側,80px 寬 | -| Thousand Separator | 自動格式化 | +| 屬性 | 值 | +| ------------------ | ------------------ | +| Height | 56px | +| Font | Monospace, 20-24px | +| Currency Selector | 內嵌右側,80px 寬 | +| Thousand Separator | 自動格式化 | --- @@ -803,24 +813,24 @@ apps/ratewise/src/ ### 11.1 動效時間規格 -| 類型 | Duration | Easing | 用途 | -|------|----------|--------|------| -| **Instant** | 0-100ms | - | 即時回饋 | -| **Fast** | 100-150ms | ease-out | 按鈕、小元素 | -| **Normal** | 200-300ms | ease-in-out | 卡片、面板 | -| **Slow** | 300-500ms | ease-in-out | Modal、頁面轉場 | +| 類型 | Duration | Easing | 用途 | +| ----------- | --------- | ----------- | --------------- | +| **Instant** | 0-100ms | - | 即時回饋 | +| **Fast** | 100-150ms | ease-out | 按鈕、小元素 | +| **Normal** | 200-300ms | ease-in-out | 卡片、面板 | +| **Slow** | 300-500ms | ease-in-out | Modal、頁面轉場 | ### 11.2 3 秒法則 > "Your animation budget sits in that 0-3s window. Use it wisely." -| 情境 | 建議時間 | -|------|----------| -| 按鈕點擊回饋 | 100-150ms | -| Hover 效果 | 200ms | -| Toast 出現 | 300ms | -| Modal 開啟 | 300-400ms | -| 頁面轉場 | 400-500ms | +| 情境 | 建議時間 | +| ------------ | ----------------- | +| 按鈕點擊回饋 | 100-150ms | +| Hover 效果 | 200ms | +| Toast 出現 | 300ms | +| Modal 開啟 | 300-400ms | +| 頁面轉場 | 400-500ms | | Loading 動畫 | 持續,1.5-2s 循環 | ### 11.3 Hover 與 Active 狀態 @@ -846,7 +856,7 @@ apps/ratewise/src/ content: ''; position: absolute; inset: 0; - background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, transparent 70%); + background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%); transform: scale(0); opacity: 0; } @@ -885,12 +895,12 @@ apps/ratewise/src/ ### 12.1 Skeleton 設計原則 -| 原則 | 說明 | -|------|------| -| **匹配佈局** | Skeleton 結構應與最終內容一致 | -| **使用 Shimmer** | 動態效果減少感知等待時間 20-30% | -| **平滑過渡** | 內容載入時使用 cross-fade | -| **避免空白** | 不使用只有 header/footer 的 skeleton | +| 原則 | 說明 | +| ---------------- | ------------------------------------ | +| **匹配佈局** | Skeleton 結構應與最終內容一致 | +| **使用 Shimmer** | 動態效果減少感知等待時間 20-30% | +| **平滑過渡** | 內容載入時使用 cross-fade | +| **避免空白** | 不使用只有 header/footer 的 skeleton | ### 12.2 Shimmer 效果實作 @@ -945,12 +955,12 @@ apps/ratewise/src/ ### 12.4 載入狀態類型 -| 類型 | 時機 | 實作 | -|------|------|------| -| **Skeleton** | 首次載入、結構已知 | 灰色區塊 + shimmer | -| **Spinner** | 短暫操作 (<2s) | 居中旋轉圖標 | -| **Progress Bar** | 長操作、進度可知 | 線性進度條 | -| **Pull-to-refresh** | 手動刷新 | 頂部 spinner | +| 類型 | 時機 | 實作 | +| ------------------- | ------------------ | ------------------ | +| **Skeleton** | 首次載入、結構已知 | 灰色區塊 + shimmer | +| **Spinner** | 短暫操作 (<2s) | 居中旋轉圖標 | +| **Progress Bar** | 長操作、進度可知 | 線性進度條 | +| **Pull-to-refresh** | 手動刷新 | 頂部 spinner | --- @@ -960,13 +970,13 @@ apps/ratewise/src/ ### 13.1 Empty State 類型 -| 類型 | 情境 | 設計重點 | -|------|------|----------| -| **First Use** | 新用戶首次使用 | 引導 + CTA | -| **User Cleared** | 完成所有任務 | 正面鼓勵 | -| **No Results** | 搜尋無結果 | 建議 + 替代方案 | -| **No Data** | 系統無資料 | 說明 + 下一步 | -| **Error** | 發生錯誤 | 清楚說明 + 解決方案 | +| 類型 | 情境 | 設計重點 | +| ---------------- | -------------- | ------------------- | +| **First Use** | 新用戶首次使用 | 引導 + CTA | +| **User Cleared** | 完成所有任務 | 正面鼓勵 | +| **No Results** | 搜尋無結果 | 建議 + 替代方案 | +| **No Data** | 系統無資料 | 說明 + 下一步 | +| **Error** | 發生錯誤 | 清楚說明 + 解決方案 | ### 13.2 Empty State 結構 @@ -989,13 +999,13 @@ apps/ratewise/src/ ### 13.3 Error State 規格 -| 錯誤類型 | 顏色 | Icon | 訊息範例 | -|----------|------|------|----------| -| **網路錯誤** | Neutral | 📶 | 無法連線,請檢查網路 | -| **載入失敗** | Warning | ⚠️ | 載入失敗,點擊重試 | -| **找不到** | Neutral | 🔍 | 找不到此頁面 | -| **伺服器錯誤** | Error | ⚠️ | 系統忙碌中,請稍後再試 | -| **權限錯誤** | Warning | 🔒 | 您沒有權限存取此內容 | +| 錯誤類型 | 顏色 | Icon | 訊息範例 | +| -------------- | ------- | ---- | ---------------------- | +| **網路錯誤** | Neutral | 📶 | 無法連線,請檢查網路 | +| **載入失敗** | Warning | ⚠️ | 載入失敗,點擊重試 | +| **找不到** | Neutral | 🔍 | 找不到此頁面 | +| **伺服器錯誤** | Error | ⚠️ | 系統忙碌中,請稍後再試 | +| **權限錯誤** | Warning | 🔒 | 您沒有權限存取此內容 | ### 13.4 Ratewise 專用 Empty States @@ -1031,12 +1041,12 @@ apps/ratewise/src/ ### 14.1 觸控目標規格 -| 規範 | 最小尺寸 | 間距 | -|------|----------|------| -| WCAG 2.2 | 24×24px | - | -| Apple HIG | 44×44pt | - | -| Android MD3 | 48×48dp | 8dp | -| 本系統 | 44×44px | 8px | +| 規範 | 最小尺寸 | 間距 | +| ----------- | -------- | ---- | +| WCAG 2.2 | 24×24px | - | +| Apple HIG | 44×44pt | - | +| Android MD3 | 48×48dp | 8dp | +| 本系統 | 44×44px | 8px | ### 14.2 Pull-to-Refresh 規格 @@ -1055,20 +1065,20 @@ apps/ratewise/src/ └──────────────────────────────────┘ ``` -| 階段 | 視覺回饋 | -|------|----------| -| 下拉中 | 顯示 spinner,opacity 隨距離增加 | -| 觸發 (≥60px) | Spinner 開始旋轉 | -| 載入中 | Spinner 持續旋轉 | -| 完成 | Spinner 收起,內容更新 | +| 階段 | 視覺回饋 | +| ------------ | -------------------------------- | +| 下拉中 | 顯示 spinner,opacity 隨距離增加 | +| 觸發 (≥60px) | Spinner 開始旋轉 | +| 載入中 | Spinner 持續旋轉 | +| 完成 | Spinner 收起,內容更新 | ### 14.3 Swipe 手勢 -| 手勢 | 動作 | 視覺回饋 | -|------|------|----------| -| Swipe Left | 刪除/封存 | 紅色背景 + 垃圾桶 icon | -| Swipe Right | 收藏/標記 | 黃色背景 + 星星 icon | -| Long Press | 多選/編輯 | 放大 + 震動 | +| 手勢 | 動作 | 視覺回饋 | +| ----------- | --------- | ---------------------- | +| Swipe Left | 刪除/封存 | 紅色背景 + 垃圾桶 icon | +| Swipe Right | 收藏/標記 | 黃色背景 + 星星 icon | +| Long Press | 多選/編輯 | 放大 + 震動 | ``` Swipe 刪除動效: @@ -1080,12 +1090,12 @@ Swipe 刪除動效: ### 14.4 無障礙替代方案 -| 手勢 | 替代操作 | -|------|----------| -| Swipe | 長按顯示選單 | +| 手勢 | 替代操作 | +| --------------- | ------------ | +| Swipe | 長按顯示選單 | | Pull-to-refresh | 頂部刷新按鈕 | -| Pinch-to-zoom | +/- 按鈕 | -| Double-tap | 明確按鈕 | +| Pinch-to-zoom | +/- 按鈕 | +| Double-tap | 明確按鈕 | --- @@ -1095,24 +1105,24 @@ Swipe 刪除動效: ### 15.1 匯率顏色規範 -| 狀態 | 顏色 | 用途 | -|------|------|------| -| **上漲** | Emerald 500 `#10b981` | 正向變動 | -| **下跌** | Red 500 `#ef4444` | 負向變動 | -| **持平** | Slate 400 `#94a3b8` | 無變動 | -| **主要數據** | Slate 900 `#0f172a` | 匯率數字 | -| **次要數據** | Slate 500 `#64748b` | 時間戳記 | +| 狀態 | 顏色 | 用途 | +| ------------ | --------------------- | -------- | +| **上漲** | Emerald 500 `#10b981` | 正向變動 | +| **下跌** | Red 500 `#ef4444` | 負向變動 | +| **持平** | Slate 400 `#94a3b8` | 無變動 | +| **主要數據** | Slate 900 `#0f172a` | 匯率數字 | +| **次要數據** | Slate 500 `#64748b` | 時間戳記 | ### 15.2 圖表顏色系統 -| 用途 | Light Mode | Dark Mode | -|------|------------|-----------| -| Primary Line | Violet 600 | Violet 400 | -| Secondary Line | Cyan 500 | Cyan 400 | -| Positive Area | Emerald 500/20 | Emerald 400/20 | -| Negative Area | Red 500/20 | Red 400/20 | -| Grid Lines | Slate 200 | Slate 700 | -| Axis Labels | Slate 500 | Slate 400 | +| 用途 | Light Mode | Dark Mode | +| -------------- | -------------- | -------------- | +| Primary Line | Violet 600 | Violet 400 | +| Secondary Line | Cyan 500 | Cyan 400 | +| Positive Area | Emerald 500/20 | Emerald 400/20 | +| Negative Area | Red 500/20 | Red 400/20 | +| Grid Lines | Slate 200 | Slate 700 | +| Axis Labels | Slate 500 | Slate 400 | ### 15.3 多幣別比較圖 @@ -1130,15 +1140,15 @@ Swipe 刪除動效: ### 15.4 數字格式化 -| 類型 | 格式 | 範例 | -|------|------|------| -| 匯率 (主要) | 4 位小數 | 31.2500 | -| 匯率 (日圓) | 4 位小數 | 0.2185 | -| 漲跌幅 | ± 百分比 | +0.12% | -| 漲跌值 | ± 數值 | +0.0375 | -| 金額 | 千分位 | 1,000.00 | -| 時間 | HH:mm | 14:30 | -| 日期 | YYYY/MM/DD | 2025/01/17 | +| 類型 | 格式 | 範例 | +| ----------- | ---------- | ---------- | +| 匯率 (主要) | 4 位小數 | 31.2500 | +| 匯率 (日圓) | 4 位小數 | 0.2185 | +| 漲跌幅 | ± 百分比 | +0.12% | +| 漲跌值 | ± 數值 | +0.0375 | +| 金額 | 千分位 | 1,000.00 | +| 時間 | HH:mm | 14:30 | +| 日期 | YYYY/MM/DD | 2025/01/17 | ### 15.5 KPI 卡片規格 @@ -1164,19 +1174,19 @@ Swipe 刪除動效: ### 16.1 對比度要求(WCAG 2.2 AA) -| 元素 | 最小對比度 | 驗證 | -|------|------------|------| -| 正文文字 | 4.5:1 | ✅ slate-900 on slate-50 = 12.6:1 | -| 大標題 (18px+) | 3:1 | ✅ | -| 互動元素 | 3:1 | ✅ violet-600 on white = 4.5:1 | -| 玻璃上文字 | 4.5:1 | ⚠️ 需要測試不同背景 | +| 元素 | 最小對比度 | 驗證 | +| -------------- | ---------- | --------------------------------- | +| 正文文字 | 4.5:1 | ✅ slate-900 on slate-50 = 12.6:1 | +| 大標題 (18px+) | 3:1 | ✅ | +| 互動元素 | 3:1 | ✅ violet-600 on white = 4.5:1 | +| 玻璃上文字 | 4.5:1 | ⚠️ 需要測試不同背景 | ### 16.2 觸控目標 -| 規範 | 最小尺寸 | -|------|----------| -| WCAG 2.2 | 24×24px | -| Apple HIG | 44×44px | +| 規範 | 最小尺寸 | +| ---------- | ------------------------------------ | +| WCAG 2.2 | 24×24px | +| Apple HIG | 44×44px | | 本系統採用 | 44×44px (min), 48×48px (comfortable) | ### 16.3 降級支援 From 524a13c3c3eb9c138b737e9ea20eea34484d2386 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Jan 2026 01:41:50 +0000 Subject: [PATCH 4/7] feat(a11y): implement 2025 design standards compliance P0 - Accessibility Compliance: - Increase --button-height-sm from 32px to 44px (WCAG 2.2) - Increase --input-height-sm from 36px to 44px (WCAG 2.2) - Increase --font-size-xs from 12px to 14px (readability) P1 - 2025 Standards: - Add fluid typography with clamp() (--font-size-fluid-*) - Add responsive breakpoint tokens (--breakpoint-xs/sm/md/lg/xl/2xl) - Add mobile-optimized body text (18px default on mobile) P2 - Enhanced Experience: - Add Container Query support (.container-responsive, @container) - Add density tokens ([data-density='compact/comfortable']) - Add M3 Expressive shape system (--shape-*) References: - WCAG 2.2 Target Size Minimum (2.5.8) - Apple HIG 44pt touch targets - Material Design 3 Expressive --- apps/ratewise/src/config/design-tokens.ts | 56 +++++++ apps/ratewise/src/styles/tokens.css | 178 ++++++++++++++++++++-- docs/dev/006_design_modernization_spec.md | 32 +++- 3 files changed, 252 insertions(+), 14 deletions(-) diff --git a/apps/ratewise/src/config/design-tokens.ts b/apps/ratewise/src/config/design-tokens.ts index e2221002..ecd7795a 100644 --- a/apps/ratewise/src/config/design-tokens.ts +++ b/apps/ratewise/src/config/design-tokens.ts @@ -479,6 +479,53 @@ export const zIndexTokens = { tooltip: 'var(--z-tooltip)', } as const; +/** + * Responsive Breakpoint Tokens - P1-2 + * + * @description 響應式斷點系統 (2025 Best Practices) + * @reference https://tailwindcss.com/docs/responsive-design + */ +export const screenTokens = { + xs: 'var(--breakpoint-xs)', // 375px - Small phones + sm: 'var(--breakpoint-sm)', // 640px - Large phones + md: 'var(--breakpoint-md)', // 768px - Tablets + lg: 'var(--breakpoint-lg)', // 1024px - Small desktops + xl: 'var(--breakpoint-xl)', // 1280px - Large desktops + '2xl': 'var(--breakpoint-2xl)', // 1536px - Extra large +} as const; + +/** + * Fluid Typography Tokens - P1-1 + * + * @description 流體排版系統使用 clamp() 實現平滑縮放 + */ +export const fluidFontSizeTokens: Record = { + sm: ['var(--font-size-fluid-sm)', { lineHeight: 'var(--line-height-normal)' }], + base: ['var(--font-size-fluid-base)', { lineHeight: 'var(--line-height-normal)' }], + lg: ['var(--font-size-fluid-lg)', { lineHeight: 'var(--line-height-snug)' }], + xl: ['var(--font-size-fluid-xl)', { lineHeight: 'var(--line-height-snug)' }], + '2xl': ['var(--font-size-fluid-2xl)', { lineHeight: 'var(--line-height-tight)' }], + '3xl': ['var(--font-size-fluid-3xl)', { lineHeight: 'var(--line-height-tight)' }], + '4xl': ['var(--font-size-fluid-4xl)', { lineHeight: 'var(--line-height-tight)' }], + '5xl': ['var(--font-size-fluid-5xl)', { lineHeight: 'var(--line-height-tight)' }], +}; + +/** + * M3 Expressive Shape Tokens - P2-3 + * + * @description Material Design 3 形狀系統 + * @reference https://m3.material.io/styles/shape/overview + */ +export const shapeTokens = { + none: 'var(--shape-none)', + 'extra-small': 'var(--shape-extra-small)', + small: 'var(--shape-small)', + medium: 'var(--shape-medium)', + large: 'var(--shape-large)', + 'extra-large': 'var(--shape-extra-large)', + full: 'var(--shape-full)', +} as const; + /** * 取得 Design Token 配置 * @@ -535,5 +582,14 @@ export function generateTailwindThemeExtension(): Config['theme'] { transitionTimingFunction: transitionTimingTokens, zIndex: zIndexTokens, }, + // P1-2: Responsive breakpoints as CSS Variables + screens: { + xs: '375px', + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, }; } diff --git a/apps/ratewise/src/styles/tokens.css b/apps/ratewise/src/styles/tokens.css index 59ae1ed9..1cbe0991 100644 --- a/apps/ratewise/src/styles/tokens.css +++ b/apps/ratewise/src/styles/tokens.css @@ -102,8 +102,8 @@ --font-sans: 'Noto Sans TC', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; --font-mono: 'SF Mono', ui-monospace, 'Cascadia Code', 'Roboto Mono', Consolas, monospace; - /* Font Sizes */ - --font-size-xs: 12px; + /* Font Sizes (2025 Best Practices: min 14px for readability) */ + --font-size-xs: 14px; /* P0 Fix: 12px → 14px for WCAG readability */ --font-size-sm: 14px; --font-size-base: 16px; --font-size-lg: 18px; @@ -113,6 +113,16 @@ --font-size-4xl: 36px; --font-size-5xl: 48px; + /* P1: Fluid Typography with clamp() */ + --font-size-fluid-sm: clamp(14px, 0.875rem + 0.1vw, 15px); + --font-size-fluid-base: clamp(16px, 1rem + 0.2vw, 18px); + --font-size-fluid-lg: clamp(18px, 1.125rem + 0.3vw, 21px); + --font-size-fluid-xl: clamp(20px, 1.25rem + 0.4vw, 24px); + --font-size-fluid-2xl: clamp(24px, 1.5rem + 0.5vw, 30px); + --font-size-fluid-3xl: clamp(30px, 1.875rem + 0.75vw, 38px); + --font-size-fluid-4xl: clamp(36px, 2.25rem + 1vw, 48px); + --font-size-fluid-5xl: clamp(48px, 3rem + 1.5vw, 64px); + /* Font Weights */ --font-weight-normal: 400; --font-weight-medium: 500; @@ -180,6 +190,46 @@ --z-popover: 1500; --z-toast: 1600; --z-tooltip: 1700; + + /* -------------------------------------------------- + 1.8 Responsive Breakpoints (P1-2: 2025 Best Practices) + @see https://tailwindcss.com/docs/responsive-design + -------------------------------------------------- */ + --breakpoint-xs: 375px; /* Small phones (iPhone SE) */ + --breakpoint-sm: 640px; /* Large phones */ + --breakpoint-md: 768px; /* Tablets */ + --breakpoint-lg: 1024px; /* Small desktops */ + --breakpoint-xl: 1280px; /* Large desktops */ + --breakpoint-2xl: 1536px; /* Extra large screens */ + + /* -------------------------------------------------- + 1.9 Density Tokens (P2-2: M3 Density System) + @see https://m3.material.io/foundations/layout/density + -------------------------------------------------- */ + --density-compact: -4px; /* Tighter spacing for data-dense UIs */ + --density-normal: 0px; /* Default spacing */ + --density-comfortable: 4px; /* Extra breathing room */ + + /* Density-adjusted spacing */ + --spacing-density-1: calc(var(--spacing-1) + var(--density-normal)); + --spacing-density-2: calc(var(--spacing-2) + var(--density-normal)); + --spacing-density-4: calc(var(--spacing-4) + var(--density-normal)); + + /* -------------------------------------------------- + 1.10 M3 Expressive Shape Tokens (P2-3) + @see https://m3.material.io/styles/shape/overview + -------------------------------------------------- */ + --shape-none: 0px; + --shape-extra-small: 4px; + --shape-small: 8px; + --shape-medium: 12px; + --shape-large: 16px; + --shape-extra-large: 28px; + --shape-full: 9999px; + + /* Shape morphing for interactive states */ + --shape-pressed: var(--shape-medium); + --shape-hovered: var(--shape-large); } /* ============================================================ @@ -317,18 +367,18 @@ --icon-xl: 32px; /* -------------------------------------------------- - 4.4 Button Heights + 4.4 Button Heights (WCAG 2.2 + Apple HIG 44pt minimum) -------------------------------------------------- */ - --button-height-sm: 32px; - --button-height-md: 40px; - --button-height-lg: 48px; + --button-height-sm: 44px; /* P0 Fix: 32px → 44px for touch target compliance */ + --button-height-md: 48px; /* MD3 comfortable touch target */ + --button-height-lg: 56px; /* Large touch target */ /* -------------------------------------------------- - 4.5 Input Heights + 4.5 Input Heights (WCAG 2.2 + Apple HIG 44pt minimum) -------------------------------------------------- */ - --input-height-sm: 36px; - --input-height-md: 44px; - --input-height-lg: 52px; + --input-height-sm: 44px; /* P0 Fix: 36px → 44px for touch target compliance */ + --input-height-md: 48px; /* MD3 comfortable touch target */ + --input-height-lg: 56px; /* Large touch target */ } /* ============================================================ @@ -584,3 +634,111 @@ .safe-area-inset-top { padding-top: env(safe-area-inset-top, 0px); } + +/* ============================================================ + 9. RESPONSIVE TYPOGRAPHY - P1-3: Mobile 優化 + ============================================================ */ + +/* Mobile-first: Default is mobile (use larger body text) */ +:root { + --font-size-body: var(--font-size-lg); /* 18px on mobile for better readability */ + --font-size-body-secondary: var(--font-size-base); /* 16px */ +} + +/* Tablet and above: Can use standard sizes */ +@media (min-width: 768px) { + :root { + --font-size-body: var(--font-size-base); /* 16px on desktop */ + --font-size-body-secondary: var(--font-size-sm); /* 14px */ + } +} + +/* ============================================================ + 10. CONTAINER QUERIES - P2-1: Component-based Responsiveness + ============================================================ */ + +/* Container Query Base Classes */ +.container-responsive { + container-type: inline-size; + container-name: responsive; +} + +.container-card { + container-type: inline-size; + container-name: card; +} + +.container-form { + container-type: inline-size; + container-name: form; +} + +/* Container Query Breakpoints */ +@container responsive (min-width: 400px) { + .cq\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@container responsive (min-width: 600px) { + .cq\:grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@container card (min-width: 300px) { + .cq\:card-horizontal { + flex-direction: row; + } + .cq\:card-padding-lg { + padding: var(--spacing-6); + } +} + +@container form (min-width: 500px) { + .cq\:form-inline { + flex-direction: row; + align-items: center; + gap: var(--spacing-4); + } +} + +/* ============================================================ + 11. DENSITY VARIANTS - P2-2: Compact/Comfortable Modes + ============================================================ */ + +/* Compact Mode - for data-dense interfaces */ +[data-density='compact'] { + --density-normal: -4px; + --spacing-1: 2px; + --spacing-2: 4px; + --spacing-3: 8px; + --spacing-4: 12px; + --spacing-6: 16px; + --spacing-8: 24px; + + --font-size-base: 14px; + --font-size-sm: 12px; + --font-size-xs: 11px; + + --button-height-sm: 36px; + --button-height-md: 40px; + --input-height-sm: 36px; + --input-height-md: 40px; +} + +/* Comfortable Mode - extra breathing room */ +[data-density='comfortable'] { + --density-normal: 4px; + --spacing-1: 6px; + --spacing-2: 12px; + --spacing-3: 16px; + --spacing-4: 20px; + --spacing-6: 28px; + --spacing-8: 40px; + + --button-height-sm: 48px; + --button-height-md: 56px; + --input-height-sm: 48px; + --input-height-md: 56px; +} diff --git a/docs/dev/006_design_modernization_spec.md b/docs/dev/006_design_modernization_spec.md index 3ff98ab0..71b3ed99 100644 --- a/docs/dev/006_design_modernization_spec.md +++ b/docs/dev/006_design_modernization_spec.md @@ -1,10 +1,34 @@ # Ratewise Design Modernization Specification -> **版本**: 2.1.0 +> **版本**: 3.0.0 > **建立日期**: 2025-01-17 -> **最後更新**: 2025-01-17 -> **狀態**: 📋 規劃中 -> **依據**: W3C DTCG 2025.10, Apple Liquid Glass, Material Design 3, Android/iOS HIG +> **最後更新**: 2025-01-18 +> **狀態**: ✅ 已實作 +> **依據**: W3C DTCG 2025.10, Apple Liquid Glass, Material Design 3, Android/iOS HIG, WCAG 2.2 + +--- + +## 變更日誌 + +### v3.0.0 (2025-01-18) - 2025 標準合規更新 + +#### P0 - 無障礙合規 (已完成) + +- ✅ `--button-height-sm`: 32px → 44px (WCAG 2.2 觸控目標) +- ✅ `--input-height-sm`: 36px → 44px (WCAG 2.2 觸控目標) +- ✅ `--font-size-xs`: 12px → 14px (可讀性最低標準) + +#### P1 - 2025 標準合規 (已完成) + +- ✅ 新增流體排版 `clamp()` tokens (`--font-size-fluid-*`) +- ✅ 新增響應式斷點 tokens (`--breakpoint-xs/sm/md/lg/xl/2xl`) +- ✅ Mobile 內文優化:使用 18px 作為預設 body 字級 + +#### P2 - 增強體驗 (已完成) + +- ✅ 新增 Container Query 支援 (`.container-responsive`, `@container`) +- ✅ 新增密度 tokens (`[data-density='compact/comfortable']`) +- ✅ 對齊 M3 Expressive 形狀系統 (`--shape-*`) --- From e0b850f6345af1b980775730a328fae3aeb225bb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Jan 2026 10:51:30 +0000 Subject: [PATCH 5/7] feat(nav): add responsive navigation system (BottomTab/NavRail/Sidebar) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Navigation Components: - BottomTabBar.tsx: Mobile navigation bar (56px, MD3 spec) - NavRail.tsx: Tablet navigation rail (80px, MD3 spec) - Sidebar.tsx: Desktop sidebar (256px/72px collapsed) - All use glass effect tokens and CSS Variables Layout System: - ResponsiveLayout.tsx: Integrates all navigation variants - ContentSection: Glass card wrapper component - PageHeader: Unified page header component Chart Optimization: - ChartContainer.tsx: Controlled width/height ratios - inline: max-height 120px, aspect-ratio 3:1 - card: max-width 480px, max-height 180px, aspect-ratio 2.5:1 - full: max-width 600px, max-height 240px, aspect-ratio 2:1 - MiniChart: 80x40px for list items - TrendIndicator: Trend display with arrow Breakpoint Strategy: - Mobile (<768px): BottomTabBar + bottom padding - Tablet (768-1023px): NavRail + left padding 80px - Desktop (≥1024px): Sidebar + left padding 256px --- .../src/components/chart/ChartContainer.tsx | 200 ++++++++++++++ apps/ratewise/src/components/chart/index.ts | 8 + .../components/layout/ResponsiveLayout.tsx | 168 ++++++++++++ apps/ratewise/src/components/layout/index.ts | 8 + .../components/navigation/BottomTabBar.tsx | 120 +++++++++ .../src/components/navigation/NavRail.tsx | 124 +++++++++ .../src/components/navigation/Sidebar.tsx | 252 ++++++++++++++++++ .../src/components/navigation/index.ts | 11 + 8 files changed, 891 insertions(+) create mode 100644 apps/ratewise/src/components/chart/ChartContainer.tsx create mode 100644 apps/ratewise/src/components/chart/index.ts create mode 100644 apps/ratewise/src/components/layout/ResponsiveLayout.tsx create mode 100644 apps/ratewise/src/components/layout/index.ts create mode 100644 apps/ratewise/src/components/navigation/BottomTabBar.tsx create mode 100644 apps/ratewise/src/components/navigation/NavRail.tsx create mode 100644 apps/ratewise/src/components/navigation/Sidebar.tsx create mode 100644 apps/ratewise/src/components/navigation/index.ts diff --git a/apps/ratewise/src/components/chart/ChartContainer.tsx b/apps/ratewise/src/components/chart/ChartContainer.tsx new file mode 100644 index 00000000..1c98f63b --- /dev/null +++ b/apps/ratewise/src/components/chart/ChartContainer.tsx @@ -0,0 +1,200 @@ +/** + * ChartContainer - 趨勢圖容器 + * + * @description 統一圖表容器,控制寬高比例避免圖表太寬或太高 + * + * 設計原則: + * - Mobile: 100% 寬度,aspect-ratio 16:9 (較矮) + * - Tablet: max-width 600px,aspect-ratio 2:1 + * - Desktop: max-width 480px,aspect-ratio 2.5:1 (更緊湊) + * + * 高度限制: + * - 最大高度 200px,避免佔用過多垂直空間 + * - 最小高度 120px,確保圖表可讀性 + * + * @version 1.0.0 + * @created 2025-01-18 + */ + +import type { ReactNode } from 'react'; + +interface ChartContainerProps { + children: ReactNode; + /** 圖表變體 */ + variant?: 'inline' | 'card' | 'full'; + /** 自訂 className */ + className?: string; + /** 顯示標題 */ + title?: string; + /** 副標題 */ + subtitle?: string; +} + +/** + * 圖表尺寸配置 + * + * variant 說明: + * - inline: 內嵌在卡片中的小圖表 (max-height: 120px) + * - card: 獨立卡片中的圖表 (max-height: 180px) + * - full: 全寬圖表頁面 (max-height: 240px) + */ +const variantStyles = { + inline: { + maxWidth: '100%', + maxHeight: '120px', + minHeight: '80px', + aspectRatio: '3 / 1', + }, + card: { + maxWidth: '480px', + maxHeight: '180px', + minHeight: '120px', + aspectRatio: '2.5 / 1', + }, + full: { + maxWidth: '600px', + maxHeight: '240px', + minHeight: '160px', + aspectRatio: '2 / 1', + }, +}; + +export function ChartContainer({ + children, + variant = 'card', + className = '', + title, + subtitle, +}: ChartContainerProps) { + const styles = variantStyles[variant]; + + return ( +
+ {/* Header */} + {(title !== undefined || subtitle !== undefined) && ( +
+ {title && ( +

+ {title} +

+ )} + {subtitle && ( +

+ {subtitle} +

+ )} +
+ )} + + {/* Chart Area */} +
+ {children} +
+
+ ); +} + +/** + * MiniChart - 迷你圖表容器(用於列表項目) + * + * @description 極簡圖表,高度固定 40px + */ +interface MiniChartProps { + children: ReactNode; + className?: string; +} + +export function MiniChart({ children, className = '' }: MiniChartProps) { + return ( +
+ {children} +
+ ); +} + +/** + * TrendIndicator - 趨勢指示器(數字+箭頭) + * + * @description 顯示漲跌趨勢的小型指示器 + */ +interface TrendIndicatorProps { + value: number; + /** 百分比還是絕對值 */ + type?: 'percent' | 'absolute'; + /** 尺寸 */ + size?: 'sm' | 'md' | 'lg'; +} + +export function TrendIndicator({ value, type = 'percent', size = 'md' }: TrendIndicatorProps) { + const isPositive = value > 0; + const isNeutral = value === 0; + + const sizeStyles = { + sm: { fontSize: '12px', iconSize: 12 }, + md: { fontSize: '14px', iconSize: 14 }, + lg: { fontSize: '16px', iconSize: 16 }, + }; + + const { fontSize, iconSize } = sizeStyles[size]; + + const displayValue = + type === 'percent' ? `${isPositive ? '+' : ''}${value.toFixed(2)}%` : value.toFixed(4); + + return ( + + {!isNeutral && ( + + + + )} + {displayValue} + + ); +} diff --git a/apps/ratewise/src/components/chart/index.ts b/apps/ratewise/src/components/chart/index.ts new file mode 100644 index 00000000..9edb457c --- /dev/null +++ b/apps/ratewise/src/components/chart/index.ts @@ -0,0 +1,8 @@ +/** + * Chart Components - Export Index + * + * @version 1.0.0 + * @created 2025-01-18 + */ + +export { ChartContainer, MiniChart, TrendIndicator } from './ChartContainer'; diff --git a/apps/ratewise/src/components/layout/ResponsiveLayout.tsx b/apps/ratewise/src/components/layout/ResponsiveLayout.tsx new file mode 100644 index 00000000..2cc05091 --- /dev/null +++ b/apps/ratewise/src/components/layout/ResponsiveLayout.tsx @@ -0,0 +1,168 @@ +/** + * ResponsiveLayout - 三端響應式佈局容器 + * + * @description 整合 BottomTabBar / NavRail / Sidebar 的響應式佈局 + * + * 斷點策略: + * - Mobile (<768px): BottomTabBar + 底部 padding + * - Tablet (768-1023px): NavRail + 左側 padding + * - Desktop (≥1024px): Sidebar + 左側 padding + * + * 設計規格: + * - Mobile: padding-bottom: 56px + safe-area + * - Tablet: padding-left: 80px + * - Desktop: padding-left: 256px (展開) / 72px (收合) + * + * @version 1.0.0 + * @created 2025-01-18 + */ + +import type { ReactNode } from 'react'; +import { BottomTabBar, NavRail, Sidebar } from '../navigation'; + +interface ResponsiveLayoutProps { + children: ReactNode; + /** 是否顯示導覽列(某些頁面可能需要隱藏) */ + showNavigation?: boolean; + /** 內容區最大寬度 */ + maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'; +} + +const maxWidthMap = { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + full: '100%', +}; + +export function ResponsiveLayout({ + children, + showNavigation = true, + maxWidth = 'xl', +}: ResponsiveLayoutProps) { + return ( +
+ {/* Navigation Components - Rendered based on screen size via CSS */} + {showNavigation && ( + <> + + + + + )} + + {/* Main Content Area */} +
+ {/* Content Container with max-width */} +
+ {children} +
+
+
+ ); +} + +/** + * ContentSection - 內容區塊容器 + * + * @description 提供一致的內容區塊樣式(玻璃效果卡片) + */ +interface ContentSectionProps { + children: ReactNode; + className?: string; + /** 使用玻璃效果 */ + glass?: boolean; + /** 自訂 padding */ + padding?: 'none' | 'sm' | 'md' | 'lg'; +} + +const paddingMap = { + none: '0', + sm: 'var(--spacing-3)', + md: 'var(--spacing-4)', + lg: 'var(--spacing-6)', +}; + +export function ContentSection({ + children, + className = '', + glass = true, + padding = 'md', +}: ContentSectionProps) { + return ( +
+ {children} +
+ ); +} + +/** + * PageHeader - 頁面標題區 + * + * @description 統一的頁面標題樣式 + */ +interface PageHeaderProps { + title: string; + subtitle?: string; + actions?: ReactNode; +} + +export function PageHeader({ title, subtitle, actions }: PageHeaderProps) { + return ( +
+
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )} +
+ {actions &&
{actions}
} +
+ ); +} diff --git a/apps/ratewise/src/components/layout/index.ts b/apps/ratewise/src/components/layout/index.ts new file mode 100644 index 00000000..35568c7b --- /dev/null +++ b/apps/ratewise/src/components/layout/index.ts @@ -0,0 +1,8 @@ +/** + * Layout Components - Export Index + * + * @version 1.0.0 + * @created 2025-01-18 + */ + +export { ResponsiveLayout, ContentSection, PageHeader } from './ResponsiveLayout'; diff --git a/apps/ratewise/src/components/navigation/BottomTabBar.tsx b/apps/ratewise/src/components/navigation/BottomTabBar.tsx new file mode 100644 index 00000000..1fbaa60d --- /dev/null +++ b/apps/ratewise/src/components/navigation/BottomTabBar.tsx @@ -0,0 +1,120 @@ +/** + * BottomTabBar - Mobile Navigation (Material Design 3) + * + * @description MD3 Navigation Bar for mobile devices (<768px) + * @see https://m3.material.io/components/navigation-bar/guidelines + * @see component-tokens.ts - bottomTabBar + * + * 設計規格: + * - 高度: 56px (MD3 標準) + * - Tab 寬度: 64-96px + * - 圖示: 24px + * - 標籤: 12px (xs) + * - 玻璃效果背景 + * + * @version 1.0.0 + * @created 2025-01-18 + */ + +import { Home, List, Settings, TrendingUp } from 'lucide-react'; +import { NavLink, useLocation } from 'react-router-dom'; + +interface TabItem { + path: string; + icon: React.ElementType; + label: string; +} + +const tabs: TabItem[] = [ + { path: '/', icon: Home, label: '首頁' }, + { path: '/list', icon: List, label: '列表' }, + { path: '/trends', icon: TrendingUp, label: '趨勢' }, + { path: '/settings', icon: Settings, label: '設定' }, +]; + +export function BottomTabBar() { + const location = useLocation(); + + return ( + + ); +} diff --git a/apps/ratewise/src/components/navigation/NavRail.tsx b/apps/ratewise/src/components/navigation/NavRail.tsx new file mode 100644 index 00000000..1e1c5c85 --- /dev/null +++ b/apps/ratewise/src/components/navigation/NavRail.tsx @@ -0,0 +1,124 @@ +/** + * NavRail - Tablet Navigation (Material Design 3) + * + * @description MD3 Navigation Rail for tablets (768px - 1023px) + * @see https://m3.material.io/components/navigation-rail/guidelines + * @see component-tokens.ts - navRail + * + * 設計規格: + * - 寬度: 80px (MD3 標準) + * - 項目: 56x56px + * - 圖示: 24px + * - 標籤: 12px + * - 玻璃效果背景 + * + * @version 1.0.0 + * @created 2025-01-18 + */ + +import { Home, List, Settings, TrendingUp, Star } from 'lucide-react'; +import { NavLink, useLocation } from 'react-router-dom'; + +interface NavItem { + path: string; + icon: React.ElementType; + label: string; +} + +const navItems: NavItem[] = [ + { path: '/', icon: Home, label: '首頁' }, + { path: '/list', icon: List, label: '列表' }, + { path: '/favorites', icon: Star, label: '收藏' }, + { path: '/trends', icon: TrendingUp, label: '趨勢' }, + { path: '/settings', icon: Settings, label: '設定' }, +]; + +export function NavRail() { + const location = useLocation(); + + return ( + + ); +} diff --git a/apps/ratewise/src/components/navigation/Sidebar.tsx b/apps/ratewise/src/components/navigation/Sidebar.tsx new file mode 100644 index 00000000..0e50ab00 --- /dev/null +++ b/apps/ratewise/src/components/navigation/Sidebar.tsx @@ -0,0 +1,252 @@ +/** + * Sidebar - Desktop Navigation + * + * @description 桌面端側邊欄導覽 (≥1024px) + * @see component-tokens.ts - sidebar + * + * 設計規格: + * - 展開寬度: 256px + * - 收合寬度: 72px + * - 項目高度: 40px + * - 玻璃效果背景 + * + * @version 1.0.0 + * @created 2025-01-18 + */ + +import { + Home, + List, + Settings, + TrendingUp, + Star, + HelpCircle, + ChevronLeft, + ChevronRight, +} from 'lucide-react'; +import { NavLink, useLocation } from 'react-router-dom'; +import { useState } from 'react'; + +interface NavItem { + path: string; + icon: React.ElementType; + label: string; +} + +interface NavSection { + title?: string; + items: NavItem[]; +} + +const navSections: NavSection[] = [ + { + items: [ + { path: '/', icon: Home, label: '首頁' }, + { path: '/list', icon: List, label: '貨幣列表' }, + { path: '/favorites', icon: Star, label: '收藏貨幣' }, + { path: '/trends', icon: TrendingUp, label: '趨勢分析' }, + ], + }, + { + title: '更多', + items: [ + { path: '/settings', icon: Settings, label: '設定' }, + { path: '/faq', icon: HelpCircle, label: '常見問題' }, + ], + }, +]; + +export function Sidebar() { + const location = useLocation(); + const [isCollapsed, setIsCollapsed] = useState(false); + + const sidebarWidth = isCollapsed ? '72px' : '256px'; + + return ( + + ); +} diff --git a/apps/ratewise/src/components/navigation/index.ts b/apps/ratewise/src/components/navigation/index.ts new file mode 100644 index 00000000..f8f4b12f --- /dev/null +++ b/apps/ratewise/src/components/navigation/index.ts @@ -0,0 +1,11 @@ +/** + * Navigation Components - Export Index + * + * @description 響應式導覽元件匯出 + * @version 1.0.0 + * @created 2025-01-18 + */ + +export { BottomTabBar } from './BottomTabBar'; +export { NavRail } from './NavRail'; +export { Sidebar } from './Sidebar'; From 94d1aae620ca2c810c1929ff52aec20464b0b6ac Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Jan 2026 17:14:19 +0000 Subject: [PATCH 6/7] feat(ui): add glassmorphism UI components with integrated trend chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GlassCard component with variant styles (base/elevated/overlay/subtle) - Add CurrencyInput with embedded currency selector and calculator button - Add RateDisplayCard with integrated 80px mini trend chart slot - Add CompactRateCard for list views (64px height) - Refactor SingleConverter to use new modular UI components - Update tests to match new component structure - All comments converted to English for open-source standards - WCAG 2.2 compliant touch targets (≥44px) - Design tokens integration for consistent theming --- .../src/components/ui/CurrencyInput.tsx | 275 ++++++++++++ apps/ratewise/src/components/ui/GlassCard.tsx | 171 ++++++++ .../src/components/ui/RateDisplayCard.tsx | 356 +++++++++++++++ apps/ratewise/src/components/ui/index.ts | 17 + .../ratewise/components/SingleConverter.tsx | 405 +++++------------- .../__tests__/SingleConverter.core.test.tsx | 21 +- .../SingleConverter.integration.test.tsx | 9 +- 7 files changed, 944 insertions(+), 310 deletions(-) create mode 100644 apps/ratewise/src/components/ui/CurrencyInput.tsx create mode 100644 apps/ratewise/src/components/ui/GlassCard.tsx create mode 100644 apps/ratewise/src/components/ui/RateDisplayCard.tsx create mode 100644 apps/ratewise/src/components/ui/index.ts diff --git a/apps/ratewise/src/components/ui/CurrencyInput.tsx b/apps/ratewise/src/components/ui/CurrencyInput.tsx new file mode 100644 index 00000000..331b1781 --- /dev/null +++ b/apps/ratewise/src/components/ui/CurrencyInput.tsx @@ -0,0 +1,275 @@ +/** + * CurrencyInput - Unified currency amount input component + * + * A compound input component that combines currency selection, amount input, + * and calculator access in a single cohesive UI element. + * + * Features: + * - Embedded currency selector (left side) + * - Embedded calculator button (right side) + * - Touch targets ≥44px (WCAG 2.2 compliant) + * - Design token integration + * + * @module components/ui/CurrencyInput + */ + +import { useRef, useState } from 'react'; +import { Calculator } from 'lucide-react'; + +interface CurrencyOption { + code: string; + flag: string; + name: string; +} + +interface CurrencyInputProps { + /** Current currency code */ + currency: string; + /** Amount value (raw string) */ + value: string; + /** Callback when amount changes */ + onChange: (value: string) => void; + /** Callback when currency changes */ + onCurrencyChange: (currency: string) => void; + /** Callback to open calculator */ + onOpenCalculator?: () => void; + /** Available currency options */ + currencies: CurrencyOption[]; + /** Formatted display value (optional) */ + displayValue?: string; + placeholder?: string; + /** Visual variant */ + variant?: 'default' | 'highlighted'; + /** Field label */ + label?: string; + /** Input aria-label */ + 'aria-label'?: string; + /** Currency selector aria-label */ + selectAriaLabel?: string; + /** Calculator button aria-label */ + calculatorAriaLabel?: string; + /** Input data-testid */ + 'data-testid'?: string; + /** Calculator button data-testid */ + calculatorTestId?: string; +} + +export function CurrencyInput({ + currency, + value, + onChange, + onCurrencyChange, + onOpenCalculator, + currencies, + displayValue, + placeholder = '0.00', + variant = 'default', + label, + 'aria-label': ariaLabel, + selectAriaLabel, + calculatorAriaLabel, + 'data-testid': dataTestId, + calculatorTestId, +}: CurrencyInputProps) { + const inputRef = useRef(null); + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState(''); + + const handleFocus = () => { + setIsEditing(true); + setEditValue(value); + }; + + const handleBlur = () => { + onChange(editValue); + setIsEditing(false); + setEditValue(''); + }; + + const handleChange = (e: React.ChangeEvent) => { + // Clean input: remove non-numeric characters except decimal point + const cleaned = e.target.value.replace(/[^\d.]/g, ''); + // Handle multiple decimal points: keep only the first one + const parts = cleaned.split('.'); + const validValue = parts.length > 2 ? parts[0] + '.' + parts.slice(1).join('') : cleaned; + setEditValue(validValue); + onChange(validValue); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + const allowedKeys = [ + 'Backspace', + 'Delete', + 'ArrowLeft', + 'ArrowRight', + 'ArrowUp', + 'ArrowDown', + 'Home', + 'End', + 'Tab', + '.', + ]; + const isNumber = /^[0-9]$/.test(e.key); + const isModifierKey = e.ctrlKey || e.metaKey; + if (!isNumber && !allowedKeys.includes(e.key) && !isModifierKey) { + e.preventDefault(); + } + }; + + const isHighlighted = variant === 'highlighted'; + + return ( +
+ {label && ( + + )} + +
+ {/* Currency selector */} + + + {/* Divider */} +
+ + {/* Amount input */} + + + {/* Calculator button */} + {onOpenCalculator && ( + + )} +
+
+ ); +} + +/** + * QuickAmountButtons - Preset amount selection buttons + */ +interface QuickAmountButtonsProps { + /** Preset amounts to display */ + amounts: readonly number[]; + /** Callback when an amount is selected */ + onSelect: (amount: number) => void; + /** Visual variant */ + variant?: 'default' | 'primary'; +} + +export function QuickAmountButtons({ + amounts, + onSelect, + variant = 'default', +}: QuickAmountButtonsProps) { + const isPrimary = variant === 'primary'; + + return ( +
+ {amounts.map((amount) => ( + + ))} +
+ ); +} diff --git a/apps/ratewise/src/components/ui/GlassCard.tsx b/apps/ratewise/src/components/ui/GlassCard.tsx new file mode 100644 index 00000000..720a2808 --- /dev/null +++ b/apps/ratewise/src/components/ui/GlassCard.tsx @@ -0,0 +1,171 @@ +/** + * GlassCard - Glassmorphism card component + * + * A reusable card component implementing Apple's Liquid Glass design language. + * Uses CSS custom properties from the design token system for consistent styling. + * + * @see https://developer.apple.com/documentation/TechnologyOverviews/liquid-glass + * @module components/ui/GlassCard + */ + +import type { ReactNode, CSSProperties } from 'react'; + +type GlassVariant = 'base' | 'elevated' | 'overlay' | 'subtle'; +type GlassPadding = 'none' | 'sm' | 'md' | 'lg' | 'xl'; + +interface GlassCardProps { + children: ReactNode; + /** Glass effect intensity level */ + variant?: GlassVariant; + /** Internal padding size */ + padding?: GlassPadding; + /** Enable glow effect */ + glow?: boolean; + /** Enable hover interactions */ + interactive?: boolean; + className?: string; + style?: CSSProperties; + onClick?: () => void; + 'aria-label'?: string; +} + +const variantStyles: Record = { + base: { + background: 'var(--glass-surface-base)', + backdropFilter: 'blur(var(--glass-blur-md, 16px))', + WebkitBackdropFilter: 'blur(var(--glass-blur-md, 16px))', + border: '1px solid var(--glass-border-light)', + }, + elevated: { + background: 'var(--glass-surface-elevated)', + backdropFilter: 'blur(var(--glass-blur-lg, 24px))', + WebkitBackdropFilter: 'blur(var(--glass-blur-lg, 24px))', + border: '1px solid var(--glass-border-medium)', + boxShadow: 'var(--shadow-lg)', + }, + overlay: { + background: 'var(--glass-surface-overlay)', + backdropFilter: 'blur(var(--glass-blur-xl, 32px))', + WebkitBackdropFilter: 'blur(var(--glass-blur-xl, 32px))', + border: '1px solid var(--glass-border-strong)', + boxShadow: 'var(--shadow-xl)', + }, + subtle: { + background: 'rgba(255, 255, 255, 0.04)', + backdropFilter: 'blur(var(--glass-blur-sm, 8px))', + WebkitBackdropFilter: 'blur(var(--glass-blur-sm, 8px))', + border: '1px solid rgba(255, 255, 255, 0.08)', + }, +}; + +const paddingStyles: Record = { + none: '0', + sm: 'var(--spacing-2, 8px)', + md: 'var(--spacing-4, 16px)', + lg: 'var(--spacing-6, 24px)', + xl: 'var(--spacing-8, 32px)', +}; + +export function GlassCard({ + children, + variant = 'base', + padding = 'md', + glow = false, + interactive = false, + className = '', + style, + onClick, + 'aria-label': ariaLabel, +}: GlassCardProps) { + const variantStyle = variantStyles[variant]; + const baseStyles: CSSProperties = { + ...variantStyle, + padding: paddingStyles[padding], + borderRadius: 'var(--radius-xl, 16px)', + transition: 'all var(--duration-normal, 200ms) var(--easing-ease-out)', + ...(glow && { + boxShadow: [variantStyle.boxShadow, 'var(--glass-shadow-glow)'].filter(Boolean).join(', '), + }), + ...style, + }; + + const Component = onClick ? 'button' : 'div'; + + return ( + + {children} + + ); +} + +interface GlassCardHeaderProps { + title: string; + subtitle?: string; + action?: ReactNode; +} + +export function GlassCardHeader({ title, subtitle, action }: GlassCardHeaderProps) { + return ( +
+
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )} +
+ {action &&
{action}
} +
+ ); +} + +interface GlassCardBodyProps { + children: ReactNode; + className?: string; +} + +export function GlassCardBody({ children, className = '' }: GlassCardBodyProps) { + return
{children}
; +} + +interface GlassCardFooterProps { + children: ReactNode; + className?: string; +} + +export function GlassCardFooter({ children, className = '' }: GlassCardFooterProps) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/ratewise/src/components/ui/RateDisplayCard.tsx b/apps/ratewise/src/components/ui/RateDisplayCard.tsx new file mode 100644 index 00000000..745b35a6 --- /dev/null +++ b/apps/ratewise/src/components/ui/RateDisplayCard.tsx @@ -0,0 +1,356 @@ +/** + * RateDisplayCard - Exchange rate display card with mini trend chart + * + * A glassmorphism card component that displays exchange rate information + * with an integrated mini trend chart slot. + * + * Features: + * - Primary rate display (large, prominent) + * - Reverse rate display (secondary) + * - Change indicator (up/down arrow with percentage) + * - Rate type toggle (spot/cash) + * - Mini trend chart slot (80px height, constrained) + * + * Size constraints: + * - Card max-width: 480px (prevents overly wide cards on desktop) + * - Trend chart height: 80px (compact but readable) + * - Trend chart width: 100% (fills card width) + * + * @module components/ui/RateDisplayCard + */ + +import type { ReactNode, CSSProperties } from 'react'; +import { TrendingUp, TrendingDown, Minus, Banknote, CreditCard } from 'lucide-react'; +import { formatExchangeRate } from '../../utils/currencyFormatter'; + +type RateType = 'spot' | 'cash'; + +interface RateDisplayCardProps { + /** Source currency code */ + fromCurrency: string; + /** Target currency code */ + toCurrency: string; + /** Primary rate (1 fromCurrency = X toCurrency) */ + rate: number; + /** Reverse rate (1 toCurrency = X fromCurrency) */ + reverseRate: number; + /** Day-over-day change percentage */ + changePercent?: number; + /** Rate type (spot or cash) */ + rateType: RateType; + /** Callback when rate type changes */ + onRateTypeChange: (type: RateType) => void; + /** Mini trend chart slot */ + trendChart?: ReactNode; + /** Whether trend data is loading */ + isLoadingTrend?: boolean; + className?: string; + style?: CSSProperties; +} + +export function RateDisplayCard({ + fromCurrency, + toCurrency, + rate, + reverseRate, + changePercent, + rateType, + onRateTypeChange, + trendChart, + isLoadingTrend = false, + className = '', + style, +}: RateDisplayCardProps) { + const isPositive = changePercent !== undefined && changePercent > 0; + const isNegative = changePercent !== undefined && changePercent < 0; + const isNeutral = changePercent === undefined || changePercent === 0; + + return ( +
+ {/* Rate information section */} +
+ {/* Rate type toggle */} +
+
+ + +
+
+ + {/* Primary rate display */} +
+
+ + 1 {fromCurrency} = + + {formatExchangeRate(rate)} + + {toCurrency} + +
+ + {/* Reverse rate + change indicator */} +
+ + 1 {toCurrency} = {formatExchangeRate(reverseRate)} {fromCurrency} + + + {/* Change indicator */} + {changePercent !== undefined && ( + + {isPositive && } + {isNegative && } + {isNeutral && } + {isPositive && '+'} + {changePercent.toFixed(2)}% + + )} +
+
+
+ + {/* Trend chart section */} +
+ {isLoadingTrend ? ( +
+
+
+ ) : trendChart ? ( +
{trendChart}
+ ) : ( +
+ No trend data +
+ )} + + {/* Trend chart label */} +
+ 30-day trend +
+
+
+ ); +} + +/** + * CompactRateCard - Compact rate card for list views + * + * Dimensions: height 64px, mini chart 60x32px + */ +interface CompactRateCardProps { + currency: string; + currencyName: string; + flag: string; + rate: number; + changePercent?: number; + miniChart?: ReactNode; + onClick?: () => void; +} + +export function CompactRateCard({ + currency, + currencyName, + flag, + rate, + changePercent, + miniChart, + onClick, +}: CompactRateCardProps) { + const isPositive = changePercent !== undefined && changePercent > 0; + const isNegative = changePercent !== undefined && changePercent < 0; + + return ( + + ); +} diff --git a/apps/ratewise/src/components/ui/index.ts b/apps/ratewise/src/components/ui/index.ts new file mode 100644 index 00000000..61baa136 --- /dev/null +++ b/apps/ratewise/src/components/ui/index.ts @@ -0,0 +1,17 @@ +/** + * UI Components - Barrel export for all UI components + * + * This module provides a centralized export point for all reusable UI components. + * Components are organized by category for easier discovery and maintenance. + * + * @module components/ui + */ + +// Glass Effect Components +export { GlassCard, GlassCardHeader, GlassCardBody, GlassCardFooter } from './GlassCard'; + +// Rate Display Components +export { RateDisplayCard, CompactRateCard } from './RateDisplayCard'; + +// Input Components +export { CurrencyInput, QuickAmountButtons } from './CurrencyInput'; diff --git a/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx b/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx index 7b65d6cc..2a86a9be 100644 --- a/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx +++ b/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect, useRef, lazy, Suspense } from 'react'; +import { useState, useEffect, lazy, Suspense } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -import { RefreshCw, Calculator } from 'lucide-react'; +import { RefreshCw } from 'lucide-react'; import { CURRENCY_DEFINITIONS, CURRENCY_QUICK_AMOUNTS } from '../constants'; import type { CurrencyCode, RateType } from '../types'; // [fix:2025-12-24] Lazy load MiniTrendChart 減少初始 JS 載入量 @@ -15,7 +15,7 @@ import { fetchHistoricalRatesRange, fetchLatestRates, } from '../../../services/exchangeRateHistoryService'; -import { formatExchangeRate, formatAmountDisplay } from '../../../utils/currencyFormatter'; +import { formatAmountDisplay } from '../../../utils/currencyFormatter'; // [fix:2025-12-24] Lazy load CalculatorKeyboard - 只在用戶點擊計算機按鈕時載入 const CalculatorKeyboard = lazy(() => import('../../calculator/components/CalculatorKeyboard').then((m) => ({ @@ -25,6 +25,9 @@ const CalculatorKeyboard = lazy(() => import { logger } from '../../../utils/logger'; import { getExchangeRate } from '../../../utils/exchangeRateCalculation'; import { useCalculatorModal } from '../hooks/useCalculatorModal'; +// 新的 UI 元件 +import { CurrencyInput, QuickAmountButtons } from '../../../components/ui'; +import { RateDisplayCard } from '../../../components/ui'; const CURRENCY_CODES = Object.keys(CURRENCY_DEFINITIONS) as CurrencyCode[]; const MAX_TREND_DAYS = 30; @@ -47,6 +50,13 @@ interface SingleConverterProps { onRateTypeChange: (type: RateType) => void; } +// 轉換貨幣定義為 CurrencyInput 所需格式 +const currencyOptions = CURRENCY_CODES.map((code) => ({ + code, + flag: CURRENCY_DEFINITIONS[code].flag, + name: CURRENCY_DEFINITIONS[code].name, +})); + export const SingleConverter = ({ fromCurrency, toCurrency, @@ -65,16 +75,9 @@ export const SingleConverter = ({ onRateTypeChange, }: SingleConverterProps) => { const [trendData, setTrendData] = useState([]); - const [_loadingTrend, setLoadingTrend] = useState(false); + const [loadingTrend, setLoadingTrend] = useState(false); const [isSwapping, setIsSwapping] = useState(false); const [showTrend, setShowTrend] = useState(false); - const swapButtonRef = useRef(null); - - // 追蹤正在編輯的輸入框(使用未格式化的值) - const [editingField, setEditingField] = useState<'from' | 'to' | null>(null); - const [editingValue, setEditingValue] = useState(''); - const fromInputRef = useRef(null); - const toInputRef = useRef(null); // 計算機鍵盤狀態(使用統一的 Hook) const calculator = useCalculatorModal<'from' | 'to'>({ @@ -128,8 +131,9 @@ export const SingleConverter = ({ return () => clearTimeout(timer); }, [isSwapping]); - // 獲取當前目標貨幣的快速金額選項 - const quickAmounts = CURRENCY_QUICK_AMOUNTS[toCurrency] || CURRENCY_QUICK_AMOUNTS.TWD; + // 獲取當前貨幣的快速金額選項 + const fromQuickAmounts = CURRENCY_QUICK_AMOUNTS[fromCurrency] || CURRENCY_QUICK_AMOUNTS.TWD; + const toQuickAmounts = CURRENCY_QUICK_AMOUNTS[toCurrency] || CURRENCY_QUICK_AMOUNTS.TWD; // Load historical data for trend chart (並行獲取優化) useEffect(() => { @@ -153,9 +157,9 @@ export const SingleConverter = ({ const historyPoints: MiniTrendDataPoint[] = historicalData .map((item) => { - const fromRate = item.data.rates[fromCurrency] ?? 1; - const toRate = item.data.rates[toCurrency] ?? 1; - const rate = fromRate / toRate; + const fromRateVal = item.data.rates[fromCurrency] ?? 1; + const toRateVal = item.data.rates[toCurrency] ?? 1; + const rate = fromRateVal / toRateVal; return { date: item.date, // Keep full YYYY-MM-DD format for lightweight-charts @@ -234,200 +238,70 @@ export const SingleConverter = ({ }; }, [trendData]); + // 趨勢圖渲染 + const renderTrendChart = () => ( +
+ + 趨勢圖載入失敗 +
+ } + onError={(error) => { + logger.error('MiniTrendChart loading failed', error); + }} + > + {trendData.length === 0 ? ( + + ) : ( + }> + + + )} + +
+ ); + return ( <> + {/* 來源貨幣輸入 */}
- -
- - { - setEditingField('from'); - setEditingValue(fromAmount); - }} - onChange={(e) => { - const cleaned = e.target.value.replace(/[^\d.]/g, ''); - const parts = cleaned.split('.'); - const validValue = - parts.length > 2 ? parts[0] + '.' + parts.slice(1).join('') : cleaned; - setEditingValue(validValue); - onFromAmountChange(validValue); - }} - onBlur={() => { - onFromAmountChange(editingValue); - setEditingField(null); - setEditingValue(''); - }} - onKeyDown={(e) => { - const allowedKeys = [ - 'Backspace', - 'Delete', - 'ArrowLeft', - 'ArrowRight', - 'ArrowUp', - 'ArrowDown', - 'Home', - 'End', - 'Tab', - '.', - ]; - const isNumber = /^[0-9]$/.test(e.key); - const isModifierKey = e.ctrlKey || e.metaKey; - if (!isNumber && !allowedKeys.includes(e.key) && !isModifierKey) { - e.preventDefault(); - } - }} - className="w-full pl-32 pr-14 py-3 text-2xl font-bold border-2 border-neutral rounded-2xl focus:outline-none focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-300" - placeholder="0.00" - aria-label={`轉換金額 (${fromCurrency})`} - /> - {/* 計算機按鈕 */} - -
-
- {(CURRENCY_QUICK_AMOUNTS[fromCurrency] || CURRENCY_QUICK_AMOUNTS.TWD).map((amount) => ( - - ))} -
+ onFromCurrencyChange(code as CurrencyCode)} + currencies={currencyOptions} + displayValue={formatAmountDisplay(fromAmount, fromCurrency)} + onOpenCalculator={() => calculator.openCalculator('from')} + aria-label={`轉換金額 (${fromCurrency})`} + selectAriaLabel="選擇來源貨幣" + calculatorAriaLabel="開啟計算機 (轉換金額)" + data-testid="amount-input" + calculatorTestId="calculator-trigger-from" + /> +
+ {/* 匯率顯示卡片 + 交換按鈕 */}
- {/* 匯率卡片 - 懸停效果 - 移除 overflow-hidden 避免遮蔽 tooltip */} -
- {/* 光澤效果 */} -
- - {/* 匯率資訊區塊 - 包含切換按鈕和匯率顯示 */} -
- {/* 匯率類型切換按鈕 - 融合背景漸層的玻璃擬態設計 */} -
- - -
- - {/* 匯率顯示 */} -
-
- 1 {fromCurrency} = {formatExchangeRate(exchangeRate)} {toCurrency} -
-
- 1 {toCurrency} = {formatExchangeRate(reverseRate)} {fromCurrency} -
-
-
- - {/* 滿版趨勢圖 - 下半部 - 懸停放大 + 進場動畫 */} -
-
- - 趨勢圖載入失敗 -
- } - onError={(error) => { - logger.error('MiniTrendChart loading failed', error); - }} - > - {trendData.length === 0 ? ( - - ) : ( - }> - - - )} - -
- {/* 互動提示 */} -
- - 查看趨勢圖 - -
-
-
+ {/* 匯率卡片(含趨勢圖) */} + {/* 轉換按鈕 - 高級微互動 */}
@@ -440,7 +314,6 @@ export const SingleConverter = ({ {/* 按鈕本體 */}
+ {/* 目標貨幣輸入 */}
- -
- - { - setEditingField('to'); - setEditingValue(toAmount); - }} - onChange={(e) => { - const cleaned = e.target.value.replace(/[^\d.]/g, ''); - const parts = cleaned.split('.'); - const validValue = - parts.length > 2 ? parts[0] + '.' + parts.slice(1).join('') : cleaned; - setEditingValue(validValue); - onToAmountChange(validValue); - }} - onBlur={() => { - onToAmountChange(editingValue); - setEditingField(null); - setEditingValue(''); - }} - onKeyDown={(e) => { - const allowedKeys = [ - 'Backspace', - 'Delete', - 'ArrowLeft', - 'ArrowRight', - 'ArrowUp', - 'ArrowDown', - 'Home', - 'End', - 'Tab', - '.', - ]; - const isNumber = /^[0-9]$/.test(e.key); - const isModifierKey = e.ctrlKey || e.metaKey; - if (!isNumber && !allowedKeys.includes(e.key) && !isModifierKey) { - e.preventDefault(); - } - }} - className="w-full pl-32 pr-14 py-3 text-2xl font-bold border-2 border-primary-hover rounded-2xl bg-primary-bg focus:outline-none focus:border-primary focus:ring-4 focus:ring-primary/20 transition-all duration-300" - placeholder="0.00" - aria-label={`轉換結果 (${toCurrency})`} - /> - {/* 計算機按鈕 */} - -
-
- {quickAmounts.map((amount) => ( - - ))} -
+ onToCurrencyChange(code as CurrencyCode)} + currencies={currencyOptions} + displayValue={formatAmountDisplay(toAmount, toCurrency)} + variant="highlighted" + onOpenCalculator={() => calculator.openCalculator('to')} + aria-label={`轉換結果 (${toCurrency})`} + selectAriaLabel="選擇目標貨幣" + calculatorAriaLabel="開啟計算機 (轉換結果)" + calculatorTestId="calculator-trigger-to" + /> + { + // 直接設定目標貨幣金額,不需要轉換 + const decimals = CURRENCY_DEFINITIONS[toCurrency].decimals; + onToAmountChange(amount.toFixed(decimals)); + }} + variant="primary" + />
+ {/* 加入歷史記錄按鈕 */} diff --git a/apps/ratewise/src/features/ratewise/components/__tests__/SingleConverter.core.test.tsx b/apps/ratewise/src/features/ratewise/components/__tests__/SingleConverter.core.test.tsx index 28d59403..307989b5 100644 --- a/apps/ratewise/src/features/ratewise/components/__tests__/SingleConverter.core.test.tsx +++ b/apps/ratewise/src/features/ratewise/components/__tests__/SingleConverter.core.test.tsx @@ -238,12 +238,13 @@ describe('SingleConverter - 核心功能測試', () => { const { rerender } = render(); const spotButton = screen.getByLabelText('切換到即期匯率'); - expect(spotButton).toHaveClass('bg-brand-button-to'); + // 使用 aria-pressed 驗證選中狀態(更好的無障礙測試) + expect(spotButton).toHaveAttribute('aria-pressed', 'true'); rerender(); const cashButton = screen.getByLabelText('切換到現金匯率'); - expect(cashButton).toHaveClass('bg-brand-button-from'); + expect(cashButton).toHaveAttribute('aria-pressed', 'true'); }); }); @@ -392,19 +393,25 @@ describe('SingleConverter - 核心功能測試', () => { }); describe('趨勢圖進場動畫', () => { - it('should show trend chart after 300ms delay', () => { + it('should show trend chart after 300ms delay', async () => { render(); - // 初始狀態不應該顯示趨勢圖(opacity-0) - const trendContainer = document.querySelector('.opacity-0.translate-y-4'); + // Wait for async operations to complete + await act(async () => { + await Promise.resolve(); + }); + + // 初始狀態趨勢圖隱藏(opacity-0) + const trendContainer = document.querySelector('.opacity-0'); expect(trendContainer).toBeInTheDocument(); // 300ms 後應該顯示(opacity-100) - act(() => { + await act(async () => { vi.advanceTimersByTime(400); + await Promise.resolve(); }); - const visibleTrend = document.querySelector('.opacity-100.translate-y-0'); + const visibleTrend = document.querySelector('.opacity-100'); expect(visibleTrend).toBeInTheDocument(); }); }); diff --git a/apps/ratewise/src/features/ratewise/components/__tests__/SingleConverter.integration.test.tsx b/apps/ratewise/src/features/ratewise/components/__tests__/SingleConverter.integration.test.tsx index ec8eb583..c70f0dfe 100644 --- a/apps/ratewise/src/features/ratewise/components/__tests__/SingleConverter.integration.test.tsx +++ b/apps/ratewise/src/features/ratewise/components/__tests__/SingleConverter.integration.test.tsx @@ -200,12 +200,11 @@ describe('SingleConverter - 趨勢圖整合測試', () => { }); it('歷史資料為空時顯示骨架且不渲染迷你趨勢圖', async () => { + // 兩個數據源都返回空或失敗,確保 trendData 為空 vi.mocked(exchangeRateHistoryService.fetchHistoricalRatesRange).mockResolvedValue([]); - vi.mocked(exchangeRateHistoryService.fetchLatestRates).mockResolvedValue({ - updateTime: '2025/10/17 08:00:00', - source: 'Taiwan Bank', - rates: { ...mockExchangeRates }, - }); + vi.mocked(exchangeRateHistoryService.fetchLatestRates).mockRejectedValue( + new Error('Network error'), + ); render(); From cbfaeba239e012fafd79b1ecc9c0fab2802bbe7e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Jan 2026 19:19:36 +0000 Subject: [PATCH 7/7] refactor(ui): convert Chinese comments to English in SingleConverter - Add JSDoc module documentation header - Replace all Chinese inline comments with English equivalents - Replace temporary fix markers ([fix:YYYY-MM-DD]) with clean comments - Update hover tooltip text from Chinese to English - Maintain all functionality unchanged --- .../ratewise/components/SingleConverter.tsx | 86 +++++++++++-------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx b/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx index 2a86a9be..f38d7ce9 100644 --- a/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx +++ b/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx @@ -1,10 +1,24 @@ +/** + * SingleConverter - Primary currency conversion interface + * + * Features: + * - Real-time exchange rate display with trend visualization + * - Bidirectional currency input with calculator support + * - Quick amount selection buttons + * - Spot/Cash rate toggle + * - Swap currencies with haptic feedback + * + * @module features/ratewise/components/SingleConverter + */ + import { useState, useEffect, lazy, Suspense } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { RefreshCw } from 'lucide-react'; import { CURRENCY_DEFINITIONS, CURRENCY_QUICK_AMOUNTS } from '../constants'; import type { CurrencyCode, RateType } from '../types'; -// [fix:2025-12-24] Lazy load MiniTrendChart 減少初始 JS 載入量 -// lightweight-charts (144KB) 和 motion (40KB) 只在展開趨勢圖時載入 + +// Lazy load MiniTrendChart to reduce initial bundle size +// lightweight-charts (144KB) and motion (40KB) load on-demand const MiniTrendChart = lazy(() => import('./MiniTrendChart').then((m) => ({ default: m.MiniTrendChart })), ); @@ -16,7 +30,8 @@ import { fetchLatestRates, } from '../../../services/exchangeRateHistoryService'; import { formatAmountDisplay } from '../../../utils/currencyFormatter'; -// [fix:2025-12-24] Lazy load CalculatorKeyboard - 只在用戶點擊計算機按鈕時載入 + +// Lazy load CalculatorKeyboard - only loads when user opens calculator const CalculatorKeyboard = lazy(() => import('../../calculator/components/CalculatorKeyboard').then((m) => ({ default: m.CalculatorKeyboard, @@ -25,7 +40,6 @@ const CalculatorKeyboard = lazy(() => import { logger } from '../../../utils/logger'; import { getExchangeRate } from '../../../utils/exchangeRateCalculation'; import { useCalculatorModal } from '../hooks/useCalculatorModal'; -// 新的 UI 元件 import { CurrencyInput, QuickAmountButtons } from '../../../components/ui'; import { RateDisplayCard } from '../../../components/ui'; @@ -50,7 +64,7 @@ interface SingleConverterProps { onRateTypeChange: (type: RateType) => void; } -// 轉換貨幣定義為 CurrencyInput 所需格式 +// Transform currency definitions to CurrencyInput format const currencyOptions = CURRENCY_CODES.map((code) => ({ code, flag: CURRENCY_DEFINITIONS[code].flag, @@ -79,7 +93,7 @@ export const SingleConverter = ({ const [isSwapping, setIsSwapping] = useState(false); const [showTrend, setShowTrend] = useState(false); - // 計算機鍵盤狀態(使用統一的 Hook) + // Calculator keyboard state using unified hook const calculator = useCalculatorModal<'from' | 'to'>({ onConfirm: (field, result) => { if (field === 'from') { @@ -89,14 +103,14 @@ export const SingleConverter = ({ } }, getInitialValue: (field) => { - // 使用當前輸入框的實際值,如果為空或無效則使用 0 + // Use current input value, default to 0 if empty or invalid const value = field === 'from' ? fromAmount : toAmount; const parsed = parseFloat(value); return Number.isNaN(parsed) ? 0 : parsed; }, }); - // 獲取指定貨幣的匯率(優先使用 details + rateType,有 fallback 機制) + // Get exchange rate for currency (uses details + rateType with fallback) const getRate = (currency: CurrencyCode): number => { return getExchangeRate(currency, details, rateType, exchangeRates) ?? 1; }; @@ -106,24 +120,24 @@ export const SingleConverter = ({ const exchangeRate = fromRate / toRate; const reverseRate = toRate / fromRate; - // 趨勢圖進場動畫 + // Trend chart fade-in animation on mount useEffect(() => { const timer = setTimeout(() => setShowTrend(true), 300); return () => clearTimeout(timer); }, []); - // 處理交換按鈕點擊 + // Handle currency swap button click const handleSwap = () => { setIsSwapping(true); onSwapCurrencies(); - // 觸覺反饋(如果支援) + // Haptic feedback if supported if ('vibrate' in navigator) { navigator.vibrate(50); } }; - // 重置動畫狀態 + // Reset swap animation state useEffect(() => { if (!isSwapping) return; @@ -131,11 +145,11 @@ export const SingleConverter = ({ return () => clearTimeout(timer); }, [isSwapping]); - // 獲取當前貨幣的快速金額選項 + // Get quick amount presets for current currencies const fromQuickAmounts = CURRENCY_QUICK_AMOUNTS[fromCurrency] || CURRENCY_QUICK_AMOUNTS.TWD; const toQuickAmounts = CURRENCY_QUICK_AMOUNTS[toCurrency] || CURRENCY_QUICK_AMOUNTS.TWD; - // Load historical data for trend chart (並行獲取優化) + // Load historical data for trend chart (parallel fetch optimization) useEffect(() => { // Skip in test environment (avoid window is not defined error) if (typeof window === 'undefined') { @@ -169,7 +183,7 @@ export const SingleConverter = ({ .filter((item): item is MiniTrendDataPoint => item !== null) .reverse(); - // 整合即時匯率到歷史數據 + // Merge latest rates with historical data let mergedPoints = historyPoints; if (latestRates) { @@ -178,12 +192,12 @@ export const SingleConverter = ({ const latestRate = latestFromRate / latestToRate; if (Number.isFinite(latestRate) && latestRate > 0) { - // 提取日期並轉換為 YYYY-MM-DD 格式 + // Extract date and convert to YYYY-MM-DD format const latestDate = latestRates.updateTime?.split(/\s+/)[0]?.replace(/\//g, '-') ?? new Date().toISOString().slice(0, 10); - // 去重:過濾掉相同日期的歷史數據,添加最新數據點 + // Deduplicate: filter out historical data with same date, add latest point mergedPoints = [ ...historyPoints.filter((point) => point.date !== latestDate), { date: latestDate, rate: latestRate }, @@ -191,7 +205,7 @@ export const SingleConverter = ({ } } - // 按日期排序並限制最多30天 + // Sort by date and limit to 30 days const sortedPoints = mergedPoints.sort((a, b) => a.date.localeCompare(b.date)); setTrendData(sortedPoints.slice(-MAX_TREND_DAYS)); } catch { @@ -211,7 +225,7 @@ export const SingleConverter = ({ }; }, [fromCurrency, toCurrency]); - // 開發工具:強制觸發骨架屏效果(僅開發模式) + // Dev tools: Force trigger skeleton screen effect (dev mode only) /* v8 ignore next 22 */ useEffect(() => { if (!import.meta.env.DEV || typeof window === 'undefined') return; @@ -238,7 +252,7 @@ export const SingleConverter = ({ }; }, [trendData]); - // 趨勢圖渲染 + // Render trend chart with loading states const renderTrendChart = () => (
- {/* 來源貨幣輸入 */} + {/* Source currency input */}
- {/* 匯率顯示卡片 + 交換按鈕 */} + {/* Rate display card with swap button */}
- {/* 匯率卡片(含趨勢圖) */} + {/* Rate card with integrated trend chart */} - {/* 轉換按鈕 - 高級微互動 */} + {/* Swap button with advanced micro-interactions */}
- {/* 外圍光環 */} + {/* Outer glow ring */}
- {/* 按鈕本體 */} + {/* Button element */} - {/* 懸停提示 */} + {/* Hover tooltip */}
- 點擊交換 + Click to swap
- {/* 目標貨幣輸入 */} + {/* Target currency input */}
{ - // 直接設定目標貨幣金額,不需要轉換 + // Set target amount directly without conversion const decimals = CURRENCY_DEFINITIONS[toCurrency].decimals; onToAmountChange(amount.toFixed(decimals)); }} @@ -385,7 +399,7 @@ export const SingleConverter = ({ />
- {/* 加入歷史記錄按鈕 */} + {/* Add to history button */}