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

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,14 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Advanced AppDev Design Mode Example</title>
<meta name="description" content="高级AppDev设计模式插件示例展示复杂的使用场景" />
</head>
<body>
<div id="root" data-app-root="main"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
{
"name": "vite-plugin-appdev-design-mode-advanced-example",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"clsx": "^2.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.8.0",
"tailwind-merge": "^3.4.0",
"zustand": "^4.3.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.17",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.22",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"typescript": "^5.0.0",
"vite": "^5.0.0"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,119 @@
import React, { Suspense } from 'react';
import { Routes, Route, Link, useLocation } from 'react-router-dom';
import { useAppStore } from './store/appStore';
// 页面组件
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const ComponentShowcase = React.lazy(() => import('./pages/ComponentShowcase'));
const InteractiveDemo = React.lazy(() => import('./pages/InteractiveDemo'));
const ConfigurationGuide = React.lazy(() => import('./pages/ConfigurationGuide'));
// 通用组件
const LoadingSpinner: React.FC = () => (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: '2rem'
}}>
<div style={{
width: '40px',
height: '40px',
border: '3px solid #f3f3f3',
borderTop: '3px solid #3498db',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
<style>{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}</style>
</div>
);
const Navigation: React.FC = () => {
const location = useLocation();
const { theme, toggleTheme } = useAppStore();
const navItems = [
{ path: '/', label: 'Dashboard', icon: '🏠' },
{ path: '/showcase', label: 'Component Showcase', icon: '🎨' },
{ path: '/interactive', label: 'Interactive Demo', icon: '🎯' },
{ path: '/config', label: 'Configuration', icon: '⚙️' }
];
return (
<nav className={`nav ${theme}`} data-component="navigation">
<div className="nav-brand" data-element="brand">
<h1>Advanced Design Mode</h1>
<span className="version">v1.0.0</span>
</div>
<div className="nav-links" data-element="nav-links">
{navItems.map(item => (
<Link
key={item.path}
to={item.path}
className={`nav-link ${location.pathname === item.path ? 'active' : ''}`}
data-nav-item={item.path.replace('/', '') || 'dashboard'}
>
<span className="icon">{item.icon}</span>
<span className="label">{item.label}</span>
</Link>
))}
</div>
<div className="nav-actions" data-element="actions">
<button
className="theme-toggle"
onClick={toggleTheme}
data-action="toggle-theme"
title="Toggle theme"
>
{theme === 'light' ? '🌙' : '☀️'}
</button>
</div>
</nav>
);
};
const App: React.FC = () => {
const { isLoading, error } = useAppStore();
if (error) {
return (
<div className="app-error">
<h1>Application Error</h1>
<p>{error}</p>
<button onClick={() => window.location.reload()}>
Reload Application
</button>
</div>
);
}
return (
<div className={`app ${useAppStore.getState().theme}`} data-app="advanced-demo">
<Navigation />
<main className="main-content" data-component="main">
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/showcase" element={<ComponentShowcase />} />
<Route path="/interactive" element={<InteractiveDemo />} />
<Route path="/config" element={<ConfigurationGuide />} />
</Routes>
</Suspense>
</main>
<footer className="app-footer" data-component="footer">
<p>Built with Vite + React + TypeScript + AppDev Design Mode Plugin</p>
</footer>
</div>
);
};
export default App;

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { cn } from '../utils/cn';
export interface DemoElementProps {
element: {
id: string;
tag: string;
content: string;
className: string;
};
isDesignMode: boolean;
onSelect: (element: HTMLElement) => void;
}
export const DemoElement: React.FC<DemoElementProps> = ({ element, isDesignMode, onSelect }) => {
const Tag = element.tag as any;
return (
<Tag
id={element.id}
className={cn(
element.className,
"transition-all duration-200 cursor-default",
isDesignMode && "hover:outline hover:outline-2 hover:outline-blue-400 hover:outline-offset-2 cursor-pointer",
"data-[selected=true]:outline data-[selected=true]:outline-2 data-[selected=true]:outline-blue-600 data-[selected=true]:outline-offset-2"
)}
onClick={(e: React.MouseEvent) => {
if (isDesignMode) {
e.preventDefault();
onSelect(e.currentTarget as HTMLElement);
}
}}
data-element={element.id}
>
{element.content}
</Tag>
);
};

View File

@@ -0,0 +1,957 @@
@import "tailwindcss";
/* Advanced AppDev Design Mode Example Styles */
/* CSS Variables for theming */
:root {
/* Light theme colors */
--bg-primary: #ffffff;
--bg-secondary: #f8fafc;
--bg-tertiary: #f1f5f9;
--text-primary: #1e293b;
--text-secondary: #64748b;
--text-muted: #94a3b8;
--border-color: #e2e8f0;
--border-hover: #cbd5e1;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
--accent-primary: #3b82f6;
--accent-secondary: #8b5cf6;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* Typography */
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'Fira Code', 'Monaco', 'Cascadia Code', monospace;
/* Border radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
}
/* Dark theme */
.dark {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f8fafc;
--text-secondary: #cbd5e1;
--text-muted: #94a3b8;
--border-color: #334155;
--border-hover: #475569;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4);
}
/* Reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-family);
line-height: 1.6;
color: var(--text-primary);
background: var(--bg-primary);
transition: background-color 0.3s ease, color 0.3s ease;
}
/* App layout */
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Navigation */
.nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-md) var(--spacing-xl);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(10px);
}
.nav-brand {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.nav-brand h1 {
font-size: 1.25rem;
font-weight: 700;
color: var(--text-primary);
}
.nav-brand .version {
font-size: 0.75rem;
color: var(--text-muted);
background: var(--bg-tertiary);
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
}
.nav-links {
display: flex;
gap: var(--spacing-lg);
}
.nav-link {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
color: var(--text-secondary);
text-decoration: none;
border-radius: var(--radius-md);
transition: all 0.2s ease;
}
.nav-link:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
.nav-link.active {
color: var(--accent-primary);
background: rgba(59, 130, 246, 0.1);
}
.nav-link .icon {
font-size: 1.1rem;
}
.theme-toggle {
background: none;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--spacing-sm);
cursor: pointer;
font-size: 1.2rem;
transition: all 0.2s ease;
}
.theme-toggle:hover {
background: var(--bg-tertiary);
border-color: var(--border-hover);
}
/* Main content */
.main-content {
flex: 1;
padding: var(--spacing-xl);
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
/* Dashboard specific styles */
.dashboard {
display: flex;
flex-direction: column;
gap: var(--spacing-2xl);
}
/* Welcome section */
.welcome-section {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-xl);
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
border-radius: var(--radius-xl);
color: white;
}
.welcome-content h1 {
font-size: 2.5rem;
font-weight: 800;
margin-bottom: var(--spacing-sm);
}
.welcome-content p {
font-size: 1.2rem;
opacity: 0.9;
}
.status-indicators {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.status-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
background: rgba(255, 255, 255, 0.1);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
backdrop-filter: blur(10px);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Sections */
.section-title {
font-size: 1.875rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: var(--spacing-lg);
}
/* Metrics section */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-lg);
}
.metric-card {
background: var(--bg-secondary);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
transition: all 0.2s ease;
}
.metric-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.metric-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
}
.metric-header h3 {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.metric-icon {
font-size: 1.5rem;
}
.metric-value {
font-size: 2.25rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.metric-change {
font-size: 0.875rem;
font-weight: 600;
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.metric-change.up {
color: var(--success);
}
.metric-change.down {
color: var(--danger);
}
.metric-change.neutral {
color: var(--text-muted);
}
/* Feature demonstrations */
.features-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--spacing-xl);
}
.feature-demo {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
overflow: hidden;
}
.feature-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-lg);
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
.feature-header h3 {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
}
.feature-tabs {
display: flex;
background: var(--bg-primary);
border-radius: var(--radius-md);
padding: 0.25rem;
}
.tab {
padding: var(--spacing-sm) var(--spacing-md);
border: none;
background: none;
color: var(--text-secondary);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.2s ease;
}
.tab.active {
background: var(--accent-primary);
color: white;
}
.feature-description {
padding: var(--spacing-lg);
color: var(--text-secondary);
border-bottom: 1px solid var(--border-color);
}
.feature-content {
min-height: 200px;
}
.demo-container,
.code-container {
padding: var(--spacing-lg);
}
.code-container {
background: var(--bg-tertiary);
overflow-x: auto;
font-family: var(--font-mono);
font-size: 0.875rem;
line-height: 1.6;
}
/* Component registry */
.components-section {
margin-top: var(--spacing-2xl);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
}
.search-container {
position: relative;
}
.search-input {
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background: var(--bg-secondary);
color: var(--text-primary);
width: 300px;
transition: border-color 0.2s ease;
}
.search-input:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.components-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--spacing-lg);
}
.component-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
transition: all 0.2s ease;
}
.component-card:hover {
box-shadow: var(--shadow-md);
border-color: var(--border-hover);
}
.component-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
}
.component-header h4 {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
}
.component-type {
font-size: 0.75rem;
color: var(--text-muted);
background: var(--bg-tertiary);
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.props-preview {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
}
.prop-tag {
font-size: 0.75rem;
color: var(--text-secondary);
background: var(--bg-tertiary);
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
}
/* Demo specific styles */
.source-map-demo {
text-align: center;
padding: var(--spacing-lg);
}
.demo-element {
display: inline-block;
padding: var(--spacing-md) var(--spacing-lg);
background: var(--accent-primary);
color: white;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
}
.demo-element:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.component-tree {
padding: var(--spacing-lg);
font-family: var(--font-mono);
font-size: 0.875rem;
}
.tree-node {
margin: var(--spacing-sm) 0;
}
.tree-node.root>.node-label {
font-weight: 600;
color: var(--accent-primary);
}
.children {
margin-left: var(--spacing-lg);
border-left: 2px solid var(--border-color);
padding-left: var(--spacing-md);
}
.node-label {
color: var(--text-primary);
}
.live-edit-demo {
padding: var(--spacing-lg);
}
.editable-element {
background: var(--bg-tertiary);
padding: var(--spacing-lg);
border-radius: var(--radius-md);
text-align: center;
margin-bottom: var(--spacing-lg);
cursor: pointer;
transition: all 0.2s ease;
}
.editable-element:hover {
background: var(--accent-primary);
color: white;
}
.style-controls {
display: flex;
gap: var(--spacing-sm);
justify-content: center;
flex-wrap: wrap;
}
.style-btn {
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
.style-btn.primary {
background: var(--accent-primary);
color: white;
}
.style-btn.secondary {
background: var(--accent-secondary);
color: white;
}
.style-btn.danger {
background: var(--danger);
color: white;
}
.style-btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
/* Footer */
.app-footer {
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
padding: var(--spacing-xl);
text-align: center;
color: var(--text-secondary);
margin-top: auto;
}
/* App error state */
.app-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 50vh;
padding: var(--spacing-xl);
text-align: center;
}
.app-error h1 {
color: var(--danger);
margin-bottom: var(--spacing-md);
}
.app-error button {
margin-top: var(--spacing-lg);
padding: var(--spacing-md) var(--spacing-lg);
background: var(--accent-primary);
color: white;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-weight: 600;
transition: background 0.2s ease;
}
.app-error button:hover {
background: var(--accent-secondary);
}
/* Responsive design */
@media (max-width: 768px) {
.nav {
flex-direction: column;
gap: var(--spacing-md);
}
.nav-links {
flex-wrap: wrap;
justify-content: center;
}
.main-content {
padding: var(--spacing-md);
}
.welcome-section {
flex-direction: column;
text-align: center;
}
.welcome-content h1 {
font-size: 2rem;
}
.section-header {
flex-direction: column;
gap: var(--spacing-md);
align-items: stretch;
}
.search-input {
width: 100%;
}
.metrics-grid {
grid-template-columns: 1fr;
}
.components-grid {
grid-template-columns: 1fr;
}
}
/* Design mode specific styles */
[data-design-mode-enabled="true"] [data-source-info] {
position: relative;
}
[data-design-mode-enabled="true"] [data-source-info]:hover {
outline: 2px dashed var(--accent-primary) !important;
outline-offset: 2px !important;
cursor: pointer !important;
}
/* Print styles */
@media print {
.nav,
.app-footer {
display: none;
}
.main-content {
padding: 0;
}
}
/* Interactive Demo Styles */
.interactive-demo {
display: flex;
flex-direction: column;
gap: var(--spacing-xl);
}
.demo-header {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
background: var(--bg-secondary);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
}
.demo-controls {
display: flex;
justify-content: space-between;
align-items: center;
}
.design-mode-toggle {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md) var(--spacing-lg);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.design-mode-toggle.active {
background: var(--accent-primary);
color: white;
border-color: var(--accent-primary);
}
.element-editor {
background: var(--bg-primary);
padding: var(--spacing-lg);
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.editor-controls {
display: flex;
gap: var(--spacing-xl);
margin-top: var(--spacing-md);
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.demo-workspace {
display: grid;
grid-template-columns: 1fr 300px;
gap: var(--spacing-xl);
min-height: 500px;
}
.preview-area {
background: var(--bg-secondary);
border: 2px dashed var(--border-color);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
position: relative;
transition: all 0.3s ease;
}
.preview-area.design-mode-active {
border-color: var(--accent-primary);
background: rgba(59, 130, 246, 0.05);
}
.preview-content {
background: var(--bg-primary);
padding: var(--spacing-2xl);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
min-height: 100%;
}
.modifications-panel {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
padding: var(--spacing-md) var(--spacing-lg);
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.reset-btn {
font-size: 0.875rem;
color: var(--danger);
background: none;
border: none;
cursor: pointer;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
}
.reset-btn:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.1);
}
.reset-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.modifications-list {
flex: 1;
overflow-y: auto;
padding: var(--spacing-md);
}
.modification-item {
background: var(--bg-primary);
padding: var(--spacing-md);
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
margin-bottom: var(--spacing-sm);
font-size: 0.875rem;
}
.mod-header {
display: flex;
justify-content: space-between;
margin-bottom: var(--spacing-xs);
font-weight: 600;
}
.property {
color: var(--text-secondary);
font-family: var(--font-mono);
}
.mod-values {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--text-secondary);
}
.value-change {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-family: var(--font-mono);
}
.arrow {
color: var(--text-muted);
}
.new-value {
color: var(--accent-primary);
}
.mod-time {
font-size: 0.75rem;
color: var(--text-muted);
}
.demo-footer {
background: var(--bg-secondary);
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
}
.instructions ol {
margin-left: var(--spacing-xl);
margin-top: var(--spacing-md);
color: var(--text-secondary);
}
.instructions li {
margin-bottom: var(--spacing-xs);
}
/* Demo Elements */
.demo-title {
font-size: 2.5rem;
color: var(--text-primary);
margin-bottom: var(--spacing-md);
}
.demo-description {
font-size: 1.125rem;
color: var(--text-secondary);
margin-bottom: var(--spacing-xl);
max-width: 600px;
}
.demo-button {
padding: var(--spacing-md) var(--spacing-xl);
border-radius: var(--radius-md);
border: none;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.demo-button.primary {
background: var(--accent-primary);
color: white;
}
.demo-button:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.demo-card {
background: white;
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
margin-top: var(--spacing-xl);
border: 1px solid var(--border-color);
}
/* Selected state for design mode */
.selected {
outline: 2px solid var(--accent-primary) !important;
outline-offset: 2px;
position: relative;
}
.selected::after {
content: 'Selected';
position: absolute;
top: -24px;
left: 0;
background: var(--accent-primary);
color: white;
font-size: 0.75rem;
padding: 2px 6px;
border-radius: 4px;
pointer-events: none;
}

View File

@@ -0,0 +1,62 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './index.css';
import './index.css';
// 开发模式下的额外日志
if (import.meta.env.DEV) {
console.log('🚀 Advanced AppDev Design Mode Example');
console.log('🔧 Plugin Configuration:', {
enabled: true,
prefix: 'design-mode',
verbose: true,
typescript: true,
react: true,
routing: true,
stateManagement: true
});
}
// 错误边界组件
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean }
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(): { hasError: boolean } {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '2rem', textAlign: 'center', color: '#e53e3e' }}>
<h1>Something went wrong.</h1>
<p>Please refresh the page to try again.</p>
</div>
);
}
return this.props.children;
}
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ErrorBoundary>
<BrowserRouter>
<App />
</BrowserRouter>
</ErrorBoundary>
</React.StrictMode>
);

View File

@@ -0,0 +1,197 @@
import React, { useState } from 'react';
const ComponentShowcase: React.FC = () => {
const [activeCategory, setActiveCategory] = useState('buttons');
const [selectedComponent, setSelectedComponent] = useState<string | null>(null);
const categories = [
{ id: 'buttons', label: 'Buttons', icon: '🔘' },
{ id: 'forms', label: 'Forms', icon: '📝' },
{ id: 'cards', label: 'Cards', icon: '🃏' },
{ id: 'navigation', label: 'Navigation', icon: '🧭' }
];
const components = {
buttons: [
{
id: 'primary-btn',
name: 'Primary Button',
description: 'Main action button with primary styling',
component: (
<button className="btn-primary">Primary Action</button>
),
code: `<button className="btn-primary">Primary Action</button>`
},
{
id: 'secondary-btn',
name: 'Secondary Button',
description: 'Secondary action button',
component: (
<button className="btn-secondary">Secondary Action</button>
),
code: `<button className="btn-secondary">Secondary Action</button>`
},
{
id: 'danger-btn',
name: 'Danger Button',
description: 'Destructive action button',
component: (
<button className="btn-danger">Delete Item</button>
),
code: `<button className="btn-danger">Delete Item</button>`
}
],
forms: [
{
id: 'input-field',
name: 'Input Field',
description: 'Standard text input with label',
component: (
<div className="form-group">
<label>Username</label>
<input type="text" placeholder="Enter username" className="form-input" />
</div>
),
code: `<div className="form-group">
<label>Username</label>
<input type="text" placeholder="Enter username" className="form-input" />
</div>`
},
{
id: 'checkbox-group',
name: 'Checkbox Group',
description: 'Multiple choice checkboxes',
component: (
<div className="form-group">
<label>Preferences</label>
<div className="checkbox-group">
<label><input type="checkbox" /> Email notifications</label>
<label><input type="checkbox" /> SMS alerts</label>
<label><input type="checkbox" /> Push notifications</label>
</div>
</div>
),
code: `<div className="form-group">
<label>Preferences</label>
<div className="checkbox-group">
<label><input type="checkbox" /> Email notifications</label>
<label><input type="checkbox" /> SMS alerts</label>
<label><input type="checkbox" /> Push notifications</label>
</div>
</div>`
}
],
cards: [
{
id: 'info-card',
name: 'Info Card',
description: 'Card with title, content and actions',
component: (
<div className="card">
<div className="card-header">
<h3>Card Title</h3>
</div>
<div className="card-body">
<p>This is a sample card component with some descriptive content.</p>
</div>
<div className="card-actions">
<button className="btn-text">Learn More</button>
</div>
</div>
),
code: `<div className="card">
<div className="card-header">
<h3>Card Title</h3>
</div>
<div className="card-body">
<p>This is a sample card component with some descriptive content.</p>
</div>
<div className="card-actions">
<button className="btn-text">Learn More</button>
</div>
</div>`
}
],
navigation: [
{
id: 'breadcrumb',
name: 'Breadcrumb',
description: 'Navigation breadcrumb trail',
component: (
<nav className="breadcrumb">
<a href="#">Home</a>
<span>/</span>
<a href="#">Products</a>
<span>/</span>
<span>Current Page</span>
</nav>
),
code: `<nav className="breadcrumb">
<a href="#">Home</a>
<span>/</span>
<a href="#">Products</a>
<span>/</span>
<span>Current Page</span>
</nav>`
}
]
};
return (
<div className="component-showcase" data-page="showcase">
<header className="showcase-header">
<h1>Component Showcase</h1>
<p>Explore various UI components and their source mappings</p>
</header>
<div className="showcase-content">
<aside className="category-sidebar">
<h3>Categories</h3>
<nav className="category-nav">
{categories.map(category => (
<button
key={category.id}
className={`category-btn ${activeCategory === category.id ? 'active' : ''}`}
onClick={() => setActiveCategory(category.id)}
data-category={category.id}
>
<span className="icon">{category.icon}</span>
<span className="label">{category.label}</span>
</button>
))}
</nav>
</aside>
<main className="components-display">
<div className="components-grid">
{components[activeCategory as keyof typeof components]?.map(component => (
<div
key={component.id}
className={`component-item ${selectedComponent === component.id ? 'selected' : ''}`}
data-component={component.id}
onClick={() => setSelectedComponent(
selectedComponent === component.id ? null : component.id
)}
>
<div className="component-preview" data-element="preview">
{component.component}
</div>
<div className="component-info">
<h4 data-element="name">{component.name}</h4>
<p data-element="description">{component.description}</p>
</div>
{selectedComponent === component.id && (
<div className="component-code" data-element="code">
<pre><code>{component.code}</code></pre>
</div>
)}
</div>
))}
</div>
</main>
</div>
</div>
);
};
export default ComponentShowcase;

View File

@@ -0,0 +1,292 @@
import React, { useState } from 'react';
const ConfigurationGuide: React.FC = () => {
const [activeSection, setActiveSection] = useState('basic');
const sections = [
{ id: 'basic', label: 'Basic Setup', icon: '⚙️' },
{ id: 'options', label: 'Configuration Options', icon: '🔧' },
{ id: 'advanced', label: 'Advanced Usage', icon: '🚀' },
{ id: 'troubleshooting', label: 'Troubleshooting', icon: '🛠️' }
];
const configExamples = {
basic: {
title: 'Basic Setup',
description: 'Get started with the plugin in minutes',
code: `// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import appdevDesignMode from 'vite-plugin-appdev-design-mode';
export default defineConfig({
plugins: [
react(),
appdevDesignMode() // Basic configuration
]
});`,
explanation: 'This is the minimal configuration needed to get started. The plugin will use all default options.'
},
options: {
title: 'Configuration Options',
description: 'Customize the plugin behavior with various options',
code: `// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import appdevDesignMode from 'vite-plugin-appdev-design-mode';
export default defineConfig({
plugins: [
react(),
appdevDesignMode({
// Enable/disable the plugin
enabled: true,
// Only enable in development
enableInProduction: false,
// Custom attribute prefix
attributePrefix: 'design-mode',
// Enable verbose logging
verbose: true,
// File patterns to exclude
exclude: [
'node_modules',
'.git',
'dist',
'**/*.test.{ts,tsx,js,jsx}'
],
// File patterns to include
include: [
'src/**/*.{ts,tsx,js,jsx}',
'components/**/*.{ts,tsx,js,jsx}'
]
})
]
});`,
explanation: 'This configuration shows all available options and their default values.'
},
advanced: {
title: 'Advanced Usage',
description: 'Advanced patterns and integration techniques',
code: `// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import appdevDesignMode from 'vite-plugin-appdev-design-mode';
export default defineConfig({
plugins: [
react(),
// Multiple configurations for different environments
appdevDesignMode({
enabled: true,
attributePrefix: 'dev-mode',
verbose: process.env.NODE_ENV === 'development',
// Conditional inclusion based on environment
include: process.env.NODE_ENV === 'development'
? ['src/**/*.{ts,tsx,js,jsx}']
: [], // Disable in production
// Custom exclude patterns
exclude: [
'node_modules',
'.git',
'dist',
'build',
'**/*.test.{ts,tsx,js,jsx}',
'**/__tests__/**',
'stories/**',
'**/*.stories.{ts,tsx}'
]
})
],
// Server configuration for API endpoints
server: {
port: 5173,
host: true,
middlewareMode: false
}
});
// Environment-specific configuration
const isDevelopment = process.env.NODE_ENV === 'development';
export default defineConfig({
plugins: isDevelopment ? [
react(),
appdevDesignMode({
verbose: true,
attributePrefix: 'dev'
})
] : [react()]
});`,
explanation: 'This example shows how to configure the plugin differently based on environment and use cases.'
},
troubleshooting: {
title: 'Troubleshooting',
description: 'Common issues and their solutions',
code: `# Common Issues and Solutions
## Plugin not injecting source information
### Problem: Elements don't have data attributes
### Solution: Check file inclusion patterns
\`\`\`js
appdevDesignMode({
include: ['src/**/*.{ts,tsx,js,jsx}'] // Make sure this matches your project structure
})
\`\`\`
## Performance issues
### Problem: Plugin slowing down development server
### Solution: Exclude unnecessary files
\`\`\`js
appdevDesignMode({
exclude: [
'node_modules',
'**/*.test.{ts,tsx,js,jsx}',
'**/__tests__/**',
'stories/**'
]
})
\`\`\`
## TypeScript errors
### Problem: Cannot find module '@babel/core'
### Solution: Install peer dependencies
\`\`\`bash
npm install --save-dev @babel/core @babel/traverse @babel/types
\`\`\`
## Custom configuration not working
### Problem: Plugin using default configuration
### Solution: Check import path and plugin options
\`\`\`js
// Correct import
import appdevDesignMode from 'vite-plugin-appdev-design-mode';
// Make sure options are passed correctly
appdevDesignMode({
enabled: true, // This should not be undefined
attributePrefix: 'custom'
})
\`\`\`
## API endpoints not accessible
### Problem: Cannot access plugin API endpoints
### Solution: Check server configuration and CORS
\`\`\`js
export default defineConfig({
server: {
cors: true, // Enable CORS for API access
headers: {
'Access-Control-Allow-Origin': '*'
}
}
})
\`\`\``,
explanation: 'This section covers common problems developers encounter and their solutions.'
}
};
return (
<div className="config-guide" data-page="config">
<header className="guide-header">
<h1>Configuration Guide</h1>
<p>Learn how to configure and use the AppDev Design Mode plugin effectively</p>
</header>
<div className="guide-content">
<nav className="guide-sidebar">
<h3>Sections</h3>
<ul className="guide-nav">
{Object.entries(configExamples).map(([key, section]) => (
<li key={key}>
<button
className={`nav-item ${activeSection === key ? 'active' : ''}`}
onClick={() => setActiveSection(key)}
data-section={key}
>
<span className="icon">{sections.find(s => s.id === key)?.icon}</span>
<span className="label">{section.title}</span>
</button>
</li>
))}
</ul>
</nav>
<main className="guide-main">
<div className="guide-section">
<div className="section-header">
<h2 data-element="section-title">
{configExamples[activeSection as keyof typeof configExamples].title}
</h2>
<p className="section-description" data-element="section-description">
{configExamples[activeSection as keyof typeof configExamples].description}
</p>
</div>
<div className="content-blocks">
<div className="code-block" data-element="code-block">
<div className="code-header">
<h4>Configuration</h4>
<button
className="copy-btn"
onClick={() => {
navigator.clipboard.writeText(configExamples[activeSection as keyof typeof configExamples].code);
}}
data-action="copy-code"
>
📋 Copy
</button>
</div>
<pre className="code-content">
<code>{configExamples[activeSection as keyof typeof configExamples].code}</code>
</pre>
</div>
<div className="explanation-block" data-element="explanation">
<h4>Explanation</h4>
<p>{configExamples[activeSection as keyof typeof configExamples].explanation}</p>
</div>
</div>
</div>
</main>
</div>
<footer className="guide-footer">
<div className="additional-resources">
<h4>Additional Resources</h4>
<ul>
<li>
<a href="https://vitejs.dev/config/" target="_blank" rel="noopener noreferrer">
📖 Vite Configuration Documentation
</a>
</li>
<li>
<a href="https://babeljs.io/docs/babel-plugin-there/" target="_blank" rel="noopener noreferrer">
🔧 Babel Plugin API Documentation
</a>
</li>
<li>
<a href="https://github.com/vite-plugin-appdev-design-mode" target="_blank" rel="noopener noreferrer">
📦 Plugin Repository
</a>
</li>
</ul>
</div>
</footer>
</div>
);
};
export default ConfigurationGuide;

View File

@@ -0,0 +1,242 @@
import React, { useState } from 'react';
import { useAppStore } from '../store/appStore';
interface MetricCardProps {
title: string;
value: string | number;
change?: string;
trend?: 'up' | 'down' | 'neutral';
icon: string;
}
const MetricCard: React.FC<MetricCardProps> = ({ title, value, change, trend, icon }) => (
<div className="metric-card" data-component="metric-card">
<div className="metric-header" data-element="metric-header">
<h3 data-element="metric-title">{title}</h3>
<span className="metric-icon" data-element="metric-icon">{icon}</span>
</div>
<div className="metric-value" data-element="metric-value">{value}</div>
{change && (
<div className={`metric-change ${trend}`} data-element="metric-change">
{trend === 'up' && '↗️'}
{trend === 'down' && '↘️'}
{trend === 'neutral' && '➡️'}
{change}
</div>
)}
</div>
);
interface FeatureDemoProps {
title: string;
description: string;
code: string;
demo: React.ReactNode;
}
const FeatureDemo: React.FC<FeatureDemoProps> = ({ title, description, code, demo }) => {
const [showCode, setShowCode] = useState(false);
const [activeTab, setActiveTab] = useState<'demo' | 'code'>('demo');
return (
<div className="feature-demo" data-component="feature-demo">
<div className="feature-header" data-element="feature-header">
<h3 data-element="feature-title">{title}</h3>
<div className="feature-tabs" data-element="tabs">
<button
className={`tab ${activeTab === 'demo' ? 'active' : ''}`}
onClick={() => setActiveTab('demo')}
data-tab="demo"
>
Demo
</button>
<button
className={`tab ${activeTab === 'code' ? 'active' : ''}`}
onClick={() => setActiveTab('code')}
data-tab="code"
>
Code
</button>
</div>
</div>
<p className="feature-description" data-element="description">{description}</p>
<div className="feature-content" data-element="content">
{activeTab === 'demo' ? (
<div className="demo-container" data-element="demo">
{demo}
</div>
) : (
<pre className="code-container" data-element="code">
<code>{code}</code>
</pre>
)}
</div>
</div>
);
};
const Dashboard: React.FC = () => {
const { theme, user, components } = useAppStore();
const [searchTerm, setSearchTerm] = useState('');
const metrics = [
{ title: 'Total Components', value: components.length, change: '+2', trend: 'up' as const, icon: '🧩' },
{ title: 'Active Elements', value: 24, change: '+5', trend: 'up' as const, icon: '⚡' },
{ title: 'Design Mode Sessions', value: 156, change: '+12', trend: 'up' as const, icon: '🎨' },
{ title: 'Code Quality Score', value: '94%', change: '+2%', trend: 'up' as const, icon: '📊' }
];
const componentList = components.filter(comp =>
comp.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
comp.type.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="dashboard" data-page="dashboard">
{/* Welcome Section */}
<section className="welcome-section" data-section="welcome">
<div className="welcome-content" data-element="content">
<h1 data-element="title">
Welcome back, {user.name}!
</h1>
<p data-element="subtitle">
Here's what's happening with your design mode plugin today.
</p>
</div>
<div className="status-indicators" data-element="indicators">
<div className="status-item" data-status="plugin-active">
<span className="status-dot active"></span>
<span>Plugin Active</span>
</div>
<div className="status-item" data-status="theme">
<span className="theme-indicator">{theme === 'light' ? '☀️' : '🌙'}</span>
<span>{theme.charAt(0).toUpperCase() + theme.slice(1)} Mode</span>
</div>
</div>
</section>
{/* Metrics Grid */}
<section className="metrics-section" data-section="metrics">
<h2 className="section-title" data-element="section-title">System Metrics</h2>
<div className="metrics-grid">
{metrics.map((metric, index) => (
<MetricCard key={index} {...metric} />
))}
</div>
</section>
{/* Feature Demonstrations */}
<section className="features-section" data-section="features">
<h2 className="section-title" data-element="section-title">Plugin Features</h2>
<div className="features-grid">
<FeatureDemo
title="Source Mapping"
description="Automatic injection of source location information into DOM elements during compilation."
code={`// 插件自动为JSX元素添加源码信息
<div className="example" data-source-info="...">
Content here
</div>`}
demo={
<div className="source-map-demo">
<div className="demo-element" data-element="mapped">
Hover over me to see source mapping info
</div>
</div>
}
/>
<FeatureDemo
title="Component Tree"
description="Visual representation of component hierarchy with source locations."
code={`// 自动生成的组件树
App (App.tsx:12)
├── Header (Header.tsx:5)
├── Navigation (Nav.tsx:15)
└── MainContent (Main.tsx:8)`}
demo={
<div className="component-tree">
<div className="tree-node root" data-node="app">
<div className="node-label">App</div>
<div className="children">
<div className="tree-node" data-node="header">
<div className="node-label">Header</div>
</div>
<div className="tree-node" data-node="main">
<div className="node-label">MainContent</div>
</div>
</div>
</div>
</div>
}
/>
<FeatureDemo
title="Live Editing"
description="Real-time style modifications with automatic HMR integration."
code={`// 修改样式实时生效
const modifyStyles = async (elementId, newStyles) => {
await fetch('/__modify_element_source', {
method: 'POST',
body: JSON.stringify({ elementId, newStyles })
});
};`}
demo={
<div className="live-edit-demo">
<div className="editable-element" data-editable="true">
Click to edit styles
</div>
<div className="style-controls">
<button className="style-btn primary">Primary</button>
<button className="style-btn secondary">Secondary</button>
<button className="style-btn danger">Danger</button>
</div>
</div>
}
/>
</div>
</section>
{/* Component Registry */}
<section className="components-section" data-section="components">
<div className="section-header" data-element="header">
<h2 className="section-title" data-element="title">Component Registry</h2>
<div className="search-container" data-element="search">
<input
type="text"
placeholder="Search components..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
data-element="input"
/>
</div>
</div>
<div className="components-grid">
{componentList.map((component) => (
<div key={component.id} className="component-card" data-component={component.type}>
<div className="component-header">
<h4 data-element="name">{component.name}</h4>
<span className="component-type" data-element="type">{component.type}</span>
</div>
<div className="component-info" data-element="info">
<small>ID: {component.id}</small>
<div className="props-preview">
{Object.entries(component.props).slice(0, 2).map(([key, value]) => (
<span key={key} className="prop-tag">
{key}: {String(value)}
</span>
))}
</div>
</div>
</div>
))}
</div>
</section>
</div>
);
};
export default Dashboard;

View File

@@ -0,0 +1,74 @@
import React from 'react';
import { DemoElement } from '../components/DemoElement';
const InteractiveDemo: React.FC = () => {
// No local state or context needed - Design Mode is handled globally by the plugin
const demoElements = [
{
id: 'hero-title',
tag: 'h1',
content: 'Interactive Design Mode Demo',
className: 'text-4xl font-bold text-slate-900 mb-4'
},
{
id: 'hero-description',
tag: 'p',
content: 'Click on elements in design mode to modify their properties in real-time',
className: 'text-lg text-slate-600 mb-8 max-w-2xl'
},
{
id: 'demo-button',
tag: 'button',
content: 'Click Me',
className: 'px-6 py-3 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition-colors shadow-sm'
},
{
id: 'demo-card',
tag: 'div',
content: 'Sample Card Content',
className: 'mt-8 p-6 bg-white rounded-xl shadow-md border border-slate-200'
}
];
return (
<div className="flex flex-col gap-8" data-page="interactive">
<header className="flex flex-col gap-6 bg-slate-50 p-8 rounded-xl border border-slate-200">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold text-slate-900">Interactive Design Mode</h1>
<p className="text-sm text-slate-500">
Design Mode is now injected globally. Use the floating toggle in the bottom right to enable it.
</p>
</div>
</header>
<div className="grid grid-cols-1 gap-8 min-h-[600px]">
<div className="bg-slate-50 border-2 border-dashed border-slate-300 rounded-xl p-8 relative">
<div className="bg-white p-12 rounded-lg shadow-sm min-h-full">
{demoElements.map(element => (
<DemoElement
key={element.id}
element={element}
isDesignMode={false} // The global manager handles selection visuals
onSelect={() => {}} // Handled globally
/>
))}
</div>
</div>
</div>
<footer className="bg-slate-50 p-6 rounded-xl border border-slate-200">
<div className="max-w-3xl">
<h4 className="font-semibold text-slate-900 mb-4">Instructions:</h4>
<ol className="list-decimal list-inside space-y-2 text-slate-600 text-sm">
<li>Toggle <strong>Design Mode</strong> using the floating button</li>
<li>Click on any element to select and edit it</li>
<li>Modifications are applied globally</li>
</ol>
</div>
</footer>
</div>
);
};
export default InteractiveDemo;

View File

@@ -0,0 +1,173 @@
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface AppState {
// UI状态
theme: 'light' | 'dark';
isLoading: boolean;
error: string | null;
// 应用状态
currentPage: string;
user: {
name: string;
email: string;
preferences: {
showTooltips: boolean;
animations: boolean;
compactMode: boolean;
};
};
// 组件状态
components: Array<{
id: string;
name: string;
type: string;
props: Record<string, any>;
children?: string[];
parent?: string;
}>;
// 设计模式状态
designMode: {
enabled: boolean;
selectedElement: string | null;
showGrid: boolean;
showInfo: boolean;
};
// Actions
setTheme: (theme: 'light' | 'dark') => void;
toggleTheme: () => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
setCurrentPage: (page: string) => void;
updateUserPreferences: (preferences: Partial<AppState['user']['preferences']>) => void;
// 组件操作
addComponent: (component: Omit<AppState['components'][0], 'id'>) => void;
updateComponent: (id: string, updates: Partial<AppState['components'][0]>) => void;
removeComponent: (id: string) => void;
// 设计模式操作
setDesignModeEnabled: (enabled: boolean) => void;
setSelectedElement: (element: string | null) => void;
toggleGrid: () => void;
toggleInfo: () => void;
// 重置状态
reset: () => void;
}
const initialState = {
theme: 'light' as const,
isLoading: false,
error: null,
currentPage: 'dashboard',
user: {
name: 'Demo User',
email: 'demo@example.com',
preferences: {
showTooltips: true,
animations: true,
compactMode: false
}
},
components: [
{
id: 'header-1',
name: 'AppHeader',
type: 'header',
props: { title: 'Advanced Demo', showNav: true }
},
{
id: 'card-1',
name: 'InfoCard',
type: 'card',
props: { title: 'Welcome', content: 'This is an advanced example' }
}
],
designMode: {
enabled: false,
selectedElement: null,
showGrid: false,
showInfo: true
}
};
export const useAppStore = create<AppState>()(
devtools(
persist(
(set, get) => ({
...initialState,
// UI Actions
setTheme: (theme) => set({ theme }, false, 'setTheme'),
toggleTheme: () => set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light'
}), false, 'toggleTheme'),
setLoading: (loading) => set({ isLoading: loading }, false, 'setLoading'),
setError: (error) => set({ error }, false, 'setError'),
setCurrentPage: (page) => set({ currentPage: page }, false, 'setCurrentPage'),
updateUserPreferences: (preferences) => set((state) => ({
user: {
...state.user,
preferences: { ...state.user.preferences, ...preferences }
}
}), false, 'updateUserPreferences'),
// Component Actions
addComponent: (component) => set((state) => ({
components: [
...state.components,
{ ...component, id: `${component.name}-${Date.now()}` }
]
}), false, 'addComponent'),
updateComponent: (id, updates) => set((state) => ({
components: state.components.map(comp =>
comp.id === id ? { ...comp, ...updates } : comp
)
}), false, 'updateComponent'),
removeComponent: (id) => set((state) => ({
components: state.components.filter(comp => comp.id !== id)
}), false, 'removeComponent'),
// Design Mode Actions
setDesignModeEnabled: (enabled) => set((state) => ({
designMode: { ...state.designMode, enabled }
}), false, 'setDesignModeEnabled'),
setSelectedElement: (element) => set((state) => ({
designMode: { ...state.designMode, selectedElement: element }
}), false, 'setSelectedElement'),
toggleGrid: () => set((state) => ({
designMode: { ...state.designMode, showGrid: !state.designMode.showGrid }
}), false, 'toggleGrid'),
toggleInfo: () => set((state) => ({
designMode: { ...state.designMode, showInfo: !state.designMode.showInfo }
}), false, 'toggleInfo'),
// Reset
reset: () => set(initialState, false, 'reset')
}),
{
name: 'advanced-design-mode-storage',
partialize: (state) => ({
theme: state.theme,
user: state.user,
designMode: state.designMode
})
}
),
{
name: 'app-store'
}
)
);

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@pages/*": ["src/pages/*"],
"@utils/*": ["src/utils/*"],
"@types/*": ["src/types/*"],
"@hooks/*": ["src/hooks/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,77 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import appdevDesignMode from '../../packages/plugin/src/index';
export default defineConfig({
plugins: [
react(),
// 高级配置示例
appdevDesignMode({
enabled: true,
enableInProduction: false, // 只在开发环境启用
attributePrefix: 'design-mode', // 自定义属性前缀
verbose: true, // 详细日志输出
exclude: [
'node_modules',
'.git',
'dist',
'build',
'**/*.test.{ts,tsx,js,jsx}',
'**/*.spec.{ts,tsx,js,jsx}',
'**/__tests__/**',
'**/tests/**',
],
include: [
'src/**/*.{ts,tsx,js,jsx}',
'components/**/*.{ts,tsx,js,jsx}',
'pages/**/*.{ts,tsx,js,jsx}',
'!**/node_modules/**',
],
}),
],
// 高级Vite配置
server: {
port: 5174, // 使用不同端口避免冲突
host: true,
open: true,
cors: true,
fs: {
allow: ['..', '../../src'], // Allow serving the plugin source
},
},
build: {
outDir: 'dist',
sourcemap: true,
minify: 'esbuild',
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
state: ['zustand'],
},
},
},
},
resolve: {
alias: {
'@': '/src',
'@components': '/src/components',
'@pages': '/src/pages',
'@utils': '/src/utils',
},
},
css: {
modules: {
localsConvention: 'camelCaseOnly',
},
},
optimizeDeps: {
include: ['react', 'react-dom', 'react-router-dom', 'zustand'],
},
});

View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Basic AppDev Design Mode Example</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
margin: 0;
padding: 2rem;
background: linear-gradient(135deg, #74b9ff, #0984e3);
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 10px;
padding: 2rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
h1 { color: #2d3748; }
.example-box {
background: #f7fafc;
border: 2px solid #e2e8f0;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
}
.button {
background: #4299e1;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 5px;
cursor: pointer;
margin: 0.5rem;
}
.button:hover {
background: #3182ce;
}
</style>
</head>
<body>
<div class="container">
<h1 data-appdev-element="main-title">Basic Design Mode Example</h1>
<p data-appdev-element="description">
This is a simple HTML example showing how the design mode plugin works.
</p>
<div class="example-box" data-appdev-component="interactive-box">
<h2 data-appdev-element="box-title">Interactive Elements</h2>
<p data-appdev-element="box-description">
Click the buttons below to test the design mode functionality.
</p>
<button class="button" data-appdev-action="primary">Primary Button</button>
<button class="button" data-appdev-action="secondary">Secondary Button</button>
<button class="button" data-appdev-action="danger">Danger Button</button>
</div>
<div class="example-box" data-appdev-component="content-section">
<h3 data-appdev-element="content-title">Content Section</h3>
<p data-appdev-element="content-text">
This section contains various HTML elements for testing source mapping.
</p>
<ul data-appdev-element="feature-list">
<li data-appdev-item="feature-1">Source mapping works with vanilla HTML</li>
<li data-appdev-item="feature-2">Plugin adds data attributes automatically</li>
<li data-appdev-item="feature-3">No React or framework required</li>
</ul>
</div>
</div>
<script type="module" src="/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,171 @@
// Basic JavaScript example for AppDev Design Mode plugin
console.log('AppDev Design Mode Plugin - Basic Example Loaded');
// Sample data for the application
const sampleData = {
features: [
'No framework required',
'Works with vanilla HTML',
'Automatic source mapping',
'Lightweight and fast',
],
actions: {
primary: 'Primary action triggered',
secondary: 'Secondary action triggered',
danger: 'Danger action triggered',
},
};
// Interactive functionality
function initializeApp() {
console.log('Initializing basic app...');
// Add event listeners to buttons
const buttons = document.querySelectorAll('[data-appdev-action]');
buttons.forEach(button => {
button.addEventListener('click', handleButtonClick);
});
// Add click handlers to list items
const listItems = document.querySelectorAll('[data-appdev-item]');
listItems.forEach((item, index) => {
item.addEventListener('click', () => handleListItemClick(item, index));
});
// Simulate dynamic content addition
addDynamicContent();
}
function handleButtonClick(event) {
const action = event.target.getAttribute('data-appdev-action');
const message = sampleData.actions[action] || 'Unknown action';
console.log(`Button clicked: ${action}`);
// Visual feedback
event.target.style.background = '#48bb78';
setTimeout(() => {
event.target.style.background = '#4299e1';
}, 200);
showNotification(message);
}
function handleListItemClick(item, index) {
console.log(`List item clicked: ${item.textContent}`);
// Visual feedback
item.style.background = '#e6fffa';
setTimeout(() => {
item.style.background = '';
}, 300);
showNotification(`Clicked: ${item.textContent}`);
}
function addDynamicContent() {
const contentSection = document.querySelector(
'[data-appdev-component="content-section"]'
);
if (!contentSection) return;
const dynamicDiv = document.createElement('div');
dynamicDiv.setAttribute('data-appdev-component', 'dynamic-content');
dynamicDiv.setAttribute('data-dynamic', 'true');
dynamicDiv.innerHTML = `
<h4 data-appdev-element="dynamic-title">Dynamically Added Content</h4>
<p data-appdev-element="dynamic-description">
This content was added dynamically after page load.
</p>
<button class="button" data-appdev-action="dynamic" data-dynamic-button="true">
Dynamic Button
</button>
`;
contentSection.appendChild(dynamicDiv);
// Add event listener to new button
const newButton = dynamicDiv.querySelector('[data-appdev-action="dynamic"]');
newButton.addEventListener('click', event => {
console.log('Dynamic button clicked');
showNotification('Dynamic action triggered!');
event.target.style.background = '#ed8936';
setTimeout(() => {
event.target.style.background = '#4299e1';
}, 200);
});
}
function showNotification(message) {
// Remove existing notification
const existing = document.querySelector('.notification');
if (existing) {
existing.remove();
}
// Create new notification
const notification = document.createElement('div');
notification.className = 'notification';
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #48bb78;
color: white;
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
animation: slideIn 0.3s ease-out;
`;
document.body.appendChild(notification);
// Auto remove after 3 seconds
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease-in';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 3000);
}
// Add CSS animations
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
`;
document.head.appendChild(style);
// Initialize when DOM is loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeApp);
} else {
initializeApp();
}
// Log plugin status
console.log('AppDev Design Mode Plugin: Basic example ready for testing');

View File

@@ -0,0 +1,13 @@
{
"name": "vite-plugin-appdev-design-mode-basic-example",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^5.0.0"
}
}

View File

@@ -0,0 +1,13 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import appdevDesignMode from '../../packages/plugin/src/index';
export default defineConfig({
plugins: [
react(),
appdevDesignMode({
verbose: true,
attributePrefix: 'data-appdev'
})
]
});

View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,61 @@
# Design Mode Demo
这是一个完整的示例项目,展示了 `@xagi/vite-plugin-design-mode` 的所有功能。
## 功能演示
### 1. 样式编辑
- 点击任意元素查看编辑面板
- 实时修改 Tailwind CSS 类
- 自动保存到源文件
### 2. 内容编辑
- 双击文本元素进入编辑模式
- 修改后自动更新源代码
- 支持纯文本内容
### 3. Iframe 支持
- 在 iframe 中自动隐藏 UI
- 通过 postMessage 通信
- 支持外部设计面板控制
## 快速开始
```bash
# 安装依赖
npm install
# 启动开发服务器
npm run dev
```
访问 `http://localhost:5175` 查看演示。
## 使用说明
1. **启用设计模式**:点击右下角的 "Design Mode" 开关
2. **选择元素**:点击页面上的任意元素
3. **修改样式**:使用右侧面板修改背景、文字颜色、内边距等
4. **编辑内容**:双击文本元素直接编辑
5. **查看源码**:所有修改会自动保存到对应的 `.tsx` 文件
## 页面说明
- **Home**: 主页,展示核心功能卡片和可编辑示例
- **Features**: 详细功能展示,包含各种可编辑元素
- **Iframe Demo**: 演示 iframe 集成和通信机制
## 技术栈
- React 18
- TypeScript
- Vite 6
- Tailwind CSS 4
- Radix UI
- @xagi/vite-plugin-design-mode
## 注意事项
- 仅支持静态字符串 className 的修改
- 内容编辑仅支持纯文本节点
- 所有修改会直接写入源文件,建议使用版本控制

View File

@@ -0,0 +1,580 @@
# 可自定义属性面板配置指南
## 概述
本文档详细介绍了如何使用和配置可自定义的属性面板系统,该系统支持用户自定义配置,提供完整的双向同步设计模式架构。
## 功能特性
### 🎨 样式自定义
- **颜色方案**: 自定义颜色库,支持导入导出
- **样式预设**: 保存和管理常用样式组合
- **主题切换**: 浅色/深色/跟随系统主题
- **响应式布局**: 自适应不同屏幕尺寸
### 📝 内容自定义
- **富文本编辑**: 支持格式化文本编辑
- **历史记录**: 自动保存编辑历史,支持回溯
- **模板系统**: 自定义内容模板和占位符
- **快捷操作**: 支持快捷键和批量操作
### ⚙️ 高级配置
- **面板布局**: 水平和垂直布局切换
- **快捷键自定义**: 用户可自定义键盘快捷键
- **自动保存**: 配置自动保存间隔和策略
- **数据管理**: 支持配置的导入导出和重置
## 安装和配置
### 1. 基础安装
```bash
# 安装依赖
npm install antd @ant-design/icons
# 或使用 yarn
yarn add antd @ant-design/icons
```
### 2. 配置文件
创建 `vite.config.ts`:
```typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import appdevDesignMode from '@xagi/vite-plugin-design-mode';
export default defineConfig({
plugins: [
react(),
appdevDesignMode({
enabled: true,
iframeMode: {
enabled: true,
hideUI: true,
enableSelection: true,
enableDirectEdit: true,
},
batchUpdate: {
enabled: true,
debounceMs: 300,
},
bridge: {
timeout: 10000,
retryAttempts: 3,
heartbeatInterval: 30000,
debug: true,
}
})
]
});
```
### 3. 基础组件使用
```tsx
import React, { useState } from 'react';
import { PropertyPanel } from '@xagi/design-mode';
import type { PropertyPanelConfig } from '@xagi/design-mode';
function MyApp() {
const [selectedElement, setSelectedElement] = useState(null);
const [currentStyle, setCurrentStyle] = useState('');
const [currentContent, setCurrentContent] = useState('');
const handleStyleUpdate = (data) => {
console.log('Style updated:', data);
};
const handleContentUpdate = (data) => {
console.log('Content updated:', data);
};
return (
<div className="app-layout">
<PropertyPanel
selectedElement={selectedElement}
currentStyle={currentStyle}
currentContent={currentContent}
onStyleUpdate={handleStyleUpdate}
onContentUpdate={handleContentUpdate}
onStyleChange={setCurrentStyle}
onContentChange={setCurrentContent}
config={{
theme: 'light',
autoSave: true,
autoSaveInterval: 3000,
showElementInfo: true,
enableKeyboardShortcuts: true,
defaultPanel: 'style'
}}
onConfigChange={(config) => {
console.log('Config changed:', config);
}}
/>
</div>
);
}
```
## 高级配置
### 自定义配置接口
```typescript
export interface PropertyPanelConfig {
// 主题配置
theme: 'light' | 'dark' | 'auto';
// 自动保存设置
autoSave: boolean;
autoSaveInterval: number;
// 显示选项
showElementInfo: boolean;
showTooltips: boolean;
// 交互设置
enableKeyboardShortcuts: boolean;
defaultPanel: 'style' | 'content' | 'settings';
panelLayout: 'horizontal' | 'vertical';
// 自定义颜色库
colorPalette: string[];
// 自定义预设
customPresets: CustomPreset[];
// 快捷键配置
shortcuts: ShortcutConfig;
}
```
### 快捷键配置
```typescript
const shortcuts = {
save: 'Ctrl+S', // 保存当前设置
undo: 'Ctrl+Z', // 撤销操作
redo: 'Ctrl+Y', // 重做操作
togglePanel: 'F2', // 切换面板
clearAll: 'Ctrl+Delete', // 清除所有
custom1: 'Ctrl+1', // 自定义快捷键1
custom2: 'Ctrl+2', // 自定义快捷键2
custom3: 'Ctrl+3' // 自定义快捷键3
};
```
### 自定义预设管理
```typescript
interface CustomPreset {
id: string;
name: string; // 预设名称
type: 'style' | 'content'; // 预设类型
value: string; // 预设值
description: string; // 预设描述
tags: string[]; // 标签数组
favorite: boolean; // 是否收藏
createdAt: number; // 创建时间
}
// 创建自定义预设
const myPreset: CustomPreset = {
id: 'preset_001',
name: '主要按钮样式',
type: 'style',
value: 'bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600',
description: '常用的主要按钮样式',
tags: ['按钮', '主要', '蓝色'],
favorite: true,
createdAt: Date.now()
};
```
## API 接口
### 组件属性
| 属性名 | 类型 | 描述 | 默认值 |
|--------|------|------|--------|
| `selectedElement` | `ElementInfo \| null` | 当前选中的元素信息 | `null` |
| `currentStyle` | `string` | 当前样式类名 | `''` |
| `currentContent` | `string` | 当前内容 | `''` |
| `onStyleUpdate` | `(data) => void` | 样式更新回调 | 必填 |
| `onContentUpdate` | `(data) => void` | 内容更新回调 | 必填 |
| `onStyleChange` | `(style) => void` | 样式变化回调 | 必填 |
| `onContentChange` | `(content) => void` | 内容变化回调 | 必填 |
| `config` | `Partial<PropertyPanelConfig>` | 面板配置 | `undefined` |
| `onConfigChange` | `(config) => void` | 配置变化回调 | `undefined` |
### 回调函数参数
#### onStyleUpdate
```typescript
{
sourceInfo: {
fileName: string;
lineNumber: number;
columnNumber: number;
};
newClass: string;
}
```
#### onContentUpdate
```typescript
{
sourceInfo: {
fileName: string;
lineNumber: number;
columnNumber: number;
};
newContent: string;
}
```
## 事件处理
### 键盘事件
组件支持以下键盘事件:
- **Ctrl+S**: 保存当前设置
- **Ctrl+Z**: 撤销操作
- **Ctrl+Y**: 重做操作
- **F2**: 切换面板
- **Ctrl+Delete**: 清除所有
### 鼠标事件
- **单击**: 选择元素
- **双击**: 进入编辑模式
- **右键**: 显示上下文菜单
- **悬停**: 显示工具提示
## 样式定制
### 主题定制
```typescript
import { ConfigProvider } from 'antd';
// 自定义主题
const customTheme = {
algorithm: theme.darkAlgorithm,
token: {
colorPrimary: '#your-primary-color',
borderRadius: 8,
// 更多主题配置...
}
};
<ConfigProvider theme={customTheme}>
<PropertyPanel {...props} />
</ConfigProvider>
```
### 自定义样式类
```css
/* 自定义面板样式 */
.custom-property-panel {
--panel-bg: #f8f9fa;
--panel-border: #e9ecef;
--panel-text: #212529;
}
.custom-property-panel .ant-card {
background: var(--panel-bg);
border-color: var(--panel-border);
}
.custom-property-panel .ant-tabs-tab {
color: var(--panel-text);
}
```
## 数据存储
### LocalStorage 键值
- `appdev_property_panel_config`: 面板配置
- `appdev_property_panel_presets`: 自定义预设
### 数据格式
#### 配置数据格式
```json
{
"theme": "light",
"autoSave": true,
"autoSaveInterval": 3000,
"showElementInfo": true,
"showTooltips": true,
"enableKeyboardShortcuts": true,
"defaultPanel": "style",
"panelLayout": "horizontal",
"colorPalette": ["#000000", "#ffffff", "#3b82f6"],
"customPresets": [],
"shortcuts": {
"save": "Ctrl+S",
"undo": "Ctrl+Z",
"redo": "Ctrl+Y",
"togglePanel": "F2",
"clearAll": "Ctrl+Delete"
}
}
```
#### 预设数据格式
```json
[
{
"id": "preset_001",
"name": "主要按钮样式",
"type": "style",
"value": "bg-blue-500 text-white px-4 py-2 rounded-lg",
"description": "常用的主要按钮样式",
"tags": ["按钮", "主要", "蓝色"],
"favorite": true,
"createdAt": 1640995200000
}
]
```
## 错误处理
### 常见错误
#### 1. 导入配置失败
```typescript
try {
const config = JSON.parse(configString);
// 验证配置格式
if (!config.theme || !config.shortcuts) {
throw new Error('配置格式不完整');
}
} catch (error) {
console.error('导入配置失败:', error);
message.error('配置格式错误,请检查文件格式');
}
```
#### 2. 预设保存失败
```typescript
const savePreset = (preset) => {
try {
// 验证预设格式
if (!preset.name || !preset.value) {
throw new Error('预设名称和值不能为空');
}
// 保存预设
presetManager.savePreset(preset);
message.success('预设保存成功');
} catch (error) {
console.error('保存预设失败:', error);
message.error('保存失败: ' + error.message);
}
};
```
### 错误边界
```typescript
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error }) {
return (
<div className="error-boundary">
<h3></h3>
<pre>{error.message}</pre>
<button onClick={() => window.location.reload()}>
</button>
</div>
);
}
// 使用错误边界包装组件
<ErrorBoundary FallbackComponent={ErrorFallback}>
<PropertyPanel {...props} />
</ErrorBoundary>
```
## 性能优化
### 1. 组件优化
```typescript
// 使用 React.memo 优化重渲染
const PropertyPanel = React.memo<PropertyPanelProps>(({
selectedElement,
currentStyle,
currentContent,
onStyleUpdate,
onContentUpdate,
onStyleChange,
onContentChange,
config,
onConfigChange
}) => {
// 组件实现...
});
// 使用 useMemo 缓存计算结果
const processedPresets = useMemo(() => {
return presets.filter(preset => preset.favorite);
}, [presets]);
// 使用 useCallback 缓存回调函数
const handleStyleUpdate = useCallback((data) => {
onStyleUpdate(data);
}, [onStyleUpdate]);
```
### 2. 内存管理
```typescript
// 清理副作用
useEffect(() => {
const handleKeydown = (event) => {
// 处理键盘事件
};
document.addEventListener('keydown', handleKeydown);
return () => {
document.removeEventListener('keydown', handleKeydown);
};
}, []);
// 清理定时器
useEffect(() => {
const timer = setInterval(() => {
// 自动保存逻辑
}, config.autoSaveInterval);
return () => {
clearInterval(timer);
};
}, [config.autoSaveInterval]);
```
## 最佳实践
### 1. 配置管理
- 合理设置自动保存间隔,避免过于频繁的保存操作
- 为不同用户角色设置不同的默认配置
- 定期清理无用的自定义预设
### 2. 性能优化
- 大量预设数据时分页加载
- 使用虚拟滚动优化长列表显示
- 合理使用防抖和节流
### 3. 用户体验
- 提供清晰的错误提示和恢复机制
- 支持快捷键组合,满足熟练用户需求
- 保持界面的响应性和直观性
### 4. 数据安全
- 定期备份用户配置
- 验证导入数据的格式和安全性
- 提供配置恢复功能
## 故障排除
### 常见问题
#### Q: 面板不显示或加载失败
**A**: 检查以下项目:
1. 确认所有依赖包已正确安装
2. 检查网络连接是否正常
3. 查看浏览器控制台是否有错误信息
4. 验证配置格式是否正确
#### Q: 自定义预设无法保存
**A**: 可能原因:
1. LocalStorage 存储已满,尝试清理无用数据
2. 预设数据格式不正确,检查数据结构
3. 浏览器隐私设置阻止了本地存储
#### Q: 快捷键不生效
**A**: 检查项目:
1. 确认键盘事件监听器正确注册
2. 检查是否有其他组件占用了相同的快捷键
3. 验证快捷键格式是否符合系统要求
#### Q: 配置导入导出失败
**A**: 可能的问题:
1. JSON 格式错误,使用在线工具验证
2. 文件权限问题,检查文件读写权限
3. 数据大小超限,压缩或分批导入
### 调试技巧
#### 1. 启用调试模式
```typescript
const config = {
debug: true,
// 其他配置...
};
```
#### 2. 监控配置变化
```typescript
useEffect(() => {
console.log('Config changed:', config);
}, [config]);
```
#### 3. 检查存储状态
```typescript
const checkStorage = () => {
const config = localStorage.getItem('appdev_property_panel_config');
const presets = localStorage.getItem('appdev_property_panel_presets');
console.log('Stored config:', config);
console.log('Stored presets:', presets);
};
```
## 更新日志
### v2.0.0 (当前版本)
- ✨ 新增完整的可自定义属性面板
- ✨ 支持主题切换和个性化配置
- ✨ 新增预设管理系统
- ✨ 支持配置导入导出
- ✨ 增强键盘快捷键支持
- ✨ 改进错误处理和恢复机制
### v1.0.0
- 🎉 初始版本发布
- ✨ 基础的样式和内容编辑功能
- ✨ 简单的面板配置选项
## 技术支持
如果您在使用过程中遇到问题,请通过以下方式获取帮助:
1. **查看文档**: 详细阅读本文档和使用指南
2. **检查示例**: 参考 examples/demo 目录中的示例代码
3. **调试模式**: 启用 debug 模式查看详细日志
4. **社区支持**: 在 GitHub 上提交 Issue
5. **联系支持**: 发送邮件到 support@example.com
## 贡献指南
我们欢迎社区贡献!请查看 CONTRIBUTING.md 了解如何参与项目开发。
### 贡献方式
- 🐛 报告 Bug
- 💡 提出新功能建议
- 📝 完善文档
- 💻 提交代码改进
- 🧪 编写测试用例
感谢您对项目的关注和支持!🎉

View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

View File

@@ -0,0 +1,32 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>设计模式演示</title>
<!-- DEV-INJECT-START -->
<script data-id="dev-inject-monitor">
(function() {
const remote = "/sdk/dev-monitor.js";
const separator = remote.includes('?') ? '&' : '?';
const script = document.createElement('script');
script.src = remote + separator + 't=' + Date.now();
script.dataset.id = 'dev-inject-monitor-script';
script.defer = true;
// 防止重复注入
if (!document.querySelector('[data-id="dev-inject-monitor-script"]')) {
document.head.appendChild(script);
}
})();
</script>
<!-- DEV-INJECT-END -->
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
{
"name": "design-mode-demo",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-switch": "^1.1.1",
"clsx": "^2.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^2.5.5"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"tailwindcss": "^4.0.0",
"typescript": "~5.6.2",
"vite": "^7.2.6"
}
}

View File

@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -0,0 +1,64 @@
import React, { useState } from 'react';
import HomePage from './pages/HomePage';
import FeaturesPage from './pages/FeaturesPage';
import IframeDemoPage from './pages/IframeDemoPage';
function App() {
const [currentPage, setCurrentPage] = useState<'home' | 'features' | 'iframe'>('home');
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
{/* 导航栏 */}
<nav className="bg-white shadow-md">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-2xl font-bold text-indigo-600"></h1>
</div>
<div className="flex items-center space-x-4">
<button
onClick={() => setCurrentPage('home')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
currentPage === 'home'
? 'bg-indigo-600 text-white'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
</button>
<button
onClick={() => setCurrentPage('features')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
currentPage === 'features'
? 'bg-indigo-600 text-white'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
</button>
<button
onClick={() => setCurrentPage('iframe')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
currentPage === 'iframe'
? 'bg-indigo-600 text-white'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
Iframe
</button>
</div>
</div>
</div>
</nav>
{/* Page Content */}
<main>
{currentPage === 'home' && <HomePage />}
{currentPage === 'features' && <FeaturesPage />}
{currentPage === 'iframe' && <IframeDemoPage />}
</main>
</div>
);
}
export default App;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,612 @@
import React from '../../react';
import { useState, useEffect } from 'react';
import { Card, Button, Input, Select, Space, Row, Col, Divider, Tooltip, Badge } from 'antd';
import {
FontSizeOutlined,
BoldOutlined,
ItalicOutlined,
UnderlineOutlined,
AlignLeftOutlined,
AlignCenterOutlined,
AlignRightOutlined,
EditOutlined,
HistoryOutlined,
ClearOutlined,
SaveOutlined,
UndoOutlined,
EyeOutlined
} from '@ant-design/icons';
import type { ElementInfo, SourceInfo } from '../../../../packages/client-shared/src/messages';
const { TextArea } = Input;
const { Option } = Select;
export interface ContentUpdateData {
sourceInfo: SourceInfo;
newContent: string;
}
export interface ContentPanelProps {
selectedElement: ElementInfo | null;
onUpdateContent: (data: ContentUpdateData) => void;
currentContent: string;
onContentChange: (newContent: string) => void;
}
/**
* 内容编辑工具配置
*/
const contentTools = {
// 文本格式化
formatting: [
{ label: '加粗', value: '**', icon: <BoldOutlined />, description: '添加加粗格式' },
{ label: '斜体', value: '*', icon: <ItalicOutlined />, description: '添加斜体格式' },
{ label: '下划线', value: '__', icon: <UnderlineOutlined />, description: '添加下划线' }
],
// 对齐方式
alignment: [
{ label: '左对齐', value: 'text-left', icon: <AlignLeftOutlined /> },
{ label: '居中对齐', value: 'text-center', icon: <AlignCenterOutlined /> },
{ label: '右对齐', value: 'text-right', icon: <AlignRightOutlined /> }
],
// 文本样式预设
textStyles: [
{ label: '标题 1', value: 'text-4xl font-bold', preview: '大标题' },
{ label: '标题 2', value: 'text-3xl font-semibold', preview: '中标题' },
{ label: '标题 3', value: 'text-2xl font-medium', preview: '小标题' },
{ label: '正文', value: 'text-base', preview: '普通文本' },
{ label: '小字', value: 'text-sm text-gray-600', preview: '小字体' },
{ label: '说明文字', value: 'text-xs text-gray-500', preview: '说明文字' }
],
// 常用的占位符文本
placeholders: [
'请输入内容...',
'点击编辑文本',
'请输入标题',
'请输入描述',
'请输入按钮文本',
'请输入链接文本'
]
};
/**
* 内容历史记录
*/
interface ContentHistory {
id: string;
content: string;
timestamp: number;
description: string;
}
/**
* 内容编辑面板组件
*/
export const ContentPanel: React.FC<ContentPanelProps> = ({
selectedElement,
onUpdateContent,
currentContent,
onContentChange
}) => {
const [activeTab, setActiveTab] = useState<string>('edit');
const [editingContent, setEditingContent] = useState<string>(currentContent);
const [originalContent, setOriginalContent] = useState<string>(currentContent);
const [contentHistory, setContentHistory] = useState<ContentHistory[]>([]);
const [isPreviewMode, setIsPreviewMode] = useState<boolean>(false);
const [autoSave, setAutoSave] = useState<boolean>(true);
const [wordCount, setWordCount] = useState<number>(0);
const [characterCount, setCharacterCount] = useState<number>(0);
useEffect(() => {
setEditingContent(currentContent);
setOriginalContent(currentContent);
updateCounts(currentContent);
}, [currentContent]);
/**
* 更新字符和单词计数
*/
const updateCounts = (content: string) => {
setCharacterCount(content.length);
const words = content.trim().split(/\s+/).filter(word => word.length > 0);
setWordCount(words.length);
};
/**
* 处理内容变化
*/
const handleContentChange = (newContent: string) => {
setEditingContent(newContent);
onContentChange(newContent);
updateCounts(newContent);
// 自动保存历史记录
if (autoSave && newContent !== originalContent) {
addToHistory(newContent, '自动保存');
}
};
/**
* 添加到历史记录
*/
const addToHistory = (content: string, description: string) => {
const historyItem: ContentHistory = {
id: `history_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
content,
timestamp: Date.now(),
description
};
setContentHistory(prev => [historyItem, ...prev.slice(0, 9)]); // 保留最新10条记录
};
/**
* 恢复到历史记录
*/
const restoreFromHistory = (historyItem: ContentHistory) => {
setEditingContent(historyItem.content);
onContentChange(historyItem.content);
updateCounts(historyItem.content);
addToHistory(historyItem.content, '恢复历史记录');
};
/**
* 清除内容
*/
const clearContent = () => {
setEditingContent('');
onContentChange('');
updateCounts('');
addToHistory('', '清除内容');
};
/**
* 恢复原始内容
*/
const restoreOriginal = () => {
setEditingContent(originalContent);
onContentChange(originalContent);
updateCounts(originalContent);
};
/**
* 应用内容更新
*/
const applyChanges = () => {
if (!selectedElement) return;
onUpdateContent({
sourceInfo: selectedElement.sourceInfo,
newContent: editingContent
});
setOriginalContent(editingContent);
addToHistory(editingContent, '应用更改');
};
/**
* 插入格式化文本
*/
const insertFormatting = (format: string) => {
const textarea = document.querySelector('textarea[data-content-editor]') as HTMLTextAreaElement;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = editingContent.substring(start, end);
let newContent = editingContent;
let newSelectedText = selectedText;
// 根据格式化类型插入
switch (format) {
case '**':
newSelectedText = `**${selectedText}**`;
break;
case '*':
newSelectedText = `*${selectedText}*`;
break;
case '__':
newSelectedText = `__${selectedText}__`;
break;
}
newContent =
editingContent.substring(0, start) +
newSelectedText +
editingContent.substring(end);
handleContentChange(newContent);
// 重新设置光标位置
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(
start + (format === '**' || format === '__' ? 2 : 1),
start + (format === '**' || format === '__' ? 2 : 1) + selectedText.length
);
}, 0);
};
/**
* 插入占位符
*/
const insertPlaceholder = (placeholder: string) => {
const newContent = editingContent + placeholder;
handleContentChange(newContent);
};
/**
* 应用文本样式
*/
const applyTextStyle = (style: string) => {
// 这里可以应用内联样式或类名
console.log('Applying text style:', style);
};
/**
* 获取文本统计信息
*/
const getTextStats = () => {
const lines = editingContent.split('\n');
const paragraphs = editingContent.split('\n\n').filter(p => p.trim().length > 0);
return {
lines: lines.length,
paragraphs: paragraphs.length,
words: wordCount,
characters: characterCount,
charactersNoSpaces: editingContent.replace(/\s/g, '').length
};
};
if (!selectedElement) {
return (
<Card title="内容编辑" className="h-full">
<div className="flex items-center justify-center h-64 text-gray-500">
<div className="text-center">
<EditOutlined className="text-4xl mb-4" />
<p></p>
</div>
</div>
</Card>
);
}
// Check if element is editable (has static text)
if (selectedElement.isStaticText === false) {
return (
<Card title="内容编辑" className="h-full">
<div className="flex items-center justify-center h-64 text-orange-500">
<div className="text-center">
<EditOutlined className="text-4xl mb-4" />
<p className="font-semibold mb-2"></p>
<p className="text-sm text-gray-600"></p>
<p className="text-xs text-gray-500 mt-1"></p>
</div>
</div>
</Card>
);
}
const stats = getTextStats();
return (
<Card
title={
<div className="flex items-center gap-2">
<EditOutlined />
<span></span>
<Badge count={contentHistory.length} showZero />
</div>
}
className="h-full"
extra={
<Space>
<Tooltip title="预览模式">
<Button
size="small"
icon={isPreviewMode ? <EditOutlined /> : <EyeOutlined />}
onClick={() => setIsPreviewMode(!isPreviewMode)}
type={isPreviewMode ? 'primary' : 'default'}
>
{isPreviewMode ? '编辑' : '预览'}
</Button>
</Tooltip>
<Button size="small" onClick={clearContent} icon={<ClearOutlined />}>
</Button>
<Button
type="primary"
size="small"
onClick={applyChanges}
disabled={!selectedElement || editingContent === originalContent}
icon={<SaveOutlined />}
>
</Button>
</Space>
}
>
<div className="space-y-6">
{/* 元素信息 */}
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
<h4 className="text-sm font-semibold text-green-900 mb-2"></h4>
<div className="text-xs text-green-700 space-y-1">
<div><span className="font-medium">:</span> &lt;{selectedElement.tagName}&gt;</div>
<div><span className="font-medium">:</span> {selectedElement.sourceInfo.fileName.split('/').pop()}:{selectedElement.sourceInfo.lineNumber}</div>
<div><span className="font-medium">:</span> {originalContent.substring(0, 50)}{originalContent.length > 50 ? '...' : ''}</div>
</div>
</div>
{/* 标签页导航 */}
<div className="border-b border-gray-200">
<div className="flex space-x-1">
{[
{ key: 'edit', label: '编辑内容' },
{ key: 'format', label: '格式化' },
{ key: 'style', label: '文本样式' },
{ key: 'history', label: '历史记录' },
{ key: 'stats', label: '统计信息' }
].map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${activeTab === tab.key
? 'border-green-500 text-green-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 编辑内容 */}
{activeTab === 'edit' && (
<div className="space-y-4">
{isPreviewMode ? (
// 预览模式
<div className="border border-gray-200 rounded-lg p-4 min-h-32 bg-white">
<div dangerouslySetInnerHTML={{ __html: editingContent.replace(/\n/g, '<br>') }} />
</div>
) : (
// 编辑模式
<div>
<TextArea
data-content-editor
value={editingContent}
onChange={(e) => handleContentChange(e.target.value)}
placeholder="输入或编辑文本内容..."
rows={8}
className="font-mono text-sm"
/>
{/* 快速工具栏 */}
<div className="flex items-center justify-between mt-2 pt-2 border-t border-gray-100">
<Space size="small">
<Tooltip title="撤销">
<Button
size="small"
icon={<UndoOutlined />}
disabled={contentHistory.length === 0}
onClick={() => contentHistory.length > 0 && restoreFromHistory(contentHistory[0])}
/>
</Tooltip>
<Divider type="vertical" />
{contentTools.formatting.map((format, index) => (
<Tooltip key={index} title={format.description}>
<Button
size="small"
icon={format.icon}
onClick={() => insertFormatting(format.value)}
/>
</Tooltip>
))}
</Space>
<span className="text-xs text-gray-500">
{wordCount} | {characterCount}
</span>
</div>
</div>
)}
</div>
)}
{/* 格式化工具 */}
{activeTab === 'format' && (
<div className="space-y-6">
{/* 快速格式化 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="grid grid-cols-3 gap-2">
{contentTools.formatting.map((format, index) => (
<Button
key={index}
icon={format.icon}
onClick={() => insertFormatting(format.value)}
className="flex flex-col items-center p-3 h-auto"
>
<span className="text-xs">{format.label}</span>
<span className="text-xs text-gray-500 mt-1">{format.description}</span>
</Button>
))}
</div>
</div>
{/* 占位符 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="space-y-2">
{contentTools.placeholders.map((placeholder, index) => (
<Button
key={index}
size="small"
onClick={() => insertPlaceholder(placeholder)}
className="w-full text-left justify-start"
>
{placeholder}
</Button>
))}
</div>
</div>
{/* 对齐方式 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="grid grid-cols-3 gap-2">
{contentTools.alignment.map((align, index) => (
<Button
key={index}
icon={align.icon}
onClick={() => applyTextStyle(align.value)}
className="flex items-center justify-center"
>
{align.label}
</Button>
))}
</div>
</div>
</div>
)}
{/* 文本样式 */}
{activeTab === 'style' && (
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="space-y-2">
{contentTools.textStyles.map((style, index) => (
<button
key={index}
onClick={() => applyTextStyle(style.value)}
className="w-full p-3 text-left border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
>
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{style.label}</span>
<span className="text-xs text-gray-500">{style.value}</span>
</div>
<div className={`mt-1 ${style.value}`}>
{style.preview}
</div>
</button>
))}
</div>
</div>
)}
{/* 历史记录 */}
{activeTab === 'history' && (
<div>
<div className="flex items-center justify-between mb-4">
<h4 className="text-sm font-medium text-gray-900"></h4>
<Button
size="small"
icon={<HistoryOutlined />}
onClick={() => setContentHistory([])}
>
</Button>
</div>
{contentHistory.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<HistoryOutlined className="text-2xl mb-2" />
<p></p>
</div>
) : (
<div className="space-y-2 max-h-64 overflow-y-auto">
{contentHistory.map((item, index) => (
<div
key={item.id}
className="p-3 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors cursor-pointer"
onClick={() => restoreFromHistory(item)}
>
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-medium text-gray-900">
{new Date(item.timestamp).toLocaleTimeString()}
</span>
<span className="text-xs text-gray-500">{item.description}</span>
</div>
<div className="text-xs text-gray-600 truncate">
{item.content.substring(0, 60)}{item.content.length > 60 ? '...' : ''}
</div>
</div>
))}
</div>
)}
</div>
)}
{/* 统计信息 */}
{activeTab === 'stats' && (
<div>
<h4 className="text-sm font-medium text-gray-900 mb-4"></h4>
<div className="grid grid-cols-2 gap-4">
<div className="bg-gray-50 p-3 rounded-lg">
<div className="text-2xl font-bold text-gray-900">{stats.words}</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="bg-gray-50 p-3 rounded-lg">
<div className="text-2xl font-bold text-gray-900">{stats.characters}</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="bg-gray-50 p-3 rounded-lg">
<div className="text-2xl font-bold text-gray-900">{stats.charactersNoSpaces}</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="bg-gray-50 p-3 rounded-lg">
<div className="text-2xl font-bold text-gray-900">{stats.lines}</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="bg-gray-50 p-3 rounded-lg">
<div className="text-2xl font-bold text-gray-900">{stats.paragraphs}</div>
<div className="text-sm text-gray-600"></div>
</div>
</div>
{/* 阅读时间估算 */}
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<div className="text-sm text-blue-900">
<span className="font-medium">:</span> {Math.ceil(stats.words / 200)}
</div>
</div>
</div>
)}
{/* 自动保存设置 */}
<div className="pt-4 border-t border-gray-200">
<Row align="middle" gutter={16}>
<Col span={12}>
<label className="flex items-center gap-2 text-sm text-gray-700">
<input
type="checkbox"
checked={autoSave}
onChange={(e) => setAutoSave(e.target.checked)}
className="rounded"
/>
</label>
</Col>
<Col span={12}>
<Button
size="small"
onClick={restoreOriginal}
disabled={editingContent === originalContent}
block
>
</Button>
</Col>
</Row>
</div>
</div>
</Card>
);
};
export default ContentPanel;

View File

@@ -0,0 +1,856 @@
import React from '../../react';
import { useState, useEffect, useCallback } from 'react';
import {
Card,
Tabs,
Button,
Input,
Select,
Space,
Row,
Col,
Divider,
Switch,
Slider,
InputNumber,
Upload,
Modal,
Form,
List,
Tag,
ColorPicker,
Tooltip,
Popconfirm,
message,
ConfigProvider,
theme
} from 'antd';
import {
SettingOutlined,
SaveOutlined,
UploadOutlined,
DownloadOutlined,
PlusOutlined,
DeleteOutlined,
EditOutlined,
ReloadOutlined,
BulbOutlined,
EyeOutlined,
ToolOutlined,
FontSizeOutlined,
BgColorsOutlined,
BorderOutlined,
LayoutOutlined,
ThunderboltOutlined,
UserOutlined,
HistoryOutlined,
StarOutlined
} from '@ant-design/icons';
import type { ElementInfo, SourceInfo } from '../../../../packages/client-shared/src/messages';
import StylePanel from './StylePanel';
import ContentPanel from './ContentPanel';
const { TabPane } = Tabs;
const { Option } = Select;
const { TextArea } = Input;
export interface PropertyPanelConfig {
theme: 'light' | 'dark' | 'auto';
autoSave: boolean;
autoSaveInterval: number;
showElementInfo: boolean;
showTooltips: boolean;
enableKeyboardShortcuts: boolean;
defaultPanel: 'style' | 'content' | 'settings';
panelLayout: 'horizontal' | 'vertical';
colorPalette: string[];
customPresets: CustomPreset[];
shortcuts: ShortcutConfig;
}
export interface CustomPreset {
id: string;
name: string;
type: 'style' | 'content';
value: string;
description: string;
tags: string[];
favorite: boolean;
createdAt: number;
}
export interface ShortcutConfig {
save: string;
undo: string;
redo: string;
togglePanel: string;
clearAll: string;
custom1?: string;
custom2?: string;
custom3?: string;
}
export interface PropertyPanelProps {
selectedElement: ElementInfo | null;
currentStyle: string;
currentContent: string;
onStyleUpdate: (data: { sourceInfo: SourceInfo; newClass: string }) => void;
onContentUpdate: (data: { sourceInfo: SourceInfo; newContent: string }) => void;
onStyleChange: (newStyle: string) => void;
onContentChange: (newContent: string) => void;
config?: Partial<PropertyPanelConfig>;
onConfigChange?: (config: PropertyPanelConfig) => void;
}
/**
* 默认配置
*/
const defaultConfig: PropertyPanelConfig = {
theme: 'light',
autoSave: true,
autoSaveInterval: 3000,
showElementInfo: true,
showTooltips: true,
enableKeyboardShortcuts: true,
defaultPanel: 'style',
panelLayout: 'horizontal',
colorPalette: [
'#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff', '#ffff00',
'#ff00ff', '#00ffff', '#ffa500', '#800080', '#008000', '#000080'
],
customPresets: [],
shortcuts: {
save: 'Ctrl+S',
undo: 'Ctrl+Z',
redo: 'Ctrl+Y',
togglePanel: 'F2',
clearAll: 'Ctrl+Delete'
}
};
/**
* 预设管理器
*/
class PresetManager {
private storageKey = 'appdev_property_panel_presets';
getPresets(): CustomPreset[] {
try {
const stored = localStorage.getItem(this.storageKey);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
}
savePreset(preset: CustomPreset): void {
const presets = this.getPresets();
const existingIndex = presets.findIndex(p => p.id === preset.id);
if (existingIndex >= 0) {
presets[existingIndex] = { ...preset, createdAt: Date.now() };
} else {
presets.push({ ...preset, id: `preset_${Date.now()}`, createdAt: Date.now() });
}
localStorage.setItem(this.storageKey, JSON.stringify(presets));
}
deletePreset(presetId: string): void {
const presets = this.getPresets().filter(p => p.id !== presetId);
localStorage.setItem(this.storageKey, JSON.stringify(presets));
}
exportPresets(): string {
return JSON.stringify(this.getPresets(), null, 2);
}
importPresets(jsonData: string): void {
try {
const presets: CustomPreset[] = JSON.parse(jsonData);
if (Array.isArray(presets)) {
localStorage.setItem(this.storageKey, JSON.stringify(presets));
}
} catch (error) {
throw new Error('无效的预设数据格式');
}
}
}
/**
* 配置管理器
*/
class ConfigManager {
private storageKey = 'appdev_property_panel_config';
getConfig(): PropertyPanelConfig {
try {
const stored = localStorage.getItem(this.storageKey);
return stored ? { ...defaultConfig, ...JSON.parse(stored) } : defaultConfig;
} catch {
return defaultConfig;
}
}
saveConfig(config: PropertyPanelConfig): void {
localStorage.setItem(this.storageKey, JSON.stringify(config));
}
resetConfig(): PropertyPanelConfig {
localStorage.removeItem(this.storageKey);
return defaultConfig;
}
}
/**
* 快捷键管理器
*/
class ShortcutManager {
private listeners: Map<string, () => void> = new Map();
register(shortcut: string, callback: () => void): void {
this.listeners.set(shortcut, callback);
}
handleKeydown(event: KeyboardEvent): void {
const shortcuts = Array.from(this.listeners.keys());
for (const shortcut of shortcuts) {
if (this.matchShortcut(event, shortcut)) {
event.preventDefault();
this.listeners.get(shortcut)?.();
break;
}
}
}
private matchShortcut(event: KeyboardEvent, shortcut: string): boolean {
const keys = shortcut.toLowerCase().split('+');
const eventKey = event.key.toLowerCase();
const ctrl = event.ctrlKey || event.metaKey;
const shift = event.shiftKey;
const alt = event.altKey;
// 检查修饰键
if (keys.includes('ctrl') && !ctrl) return false;
if (keys.includes('shift') && !shift) return false;
if (keys.includes('alt') && !alt) return false;
// 检查主键
const mainKey = keys[keys.length - 1];
if (mainKey !== eventKey) return false;
return true;
}
}
const presetManager = new PresetManager();
const configManager = new ConfigManager();
const shortcutManager = new ShortcutManager();
/**
* 可自定义的属性面板组件
*/
export const PropertyPanel: React.FC<PropertyPanelProps> = ({
selectedElement,
currentStyle,
currentContent,
onStyleUpdate,
onContentUpdate,
onStyleChange,
onContentChange,
config: userConfig,
onConfigChange
}) => {
const [config, setConfig] = useState<PropertyPanelConfig>(() => ({
...defaultConfig,
...userConfig
}));
const [activeTab, setActiveTab] = useState(config.defaultPanel);
const [presets, setPresets] = useState<CustomPreset[]>(() => presetManager.getPresets());
const [isPresetModalVisible, setIsPresetModalVisible] = useState(false);
const [isConfigModalVisible, setIsConfigModalVisible] = useState(false);
const [newPreset, setNewPreset] = useState<Partial<CustomPreset>>({});
const [messageApi, contextHolder] = message.useMessage();
/**
* 保存配置
*/
const saveConfig = useCallback((newConfig: PropertyPanelConfig) => {
const finalConfig = { ...config, ...newConfig };
setConfig(finalConfig);
configManager.saveConfig(finalConfig);
onConfigChange?.(finalConfig);
messageApi.success('配置已保存');
}, [config, onConfigChange, messageApi]);
/**
* 保存预设
*/
const savePreset = useCallback((preset: CustomPreset) => {
presetManager.savePreset(preset);
const updatedPresets = presetManager.getPresets();
setPresets(updatedPresets);
messageApi.success(`预设 "${preset.name}" 已保存`);
}, [messageApi]);
/**
* 删除预设
*/
const deletePreset = useCallback((presetId: string) => {
presetManager.deletePreset(presetId);
const updatedPresets = presetManager.getPresets();
setPresets(updatedPresets);
messageApi.success('预设已删除');
}, [messageApi]);
/**
* 导出配置
*/
const exportConfig = useCallback(() => {
const exportData = {
config,
presets: presetManager.exportPresets()
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `property-panel-config-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
messageApi.success('配置已导出');
}, [config, messageApi]);
/**
* 导入配置
*/
const importConfig = useCallback((file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const importData = JSON.parse(e.target?.result as string);
if (importData.config) {
saveConfig(importData.config);
}
if (importData.presets) {
presetManager.importPresets(importData.presets);
setPresets(presetManager.getPresets());
}
messageApi.success('配置已导入');
} catch (error) {
messageApi.error('导入失败:无效的配置文件');
}
};
reader.readAsText(file);
}, [saveConfig, messageApi]);
/**
* 处理快捷键
*/
useEffect(() => {
if (!config.enableKeyboardShortcuts) return;
const handleKeydown = (event: KeyboardEvent) => {
shortcutManager.handleKeydown(event);
};
// 注册快捷键
shortcutManager.register(config.shortcuts.save, () => {
// 这里可以添加保存逻辑
console.log('快捷键:保存');
});
shortcutManager.register(config.shortcuts.togglePanel, () => {
setActiveTab(prev => prev === 'style' ? 'content' : prev === 'content' ? 'settings' : 'style');
});
document.addEventListener('keydown', handleKeydown);
return () => document.removeEventListener('keydown', handleKeydown);
}, [config.enableKeyboardShortcuts, config.shortcuts]);
/**
* 创建预设
*/
const handleCreatePreset = () => {
if (!newPreset.name || !newPreset.value) {
messageApi.error('请填写预设名称和值');
return;
}
const preset: CustomPreset = {
id: `preset_${Date.now()}`,
name: newPreset.name!,
type: newPreset.type!,
value: newPreset.value!,
description: newPreset.description || '',
tags: newPreset.tags || [],
favorite: false,
createdAt: Date.now()
};
savePreset(preset);
setIsPresetModalVisible(false);
setNewPreset({});
};
/**
* 获取主题配置
*/
const getThemeConfig = () => {
const theme = config.theme === 'auto'
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: config.theme;
return {
algorithm: theme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm,
token: {
colorPrimary: '#3b82f6',
borderRadius: 6,
}
};
};
/**
* 获取快速操作组件
*/
const QuickActions = () => (
<Card size="small" title="快速操作" className="mb-4">
<Row gutter={[8, 8]}>
<Col span={6}>
<Tooltip title="保存当前设置">
<Button size="small" icon={<SaveOutlined />} block />
</Tooltip>
</Col>
<Col span={6}>
<Tooltip title="恢复默认">
<Button size="small" icon={<ReloadOutlined />} block />
</Tooltip>
</Col>
<Col span={6}>
<Tooltip title="导出配置">
<Button size="small" icon={<DownloadOutlined />} onClick={exportConfig} block />
</Tooltip>
</Col>
<Col span={6}>
<Tooltip title="导入配置">
<Upload
size="small"
showUploadList={false}
beforeUpload={(file) => {
importConfig(file);
return false;
}}
>
<Button size="small" icon={<UploadOutlined />} block />
</Upload>
</Tooltip>
</Col>
</Row>
</Card>
);
/**
* 获取元素信息组件
*/
const ElementInfo = () => (
selectedElement && config.showElementInfo ? (
<Card size="small" title="元素信息" className="mb-4">
<div className="space-y-2 text-sm">
<div><span className="font-medium">:</span> &lt;{selectedElement.tagName}&gt;</div>
<div><span className="font-medium">:</span> {selectedElement.className || '无'}</div>
<div><span className="font-medium">:</span> {selectedElement.sourceInfo.fileName.split('/').pop()}</div>
<div><span className="font-medium">:</span> {selectedElement.sourceInfo.lineNumber}:{selectedElement.sourceInfo.columnNumber}</div>
<div><span className="font-medium">:</span> {selectedElement.textContent.substring(0, 30)}{selectedElement.textContent.length > 30 ? '...' : ''}</div>
</div>
</Card>
) : null
);
/**
* 获取自定义预设组件
*/
const CustomPresets = () => (
<Card
size="small"
title={
<div className="flex items-center justify-between">
<span></span>
<Button
size="small"
icon={<PlusOutlined />}
onClick={() => setIsPresetModalVisible(true)}
>
</Button>
</div>
}
>
<List
size="small"
dataSource={presets}
renderItem={(preset) => (
<List.Item
actions={[
<Tooltip title="应用预设">
<Button
size="small"
icon={<StarOutlined />}
onClick={() => {
if (preset.type === 'style') {
onStyleChange(preset.value);
} else {
onContentChange(preset.value);
}
}}
/>
</Tooltip>,
<Popconfirm
title="确定要删除这个预设吗?"
onConfirm={() => deletePreset(preset.id)}
>
<Button size="small" icon={<DeleteOutlined />} danger />
</Popconfirm>
]}
>
<List.Item.Meta
title={<span className="text-sm">{preset.name}</span>}
description={
<div className="text-xs">
<div>{preset.value}</div>
<div className="mt-1">
{preset.tags.map(tag => (
<Tag key={tag} size="small">{tag}</Tag>
))}
</div>
</div>
}
/>
</List.Item>
)}
/>
</Card>
);
return (
<ConfigProvider theme={getThemeConfig()}>
{contextHolder}
<div className="h-full flex flex-col" style={{ background: 'transparent' }}>
{/* 顶部工具栏 */}
<div className="p-4 border-b border-gray-200 bg-white">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<SettingOutlined />
<span className="font-semibold"></span>
{selectedElement && (
<Tag color="blue">{selectedElement.tagName}</Tag>
)}
</div>
<Space>
<Tooltip title="面板设置">
<Button
size="small"
icon={<ToolOutlined />}
onClick={() => setIsConfigModalVisible(true)}
/>
</Tooltip>
<Tooltip title="主题切换">
<Button
size="small"
icon={config.theme === 'dark' ? <BulbOutlined /> : <EyeOutlined />}
onClick={() => saveConfig({
theme: config.theme === 'light' ? 'dark' : config.theme === 'dark' ? 'auto' : 'light'
})}
/>
</Tooltip>
</Space>
</div>
</div>
{/* 主体内容 */}
<div className="flex-1 overflow-hidden">
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
size="small"
className="h-full flex flex-col"
>
<TabPane tab="样式" key="style" icon={<BgColorsOutlined />}>
<div className="h-full overflow-y-auto p-4 space-y-4">
<QuickActions />
<ElementInfo />
<StylePanel
selectedElement={selectedElement}
onUpdateStyle={onStyleUpdate}
currentClass={currentStyle}
onClassChange={onStyleChange}
/>
<CustomPresets />
</div>
</TabPane>
<TabPane tab="内容" key="content" icon={<EditOutlined />}>
<div className="h-full overflow-y-auto p-4 space-y-4">
<QuickActions />
<ElementInfo />
<ContentPanel
selectedElement={selectedElement}
onUpdateContent={onContentUpdate}
currentContent={currentContent}
onContentChange={onContentChange}
/>
</div>
</TabPane>
<TabPane tab="设置" key="settings" icon={<SettingOutlined />}>
<div className="h-full overflow-y-auto p-4 space-y-4">
<Card size="small" title="面板设置">
<Form layout="vertical">
<Form.Item label="主题">
<Select
value={config.theme}
onChange={(theme) => saveConfig({ theme })}
>
<Option value="light"></Option>
<Option value="dark"></Option>
<Option value="auto"></Option>
</Select>
</Form.Item>
<Form.Item label="默认面板">
<Select
value={config.defaultPanel}
onChange={(defaultPanel) => saveConfig({ defaultPanel })}
>
<Option value="style"></Option>
<Option value="content"></Option>
<Option value="settings"></Option>
</Select>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="自动保存">
<Switch
checked={config.autoSave}
onChange={(autoSave) => saveConfig({ autoSave })}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="自动保存间隔 (ms)">
<InputNumber
min={1000}
max={10000}
step={500}
value={config.autoSaveInterval}
onChange={(autoSaveInterval) => saveConfig({ autoSaveInterval })}
style={{ width: '100%' }}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="显示元素信息">
<Switch
checked={config.showElementInfo}
onChange={(showElementInfo) => saveConfig({ showElementInfo })}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="启用工具提示">
<Switch
checked={config.showTooltips}
onChange={(showTooltips) => saveConfig({ showTooltips })}
/>
</Form.Item>
</Col>
</Row>
<Form.Item label="快捷键设置">
<Space direction="vertical" style={{ width: '100%' }}>
<Input
addonBefore="保存"
value={config.shortcuts.save}
onChange={(e) => saveConfig({
shortcuts: { ...config.shortcuts, save: e.target.value }
})}
/>
<Input
addonBefore="撤销"
value={config.shortcuts.undo}
onChange={(e) => saveConfig({
shortcuts: { ...config.shortcuts, undo: e.target.value }
})}
/>
<Input
addonBefore="重做"
value={config.shortcuts.redo}
onChange={(e) => saveConfig({
shortcuts: { ...config.shortcuts, redo: e.target.value }
})}
/>
<Input
addonBefore="切换面板"
value={config.shortcuts.togglePanel}
onChange={(e) => saveConfig({
shortcuts: { ...config.shortcuts, togglePanel: e.target.value }
})}
/>
</Space>
</Form.Item>
</Form>
</Card>
<Card size="small" title="颜色配置">
<div className="space-y-3">
<div>
<label className="block text-sm font-medium mb-2"></label>
<div className="flex flex-wrap gap-2">
{config.colorPalette.map((color, index) => (
<div
key={index}
className="w-8 h-8 rounded border border-gray-300 cursor-pointer hover:scale-110 transition-transform"
style={{ backgroundColor: color }}
onClick={() => {
// 移除颜色
const newPalette = config.colorPalette.filter((_, i) => i !== index);
saveConfig({ colorPalette: newPalette });
}}
/>
))}
<Button
size="small"
icon={<PlusOutlined />}
onClick={() => {
// 添加颜色
const newColor = '#' + Math.floor(Math.random()*16777215).toString(16);
saveConfig({ colorPalette: [...config.colorPalette, newColor] });
}}
>
</Button>
</div>
</div>
</div>
</Card>
<Card size="small" title="数据管理">
<Row gutter={8}>
<Col span={8}>
<Button icon={<DownloadOutlined />} onClick={exportConfig} block>
</Button>
</Col>
<Col span={8}>
<Upload
beforeUpload={(file) => {
importConfig(file);
return false;
}}
showUploadList={false}
>
<Button icon={<UploadOutlined />} block>
</Button>
</Upload>
</Col>
<Col span={8}>
<Popconfirm
title="确定要重置所有配置吗?"
onConfirm={() => {
const resetConfig = configManager.resetConfig();
setConfig(resetConfig);
setPresets(presetManager.getPresets());
messageApi.success('配置已重置');
}}
>
<Button icon={<ReloadOutlined />} danger block>
</Button>
</Popconfirm>
</Col>
</Row>
</Card>
</div>
</TabPane>
</Tabs>
</div>
{/* 创建预设模态框 */}
<Modal
title="创建自定义预设"
open={isPresetModalVisible}
onOk={handleCreatePreset}
onCancel={() => {
setIsPresetModalVisible(false);
setNewPreset({});
}}
okText="创建"
cancelText="取消"
>
<Form layout="vertical">
<Form.Item label="预设名称" required>
<Input
value={newPreset.name}
onChange={(e) => setNewPreset({ ...newPreset, name: e.target.value })}
placeholder="输入预设名称"
/>
</Form.Item>
<Form.Item label="预设类型" required>
<Select
value={newPreset.type}
onChange={(type) => setNewPreset({ ...newPreset, type })}
placeholder="选择预设类型"
>
<Option value="style"></Option>
<Option value="content"></Option>
</Select>
</Form.Item>
<Form.Item label="预设值" required>
<TextArea
value={newPreset.value}
onChange={(e) => setNewPreset({ ...newPreset, value: e.target.value })}
placeholder="输入预设值"
rows={3}
/>
</Form.Item>
<Form.Item label="描述">
<TextArea
value={newPreset.description}
onChange={(e) => setNewPreset({ ...newPreset, description: e.target.value })}
placeholder="输入预设描述"
rows={2}
/>
</Form.Item>
<Form.Item label="标签">
<Input
value={newPreset.tags?.join(',')}
onChange={(e) => setNewPreset({
...newPreset,
tags: e.target.value.split(',').map(tag => tag.trim()).filter(Boolean)
})}
placeholder="输入标签,用逗号分隔"
/>
</Form.Item>
</Form>
</Modal>
</div>
</ConfigProvider>
);
};
export default PropertyPanel;

View File

@@ -0,0 +1,567 @@
import React from '../../react';
import { useState, useEffect } from 'react';
import { Card, Button, Input, Select, Slider, ColorPicker, InputNumber, Divider, Tag, Space, Row, Col } from 'antd';
import { BoldOutlined, ItalicOutlined, UnderlineOutlined, FontSizeOutlined, BgColorsOutlined, BorderOutlined } from '@ant-design/icons';
import type { ElementInfo, SourceInfo } from '../../../../packages/client-shared/src/messages';
const { TextArea } = Input;
const { Option } = Select;
export interface StyleUpdateData {
sourceInfo: SourceInfo;
newClass: string;
}
export interface StylePanelProps {
selectedElement: ElementInfo | null;
onUpdateStyle: (data: StyleUpdateData) => void;
currentClass: string;
onClassChange: (newClass: string) => void;
}
/**
* 样式预设配置
*/
const stylePresets = {
colors: {
backgrounds: [
{ label: '白色', value: 'bg-white', color: '#ffffff' },
{ label: '浅灰色', value: 'bg-gray-100', color: '#f3f4f6' },
{ label: '浅蓝色', value: 'bg-blue-100', color: '#dbeafe' },
{ label: '浅绿色', value: 'bg-green-100', color: '#dcfce7' },
{ label: '浅红色', value: 'bg-red-100', color: '#fee2e2' },
{ label: '浅黄色', value: 'bg-yellow-100', color: '#fef3c7' },
{ label: '浅紫色', value: 'bg-purple-100', color: '#f3e8ff' },
{ label: '深蓝色', value: 'bg-blue-600', color: '#2563eb' },
],
text: [
{ label: '黑色', value: 'text-black', color: '#000000' },
{ label: '深灰色', value: 'text-gray-900', color: '#111827' },
{ label: '中灰色', value: 'text-gray-600', color: '#4b5563' },
{ label: '蓝色', value: 'text-blue-600', color: '#2563eb' },
{ label: '绿色', value: 'text-green-600', color: '#16a34a' },
{ label: '红色', value: 'text-red-600', color: '#dc2626' },
{ label: '黄色', value: 'text-yellow-600', color: '#ca8a04' },
{ label: '紫色', value: 'text-purple-600', color: '#9333ea' },
]
},
spacing: {
padding: [
{ label: '无内边距', value: 'p-0', preview: 'padding: 0px' },
{ label: '小内边距', value: 'p-2', preview: 'padding: 8px' },
{ label: '中等内边距', value: 'p-4', preview: 'padding: 16px' },
{ label: '大内边距', value: 'p-6', preview: 'padding: 24px' },
{ label: '超大内边距', value: 'p-8', preview: 'padding: 32px' },
],
margin: [
{ label: '无外边距', value: 'm-0', preview: 'margin: 0px' },
{ label: '小外边距', value: 'm-2', preview: 'margin: 8px' },
{ label: '中等外边距', value: 'm-4', preview: 'margin: 16px' },
{ label: '大外边距', value: 'm-6', preview: 'margin: 24px' },
]
},
border: {
radius: [
{ label: '无圆角', value: 'rounded-none', preview: 'border-radius: 0px' },
{ label: '小圆角', value: 'rounded-sm', preview: 'border-radius: 2px' },
{ label: '中等圆角', value: 'rounded-md', preview: 'border-radius: 6px' },
{ label: '大圆角', value: 'rounded-lg', preview: 'border-radius: 8px' },
{ label: '超大圆角', value: 'rounded-xl', preview: 'border-radius: 12px' },
{ label: '完全圆角', value: 'rounded-full', preview: 'border-radius: 9999px' },
],
width: [
{ label: '无边框', value: 'border-0', preview: 'border-width: 0px' },
{ label: '细边框', value: 'border', preview: 'border-width: 1px' },
{ label: '中等边框', value: 'border-2', preview: 'border-width: 2px' },
{ label: '粗边框', value: 'border-4', preview: 'border-width: 4px' },
]
},
typography: {
fontSize: [
{ label: '很小', value: 'text-xs', preview: 'font-size: 12px' },
{ label: '小', value: 'text-sm', preview: 'font-size: 14px' },
{ label: '正常', value: 'text-base', preview: 'font-size: 16px' },
{ label: '大', value: 'text-lg', preview: 'font-size: 18px' },
{ label: '很大', value: 'text-xl', preview: 'font-size: 20px' },
{ label: '巨大', value: 'text-2xl', preview: 'font-size: 24px' },
],
fontWeight: [
{ label: '细体', value: 'font-thin', preview: 'font-weight: 100' },
{ label: '特细', value: 'font-extralight', preview: 'font-weight: 200' },
{ label: '细体', value: 'font-light', preview: 'font-weight: 300' },
{ label: '正常', value: 'font-normal', preview: 'font-weight: 400' },
{ label: '中粗', value: 'font-medium', preview: 'font-weight: 500' },
{ label: '半粗', value: 'font-semibold', preview: 'font-weight: 600' },
{ label: '粗体', value: 'font-bold', preview: 'font-weight: 700' },
{ label: '超粗', value: 'font-black', preview: 'font-weight: 900' },
]
},
layout: {
display: [
{ label: '块级', value: 'block', preview: 'display: block' },
{ label: '内联', value: 'inline', preview: 'display: inline' },
{ label: '内联块', value: 'inline-block', preview: 'display: inline-block' },
{ label: '弹性', value: 'flex', preview: 'display: flex' },
{ label: '网格', value: 'grid', preview: 'display: grid' },
{ label: '隐藏', value: 'hidden', preview: 'display: none' },
],
position: [
{ label: '静态', value: 'static', preview: 'position: static' },
{ label: '相对', value: 'relative', preview: 'position: relative' },
{ label: '绝对', value: 'absolute', preview: 'position: absolute' },
{ label: '固定', value: 'fixed', preview: 'position: fixed' },
{ label: '粘性', value: 'sticky', preview: 'position: sticky' },
]
}
};
/**
* 样式编辑面板组件
*/
export const StylePanel: React.FC<StylePanelProps> = ({
selectedElement,
onUpdateStyle,
currentClass,
onClassChange
}) => {
const [activeTab, setActiveTab] = useState<string>('presets');
const [customStyles, setCustomStyles] = useState<string>(currentClass);
const [colorPickerVisible, setColorPickerVisible] = useState(false);
const [customColor, setCustomColor] = useState('#3b82f6');
useEffect(() => {
setCustomStyles(currentClass);
}, [currentClass]);
/**
* 添加样式类
*/
const addStyleClass = (className: string) => {
const newStyles = mergeClasses(customStyles, className);
setCustomStyles(newStyles);
onClassChange(newStyles);
};
/**
* 移除样式类
*/
const removeStyleClass = (className: string) => {
const newStyles = removeClass(customStyles, className);
setCustomStyles(newStyles);
onClassChange(newStyles);
};
/**
* 更新样式
*/
const updateStyles = () => {
if (!selectedElement) return;
onUpdateStyle({
sourceInfo: selectedElement.sourceInfo,
newClass: customStyles
});
};
/**
* 清除所有样式
*/
const clearAllStyles = () => {
setCustomStyles('');
onClassChange('');
};
/**
* 合并样式类
*/
const mergeClasses = (existing: string, newClass: string): string => {
const classes = existing.split(' ').filter(Boolean);
const newClasses = newClass.split(' ').filter(Boolean);
// 移除冲突的同类样式
const filteredClasses = classes.filter(cls => {
return !newClasses.some(newCls =>
(cls.startsWith('bg-') && newCls.startsWith('bg-')) ||
(cls.startsWith('text-') && newCls.startsWith('text-')) ||
(cls.startsWith('p-') && newCls.startsWith('p-')) ||
(cls.startsWith('m-') && newCls.startsWith('m-')) ||
(cls.startsWith('rounded') && newCls.startsWith('rounded')) ||
(cls.startsWith('border-') && newCls.startsWith('border-')) ||
(cls.startsWith('text-') && newCls.startsWith('text-')) ||
(cls.startsWith('font-') && newCls.startsWith('font-')) ||
(cls.startsWith('flex') && newCls.startsWith('flex')) ||
(cls.startsWith('grid') && newCls.startsWith('grid')) ||
cls === newCls
);
});
return [...filteredClasses, ...newClasses].join(' ').trim();
};
/**
* 移除样式类
*/
const removeClass = (existing: string, classToRemove: string): string => {
return existing.split(' ')
.filter(cls => cls !== classToRemove && !cls.startsWith(classToRemove.split('-')[0]))
.join(' ')
.trim();
};
/**
* 获取当前应用的样式类
*/
const getAppliedStyles = (): string[] => {
return customStyles.split(' ').filter(Boolean);
};
if (!selectedElement) {
return (
<Card title="样式编辑" className="h-full">
<div className="flex items-center justify-center h-64 text-gray-500">
<div className="text-center">
<BgColorsOutlined className="text-4xl mb-4" />
<p></p>
</div>
</div>
</Card>
);
}
return (
<Card
title={
<div className="flex items-center gap-2">
<BgColorsOutlined />
<span></span>
</div>
}
className="h-full"
extra={
<Space>
<Button size="small" onClick={clearAllStyles}>
</Button>
<Button
type="primary"
size="small"
onClick={updateStyles}
disabled={!selectedElement}
>
</Button>
</Space>
}
>
<div className="space-y-6">
{/* 元素信息 */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<h4 className="text-sm font-semibold text-blue-900 mb-2"></h4>
<div className="text-xs text-blue-700 space-y-1">
<div><span className="font-medium">:</span> &lt;{selectedElement.tagName}&gt;</div>
<div><span className="font-medium">:</span> {selectedElement.sourceInfo.fileName.split('/').pop()}:{selectedElement.sourceInfo.lineNumber}</div>
</div>
</div>
{/* 快速预设标签页 */}
<div className="border-b border-gray-200">
<div className="flex space-x-1">
{[
{ key: 'presets', label: '预设样式' },
{ key: 'colors', label: '颜色' },
{ key: 'typography', label: '字体' },
{ key: 'layout', label: '布局' },
{ key: 'custom', label: '自定义' }
].map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.key
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
{/* 预设样式 */}
{activeTab === 'presets' && (
<div className="space-y-6">
{/* 背景颜色 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="grid grid-cols-2 gap-2">
{stylePresets.colors.backgrounds.map((color, index) => (
<button
key={index}
onClick={() => addStyleClass(color.value)}
className="flex items-center gap-2 p-2 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
>
<div
className={`w-4 h-4 rounded ${color.value}`}
style={{ backgroundColor: color.color }}
/>
<span className="text-xs">{color.label}</span>
</button>
))}
</div>
</div>
{/* 圆角 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="grid grid-cols-2 gap-2">
{stylePresets.border.radius.map((radius, index) => (
<button
key={index}
onClick={() => addStyleClass(radius.value)}
className="p-2 text-xs border border-gray-200 rounded-lg hover:border-gray-300 transition-colors text-left"
>
<div className="font-medium">{radius.label}</div>
<div className="text-gray-500">{radius.preview}</div>
</button>
))}
</div>
</div>
{/* 边距 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="grid grid-cols-2 gap-2">
{stylePresets.spacing.padding.map((padding, index) => (
<button
key={index}
onClick={() => addStyleClass(padding.value)}
className="p-2 text-xs border border-gray-200 rounded-lg hover:border-gray-300 transition-colors text-left"
>
<div className="font-medium">{padding.label}</div>
<div className="text-gray-500">{padding.preview}</div>
</button>
))}
</div>
</div>
</div>
)}
{/* 颜色面板 */}
{activeTab === 'colors' && (
<div className="space-y-6">
{/* 背景颜色 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="grid grid-cols-2 gap-2">
{stylePresets.colors.backgrounds.map((color, index) => (
<button
key={index}
onClick={() => addStyleClass(color.value)}
className="flex items-center gap-2 p-2 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
>
<div
className={`w-4 h-4 rounded ${color.value}`}
style={{ backgroundColor: color.color }}
/>
<span className="text-xs">{color.label}</span>
</button>
))}
</div>
</div>
{/* 文字颜色 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="grid grid-cols-2 gap-2">
{stylePresets.colors.text.map((color, index) => (
<button
key={index}
onClick={() => addStyleClass(color.value)}
className="flex items-center gap-2 p-2 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
>
<div
className="w-4 h-4 rounded border border-gray-300"
style={{ backgroundColor: color.color }}
/>
<span className="text-xs">{color.label}</span>
</button>
))}
</div>
</div>
{/* 自定义颜色 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="space-y-3">
<div className="flex gap-2">
<input
type="color"
value={customColor}
onChange={(e) => setCustomColor(e.target.value)}
className="w-8 h-8 border border-gray-300 rounded cursor-pointer"
/>
<Input
value={customColor}
onChange={(e) => setCustomColor(e.target.value)}
placeholder="#3b82f6"
/>
</div>
<Row gutter={8}>
<Col span={12}>
<Button
block
size="small"
onClick={() => addStyleClass(`bg-[${customColor}]`)}
>
</Button>
</Col>
<Col span={12}>
<Button
block
size="small"
onClick={() => addStyleClass(`text-[${customColor}]`)}
>
</Button>
</Col>
</Row>
</div>
</div>
</div>
)}
{/* 字体面板 */}
{activeTab === 'typography' && (
<div className="space-y-6">
{/* 字体大小 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="space-y-2">
{stylePresets.typography.fontSize.map((fontSize, index) => (
<button
key={index}
onClick={() => addStyleClass(fontSize.value)}
className="w-full p-2 text-left text-xs border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
>
<div className="font-medium">{fontSize.label}</div>
<div className="text-gray-500">{fontSize.preview}</div>
</button>
))}
</div>
</div>
{/* 字体粗细 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="space-y-2">
{stylePresets.typography.fontWeight.map((fontWeight, index) => (
<button
key={index}
onClick={() => addStyleClass(fontWeight.value)}
className="w-full p-2 text-left text-xs border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
>
<div className="font-medium">{fontWeight.label}</div>
<div className="text-gray-500">{fontWeight.preview}</div>
</button>
))}
</div>
</div>
</div>
)}
{/* 布局面板 */}
{activeTab === 'layout' && (
<div className="space-y-6">
{/* 显示类型 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="space-y-2">
{stylePresets.layout.display.map((display, index) => (
<button
key={index}
onClick={() => addStyleClass(display.value)}
className="w-full p-2 text-left text-xs border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
>
<div className="font-medium">{display.label}</div>
<div className="text-gray-500">{display.preview}</div>
</button>
))}
</div>
</div>
{/* 定位 */}
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="space-y-2">
{stylePresets.layout.position.map((position, index) => (
<button
key={index}
onClick={() => addStyleClass(position.value)}
className="w-full p-2 text-left text-xs border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
>
<div className="font-medium">{position.label}</div>
<div className="text-gray-500">{position.preview}</div>
</button>
))}
</div>
</div>
</div>
)}
{/* 自定义样式 */}
{activeTab === 'custom' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
className
</label>
<TextArea
value={customStyles}
onChange={(e) => setCustomStyles(e.target.value)}
placeholder="输入 CSS 类名..."
rows={4}
className="font-mono text-sm"
/>
</div>
<div className="flex gap-2">
<Button onClick={updateStyles} type="primary" block>
</Button>
<Button onClick={clearAllStyles} block>
</Button>
</div>
</div>
)}
{/* 已应用的样式标签 */}
{getAppliedStyles().length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-900 mb-3"></h4>
<div className="flex flex-wrap gap-1">
{getAppliedStyles().map((styleClass, index) => (
<Tag
key={index}
closable
onClose={() => removeStyleClass(styleClass)}
className="mb-1"
>
{styleClass}
</Tag>
))}
</div>
</div>
)}
</div>
</Card>
);
};
export default StylePanel;

View File

@@ -0,0 +1 @@
@import "tailwindcss";

View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@@ -0,0 +1,499 @@
import React from '../react';
import { useState, useEffect } from 'react';
import { Card, Row, Col, Button, Space, Divider, message, Badge } from 'antd';
import { ThunderboltOutlined, CodeOutlined, BgColorsOutlined, EditOutlined, EyeOutlined, ReloadOutlined, WarningOutlined, CheckCircleOutlined } from '@ant-design/icons';
import type { ElementInfo, SourceInfo } from '../../../../packages/client-shared/src/messages';
import PropertyPanel from '../external-panel/PropertyPanel';
import type { PropertyPanelConfig } from '../external-panel/PropertyPanel';
import { bridge } from '../../../../packages/client-react/src/bridge';
export default function CustomizableDemoPage() {
const [selectedElement, setSelectedElement] = useState<ElementInfo | null>(null);
const [currentStyle, setCurrentStyle] = useState<string>('');
const [currentContent, setCurrentContent] = useState<string>('');
const [demoConfig, setDemoConfig] = useState<PropertyPanelConfig | undefined>();
const [bridgeStatus, setBridgeStatus] = useState<'checking' | 'connected' | 'disconnected' | 'error'>('checking');
const [bridgeInfo, setBridgeInfo] = useState<any>(null);
// Bridge状态检查
useEffect(() => {
const checkBridgeStatus = async () => {
try {
// 检查环境信息
const envInfo = bridge.getEnvironmentInfo();
setBridgeInfo(envInfo);
console.log('[Demo] Bridge environment info:', envInfo);
if (!envInfo.isIframe) {
setBridgeStatus('disconnected');
console.log('[Demo] Running in main window, bridge connection not needed');
return;
}
// 执行健康检查
const health = await bridge.healthCheck();
console.log('[Demo] Bridge health check:', health);
if (health.status === 'healthy') {
setBridgeStatus('connected');
} else if (health.status === 'degraded') {
setBridgeStatus('error');
} else {
setBridgeStatus('disconnected');
}
} catch (error) {
console.error('[Demo] Bridge health check failed:', error);
setBridgeStatus('error');
}
};
// 立即检查一次
checkBridgeStatus();
// 定期检查
const statusTimer = setInterval(checkBridgeStatus, 10000); // 10秒检查一次
return () => clearInterval(statusTimer);
}, []);
/**
* 获取Bridge状态显示组件
*/
const BridgeStatusPanel = () => {
const getStatusIcon = () => {
switch (bridgeStatus) {
case 'connected':
return <CheckCircleOutlined className="text-green-500" />;
case 'disconnected':
return <WarningOutlined className="text-yellow-500" />;
case 'error':
return <WarningOutlined className="text-red-500" />;
default:
return <div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />;
}
};
const getStatusText = () => {
switch (bridgeStatus) {
case 'connected':
return 'Bridge已连接';
case 'disconnected':
return 'Bridge未连接主窗口模式';
case 'error':
return 'Bridge连接错误';
case 'checking':
return '检查Bridge状态...';
default:
return '未知状态';
}
};
const getStatusColor = () => {
switch (bridgeStatus) {
case 'connected':
return 'success';
case 'disconnected':
return 'warning';
case 'error':
return 'error';
default:
return 'processing';
}
};
return (
<Card size="small" title="Bridge状态" className="mb-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{getStatusIcon()}
<span className="text-sm">{getStatusText()}</span>
</div>
<Space>
<Button
size="small"
onClick={() => {
bridge.diagnose();
message.info('Bridge诊断信息已输出到控制台');
}}
>
</Button>
<Button
size="small"
onClick={() => window.location.reload()}
>
</Button>
</Space>
</div>
{bridgeInfo && (
<div className="mt-2 text-xs text-gray-500 space-y-1">
<div>: {bridgeInfo.isIframe ? 'Iframe' : '主窗口'}</div>
<div>: {bridgeInfo.location.substring(0, 50)}...</div>
<div>: {bridgeInfo.origin}</div>
</div>
)}
</Card>
);
};
// 模拟iframe内容
const [iframeSrc] = useState(() => {
// 生成一个包含源码映射的演示HTML
return `data:text/html;charset=utf-8,${encodeURIComponent(`
<!DOCTYPE html>
<html>
<head>
<title>设计模式演示</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.container { max-width: 800px; margin: 0 auto; }
.hero { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px; border-radius: 12px; text-align: center; margin-bottom: 30px; }
.hero h1 { margin: 0 0 10px 0; font-size: 2.5rem; font-weight: bold; }
.hero p { margin: 0; font-size: 1.2rem; opacity: 0.9; }
.features { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; }
.feature-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border: 1px solid #e5e7eb; }
.feature-card h3 { margin: 0 0 10px 0; color: #1f2937; font-size: 1.25rem; }
.feature-card p { margin: 0; color: #6b7280; line-height: 1.6; }
.cta { text-align: center; background: #f9fafb; padding: 30px; border-radius: 8px; border: 1px solid #e5e7eb; }
.cta-button { background: #3b82f6; color: white; padding: 12px 24px; border: none; border-radius: 6px; font-size: 1rem; font-weight: 500; cursor: pointer; transition: background-color 0.2s; }
.cta-button:hover { background: #2563eb; }
.code-block { background: #1f2937; color: #f9fafb; padding: 20px; border-radius: 8px; font-family: 'Courier New', monospace; margin: 20px 0; overflow-x: auto; }
.highlight { background: rgba(59, 130, 246, 0.1); padding: 2px 4px; border-radius: 4px; }
</style>
</head>
<body>
<div class="container">
<div class="hero">
<h1 data-source-file="src/components/Hero.tsx" data-source-line="10" data-source-column="4">设计模式演示</h1>
<p data-source-file="src/components/Hero.tsx" data-source-line="11" data-source-column="4">体验强大的可视化开发工具,让设计触手可及</p>
</div>
<div class="features">
<div class="feature-card">
<h3 data-source-file="src/components/FeatureCard.tsx" data-source-line="5" data-source-column="8">实时预览</h3>
<p data-source-file="src/components/FeatureCard.tsx" data-source-line="6" data-source-column="8">即时查看修改效果,所见即所得的开发体验</p>
</div>
<div class="feature-card">
<h3 data-source-file="src/components/FeatureCard.tsx" data-source-line="12" data-source-column="8">智能提示</h3>
<p data-source-file="src/components/FeatureCard.tsx" data-source-line="13" data-source-column="8">AI驱动的智能建议提升开发效率</p>
</div>
<div class="feature-card">
<h3 data-source-file="src/components/FeatureCard.tsx" data-source-line="19" data-source-column="8">一键部署</h3>
<p data-source-file="src/components/FeatureCard.tsx" data-source-line="20" data-source-column="8">无缝集成CI/CD流程快速交付产品</p>
</div>
</div>
<div class="cta">
<h2 data-source-file="src/components/CallToAction.tsx" data-source-line="8" data-source-column="12">准备好开始了吗?</h2>
<p data-source-file="src/components/CallToAction.tsx" data-source-line="9" data-source-column="12">立即体验下一代设计开发工具</p>
<button class="cta-button" data-source-file="src/components/CallToAction.tsx" data-source-line="15" data-source-column="8">开始使用</button>
</div>
<div class="code-block">
<div data-source-file="src/utils/code-example.ts" data-source-line="5" data-source-column="4">// 简单的使用示例</div>
<div data-source-file="src/utils/code-example.ts" data-source-line="6" data-source-column="4">import { DesignMode } from '@xagi/design-mode';</div>
<div data-source-file="src/utils/code-example.ts" data-source-line="7" data-source-column="4"></div>
<div data-source-file="src/utils/code-example.ts" data-source-line="8" data-source-column="4">const config = {</div>
<div data-source-file="src/utils/code-example.ts" data-source-line="9" data-source-column="8">enableSelection: true,</div>
<div data-source-file="src/utils/code-example.ts" data-source-line="10" data-source-column="8">autoSave: true</div>
<div data-source-file="src/utils/code-example.ts" data-source-line="11" data-source-column="4">};</div>
<div data-source-file="src/utils/code-example.ts" data-source-line="12" data-source-column="4"></div>
<div data-source-file="src/utils/code-example.ts" data-source-line="13" data-source-column="4">DesignMode.init(config);</div>
</div>
</div>
<script>
// 添加点击事件监听
document.addEventListener('DOMContentLoaded', function() {
// 监听所有可编辑元素的点击
const editableElements = document.querySelectorAll('[data-source-file]');
editableElements.forEach(element => {
element.style.cursor = 'pointer';
element.addEventListener('click', function(e) {
e.stopPropagation();
// 移除之前的高亮
document.querySelectorAll('[data-selected="true"]').forEach(el => {
el.removeAttribute('data-selected');
el.style.outline = '';
});
// 添加高亮
this.setAttribute('data-selected', 'true');
this.style.outline = '2px solid #3b82f6';
this.style.outlineOffset = '2px';
// 发送选中消息到父窗口
// 判断是否为静态文本:检查元素是否有 static-content 属性
const isStaticText = this.hasAttribute('data-source-static-content');
const elementInfo = {
tagName: this.tagName.toLowerCase(),
className: this.className || '',
textContent: this.textContent || '',
sourceInfo: {
fileName: this.getAttribute('data-source-file'),
lineNumber: parseInt(this.getAttribute('data-source-line')),
columnNumber: parseInt(this.getAttribute('data-source-column'))
},
isStaticText: isStaticText || false // 默认为 false
};
window.parent.postMessage({
type: 'ELEMENT_SELECTED',
payload: { elementInfo }
}, '*');
console.log('Element selected:', elementInfo);
});
});
// 点击空白处取消选择
document.addEventListener('click', function(e) {
if (!e.target.hasAttribute('data-source-file')) {
document.querySelectorAll('[data-selected="true"]').forEach(el => {
el.removeAttribute('data-selected');
el.style.outline = '';
});
window.parent.postMessage({
type: 'ELEMENT_DESELECTED'
}, '*');
}
});
});
</script>
</body>
</html>
`)}`);
/**
* 处理iframe中的元素选择
*/
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const { type, payload } = event.data;
switch (type) {
case 'ELEMENT_SELECTED':
setSelectedElement(payload.elementInfo);
setCurrentStyle(payload.elementInfo.className);
setCurrentContent(payload.elementInfo.textContent);
console.log('[Parent] Element selected:', payload.elementInfo);
break;
case 'ELEMENT_DESELECTED':
setSelectedElement(null);
console.log('[Parent] Element deselected');
break;
case 'STYLE_UPDATED':
console.log('[Parent] Style updated:', payload);
// 更新当前样式
if (selectedElement) {
setCurrentStyle(payload.newClass);
}
break;
case 'CONTENT_UPDATED':
console.log('[Parent] Content updated:', payload);
// 更新当前内容
if (selectedElement) {
setCurrentContent(payload.newValue);
}
break;
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [selectedElement]);
/**
* 处理样式更新
*/
const handleStyleUpdate = (data: { sourceInfo: SourceInfo; newClass: string }) => {
// 这里可以发送消息到iframe或者直接调用API
console.log('Style update requested:', data);
// 模拟更新成功
message.success('样式更新成功');
};
/**
* 处理内容更新
*/
const handleContentUpdate = (data: { sourceInfo: SourceInfo; newContent: string }) => {
// 这里可以发送消息到iframe或者直接调用API
console.log('Content update requested:', data);
// 模拟更新成功
message.success('内容更新成功');
};
/**
* 配置更改处理
*/
const handleConfigChange = (config: PropertyPanelConfig) => {
setDemoConfig(config);
console.log('Panel config changed:', config);
};
/**
* 重置演示
*/
const resetDemo = () => {
setSelectedElement(null);
setCurrentStyle('');
setCurrentContent('');
// 发送重置消息到iframe
const iframe = document.querySelector('iframe') as HTMLIFrameElement;
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage({ type: 'RESET_DEMO' }, '*');
}
message.info('演示已重置');
};
return (
<div className="min-h-screen bg-gray-50">
{/* 顶部标题栏 */}
<div className="bg-white border-b border-gray-200 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
<ThunderboltOutlined className="text-white text-lg" />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-sm text-gray-600"></p>
</div>
</div>
<Space>
<Button icon={<CodeOutlined />} onClick={() => window.open('https://github.com/your-repo', '_blank')}>
</Button>
<Button icon={<ReloadOutlined />} onClick={resetDemo}>
</Button>
</Space>
</div>
</div>
<div className="flex h-[calc(100vh-88px)]">
{/* 左侧:可自定义属性面板 */}
<div className="w-96 bg-white border-r border-gray-200 flex flex-col">
{/* Bridge状态面板 */}
<div className="p-4 border-b border-gray-200">
<BridgeStatusPanel />
</div>
{/* 主体面板内容 */}
<div className="flex-1 overflow-hidden">
<PropertyPanel
selectedElement={selectedElement}
currentStyle={currentStyle}
currentContent={currentContent}
onStyleUpdate={handleStyleUpdate}
onContentUpdate={handleContentUpdate}
onStyleChange={setCurrentStyle}
onContentChange={setCurrentContent}
config={demoConfig}
onConfigChange={handleConfigChange}
/>
</div>
</div>
{/* 右侧iframe预览 */}
<div className="flex-1 bg-white">
<div className="h-full flex flex-col">
{/* 预览工具栏 */}
<div className="border-b border-gray-200 px-6 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<EyeOutlined className="text-blue-500" />
<span className="font-medium text-gray-900"></span>
{selectedElement && (
<>
<Divider type="vertical" />
<span className="text-sm text-gray-600">
: &lt;{selectedElement.tagName}&gt;
</span>
</>
)}
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
<span className="text-xs text-gray-500"></span>
</div>
</div>
</div>
</div>
{/* iframe容器 */}
<div className="flex-1 bg-gray-100">
<iframe
src={iframeSrc}
className="w-full h-full border-0"
title="可自定义属性面板演示"
sandbox="allow-scripts allow-same-origin"
/>
</div>
</div>
</div>
</div>
{/* 底部功能说明 */}
<div className="border-t border-gray-200 bg-white px-6 py-4">
<div className="grid grid-cols-3 gap-6">
<Card size="small" className="h-full">
<div className="flex items-center gap-2 mb-2">
<BgColorsOutlined className="text-blue-500" />
<span className="font-medium"></span>
</div>
<ul className="text-sm text-gray-600 space-y-1">
<li> </li>
<li> </li>
<li> //</li>
<li> </li>
</ul>
</Card>
<Card size="small" className="h-full">
<div className="flex items-center gap-2 mb-2">
<EditOutlined className="text-green-500" />
<span className="font-medium"></span>
</div>
<ul className="text-sm text-gray-600 space-y-1">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</Card>
<Card size="small" className="h-full">
<div className="flex items-center gap-2 mb-2">
<ToolOutlined className="text-purple-500" />
<span className="font-medium"></span>
</div>
<ul className="text-sm text-gray-600 space-y-1">
<li> </li>
<li> </li>
<li> </li>
<li> </li>
</ul>
</Card>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,107 @@
import React from 'react';
export default function FeaturesPage() {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<h1 className="text-4xl font-bold text-gray-900 mb-8"></h1>
{/* 功能 1: 样式编辑 */}
<section className="mb-12 bg-white rounded-xl shadow-lg p-8">
<h2 className="text-2xl font-bold text-indigo-600 mb-4">1. </h2>
<p className="text-gray-700 mb-6">
使 Tailwind CSS
</p>
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-slate-100 rounded-md">
<p className="text-sm text-slate-600"></p>
</div>
<div className="p-4 bg-blue-100 rounded-md">
<p className="text-sm text-blue-600"></p>
</div>
<div className="p-6 bg-red-50 rounded-lg">
<p className="text-base text-red-700"></p>
</div>
<div className="p-2 bg-green-50 rounded-full">
<p className="text-xs text-green-700"> + </p>
</div>
</div>
</section>
{/* 功能 2: 内容编辑 */}
<section className="mb-12 bg-white rounded-xl shadow-lg p-8">
<h2 className="text-2xl font-bold text-green-600 mb-4">2. </h2>
<p className="text-gray-700 mb-6">
</p>
<div className="space-y-4">
<div className="p-4 bg-gray-50 rounded-md">
<h3 className="text-lg font-semibold text-gray-900"></h3>
<p className="text-gray-600"></p>
</div>
<div className="p-4 bg-blue-50 rounded-md">
<p className="text-blue-900">11</p>
</div>
</div>
</section>
{/* 功能 3: 源码映射 */}
<section className="mb-12 bg-white rounded-xl shadow-lg p-8">
<h2 className="text-2xl font-bold text-purple-600 mb-4">3. </h2>
<p className="text-gray-700 mb-6">
</p>
<div className="bg-gray-900 text-green-400 p-4 rounded-md font-mono text-sm">
<div>data-source-file="/src/pages/FeaturesPage.tsx"</div>
<div>data-source-line="42"</div>
<div>data-source-column="8"</div>
</div>
</section>
{/* 功能 4: 实时更新 */}
<section className="mb-12 bg-white rounded-xl shadow-lg p-8">
<h2 className="text-2xl font-bold text-orange-600 mb-4">4. </h2>
<p className="text-gray-700 mb-6">
</p>
<div className="flex items-center space-x-4">
<div className="flex-1 p-4 bg-orange-50 rounded-md text-center">
<p className="text-orange-700 font-medium"></p>
</div>
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
<div className="flex-1 p-4 bg-orange-100 rounded-lg text-center">
<p className="text-orange-900 font-bold"></p>
</div>
</div>
</section>
{/* 交互演示 */}
<section className="bg-gradient-to-r from-indigo-500 to-purple-600 rounded-xl shadow-xl p-8 text-white">
<h2 className="text-3xl font-bold mb-4"></h2>
<p className="text-indigo-100 mb-6">
</p>
<div className="grid md:grid-cols-3 gap-4">
<div className="bg-white bg-opacity-20 backdrop-blur-sm p-6 rounded-lg">
<h3 className="text-xl font-semibold mb-2"> 1</h3>
<p className="text-indigo-100"></p>
</div>
<div className="bg-white bg-opacity-20 backdrop-blur-sm p-6 rounded-lg">
<h3 className="text-xl font-semibold mb-2"> 2</h3>
<p className="text-indigo-100"></p>
</div>
<div className="bg-white bg-opacity-20 backdrop-blur-sm p-6 rounded-lg">
<h3 className="text-xl font-semibold mb-2"> 3</h3>
<p className="text-indigo-100"></p>
</div>
</div>
</section>
</div>
);
}

View File

@@ -0,0 +1,114 @@
import React from 'react';
export default function HomePage() {
const title = "欢迎使用设计模式";
const content = "体验实时可视化编辑,自动更新源代码。";
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* 主标题区域 */}
<div className="text-center mb-16">
<h1 className="text-5xl font-bold text-gray-900 mb-4">
{title}
</h1>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
{content}
{'--变量测试--'}
</p>
</div>
{/* 功能卡片 */}
<div className="grid md:grid-cols-3 gap-8 mb-16">
<div className="rounded-lg shadow-lg p-6 bg-green-100">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2 bg-red-100"></h3>
<p className="text-gray-600 bg-gray-100">
Tailwind CSS
</p>
</div>
<div className="rounded-lg shadow-lg p-6 bg-green-100">
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2 bg-green-100">12</h3>
<p className="text-gray-600 bg-gray-100">
</p>
</div>
<div className="bg-white rounded-lg shadow-lg p-6">
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2 bg-blue-100"></h3>
<p className="text-gray-600 bg-gray-100">
</p>
</div>
</div>
<div className="bg-white rounded-xl shadow-xl p-8">
3111
<div className="space-y-4">
<div className="p-6 bg-blue-50 rounded-lg border-2 border-blue-200">
<h3 className="text-xl font-semibold text-blue-900 mb-2"></h3>
<p className="text-blue-700 bg-blue-100 bg-red-100">
</p>
</div>
<div className="p-6 bg-green-50 rounded-lg border-2 border-green-200">
1
<p className="text-green-700">
</p>
</div>
<div className="p-6 bg-purple-50 rounded-lg border-2 border-purple-200">
<h3 className="text-xl font-semibold text-purple-900 mb-2"></h3>
<p className="text-purple-700 bg-green-100">
3434341212121212
</p>
</div>
</div>
</div>
{/* 列表修改测试区域 */}
<div className="mt-16">
<h2 className="text-3xl font-bold text-gray-900 mb-8 text-center"></h2>
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3'>
{[
{ text: "写一篇关于人工智能的文章", icon: (props: any) => <svg {...props} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /></svg> },
{ text: "解释量子计算", icon: (props: any) => <svg {...props} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" /></svg> },
{ text: "设计一个Logo", icon: (props: any) => <svg {...props} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg> },
].slice(0, 6).map((prompt, index) => {
const IconComponent = prompt.icon;
return (
<div
key={index}
className='cursor-pointer border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-all duration-200 bg-white rounded-lg shadow-sm'
>
<div className='p-3'>
<div className='flex items-center gap-2 bg-yellow-100'>
<IconComponent className='w-4 h-4 text-blue-600' />
<span className='text-sm text-gray-700 truncate'>
"{prompt.text}"
</span>
<p></p>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,515 @@
import React from '../react';
import { useState, useEffect } from 'react';
import type { ElementInfo } from '@/types/messages';
// 确保React在所有情况下都可用
if (typeof React === 'undefined') {
throw new Error(
'React is not imported. Please ensure React is imported correctly.'
);
}
export default function IframeDemoPage() {
const [iframeDesignMode, setIframeDesignMode] = useState(false);
const [selectedElement, setSelectedElement] = useState<ElementInfo | null>(
null
);
const [editingContent, setEditingContent] = useState('');
const [editingClass, setEditingClass] = useState('');
const [pendingChanges, setPendingChanges] = useState<
Array<{
type: 'style' | 'content';
sourceInfo: any;
newValue: string;
originalValue?: string;
}>
>([]);
const iframeRef = React.useRef<HTMLIFrameElement>(null);
// Listen for messages from iframe
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
// Debug log for message source
if (event.data.type === 'ELEMENT_SELECTED') {
console.log('[Parent] Received ELEMENT_SELECTED', {
source: event.source,
iframeWindow: iframeRef.current?.contentWindow,
isMatch: iframeRef.current && event.source === iframeRef.current.contentWindow,
data: event.data
});
}
if (event.data.type === 'ADD_TO_CHAT') {
return console.log('[Parent] Add to chat:', event.data.payload);
}
if (event.data.type === 'COPY_ELEMENT') {
return console.log('[Parent] Copy element:', event.data.payload);
}
// Only accept messages from the iframe
if (iframeRef.current && event.source !== iframeRef.current.contentWindow) {
if (event.data.type === 'ELEMENT_SELECTED') {
console.warn('[Parent] Ignoring message from non-iframe source');
}
return;
}
const { type, payload } = event.data;
switch (type) {
case 'DESIGN_MODE_CHANGED':
setIframeDesignMode(event.data.enabled);
break;
case 'ELEMENT_SELECTED':
console.log('[Parent] Processing ELEMENT_SELECTED', payload);
// 验证 sourceInfo 是否有效
if (
!payload.elementInfo?.sourceInfo ||
!payload.elementInfo.sourceInfo.fileName ||
payload.elementInfo.sourceInfo.lineNumber === 0
) {
console.warn(
'[Parent] Invalid sourceInfo received:',
payload.elementInfo?.sourceInfo
);
console.warn('[Parent] This may cause update operations to fail');
}
setSelectedElement(payload.elementInfo);
setEditingContent(payload.elementInfo.textContent);
setEditingClass(payload.elementInfo.className);
break;
case 'ELEMENT_DESELECTED':
setSelectedElement(null);
console.log('[Parent] Element deselected');
break;
case 'CONTENT_UPDATED':
console.log('[Parent] Content updated:', payload);
break;
case 'STYLE_UPDATED':
console.log('[Parent] Style updated:', payload);
break;
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, []);
// Debounce hook
const useDebounce = <T,>(value: T, delay: number): T => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
};
const debouncedContent = useDebounce(editingContent, 300);
const debouncedClass = useDebounce(editingClass, 300);
// Upsert pending change
const upsertPendingChange = (
type: 'style' | 'content',
newValue: string,
originalValue?: string
) => {
if (!selectedElement) return;
setPendingChanges(prev => {
const existingIndex = prev.findIndex(
item =>
item.type === type &&
item.sourceInfo.fileName === selectedElement.sourceInfo.fileName &&
item.sourceInfo.lineNumber === selectedElement.sourceInfo.lineNumber
);
const newChange = {
type,
sourceInfo: selectedElement.sourceInfo,
newValue,
originalValue: originalValue || (type === 'style' ? selectedElement.className : selectedElement.textContent),
};
if (existingIndex >= 0) {
const newList = [...prev];
newList[existingIndex] = newChange;
return newList;
} else {
return [...prev, newChange];
}
});
};
const lastSelectedElementRef = React.useRef(selectedElement);
// Real-time content update
useEffect(() => {
if (!selectedElement) return;
// If selection changed, skip this update cycle
if (selectedElement !== lastSelectedElementRef.current) {
console.log('[Parent] Skipping content update due to selection change');
return;
}
console.log('[Parent] Content effect triggered', {
debouncedContent,
currentContent: selectedElement.textContent,
shouldUpdate: debouncedContent !== selectedElement.textContent
});
if (debouncedContent === selectedElement.textContent) return;
console.log('[Parent] Sending UPDATE_CONTENT', debouncedContent);
upsertPendingChange('content', debouncedContent);
const iframe = iframeRef.current;
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(
{
type: 'UPDATE_CONTENT',
payload: {
sourceInfo: selectedElement.sourceInfo,
newContent: debouncedContent,
persist: false,
},
timestamp: Date.now(),
},
'*'
);
}
}, [debouncedContent, selectedElement]); // Removed upsertPendingChange to fix infinite loop
// Real-time style update
useEffect(() => {
if (!selectedElement) return;
// If selection changed, skip this update cycle
if (selectedElement !== lastSelectedElementRef.current) {
console.log('[Parent] Skipping style update due to selection change');
return;
}
if (debouncedClass === selectedElement.className) return;
upsertPendingChange('style', debouncedClass);
const iframe = iframeRef.current;
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(
{
type: 'UPDATE_STYLE',
payload: {
sourceInfo: selectedElement.sourceInfo,
newClass: debouncedClass,
persist: false,
},
timestamp: Date.now(),
},
'*'
);
}
}, [debouncedClass, selectedElement]); // Removed upsertPendingChange to fix infinite loop
// Update lastSelectedElementRef AFTER other effects
useEffect(() => {
lastSelectedElementRef.current = selectedElement;
}, [selectedElement]);
// Style Manager Logic
const toggleStyle = (newStyle: string, categoryRegex: RegExp) => {
let currentClasses = editingClass.split(' ').filter(c => c.trim());
// Remove existing class in the same category
currentClasses = currentClasses.filter(c => !categoryRegex.test(c));
// Add new style if it's not empty (allows clearing style)
if (newStyle) {
currentClasses.push(newStyle);
}
setEditingClass(currentClasses.join(' '));
};
const hasStyle = (style: string) => {
return editingClass.split(' ').includes(style);
};
// UI Controls
const renderColorPicker = (prefix: string, colors: string[], label: string) => (
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">{label}</label>
<div className="flex flex-wrap gap-2">
{colors.map(color => {
const styleClass = `${prefix}-${color}`;
const isActive = hasStyle(styleClass);
return (
<button
key={color}
onClick={() => toggleStyle(styleClass, new RegExp(`^${prefix}-[a-z]+(-\\d+)?$`))}
className={`w-8 h-8 rounded-full border-2 transition-all ${isActive ? 'border-gray-900 scale-110' : 'border-transparent hover:scale-105'
} ${styleClass.replace('text-', 'bg-')}`} // Use bg color for preview
title={color}
/>
);
})}
<button
onClick={() => toggleStyle('', new RegExp(`^${prefix}-[a-z]+(-\\d+)?$`))}
className="px-2 py-1 text-xs text-gray-500 border border-gray-300 rounded hover:bg-gray-100"
>
</button>
</div>
</div>
);
const renderSelect = (prefix: string, options: { label: string; value: string }[], label: string) => (
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">{label}</label>
<div className="flex flex-wrap gap-2">
{options.map(opt => {
const styleClass = opt.value ? `${prefix}-${opt.value}` : '';
const isActive = styleClass ? hasStyle(styleClass) : !options.some(o => o.value && hasStyle(`${prefix}-${o.value}`));
return (
<button
key={opt.label}
onClick={() => toggleStyle(styleClass, new RegExp(`^${prefix}(-[a-z0-9]+)?$`))}
className={`px-3 py-1 text-sm rounded border transition-all ${isActive
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
}`}
>
{opt.label}
</button>
)
})}
</div>
</div>
);
// Toggle design mode in iframe
const toggleIframeDesignMode = () => {
const iframe = iframeRef.current;
console.log('[Parent] Toggling iframe design mode...', iframe);
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(
{
type: 'TOGGLE_DESIGN_MODE',
enabled: !iframeDesignMode,
timestamp: Date.now(),
},
'*'
);
}
};
// 保存所有更改
const saveChanges = async () => {
if (pendingChanges.length === 0) return;
console.log('[Parent] Saving changes...', pendingChanges);
try {
const response = await fetch('/__appdev_design_mode/batch-update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
updates: pendingChanges.map(change => ({
filePath: change.sourceInfo.fileName,
line: change.sourceInfo.lineNumber,
column: change.sourceInfo.columnNumber,
newValue: change.newValue,
type: change.type,
originalValue: change.originalValue,
})),
}),
});
if (response.ok) {
const result = await response.json();
console.log('[Parent] Batch update success:', result);
alert(`成功保存 ${result.summary.successful} 个更改!`);
setPendingChanges([]); // 清空待保存列表
} else {
const error = await response.json();
console.error('[Parent] Batch update failed:', error);
alert('保存失败,请查看控制台错误信息。');
}
} catch (error) {
console.error('[Parent] Error saving changes:', error);
alert('保存出错,请检查网络连接。');
}
};
return (
<div className='min-h-screen bg-gray-100' data-selection-exclude="true">
{/* Top Control Bar */}
<div className='bg-white border-b border-gray-200 px-6 py-4'>
<div className='flex items-center justify-between'>
<h1 className='text-2xl font-bold text-gray-900'>
Iframe
</h1>
<div className='flex gap-4'>
{pendingChanges.length > 0 && (
<button
onClick={saveChanges}
className='px-6 py-2 rounded-lg font-semibold bg-blue-600 hover:bg-blue-700 text-white transition-all shadow-md flex items-center gap-2'
>
<span> ({pendingChanges.length})</span>
<svg
className='w-4 h-4'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4'
/>
</svg>
</button>
)}
<button
onClick={toggleIframeDesignMode}
className={`px-6 py-2 rounded-lg font-semibold transition-all ${iframeDesignMode
? 'bg-green-500 hover:bg-green-600 text-white'
: 'bg-gray-200 hover:bg-gray-300 text-gray-700'
}`}
>
{iframeDesignMode ? '✓ 设计模式已启用' : '启用设计模式'}
</button>
</div>
</div>
</div>
<div className='flex h-[calc(100vh-73px)]'>
{/* Left: External Property Panel */}
<div className='w-80 bg-white border-r border-gray-200 overflow-y-auto'>
<div className='p-6'>
<h2 className='text-lg font-bold text-gray-900 mb-4'>
</h2>
{selectedElement ? (
<div className='space-y-6'>
{/* Element Info */}
<div className='bg-blue-50 border border-blue-200 rounded-lg p-4'>
<h3 className='text-sm font-semibold text-blue-900 mb-2'>
</h3>
<div className='text-xs text-blue-700 space-y-1'>
<div>
<span className='font-medium'>:</span>{' '}
{selectedElement.tagName}
</div>
<div className='break-all'>
<span className='font-medium'>:</span>{' '}
{selectedElement.sourceInfo.fileName.split('/').pop()}
</div>
</div>
</div>
{/* Content Editor - 仅当 isStaticText 为 true 时显示 */}
{selectedElement.isStaticText && (
<div>
<label className='block text-sm font-medium text-gray-700 mb-2'>
</label>
<textarea
value={editingContent}
onChange={e => setEditingContent(e.target.value)}
className='w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent'
rows={3}
/>
</div>
)}
<hr className="border-gray-200" />
{/* Smart Style Controls */}
<div>
<h3 className="text-md font-bold text-gray-900 mb-4"></h3>
{renderColorPicker('bg', ['white', 'gray-100', 'red-100', 'blue-100', 'green-100', 'yellow-100', 'purple-100'], '背景颜色')}
{renderColorPicker('text', ['gray-900', 'gray-500', 'red-600', 'blue-600', 'green-600', 'purple-600', 'white'], '文字颜色')}
{renderSelect('p', [
{ label: '无', value: '0' },
{ label: '小', value: '2' },
{ label: '中', value: '4' },
{ label: '大', value: '8' },
{ label: '特大', value: '12' }
], '内边距 (Padding)')}
{renderSelect('rounded', [
{ label: '直角', value: 'none' },
{ label: '小圆角', value: 'sm' },
{ label: '圆角', value: 'md' },
{ label: '大圆角', value: 'lg' },
{ label: '全圆角', value: 'full' }
], '圆角 (Border Radius)')}
{renderSelect('text', [
{ label: '小', value: 'sm' },
{ label: '默认', value: 'base' },
{ label: '中', value: 'lg' },
{ label: '大', value: 'xl' },
{ label: '特大', value: '2xl' }
], '文字大小 (Font Size)')}
</div>
{/* Raw Style Editor */}
<div className="mt-6 pt-6 border-t border-gray-200">
<label className='block text-sm font-medium text-gray-500 mb-2'>
ClassName ()
</label>
<textarea
value={editingClass}
onChange={e => setEditingClass(e.target.value)}
className='w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500 focus:border-transparent font-mono text-xs text-gray-500'
rows={2}
/>
</div>
</div>
) : (
<div className='text-center py-12 text-gray-500'>
<p className='text-sm'>
{iframeDesignMode
? '点击 iframe 内的元素开始编辑'
: '请先启用设计模式'}
</p>
</div>
)}
</div>
</div>
{/* Right: Iframe Preview */}
<div className='flex-1 bg-gray-50 p-6'>
<div className='h-full bg-white rounded-lg shadow-xl overflow-hidden'>
<iframe
ref={iframeRef}
src={window.location.origin}
className='w-full h-full'
title='设计模式 Iframe 演示'
/>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,10 @@
// React重新导出确保在所有情况下都能正确导入
export { useState, useEffect, useCallback, useMemo, useRef, useReducer } from 'react';
export { createContext, useContext } from 'react';
export { createElement, Fragment } from 'react';
export type { ReactNode, ReactElement, ComponentType } from 'react';
// React主对象重新导出兼容旧版本
import * as ReactModule from 'react';
export default ReactModule;
export const React = ReactModule;

View File

@@ -0,0 +1,40 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"types": [
"vite/client"
],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": [
"../../packages/plugin/src/*"
]
}
},
"include": [
"src"
]
}

View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,44 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import appdevDesignMode from '@xagi/vite-plugin-design-mode';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
// <!-- DEV-INJECT-START -->
{
name: 'dev-inject',
enforce: 'post', // 确保在 HTML 注入阶段最后执行
transformIndexHtml(html) {
if (!html.includes('data-id="dev-inject-monitor"')) {
return html.replace("</head>", `
<script data-id="dev-inject-monitor">
(function() {
const remote = "/sdk/dev-monitor.js";
const separator = remote.includes('?') ? '&' : '?';
const script = document.createElement('script');
script.src = remote + separator + 't=' + Date.now();
script.dataset.id = 'dev-inject-monitor-script';
script.defer = true;
// 防止重复注入
if (!document.querySelector('[data-id="dev-inject-monitor-script"]')) {
document.head.appendChild(script);
}
})();
</script>
\n</head>`);
}
return html;
}
},
// <!-- DEV-INJECT-END -->
react(),
appdevDesignMode()
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
});

View File

@@ -0,0 +1,146 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Build outputs
dist/
build/
*.local
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
logs/
*.log
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output/
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache/
.parcel-cache/
# next.js build output
.next/
# nuxt.js build output
.nuxt/
# vuepress build output
.vuepress/dist/
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# TypeScript cache
*.tsbuildinfo
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache/
.parcel-cache/
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Design system files
*.tokens.json
design-tokens/
tokens/
# Generated files
generated/
auto-generated/

View File

@@ -0,0 +1,449 @@
# AppDev Design Mode - Full Featured Showcase
这是一个完整展示 `@vite-plugin-appdev-design-mode` 插件最新功能的示例项目。该项目构建了一个现代化的设计系统管理平台,展示了插件在可视化编辑、实时预览、源码调试等方面的强大功能。
## 🚀 功能特性
### 核心功能
1. **组件可视化编辑器**
- 拖拽式组件编辑
- 实时属性编辑面板
- 多设备预览支持
- 组件变体管理
2. **设计系统管理**
- 完整的令牌管理系统
- 颜色调色板管理
- 字体和排版令牌
- 间距和阴影令牌
- 动画和时间令牌
3. **实时预览系统**
- 多设备响应式预览
- 主题切换支持
- 实时组件更新
- 全屏预览模式
4. **源码调试增强**
- 自动源码映射
- 组件层次可视化
- 调试信息展示
- 设计模式指示器
### 技术亮点
- **React 19** 最新特性支持
- **Vite 6** 高性能构建
- **TailwindCSS 4** 现代化样式方案
- **Radix UI** 无障碍组件库
- **TypeScript** 完整类型支持
- **Framer Motion** 流畅动画效果
- **Zustand** 轻量级状态管理
## 🛠️ 技术栈
### 前端框架
- **React 19.2.0** - 最新的 React 版本,支持并发特性
- **TypeScript 5.6** - 严格的类型检查
- **Vite 6.0** - 极速的开发服务器和构建工具
### UI 组件
- **Radix UI** - 无障碍的底层组件
- **TailwindCSS 4.1** - 实用优先的 CSS 框架
- **Framer Motion 10.18** - 高性能动画库
### 开发工具
- **AntV X6** - 图形可视化(可选)
- **React Hook Form** - 表单管理
- **Zustand** - 状态管理
- **React Query** - 服务器状态管理
### 构建配置
- **PostCSS** - CSS 后处理器
- **Autoprefixer** - 自动添加浏览器前缀
- **ESLint** - 代码质量检查
- **Prettier** - 代码格式化
## 📦 安装和运行
### 环境要求
- Node.js 18.0.0 或更高版本
- pnpm 8.0.0 或更高版本
- 现代浏览器支持
### 安装依赖
```bash
# 克隆项目
git clone <repository-url>
cd vite-plugin-appdev-design-mode/examples/full-featured
# 安装依赖
pnpm install
```
### 开发模式
```bash
# 启动开发服务器
pnpm dev
# 访问 http://localhost:5175
```
### 构建生产版本
```bash
# 构建项目
pnpm build
# 预览生产版本
pnpm preview
```
### 代码检查
```bash
# 类型检查
pnpm type-check
# 代码规范检查
pnpm lint
```
## 📁 项目结构
```
examples/full-featured/
├── src/
│ ├── components/ # 可复用组件
│ │ ├── ui/ # 基础 UI 组件
│ │ ├── layout/ # 布局组件
│ │ ├── editor/ # 编辑器组件
│ │ ├── design-system/ # 设计系统组件
│ │ └── showcase/ # 展示组件
│ ├── pages/ # 页面组件
│ │ ├── Dashboard.tsx # 仪表盘页面
│ │ ├── DesignSystem.tsx # 设计系统页面
│ │ ├── ComponentEditor.tsx # 组件编辑器页面
│ │ └── LivePreview.tsx # 实时预览页面
│ ├── hooks/ # 自定义 Hooks
│ ├── store/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── types/ # TypeScript 类型定义
│ ├── App.tsx # 主应用组件
│ ├── main.tsx # 应用入口
│ └── index.css # 全局样式
├── public/ # 静态资源
├── package.json # 项目配置
├── vite.config.ts # Vite 配置
├── tailwind.config.js # TailwindCSS 配置
├── tsconfig.json # TypeScript 配置
└── README.md # 项目文档
```
## 🎨 设计系统令牌
### 颜色令牌
项目定义了完整的颜色系统:
```css
/* 主色调 */
--color-primary-50: #eff6ff;
--color-primary-500: #3b82f6;
--color-primary-900: #1e3a8a;
/* 次要色调 */
--color-secondary-50: #faf5ff;
--color-secondary-500: #a855f7;
/* 功能色彩 */
--color-success-500: #22c55e;
--color-warning-500: #eab308;
--color-error-500: #ef4444;
```
### 字体令牌
```css
/* 字体家族 */
--font-family-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
--font-family-mono: 'JetBrains Mono', ui-monospace, monospace;
/* 字体大小 */
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
```
### 间距令牌
```css
/* 基础间距 */
--spacing-1: 0.25rem; /* 4px */
--spacing-4: 1rem; /* 16px */
--spacing-8: 2rem; /* 32px */
```
## 🔧 插件配置
### 基础配置
```typescript
// vite.config.ts
appdevDesignMode({
enabled: true,
enableInProduction: false,
attributePrefix: 'design-mode',
verbose: true,
exclude: [
'node_modules',
'dist',
'**/*.test.{ts,tsx,js,jsx}'
],
include: [
'src/**/*.{ts,tsx,js,jsx}',
'components/**/*.{ts,tsx,js,jsx}',
'pages/**/*.{ts,tsx,js,jsx}'
]
})
```
### 高级配置选项
- **enabled**: 启用/禁用设计模式
- **enableInProduction**: 生产环境是否启用
- **attributePrefix**: 自定义属性前缀
- **verbose**: 详细日志输出
- **exclude/include**: 文件过滤规则
## 🎯 功能演示
### 1. 组件可视化编辑
访问 `/editor` 页面体验:
- 拖拽式组件编辑
- 实时属性面板编辑
- 多设备预览切换
- 组件代码生成
### 2. 设计系统管理
访问 `/design-system` 页面体验:
- 完整的令牌管理系统
- 颜色调色板可视化
- 字体和排版管理
- 间距和阴影令牌
### 3. 实时预览
访问 `/preview` 页面体验:
- 多设备响应式预览
- 主题切换Light/Dark/System
- 实时组件更新
- 全屏预览模式
### 4. 源码调试
在开发模式下:
- 自动源码映射
- 组件层次可视化
- 设计模式指示器
- 调试信息展示
## 🚀 性能优化
### 代码分割
项目使用 React.lazy() 实现路由级别的代码分割:
```typescript
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const ComponentEditor = React.lazy(() => import('./pages/ComponentEditor'));
```
### 资源优化
- 图片懒加载
- CSS 压缩和优化
- JavaScript 代码分割
- 浏览器缓存策略
### 响应式设计
项目完全支持响应式设计:
- 移动端优先设计
- 断点适配
- 触摸友好交互
- 性能优化
## 🎨 主题系统
### 暗色模式支持
项目支持完整的暗色模式:
```css
/* CSS 变量定义 */
:root {
--color-bg-primary: #ffffff;
--color-text-primary: #171717;
}
.dark {
--color-bg-primary: #0a0a0a;
--color-text-primary: #fafafa;
}
```
### 自定义主题
用户可以通过设计系统页面自定义主题:
- 颜色调色板
- 字体设置
- 间距调整
- 动画效果
## ♿ 无障碍支持
项目注重无障碍访问:
- 语义化 HTML 结构
- ARIA 属性支持
- 键盘导航
- 屏幕阅读器兼容
- 高对比度模式
## 🔍 调试功能
### 开发模式特性
- 组件调试面板
- 状态检查工具
- 性能监控
- 错误边界
### 生产模式优化
- 错误处理
- 性能监控
- 用户体验优化
- 日志记录
## 📱 响应式设计
### 断点系统
```css
/* TailwindCSS 断点 */
sm: 640px /* 小型设备 */
md: 768px /* 中型设备 */
lg: 1024px /* 大型设备 */
xl: 1280px /* 超大设备 */
2xl: 1536px /* 2K 显示器 */
```
### 移动端优化
- 触摸手势支持
- 移动端菜单
- 响应式图片
- 性能优化
## 🧪 测试策略
### 单元测试
```bash
# 运行单元测试
pnpm test
# 生成覆盖率报告
pnpm test:coverage
```
### 集成测试
- 组件测试
- 页面测试
- 用户流程测试
### E2E 测试
- 浏览器自动化测试
- 用户交互测试
- 跨浏览器兼容性
## 📚 最佳实践
### 代码规范
- ESLint 配置
- Prettier 格式化
- TypeScript 严格模式
- 组件命名约定
### 性能最佳实践
- React.memo 优化
- useMemo 和 useCallback
- 虚拟滚动
- 懒加载策略
### 安全最佳实践
- XSS 防护
- CSRF 保护
- 内容安全策略
- 依赖安全管理
## 🤝 贡献指南
### 开发流程
1. Fork 项目
2. 创建功能分支
3. 提交代码更改
4. 创建 Pull Request
### 代码规范
- 使用 TypeScript
- 遵循 ESLint 规则
- 添加必要的注释
- 编写测试用例
## 📄 许可证
本项目采用 MIT 许可证。详见 [LICENSE](LICENSE) 文件。
## 🙏 致谢
感谢以下开源项目的支持:
- [React](https://react.dev/)
- [Vite](https://vitejs.dev/)
- [TailwindCSS](https://tailwindcss.com/)
- [Radix UI](https://www.radix-ui.com/)
- [Framer Motion](https://www.framer.com/motion/)
## 📞 联系我们
如有问题或建议,请通过以下方式联系:
- 创建 [Issue](../../issues)
- 发送邮件至 [team@xagi.dev](mailto:team@xagi.dev)
- 访问我们的 [官网](https://vite-plugin-appdev-design-mode.dev/)
---
**© 2024 XAGI Team. All rights reserved.**

View File

@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Complete showcase of vite-plugin-appdev-design-mode latest features - Visual component editor with real-time design system management" />
<meta name="keywords" content="design mode, visual editor, component library, react, vite, tailwindcss, radix-ui" />
<meta name="author" content="XAGI Team" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://vite-plugin-appdev-design-mode.dev/" />
<meta property="og:title" content="AppDev Design Mode - Full Featured Showcase" />
<meta property="og:description" content="Complete showcase of vite-plugin-appdev-design-mode latest features" />
<meta property="og:image" content="/og-image.png" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://vite-plugin-appdev-design-mode.dev/" />
<meta property="twitter:title" content="AppDev Design Mode - Full Featured Showcase" />
<meta property="twitter:description" content="Complete showcase of vite-plugin-appdev-design-mode latest features" />
<meta property="twitter:image" content="/og-image.png" />
<!-- Preload critical fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<title>AppDev Design Mode - Full Featured Showcase</title>
<style>
/* Critical CSS for loading state */
.loading-screen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
color: white;
font-family: 'Inter', sans-serif;
}
.loading-logo {
width: 80px;
height: 80px;
margin-bottom: 20px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-text {
font-size: 24px;
font-weight: 600;
margin-bottom: 10px;
opacity: 0;
animation: fadeInUp 0.8s ease-out 0.2s forwards;
}
.loading-subtitle {
font-size: 14px;
opacity: 0.8;
opacity: 0;
animation: fadeInUp 0.8s ease-out 0.4s forwards;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* Design mode debug styles */
[data-design-mode-active] {
outline: 2px solid #3b82f6 !important;
outline-offset: 2px !important;
position: relative;
}
[data-design-mode-active]::before {
content: attr(data-design-mode-component);
position: absolute;
top: -8px;
left: -2px;
background: #3b82f6;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-family: monospace;
z-index: 1000;
pointer-events: none;
}
/* Component highlight styles */
.component-highlight {
transition: all 0.2s ease-in-out;
border: 2px solid transparent;
}
.component-highlight:hover {
border-color: #3b82f6;
background-color: rgba(59, 130, 246, 0.05);
}
</style>
</head>
<body>
<div id="root">
<!-- Loading screen shown while React loads -->
<div class="loading-screen" id="loading-screen">
<div class="loading-logo"></div>
<div class="loading-text">AppDev Design Mode</div>
<div class="loading-subtitle">Full Featured Showcase</div>
</div>
</div>
<!-- Error boundary for critical failures -->
<script>
window.addEventListener('error', function(e) {
console.error('Critical error:', e.error);
const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) {
loadingScreen.innerHTML = `
<div style="text-align: center; padding: 20px;">
<h2 style="color: #ef4444; margin-bottom: 10px;">Something went wrong</h2>
<p style="opacity: 0.8; margin-bottom: 20px;">Failed to load the application</p>
<button
onclick="window.location.reload()"
style="
background: #3b82f6;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
"
>
Reload Page
</button>
</div>
`;
}
});
// Hide loading screen once React has loaded
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen && loadingScreen.parentElement) {
loadingScreen.style.opacity = '0';
loadingScreen.style.transition = 'opacity 0.5s ease-out';
setTimeout(function() {
loadingScreen.remove();
}, 500);
}
}, 1000);
});
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,77 @@
{
"name": "vite-plugin-appdev-design-mode-full-featured-example",
"version": "1.0.0",
"description": "Complete showcase of vite-plugin-appdev-design-mode latest features",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"type-check": "tsc --noEmit",
"lint": "eslint src --ext .ts,.tsx",
"clean": "rm -rf dist"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.4",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-toolbar": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.17.0",
"clsx": "^2.1.0",
"framer-motion": "^10.18.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-hook-form": "^7.49.2",
"react-router-dom": "^6.21.0",
"recharts": "^2.10.1",
"tailwind-merge": "^2.2.0",
"zustand": "^4.4.7"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@types/babel__core": "^7.20.5",
"@types/node": "^20.11.0",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.33",
"tailwindcss": "^4.1.0",
"typescript": "^5.6.3",
"vite": "^6.0.0"
},
"keywords": [
"vite",
"react",
"design-mode",
"component-editor",
"visual-editor",
"radix-ui",
"tailwindcss"
],
"author": "XAGI Team",
"license": "MIT"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
export default {
plugins: {
autoprefixer: {},
},
}

View File

@@ -0,0 +1,318 @@
/**
* @fileoverview 主应用组件
* @description 负责应用的路由配置、布局和设计模式集成
*/
import React, { Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { useDesignModeStore } from './main';
// 页面组件(懒加载)
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const ComponentEditor = React.lazy(() => import('./pages/ComponentEditor'));
const DesignSystem = React.lazy(() => import('./pages/DesignSystem'));
const LivePreview = React.lazy(() => import('./pages/LivePreview'));
// 布局组件
import AppLayout from './components/layout/AppLayout';
import LoadingSpinner from './components/ui/LoadingSpinner';
// 页面过渡动画配置
const pageVariants = {
initial: {
opacity: 0,
y: 20,
},
in: {
opacity: 1,
y: 0,
},
out: {
opacity: 0,
y: -20,
},
};
const pageTransition = {
type: 'tween',
ease: 'anticipate',
duration: 0.3,
};
/**
* 页面容器组件 - 提供统一的页面布局和动画
*/
const PageContainer: React.FC<{
children: React.ReactNode;
title: string;
description?: string;
className?: string;
}> = ({ children, title, description, className = '' }) => (
<motion.div
initial="initial"
animate="in"
exit="out"
variants={pageVariants}
transition={pageTransition}
className={`page-container ${className}`}
data-page="main-content"
data-component="page-container"
data-element="content-wrapper"
>
<div
className="page-header mb-6"
data-element="page-header"
>
<h1
className="text-3xl font-bold text-neutral-900 dark:text-neutral-100"
data-element="page-title"
data-design-mode-component="PageTitle"
>
{title}
</h1>
{description && (
<p
className="mt-2 text-neutral-600 dark:text-neutral-400"
data-element="page-description"
>
{description}
</p>
)}
</div>
<div
className="page-content"
data-element="page-content"
>
{children}
</div>
</motion.div>
);
/**
* 页面路由配置组件
*/
const AppRoutes: React.FC = () => {
return (
<AppLayout>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
{/* 默认重定向到仪表盘 */}
<Route
path="/"
element={
<Navigate
to="/dashboard"
replace
data-nav="/dashboard"
data-component="Navigation"
/>
}
/>
{/* 仪表盘页面 */}
<Route
path="/dashboard"
element={
<PageContainer
title="Dashboard"
description="Overview of design mode features and system status"
>
<Dashboard />
</PageContainer>
}
/>
{/* 组件编辑器页面 */}
<Route
path="/editor"
element={
<PageContainer
title="Component Editor"
description="Visual editor for building and customizing components"
className="h-full"
>
<ComponentEditor />
</PageContainer>
}
/>
{/* 设计系统页面 */}
<Route
path="/design-system"
element={
<PageContainer
title="Design System"
description="Manage colors, typography, spacing, and component variants"
>
<DesignSystem />
</PageContainer>
}
/>
{/* 实时预览页面 */}
<Route
path="/preview"
element={
<PageContainer
title="Live Preview"
description="Real-time preview of components and design changes"
className="h-full"
>
<LivePreview />
</PageContainer>
}
/>
{/* 404 页面 */}
<Route
path="*"
element={
<PageContainer title="404 - Page Not Found">
<div className="text-center py-12">
<h2 className="text-2xl font-semibold text-neutral-900 mb-4">
Page Not Found
</h2>
<p className="text-neutral-600 mb-6">
The page you're looking for doesn't exist.
</p>
<a
href="/dashboard"
className="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors"
data-action="navigate"
data-component="Button"
>
Go to Dashboard
</a>
</div>
</PageContainer>
}
/>
</Routes>
</Suspense>
</AppLayout>
);
};
/**
* 设计模式集成组件
*/
const DesignModeIntegration: React.FC = () => {
const { isDesignModeEnabled, selectedComponent, hoveredComponent } = useDesignModeStore();
// 为组件添加设计模式属性
React.useEffect(() => {
if (!isDesignModeEnabled) return;
const addDesignModeAttributes = () => {
const components = document.querySelectorAll('[data-component]');
components.forEach((element, index) => {
const componentName = element.getAttribute('data-component');
const elementName = element.getAttribute('data-element');
const action = element.getAttribute('data-action');
// 添加设计模式属性
element.setAttribute('data-design-mode', 'true');
element.setAttribute('data-design-mode-component', componentName || 'Unknown');
if (elementName) {
element.setAttribute('data-design-mode-element', elementName);
}
if (action) {
element.setAttribute('data-design-mode-action', action);
}
});
};
// 监听 DOM 变化
const observer = new MutationObserver(addDesignModeAttributes);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
});
// 初始调用
addDesignModeAttributes();
return () => {
observer.disconnect();
};
}, [isDesignModeEnabled]);
// 添加全局样式
React.useEffect(() => {
if (isDesignModeEnabled) {
const style = document.createElement('style');
style.id = 'design-mode-styles';
style.textContent = `
[data-design-mode="true"] {
cursor: crosshair;
}
[data-design-mode="true"]:hover {
outline: 2px solid #3b82f6 !important;
outline-offset: 2px !important;
background-color: rgba(59, 130, 246, 0.05) !important;
}
[data-design-mode-active="true"] {
outline: 3px solid #ef4444 !important;
outline-offset: 2px !important;
background-color: rgba(239, 68, 68, 0.1) !important;
}
[data-design-mode-hover="true"] {
outline: 2px solid #f59e0b !important;
outline-offset: 2px !important;
background-color: rgba(245, 158, 11, 0.1) !important;
}
`;
document.head.appendChild(style);
return () => {
document.getElementById('design-mode-styles')?.remove();
};
}
}, [isDesignModeEnabled]);
return null;
};
/**
* 主应用组件
*/
const App: React.FC = () => {
const { isDesignModeEnabled } = useDesignModeStore();
return (
<div
className={`app ${isDesignModeEnabled ? 'design-mode-enabled' : ''}`}
data-app="full-featured-showcase"
data-component="App"
data-design-mode={isDesignModeEnabled ? 'true' : 'false'}
>
{/* 设计模式集成 */}
<DesignModeIntegration />
{/* 主要内容 */}
<AnimatePresence mode="wait">
<AppRoutes />
</AnimatePresence>
{/* 开发模式信息 */}
{import.meta.env.DEV && (
<div
className="fixed bottom-4 right-4 bg-neutral-800 text-white p-3 rounded-lg text-xs z-50"
data-component="DevInfo"
data-element="debug-panel"
>
<div>Design Mode: {isDesignModeEnabled ? 'Enabled' : 'Disabled'}</div>
<div>React: {React.version}</div>
<div>Vite: {import.meta.env.MODE}</div>
</div>
)}
</div>
);
};
export default App;

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;

View File

@@ -0,0 +1,388 @@
/**
* @fileoverview 全局样式文件
* @description 包含 TailwindCSS 导入、基础样式重置和应用级样式
*/
/* TailwindCSS 基础导入 */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 自定义 CSS 变量 */
:root {
/* 颜色变量 */
--color-primary-50: #eff6ff;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
--color-secondary-50: #faf5ff;
--color-secondary-500: #a855f7;
--color-secondary-600: #9333ea;
/* 字体变量 */
--font-family-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
--font-family-mono: 'JetBrains Mono', ui-monospace, monospace;
/* 阴影变量 */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
/* 圆角变量 */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
}
/* 暗色主题变量 */
.dark {
--color-bg-primary: #0a0a0a;
--color-bg-secondary: #171717;
--color-text-primary: #fafafa;
--color-text-secondary: #a3a3a3;
}
/* 基础样式重置 */
* {
box-sizing: border-box;
}
html {
font-family: var(--font-family-sans);
line-height: 1.5;
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
padding: 0;
min-height: 100vh;
background-color: #fafafa;
color: #171717;
overflow-x: hidden;
}
/* 应用容器样式 */
.app {
min-height: 100vh;
position: relative;
transition: all 0.3s ease;
}
.app.design-mode-enabled {
cursor: crosshair;
}
/* 页面容器 */
.page-container {
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
padding-top: 2rem;
padding-bottom: 2rem;
}
.page-header {
@apply mb-8;
border-bottom: 1px solid #e5e5e5;
padding-bottom: 1.5rem;
}
.page-content {
@apply w-full;
}
/* 布局组件样式 */
.layout-container {
@apply min-h-screen flex flex-col;
}
.layout-sidebar {
@apply w-64 bg-white dark:bg-neutral-900 border-r border-neutral-200 dark:border-neutral-800;
transition: width 0.3s ease;
}
.layout-main {
@apply flex-1 flex flex-col overflow-hidden;
}
.layout-header {
@apply h-16 bg-white dark:bg-neutral-900 border-b border-neutral-200 dark:border-neutral-800 flex items-center px-6;
}
.layout-content {
@apply flex-1 overflow-auto bg-neutral-50 dark:bg-neutral-800;
}
/* 组件高亮样式 */
.component-highlight {
@apply transition-all duration-200 ease-in-out;
position: relative;
}
.component-highlight:hover {
@apply bg-primary-50 dark:bg-primary-900/20;
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
/* 设计模式样式 */
[data-design-mode="true"] {
transition: all 0.2s ease;
}
[data-design-mode="true"]:hover {
@apply bg-primary-50 border-primary-200;
outline: 2px solid theme('colors.primary.500');
outline-offset: 2px;
}
[data-design-mode-active="true"] {
@apply bg-error-50 border-error-200;
outline: 3px solid theme('colors.error.500');
outline-offset: 2px;
animation: pulse-error 1s ease-in-out;
}
[data-design-mode-hover="true"] {
@apply bg-warning-50 border-warning-200;
outline: 2px solid theme('colors.warning.500');
outline-offset: 2px;
}
@keyframes pulse-error {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* 表单组件样式 */
.form-group {
@apply space-y-2;
}
.form-label {
@apply block text-sm font-medium text-neutral-700 dark:text-neutral-300;
}
.form-input {
@apply w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-md shadow-sm;
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500;
@apply dark:bg-neutral-700 dark:text-neutral-100;
}
.form-input:invalid {
@apply border-error-300 focus:ring-error-500 focus:border-error-500;
}
/* 按钮组件样式 */
.btn {
@apply inline-flex items-center justify-center px-4 py-2 border font-medium rounded-md;
@apply transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2;
@apply disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-primary {
@apply btn bg-primary-600 border-primary-600 text-white;
@apply hover:bg-primary-700 hover:border-primary-700 focus:ring-primary-500;
}
.btn-secondary {
@apply btn bg-neutral-200 border-neutral-300 text-neutral-900;
@apply hover:bg-neutral-300 hover:border-neutral-400 focus:ring-neutral-500;
@apply dark:bg-neutral-700 dark:border-neutral-600 dark:text-neutral-100;
@apply dark:hover:bg-neutral-600 dark:hover:border-neutral-500;
}
.btn-outline {
@apply btn border-primary-600 text-primary-600 bg-transparent;
@apply hover:bg-primary-600 hover:text-white focus:ring-primary-500;
}
.btn-sm {
@apply px-3 py-1.5 text-sm;
}
.btn-lg {
@apply px-6 py-3 text-lg;
}
/* 卡片组件样式 */
.card {
@apply bg-white dark:bg-neutral-900 rounded-lg shadow border border-neutral-200 dark:border-neutral-800;
@apply transition-shadow duration-200;
}
.card:hover {
box-shadow: var(--shadow-lg);
}
.card-header {
@apply px-6 py-4 border-b border-neutral-200 dark:border-neutral-800;
}
.card-body {
@apply px-6 py-4;
}
.card-footer {
@apply px-6 py-4 border-t border-neutral-200 dark:border-neutral-800;
}
/* 模态框样式 */
.modal-overlay {
@apply fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50;
backdrop-filter: blur(4px);
}
.modal-content {
@apply bg-white dark:bg-neutral-900 rounded-lg shadow-xl max-w-md w-full mx-4;
@apply animate-in fade-in duration-200;
}
/* 工具提示样式 */
.tooltip {
@apply absolute z-50 px-2 py-1 text-xs text-white bg-neutral-900 rounded shadow-lg;
@apply pointer-events-none opacity-0 transition-opacity duration-200;
}
.tooltip.show {
@apply opacity-100;
}
/* 加载状态样式 */
.loading-spinner {
@apply inline-block w-4 h-4 border-2 border-current border-t-transparent rounded-full;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-dots {
@apply flex space-x-1;
}
.loading-dots > div {
@apply w-2 h-2 bg-current rounded-full;
animation: loading-dots 1.4s ease-in-out infinite both;
}
.loading-dots > div:nth-child(1) { animation-delay: -0.32s; }
.loading-dots > div:nth-child(2) { animation-delay: -0.16s; }
@keyframes loading-dots {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
/* 响应式设计增强 */
@media (max-width: 768px) {
.layout-sidebar {
@apply fixed inset-y-0 left-0 z-40 transform -translate-x-full;
transition: transform 0.3s ease;
}
.layout-sidebar.open {
@apply translate-x-0;
}
.page-container {
@apply px-4;
padding-top: 1rem;
padding-bottom: 1rem;
}
}
/* 无障碍支持 */
.sr-only {
@apply absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0;
clip: rect(0, 0, 0, 0);
}
/* 高对比度模式支持 */
@media (prefers-contrast: high) {
.card {
@apply border-2;
}
.btn {
@apply border-2;
}
}
/* 减少动画支持 */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* 打印样式 */
@media print {
.layout-sidebar,
.layout-header {
@apply hidden;
}
.layout-content {
@apply ml-0;
}
.page-container {
@apply max-w-none px-0;
}
}
/* 自定义滚动条 */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: theme('colors.neutral.400') theme('colors.neutral.100');
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: theme('colors.neutral.100');
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: theme('colors.neutral.400');
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: theme('colors.neutral.500');
}
/* 暗色主题滚动条 */
.dark .custom-scrollbar {
scrollbar-color: theme('colors.neutral.600') theme('colors.neutral.800');
}
.dark .custom-scrollbar::-webkit-scrollbar-track {
background: theme('colors.neutral.800');
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background: theme('colors.neutral.600');
}
.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: theme('colors.neutral.500');
}

View File

@@ -0,0 +1,215 @@
/**
* @fileoverview 应用程序入口文件
* @description 负责初始化 React 应用、路由、状态管理和设计模式桥接
*/
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
import { devtools } from 'zustand/middleware';
import { AnimatePresence } from 'framer-motion';
// 导入应用样式
import './index.css';
// 导入主要组件
import App from './App';
// 创建设计模式状态存储
const useDesignModeStore = create<{
isDesignModeEnabled: boolean;
selectedComponent: any;
hoveredComponent: any;
componentTree: any[];
toggleDesignMode: () => void;
setSelectedComponent: (component: any) => void;
setHoveredComponent: (component: any) => void;
setComponentTree: (tree: any[]) => void;
clearSelection: () => void;
}>()(
devtools(
subscribeWithSelector((set, get) => ({
// 设计模式状态
isDesignModeEnabled: true,
selectedComponent: null,
hoveredComponent: null,
componentTree: [],
// 操作方法
toggleDesignMode: () => set((state) => ({
isDesignModeEnabled: !state.isDesignModeEnabled
})),
setSelectedComponent: (component) => set({ selectedComponent: component }),
setHoveredComponent: (component) => set({ hoveredComponent: component }),
setComponentTree: (tree) => set({ componentTree: tree }),
// 清除选择
clearSelection: () => set({
selectedComponent: null,
hoveredComponent: null
}),
})),
{
name: 'design-mode-store',
}
)
);
// 创建 React Query 客户端配置
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5分钟
gcTime: 1000 * 60 * 30, // 30分钟
retry: (failureCount, error) => {
if (error instanceof Error && error.message.includes('404')) {
return false;
}
return failureCount < 3;
},
refetchOnWindowFocus: false,
},
mutations: {
retry: 1,
},
},
});
// 错误处理组件
const ErrorBoundary: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [hasError, setHasError] = React.useState(false);
const [error, setError] = React.useState<Error | null>(null);
React.useEffect(() => {
const handleError = (error: ErrorEvent) => {
console.error('Global error caught:', error);
setHasError(true);
setError(error.error || new Error(error.message));
};
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
console.error('Unhandled promise rejection:', event.reason);
setHasError(true);
setError(event.reason instanceof Error ? event.reason : new Error(String(event.reason)));
};
window.addEventListener('error', handleError);
window.addEventListener('unhandledrejection', handleUnhandledRejection);
return () => {
window.removeEventListener('error', handleError);
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
};
}, []);
if (hasError) {
return (
<div className="min-h-screen flex items-center justify-center bg-neutral-50">
<div className="max-w-md p-6 bg-white rounded-lg shadow-lg">
<div className="flex items-center mb-4">
<div className="w-8 h-8 bg-error-100 rounded-full flex items-center justify-center mr-3">
<svg className="w-4 h-4 text-error-600" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<h2 className="text-lg font-semibold text-neutral-900">Application Error</h2>
</div>
<p className="text-neutral-600 mb-4">
Something went wrong while loading the application. Please try refreshing the page.
</p>
{error && (
<details className="mb-4 p-3 bg-neutral-50 rounded border">
<summary className="cursor-pointer text-sm font-medium text-neutral-700 mb-2">
Error Details
</summary>
<pre className="text-xs text-neutral-600 overflow-auto">
{error.message}
{error.stack && `\n\n${error.stack}`}
</pre>
</details>
)}
<div className="flex space-x-3">
<button
onClick={() => window.location.reload()}
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors"
>
Reload Page
</button>
<button
onClick={() => {
setHasError(false);
setError(null);
}}
className="flex-1 px-4 py-2 bg-neutral-200 text-neutral-700 rounded-md hover:bg-neutral-300 transition-colors"
>
Try Again
</button>
</div>
</div>
</div>
);
}
return <>{children}</>;
};
// 主应用组件
const RootApp: React.FC = () => {
return (
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<AnimatePresence mode="wait">
<App />
</AnimatePresence>
</BrowserRouter>
</QueryClientProvider>
</ErrorBoundary>
);
};
// 渲染应用
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error('Root element not found');
}
const root = ReactDOM.createRoot(rootElement);
root.render(<RootApp />);
// 开发环境下的热模块替换
if (import.meta.hot) {
import.meta.hot.accept();
}
// 设计模式桥接(如果存在)
declare global {
interface Window {
__APPDEV_DESIGN_MODE__?: {
store: typeof useDesignModeStore;
toggleDesignMode: () => void;
selectComponent: (component: any) => void;
highlightComponent: (component: any) => void;
};
}
}
// 初始化设计模式桥接
if (typeof window !== 'undefined') {
window.__APPDEV_DESIGN_MODE__ = {
store: useDesignModeStore,
toggleDesignMode: () => useDesignModeStore.getState().toggleDesignMode(),
selectComponent: (component) => useDesignModeStore.getState().setSelectedComponent(component),
highlightComponent: (component) => useDesignModeStore.getState().setHoveredComponent(component),
};
}
export { useDesignModeStore };
export default RootApp;

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;

View File

@@ -0,0 +1,599 @@
/**
* @fileoverview 设计系统状态管理
* @description 使用 Zustand 管理设计系统的全局状态
*/
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
import { devtools } from 'zustand/middleware';
import {
DesignSystemState,
DesignSystemAction,
DesignToken,
ComponentVariant,
ComponentLibrary,
DesignSystemConfig,
DesignSystemEvent
} from '../types/design-system';
/**
* 设计系统默认配置
*/
const defaultDesignSystemConfig: DesignSystemConfig = {
name: 'Full Featured Design System',
version: '1.0.0',
description: 'A comprehensive design system with tokens, components, and utilities',
author: 'XAGI Team',
license: 'MIT',
tokens: {
colors: [
{
name: 'primary.50',
value: '#eff6ff',
category: 'color',
description: 'Lightest primary color',
palette: 'primary',
scale: '50',
tags: ['primary', 'light', 'background']
},
{
name: 'primary.500',
value: '#3b82f6',
category: 'color',
description: 'Main primary color',
palette: 'primary',
scale: '500',
tags: ['primary', 'main', 'interactive']
},
{
name: 'primary.900',
value: '#1e3a8a',
category: 'color',
description: 'Darkest primary color',
palette: 'primary',
scale: '900',
tags: ['primary', 'dark', 'text']
},
{
name: 'neutral.50',
value: '#fafafa',
category: 'color',
description: 'Lightest neutral color',
palette: 'neutral',
scale: '50',
tags: ['neutral', 'light', 'background']
},
{
name: 'neutral.500',
value: '#737373',
category: 'color',
description: 'Main neutral color',
palette: 'neutral',
scale: '500',
tags: ['neutral', 'main', 'text']
},
{
name: 'neutral.900',
value: '#171717',
category: 'color',
description: 'Darkest neutral color',
palette: 'neutral',
scale: '900',
tags: ['neutral', 'dark', 'text']
}
],
typography: [
{
name: 'font-family.sans',
value: {
fontFamily: 'Inter, ui-sans-serif, system-ui, sans-serif',
},
category: 'typography',
description: 'Default sans-serif font family',
type: 'body',
tags: ['typography', 'font', 'sans-serif']
},
{
name: 'font-family.mono',
value: {
fontFamily: 'JetBrains Mono, ui-monospace, monospace',
},
category: 'typography',
description: 'Default monospace font family',
type: 'code',
tags: ['typography', 'font', 'monospace']
},
{
name: 'font-size.base',
value: {
fontSize: '1rem',
lineHeight: '1.5',
},
category: 'typography',
description: 'Base font size',
type: 'body',
scale: 'base',
tags: ['typography', 'font-size', 'base']
},
{
name: 'font-size.lg',
value: {
fontSize: '1.125rem',
lineHeight: '1.75',
},
category: 'typography',
description: 'Large font size',
type: 'body',
scale: 'lg',
tags: ['typography', 'font-size', 'large']
}
],
spacing: [
{
name: 'spacing.0',
value: '0',
category: 'spacing',
description: 'No spacing',
scale: '0',
tags: ['spacing', 'zero']
},
{
name: 'spacing.1',
value: '0.25rem',
category: 'spacing',
description: 'Extra small spacing',
scale: '1',
tags: ['spacing', 'extra-small']
},
{
name: 'spacing.4',
value: '1rem',
category: 'spacing',
description: 'Medium spacing',
scale: '4',
tags: ['spacing', 'medium']
},
{
name: 'spacing.8',
value: '2rem',
category: 'spacing',
description: 'Large spacing',
scale: '8',
tags: ['spacing', 'large']
}
],
shadows: [
{
name: 'shadow.sm',
value: {
offsetX: '0',
offsetY: '1px',
blurRadius: '2px',
spreadRadius: '0',
color: 'rgb(0 0 0 / 0.05)',
},
category: 'shadow',
description: 'Small shadow',
elevation: 1,
tags: ['shadow', 'small', 'subtle']
},
{
name: 'shadow.md',
value: {
offsetX: '0',
offsetY: '4px',
blurRadius: '6px',
spreadRadius: '-1px',
color: 'rgb(0 0 0 / 0.1)',
},
category: 'shadow',
description: 'Medium shadow',
elevation: 2,
tags: ['shadow', 'medium', 'elevated']
},
{
name: 'shadow.lg',
value: {
offsetX: '0',
offsetY: '10px',
blurRadius: '15px',
spreadRadius: '-3px',
color: 'rgb(0 0 0 / 0.1)',
},
category: 'shadow',
description: 'Large shadow',
elevation: 3,
tags: ['shadow', 'large', 'prominent']
}
],
borderRadius: [
{
name: 'radius.sm',
value: '0.125rem',
category: 'border-radius',
description: 'Small border radius',
shape: 'small',
tags: ['border-radius', 'small', 'subtle']
},
{
name: 'radius.md',
value: '0.375rem',
category: 'border-radius',
description: 'Medium border radius',
shape: 'medium',
tags: ['border-radius', 'medium', 'standard']
},
{
name: 'radius.lg',
value: '0.5rem',
category: 'border-radius',
description: 'Large border radius',
shape: 'large',
tags: ['border-radius', 'large', 'prominent']
},
{
name: 'radius.full',
value: '9999px',
category: 'border-radius',
description: 'Full border radius',
shape: 'full',
tags: ['border-radius', 'full', 'circular']
}
],
animations: [
{
name: 'duration.fast',
value: {
duration: '150ms',
timingFunction: 'ease-in-out',
},
category: 'animation',
description: 'Fast animation duration',
type: 'transition',
tags: ['animation', 'duration', 'fast']
},
{
name: 'duration.medium',
value: {
duration: '300ms',
timingFunction: 'ease-in-out',
},
category: 'animation',
description: 'Medium animation duration',
type: 'transition',
tags: ['animation', 'duration', 'medium']
},
{
name: 'duration.slow',
value: {
duration: '500ms',
timingFunction: 'ease-in-out',
},
category: 'animation',
description: 'Slow animation duration',
type: 'transition',
tags: ['animation', 'duration', 'slow']
}
]
},
meta: {
created: new Date().toISOString(),
updated: new Date().toISOString(),
tags: ['design-system', 'tokens', 'components'],
category: 'light'
}
};
/**
* 设计系统状态管理 Store
*/
const useDesignSystemStore = create<DesignSystemState & {
dispatch: React.Dispatch<DesignSystemAction>;
events: DesignSystemEvent[];
addEvent: (event: Omit<DesignSystemEvent, 'timestamp'>) => void;
clearEvents: () => void;
}>()(
devtools(
subscribeWithSelector((set, get) => ({
// 初始状态
config: defaultDesignSystemConfig,
libraries: [],
selectedToken: null,
selectedComponent: null,
selectedLibrary: null,
searchQuery: '',
filterBy: {
category: null,
status: null,
tags: []
},
viewMode: 'grid',
theme: 'light',
isLoading: false,
error: null,
// 事件历史
events: [],
// 分发器函数
dispatch: (action: DesignSystemAction) => {
set((state) => {
const newState = reducer(state, action);
// 添加事件
if (action.type !== 'RESET_STATE') {
get().addEvent({
type: getActionType(action),
payload: action.payload,
});
}
return newState;
});
},
// 事件管理
addEvent: (event: Omit<DesignSystemEvent, 'timestamp'>) => {
set((state) => ({
events: [
{
...event,
timestamp: new Date().toISOString()
},
...state.events.slice(0, 99) // 保留最近100个事件
]
}));
},
clearEvents: () => {
set({ events: [] });
},
// 便捷操作方法
setConfig: (config: DesignSystemConfig) => {
get().dispatch({ type: 'SET_CONFIG', payload: config });
},
setSelectedToken: (token: DesignToken | null) => {
get().dispatch({ type: 'SET_SELECTED_TOKEN', payload: token });
},
setSelectedComponent: (component: ComponentVariant | null) => {
get().dispatch({ type: 'SET_SELECTED_COMPONENT', payload: component });
},
setSearchQuery: (query: string) => {
get().dispatch({ type: 'SET_SEARCH_QUERY', payload: query });
},
setFilter: (filter: Partial<DesignSystemState['filterBy']>) => {
get().dispatch({ type: 'SET_FILTER', payload: filter });
},
resetState: () => {
get().dispatch({ type: 'RESET_STATE' });
},
})),
{
name: 'design-system-store',
}
)
);
/**
* Reducer 函数处理状态更新
*/
function reducer(state: DesignSystemState, action: DesignSystemAction): DesignSystemState {
switch (action.type) {
case 'SET_CONFIG':
return {
...state,
config: action.payload,
error: null,
};
case 'SET_LIBRARIES':
return {
...state,
libraries: action.payload,
error: null,
};
case 'ADD_LIBRARY':
return {
...state,
libraries: [...state.libraries, action.payload],
error: null,
};
case 'UPDATE_LIBRARY':
return {
...state,
libraries: state.libraries.map(lib =>
lib.id === action.payload.id ? action.payload : lib
),
error: null,
};
case 'DELETE_LIBRARY':
return {
...state,
libraries: state.libraries.filter(lib => lib.id !== action.payload),
error: null,
};
case 'SET_SELECTED_TOKEN':
return {
...state,
selectedToken: action.payload,
};
case 'SET_SELECTED_COMPONENT':
return {
...state,
selectedComponent: action.payload,
};
case 'SET_SELECTED_LIBRARY':
return {
...state,
selectedLibrary: action.payload,
};
case 'SET_SEARCH_QUERY':
return {
...state,
searchQuery: action.payload,
};
case 'SET_FILTER':
return {
...state,
filterBy: {
...state.filterBy,
...action.payload,
},
};
case 'SET_VIEW_MODE':
return {
...state,
viewMode: action.payload,
};
case 'SET_THEME':
return {
...state,
theme: action.payload,
};
case 'SET_LOADING':
return {
...state,
isLoading: action.payload,
};
case 'SET_ERROR':
return {
...state,
error: action.payload,
isLoading: false,
};
case 'RESET_STATE':
return {
...state,
selectedToken: null,
selectedComponent: null,
selectedLibrary: null,
searchQuery: '',
filterBy: {
category: null,
status: null,
tags: [],
},
error: null,
isLoading: false,
};
default:
return state;
}
}
/**
* 获取操作类型
*/
function getActionType(action: DesignSystemAction): DesignSystemEvent['type'] {
switch (action.type) {
case 'SET_CONFIG':
return 'library_imported';
case 'ADD_LIBRARY':
return 'library_imported';
case 'UPDATE_LIBRARY':
return 'component_updated';
case 'DELETE_LIBRARY':
return 'component_deleted';
case 'SET_SELECTED_TOKEN':
return action.payload ? 'token_created' : 'component_deleted';
case 'SET_SELECTED_COMPONENT':
return action.payload ? 'component_created' : 'component_deleted';
case 'SET_THEME':
return 'theme_switched';
default:
return 'token_updated';
}
}
/**
* 选择器函数
*/
export const useDesignTokens = () => {
const config = useDesignSystemStore(state => state.config);
return config?.tokens || { colors: [], typography: [], spacing: [], shadows: [], borderRadius: [], animations: [] };
};
export const useColors = () => {
const tokens = useDesignTokens();
return tokens.colors || [];
};
export const useTypography = () => {
const tokens = useDesignTokens();
return tokens.typography || [];
};
export const useSpacing = () => {
const tokens = useDesignTokens();
return tokens.spacing || [];
};
export const useShadows = () => {
const tokens = useDesignTokens();
return tokens.shadows || [];
};
export const useBorderRadius = () => {
const tokens = useDesignTokens();
return tokens.borderRadius || [];
};
export const useAnimations = () => {
const tokens = useDesignTokens();
return tokens.animations || [];
};
export const useLibraries = () => {
return useDesignSystemStore(state => state.libraries);
};
export const useSelectedToken = () => {
return useDesignSystemStore(state => state.selectedToken);
};
export const useSelectedComponent = () => {
return useDesignSystemStore(state => state.selectedComponent);
};
export const useSearchAndFilters = () => {
return useDesignSystemStore(state => ({
searchQuery: state.searchQuery,
filterBy: state.filterBy,
viewMode: state.viewMode,
theme: state.theme,
}));
};
export const useLoadingAndError = () => {
return useDesignSystemStore(state => ({
isLoading: state.isLoading,
error: state.error,
}));
};
export const useEvents = () => {
return useDesignSystemStore(state => state.events);
};
// 导出 Store 和选择器
export default useDesignSystemStore;

View File

@@ -0,0 +1,364 @@
/**
* @fileoverview 设计系统类型定义
* @description 定义设计系统相关的TypeScript类型和接口
*/
// 设计令牌基础类型
export interface DesignToken {
name: string;
value: string | number | Record<string, any>; // 允许复杂对象
category: 'color' | 'typography' | 'spacing' | 'shadow' | 'border-radius' | 'z-index' | 'animation';
description?: string;
deprecated?: boolean;
tags?: string[];
}
// 色彩令牌
export interface ColorToken extends DesignToken {
category: 'color';
value: string;
hue?: number;
saturation?: number;
lightness?: number;
alpha?: number;
palette?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'neutral';
scale?: string; // e.g., '50', '100', '500', '900'
}
// 字体令牌
export interface TypographyToken extends DesignToken {
category: 'typography';
value: {
fontFamily?: string;
fontSize?: string;
fontWeight?: number | string;
lineHeight?: string | number;
letterSpacing?: string;
textTransform?: 'none' | 'uppercase' | 'lowercase' | 'capitalize';
};
type?: 'heading' | 'body' | 'caption' | 'code' | 'display';
scale?: string;
}
// 间距令牌
export interface SpacingToken extends DesignToken {
category: 'spacing';
value: string | number;
scale?: string;
unit?: 'px' | 'rem' | 'em' | 'vh' | 'vw' | '%';
}
// 阴影令牌
export interface ShadowToken extends DesignToken {
category: 'shadow';
value: {
offsetX?: string | number;
offsetY?: string | number;
blurRadius?: string | number;
spreadRadius?: string | number;
color: string;
inset?: boolean;
};
elevation?: number;
}
// 圆角令牌
export interface BorderRadiusToken extends DesignToken {
category: 'border-radius';
value: string | number;
unit?: 'px' | 'rem' | 'em' | '%';
shape?: 'none' | 'small' | 'medium' | 'large' | 'full';
}
// 动画令牌
export interface AnimationToken extends DesignToken {
category: 'animation';
value: {
duration?: string;
timingFunction?: string;
delay?: string;
iterationCount?: number | 'infinite';
direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
fillMode?: 'none' | 'forwards' | 'backwards' | 'both';
};
type?: 'transition' | 'animation' | 'transform';
}
// 设计系统配置
export interface DesignSystemConfig {
name: string;
version: string;
description: string;
author: string;
license: string;
tokens: {
colors: ColorToken[];
typography: TypographyToken[];
spacing: SpacingToken[];
shadows: ShadowToken[];
borderRadius: BorderRadiusToken[];
animations: AnimationToken[];
};
meta: {
created: string;
updated: string;
tags: string[];
category: 'light' | 'dark' | 'auto';
};
}
// 组件变体
export interface ComponentVariant {
id: string;
name: string;
description: string;
category: string;
props: Record<string, any>;
styles: Record<string, any>;
preview?: string;
code?: string;
usage?: string;
accessibility?: string[];
status: 'draft' | 'review' | 'approved' | 'deprecated';
}
// 组件库
export interface ComponentLibrary {
id: string;
name: string;
version: string;
description: string;
author: string;
components: ComponentVariant[];
categories: string[];
tags: string[];
license: string;
dependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
}
// 设计系统状态
export interface DesignSystemState {
config: DesignSystemConfig | null;
libraries: ComponentLibrary[];
selectedToken: DesignToken | null;
selectedComponent: ComponentVariant | null;
selectedLibrary: ComponentLibrary | null;
searchQuery: string;
filterBy: {
category: string | null;
status: string | null;
tags: string[];
};
viewMode: 'grid' | 'list' | 'table';
theme: 'light' | 'dark' | 'auto';
isLoading: boolean;
error: string | null;
}
// 操作类型
export type DesignSystemAction =
| { type: 'SET_CONFIG'; payload: DesignSystemConfig }
| { type: 'SET_LIBRARIES'; payload: ComponentLibrary[] }
| { type: 'ADD_LIBRARY'; payload: ComponentLibrary }
| { type: 'UPDATE_LIBRARY'; payload: ComponentLibrary }
| { type: 'DELETE_LIBRARY'; payload: string }
| { type: 'SET_SELECTED_TOKEN'; payload: DesignToken | null }
| { type: 'SET_SELECTED_COMPONENT'; payload: ComponentVariant | null }
| { type: 'SET_SELECTED_LIBRARY'; payload: ComponentLibrary | null }
| { type: 'SET_SEARCH_QUERY'; payload: string }
| { type: 'SET_FILTER'; payload: Partial<DesignSystemState['filterBy']> }
| { type: 'SET_VIEW_MODE'; payload: 'grid' | 'list' | 'table' }
| { type: 'SET_THEME'; payload: 'light' | 'dark' | 'auto' }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null }
| { type: 'RESET_STATE' };
// 导出配置
export interface ExportConfig {
format: 'json' | 'css' | 'scss' | 'tailwind' | 'typescript' | 'figma';
include: {
tokens: boolean;
components: boolean;
utilities: boolean;
comments: boolean;
};
output: {
fileName?: string;
directory?: string;
structure: 'flat' | 'nested';
};
options: {
prefix?: string;
suffix?: string;
transform?: (value: any, key: string) => any;
filter?: (token: DesignToken) => boolean;
};
}
// 导入配置
export interface ImportConfig {
source: {
type: 'file' | 'url' | 'api' | 'clipboard';
path?: string;
url?: string;
api?: {
endpoint: string;
method: 'GET' | 'POST';
headers?: Record<string, string>;
body?: any;
};
};
options: {
merge: boolean;
validate: boolean;
backup: boolean;
overwrite: boolean;
};
}
// 验证结果
export interface ValidationResult {
isValid: boolean;
errors: ValidationError[];
warnings: ValidationWarning[];
suggestions: ValidationSuggestion[];
}
export interface ValidationError {
code: string;
message: string;
path: string;
severity: 'error' | 'critical';
fix?: string;
}
export interface ValidationWarning {
code: string;
message: string;
path: string;
severity: 'warning' | 'info';
suggestion?: string;
}
export interface ValidationSuggestion {
message: string;
action: string;
impact: 'low' | 'medium' | 'high';
}
// 设计系统分析
export interface DesignSystemAnalytics {
overview: {
totalTokens: number;
totalComponents: number;
totalLibraries: number;
coverage: number;
};
usage: {
mostUsedTokens: { token: string; count: number }[];
mostUsedComponents: { component: string; count: number }[];
componentDistribution: { category: string; count: number }[];
};
quality: {
accessibilityScore: number;
consistencyScore: number;
documentationScore: number;
testCoverage: number;
};
performance: {
bundleSize: number;
loadTime: number;
renderTime: number;
};
}
// 主题配置
export interface ThemeConfig {
id: string;
name: string;
displayName: string;
description: string;
type: 'light' | 'dark' | 'auto';
tokens: Partial<DesignSystemConfig['tokens']>;
overrides: Record<string, any>;
meta: {
author: string;
version: string;
tags: string[];
category: 'default' | 'accessibility' | 'branded' | 'custom';
};
}
// 组件生成器配置
export interface ComponentGeneratorConfig {
name: string;
template: string;
props: {
name: string;
type: string;
required: boolean;
default?: any;
description?: string;
}[];
styles: {
[key: string]: any;
};
variants: {
[key: string]: ComponentVariant;
};
accessibility: {
ariaRoles: string[];
ariaProperties: Record<string, string>;
keyboardNavigation: string[];
screenReaderSupport: string[];
};
}
// 设计系统工具函数参数
export interface ToolFunction {
id: string;
name: string;
description: string;
category: 'validation' | 'conversion' | 'generation' | 'analysis' | 'export';
parameters: {
name: string;
type: string;
required: boolean;
description: string;
default?: any;
}[];
returns: {
type: string;
description: string;
};
execute: (params: Record<string, any>) => Promise<any>;
}
// 设计系统事件
export interface DesignSystemEvent {
type: 'token_created' | 'token_updated' | 'token_deleted' | 'component_created' | 'component_updated' | 'component_deleted' | 'theme_switched' | 'library_imported' | 'library_exported';
payload: any;
timestamp: string;
userId?: string;
sessionId?: string;
}
// 设计系统通知
export interface DesignSystemNotification {
id: string;
type: 'info' | 'success' | 'warning' | 'error';
title: string;
message: string;
actions?: {
label: string;
action: string;
primary?: boolean;
}[];
timestamp: string;
read: boolean;
persistent?: boolean;
}
// 具体接口已经在上面定义了,这里不需要重复导出

View File

@@ -0,0 +1,395 @@
/**
* @fileoverview 工具函数集合
* @description 提供项目中常用的工具函数
*/
import { clsx, type ClassValue } from 'clsx';
/**
* 合并 CSS 类名
* @param inputs - CSS 类名数组
* @returns 合并后的 CSS 类名字符串
*/
export function cn(...inputs: ClassValue[]) {
return clsx(inputs);
}
/**
* 格式化日期
* @param date - 日期对象或字符串
* @param locale - 语言代码,默认 'zh-CN'
* @returns 格式化后的日期字符串
*/
export function formatDate(date: Date | string, locale: string = 'zh-CN'): string {
const dateObj = typeof date === 'string' ? new Date(date) : date;
return dateObj.toLocaleDateString(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
/**
* 格式化时间
* @param date - 日期对象或字符串
* @param locale - 语言代码,默认 'zh-CN'
* @returns 格式化后的时间字符串
*/
export function formatTime(date: Date | string, locale: string = 'zh-CN'): string {
const dateObj = typeof date === 'string' ? new Date(date) : date;
return dateObj.toLocaleTimeString(locale, {
hour: '2-digit',
minute: '2-digit',
});
}
/**
* 格式化日期时间
* @param date - 日期对象或字符串
* @param locale - 语言代码,默认 'zh-CN'
* @returns 格式化后的日期时间字符串
*/
export function formatDateTime(
date: Date | string,
locale: string = 'zh-CN'
): string {
const dateObj = typeof date === 'string' ? new Date(date) : date;
return `${formatDate(dateObj, locale)} ${formatTime(dateObj, locale)}`;
}
/**
* 生成随机 ID
* @param length - ID 长度,默认 8
* @returns 随机 ID 字符串
*/
export function generateId(length: number = 8): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 防抖函数
* @param func - 要防抖的函数
* @param delay - 延迟时间(毫秒)
* @returns 防抖后的函数
*/
export function debounce<T extends (...args: any[]) => any>(
func: T,
delay: number
): (...args: Parameters<T>) => void {
let timeoutId: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(null, args), delay);
};
}
/**
* 节流函数
* @param func - 要节流的函数
* @param delay - 延迟时间(毫秒)
* @returns 节流后的函数
*/
export function throttle<T extends (...args: any[]) => any>(
func: T,
delay: number
): (...args: Parameters<T>) => void {
let lastCall = 0;
return (...args: Parameters<T>) => {
const now = new Date().getTime();
if (now - lastCall < delay) {
return;
}
lastCall = now;
return func.apply(null, args);
};
}
/**
* 深拷贝对象
* @param obj - 要拷贝的对象
* @returns 深拷贝后的对象
*/
export function deepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime()) as unknown as T;
}
if (obj instanceof Array) {
return obj.map(item => deepClone(item)) as unknown as T;
}
if (typeof obj === 'object') {
const clonedObj = {} as T;
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}
return clonedObj;
}
return obj;
}
/**
* 检查是否为移动设备
* @returns 是否为移动设备
*/
export function isMobile(): boolean {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
}
/**
* 检查是否为平板设备
* @returns 是否为平板设备
*/
export function isTablet(): boolean {
return /iPad|Android(?=.*\bTablet\b)|Windows(?=.*\bTouch\b)/i.test(
navigator.userAgent
);
}
/**
* 检查是否支持触摸
* @returns 是否支持触摸
*/
export function isTouchDevice(): boolean {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
}
/**
* 获取文件扩展名
* @param filename - 文件名
* @returns 文件扩展名
*/
export function getFileExtension(filename: string): string {
return filename.slice(((filename.lastIndexOf('.') - 1) >>> 0) + 2);
}
/**
* 格式化文件大小
* @param bytes - 字节数
* @param decimals - 小数位数,默认 2
* @returns 格式化后的文件大小字符串
*/
export function formatFileSize(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
/**
* 复制文本到剪贴板
* @param text - 要复制的文本
* @returns Promise<boolean> 是否复制成功
*/
export async function copyToClipboard(text: string): Promise<boolean> {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return true;
} else {
// 降级方案
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const result = document.execCommand('copy');
textArea.remove();
return result;
}
} catch (error) {
console.error('Failed to copy text:', error);
return false;
}
}
/**
* 下载文件
* @param content - 文件内容
* @param filename - 文件名
* @param mimeType - MIME 类型
*/
export function downloadFile(
content: string,
filename: string,
mimeType: string = 'text/plain'
): void {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
/**
* 获取 URL 参数
* @param key - 参数名
* @returns 参数值或 null
*/
export function getUrlParam(key: string): string | null {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(key);
}
/**
* 设置 URL 参数
* @param key - 参数名
* @param value - 参数值
*/
export function setUrlParam(key: string, value: string): void {
const url = new URL(window.location.href);
url.searchParams.set(key, value);
window.history.replaceState({}, '', url.toString());
}
/**
* 移除 URL 参数
* @param key - 参数名
*/
export function removeUrlParam(key: string): void {
const url = new URL(window.location.href);
url.searchParams.delete(key);
window.history.replaceState({}, '', url.toString());
}
/**
* 检查对象是否为空
* @param obj - 要检查的对象
* @returns 是否为空
*/
export function isEmpty(obj: any): boolean {
if (obj == null) return true;
if (Array.isArray(obj) || typeof obj === 'string') return obj.length === 0;
if (obj instanceof Date) return false;
return Object.keys(obj).length === 0;
}
/**
* 唯一数组
* @param array - 原数组
* @returns 去重后的数组
*/
export function uniqueArray<T>(array: T[]): T[] {
return [...new Set(array)];
}
/**
* 数组分组
* @param array - 原数组
* @param size - 每组大小
* @returns 分组后的二维数组
*/
export function chunkArray<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
/**
* 对象数组根据某个属性分组
* @param array - 对象数组
* @param key - 分组键
* @returns 分组后的对象
*/
export function groupBy<T extends Record<string, any>>(
array: T[],
key: keyof T
): Record<string, T[]> {
return array.reduce((groups, item) => {
const group = String(item[key]);
groups[group] = groups[group] || [];
groups[group].push(item);
return groups;
}, {} as Record<string, T[]>);
}
/**
* 等待指定时间
* @param ms - 等待时间(毫秒)
* @returns Promise
*/
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 重试函数
* @param fn - 要重试的函数
* @param retries - 重试次数
* @param delay - 重试间隔(毫秒)
* @returns Promise
*/
export async function retry<T>(
fn: () => Promise<T>,
retries: number = 3,
delay: number = 1000
): Promise<T> {
try {
return await fn();
} catch (error) {
if (retries > 0) {
await sleep(delay);
return retry(fn, retries - 1, delay);
}
throw error;
}
}
/**
* 获取随机颜色
* @returns 随机颜色十六进制字符串
*/
export function getRandomColor(): string {
return '#' + Math.floor(Math.random() * 16777215).toString(16);
}
/**
* 计算相对时间
* @param date - 日期
* @returns 相对时间字符串
*/
export function getRelativeTime(date: Date | string): string {
const now = new Date();
const target = typeof date === 'string' ? new Date(date) : date;
const diffInSeconds = Math.floor((now.getTime() - target.getTime()) / 1000);
if (diffInSeconds < 60) {
return '刚刚';
} else if (diffInSeconds < 3600) {
return `${Math.floor(diffInSeconds / 60)} 分钟前`;
} else if (diffInSeconds < 86400) {
return `${Math.floor(diffInSeconds / 3600)} 小时前`;
} else if (diffInSeconds < 2592000) {
return `${Math.floor(diffInSeconds / 86400)} 天前`;
} else {
return formatDate(target);
}
}

View File

@@ -0,0 +1,139 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
// 设计系统色彩配置
colors: {
// 主色调 - 蓝色系
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
// 次要色调 - 紫色系
secondary: {
50: '#faf5ff',
100: '#f3e8ff',
200: '#e9d5ff',
300: '#d8b4fe',
400: '#c084fc',
500: '#a855f7',
600: '#9333ea',
700: '#7c3aed',
800: '#6b21a8',
900: '#581c87',
950: '#3b0764',
},
// 成功色调 - 绿色系
success: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
950: '#052e16',
},
// 警告色调 - 黄色系
warning: {
50: '#fefce8',
100: '#fef9c3',
200: '#fef08a',
300: '#fde047',
400: '#facc15',
500: '#eab308',
600: '#ca8a04',
700: '#a16207',
800: '#854d0e',
900: '#713f12',
950: '#422006',
},
// 错误色调 - 红色系
error: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
950: '#450a0a',
},
// 中性色调 - 灰色系
neutral: {
50: '#fafafa',
100: '#f5f5f5',
200: '#e5e5e5',
300: '#d4d4d4',
400: '#a3a3a3',
500: '#737373',
600: '#525252',
700: '#404040',
800: '#262626',
900: '#171717',
950: '#0a0a0a',
},
},
// 字体配置
fontFamily: {
sans: [
'Inter',
'ui-sans-serif',
'system-ui',
'sans-serif'
],
mono: [
'JetBrains Mono',
'ui-monospace',
'monospace'
],
},
// 间距配置
spacing: {
'18': '4.5rem',
'88': '22rem',
'128': '32rem',
'144': '36rem',
},
// 圆角配置
borderRadius: {
'xs': '0.125rem',
'sm': '0.25rem',
'md': '0.375rem',
'lg': '0.5rem',
'xl': '0.75rem',
'2xl': '1rem',
'3xl': '1.5rem',
'4xl': '2rem',
},
},
},
// 插件配置 - 移除可能导致问题的插件
plugins: [],
// 暗色模式配置
darkMode: 'class',
}

View File

@@ -0,0 +1,89 @@
{
"compilerOptions": {
// 目标版本
"target": "ES2022",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
// 模块系统
"module": "ESNext",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
// 严格模式设置
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
// 路径映射
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@pages/*": ["./src/pages/*"],
"@hooks/*": ["./src/hooks/*"],
"@store/*": ["./src/store/*"],
"@utils/*": ["./src/utils/*"],
"@types/*": ["./src/types/*"]
},
// 类型检查选项
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
// 实验性功能
"allowJs": true,
"checkJs": false,
// React 相关配置
"jsx": "react-jsx",
"jsxImportSource": "react",
// 装饰器支持(如果需要)
"experimentalDecorators": false,
"emitDecoratorMetadata": false,
// 输出配置
"declaration": false,
"declarationMap": false,
"sourceMap": true,
// 其他配置
"removeComments": false,
"preserveConstEnums": true,
// 性能优化
"incremental": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsbuildinfo.json"
},
"include": [
"src/**/*",
"vite.config.ts",
"tailwind.config.js",
"postcss.config.js"
],
"exclude": [
"node_modules",
"dist",
"build",
"**/*.test.ts",
"**/*.test.tsx",
"**/*.spec.ts",
"**/*.spec.tsx"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"skipLibCheck": true
},
"include": [
"vite.config.ts",
"tailwind.config.js",
"postcss.config.js"
]
}

View File

@@ -0,0 +1,50 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import appdevDesignMode from '../../packages/plugin/src/index';
export default defineConfig({
plugins: [
react(),
// AppDev Design Mode 插件配置
appdevDesignMode({
enabled: true,
enableInProduction: false,
attributePrefix: 'design-mode',
verbose: false, // 关闭详细日志避免频繁输出
exclude: [
'node_modules',
'.git',
'dist',
'build',
'**/*.test.{ts,tsx,js,jsx}',
'**/*.spec.{ts,tsx,js,jsx}',
'**/__tests__/**',
'**/tests/**',
],
include: [
'src/**/*.{ts,tsx,js,jsx}',
],
}),
],
server: {
port: 5177,
host: true,
open: false, // 不自动打开浏览器
},
build: {
outDir: 'dist',
sourcemap: true,
},
resolve: {
alias: {
'@': '/src',
},
},
css: {
devSourcemap: true,
},
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AppDev Design Mode Example1</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
{
"name": "vite-plugin-appdev-design-mode-react-example",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.0"
}
}

View File

@@ -0,0 +1,473 @@
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.App {
min-height: 100vh;
color: #333;
}
/* 头部样式 */
.main-header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 1rem 2rem;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
position: sticky;
top: 0;
z-index: 100;
}
.main-header .title {
font-size: 1.5rem;
font-weight: 700;
color: #4a5568;
margin-bottom: 0.5rem;
}
.navigation .nav-list {
display: flex;
list-style: none;
gap: 2rem;
}
.nav-link {
text-decoration: none;
color: #4a5568;
font-weight: 500;
transition: color 0.3s ease;
}
.nav-link:hover {
color: #667eea;
}
/* 主要内容区域 */
.main-content {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
/* 英雄区域 */
.hero-section {
text-align: center;
padding: 4rem 0;
margin-bottom: 3rem;
}
.hero-content {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
border-radius: 20px;
padding: 3rem;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.hero-title {
font-size: 3rem;
font-weight: 800;
color: white;
margin-bottom: 1.5rem;
text-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.hero-description {
font-size: 1.2rem;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 2rem;
line-height: 1.6;
}
.hero-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.hero-btn {
padding: 0.75rem 2rem;
border: none;
border-radius: 50px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
}
.hero-btn.primary {
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
color: white;
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4);
}
.hero-btn.secondary {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.hero-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
}
/* 计数器组件 */
.counter-container {
background: white;
border-radius: 15px;
padding: 2rem;
margin: 2rem 0;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
text-align: center;
}
.counter-container h2 {
margin-bottom: 1.5rem;
color: #4a5568;
}
.counter-display {
font-size: 2rem;
font-weight: 700;
color: #667eea;
margin-bottom: 2rem;
padding: 1rem;
background: #f7fafc;
border-radius: 10px;
}
.counter-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.counter-btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
min-width: 80px;
}
.counter-btn.increment {
background: linear-gradient(45deg, #48bb78, #38a169);
color: white;
}
.counter-btn.decrement {
background: linear-gradient(45deg, #f56565, #e53e3e);
color: white;
}
.counter-btn.reset {
background: linear-gradient(45deg, #a0aec0, #718096);
color: white;
}
.counter-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
/* 特性区域 */
.features-section {
margin: 3rem 0;
}
.section-title {
text-align: center;
font-size: 2.5rem;
font-weight: 700;
color: white;
margin-bottom: 3rem;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
padding: 0 1rem;
}
.feature-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
border-radius: 15px;
padding: 2rem;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
background: rgba(255, 255, 255, 0.15);
}
.feature-icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.feature-title {
font-size: 1.5rem;
font-weight: 600;
color: white;
margin-bottom: 1rem;
}
.feature-description {
color: rgba(255, 255, 255, 0.9);
line-height: 1.6;
}
/* 动态内容区域 */
.dynamic-section {
background: white;
border-radius: 15px;
padding: 2rem;
margin: 2rem 0;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.dynamic-controls {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.toggle-btn, .add-item-btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.toggle-btn {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
}
.add-item-btn {
background: linear-gradient(45deg, #48bb78, #38a169);
color: white;
}
.dynamic-content {
animation: fadeIn 0.3s ease-in;
}
.items-list {
list-style: none;
}
.list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
background: #f7fafc;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.item-text {
font-weight: 500;
}
.remove-btn {
background: #f56565;
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
}
.remove-btn:hover {
background: #e53e3e;
transform: scale(1.1);
}
/* 表单区域 */
.form-section {
background: white;
border-radius: 15px;
padding: 2rem;
margin: 2rem 0;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.contact-form {
max-width: 500px;
margin: 0 auto;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #4a5568;
}
.form-input, .form-textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s ease;
font-family: inherit;
}
.form-input:focus, .form-textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-textarea {
resize: vertical;
min-height: 100px;
}
.submit-btn {
width: 100%;
padding: 0.75rem;
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
/* 页脚 */
.main-footer {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
text-align: center;
padding: 2rem;
color: white;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
/* 动画 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-content {
padding: 1rem;
}
.hero-title {
font-size: 2rem;
}
.hero-content {
padding: 2rem 1rem;
}
.hero-buttons {
flex-direction: column;
align-items: center;
}
.hero-btn {
width: 200px;
}
.features-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.counter-buttons {
flex-direction: column;
align-items: center;
}
.dynamic-controls {
flex-direction: column;
}
.navigation .nav-list {
flex-direction: column;
gap: 0.5rem;
}
}
/* 设计模式高亮样式(当启用设计模式时显示) */
[data-source-info]:hover {
outline: 2px dashed #667eea !important;
outline-offset: 2px !important;
cursor: pointer !important;
position: relative !important;
}
[data-source-info]:hover::after {
content: attr(data-source-info);
position: absolute;
bottom: 100%;
left: 0;
background: #333;
color: white;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
white-space: nowrap;
z-index: 1000;
opacity: 0;
animation: fadeIn 0.3s ease-in forwards;
}

View File

@@ -0,0 +1,310 @@
import React, { useState } from 'react';
import './App.css';
// 函数组件示例
function HeaderComponent() {
return (
<header className='main-header' data-component='header'>
<h1 className='title' data-element='main-title'>
Vite Design Mode Plugin Demo
</h1>
<nav className='navigation' data-component='nav'>
<ul className='nav-list'>
<li>
<a href='#features' className='nav-link'>
Features
</a>
</li>
<li>
<a href='#examples' className='nav-link'>
Examples
</a>
</li>
<li>
<a href='#docs' className='nav-link'>
Documentation
</a>
</li>
</ul>
</nav>
</header>
);
}
// 类组件示例
class CounterComponent extends React.Component<{ initial?: number }> {
state = {
count: this.props.initial || 0,
};
render() {
return (
<div className='counter-container' data-component='counter'>
<h2 data-element='counter-title'></h2>
<div className='counter-display'>
<span className='counter-value' data-element='counter-value'>
: {this.state.count}
</span>
</div>
<div className='counter-buttons'>
<button
className='counter-btn increment'
onClick={() => this.setState({ count: this.state.count + 1 })}
data-action='increment'
>
+1
</button>
<button
className='counter-btn decrement'
onClick={() => this.setState({ count: this.state.count - 1 })}
data-action='decrement'
>
-1
</button>
<button
className='counter-btn reset'
onClick={() => this.setState({ count: 0 })}
data-action='reset'
>
</button>
</div>
</div>
);
}
}
// 复杂嵌套组件示例
function FeatureSection() {
const features = [
{
id: 'source-mapping',
title: '源码映射',
description: '自动注入源码位置信息到DOM元素',
icon: '🗺️',
},
{
id: 'ast-analysis',
title: 'AST分析',
description: '基于Babel AST的精确组件识别',
icon: '🔍',
},
{
id: 'hot-reload',
title: '热更新',
description: '与Vite开发服务器无缝集成',
icon: '⚡',
},
];
return (
<section className='features-section' data-section='features'>
<h2 className='section-title' data-element='section-title'>
</h2>
<div className='features-grid'>
{features.map(feature => (
<div
key={feature.id}
className='feature-card'
data-feature={feature.id}
>
<div className='feature-icon' data-element='feature-icon'>
{feature.icon}
</div>
<h3 className='feature-title' data-element='feature-title'>
{feature.title}
</h3>
<p
className='feature-description'
data-element='feature-description'
>
{feature.description}
</p>
</div>
))}
</div>
</section>
);
}
// 动态内容示例
function DynamicContent() {
const [isVisible, setIsVisible] = useState(false);
const [items, setItems] = useState(['项目1', '项目2', '项目3']);
return (
<div className='dynamic-section' data-section='dynamic'>
<h2 className='section-title' data-element='dynamic-title'>
</h2>
<div className='dynamic-controls'>
<button
className='toggle-btn'
onClick={() => setIsVisible(!isVisible)}
data-action='toggle-visibility'
>
{isVisible ? '隐藏' : '显示'}
</button>
<button
className='add-item-btn'
onClick={() => setItems([...items, `项目${items.length + 1}`])}
data-action='add-item'
>
</button>
</div>
{isVisible && (
<div className='dynamic-content' data-element='dynamic-list'>
<ul className='items-list'>
{items.map((item, index) => (
<li key={index} className='list-item' data-item={index}>
<span className='item-text'>{item}</span>
<button
className='remove-btn'
onClick={() => setItems(items.filter((_, i) => i !== index))}
data-action='remove-item'
>
×
</button>
</li>
))}
</ul>
</div>
)}
</div>
);
}
// 表单组件示例
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
alert(`表单提交成功!\n姓名: ${formData.name}\n邮箱: ${formData.email}`);
};
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
return (
<section className='form-section' data-section='contact'>
<h2 className='section-title' data-element='form-title'>
</h2>
<form
className='contact-form'
onSubmit={handleSubmit}
data-form='contact'
>
<div className='form-group'>
<label htmlFor='name' className='form-label'>
</label>
<input
type='text'
id='name'
name='name'
value={formData.name}
onChange={handleChange}
className='form-input'
data-element='name-input'
required
/>
</div>
<div className='form-group'>
<label htmlFor='email' className='form-label'>
</label>
<input
type='email'
id='email'
name='email'
value={formData.email}
onChange={handleChange}
className='form-input'
data-element='email-input'
required
/>
</div>
<div className='form-group'>
<label htmlFor='message' className='form-label'>
</label>
<textarea
id='message'
name='message'
value={formData.message}
onChange={handleChange}
className='form-textarea'
data-element='message-textarea'
rows={4}
required
/>
</div>
<button type='submit' className='submit-btn' data-action='submit-form'>
</button>
</form>
</section>
);
}
// 主应用组件
function App() {
return (
<div className='App' data-appdev-component='App'>
<HeaderComponent />
<main className='main-content'>
<section className='hero-section' data-section='hero'>
<div className='hero-content'>
<h1 className='hero-title' data-element='hero-title'>
Vite Plugin AppDev Design Mode
</h1>
<p className='hero-description' data-element='hero-description'>
Vite插件React开发者提供源码映射和可视化编辑功能
</p>
<div className='hero-buttons'>
<button className='hero-btn primary' data-action='get-started'>
使
</button>
<button className='hero-btn secondary' data-action='view-docs'>
</button>
</div>
</div>
</section>
<CounterComponent initial={5} />
<FeatureSection />
<DynamicContent />
<ContactForm />
</main>
<footer className='main-footer' data-component='footer'>
<p className='footer-text' data-element='footer-text'>
© 2024 Vite Plugin AppDev Design Mode. All rights reserved.
</p>
</footer>
</div>
);
}
export default App;

View File

@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import appdevDesignMode from '../../packages/plugin/src/index';
export default defineConfig({
plugins: [
react(),
appdevDesignMode({
enabled: true,
verbose: true,
attributePrefix: 'data-source',
exclude: ['node_modules'],
include: ['**/*.{tsx,ts,jsx,js}'],
}),
],
});

View File

@@ -0,0 +1,6 @@
# pnpm 磁盘空间优化配置
# 自动生成于 2025-11-22 16:14:26
package-import-method=hardlink
auto-install-peers=true
registry=https://registry.npmmirror.com

View File

@@ -0,0 +1,21 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": {
"includes": ["src/**/*.{js,jsx,ts,tsx}"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": false,
"correctness": {
"noUndeclaredDependencies": "error"
},
"suspicious": {
"noRedeclare": "error"
}
}
},
"formatter": {
"enabled": false
}
}

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,12 @@
{
"name": "公众号推文助手",
"description": "专为微信公众号内容创作者设计,提供图文生成、图片配文、视频脚本创作等功能",
"icon": "https://agent-statics-tc.nuwax.com/store/cbc0aac4615e4665b31e199a55fb4870.png",
"coverImg": "https://agent-statics-tc.nuwax.com/store/4ef409a57fd04c589437058da1aee15e.png",
"coverImgSourceType": "SYSTEM",
"needLogin": true,
"proxyConfigs": null,
"pageArgConfigs": null,
"dataSourcePlugins": null,
"dataSourceWorkflows": null
}

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>公众号推文助手</title>
<meta name="description" content="专为微信公众号内容创作者设计的一站式推文制作工具" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,90 @@
{
"name": "react-admin",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build:production": "NODE_ENV=production vite build"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@supabase/supabase-js": "^2.76.1",
"axios": "^1.12.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.6.0",
"eventsource-parser": "^3.0.6",
"input-otp": "^1.4.2",
"ky": "^1.13.0",
"lucide-react": "^0.548.0",
"next-themes": "^0.4.6",
"qrcode": "^1.5.4",
"react": "^18.0.0",
"react-day-picker": "^8.10.1",
"react-dom": "^18.0.0",
"react-dropzone": "^14.3.8",
"react-helmet-async": "^2.0.5",
"react-hook-form": "^7.65.0",
"react-resizable-panels": "^2.1.8",
"react-router": "^7.9.4",
"react-router-dom": "^6.30.0",
"recharts": "^2.15.3",
"sonner": "^2.0.7",
"streamdown": "^1.4.0",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"video-react": "^0.16.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@biomejs/biome": "2.3.0",
"@types/lodash": "^4.17.20",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/video-react": "^0.15.8",
"@typescript/native-preview": "7.0.0-dev.20251024.1",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.11",
"typescript": "~5.9.3",
"vite": "^5.1.4",
"vite-plugin-svgr": "^4.5.0",
"@xagi/vite-plugin-design-mode": "^1.1.0-beta.8"
},
"overrides": {
"react-helmet-async": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
}
}

View File

@@ -0,0 +1,3 @@
catalog:
'@react-three/drei': 9.122.0
'@react-three/fiber': 8.18.0

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,20 @@
<svg width="472" height="158" viewBox="0 0 472 158" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="203.103" y="41.7015" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="246.752" y="41.7015" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="258.201" y="98.2308" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="191.654" y="98.2308" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="207.396" y="82.847" width="57.5655" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="152.769" y="15.167" width="166.462" height="130.311" rx="28" stroke="#7592FF" stroke-width="24"/>
<rect x="0.0405273" y="0.522461" width="32.6255" height="77.5957" rx="6.26271" fill="#7592FF"/>
<rect x="0.0405273" y="0.522461" width="32.6255" height="77.5957" rx="6.26271" stroke="#7592FF"/>
<rect x="75.8726" y="3.16797" width="32.6255" height="154.31" rx="6.26271" fill="#7592FF"/>
<rect x="75.8726" y="3.16797" width="32.6255" height="154.31" rx="6.26271" stroke="#7592FF"/>
<rect x="16.7939" y="91.3438" width="32.6255" height="77.5957" rx="6.26271" transform="rotate(-90 16.7939 91.3438)" fill="#7592FF"/>
<rect x="16.7939" y="91.3438" width="32.6255" height="77.5957" rx="6.26271" transform="rotate(-90 16.7939 91.3438)" stroke="#7592FF"/>
<rect x="363.502" y="0.522461" width="32.6255" height="77.5957" rx="6.26271" fill="#7592FF"/>
<rect x="363.502" y="0.522461" width="32.6255" height="77.5957" rx="6.26271" stroke="#7592FF"/>
<rect x="439.334" y="3.16797" width="32.6255" height="154.31" rx="6.26271" fill="#7592FF"/>
<rect x="439.334" y="3.16797" width="32.6255" height="154.31" rx="6.26271" stroke="#7592FF"/>
<rect x="380.255" y="91.3438" width="32.6255" height="77.5957" rx="6.26271" transform="rotate(-90 380.255 91.3438)" fill="#7592FF"/>
<rect x="380.255" y="91.3438" width="32.6255" height="77.5957" rx="6.26271" transform="rotate(-90 380.255 91.3438)" stroke="#7592FF"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,20 @@
<svg width="472" height="158" viewBox="0 0 472 158" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="203.103" y="41.7015" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="246.752" y="41.7015" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="258.201" y="98.2303" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="191.654" y="98.2303" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="207.396" y="82.847" width="57.5655" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="152.769" y="15.167" width="166.462" height="130.311" rx="28" stroke="#465FFF" stroke-width="24"/>
<rect x="0.0405273" y="0.522461" width="32.6255" height="77.5957" rx="6.26271" fill="#465FFF"/>
<rect x="0.0405273" y="0.522461" width="32.6255" height="77.5957" rx="6.26271" stroke="#465FFF"/>
<rect x="75.8726" y="3.16748" width="32.6255" height="154.31" rx="6.26271" fill="#465FFF"/>
<rect x="75.8726" y="3.16748" width="32.6255" height="154.31" rx="6.26271" stroke="#465FFF"/>
<rect x="16.7939" y="91.3442" width="32.6255" height="77.5957" rx="6.26271" transform="rotate(-90 16.7939 91.3442)" fill="#465FFF"/>
<rect x="16.7939" y="91.3442" width="32.6255" height="77.5957" rx="6.26271" transform="rotate(-90 16.7939 91.3442)" stroke="#465FFF"/>
<rect x="363.502" y="0.522461" width="32.6255" height="77.5957" rx="6.26271" fill="#465FFF"/>
<rect x="363.502" y="0.522461" width="32.6255" height="77.5957" rx="6.26271" stroke="#465FFF"/>
<rect x="439.334" y="3.16748" width="32.6255" height="154.31" rx="6.26271" fill="#465FFF"/>
<rect x="439.334" y="3.16748" width="32.6255" height="154.31" rx="6.26271" stroke="#465FFF"/>
<rect x="380.255" y="91.3442" width="32.6255" height="77.5957" rx="6.26271" transform="rotate(-90 380.255 91.3442)" fill="#465FFF"/>
<rect x="380.255" y="91.3442" width="32.6255" height="77.5957" rx="6.26271" transform="rotate(-90 380.255 91.3442)" stroke="#465FFF"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,24 @@
<svg width="562" height="156" viewBox="0 0 562 156" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.161133" y="13.4297" width="32.6255" height="71" rx="6.26271" fill="#7592FF"/>
<rect x="0.161133" y="13.4297" width="32.6255" height="71" rx="6.26271" stroke="#7592FF"/>
<rect x="88.2891" y="80.1504" width="32.6255" height="63.5801" rx="6.26271" fill="#7592FF"/>
<rect x="88.2891" y="80.1504" width="32.6255" height="63.5801" rx="6.26271" stroke="#7592FF"/>
<rect x="15.5254" y="33.4668" width="32.6255" height="105.389" rx="6.26271" transform="rotate(-90 15.5254 33.4668)" fill="#7592FF"/>
<rect x="15.5254" y="33.4668" width="32.6255" height="105.389" rx="6.26271" transform="rotate(-90 15.5254 33.4668)" stroke="#7592FF"/>
<rect x="0.161133" y="155.16" width="30" height="107.028" rx="6.26271" transform="rotate(-90 0.161133 155.16)" fill="#7592FF"/>
<rect x="0.161133" y="155.16" width="30" height="107.028" rx="6.26271" transform="rotate(-90 0.161133 155.16)" stroke="#7592FF"/>
<rect x="15.5254" y="96.3398" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 15.5254 96.3398)" fill="#7592FF"/>
<rect x="15.5254" y="96.3398" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 15.5254 96.3398)" stroke="#7592FF"/>
<rect x="162.915" y="12.8496" width="166.462" height="130.311" rx="28" stroke="#7592FF" stroke-width="24"/>
<rect x="213.52" y="42.0287" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="257.168" y="42.0287" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="268.618" y="98.558" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="202.071" y="98.558" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="217.813" y="83.1732" width="57.5655" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="383.377" y="12.8496" width="166.462" height="130.311" rx="28" stroke="#7592FF" stroke-width="24"/>
<rect x="433.982" y="42.0287" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="477.63" y="42.0287" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="489.079" y="98.558" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="422.533" y="98.558" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="438.275" y="83.1732" width="57.5655" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,24 @@
<svg width="562" height="156" viewBox="0 0 562 156" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.161133" y="13.4292" width="32.6255" height="71" rx="6.26271" fill="#465FFF"/>
<rect x="0.161133" y="13.4292" width="32.6255" height="71" rx="6.26271" stroke="#465FFF"/>
<rect x="88.2891" y="80.1499" width="32.6255" height="63.5801" rx="6.26271" fill="#465FFF"/>
<rect x="88.2891" y="80.1499" width="32.6255" height="63.5801" rx="6.26271" stroke="#465FFF"/>
<rect x="15.5254" y="33.4673" width="32.6255" height="105.389" rx="6.26271" transform="rotate(-90 15.5254 33.4673)" fill="#465FFF"/>
<rect x="15.5254" y="33.4673" width="32.6255" height="105.389" rx="6.26271" transform="rotate(-90 15.5254 33.4673)" stroke="#465FFF"/>
<rect x="0.161133" y="155.16" width="30" height="107.028" rx="6.26271" transform="rotate(-90 0.161133 155.16)" fill="#465FFF"/>
<rect x="0.161133" y="155.16" width="30" height="107.028" rx="6.26271" transform="rotate(-90 0.161133 155.16)" stroke="#465FFF"/>
<rect x="15.5254" y="96.3398" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 15.5254 96.3398)" fill="#465FFF"/>
<rect x="15.5254" y="96.3398" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 15.5254 96.3398)" stroke="#465FFF"/>
<rect x="162.915" y="12.8496" width="166.462" height="130.311" rx="28" stroke="#465FFF" stroke-width="24"/>
<rect x="213.52" y="42.0287" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="257.168" y="42.0287" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="268.618" y="98.558" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="202.071" y="98.558" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="217.813" y="83.1732" width="57.5655" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="383.377" y="12.8496" width="166.462" height="130.311" rx="28" stroke="#465FFF" stroke-width="24"/>
<rect x="433.982" y="42.0287" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="477.63" y="42.0287" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="489.079" y="98.558" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="422.533" y="98.558" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="438.275" y="83.1732" width="57.5655" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,26 @@
<svg width="494" height="156" viewBox="0 0 494 156" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.515625" y="13.4492" width="32.6255" height="71" rx="6.26271" fill="#7592FF"/>
<rect x="0.515625" y="13.4492" width="32.6255" height="71" rx="6.26271" stroke="#7592FF"/>
<rect x="88.6436" y="80.1699" width="32.6255" height="63.5801" rx="6.26271" fill="#7592FF"/>
<rect x="88.6436" y="80.1699" width="32.6255" height="63.5801" rx="6.26271" stroke="#7592FF"/>
<rect x="15.8799" y="33.4863" width="32.6255" height="105.389" rx="6.26271" transform="rotate(-90 15.8799 33.4863)" fill="#7592FF"/>
<rect x="15.8799" y="33.4863" width="32.6255" height="105.389" rx="6.26271" transform="rotate(-90 15.8799 33.4863)" stroke="#7592FF"/>
<rect x="0.515625" y="155.18" width="30" height="107.028" rx="6.26271" transform="rotate(-90 0.515625 155.18)" fill="#7592FF"/>
<rect x="0.515625" y="155.18" width="30" height="107.028" rx="6.26271" transform="rotate(-90 0.515625 155.18)" stroke="#7592FF"/>
<rect x="15.8799" y="96.3594" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 15.8799 96.3594)" fill="#7592FF"/>
<rect x="15.8799" y="96.3594" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 15.8799 96.3594)" stroke="#7592FF"/>
<rect x="163.27" y="12.8691" width="166.462" height="130.311" rx="28" stroke="#7592FF" stroke-width="24"/>
<rect x="213.874" y="42.0482" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="257.523" y="42.0482" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="268.972" y="98.5775" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="202.425" y="98.5775" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="218.167" y="83.1927" width="57.5655" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="460.859" y="11.1885" width="32.6255" height="132.562" rx="6.26271" fill="#7592FF"/>
<rect x="460.859" y="11.1885" width="32.6255" height="132.562" rx="6.26271" stroke="#7592FF"/>
<rect x="371.731" y="33.4453" width="32.6255" height="107.028" rx="6.26271" transform="rotate(-90 371.731 33.4453)" fill="#7592FF"/>
<rect x="371.731" y="33.4453" width="32.6255" height="107.028" rx="6.26271" transform="rotate(-90 371.731 33.4453)" stroke="#7592FF"/>
<rect x="371.731" y="155.18" width="30" height="107.028" rx="6.26271" transform="rotate(-90 371.731 155.18)" fill="#7592FF"/>
<rect x="371.731" y="155.18" width="30" height="107.028" rx="6.26271" transform="rotate(-90 371.731 155.18)" stroke="#7592FF"/>
<rect x="388.096" y="93.7812" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 388.096 93.7812)" fill="#7592FF"/>
<rect x="388.096" y="93.7812" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 388.096 93.7812)" stroke="#7592FF"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,26 @@
<svg width="494" height="156" viewBox="0 0 494 156" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.515625" y="13.4492" width="32.6255" height="71" rx="6.26271" fill="#465FFF"/>
<rect x="0.515625" y="13.4492" width="32.6255" height="71" rx="6.26271" stroke="#465FFF"/>
<rect x="88.6436" y="80.1699" width="32.6255" height="63.5801" rx="6.26271" fill="#465FFF"/>
<rect x="88.6436" y="80.1699" width="32.6255" height="63.5801" rx="6.26271" stroke="#465FFF"/>
<rect x="15.8799" y="33.4873" width="32.6255" height="105.389" rx="6.26271" transform="rotate(-90 15.8799 33.4873)" fill="#465FFF"/>
<rect x="15.8799" y="33.4873" width="32.6255" height="105.389" rx="6.26271" transform="rotate(-90 15.8799 33.4873)" stroke="#465FFF"/>
<rect x="0.515625" y="155.18" width="30" height="107.028" rx="6.26271" transform="rotate(-90 0.515625 155.18)" fill="#465FFF"/>
<rect x="0.515625" y="155.18" width="30" height="107.028" rx="6.26271" transform="rotate(-90 0.515625 155.18)" stroke="#465FFF"/>
<rect x="15.8799" y="96.3599" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 15.8799 96.3599)" fill="#465FFF"/>
<rect x="15.8799" y="96.3599" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 15.8799 96.3599)" stroke="#465FFF"/>
<rect x="163.27" y="12.8696" width="166.462" height="130.311" rx="28" stroke="#465FFF" stroke-width="24"/>
<rect x="213.874" y="42.0487" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="257.523" y="42.0487" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="268.972" y="98.578" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="202.425" y="98.578" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="218.167" y="83.1932" width="57.5655" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="460.859" y="11.188" width="32.6255" height="132.562" rx="6.26271" fill="#465FFF"/>
<rect x="460.859" y="11.188" width="32.6255" height="132.562" rx="6.26271" stroke="#465FFF"/>
<rect x="371.731" y="33.4458" width="32.6255" height="107.028" rx="6.26271" transform="rotate(-90 371.731 33.4458)" fill="#465FFF"/>
<rect x="371.731" y="33.4458" width="32.6255" height="107.028" rx="6.26271" transform="rotate(-90 371.731 33.4458)" stroke="#465FFF"/>
<rect x="371.731" y="155.18" width="30" height="107.028" rx="6.26271" transform="rotate(-90 371.731 155.18)" fill="#465FFF"/>
<rect x="371.731" y="155.18" width="30" height="107.028" rx="6.26271" transform="rotate(-90 371.731 155.18)" stroke="#465FFF"/>
<rect x="388.096" y="93.7812" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 388.096 93.7812)" fill="#465FFF"/>
<rect x="388.096" y="93.7812" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 388.096 93.7812)" stroke="#465FFF"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,53 @@
<svg width="231" height="48" viewBox="0 0 231 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.425781 12.6316C0.425781 5.65535 6.08113 0 13.0574 0H35.7942C42.7704 0 48.4258 5.65535 48.4258 12.6316V35.3684C48.4258 42.3446 42.7704 48 35.7942 48H13.0574C6.08113 48 0.425781 42.3446 0.425781 35.3684V12.6316Z" fill="#465FFF"/>
<g filter="url(#filter0_d_3903_56743)">
<path d="M13.0615 12.6323C13.0615 11.237 14.1926 10.106 15.5878 10.106C16.9831 10.106 18.1142 11.237 18.1142 12.6323V35.3691C18.1142 36.7644 16.9831 37.8954 15.5878 37.8954C14.1926 37.8954 13.0615 36.7644 13.0615 35.3691V12.6323Z" fill="white"/>
</g>
<g filter="url(#filter1_d_3903_56743)">
<path d="M22.5391 22.7353C22.5391 21.3401 23.6701 20.209 25.0654 20.209C26.4606 20.209 27.5917 21.3401 27.5917 22.7353V35.3669C27.5917 36.7621 26.4606 37.8932 25.0654 37.8932C23.6701 37.8932 22.5391 36.7621 22.5391 35.3669V22.7353Z" fill="white" fill-opacity="0.9" shape-rendering="crispEdges"/>
</g>
<g filter="url(#filter2_d_3903_56743)">
<path d="M32.0078 16.4189C32.0078 15.0236 33.1389 13.8926 34.5341 13.8926C35.9294 13.8926 37.0604 15.0236 37.0604 16.4189V35.3663C37.0604 36.7615 35.9294 37.8926 34.5341 37.8926C33.1389 37.8926 32.0078 36.7615 32.0078 35.3663V16.4189Z" fill="white" fill-opacity="0.7" shape-rendering="crispEdges"/>
</g>
<path d="M66.4258 15.1724H74.0585V37.0363H78.6239V15.1724H86.2567V10.9637H66.4258V15.1724Z" fill="white"/>
<path d="M91.3521 37.5C94.0984 37.5 96.4881 36.2516 97.2371 34.4326L97.5581 37.0363H101.375V26.3362C101.375 21.4498 98.4498 18.8818 93.7061 18.8818C88.9267 18.8818 85.788 21.3785 85.788 25.1948H89.4974C89.4974 23.3402 90.9241 22.2701 93.4921 22.2701C95.7035 22.2701 97.1301 23.2332 97.1301 25.6229V26.0152L91.8514 26.4075C87.6784 26.7285 85.3243 28.7616 85.3243 32.0073C85.3243 35.3243 87.607 37.5 91.3521 37.5ZM92.7788 34.2186C90.8171 34.2186 89.747 33.4339 89.747 31.8289C89.747 30.4022 90.7814 29.5106 93.4921 29.2609L97.1658 28.9756V29.9029C97.1658 32.6136 95.4538 34.2186 92.7788 34.2186Z" fill="white"/>
<path d="M107.825 15.8857C109.252 15.8857 110.429 14.7087 110.429 13.2464C110.429 11.784 109.252 10.6427 107.825 10.6427C106.327 10.6427 105.15 11.784 105.15 13.2464C105.15 14.7087 106.327 15.8857 107.825 15.8857ZM105.649 37.0363H110.001V19.4168H105.649V37.0363Z" fill="white"/>
<path d="M118.883 37.0363V10.5H114.568V37.0363H118.883Z" fill="white"/>
<path d="M126.337 37.0363L128.441 31.0086H138.179L140.283 37.0363H145.098L135.682 10.9637H131.009L121.593 37.0363H126.337ZM132.757 18.7391C133.007 18.0258 133.221 17.2411 133.328 16.7417C133.399 17.2768 133.649 18.0614 133.863 18.7391L136.859 27.1565H129.797L132.757 18.7391Z" fill="white"/>
<path d="M154.165 37.5C156.84 37.5 159.122 36.323 160.192 34.29L160.478 37.0363H164.472V10.5H160.157V21.6638C159.051 19.9161 156.875 18.8818 154.414 18.8818C149.1 18.8818 145.89 22.8052 145.89 28.2979C145.89 33.755 149.064 37.5 154.165 37.5ZM155.128 33.5053C152.096 33.5053 150.241 31.2939 150.241 28.1552C150.241 25.0165 152.096 22.7695 155.128 22.7695C158.159 22.7695 160.121 24.9808 160.121 28.1552C160.121 31.3296 158.159 33.5053 155.128 33.5053Z" fill="white"/>
<path d="M173.359 37.0363V27.0495C173.359 24.1962 175.035 22.8408 177.104 22.8408C179.172 22.8408 180.492 24.1605 180.492 26.6215V37.0363H184.843V27.0495C184.843 24.1605 186.448 22.8052 188.553 22.8052C190.621 22.8052 191.977 24.1248 191.977 26.6572V37.0363H196.292V25.5159C196.292 21.4498 193.938 18.8818 189.658 18.8818C186.983 18.8818 184.915 20.2015 184.023 22.2345C183.096 20.2015 181.241 18.8818 178.566 18.8818C176.034 18.8818 174.25 20.0231 173.359 21.4855L173.002 19.4168H169.007V37.0363H173.359Z" fill="white"/>
<path d="M202.74 15.8857C204.167 15.8857 205.344 14.7087 205.344 13.2464C205.344 11.784 204.167 10.6427 202.74 10.6427C201.242 10.6427 200.065 11.784 200.065 13.2464C200.065 14.7087 201.242 15.8857 202.74 15.8857ZM200.564 37.0363H204.916V19.4168H200.564V37.0363Z" fill="white"/>
<path d="M213.763 37.0363V27.5489C213.763 24.6955 215.403 22.8408 218.078 22.8408C220.325 22.8408 221.788 24.2675 221.788 27.2279V37.0363H226.139V26.1935C226.139 21.6281 223.856 18.8818 219.434 18.8818C217.044 18.8818 214.904 19.9161 213.798 21.6995L213.442 19.4168H209.411V37.0363H213.763Z" fill="white"/>
<defs>
<filter id="filter0_d_3903_56743" x="12.0615" y="9.60596" width="7.05273" height="29.7896" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3903_56743"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3903_56743" result="shape"/>
</filter>
<filter id="filter1_d_3903_56743" x="21.5391" y="19.709" width="7.05273" height="19.6843" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3903_56743"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3903_56743" result="shape"/>
</filter>
<filter id="filter2_d_3903_56743" x="31.0078" y="13.3926" width="7.05273" height="26" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3903_56743"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3903_56743" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

Some files were not shown because too many files have changed in this diff Show More