Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions apps/ratewise/src/components/chart/ChartContainer.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={`chart-container ${className}`}>
{/* Header */}
{(title !== undefined || subtitle !== undefined) && (
<div className="mb-2">
{title && (
<h3
style={{
fontSize: 'var(--font-size-sm, 14px)',
fontWeight: 600,
color: 'var(--color-text-primary)',
}}
>
{title}
</h3>
)}
{subtitle && (
<p
style={{
fontSize: 'var(--font-size-xs, 14px)',
color: 'var(--color-text-tertiary)',
}}
>
{subtitle}
</p>
)}
</div>
)}

{/* Chart Area */}
<div
className="relative w-full"
style={{
maxWidth: styles.maxWidth,
maxHeight: styles.maxHeight,
minHeight: styles.minHeight,
aspectRatio: styles.aspectRatio,
}}
>
{children}
</div>
</div>
);
}

/**
* MiniChart - 迷你圖表容器(用於列表項目)
*
* @description 極簡圖表,高度固定 40px
*/
interface MiniChartProps {
children: ReactNode;
className?: string;
}

export function MiniChart({ children, className = '' }: MiniChartProps) {
return (
<div
className={`mini-chart ${className}`}
style={{
width: '80px',
height: '40px',
flexShrink: 0,
}}
>
{children}
</div>
);
}

/**
* 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 (
<span
className="inline-flex items-center gap-0.5"
style={{
fontSize,
fontWeight: 500,
color: isNeutral
? 'var(--color-text-tertiary)'
: isPositive
? 'var(--color-status-success)'
: 'var(--color-status-error)',
}}
>
{!isNeutral && (
<svg
width={iconSize}
height={iconSize}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
style={{
transform: isPositive ? 'rotate(0deg)' : 'rotate(180deg)',
}}
>
<path d="M12 19V5M5 12l7-7 7 7" />
</svg>
)}
{displayValue}
</span>
);
}
8 changes: 8 additions & 0 deletions apps/ratewise/src/components/chart/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Chart Components - Export Index
*
* @version 1.0.0
* @created 2025-01-18
*/

export { ChartContainer, MiniChart, TrendIndicator } from './ChartContainer';
168 changes: 168 additions & 0 deletions apps/ratewise/src/components/layout/ResponsiveLayout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen" style={{ background: 'var(--color-bg-primary)' }}>
{/* Navigation Components - Rendered based on screen size via CSS */}
{showNavigation && (
<>
<BottomTabBar />
<NavRail />
<Sidebar />
</>
)}

{/* Main Content Area */}
<main
className="
min-h-screen
pb-[calc(56px+env(safe-area-inset-bottom))]
md:pb-0
md:pl-[80px]
lg:pl-[256px]
transition-[padding] duration-300
"
>
{/* Content Container with max-width */}
<div
className="mx-auto px-4 py-6 md:px-6 lg:px-8"
style={{
maxWidth: maxWidthMap[maxWidth],
}}
>
{children}
</div>
</main>
</div>
);
}

/**
* 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 (
<section
className={`rounded-xl ${className}`}
style={{
padding: paddingMap[padding],
...(glass
? {
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)',
}
: {}),
}}
>
{children}
</section>
);
}

/**
* PageHeader - 頁面標題區
*
* @description 統一的頁面標題樣式
*/
interface PageHeaderProps {
title: string;
subtitle?: string;
actions?: ReactNode;
}

export function PageHeader({ title, subtitle, actions }: PageHeaderProps) {
return (
<header className="flex items-start justify-between mb-6">
<div>
<h1
style={{
fontSize: 'var(--font-size-2xl, 24px)',
fontWeight: 700,
color: 'var(--color-text-primary)',
lineHeight: 'var(--line-height-tight)',
}}
>
{title}
</h1>
{subtitle && (
<p
className="mt-1"
style={{
fontSize: 'var(--font-size-sm, 14px)',
color: 'var(--color-text-secondary)',
}}
>
{subtitle}
</p>
)}
</div>
{actions && <div className="flex items-center gap-2">{actions}</div>}
</header>
);
}
8 changes: 8 additions & 0 deletions apps/ratewise/src/components/layout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Layout Components - Export Index
*
* @version 1.0.0
* @created 2025-01-18
*/

export { ResponsiveLayout, ContentSection, PageHeader } from './ResponsiveLayout';
Loading
Loading