"添加前端模板和运行代码模块"
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user