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 (
+
+ );
+}
+
+/**
+ * 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';
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/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/config/design-tokens.ts b/apps/ratewise/src/config/design-tokens.ts
index 5754ae91..ecd7795a 100644
--- a/apps/ratewise/src/config/design-tokens.ts
+++ b/apps/ratewise/src/config/design-tokens.ts
@@ -352,6 +352,180 @@ 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;
+
+/**
+ * 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 配置
*
@@ -365,7 +539,13 @@ export const darkTheme = {
* ```
*/
export function getDesignTokens() {
- return { colors: semanticColors };
+ return {
+ colors: semanticColors,
+ spacing: spacingTokens,
+ borderRadius: borderRadiusTokens,
+ boxShadow: boxShadowTokens,
+ fontSize: fontSizeTokens,
+ };
}
/**
@@ -394,6 +574,22 @@ export function generateTailwindThemeExtension(): Config['theme'] {
return {
extend: {
colors: semanticColors,
+ spacing: spacingTokens,
+ borderRadius: borderRadiusTokens,
+ boxShadow: boxShadowTokens,
+ fontSize: fontSizeTokens,
+ transitionDuration: transitionDurationTokens,
+ 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/features/ratewise/components/SingleConverter.tsx b/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx
index 7b65d6cc..f38d7ce9 100644
--- a/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx
+++ b/apps/ratewise/src/features/ratewise/components/SingleConverter.tsx
@@ -1,10 +1,24 @@
-import { useState, useEffect, useRef, lazy, Suspense } from 'react';
+/**
+ * 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, 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 載入量
-// 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 })),
);
@@ -15,8 +29,9 @@ import {
fetchHistoricalRatesRange,
fetchLatestRates,
} from '../../../services/exchangeRateHistoryService';
-import { formatExchangeRate, formatAmountDisplay } from '../../../utils/currencyFormatter';
-// [fix:2025-12-24] Lazy load CalculatorKeyboard - 只在用戶點擊計算機按鈕時載入
+import { formatAmountDisplay } from '../../../utils/currencyFormatter';
+
+// Lazy load CalculatorKeyboard - only loads when user opens calculator
const CalculatorKeyboard = lazy(() =>
import('../../calculator/components/CalculatorKeyboard').then((m) => ({
default: m.CalculatorKeyboard,
@@ -25,6 +40,8 @@ const CalculatorKeyboard = lazy(() =>
import { logger } from '../../../utils/logger';
import { getExchangeRate } from '../../../utils/exchangeRateCalculation';
import { useCalculatorModal } from '../hooks/useCalculatorModal';
+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 +64,13 @@ interface SingleConverterProps {
onRateTypeChange: (type: RateType) => void;
}
+// Transform currency definitions to CurrencyInput format
+const currencyOptions = CURRENCY_CODES.map((code) => ({
+ code,
+ flag: CURRENCY_DEFINITIONS[code].flag,
+ name: CURRENCY_DEFINITIONS[code].name,
+}));
+
export const SingleConverter = ({
fromCurrency,
toCurrency,
@@ -65,18 +89,11 @@ 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)
+ // Calculator keyboard state using unified hook
const calculator = useCalculatorModal<'from' | 'to'>({
onConfirm: (field, result) => {
if (field === 'from') {
@@ -86,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;
};
@@ -103,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;
@@ -128,10 +145,11 @@ export const SingleConverter = ({
return () => clearTimeout(timer);
}, [isSwapping]);
- // 獲取當前目標貨幣的快速金額選項
- const quickAmounts = CURRENCY_QUICK_AMOUNTS[toCurrency] || CURRENCY_QUICK_AMOUNTS.TWD;
+ // 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') {
@@ -153,9 +171,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
@@ -165,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) {
@@ -174,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 },
@@ -187,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 {
@@ -207,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;
@@ -234,213 +252,82 @@ export const SingleConverter = ({
};
}, [trendData]);
+ // Render trend chart with loading states
+ const renderTrendChart = () => (
+
+
+ 趨勢圖載入失敗
+
+ }
+ onError={(error) => {
+ logger.error('MiniTrendChart loading failed', error);
+ }}
+ >
+ {trendData.length === 0 ? (
+
+ ) : (
+ }>
+
+
+ )}
+
+
+ );
+
return (
<>
+ {/* Source currency input */}
-
-
-
- {
- 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"
+ />
+
+ {/* Rate display card with swap button */}
- {/* 匯率卡片 - 懸停效果 - 移除 overflow-hidden 避免遮蔽 tooltip */}
-
- {/* 光澤效果 */}
-
-
- {/* 匯率資訊區塊 - 包含切換按鈕和匯率顯示 */}
-
- {/* 匯率類型切換按鈕 - 融合背景漸層的玻璃擬態設計 */}
-
-
-
-
-
- {/* 匯率顯示 */}
-
-
- 1 {fromCurrency} = {formatExchangeRate(exchangeRate)} {toCurrency}
-
-
- 1 {toCurrency} = {formatExchangeRate(reverseRate)} {fromCurrency}
-
-
-
-
- {/* 滿版趨勢圖 - 下半部 - 懸停放大 + 進場動畫 */}
-
-
-
- 趨勢圖載入失敗
-
- }
- onError={(error) => {
- logger.error('MiniTrendChart loading failed', error);
- }}
- >
- {trendData.length === 0 ? (
-
- ) : (
-
}>
-
-
- )}
-
-
- {/* 互動提示 */}
-
-
- 查看趨勢圖
-
-
-
-
+ {/* 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 */}
-
-
-
- {
- 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"
+ />
+ {
+ // Set target amount directly without conversion
+ const decimals = CURRENCY_DEFINITIONS[toCurrency].decimals;
+ onToAmountChange(amount.toFixed(decimals));
+ }}
+ variant="primary"
+ />
+ {/* Add to history button */}
- {/* 計算機鍵盤 Bottom Sheet */}
- {/* [fix:2025-12-25] 始終渲染 CalculatorKeyboard,讓彩蛋在計算機關閉後仍可顯示 */}
+ {/* Calculator keyboard bottom sheet */}
+ {/* Always render CalculatorKeyboard to allow easter eggs after close */}
{
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();
diff --git a/apps/ratewise/src/styles/tokens.css b/apps/ratewise/src/styles/tokens.css
new file mode 100644
index 00000000..1cbe0991
--- /dev/null
+++ b/apps/ratewise/src/styles/tokens.css
@@ -0,0 +1,744 @@
+/**
+ * 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 (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;
+ --font-size-xl: 20px;
+ --font-size-2xl: 24px;
+ --font-size-3xl: 30px;
+ --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;
+ --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;
+
+ /* --------------------------------------------------
+ 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);
+}
+
+/* ============================================================
+ 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 (WCAG 2.2 + Apple HIG 44pt minimum)
+ -------------------------------------------------- */
+ --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 (WCAG 2.2 + Apple HIG 44pt minimum)
+ -------------------------------------------------- */
+ --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 */
+}
+
+/* ============================================================
+ 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);
+}
+
+/* ============================================================
+ 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
new file mode 100644
index 00000000..71b3ed99
--- /dev/null
+++ b/docs/dev/006_design_modernization_spec.md
@@ -0,0 +1,1291 @@
+# Ratewise Design Modernization Specification
+
+> **版本**: 3.0.0
+> **建立日期**: 2025-01-17
+> **最後更新**: 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-*`)
+
+---
+
+## 目錄
+
+1. [設計標準引用](#1-設計標準引用)
+2. [Token 架構總覽](#2-token-架構總覽)
+3. [色彩系統](#3-色彩系統)
+4. [液態玻璃規格](#4-液態玻璃規格)
+5. [排版系統](#5-排版系統)
+6. [間距與尺寸](#6-間距與尺寸)
+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-實作檢查清單)
+
+---
+
+## 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. Grid 系統與 Layout
+
+> 參考來源: [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 響應式斷點
+
+| 斷點 | 寬度 | 裝置 | 導覽模式 |
+| ---- | ----------- | -------- | ---------- |
+| `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.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
+```
+
+---
+
+## 16. 無障礙設計
+
+### 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 | ⚠️ 需要測試不同背景 |
+
+### 16.2 觸控目標
+
+| 規範 | 最小尺寸 |
+| ---------- | ------------------------------------ |
+| WCAG 2.2 | 24×24px |
+| Apple HIG | 44×44px |
+| 本系統採用 | 44×44px (min), 48×48px (comfortable) |
+
+### 16.3 降級支援
+
+```css
+/* 偏好降低透明度 */
+@media (prefers-reduced-transparency: reduce) { ... }
+
+/* 偏好減少動態 */
+@media (prefers-reduced-motion: reduce) { ... }
+
+/* 高對比模式 */
+[data-contrast="high"] { ... }
+```
+
+---
+
+## 17. 實作檢查清單
+
+### 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