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

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,711 @@
/**
* @fileoverview 组件编辑器页面
* @description 提供可视化的组件编辑功能,支持拖拽、属性编辑和实时预览
*/
import React, { useState, useCallback, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Plus,
Play,
Square,
Save,
Download,
Upload,
Eye,
EyeOff,
Settings,
Layers,
Code,
Smartphone,
Monitor,
Tablet
} from 'lucide-react';
import { Card, CardBody, CardHeader } from '../components/ui/Card';
import { Button, IconButton } from '../components/ui/Button';
import { LoadingSpinner } from '../components/ui/LoadingSpinner';
import { useDesignModeStore } from '../main';
// 编辑模式类型
type EditMode = 'design' | 'code' | 'preview';
// 设备类型
type DeviceType = 'desktop' | 'tablet' | 'mobile';
// 组件库数据
const COMPONENT_LIBRARY = [
{
id: 'button',
name: 'Button',
category: 'Input',
icon: '🔘',
description: 'Interactive button component',
props: {
variant: 'primary',
size: 'md',
disabled: false,
loading: false
},
variants: ['primary', 'secondary', 'outline', 'ghost', 'danger'],
sizes: ['sm', 'md', 'lg', 'xl']
},
{
id: 'card',
name: 'Card',
category: 'Layout',
icon: '🃏',
description: 'Container component with header, body, and footer',
props: {
variant: 'default',
size: 'md',
hover: false,
interactive: false
},
variants: ['default', 'elevated', 'outlined', 'ghost', 'filled'],
sizes: ['sm', 'md', 'lg', 'xl']
},
{
id: 'input',
name: 'Input',
category: 'Form',
icon: '📝',
description: 'Text input field component',
props: {
type: 'text',
placeholder: 'Enter text...',
disabled: false,
required: false,
error: false
},
variants: ['default', 'filled', 'underlined'],
sizes: ['sm', 'md', 'lg']
},
{
id: 'modal',
name: 'Modal',
category: 'Overlay',
icon: '🪟',
description: 'Overlay modal dialog',
props: {
open: false,
size: 'md',
closable: true,
maskClosable: true
},
variants: ['default', 'fullscreen', 'drawer'],
sizes: ['sm', 'md', 'lg', 'xl']
}
];
// 组件预览组件
const ComponentPreview: React.FC<{
component: any;
deviceType: DeviceType;
isPlaying: boolean;
}> = ({ component, deviceType, isPlaying }) => {
const renderComponent = () => {
switch (component.id) {
case 'button':
return (
<button
className={`
px-4 py-2 rounded-md font-medium transition-colors
${component.props.variant === 'primary' ? 'bg-primary-600 text-white hover:bg-primary-700' :
component.props.variant === 'secondary' ? 'bg-neutral-200 text-neutral-900 hover:bg-neutral-300' :
component.props.variant === 'outline' ? 'border border-primary-600 text-primary-600 hover:bg-primary-600 hover:text-white' :
component.props.variant === 'danger' ? 'bg-error-600 text-white hover:bg-error-700' :
'bg-transparent text-neutral-700 hover:bg-neutral-100'
}
${component.props.size === 'sm' ? 'px-3 py-1.5 text-sm' :
component.props.size === 'lg' ? 'px-6 py-3 text-lg' :
component.props.size === 'xl' ? 'px-8 py-4 text-xl' :
'px-4 py-2 text-sm'
}
${component.props.disabled ? 'opacity-50 cursor-not-allowed' : ''}
`}
disabled={component.props.disabled}
data-component="Button"
data-element="preview-button"
data-variant={component.props.variant}
data-size={component.props.size}
>
{isPlaying ? 'Loading...' : 'Button Component'}
</button>
);
case 'card':
return (
<div
className={`
bg-white dark:bg-neutral-900 rounded-lg overflow-hidden
${component.props.variant === 'elevated' ? 'shadow-lg' :
component.props.variant === 'outlined' ? 'border-2 border-primary-200 dark:border-primary-800' :
component.props.variant === 'filled' ? 'bg-neutral-50 dark:bg-neutral-800' :
'border border-neutral-200 dark:border-neutral-800'
}
${component.props.hover ? 'hover:shadow-md transition-shadow' : ''}
${component.props.size === 'sm' ? 'p-3' :
component.props.size === 'lg' ? 'p-6' :
component.props.size === 'xl' ? 'p-8' :
'p-4'
}
`}
data-component="Card"
data-element="preview-card"
data-variant={component.props.variant}
data-size={component.props.size}
>
<div className="font-semibold text-neutral-900 dark:text-neutral-100 mb-2">
Card Header
</div>
<div className="text-neutral-600 dark:text-neutral-400 text-sm mb-4">
This is a preview of the {component.name} component.
</div>
<div className="text-xs text-neutral-500">
Card content goes here...
</div>
</div>
);
case 'input':
return (
<div className="space-y-2">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
Input Label
</label>
<input
type={component.props.type}
placeholder={component.props.placeholder}
disabled={component.props.disabled}
required={component.props.required}
className={`
w-full px-3 py-2 border rounded-md transition-colors
${component.props.error ? 'border-error-300 focus:border-error-500 focus:ring-error-500' :
'border-neutral-300 dark:border-neutral-600 focus:border-primary-500 focus:ring-primary-500'
}
${component.props.variant === 'filled' ? 'bg-neutral-50 dark:bg-neutral-800' : 'bg-white dark:bg-neutral-900'}
disabled:opacity-50 disabled:cursor-not-allowed
`}
data-component="Input"
data-element="preview-input"
data-variant={component.props.variant}
data-error={component.props.error ? 'true' : 'false'}
/>
</div>
);
case 'modal':
return (
<div className="relative">
<div className="p-8 bg-neutral-100 dark:bg-neutral-800 rounded-lg border-2 border-dashed border-neutral-300 dark:border-neutral-600">
<div className="text-center text-neutral-500 dark:text-neutral-400">
<div className="w-16 h-16 bg-neutral-200 dark:bg-neutral-700 rounded-full flex items-center justify-center mx-auto mb-4">
🪟
</div>
<p className="text-sm">
{component.props.open ? 'Modal is open' : 'Click to open modal'}
</p>
<p className="text-xs mt-2">
Size: {component.props.size} Closable: {component.props.closable ? 'Yes' : 'No'}
</p>
</div>
</div>
</div>
);
default:
return (
<div className="p-8 bg-neutral-100 dark:bg-neutral-800 rounded-lg border-2 border-dashed border-neutral-300 dark:border-neutral-600 text-center">
<p className="text-neutral-500 dark:text-neutral-400">
Component preview not available
</p>
</div>
);
}
};
const getDeviceClasses = () => {
switch (deviceType) {
case 'mobile':
return 'max-w-sm mx-auto';
case 'tablet':
return 'max-w-2xl mx-auto';
default:
return 'w-full';
}
};
return (
<div
className={`component-preview ${getDeviceClasses()}`}
data-component="ComponentPreview"
data-element="preview"
data-device={deviceType}
data-component-id={component.id}
>
{renderComponent()}
</div>
);
};
// 属性面板组件
const PropertyPanel: React.FC<{
component: any;
onUpdate: (props: any) => void;
}> = ({ component, onUpdate }) => {
const updateProp = (key: string, value: any) => {
onUpdate({
...component.props,
[key]: value
});
};
return (
<Card>
<CardHeader
title={`${component.name} Properties`}
subtitle={`${component.category}${component.id}`}
/>
<CardBody className="space-y-6">
{/* 变体选择 */}
{component.variants && (
<div className="space-y-2">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
Variant
</label>
<select
value={component.props.variant}
onChange={(e) => updateProp('variant', e.target.value)}
className="w-full px-3 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="Select"
data-element="variant-select"
data-component-prop="variant"
>
{component.variants.map((variant: string) => (
<option key={variant} value={variant}>
{variant.charAt(0).toUpperCase() + variant.slice(1)}
</option>
))}
</select>
</div>
)}
{/* 尺寸选择 */}
{component.sizes && (
<div className="space-y-2">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
Size
</label>
<select
value={component.props.size}
onChange={(e) => updateProp('size', e.target.value)}
className="w-full px-3 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="Select"
data-element="size-select"
data-component-prop="size"
>
{component.sizes.map((size: string) => (
<option key={size} value={size}>
{size.toUpperCase()}
</option>
))}
</select>
</div>
)}
{/* 布尔属性 */}
{['disabled', 'loading', 'hover', 'interactive', 'required', 'error'].map((prop) => {
if (!(prop in component.props)) return null;
return (
<div key={prop} className="flex items-center justify-between">
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
{prop.charAt(0).toUpperCase() + prop.slice(1)}
</label>
<button
onClick={() => updateProp(prop, !component.props[prop])}
className={`
relative inline-flex h-6 w-11 items-center rounded-full transition-colors
${component.props[prop] ? 'bg-primary-600' : 'bg-neutral-200 dark:bg-neutral-700'}
`}
data-component="Toggle"
data-element="boolean-toggle"
data-prop={prop}
data-value={component.props[prop] ? 'true' : 'false'}
>
<span
className={`
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
${component.props[prop] ? 'translate-x-6' : 'translate-x-1'}
`}
/>
</button>
</div>
);
})}
{/* 文本属性 */}
{['placeholder', 'type'].map((prop) => {
if (!(prop in component.props)) return null;
return (
<div key={prop} className="space-y-2">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300">
{prop.charAt(0).toUpperCase() + prop.slice(1)}
</label>
<input
type="text"
value={component.props[prop]}
onChange={(e) => updateProp(prop, e.target.value)}
className="w-full px-3 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="text-input"
data-component-prop={prop}
/>
</div>
);
})}
</CardBody>
</Card>
);
};
/**
* 组件编辑器主页面
*/
const ComponentEditor: React.FC = () => {
const [selectedComponent, setSelectedComponent] = useState(COMPONENT_LIBRARY[0]);
const [editMode, setEditMode] = useState<EditMode>('design');
const [deviceType, setDeviceType] = useState<DeviceType>('desktop');
const [isPlaying, setIsPlaying] = useState(false);
const [code, setCode] = useState('');
const { isDesignModeEnabled } = useDesignModeStore();
// 组件属性更新处理
const handleComponentUpdate = useCallback((newProps: any) => {
setSelectedComponent((prev: any) => ({
...prev,
props: newProps
}));
}, []);
// 开始/停止播放状态
const togglePlayState = () => {
setIsPlaying(!isPlaying);
};
// 生成代码
const generateCode = () => {
const propsString = Object.entries(selectedComponent.props)
.map(([key, value]) => `${key}="${value}"`)
.join(' ');
const codeString = `<${selectedComponent.id}${propsString ? ' ' + propsString : ''} />`;
setCode(codeString);
setEditMode('code');
};
// 复制代码
const copyCode = async () => {
try {
await navigator.clipboard.writeText(code);
// 可以添加成功提示
} catch (error) {
console.error('Failed to copy code:', error);
}
};
return (
<div
className="component-editor h-full flex flex-col"
data-component="ComponentEditor"
data-design-mode={isDesignModeEnabled ? 'true' : 'false'}
data-edit-mode={editMode}
data-device={deviceType}
>
{/* 编辑器工具栏 */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="editor-toolbar border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4"
data-element="toolbar"
>
<div className="flex items-center justify-between">
{/* 左侧:组件库和模式切换 */}
<div className="flex items-center space-x-4" data-element="left-controls">
{/* 组件库选择 */}
<div className="flex items-center space-x-2" data-element="component-selector">
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Component:
</label>
<select
value={selectedComponent.id}
onChange={(e) => {
const component = COMPONENT_LIBRARY.find(c => c.id === e.target.value);
if (component) setSelectedComponent(component);
}}
className="px-3 py-1.5 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="Select"
data-element="component-select"
>
{COMPONENT_LIBRARY.map((component) => (
<option key={component.id} value={component.id}>
{component.icon} {component.name}
</option>
))}
</select>
</div>
{/* 模式切换 */}
<div className="flex border border-neutral-300 dark:border-neutral-600 rounded-md overflow-hidden" data-element="mode-toggle">
{(['design', 'code', 'preview'] as EditMode[]).map((mode) => (
<button
key={mode}
onClick={() => setEditMode(mode)}
className={`
px-3 py-1.5 text-sm transition-colors flex items-center space-x-1
${editMode === mode
? '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="ModeButton"
data-element="mode-button"
data-mode={mode}
>
{mode === 'design' && <Settings className="w-4 h-4" />}
{mode === 'code' && <Code className="w-4 h-4" />}
{mode === 'preview' && <Eye className="w-4 h-4" />}
<span className="capitalize">{mode}</span>
</button>
))}
</div>
</div>
{/* 右侧:操作按钮 */}
<div className="flex items-center space-x-3" data-element="right-controls">
{/* 设备切换 */}
{editMode === 'preview' && (
<div className="flex border border-neutral-300 dark:border-neutral-600 rounded-md overflow-hidden" data-element="device-toggle">
{[
{ type: 'desktop', icon: Monitor, label: 'Desktop' },
{ type: 'tablet', icon: Tablet, label: 'Tablet' },
{ type: 'mobile', icon: Smartphone, label: 'Mobile' }
].map(({ type, icon: Icon, label }) => (
<button
key={type}
onClick={() => setDeviceType(type as DeviceType)}
className={`
px-3 py-1.5 text-sm transition-colors flex items-center space-x-1
${deviceType === type
? '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="DeviceButton"
data-element="device-button"
data-device={type}
title={label}
>
<Icon className="w-4 h-4" />
</button>
))}
</div>
)}
{/* 播放按钮 */}
{selectedComponent.props.hasOwnProperty('loading') && (
<IconButton
variant={isPlaying ? 'primary' : 'ghost'}
onClick={togglePlayState}
data-component="IconButton"
data-element="play-button"
data-action="toggle-play-state"
>
{isPlaying ? <Square className="w-4 h-4" /> : <Play className="w-4 h-4" />}
</IconButton>
)}
{/* 生成代码 */}
<Button
variant="outline"
onClick={generateCode}
data-component="Button"
data-element="generate-code-button"
data-action="generate-code"
>
<Code className="w-4 h-4 mr-2" />
Generate Code
</Button>
{/* 保存 */}
<IconButton
variant="ghost"
data-component="IconButton"
data-element="save-button"
data-action="save-component"
>
<Save className="w-4 h-4" />
</IconButton>
</div>
</div>
</motion.div>
{/* 编辑器主要内容 */}
<div className="flex-1 flex overflow-hidden">
{/* 组件库面板 */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="component-library w-64 border-r border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-800 overflow-y-auto"
data-element="library-panel"
>
<div className="p-4" data-element="library-content">
<h3 className="text-sm font-semibold text-neutral-700 dark:text-neutral-300 mb-4">
Component Library
</h3>
<div className="space-y-2">
{COMPONENT_LIBRARY.map((component) => (
<button
key={component.id}
onClick={() => setSelectedComponent(component)}
className={`
w-full text-left p-3 rounded-lg border transition-colors
${selectedComponent.id === component.id
? 'bg-primary-50 dark:bg-primary-900/20 border-primary-200 dark:border-primary-800'
: 'bg-white dark:bg-neutral-900 border-neutral-200 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-700'
}
`}
data-component="LibraryItem"
data-element="library-item"
data-component-id={component.id}
>
<div className="flex items-center space-x-3">
<span className="text-lg">{component.icon}</span>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm text-neutral-900 dark:text-neutral-100 truncate">
{component.name}
</div>
<div className="text-xs text-neutral-500 truncate">
{component.category}
</div>
</div>
</div>
</button>
))}
</div>
</div>
</motion.div>
{/* 主编辑区域 */}
<div className="flex-1 flex flex-col overflow-hidden">
<AnimatePresence mode="wait">
{editMode === 'design' && (
<motion.div
key="design"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="flex-1 flex"
data-element="design-mode"
>
{/* 设计画布 */}
<div className="flex-1 bg-neutral-100 dark:bg-neutral-900 p-8 overflow-auto">
<div className="max-w-4xl mx-auto">
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-lg p-8 min-h-96">
<ComponentPreview
component={selectedComponent}
deviceType={deviceType}
isPlaying={isPlaying}
/>
</div>
</div>
</div>
{/* 属性面板 */}
<div className="w-80 border-l border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 overflow-y-auto">
<div className="p-4">
<PropertyPanel
component={selectedComponent}
onUpdate={handleComponentUpdate}
/>
</div>
</div>
</motion.div>
)}
{editMode === 'code' && (
<motion.div
key="code"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="flex-1 flex flex-col"
data-element="code-mode"
>
{/* 代码编辑器 */}
<div className="flex-1 p-6 bg-neutral-900 text-green-400 font-mono text-sm overflow-auto">
<div className="flex items-center justify-between mb-4">
<h3 className="text-white font-semibold">Generated Code</h3>
<Button
variant="ghost"
size="sm"
onClick={copyCode}
data-component="Button"
data-element="copy-code-button"
data-action="copy-code"
>
Copy
</Button>
</div>
<pre className="bg-neutral-800 p-4 rounded-lg overflow-x-auto">
<code data-element="code-content">
{code || '// Click "Generate Code" to create component code'}
</code>
</pre>
</div>
{/* 代码预览 */}
<div className="h-64 border-t border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6">
<ComponentPreview
component={selectedComponent}
deviceType={deviceType}
isPlaying={isPlaying}
/>
</div>
</motion.div>
)}
{editMode === 'preview' && (
<motion.div
key="preview"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="flex-1 bg-neutral-100 dark:bg-neutral-900 overflow-auto"
data-element="preview-mode"
>
<div className="min-h-full p-8">
<div className="max-w-6xl mx-auto">
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-lg overflow-hidden">
<div className="p-8">
<ComponentPreview
component={selectedComponent}
deviceType={deviceType}
isPlaying={isPlaying}
/>
</div>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
);
};
export default ComponentEditor;

View File

@@ -0,0 +1,527 @@
/**
* @fileoverview 仪表盘页面组件
* @description 设计系统的概览仪表盘,展示系统状态、统计信息和快速入口
*/
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import {
Palette,
Type,
Layout,
Zap,
ArrowRight,
TrendingUp,
Users,
Activity,
CheckCircle,
AlertCircle,
Clock
} from 'lucide-react';
import { Card, CardBody, CardHeader, CardGroup } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { LoadingSpinner } from '../components/ui/LoadingSpinner';
import { useDesignTokens, useColors, useTypography, useSpacing, useEvents } from '../store/design-system-store';
// 简化统计卡片组件
interface StatsCardProps {
title: string;
value: string | number;
icon: React.ReactNode;
trend?: {
value: number;
label: string;
};
color: 'primary' | 'secondary' | 'success' | 'warning' | 'error';
}
// 统计卡片组件
const StatsCard: React.FC<StatsCardProps> = ({ title, value, icon, trend, color }) => {
const colorClasses = {
primary: 'from-primary-500 to-primary-600',
secondary: 'from-secondary-500 to-secondary-600',
success: 'from-success-500 to-success-600',
warning: 'from-warning-500 to-warning-600',
error: 'from-error-500 to-error-600',
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
whileHover={{ y: -2 }}
className="stats-card"
data-component="StatsCard"
data-element="card"
data-color={color}
>
<Card interactive className="h-full">
<CardBody className="p-6">
<div className="flex items-center justify-between mb-4">
<div className={`
w-12 h-12 rounded-lg bg-gradient-to-br ${colorClasses[color]}
flex items-center justify-center text-white shadow-lg
`} data-element="icon">
{icon}
</div>
{trend && (
<div className="flex items-center text-sm" data-element="trend">
<TrendingUp className="w-4 h-4 text-success-500 mr-1" />
<span className="text-success-600 dark:text-success-400">
+{trend.value}%
</span>
</div>
)}
</div>
<div className="space-y-2" data-element="content">
<h3 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100" data-element="value">
{value}
</h3>
<p className="text-neutral-600 dark:text-neutral-400 text-sm" data-element="label">
{title}
</p>
{trend && (
<p className="text-xs text-neutral-500" data-element="trend-label">
{trend.label}
</p>
)}
</div>
</CardBody>
</Card>
</motion.div>
);
};
// 活动项组件
interface ActivityItemProps {
type: 'token' | 'component' | 'library' | 'theme';
title: string;
description: string;
timestamp: string;
status: 'success' | 'pending' | 'error';
}
// 活动项组件
const ActivityItem: React.FC<ActivityItemProps> = ({ type, title, description, timestamp, status }) => {
const getTypeIcon = () => {
switch (type) {
case 'token':
return <Palette className="w-4 h-4" />;
case 'component':
return <Layout className="w-4 h-4" />;
case 'library':
return <Type className="w-4 h-4" />;
case 'theme':
return <Zap className="w-4 h-4" />;
default:
return <Activity className="w-4 h-4" />;
}
};
const getStatusColor = () => {
switch (status) {
case 'success':
return 'text-success-500';
case 'pending':
return 'text-warning-500';
case 'error':
return 'text-error-500';
default:
return 'text-neutral-500';
}
};
const getStatusIcon = () => {
switch (status) {
case 'success':
return <CheckCircle className="w-4 h-4" />;
case 'pending':
return <Clock className="w-4 h-4" />;
case 'error':
return <AlertCircle className="w-4 h-4" />;
default:
return null;
}
};
return (
<div className="activity-item flex items-start space-x-3 p-3 hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg transition-colors" data-component="ActivityItem" data-element="item" data-type={type} data-status={status}>
<div className={`
w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0
${status === 'success' ? 'bg-success-100 dark:bg-success-900/20' :
status === 'pending' ? 'bg-warning-100 dark:bg-warning-900/20' :
'bg-error-100 dark:bg-error-900/20'
}
`} data-element="icon-container">
{getTypeIcon()}
</div>
<div className="flex-1 min-w-0" data-element="content">
<div className="flex items-center justify-between mb-1" data-element="header">
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100 truncate" data-element="title">
{title}
</p>
<div className={`flex items-center ${getStatusColor()}`} data-element="status">
{getStatusIcon()}
</div>
</div>
<p className="text-xs text-neutral-600 dark:text-neutral-400 mb-2" data-element="description">
{description}
</p>
<p className="text-xs text-neutral-500" data-element="timestamp">
{timestamp}
</p>
</div>
</div>
);
};
/**
* 仪表盘主组件
*/
const Dashboard: React.FC = () => {
const [isLoading, setIsLoading] = useState(true);
// 获取设计系统数据
const colors = useColors();
const typography = useTypography();
const spacing = useSpacing();
const events = useEvents();
// 模拟数据加载
useEffect(() => {
const timer = setTimeout(() => {
setIsLoading(false);
}, 1000);
return () => clearTimeout(timer);
}, []);
// 统计数据
const stats = {
totalTokens: colors.length + typography.length + spacing.length,
totalColors: colors.length,
totalTypography: typography.length,
totalSpacing: spacing.length,
totalComponents: 24, // 模拟数据
totalLibraries: 3, // 模拟数据
designScore: 94, // 模拟数据
};
// 最近活动(基于真实事件)
const recentActivities = events.slice(0, 5).map((event, index) => ({
type: event.type.includes('token') ? 'token' : event.type.includes('component') ? 'component' : 'theme',
title: `System ${event.type}`,
description: `System ${event.type} was updated`,
timestamp: new Date(event.timestamp).toLocaleTimeString(),
status: 'success' as const
}));
// 如果加载中,显示加载状态
if (isLoading) {
return (
<div className="dashboard-loading" data-component="DashboardLoading" data-element="loading">
<div className="flex items-center justify-center h-64">
<LoadingSpinner size="lg" />
<span className="ml-3 text-neutral-600 dark:text-neutral-400">Loading dashboard...</span>
</div>
</div>
);
}
return (
<div className="dashboard space-y-8" data-component="Dashboard" data-element="dashboard">
{/* 欢迎区域 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="welcome-section" data-element="welcome-section"
>
<div className="bg-gradient-to-r from-primary-500 to-secondary-500 rounded-xl p-8 text-white" data-element="hero">
<div className="max-w-3xl" data-element="content">
<h1 className="text-3xl font-bold mb-4" data-element="title">
Welcome to Design System Studio
</h1>
<p className="text-primary-100 mb-6 text-lg" data-element="subtitle">
Manage your design tokens, components, and build consistent user interfaces.
</p>
<div className="flex flex-wrap gap-3" data-element="actions">
<Button
variant="secondary"
className="bg-white/20 border-white/30 text-white hover:bg-white/30"
data-component="Button"
data-element="get-started-button"
data-action="navigate-editor"
>
Get Started
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
<Button
variant="ghost"
className="text-white border-white/30 hover:bg-white/10"
data-component="Button"
data-element="learn-more-button"
data-action="navigate-docs"
>
Learn More
</Button>
</div>
</div>
</div>
</motion.div>
{/* 统计卡片 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="stats-section" data-element="stats-section"
>
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100 mb-6" data-element="section-title">
System Overview
</h2>
<CardGroup columns={4} gap="lg" data-element="stats-grid">
<StatsCard
title="Total Tokens"
value={stats.totalTokens}
icon={<span className="text-2xl">🎨</span>}
trend={{ value: 12, label: 'from last month' }}
color="primary"
/>
<StatsCard
title="Design Score"
value={`${stats.designScore}%`}
icon={<span className="text-2xl"></span>}
trend={{ value: 5, label: 'improvement' }}
color="success"
/>
<StatsCard
title="Components"
value={stats.totalComponents}
icon={<span className="text-2xl">🧩</span>}
trend={{ value: 8, label: 'new this month' }}
color="secondary"
/>
<StatsCard
title="Libraries"
value={stats.totalLibraries}
icon={<span className="text-2xl">📚</span>}
color="warning"
/>
</CardGroup>
</motion.div>
{/* 内容区域 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8" data-element="content-grid">
{/* 左侧列 */}
<div className="lg:col-span-2 space-y-8" data-element="left-column">
{/* 快速访问 */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
className="quick-access" data-element="quick-access"
>
<Card>
<CardHeader title="Quick Access" />
<CardBody>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4" data-element="access-grid">
<Button
variant="outline"
className="justify-start h-auto p-4"
data-component="Button"
data-element="design-system-button"
data-action="navigate-design-system"
>
<div className="flex items-center space-x-3" data-element="content">
<Palette className="w-5 h-5 text-primary-500" />
<div className="text-left">
<div className="font-medium">Design System</div>
<div className="text-sm text-neutral-500">Manage tokens & themes</div>
</div>
</div>
</Button>
<Button
variant="outline"
className="justify-start h-auto p-4"
data-component="Button"
data-element="component-editor-button"
data-action="navigate-editor"
>
<div className="flex items-center space-x-3" data-element="content">
<Layout className="w-5 h-5 text-secondary-500" />
<div className="text-left">
<div className="font-medium">Component Editor</div>
<div className="text-sm text-neutral-500">Build & customize</div>
</div>
</div>
</Button>
<Button
variant="outline"
className="justify-start h-auto p-4"
data-component="Button"
data-element="live-preview-button"
data-action="navigate-preview"
>
<div className="flex items-center space-x-3" data-element="content">
<Zap className="w-5 h-5 text-success-500" />
<div className="text-left">
<div className="font-medium">Live Preview</div>
<div className="text-sm text-neutral-500">Real-time testing</div>
</div>
</div>
</Button>
<Button
variant="outline"
className="justify-start h-auto p-4"
data-component="Button"
data-element="libraries-button"
data-action="navigate-libraries"
>
<div className="flex items-center space-x-3" data-element="content">
<Users className="w-5 h-5 text-warning-500" />
<div className="text-left">
<div className="font-medium">Libraries</div>
<div className="text-sm text-neutral-500">Component collections</div>
</div>
</div>
</Button>
</div>
</CardBody>
</Card>
</motion.div>
{/* 令牌统计 */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 }}
className="token-stats" data-element="token-stats"
>
<Card>
<CardHeader title="Token Distribution" />
<CardBody>
<div className="space-y-4" data-element="token-grid">
<div className="flex items-center justify-between p-3 bg-neutral-50 dark:bg-neutral-800 rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-3 h-3 bg-primary-500 rounded-full"></div>
<span className="text-sm font-medium">Colors</span>
</div>
<span className="text-sm text-neutral-600 dark:text-neutral-400">{stats.totalColors} tokens</span>
</div>
<div className="flex items-center justify-between p-3 bg-neutral-50 dark:bg-neutral-800 rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-3 h-3 bg-secondary-500 rounded-full"></div>
<span className="text-sm font-medium">Typography</span>
</div>
<span className="text-sm text-neutral-600 dark:text-neutral-400">{stats.totalTypography} tokens</span>
</div>
<div className="flex items-center justify-between p-3 bg-neutral-50 dark:bg-neutral-800 rounded-lg">
<div className="flex items-center space-x-3">
<div className="w-3 h-3 bg-success-500 rounded-full"></div>
<span className="text-sm font-medium">Spacing</span>
</div>
<span className="text-sm text-neutral-600 dark:text-neutral-400">{stats.totalSpacing} tokens</span>
</div>
</div>
</CardBody>
</Card>
</motion.div>
</div>
{/* 右侧列 */}
<div className="space-y-8" data-element="right-column">
{/* 最近活动 */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 }}
className="recent-activity" data-element="recent-activity"
>
<Card>
<CardHeader title="Recent Activity" />
<CardBody>
<div className="space-y-3" data-element="activity-list">
{recentActivities.length > 0 ? (
recentActivities.map((activity, index) => (
<ActivityItem key={index} {...activity} />
))
) : (
<div className="text-center py-8 text-neutral-500 dark:text-neutral-400">
<Activity className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No recent activity</p>
</div>
)}
</div>
{events.length > 5 && (
<div className="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-800">
<Button
variant="ghost"
size="sm"
className="w-full"
data-component="Button"
data-element="view-all-activity-button"
data-action="navigate-activity"
>
View All Activity
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</div>
)}
</CardBody>
</Card>
</motion.div>
{/* 系统状态 */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
className="system-status" data-element="system-status"
>
<Card>
<CardHeader title="System Status" />
<CardBody>
<div className="space-y-4" data-element="status-items">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<CheckCircle className="w-5 h-5 text-success-500" />
<span className="text-sm font-medium">Design Mode</span>
</div>
<span className="text-sm text-success-600 dark:text-success-400">Active</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<CheckCircle className="w-5 h-5 text-success-500" />
<span className="text-sm font-medium">Token Sync</span>
</div>
<span className="text-sm text-success-600 dark:text-success-400">Synced</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<CheckCircle className="w-5 h-5 text-success-500" />
<span className="text-sm font-medium">Component Library</span>
</div>
<span className="text-sm text-success-600 dark:text-success-400">Updated</span>
</div>
</div>
</CardBody>
</Card>
</motion.div>
</div>
</div>
</div>
);
};
export default Dashboard;

View File

@@ -0,0 +1,496 @@
/**
* @fileoverview 设计系统管理页面
* @description 展示和管理设计系统的所有令牌,包括颜色、字体、间距等
*/
import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Palette,
Type,
Ruler,
Shadow,
Circle,
Zap,
Search,
Filter,
Grid,
List,
Download,
Upload,
Settings,
Plus,
Eye,
EyeOff
} from 'lucide-react';
import { Card, CardBody, CardHeader } from '../components/ui/Card';
import { Button, IconButton } from '../components/ui/Button';
import { TokenCard } from '../components/design-system/TokenCard';
import { ColorPalette } from '../components/design-system/ColorPalette';
import {
useColors,
useTypography,
useSpacing,
useShadows,
useBorderRadius,
useAnimations,
useSelectedToken,
useDesignSystemStore
} from '../store/design-system-store';
import { clsx } from 'clsx';
// 令牌类型定义
type TokenType = 'all' | 'color' | 'typography' | 'spacing' | 'shadow' | 'border-radius' | 'animation';
// 令牌类别配置
const TOKEN_CATEGORIES = {
all: { label: 'All Tokens', icon: Settings, color: 'neutral' },
color: { label: 'Colors', icon: Palette, color: 'primary' },
typography: { label: 'Typography', icon: Type, color: 'secondary' },
spacing: { label: 'Spacing', icon: Ruler, color: 'success' },
shadow: { label: 'Shadows', icon: Shadow, color: 'warning' },
'border-radius': { label: 'Border Radius', icon: Circle, color: 'error' },
animation: { label: 'Animation', icon: Zap, color: 'info' },
} as const;
/**
* 设计系统主页面组件
*/
const DesignSystem: React.FC = () => {
const [activeCategory, setActiveCategory] = useState<TokenType>('all');
const [searchQuery, setSearchQuery] = useState('');
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [showDeprecated, setShowDeprecated] = useState(true);
const [selectedToken, setSelectedToken] = useState(null);
// 获取所有令牌数据
const colors = useColors();
const typography = useTypography();
const spacing = useSpacing();
const shadows = useShadows();
const borderRadius = useBorderRadius();
const animations = useAnimations();
// 合并所有令牌
const allTokens = useMemo(() => {
return [
...colors,
...typography,
...spacing,
...shadows,
...borderRadius,
...animations,
];
}, [colors, typography, spacing, shadows, borderRadius, animations]);
// 过滤令牌
const filteredTokens = useMemo(() => {
return allTokens.filter(token => {
// 类别过滤
const matchesCategory = activeCategory === 'all' || token.category === activeCategory;
// 搜索过滤
const matchesSearch = searchQuery === '' ||
token.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
token.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
token.value.toString().toLowerCase().includes(searchQuery.toLowerCase());
// 废弃状态过滤
const matchesDeprecated = showDeprecated || !token.deprecated;
return matchesCategory && matchesSearch && matchesDeprecated;
});
}, [allTokens, activeCategory, searchQuery, showDeprecated]);
// 渲染特定类别的内容
const renderCategoryContent = () => {
switch (activeCategory) {
case 'color':
return (
<ColorPalette
variant="grid"
showControls={false}
interactive={true}
/>
);
case 'typography':
return (
<div className="token-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{typography.map((token, index) => (
<motion.div
key={token.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<TokenCard token={token} interactive={true} />
</motion.div>
))}
</div>
);
case 'spacing':
return (
<div className="token-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{spacing.map((token, index) => (
<motion.div
key={token.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<TokenCard token={token} variant="compact" interactive={true} />
</motion.div>
))}
</div>
);
case 'shadow':
return (
<div className="token-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{shadows.map((token, index) => (
<motion.div
key={token.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<TokenCard token={token} interactive={true} />
</motion.div>
))}
</div>
);
case 'border-radius':
return (
<div className="token-grid grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{borderRadius.map((token, index) => (
<motion.div
key={token.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<TokenCard token={token} variant="compact" interactive={true} />
</motion.div>
))}
</div>
);
case 'animation':
return (
<div className="token-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{animations.map((token, index) => (
<motion.div
key={token.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<TokenCard token={token} interactive={true} />
</motion.div>
))}
</div>
);
default:
return (
<div className="token-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<AnimatePresence>
{filteredTokens.map((token, index) => (
<motion.div
key={token.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ delay: index * 0.02 }}
>
<TokenCard token={token} interactive={true} />
</motion.div>
))}
</AnimatePresence>
</div>
);
}
};
// 获取类别统计
const getCategoryStats = (type: TokenType) => {
switch (type) {
case 'color': return colors.length;
case 'typography': return typography.length;
case 'spacing': return spacing.length;
case 'shadow': return shadows.length;
case 'border-radius': return borderRadius.length;
case 'animation': return animations.length;
default: return allTokens.length;
}
};
return (
<div className="design-system-page" data-component="DesignSystemPage">
{/* 页面头部 */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="page-header mb-8" data-element="header"
>
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
<div className="space-y-2" data-element="title-section">
<h1 className="text-3xl font-bold text-neutral-900 dark:text-neutral-100">
Design System
</h1>
<p className="text-neutral-600 dark:text-neutral-400">
Manage your design tokens, typography, colors, and component guidelines
</p>
</div>
<div className="flex items-center space-x-3" data-element="actions">
<Button
variant="outline"
icon={<Upload className="w-4 h-4" />}
data-component="Button"
data-element="import-button"
data-action="import-tokens"
>
Import
</Button>
<Button
variant="outline"
icon={<Download className="w-4 h-4" />}
data-component="Button"
data-element="export-button"
data-action="export-tokens"
>
Export
</Button>
<Button
icon={<Plus className="w-4 h-4" />}
data-component="Button"
data-element="add-token-button"
data-action="add-token"
>
Add Token
</Button>
</div>
</div>
</motion.div>
{/* 控制面板 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="control-panel mb-8" data-element="controls"
>
<Card>
<CardBody className="p-4">
<div className="flex flex-col lg:flex-row lg:items-center space-y-4 lg:space-y-0 lg:space-x-6">
{/* 搜索框 */}
<div className="flex-1 max-w-md" data-element="search">
<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 tokens..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 pr-4 py-2 w-full 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-tokens"
/>
</div>
</div>
{/* 视图模式切换 */}
<div className="flex items-center space-x-2" data-element="view-mode">
<span className="text-sm text-neutral-600 dark:text-neutral-400">View:</span>
<div className="flex border border-neutral-300 dark:border-neutral-600 rounded-md overflow-hidden">
<button
onClick={() => setViewMode('grid')}
className={clsx(
'px-3 py-1.5 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-1.5 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 className="flex items-center space-x-2" data-element="deprecated-filter">
<IconButton
variant={showDeprecated ? 'primary' : 'ghost'}
onClick={() => setShowDeprecated(!showDeprecated)}
size="sm"
data-component="IconButton"
data-element="deprecated-toggle"
data-action="toggle-deprecated"
>
{showDeprecated ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
</IconButton>
<span className="text-sm text-neutral-600 dark:text-neutral-400">
Show deprecated
</span>
</div>
</div>
</CardBody>
</Card>
</motion.div>
{/* 类别选择器 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="category-tabs mb-8" data-element="categories"
>
<div className="flex flex-wrap gap-2" data-element="tab-list">
{Object.entries(TOKEN_CATEGORIES).map(([key, config]) => {
const Icon = config.icon;
const count = getCategoryStats(key as TokenType);
const isActive = activeCategory === key;
return (
<button
key={key}
onClick={() => setActiveCategory(key as TokenType)}
className={clsx(
'flex items-center space-x-2 px-4 py-2 rounded-lg border transition-all duration-200',
isActive
? 'bg-primary-500 text-white border-primary-500 shadow-md'
: '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="CategoryTab"
data-element="category-tab"
data-category={key}
data-active={isActive ? 'true' : 'false'}
>
<Icon className="w-4 h-4" />
<span className="font-medium">{config.label}</span>
<span className={clsx(
'px-2 py-0.5 text-xs rounded-full',
isActive
? 'bg-white/20 text-white'
: 'bg-neutral-100 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-400'
)}>
{count}
</span>
</button>
);
})}
</div>
</motion.div>
{/* 主要内容区域 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="main-content" data-element="content"
>
{/* 空状态 */}
{filteredTokens.length === 0 && activeCategory !== 'color' ? (
<Card>
<CardBody className="p-12 text-center">
<div className="w-16 h-16 bg-neutral-100 dark:bg-neutral-800 rounded-full flex items-center justify-center mx-auto mb-4">
<Search className="w-8 h-8 text-neutral-400" />
</div>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2">
No tokens found
</h3>
<p className="text-neutral-600 dark:text-neutral-400 mb-6">
Try adjusting your search criteria or browse different categories.
</p>
<Button
variant="outline"
onClick={() => {
setSearchQuery('');
setActiveCategory('all');
}}
data-component="Button"
data-element="clear-filters-button"
data-action="clear-filters"
>
Clear Filters
</Button>
</CardBody>
</Card>
) : (
<AnimatePresence mode="wait">
<motion.div
key={activeCategory}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
className="category-content" data-element="category-content"
>
{renderCategoryContent()}
</motion.div>
</AnimatePresence>
)}
</motion.div>
{/* 统计信息 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="stats-footer mt-12 pt-8 border-t border-neutral-200 dark:border-neutral-800" data-element="stats"
>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 text-center" data-element="stats-grid">
{Object.entries(TOKEN_CATEGORIES).slice(1).map(([key, config]) => {
const Icon = config.icon;
const count = getCategoryStats(key as TokenType);
return (
<div
key={key}
className="stat-item p-4 bg-neutral-50 dark:bg-neutral-800 rounded-lg"
data-component="StatItem"
data-element="stat-item"
data-category={key}
>
<Icon className={clsx(
'w-6 h-6 mx-auto mb-2',
`text-${config.color}-500`
)} />
<div className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
{count}
</div>
<div className="text-sm text-neutral-600 dark:text-neutral-400">
{config.label}
</div>
</div>
);
})}
</div>
</motion.div>
</div>
);
};
export default DesignSystem;

View File

@@ -0,0 +1,566 @@
/**
* @fileoverview 实时预览页面
* @description 展示组件的实时预览效果,支持不同设备和主题切换
*/
import React, { useState, useCallback, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
RefreshCw,
Maximize,
Smartphone,
Tablet,
Monitor,
Moon,
Sun,
Settings,
Eye,
EyeOff,
Play,
Pause,
Download,
Share2
} from 'lucide-react';
import { Card, CardBody, CardHeader } from '../components/ui/Card';
import { Button, IconButton } from '../components/ui/Button';
import { LoadingSpinner } from '../components/ui/LoadingSpinner';
import { useDesignModeStore } from '../main';
// 设备类型
type DeviceType = 'desktop' | 'tablet' | 'mobile';
// 主题类型
type ThemeType = 'light' | 'dark' | 'system';
// 预览模式
type PreviewMode = 'component' | 'page' | 'interaction';
// 示例组件数据
const PREVIEW_COMPONENTS = [
{
id: 'hero-section',
name: 'Hero Section',
description: 'Landing page hero with call-to-action',
category: 'Layout',
component: (
<div className="bg-gradient-to-r from-primary-600 to-secondary-600 text-white py-20 px-8 text-center">
<h1 className="text-5xl font-bold mb-6">Design System Studio</h1>
<p className="text-xl mb-8 max-w-2xl mx-auto">
Build consistent and beautiful user interfaces with our comprehensive design system toolkit.
</p>
<div className="flex justify-center space-x-4">
<button className="bg-white text-primary-600 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-colors">
Get Started
</button>
<button className="border-2 border-white text-white px-8 py-3 rounded-lg font-semibold hover:bg-white hover:text-primary-600 transition-colors">
Learn More
</button>
</div>
</div>
)
},
{
id: 'feature-grid',
name: 'Feature Grid',
description: 'Grid of features with icons and descriptions',
category: 'Content',
component: (
<div className="py-16 px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Powerful Features</h2>
<p className="text-gray-600 max-w-2xl mx-auto">
Everything you need to build and maintain a consistent design system.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-6xl mx-auto">
{[
{ icon: '🎨', title: 'Design Tokens', desc: 'Centralized design decisions' },
{ icon: '🔧', title: 'Component Library', desc: 'Reusable UI components' },
{ icon: '📱', title: 'Responsive Design', desc: 'Mobile-first approach' },
{ icon: '⚡', title: 'Fast Performance', desc: 'Optimized for speed' },
{ icon: '🎯', title: 'Accessibility', desc: 'WCAG compliant design' },
{ icon: '🔄', title: 'Real-time Sync', desc: 'Instant updates across team' }
].map((feature, index) => (
<div key={index} className="text-center p-6 bg-gray-50 rounded-lg">
<div className="text-4xl mb-4">{feature.icon}</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">{feature.title}</h3>
<p className="text-gray-600">{feature.desc}</p>
</div>
))}
</div>
</div>
)
},
{
id: 'form-example',
name: 'Contact Form',
description: 'Complete contact form with validation',
category: 'Form',
component: (
<div className="max-w-2xl mx-auto py-16 px-8">
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4">Contact Us</h2>
<p className="text-gray-600">We'd love to hear from you. Send us a message!</p>
</div>
<form className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">First Name</label>
<input
type="text"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="John"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Last Name</label>
<input
type="text"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Doe"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Email</label>
<input
type="email"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="john@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Message</label>
<textarea
rows={4}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Tell us more about your project..."
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
/>
<label className="ml-2 text-sm text-gray-600">
I agree to the <a href="#" className="text-primary-600 hover:underline">Terms of Service</a> and <a href="#" className="text-primary-600 hover:underline">Privacy Policy</a>
</label>
</div>
<button
type="submit"
className="w-full bg-primary-600 text-white py-3 px-6 rounded-lg font-semibold hover:bg-primary-700 transition-colors"
>
Send Message
</button>
</form>
</div>
)
}
];
// 设备预览组件
const DevicePreview: React.FC<{
component: React.ReactNode;
deviceType: DeviceType;
theme: ThemeType;
isLoading?: boolean;
}> = ({ component, deviceType, theme, isLoading = false }) => {
const getDeviceStyles = () => {
switch (deviceType) {
case 'mobile':
return {
width: '375px',
height: '667px',
maxWidth: '100%',
borderRadius: '25px',
border: '1px solid #e5e5e5'
};
case 'tablet':
return {
width: '768px',
height: '1024px',
maxWidth: '100%',
borderRadius: '15px',
border: '1px solid #e5e5e5'
};
default:
return {
width: '100%',
height: '600px',
borderRadius: '8px',
border: '1px solid #e5e5e5'
};
}
};
const deviceStyles = getDeviceStyles();
return (
<div
className="device-preview flex justify-center items-center bg-gray-100 dark:bg-gray-900 p-8"
data-component="DevicePreview"
data-element="preview-container"
data-device={deviceType}
data-theme={theme}
>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
className="device-frame bg-white dark:bg-gray-800 overflow-hidden shadow-2xl"
style={deviceStyles}
data-element="device-frame"
>
{isLoading ? (
<div className="flex items-center justify-center h-full">
<LoadingSpinner size="lg" />
</div>
) : (
<div
className="preview-content h-full overflow-auto"
data-element="preview-content"
>
{component}
</div>
)}
</motion.div>
</div>
);
};
/**
* 实时预览主页面
*/
const LivePreview: React.FC = () => {
const [selectedComponent, setSelectedComponent] = useState(PREVIEW_COMPONENTS[0]);
const [deviceType, setDeviceType] = useState<DeviceType>('desktop');
const [theme, setTheme] = useState<ThemeType>('light');
const [previewMode, setPreviewMode] = useState<PreviewMode>('component');
const [isFullscreen, setIsFullscreen] = useState(false);
const [isAutoRefresh, setIsAutoRefresh] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [lastUpdate, setLastUpdate] = useState(new Date());
const { isDesignModeEnabled } = useDesignModeStore();
const previewRef = useRef<HTMLDivElement>(null);
// 刷新预览
const refreshPreview = useCallback(async () => {
setIsLoading(true);
// 模拟加载延迟
await new Promise(resolve => setTimeout(resolve, 500));
setIsLoading(false);
setLastUpdate(new Date());
}, []);
// 切换全屏
const toggleFullscreen = useCallback(() => {
if (!document.fullscreenElement) {
previewRef.current?.requestFullscreen();
setIsFullscreen(true);
} else {
document.exitFullscreen();
setIsFullscreen(false);
}
}, []);
// 应用主题
const applyTheme = useCallback((newTheme: ThemeType) => {
setTheme(newTheme);
const root = document.documentElement;
switch (newTheme) {
case 'dark':
root.classList.add('dark');
break;
case 'light':
root.classList.remove('dark');
break;
case 'system':
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
root.classList.toggle('dark', prefersDark);
break;
}
}, []);
// 初始化主题
React.useEffect(() => {
applyTheme(theme);
}, [theme, applyTheme]);
// 设备切换时刷新
React.useEffect(() => {
if (isAutoRefresh) {
refreshPreview();
}
}, [deviceType, selectedComponent, isAutoRefresh, refreshPreview]);
return (
<div
ref={previewRef}
className="live-preview h-full flex flex-col"
data-component="LivePreview"
data-design-mode={isDesignModeEnabled ? 'true' : 'false'}
data-preview-mode={previewMode}
data-device={deviceType}
data-theme={theme}
>
{/* 预览控制栏 */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="preview-toolbar border-b border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4"
data-element="toolbar"
>
<div className="flex items-center justify-between">
{/* 左侧:组件选择和模式 */}
<div className="flex items-center space-x-4" data-element="left-controls">
{/* 组件选择 */}
<div className="flex items-center space-x-2">
<label className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Preview:
</label>
<select
value={selectedComponent.id}
onChange={(e) => {
const component = PREVIEW_COMPONENTS.find(c => c.id === e.target.value);
if (component) setSelectedComponent(component);
}}
className="px-3 py-1.5 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="Select"
data-element="component-select"
>
{PREVIEW_COMPONENTS.map((component) => (
<option key={component.id} value={component.id}>
{component.name}
</option>
))}
</select>
</div>
{/* 预览模式切换 */}
<div className="flex border border-neutral-300 dark:border-neutral-600 rounded-md overflow-hidden">
{[
{ mode: 'component' as PreviewMode, icon: Settings, label: 'Component' },
{ mode: 'page' as PreviewMode, icon: Monitor, label: 'Page' },
{ mode: 'interaction' as PreviewMode, icon: Play, label: 'Interaction' }
].map(({ mode, icon: Icon, label }) => (
<button
key={mode}
onClick={() => setPreviewMode(mode)}
className={`
px-3 py-1.5 text-sm transition-colors flex items-center space-x-1
${previewMode === mode
? '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="ModeButton"
data-element="mode-button"
data-mode={mode}
>
<Icon className="w-4 h-4" />
<span>{label}</span>
</button>
))}
</div>
</div>
{/* 右侧:控制按钮 */}
<div className="flex items-center space-x-3" data-element="right-controls">
{/* 设备切换 */}
<div className="flex border border-neutral-300 dark:border-neutral-600 rounded-md overflow-hidden" data-element="device-toggle">
{[
{ type: 'mobile' as DeviceType, icon: Smartphone, label: 'Mobile' },
{ type: 'tablet' as DeviceType, icon: Tablet, label: 'Tablet' },
{ type: 'desktop' as DeviceType, icon: Monitor, label: 'Desktop' }
].map(({ type, icon: Icon, label }) => (
<button
key={type}
onClick={() => setDeviceType(type)}
className={`
px-3 py-1.5 text-sm transition-colors flex items-center space-x-1
${deviceType === type
? '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="DeviceButton"
data-element="device-button"
data-device={type}
title={label}
>
<Icon className="w-4 h-4" />
</button>
))}
</div>
{/* 主题切换 */}
<div className="flex border border-neutral-300 dark:border-neutral-600 rounded-md overflow-hidden">
{[
{ type: 'light' as ThemeType, icon: Sun, label: 'Light' },
{ type: 'dark' as ThemeType, icon: Moon, label: 'Dark' },
{ type: 'system' as ThemeType, icon: Settings, label: 'System' }
].map(({ type, icon: Icon, label }) => (
<button
key={type}
onClick={() => applyTheme(type)}
className={`
px-3 py-1.5 text-sm transition-colors flex items-center space-x-1
${theme === type
? '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="ThemeButton"
data-element="theme-button"
data-theme={type}
title={label}
>
<Icon className="w-4 h-4" />
</button>
))}
</div>
{/* 自动刷新切换 */}
<IconButton
variant={isAutoRefresh ? 'primary' : 'ghost'}
onClick={() => setIsAutoRefresh(!isAutoRefresh)}
title="Auto Refresh"
data-component="IconButton"
data-element="auto-refresh-toggle"
data-action="toggle-auto-refresh"
>
{isAutoRefresh ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
</IconButton>
{/* 刷新按钮 */}
<IconButton
variant="ghost"
onClick={refreshPreview}
disabled={isLoading}
title="Refresh Preview"
data-component="IconButton"
data-element="refresh-button"
data-action="refresh-preview"
>
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
</IconButton>
{/* 全屏按钮 */}
<IconButton
variant="ghost"
onClick={toggleFullscreen}
title="Fullscreen"
data-component="IconButton"
data-element="fullscreen-button"
data-action="toggle-fullscreen"
>
<Maximize className="w-4 h-4" />
</IconButton>
</div>
</div>
</motion.div>
{/* 预览信息栏 */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="preview-info bg-neutral-50 dark:bg-neutral-800 px-4 py-2 text-sm text-neutral-600 dark:text-neutral-400 border-b border-neutral-200 dark:border-neutral-700"
data-element="info-bar"
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4" data-element="preview-info-left">
<span data-element="component-name">
{selectedComponent.name} • {selectedComponent.category}
</span>
<span data-element="device-info">
{deviceType.charAt(0).toUpperCase() + deviceType.slice(1)} • {theme.charAt(0).toUpperCase() + theme.slice(1)} Mode
</span>
</div>
<div className="flex items-center space-x-4" data-element="preview-info-right">
<span data-element="last-update">
Last updated: {lastUpdate.toLocaleTimeString()}
</span>
{isAutoRefresh && (
<span className="text-success-600 dark:text-success-400" data-element="auto-refresh-status">
● Auto-refresh ON
</span>
)}
</div>
</div>
</motion.div>
{/* 主要预览区域 */}
<div className="flex-1 relative overflow-hidden">
<AnimatePresence mode="wait">
<motion.div
key={`${selectedComponent.id}-${deviceType}-${theme}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="preview-area h-full"
data-element="preview-area"
>
<DevicePreview
component={selectedComponent.component}
deviceType={deviceType}
theme={theme}
isLoading={isLoading}
/>
</motion.div>
</AnimatePresence>
{/* 覆盖层指示器 */}
<div className="absolute top-4 right-4 bg-black bg-opacity-50 text-white px-3 py-1 rounded text-sm">
{selectedComponent.description}
</div>
</div>
{/* 操作栏 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="preview-actions border-t border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4"
data-element="actions-bar"
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3" data-element="left-actions">
<Button
variant="outline"
size="sm"
icon={<Download className="w-4 h-4" />}
data-component="Button"
data-element="download-button"
data-action="download-preview"
>
Download
</Button>
<Button
variant="outline"
size="sm"
icon={<Share2 className="w-4 h-4" />}
data-component="Button"
data-element="share-button"
data-action="share-preview"
>
Share
</Button>
</div>
<div className="flex items-center space-x-3" data-element="right-actions">
<div className="text-sm text-neutral-600 dark:text-neutral-400">
Preview Mode: {previewMode.charAt(0).toUpperCase() + previewMode.slice(1)}
</div>
<div className="w-3 h-3 bg-success-500 rounded-full animate-pulse"></div>
</div>
</div>
</motion.div>
</div>
);
};
export default LivePreview;