"添加前端模板和运行代码模块"

This commit is contained in:
Codex
2026-06-01 13:43:09 +08:00
parent 24045274ad
commit 8092c4b1f8
587 changed files with 99761 additions and 0 deletions

View File

@@ -0,0 +1,484 @@
/**
* @fileoverview 颜色面板组件
* @description 展示和管理设计系统中的颜色令牌
*/
import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Search, Filter, Grid, List, Plus, MoreVertical } from 'lucide-react';
import { Card, CardBody, CardHeader } from '../ui/Card';
import { Button, IconButton } from '../ui/Button';
import { ColorToken } from '../../types/design-system';
import { useColors, useDesignSystemStore } from '../../store/design-system-store';
import { clsx } from 'clsx';
// 颜色面板属性接口
interface ColorPaletteProps {
title?: string;
variant?: 'grid' | 'list' | 'swatch';
showControls?: boolean;
interactive?: boolean;
className?: string;
}
// 颜色色板属性接口
interface ColorSwatchProps {
token: ColorToken;
variant?: 'default' | 'compact' | 'large';
showLabel?: boolean;
showValue?: boolean;
interactive?: boolean;
onClick?: () => void;
className?: string;
}
/**
* 颜色色板组件
*/
const ColorSwatch: React.FC<ColorSwatchProps> = ({
token,
variant = 'default',
showLabel = true,
showValue = true,
interactive = true,
onClick,
className
}) => {
const [copied, setCopied] = useState(false);
const { setSelectedToken } = useDesignSystemStore();
// 复制颜色值
const copyColor = async (e: React.MouseEvent) => {
e.stopPropagation();
try {
await navigator.clipboard.writeText(token.value);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error('Failed to copy color:', error);
}
};
// 处理点击
const handleClick = () => {
if (interactive) {
setSelectedToken(token);
onClick?.();
}
};
// 尺寸配置
const sizeConfig = {
default: { width: 'w-full', height: 'h-20', padding: 'p-4' },
compact: { width: 'w-full', height: 'h-12', padding: 'p-2' },
large: { width: 'w-full', height: 'h-32', padding: 'p-6' },
};
const config = sizeConfig[variant];
// 显示文本颜色(基于背景亮度)
const getContrastColor = (hexColor: string) => {
// 简单的亮度计算
const r = parseInt(hexColor.slice(1, 3), 16);
const g = parseInt(hexColor.slice(3, 5), 16);
const b = parseInt(hexColor.slice(5, 7), 16);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 128 ? '#000000' : '#ffffff';
};
return (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
whileHover={interactive ? { scale: 1.02 } : {}}
whileTap={interactive ? { scale: 0.98 } : {}}
className={clsx(
'color-swatch cursor-pointer group relative overflow-hidden rounded-lg border border-neutral-200 dark:border-neutral-700 transition-all duration-200',
config.width,
config.height,
{
'hover:shadow-lg': interactive,
'hover:border-primary-300': interactive,
},
className
)}
data-component="ColorSwatch"
data-element="swatch"
data-token-name={token.name}
data-palette={token.palette}
data-scale={token.scale}
style={{ backgroundColor: token.value }}
onClick={handleClick}
>
{/* 颜色背景 */}
<div
className="absolute inset-0"
style={{ backgroundColor: token.value }}
data-element="background"
/>
{/* 覆盖层 */}
<div
className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-10 transition-all duration-200"
data-element="overlay"
/>
{/* 色板内容 */}
<div
className={clsx(
'relative z-10 flex flex-col justify-between h-full',
config.padding
)}
data-element="content"
>
{/* 顶部标签 */}
{showLabel && (
<div className="flex items-center justify-between" data-element="header">
<div className="flex items-center space-x-2" data-element="labels">
<span
className="px-2 py-1 text-xs font-semibold rounded backdrop-blur-sm"
style={{
backgroundColor: 'rgba(0, 0, 0, 0.2)',
color: getContrastColor(token.value)
}}
data-element="name"
>
{token.name}
</span>
{token.deprecated && (
<span className="px-1.5 py-0.5 text-xs bg-warning-500 text-white rounded backdrop-blur-sm">
Deprecated
</span>
)}
</div>
{interactive && (
<IconButton
size="sm"
variant="ghost"
onClick={copyColor}
className="opacity-0 group-hover:opacity-100 transition-opacity"
data-component="IconButton"
data-element="copy-button"
data-action="copy-color"
>
{copied ? (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="text-success-400"
>
</motion.div>
) : (
<span
className="text-sm font-mono"
style={{ color: getContrastColor(token.value) }}
>
#
</span>
)}
</IconButton>
)}
</div>
)}
{/* 底部信息 */}
{showValue && (
<div className="space-y-1" data-element="footer">
<div
className="text-xs font-mono backdrop-blur-sm px-2 py-1 rounded"
style={{
backgroundColor: 'rgba(0, 0, 0, 0.2)',
color: getContrastColor(token.value)
}}
data-element="value"
>
{token.value}
</div>
{token.scale && token.palette && (
<div
className="text-xs backdrop-blur-sm px-2 py-1 rounded"
style={{
backgroundColor: 'rgba(0, 0, 0, 0.1)',
color: getContrastColor(token.value)
}}
data-element="metadata"
>
{token.palette} {token.scale}
</div>
)}
</div>
)}
</div>
{/* 选择指示器 */}
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute top-2 right-2 w-3 h-3 bg-primary-500 rounded-full border-2 border-white shadow-sm"
data-element="selected-indicator"
/>
</AnimatePresence>
</motion.div>
);
};
/**
* 颜色面板主组件
*/
const ColorPalette: React.FC<ColorPaletteProps> = ({
title = 'Color Palette',
variant = 'grid',
showControls = true,
interactive = true,
className
}) => {
const [searchQuery, setSearchQuery] = useState('');
const [selectedPalette, setSelectedPalette] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [showFilters, setShowFilters] = useState(false);
const colors = useColors();
const { setSelectedToken, selectedToken } = useDesignSystemStore();
// 过滤和搜索颜色
const filteredColors = useMemo(() => {
return colors.filter(color => {
const matchesSearch = searchQuery === '' ||
color.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
color.value.toLowerCase().includes(searchQuery.toLowerCase()) ||
color.description?.toLowerCase().includes(searchQuery.toLowerCase());
const matchesPalette = selectedPalette === null ||
color.palette === selectedPalette;
return matchesSearch && matchesPalette;
});
}, [colors, searchQuery, selectedPalette]);
// 按调色板分组
const colorsByPalette = useMemo(() => {
const grouped: Record<string, ColorToken[]> = {};
filteredColors.forEach(color => {
const palette = color.palette || 'other';
if (!grouped[palette]) {
grouped[palette] = [];
}
grouped[palette].push(color);
});
return grouped;
}, [filteredColors]);
// 获取所有调色板
const palettes = useMemo(() => {
const paletteSet = new Set(colors.map(color => color.palette || 'other'));
return Array.from(paletteSet);
}, [colors]);
return (
<div
className={clsx('color-palette', className)}
data-component="ColorPalette"
data-element="palette"
data-variant={variant}
data-view-mode={viewMode}
>
{/* 控制面板 */}
{showControls && (
<Card className="mb-6">
<CardBody className="p-4">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
{/* 标题 */}
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{title}
</h3>
{/* 控制按钮 */}
<div className="flex items-center space-x-3">
{/* 搜索框 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-neutral-400 w-4 h-4" />
<input
type="text"
placeholder="Search colors..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 pr-4 py-2 border border-neutral-300 dark:border-neutral-600 rounded-md bg-white dark:bg-neutral-800 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
data-component="Input"
data-element="search-input"
data-placeholder="search-colors"
/>
</div>
{/* 过滤器按钮 */}
<IconButton
variant={showFilters ? 'primary' : 'ghost'}
onClick={() => setShowFilters(!showFilters)}
data-component="IconButton"
data-element="filter-button"
data-action="toggle-filters"
>
<Filter className="w-4 h-4" />
</IconButton>
{/* 视图模式切换 */}
<div className="flex border border-neutral-300 dark:border-neutral-600 rounded-md overflow-hidden">
<button
onClick={() => setViewMode('grid')}
className={clsx(
'px-3 py-2 text-sm transition-colors',
viewMode === 'grid'
? 'bg-primary-500 text-white'
: 'bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-700'
)}
data-component="ToggleButton"
data-element="grid-view-button"
data-action="set-view-grid"
>
<Grid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={clsx(
'px-3 py-2 text-sm transition-colors',
viewMode === 'list'
? 'bg-primary-500 text-white'
: 'bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-700'
)}
data-component="ToggleButton"
data-element="list-view-button"
data-action="set-view-list"
>
<List className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* 过滤器面板 */}
<AnimatePresence>
{showFilters && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-800"
data-element="filters"
>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedPalette(null)}
className={clsx(
'px-3 py-1.5 text-sm rounded-full border transition-colors',
selectedPalette === null
? 'bg-primary-500 text-white border-primary-500'
: 'bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 border-neutral-300 dark:border-neutral-600 hover:bg-neutral-50 dark:hover:bg-neutral-700'
)}
data-component="FilterChip"
data-element="all-palettes-chip"
data-action="select-all-palettes"
>
All Palettes
</button>
{palettes.map(palette => (
<button
key={palette}
onClick={() => setSelectedPalette(palette)}
className={clsx(
'px-3 py-1.5 text-sm rounded-full border transition-colors capitalize',
selectedPalette === palette
? 'bg-primary-500 text-white border-primary-500'
: 'bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 border-neutral-300 dark:border-neutral-600 hover:bg-neutral-50 dark:hover:bg-neutral-700'
)}
data-component="FilterChip"
data-element={`${palette}-palette-chip`}
data-action={`select-${palette}-palette`}
>
{palette}
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</CardBody>
</Card>
)}
{/* 颜色网格/列表 */}
<div className="space-y-8">
{Object.entries(colorsByPalette).map(([palette, paletteColors]) => (
<div key={palette} className="palette-group" data-element="palette-group" data-palette={palette}>
<h4 className="text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-4 capitalize">
{palette} Palette ({paletteColors.length})
</h4>
<div className={clsx(
'grid gap-4',
{
'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8': viewMode === 'grid',
'space-y-2': viewMode === 'list',
}
)}>
<AnimatePresence>
{paletteColors.map((color, index) => (
<motion.div
key={color.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<ColorSwatch
token={color}
variant={viewMode === 'list' ? 'compact' : 'default'}
showLabel={viewMode === 'grid'}
showValue={viewMode === 'grid'}
interactive={interactive}
/>
</motion.div>
))}
</AnimatePresence>
</div>
</div>
))}
</div>
{/* 空状态 */}
{filteredColors.length === 0 && (
<div className="text-center py-12" data-element="empty-state">
<div className="w-12 h-12 bg-neutral-100 dark:bg-neutral-800 rounded-full flex items-center justify-center mx-auto mb-4">
<Search className="w-6 h-6 text-neutral-400" />
</div>
<h3 className="text-lg font-medium text-neutral-900 dark:text-neutral-100 mb-2">
No colors found
</h3>
<p className="text-neutral-600 dark:text-neutral-400 mb-4">
Try adjusting your search or filter criteria.
</p>
<Button
variant="outline"
onClick={() => {
setSearchQuery('');
setSelectedPalette(null);
}}
data-component="Button"
data-element="clear-filters-button"
data-action="clear-all-filters"
>
Clear Filters
</Button>
</div>
)}
</div>
);
};
export { ColorPalette, ColorSwatch };
export type { ColorPaletteProps, ColorSwatchProps };
export default ColorPalette;

View File

@@ -0,0 +1,395 @@
/**
* @fileoverview 令牌卡片组件
* @description 展示设计系统令牌的可视化组件
*/
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { Copy, Check, Edit3, Trash2, Tag } from 'lucide-react';
import { Card, CardBody, CardHeader } from '../ui/Card';
import { Button, IconButton } from '../ui/Button';
import { DesignToken, ColorToken, TypographyToken, SpacingToken, ShadowToken, BorderRadiusToken, AnimationToken } from '../../types/design-system';
import { useDesignSystemStore } from '../../store/design-system-store';
import { clsx } from 'clsx';
// 令牌卡片属性接口
interface TokenCardProps {
token: DesignToken;
variant?: 'default' | 'compact' | 'detailed';
interactive?: boolean;
showActions?: boolean;
className?: string;
}
/**
* 令牌卡片主组件
*/
const TokenCard: React.FC<TokenCardProps> = ({
token,
variant = 'default',
interactive = true,
showActions = true,
className
}) => {
const [copied, setCopied] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const { setSelectedToken } = useDesignSystemStore();
// 复制令牌值
const copyToken = async () => {
try {
await navigator.clipboard.writeText(String(token.value));
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error('Failed to copy token:', error);
}
};
// 选择令牌
const handleSelect = () => {
if (interactive) {
setSelectedToken(token);
}
};
// 渲染令牌预览
const renderTokenPreview = () => {
switch (token.category) {
case 'color':
return <ColorTokenPreview token={token as ColorToken} />;
case 'typography':
return <TypographyTokenPreview token={token as TypographyToken} />;
case 'spacing':
return <SpacingTokenPreview token={token as SpacingToken} />;
case 'shadow':
return <ShadowTokenPreview token={token as ShadowToken} />;
case 'border-radius':
return <BorderRadiusTokenPreview token={token as BorderRadiusToken} />;
case 'animation':
return <AnimationTokenPreview token={token as AnimationToken} />;
default:
return <DefaultTokenPreview token={token} />;
}
};
// 渲染令牌信息
const renderTokenInfo = () => {
const categoryIcons = {
color: '🎨',
typography: '📝',
spacing: '📏',
shadow: '🌑',
'border-radius': '🔘',
'z-index': '📊',
animation: '⚡',
};
return (
<div className="token-info" data-element="info">
<div className="flex items-center space-x-2 mb-2" data-element="header">
<span className="text-lg" data-element="icon">
{categoryIcons[token.category]}
</span>
<h4 className="font-semibold text-sm text-neutral-900 dark:text-neutral-100" data-element="name">
{token.name}
</h4>
{token.deprecated && (
<span className="px-2 py-0.5 text-xs bg-warning-100 text-warning-800 dark:bg-warning-900/40 dark:text-warning-200 rounded-full">
Deprecated
</span>
)}
</div>
{token.description && (
<p className="text-xs text-neutral-600 dark:text-neutral-400 mb-3" data-element="description">
{token.description}
</p>
)}
{token.tags && token.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mb-3" data-element="tags">
{token.tags.slice(0, 3).map((tag, index) => (
<span
key={index}
className="inline-flex items-center px-2 py-0.5 text-xs bg-neutral-100 dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 rounded"
>
<Tag className="w-3 h-3 mr-1" />
{tag}
</span>
))}
{token.tags.length > 3 && (
<span className="text-xs text-neutral-500">
+{token.tags.length - 3} more
</span>
)}
</div>
)}
</div>
);
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
whileHover={interactive ? { y: -2 } : {}}
className={clsx(
'token-card',
{
'cursor-pointer': interactive,
'opacity-75': token.deprecated,
},
className
)}
data-component="TokenCard"
data-element="card"
data-category={token.category}
data-name={token.name}
onClick={handleSelect}
>
<Card
variant="default"
size="sm"
hover={interactive}
interactive={interactive}
className="h-full"
>
<CardBody className="p-4">
{/* 令牌预览 */}
<div className="token-preview mb-4" data-element="preview">
{renderTokenPreview()}
</div>
{/* 令牌信息 */}
{renderTokenInfo()}
{/* 令牌值和操作 */}
<div className="token-actions flex items-center justify-between mt-4 pt-3 border-t border-neutral-200 dark:border-neutral-800" data-element="actions">
<div className="token-value flex items-center space-x-2" data-element="value">
<code className="text-xs bg-neutral-100 dark:bg-neutral-800 px-2 py-1 rounded font-mono">
{typeof token.value === 'object' ? JSON.stringify(token.value) : token.value}
</code>
</div>
{showActions && (
<div className="token-buttons flex items-center space-x-1" data-element="buttons">
<IconButton
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
copyToken();
}}
aria-label="Copy token value"
data-component="IconButton"
data-element="copy-button"
data-action="copy-value"
>
{copied ? (
<Check className="w-3 h-3 text-success-600" />
) : (
<Copy className="w-3 h-3" />
)}
</IconButton>
{interactive && (
<IconButton
size="sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
setIsEditing(!isEditing);
}}
aria-label="Edit token"
data-component="IconButton"
data-element="edit-button"
data-action="edit-token"
>
<Edit3 className="w-3 h-3" />
</IconButton>
)}
</div>
)}
</div>
{/* 编辑模式 */}
{isEditing && interactive && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="token-editor mt-3 pt-3 border-t border-neutral-200 dark:border-neutral-800"
data-element="editor"
>
<div className="space-y-2">
<label className="block text-xs font-medium text-neutral-700 dark:text-neutral-300">
Token Value
</label>
<textarea
className="w-full px-2 py-1 text-xs border border-neutral-300 dark:border-neutral-600 rounded bg-white dark:bg-neutral-800"
rows={2}
value={typeof token.value === 'object' ? JSON.stringify(token.value, null, 2) : String(token.value)}
readOnly
/>
<div className="flex justify-end space-x-2">
<Button
size="sm"
variant="ghost"
onClick={() => setIsEditing(false)}
>
Cancel
</Button>
<Button
size="sm"
variant="primary"
>
Save
</Button>
</div>
</div>
</motion.div>
)}
</CardBody>
</Card>
</motion.div>
);
};
// 颜色令牌预览组件
const ColorTokenPreview: React.FC<{ token: ColorToken }> = ({ token }) => {
return (
<div className="color-token-preview" data-element="color-preview">
<div
className="color-swatch w-full h-16 rounded-lg border border-neutral-200 dark:border-neutral-700"
style={{ backgroundColor: token.value }}
data-element="swatch"
/>
<div className="color-info mt-2 text-center">
<div className="text-xs font-mono" data-element="hex-value">
{token.value}
</div>
{token.scale && (
<div className="text-xs text-neutral-500" data-element="scale">
{token.palette} {token.scale}
</div>
)}
</div>
</div>
);
};
// 字体令牌预览组件
const TypographyTokenPreview: React.FC<{ token: TypographyToken }> = ({ token }) => {
const { fontSize, fontWeight, lineHeight } = token.value;
return (
<div className="typography-token-preview p-3 bg-neutral-50 dark:bg-neutral-800 rounded" data-element="typography-preview">
<div
style={{
fontSize: fontSize || '1rem',
fontWeight: fontWeight || 400,
lineHeight: lineHeight || '1.5',
}}
data-element="text-sample"
>
The quick brown fox
</div>
<div className="mt-2 text-xs text-neutral-600 dark:text-neutral-400 space-y-1" data-element="typography-info">
{fontSize && <div>Size: {fontSize}</div>}
{fontWeight && <div>Weight: {fontWeight}</div>}
{lineHeight && <div>Line Height: {lineHeight}</div>}
</div>
</div>
);
};
// 间距令牌预览组件
const SpacingTokenPreview: React.FC<{ token: SpacingToken }> = ({ token }) => {
return (
<div className="spacing-token-preview" data-element="spacing-preview">
<div className="flex items-center space-x-2">
<div
className="bg-primary-200 dark:bg-primary-800 h-4 rounded"
style={{ width: token.value }}
data-element="spacing-bar"
/>
<span className="text-xs font-mono" data-element="spacing-value">
{token.value}
</span>
</div>
</div>
);
};
// 阴影令牌预览组件
const ShadowTokenPreview: React.FC<{ token: ShadowToken }> = ({ token }) => {
const { offsetX = '0', offsetY = '0', blurRadius = '0', spreadRadius = '0', color = 'transparent' } = token.value;
return (
<div className="shadow-token-preview" data-element="shadow-preview">
<div
className="shadow-demo w-full h-16 bg-white dark:bg-neutral-900 rounded-lg border border-neutral-200 dark:border-neutral-700"
style={{
boxShadow: `${offsetX} ${offsetY} ${blurRadius} ${spreadRadius} ${color}`,
}}
data-element="shadow-box"
/>
<div className="mt-2 text-xs text-neutral-600 dark:text-neutral-400 space-y-1" data-element="shadow-info">
<div>X: {offsetX}</div>
<div>Y: {offsetY}</div>
<div>Blur: {blurRadius}</div>
<div>Spread: {spreadRadius}</div>
</div>
</div>
);
};
// 圆角令牌预览组件
const BorderRadiusTokenPreview: React.FC<{ token: BorderRadiusToken }> = ({ token }) => {
return (
<div className="border-radius-token-preview" data-element="radius-preview">
<div
className="radius-demo w-full h-16 bg-primary-100 dark:bg-primary-900/20"
style={{ borderRadius: token.value }}
data-element="radius-box"
/>
<div className="mt-2 text-center text-xs font-mono" data-element="radius-value">
{token.value}
</div>
</div>
);
};
// 动画令牌预览组件
const AnimationTokenPreview: React.FC<{ token: AnimationToken }> = ({ token }) => {
const { duration = '300ms', timingFunction = 'ease' } = token.value;
return (
<div className="animation-token-preview" data-element="animation-preview">
<div className="flex items-center space-x-2">
<div className="animation-demo w-8 h-8 bg-primary-500 rounded-full" data-element="animated-circle" />
<div className="text-xs space-y-1" data-element="animation-info">
<div>Duration: {duration}</div>
<div>Easing: {timingFunction}</div>
</div>
</div>
</div>
);
};
// 默认令牌预览组件
const DefaultTokenPreview: React.FC<{ token: DesignToken }> = ({ token }) => {
return (
<div className="default-token-preview p-3 bg-neutral-50 dark:bg-neutral-800 rounded" data-element="default-preview">
<div className="text-center text-sm font-mono" data-element="value">
{typeof token.value === 'object' ? JSON.stringify(token.value) : String(token.value)}
</div>
</div>
);
};
export { TokenCard, ColorTokenPreview, TypographyTokenPreview, SpacingTokenPreview, ShadowTokenPreview, BorderRadiusTokenPreview, AnimationTokenPreview };
export type { TokenCardProps };
export default TokenCard;

View File

@@ -0,0 +1,283 @@
/**
* @fileoverview 应用布局组件
* @description 提供侧边栏导航、顶部栏和主内容区域的布局结构
*/
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { useDesignModeStore } from '../../main';
// 导航项目类型定义
interface NavItem {
path: string;
label: string;
icon: string;
description: string;
badge?: string | number;
}
/**
* 应用布局主组件
*/
const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const location = useLocation();
const { isDesignModeEnabled, toggleDesignMode } = useDesignModeStore();
// 导航菜单配置
const navItems: NavItem[] = [
{
path: '/dashboard',
label: 'Dashboard',
icon: '🏠',
description: 'Overview and system status',
badge: 'Main'
},
{
path: '/editor',
label: 'Component Editor',
icon: '🎨',
description: 'Visual component builder',
badge: 'Pro'
},
{
path: '/design-system',
label: 'Design System',
icon: '🎯',
description: 'Design tokens and guidelines',
badge: 'Core'
},
{
path: '/preview',
label: 'Live Preview',
icon: '⚡',
description: 'Real-time component preview'
}
];
// 导航项目组件
const NavItemComponent: React.FC<{ item: NavItem; isActive: boolean }> = ({ item, isActive }) => (
<Link
to={item.path}
className={`
nav-item group relative flex items-center px-4 py-3 text-sm font-medium rounded-lg
transition-all duration-200 hover:bg-neutral-100 dark:hover:bg-neutral-800
${isActive
? 'bg-primary-50 text-primary-700 dark:bg-primary-900/20 dark:text-primary-300'
: 'text-neutral-700 dark:text-neutral-300'
}
`}
data-component="NavItem"
data-element="nav-link"
data-nav-item={item.path.replace('/', '')}
onClick={() => setSidebarOpen(false)}
>
<span className="nav-item-icon text-lg mr-3" data-element="icon">
{item.icon}
</span>
<div className="nav-item-content flex-1 min-w-0" data-element="content">
<div className="nav-item-label font-medium" data-element="label">
{item.label}
</div>
<div className="nav-item-description text-xs text-neutral-500 dark:text-neutral-400 mt-0.5" data-element="description">
{item.description}
</div>
</div>
{item.badge && (
<span
className="nav-item-badge ml-2 px-2 py-0.5 text-xs font-semibold rounded-full bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300"
data-element="badge"
>
{item.badge}
</span>
)}
{/* 设计模式属性 */}
<div
className="design-mode-indicator absolute right-2 top-1/2 transform -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity"
data-design-mode={isDesignModeEnabled ? 'true' : 'false'}
>
<div className="w-2 h-2 bg-primary-500 rounded-full"></div>
</div>
</Link>
);
return (
<div
className="app-layout layout-container"
data-component="AppLayout"
data-design-mode={isDesignModeEnabled ? 'true' : 'false'}
>
{/* 移动端遮罩层 */}
<AnimatePresence>
{sidebarOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-30 bg-black bg-opacity-50 lg:hidden"
data-component="MobileOverlay"
data-element="overlay"
onClick={() => setSidebarOpen(false)}
/>
)}
</AnimatePresence>
{/* 侧边栏 */}
<motion.aside
initial={false}
animate={{
x: sidebarOpen ? 0 : '-100%',
}}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className={`
layout-sidebar fixed inset-y-0 left-0 z-40 flex flex-col
bg-white dark:bg-neutral-900 border-r border-neutral-200 dark:border-neutral-800
lg:translate-x-0 lg:static lg:inset-0 lg:z-auto
${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
`}
data-component="Sidebar"
data-element="sidebar"
>
{/* 侧边栏头部 */}
<div className="sidebar-header flex items-center justify-between h-16 px-6 border-b border-neutral-200 dark:border-neutral-800" data-element="header">
<div className="sidebar-brand flex items-center" data-element="brand">
<div className="w-8 h-8 bg-gradient-to-br from-primary-500 to-secondary-500 rounded-lg flex items-center justify-center mr-3">
<span className="text-white font-bold text-sm">AD</span>
</div>
<div className="brand-content">
<h1 className="text-lg font-bold text-neutral-900 dark:text-neutral-100">
Design Mode
</h1>
<p className="text-xs text-neutral-500 dark:text-neutral-400">
Full Featured Showcase
</p>
</div>
</div>
{/* 移动端关闭按钮 */}
<button
onClick={() => setSidebarOpen(false)}
className="lg:hidden p-1 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-800"
data-component="IconButton"
data-element="close-button"
data-action="close-sidebar"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* 导航菜单 */}
<nav className="sidebar-nav flex-1 px-4 py-6 space-y-2 overflow-y-auto custom-scrollbar" data-element="nav">
{navItems.map((item) => (
<NavItemComponent
key={item.path}
item={item}
isActive={location.pathname === item.path}
/>
))}
</nav>
{/* 侧边栏底部 */}
<div className="sidebar-footer p-4 border-t border-neutral-200 dark:border-neutral-800" data-element="footer">
<div className="sidebar-actions space-y-2" data-element="actions">
<button
onClick={toggleDesignMode}
className={`
w-full flex items-center justify-center px-3 py-2 text-sm font-medium rounded-md
transition-colors duration-200
${isDesignModeEnabled
? 'bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300'
: 'bg-neutral-100 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300'
}
`}
data-component="ToggleButton"
data-element="design-mode-toggle"
data-action="toggle-design-mode"
>
<span className="mr-2">{isDesignModeEnabled ? '🎯' : '👁️'}</span>
{isDesignModeEnabled ? 'Design Mode ON' : 'Design Mode OFF'}
</button>
<div className="text-center text-xs text-neutral-500 dark:text-neutral-400">
Version 1.0.0
</div>
</div>
</div>
</motion.aside>
{/* 主内容区域 */}
<div className="layout-main flex flex-col min-h-screen">
{/* 顶部导航栏 */}
<header className="layout-header h-16 bg-white dark:bg-neutral-900 border-b border-neutral-200 dark:border-neutral-800 flex items-center justify-between px-6 relative z-10" data-component="Header" data-element="header">
{/* 移动端菜单按钮 */}
<button
onClick={() => setSidebarOpen(true)}
className="lg:hidden p-2 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-800"
data-component="IconButton"
data-element="mobile-menu-button"
data-action="toggle-sidebar"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
{/* 页面标题和面包屑 */}
<div className="header-content flex-1" data-element="content">
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
{navItems.find(item => item.path === location.pathname)?.label || 'Unknown Page'}
</h2>
</div>
{/* 顶部操作区域 */}
<div className="header-actions flex items-center space-x-4" data-element="actions">
{/* 设计模式状态指示器 */}
<div
className={`
header-status-indicator flex items-center px-3 py-1 rounded-full text-xs font-medium
${isDesignModeEnabled
? 'bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300'
: 'bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400'
}
`}
data-element="status-indicator"
>
<div className={`
w-2 h-2 rounded-full mr-2
${isDesignModeEnabled ? 'bg-primary-500' : 'bg-neutral-400'}
`}></div>
{isDesignModeEnabled ? 'Design Mode' : 'Preview Mode'}
</div>
{/* 用户头像/设置 */}
<button
className="header-user-button p-2 rounded-full hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors"
data-component="AvatarButton"
data-element="user-avatar"
data-action="open-user-menu"
>
<div className="w-8 h-8 bg-gradient-to-br from-primary-400 to-secondary-400 rounded-full flex items-center justify-center">
<span className="text-white font-medium text-sm">U</span>
</div>
</button>
</div>
</header>
{/* 主内容 */}
<main
className="layout-content flex-1 overflow-auto bg-neutral-50 dark:bg-neutral-800"
data-component="Main"
data-element="main-content"
>
{children}
</main>
</div>
</div>
);
};
export default AppLayout;

View File

@@ -0,0 +1,274 @@
/**
* @fileoverview 按钮组件
* @description 提供各种样式和状态的按钮组件
*/
import React, { forwardRef } from 'react';
import { motion } from 'framer-motion';
import { clsx } from 'clsx';
// 按钮变体类型
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'link' | 'danger';
// 按钮尺寸类型
type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
// 按钮属性接口
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
loading?: boolean;
loadingText?: string;
icon?: React.ReactNode;
iconPosition?: 'left' | 'right';
fullWidth?: boolean;
children?: React.ReactNode;
className?: string;
variantClass?: string;
sizeClass?: string;
}
/**
* 按钮组件实现
*/
const Button = forwardRef<HTMLButtonElement, ButtonProps>(({
variant = 'primary',
size = 'md',
loading = false,
loadingText,
icon,
iconPosition = 'left',
fullWidth = false,
children,
className,
disabled,
variantClass,
sizeClass,
onClick,
...props
}, ref) => {
// 按钮变体样式配置
const variantStyles: Record<ButtonVariant, string> = {
primary: `
bg-primary-600 border-primary-600 text-white
hover:bg-primary-700 hover:border-primary-700
focus:ring-primary-500 focus:ring-offset-2
active:bg-primary-800 active:border-primary-800
disabled:bg-primary-300 disabled:border-primary-300
`,
secondary: `
bg-neutral-200 border-neutral-300 text-neutral-900
hover:bg-neutral-300 hover:border-neutral-400
focus:ring-neutral-500 focus:ring-offset-2
active:bg-neutral-400 active:border-neutral-500
disabled:bg-neutral-100 disabled:border-neutral-200
dark:bg-neutral-700 dark:border-neutral-600 dark:text-neutral-100
dark:hover:bg-neutral-600 dark:hover:border-neutral-500
dark:active:bg-neutral-500 dark:active:border-neutral-400
dark:disabled:bg-neutral-800 dark:disabled:border-neutral-700
`,
outline: `
bg-transparent border-primary-600 text-primary-600
hover:bg-primary-600 hover:text-white
focus:ring-primary-500 focus:ring-offset-2
active:bg-primary-700 active:border-primary-700
disabled:text-primary-300 disabled:border-primary-300
disabled:hover:bg-transparent disabled:hover:text-primary-300
`,
ghost: `
bg-transparent border-transparent text-neutral-700
hover:bg-neutral-100 hover:text-neutral-900
focus:ring-neutral-500 focus:ring-offset-2
active:bg-neutral-200 active:text-neutral-900
disabled:text-neutral-400
dark:text-neutral-300 dark:hover:bg-neutral-800 dark:hover:text-neutral-100
dark:active:bg-neutral-700 dark:active:text-neutral-100
`,
link: `
bg-transparent border-transparent text-primary-600
hover:text-primary-700 hover:underline
focus:ring-primary-500 focus:ring-offset-2
active:text-primary-800
disabled:text-primary-300 disabled:no-underline
`,
danger: `
bg-error-600 border-error-600 text-white
hover:bg-error-700 hover:border-error-700
focus:ring-error-500 focus:ring-offset-2
active:bg-error-800 active:border-error-800
disabled:bg-error-300 disabled:border-error-300
`,
};
// 按钮尺寸样式配置
const sizeStyles: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
xl: 'px-8 py-4 text-lg',
};
// 计算按钮内容
const buttonContent = (
<span className="button-content flex items-center justify-center space-x-2">
{icon && iconPosition === 'left' && !loading && (
<span className="button-icon-left" data-element="icon-left">
{icon}
</span>
)}
{loading && (
<span className="button-loading-icon" data-element="loading-icon">
<div className="loading-spinner w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
</span>
)}
<span className="button-text" data-element="text">
{loading && loadingText ? loadingText : children}
</span>
{icon && iconPosition === 'right' && !loading && (
<span className="button-icon-right" data-element="icon-right">
{icon}
</span>
)}
</span>
);
// 应用自定义样式类
const combinedVariantClass = variantClass || variantStyles[variant];
const combinedSizeClass = sizeClass || sizeStyles[size];
return (
<motion.button
ref={ref}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className={clsx(
// 基础样式
`
inline-flex items-center justify-center font-medium rounded-md border
transition-all duration-200 focus:outline-none focus:ring-2
disabled:opacity-50 disabled:cursor-not-allowed
disabled:transform-none
`,
// 变体样式
combinedVariantClass,
// 尺寸样式
combinedSizeClass,
// 全宽度
fullWidth && 'w-full',
// 自定义类名
className
)}
disabled={disabled || loading}
onClick={onClick}
data-component="Button"
data-element="button"
data-variant={variant}
data-size={size}
data-loading={loading ? 'true' : 'false'}
data-disabled={disabled ? 'true' : 'false'}
{...props}
>
{buttonContent}
</motion.button>
);
});
// 显示名称
Button.displayName = 'Button';
/**
* 图标按钮组件
*/
interface IconButtonProps extends Omit<ButtonProps, 'children' | 'size'> {
size?: Exclude<ButtonSize, 'xl'>;
'aria-label': string; // 无障碍属性必需
}
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(({
size = 'md',
className,
...props
}, ref) => {
const iconSizeStyles: Record<ButtonSize, string> = {
sm: 'w-8 h-8 p-1.5',
md: 'w-10 h-10 p-2',
lg: 'w-12 h-12 p-2.5',
xl: 'w-14 h-14 p-3',
};
return (
<Button
ref={ref}
size={size}
className={clsx(
'!p-0 !rounded-full',
iconSizeStyles[size],
className
)}
{...props}
/>
);
});
IconButton.displayName = 'IconButton';
/**
* 浮动操作按钮
*/
interface FloatingActionButtonProps extends Omit<IconButtonProps, 'variant'> {
variant?: ButtonVariant;
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
}
const FloatingActionButton = forwardRef<HTMLButtonElement, FloatingActionButtonProps>(({
position = 'bottom-right',
className,
...props
}, ref) => {
const positionStyles = {
'bottom-right': 'fixed bottom-6 right-6 z-50',
'bottom-left': 'fixed bottom-6 left-6 z-50',
'top-right': 'fixed top-6 right-6 z-50',
'top-left': 'fixed top-6 left-6 z-50',
};
return (
<motion.div
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className={clsx('floating-action-button', positionStyles[position])}
data-component="FloatingActionButton"
data-element="fab"
data-position={position}
>
<IconButton
ref={ref}
size="lg"
variant="primary"
className={clsx(
`
!rounded-full !shadow-lg !shadow-primary-500/25
hover:!shadow-xl hover:!shadow-primary-500/30
bg-gradient-to-br from-primary-500 to-primary-600
hover:from-primary-600 hover:to-primary-700
border-0
`,
className
)}
{...props}
/>
</motion.div>
);
});
FloatingActionButton.displayName = 'FloatingActionButton';
export { Button, IconButton, FloatingActionButton };
export type { ButtonProps, IconButtonProps, FloatingActionButtonProps, ButtonVariant, ButtonSize };
export default Button;

View File

@@ -0,0 +1,421 @@
/**
* @fileoverview 卡片组件
* @description 提供各种样式和布局的卡片容器组件
*/
import React, { forwardRef } from 'react';
import { motion } from 'framer-motion';
import { clsx } from 'clsx';
// 卡片变体类型
type CardVariant = 'default' | 'elevated' | 'outlined' | 'ghost' | 'filled';
// 卡片尺寸类型
type CardSize = 'sm' | 'md' | 'lg' | 'xl';
// 卡片属性接口
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: CardVariant;
size?: CardSize;
hover?: boolean;
interactive?: boolean;
loading?: boolean;
children?: React.ReactNode;
className?: string;
variantClass?: string;
sizeClass?: string;
}
// 卡片标题属性接口
interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
title?: string;
subtitle?: string;
action?: React.ReactNode;
children?: React.ReactNode;
className?: string;
}
// 卡片内容属性接口
interface CardBodyProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
className?: string;
}
// 卡片底部属性接口
interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
className?: string;
}
// 卡片图片属性接口
interface CardImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
src: string;
alt: string;
overlay?: boolean;
height?: string | number;
className?: string;
}
/**
* 卡片组件实现
*/
const Card = forwardRef<HTMLDivElement, CardProps>(({
variant = 'default',
size = 'md',
hover = false,
interactive = false,
loading = false,
children,
className,
variantClass,
sizeClass,
onClick,
...props
}, ref) => {
// 卡片变体样式配置
const variantStyles: Record<CardVariant, string> = {
default: `
bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800
`,
elevated: `
bg-white dark:bg-neutral-900 border-0 shadow-lg dark:shadow-neutral-900/20
hover:shadow-xl transition-shadow duration-300
`,
outlined: `
bg-transparent border-2 border-primary-200 dark:border-primary-800
hover:border-primary-300 dark:hover:border-primary-700
`,
ghost: `
bg-transparent border-0
hover:bg-neutral-50 dark:hover:bg-neutral-800/50
`,
filled: `
bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700
`,
};
// 卡片尺寸样式配置
const sizeStyles: Record<CardSize, string> = {
sm: 'p-3 space-y-2',
md: 'p-4 space-y-3',
lg: 'p-6 space-y-4',
xl: 'p-8 space-y-6',
};
// 悬停效果样式
const hoverStyles = hover || interactive ? `
transition-all duration-200 ease-in-out cursor-pointer
hover:shadow-md hover:-translate-y-1
hover:shadow-neutral-500/10 dark:hover:shadow-neutral-900/20
` : '';
// 交互样式
const interactiveStyles = interactive ? `
active:scale-95 active:shadow-sm
` : '';
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
whileHover={hover || interactive ? { y: -4 } : {}}
whileTap={interactive ? { scale: 0.98 } : {}}
className={clsx(
// 基础样式
`
card rounded-lg overflow-hidden
${interactive ? 'cursor-pointer' : ''}
${loading ? 'pointer-events-none' : ''}
`,
// 变体样式
variantStyles[variant],
// 尺寸样式
sizeStyles[size],
// 悬停和交互样式
hoverStyles,
interactiveStyles,
// 自定义类名
className
)}
onClick={interactive ? onClick : undefined}
data-component="Card"
data-element="card"
data-variant={variant}
data-size={size}
data-hover={hover ? 'true' : 'false'}
data-interactive={interactive ? 'true' : 'false'}
data-loading={loading ? 'true' : 'false'}
{...props}
>
{loading ? (
<CardSkeleton variant={variant} size={size} />
) : (
children
)}
</motion.div>
);
});
Card.displayName = 'Card';
/**
* 卡片头部组件
*/
const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(({
title,
subtitle,
action,
children,
className,
...props
}, ref) => {
return (
<div
ref={ref}
className={clsx(
'card-header flex items-start justify-between',
className
)}
data-component="CardHeader"
data-element="header"
{...props}
>
<div className="header-content flex-1" data-element="content">
{title && (
<h3
className="text-lg font-semibold text-neutral-900 dark:text-neutral-100"
data-element="title"
>
{title}
</h3>
)}
{subtitle && (
<p
className="text-sm text-neutral-600 dark:text-neutral-400 mt-1"
data-element="subtitle"
>
{subtitle}
</p>
)}
{children}
</div>
{action && (
<div className="header-action ml-4 flex-shrink-0" data-element="action">
{action}
</div>
)}
</div>
);
});
CardHeader.displayName = 'CardHeader';
/**
* 卡片内容组件
*/
const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(({
children,
className,
...props
}, ref) => {
return (
<div
ref={ref}
className={clsx('card-body', className)}
data-component="CardBody"
data-element="body"
{...props}
>
{children}
</div>
);
});
CardBody.displayName = 'CardBody';
/**
* 卡片底部组件
*/
const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(({
children,
className,
...props
}, ref) => {
return (
<div
ref={ref}
className={clsx(
'card-footer flex items-center justify-between',
'border-t border-neutral-200 dark:border-neutral-800 pt-4 mt-4',
className
)}
data-component="CardFooter"
data-element="footer"
{...props}
>
{children}
</div>
);
});
CardFooter.displayName = 'CardFooter';
/**
* 卡片图片组件
*/
const CardImage = forwardRef<HTMLImageElement, CardImageProps>(({
src,
alt,
overlay = false,
height = 200,
className,
...props
}, ref) => {
return (
<div
className={clsx(
'card-image relative overflow-hidden',
className
)}
data-component="CardImage"
data-element="image"
data-overlay={overlay ? 'true' : 'false'}
>
<img
ref={ref}
src={src}
alt={alt}
className={clsx(
'w-full object-cover',
overlay && 'absolute inset-0 h-full'
)}
style={{ height }}
data-element="img"
{...props}
/>
{overlay && (
<div
className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"
data-element="overlay"
/>
)}
</div>
);
});
CardImage.displayName = 'CardImage';
/**
* 卡片骨架屏组件
*/
const CardSkeleton: React.FC<{ variant: CardVariant; size: CardSize }> = ({ variant, size }) => {
const skeletonSizes = {
sm: { padding: 'p-3', gap: 'space-y-2' },
md: { padding: 'p-4', gap: 'space-y-3' },
lg: { padding: 'p-6', gap: 'space-y-4' },
xl: { padding: 'p-8', gap: 'space-y-6' },
};
const { padding, gap } = skeletonSizes[size];
return (
<div className={`card-skeleton ${padding} ${gap}`} data-element="skeleton">
{/* 头部骨架 */}
<div className="flex items-start justify-between" data-element="header-skeleton">
<div className="flex-1" data-element="content-skeleton">
<div className="h-5 bg-neutral-200 dark:bg-neutral-700 rounded w-3/4 mb-2" />
<div className="h-4 bg-neutral-200 dark:bg-neutral-700 rounded w-1/2" />
</div>
<div className="ml-4" data-element="action-skeleton">
<div className="w-8 h-8 bg-neutral-200 dark:bg-neutral-700 rounded" />
</div>
</div>
{/* 内容骨架 */}
<div className="space-y-3" data-element="body-skeleton">
<div className="h-4 bg-neutral-200 dark:bg-neutral-700 rounded" />
<div className="h-4 bg-neutral-200 dark:bg-neutral-700 rounded w-5/6" />
<div className="h-4 bg-neutral-200 dark:bg-neutral-700 rounded w-4/6" />
</div>
{/* 底部骨架 */}
<div className="flex items-center justify-between pt-4 border-t border-neutral-200 dark:border-neutral-800" data-element="footer-skeleton">
<div className="h-4 bg-neutral-200 dark:bg-neutral-700 rounded w-1/3" />
<div className="h-8 w-20 bg-neutral-200 dark:bg-neutral-700 rounded" />
</div>
</div>
);
};
/**
* 卡片组组件
*/
interface CardGroupProps extends React.HTMLAttributes<HTMLDivElement> {
columns?: 1 | 2 | 3 | 4;
gap?: 'sm' | 'md' | 'lg' | 'xl';
children?: React.ReactNode;
}
const CardGroup: React.FC<CardGroupProps> = ({
columns = 3,
gap = 'md',
children,
className,
...props
}) => {
const columnClasses = {
1: 'grid-cols-1',
2: 'grid-cols-1 md:grid-cols-2',
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
};
const gapClasses = {
sm: 'gap-3',
md: 'gap-4',
lg: 'gap-6',
xl: 'gap-8',
};
return (
<div
className={clsx(
'card-group grid',
columnClasses[columns],
gapClasses[gap],
className
)}
data-component="CardGroup"
data-element="group"
data-columns={columns}
data-gap={gap}
{...props}
>
{children}
</div>
);
};
export {
Card,
CardHeader,
CardBody,
CardFooter,
CardImage,
CardGroup,
CardSkeleton
};
export type {
CardProps,
CardHeaderProps,
CardBodyProps,
CardFooterProps,
CardImageProps,
CardGroupProps,
CardVariant,
CardSize
};
export default Card;

View File

@@ -0,0 +1,297 @@
/**
* @fileoverview 加载动画组件
* @description 提供各种加载状态的可复用组件
*/
import React from 'react';
import { motion } from 'framer-motion';
/**
* 旋转加载器组件
*/
const LoadingSpinner: React.FC<{
size?: 'sm' | 'md' | 'lg' | 'xl';
color?: 'primary' | 'secondary' | 'neutral' | 'white';
className?: string;
}> = ({
size = 'md',
color = 'primary',
className = ''
}) => {
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-6 h-6',
lg: 'w-8 h-8',
xl: 'w-12 h-12'
};
const colorClasses = {
primary: 'border-primary-600 border-t-transparent',
secondary: 'border-secondary-600 border-t-transparent',
neutral: 'border-neutral-400 border-t-transparent',
white: 'border-white border-t-transparent'
};
return (
<div
className={`
inline-block border-2 rounded-full animate-spin
${sizeClasses[size]}
${colorClasses[color]}
${className}
`}
data-component="LoadingSpinner"
data-element="spinner"
data-size={size}
data-color={color}
/>
);
};
/**
* 点状加载器组件
*/
const LoadingDots: React.FC<{
size?: 'sm' | 'md' | 'lg';
color?: 'primary' | 'secondary' | 'neutral' | 'white';
className?: string;
}> = ({
size = 'md',
color = 'primary',
className = ''
}) => {
const sizeClasses = {
sm: 'w-1 h-1',
md: 'w-2 h-2',
lg: 'w-3 h-3'
};
const colorClasses = {
primary: 'bg-primary-600',
secondary: 'bg-secondary-600',
neutral: 'bg-neutral-400',
white: 'bg-white'
};
return (
<div
className={`
loading-dots flex space-x-1
${className}
`}
data-component="LoadingDots"
data-element="dots"
data-size={size}
data-color={color}
>
{[0, 1, 2].map((index) => (
<motion.div
key={index}
className={`
${sizeClasses[size]} ${colorClasses[color]} rounded-full
`}
data-element="dot"
data-dot-index={index}
animate={{
scale: [1, 1.2, 1],
opacity: [0.7, 1, 0.7],
}}
transition={{
duration: 1.4,
repeat: Infinity,
delay: index * 0.16,
ease: 'easeInOut'
}}
/>
))}
</div>
);
};
/**
* 脉冲加载器组件
*/
const LoadingPulse: React.FC<{
size?: 'sm' | 'md' | 'lg' | 'xl';
color?: 'primary' | 'secondary' | 'neutral' | 'white';
className?: string;
}> = ({
size = 'md',
color = 'primary',
className = ''
}) => {
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-6 h-6',
lg: 'w-8 h-8',
xl: 'w-12 h-12'
};
const colorClasses = {
primary: 'bg-primary-600',
secondary: 'bg-secondary-600',
neutral: 'bg-neutral-400',
white: 'bg-white'
};
return (
<div
className={`
${sizeClasses[size]} ${colorClasses[color]} rounded-full
animate-pulse
${className}
`}
data-component="LoadingPulse"
data-element="pulse"
data-size={size}
data-color={color}
/>
);
};
/**
* 页面级加载组件
*/
const PageLoading: React.FC<{
title?: string;
description?: string;
variant?: 'spinner' | 'dots' | 'pulse';
className?: string;
}> = ({
title = 'Loading...',
description = 'Please wait while we prepare your content.',
variant = 'spinner',
className = ''
}) => {
const renderLoader = () => {
switch (variant) {
case 'dots':
return <LoadingDots size="lg" color="primary" />;
case 'pulse':
return <LoadingPulse size="xl" color="primary" />;
default:
return <LoadingSpinner size="lg" color="primary" />;
}
};
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={`
page-loading flex flex-col items-center justify-center min-h-[400px]
text-center p-8
${className}
`}
data-component="PageLoading"
data-element="loading-container"
data-variant={variant}
>
<motion.div
initial={{ scale: 0.8 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
className="loading-animation mb-6"
data-element="animation"
>
{renderLoader()}
</motion.div>
<motion.h3
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.1 }}
className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2"
data-element="title"
>
{title}
</motion.h3>
<motion.p
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2 }}
className="text-neutral-600 dark:text-neutral-400 max-w-sm"
data-element="description"
>
{description}
</motion.p>
</motion.div>
);
};
/**
* 按钮内加载组件
*/
const ButtonLoading: React.FC<{
size?: 'sm' | 'md' | 'lg';
color?: 'primary' | 'secondary' | 'neutral' | 'white';
text?: string;
}> = ({
size = 'sm',
color = 'white',
text = 'Loading...'
}) => {
return (
<div
className="button-loading flex items-center space-x-2"
data-component="ButtonLoading"
data-element="button-loading"
>
<LoadingSpinner size={size} color={color} />
<span className="sr-only">{text}</span>
</div>
);
};
/**
* 骨架屏加载组件
*/
const SkeletonLoader: React.FC<{
lines?: number;
className?: string;
}> = ({
lines = 3,
className = ''
}) => {
return (
<div
className={`skeleton-loader space-y-3 ${className}`}
data-component="SkeletonLoader"
data-element="skeleton"
>
{Array.from({ length: lines }).map((_, index) => (
<motion.div
key={index}
className={`
skeleton-line h-4 bg-neutral-200 dark:bg-neutral-700 rounded
${index === lines - 1 ? 'w-3/4' : 'w-full'}
`}
data-element="skeleton-line"
data-line-index={index}
animate={{
opacity: [0.5, 1, 0.5],
}}
transition={{
duration: 1.5,
repeat: Infinity,
delay: index * 0.1,
ease: 'easeInOut'
}}
/>
))}
</div>
);
};
export {
LoadingSpinner,
LoadingDots,
LoadingPulse,
PageLoading,
ButtonLoading,
SkeletonLoader
};
export default LoadingSpinner;