Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:36:12 +08:00
commit 7df054b028
15 changed files with 4043 additions and 0 deletions

View File

@@ -0,0 +1,266 @@
# Backstage Style Web Skill
## 概述
这个skill基于`backstage-style-web`目录中的企业级设计系统帮助用户生成现代化的React网站应用。该设计系统基于Radix UI、Tailwind CSS和TypeScript构建提供了完整的组件库和设计规范。
## 功能特性
### 🎨 设计系统
- 完整的色彩体系11级品牌色 + 功能色)
- 响应式排版系统Inter/Poppins字体
- 统一的间距和阴影规范
- 支持亮色/暗色主题切换
### 🧩 组件库
- 47+ 企业级React组件
- 基于Radix UI的可访问性支持
- TypeScript类型安全
- CVA变体系统
### 📱 响应式设计
- 移动优先的设计方法
- 自适应布局组件
- 智能侧边栏(桌面折叠,移动抽屉)
- 容器查询支持
### ♿ 可访问性
- ARIA标准支持
- 键盘导航
- 屏幕阅读器兼容
- 焦点管理
## 应用类型
### 1. 管理后台 (Admin Dashboards)
- 数据可视化面板
- 用户管理系统
- 分析报告界面
- 实时监控台
### 2. 开发者工具 (Developer Tools)
- API文档界面
- 配置管理工具
- 构建和部署面板
- 团队协作平台
### 3. 数据管理应用 (Data Management)
- CRUD操作界面
- 高级筛选和搜索
- 批量操作功能
- 数据导出工具
### 4. 内容管理系统 (CMS)
- 内容创建和编辑
- 工作流程管理
- 用户权限控制
- 发布控制台
## 技术栈
```
React 18+ + TypeScript
├── UI框架: Radix UI Primitives
├── 样式: Tailwind CSS + CVA
├── 表单: React Hook Form + Zod
├── 图标: Lucide React
├── 主题: CSS Variables
└── 构建: Next.js / Vite
```
## 文件结构
```
.claude/skills/backstage-style-web/
├── SKILL.md # 主要skill描述
├── README.md # 说明文档
├── assets/
│ └── component_templates.md # 组件模板库
├── references/
│ └── design_system_guide.md # 设计系统指南
└── examples/
└── admin_dashboard_example.md # 完整示例项目
```
## 使用方法
### 1. 启用Skill
```bash
# 在Claude Code中使用
/skill backstage-style-web
```
### 2. 常见用法示例
#### 创建管理后台
```
请帮我创建一个用户管理的管理后台,需要包含:
- 用户列表和搜索功能
- 添加/编辑用户表单
- 用户状态管理
- 权限角色分配
使用Backstage style。
```
#### 创建数据分析面板
```
需要一个数据分析面板,包括:
- 关键指标卡片
- 趋势图表
- 实时数据更新
- 导出功能
使用Backstage design system。
```
#### 创建设置页面
```
帮我创建一个设置页面,包含:
- 个人资料管理
- 通知偏好设置
- 主题切换
- 安全设置
使用Backstage风格。
```
## 核心组件示例
### 页面布局
```typescript
<SidebarProvider>
<Sidebar>
<SidebarHeader>App Name</SidebarHeader>
<SidebarContent>Navigation</SidebarContent>
</Sidebar>
<main>
<PageLayout title="Dashboard">
{content}
</PageLayout>
</main>
</SidebarProvider>
```
### 数据展示
```typescript
<Card>
<CardHeader>
<CardTitle>Analytics</CardTitle>
<CardAction>
<Button>Export</Button>
</CardAction>
</CardHeader>
<CardContent>
<DataTable data={data} />
</CardContent>
</Card>
```
### 表单处理
```typescript
<Form {...form}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</Form>
```
## 设计原则
### 1. 一致性优先
- 统一的设计语言
- 可预测的交互模式
- 标准化的组件API
### 2. 可访问性
- 遵循WCAG 2.1标准
- 键盘和屏幕阅读器支持
- 语义化HTML结构
### 3. 性能优化
- 组件懒加载
- 代码分割
- 优化的包大小
### 4. 开发体验
- TypeScript类型安全
- 清晰的组件文档
- 一致的命名约定
## 主题配置
### 色彩系统
```css
/* 品牌色 */
--brand-500: #6a4040; /* 主色 */
--brand-600: #553333; /* 悬停色 */
/* 功能色 */
--success: emerald色系
--warning: amber色系
--danger: rose色系
--info: blue色系
```
### 间距系统
```css
/* 4px基础单位 */
spacing/1: 4px /* XS */
spacing/4: 16px /* 组件间距 */
spacing/6: 24px /* 标准间距 */
spacing/8: 32px /* 区块间距 */
```
## 最佳实践
### 1. 组件设计
- 优先使用复合组件模式
- 保持props接口简洁
- 支持as polymorphic属性
### 2. 样式管理
- 使用Tailwind utility classes
- CVA处理组件变体
- CSS变量支持主题切换
### 3. 状态管理
- React Hook Form处理表单
- Context API共享状态
- URL状态管理筛选器
### 4. 性能优化
- memo化昂贵组件
- 虚拟化长列表
- 代码分割路由
## 更新日志
### v1.0.0 (2025-10-29)
- ✅ 初始版本发布
- ✅ 完整的47个组件库
- ✅ 设计系统文档
- ✅ 管理后台示例
- ✅ 组件模板库
- ✅ 响应式设计支持
- ✅ 可访问性实现
## 技术支持
如果在使用过程中遇到问题:
1. 查看`references/design_system_guide.md`了解设计规范
2. 参考`assets/component_templates.md`获取组件模板
3. 查看`examples/admin_dashboard_example.md`获取完整示例
4. 确保已安装所需的依赖包
## 许可证
基于原始的backstage-style-web设计系统遵循相同的开源许可证。

View File

@@ -0,0 +1,530 @@
---
name: backstage-style-web
description: Generate enterprise-grade web applications using Backstage Design System. Create admin dashboards, developer tools, and internal applications with modern React components, dark/light themes, and responsive design patterns based on Radix UI and Tailwind CSS.
---
# Backstage Style Web Generator
## Overview
Create professional, enterprise-grade web applications using the Backstage Design System. This skill generates modern React applications with TypeScript, featuring comprehensive UI components, dark/light theme support, responsive design, and accessibility-first patterns. Perfect for building admin dashboards, developer tools, internal applications, and data management interfaces.
# Backstage Design System Implementation Guide
## Core Architecture Overview
The Backstage Design System is built on modern React patterns and industry-standard libraries:
### Technology Stack
- **React 18+** with TypeScript
- **Radix UI Primitives** for accessible, headless components
- **Tailwind CSS** for utility-first styling
- **Class Variance Authority (CVA)** for type-safe component variants
- **React Hook Form** with Zod validation
- **Lucide React** for consistent iconography
### Key Design Principles
1. **Accessibility First**: Built on Radix UI primitives with ARIA support
2. **Type Safety**: Full TypeScript coverage with strict typing
3. **Composability**: Components designed for flexible composition
4. **Consistency**: Unified design tokens and patterns
5. **Performance**: Optimized for bundle size and runtime performance
## Component Architecture Patterns
### 1. Compound Components
Components are designed with multiple sub-components for maximum flexibility:
```typescript
// Card component structure
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
<CardAction>Action buttons</CardAction>
</CardHeader>
<CardContent>
Main content
</CardContent>
<CardFooter>
Footer content
</CardFooter>
</Card>
```
### 2. Variant-Based Styling
Using CVA for type-safe, predictable component variants:
```typescript
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
```
### 3. Data Attributes for Styling
Consistent use of data attributes for component identification and styling:
```typescript
<div
data-slot="card"
className="bg-card text-card-foreground rounded-lg border"
>
<div data-slot="card-header">
{/* Header content */}
</div>
</div>
```
## Theme System Implementation
### 1. Color Architecture
#### Brand Colors (11-step scale)
```css
:root {
--brand-050: #F5F1F0; /* 最浅背景色 */
--brand-100: #E9DDDB; /* Hover background / light tint */
--brand-200: #D6BEBB; /* Soft background */
--brand-300: #B78F8A; /* Subtle brand tint */
--brand-400: #94635E; /* Hover / outline */
--brand-500: #6A4040; /* Primary brand color */
--brand-600: #5B3535; /* Active / pressed */
--brand-700: #4C2D2D; /* Stronger contrast */
--brand-800: #3E2525; /* Deep background */
--brand-900: #2E1C1C; /* Text on light */
--brand-950: #1C1010; /* Text on dark / darkest tone */
}
```
#### Semantic Color Tokens
```css
:root {
/* === Light Mode === */
--background: hsl(48, 33%, 98%);
--foreground: hsl(0, 25%, 33%);
--card: hsl(48, 33%, 98%);
--card-foreground: hsl(0, 25%, 33%);
--popover: hsl(48, 33%, 98%);
--popover-foreground: hsl(0, 25%, 33%);
--primary: hsl(222, 28%, 14%);
--primary-foreground: hsl(48, 33%, 98%);
--secondary: hsl(210, 25%, 96%);
--secondary-foreground: hsl(0, 25%, 33%);
--muted: hsl(210, 25%, 96%);
--muted-foreground: hsl(215, 9%, 46%);
--accent: hsl(210, 25%, 96%);
--accent-foreground: hsl(0, 25%, 33%);
--destructive: hsl(0, 82%, 67%);
--destructive-foreground: hsl(48, 33%, 98%);
--border: hsl(0, 14%, 94%);
--input: hsl(214, 27%, 91%);
--ring: hsl(0, 25%, 33%);
}
[data-theme='dark'] {
/* === Dark Mode === */
--background: hsl(0, 25%, 6%);
--foreground: hsl(48, 33%, 98%);
--card: hsl(0, 25%, 6%);
--card-foreground: hsl(48, 33%, 98%);
--popover: hsl(0, 25%, 6%);
--popover-foreground: hsl(48, 33%, 98%);
--primary: hsl(48, 33%, 98%);
--primary-foreground: hsl(0, 25%, 33%);
--secondary: hsl(0, 22%, 14%);
--secondary-foreground: hsl(48, 33%, 98%);
--muted: hsl(0, 22%, 14%);
--muted-foreground: hsl(0, 20%, 75%);
--accent: hsl(0, 22%, 14%);
--accent-foreground: hsl(48, 33%, 98%);
--destructive: hsl(0, 82%, 67%);
--destructive-foreground: hsl(48, 33%, 98%);
--border: hsl(0, 22%, 14%);
--input: hsl(0, 22%, 14%);
--ring: hsl(0, 25%, 33%);
}
```
### 2. Font Families
```css
/* Import custom fonts */
@font-face {
font-family: 'Instrument Serif';
src: url('./references/InstrumentSerif-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Instrument Serif';
src: url('./references/InstrumentSerif-Italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
}
/* Typography Rules */
/* Paragraph and text content */
p, .text-body, .text-content {
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
/* Headings */
h1, h2, h3, h4, h5, h6,
.heading-1, .heading-2, .heading-3, .heading-4, .heading-5, .heading-6 {
font-family: 'Instrument Serif', Georgia, 'Times New Roman', serif;
font-weight: 400;
}
/* Prepositions in headings should use italic */
h1 .preposition, h2 .preposition, h3 .preposition,
h4 .preposition, h5 .preposition, h6 .preposition,
.heading-1 .preposition, .heading-2 .preposition, .heading-3 .preposition,
.heading-4 .preposition, .heading-5 .preposition, .heading-6 .preposition {
font-style: italic;
}
```
### 3. Typography Scale
```css
.text-xxs { font-size: 8px; } /* Micro text */
.text-xs { font-size: 10px; } /* Caption */
.text-sm { font-size: 12px; } /* Secondary text */
.text-base { font-size: 14px; } /* Default body */
.text-lg { font-size: 16px; } /* Paragraph */
.display-xl { font-size: 60px; } /* Hero headings */
```
### 4. Spacing System (4px base unit)
```css
.space-1 { margin: 4px; } /* XS */
.space-2 { margin: 8px; } /* SM */
.space-3 { margin: 12px; } /* MD */
.space-4 { margin: 16px; } /* LG - Component spacing */
.space-6 { margin: 24px; } /* Standard spacing */
.space-8 { margin: 32px; } /* Section padding */
.space-16 { margin: 64px; } /* Large sections */
```
## Responsive Design Strategy
### 1. Breakpoint System
```css
/* Mobile-first approach */
sm: '640px', /* Small devices */
md: '768px', /* Tablets */
lg: '1024px', /* Laptops */
xl: '1280px', /* Desktops */
2xl: '1536px' /* Large screens */
```
### 2. Component Responsiveness
```typescript
// Example: Responsive grid
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Cards automatically adapt */}
</div>
// Custom mobile hook
const isMobile = useIsMobile(); // 768px breakpoint
```
### 3. Sidebar Adaptation
```typescript
// Desktop: Collapsible sidebar
// Mobile: Transforms to sheet/drawer
{isMobile ? (
<Sheet open={openMobile} onOpenChange={setOpenMobile}>
<SheetContent side="left" className="p-0">
<SidebarContent />
</SheetContent>
</Sheet>
) : (
<Sidebar className={cn(
"transition-all duration-300",
state === "collapsed" ? "w-[48px]" : "w-[256px]"
)}>
<SidebarContent />
</Sidebar>
)}
```
## Accessibility Implementation
### 1. ARIA Patterns
```typescript
// Proper ARIA labeling
<button
aria-label="Close dialog"
aria-expanded={isOpen}
aria-controls="dialog-content"
>
<X className="h-4 w-4" />
</button>
// Form accessibility
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
id="email"
type="email"
aria-describedby="email-description"
aria-invalid={hasError}
/>
<div id="email-description" className="text-sm text-muted-foreground">
We'll never share your email
</div>
```
### 2. Keyboard Navigation
```typescript
// Custom keyboard event handling
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
if (event.key === 'Enter' || event.key === ' ') {
onActivate();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [onClose, onActivate]);
```
### 3. Focus Management
```typescript
// Focus trap for modals
import { FocusTrap } from '@radix-ui/react-focus-trap';
<FocusTrap asChild>
<div className="modal-content">
{/* Modal content with proper focus order */}
</div>
</FocusTrap>
```
## Form Architecture
### 1. React Hook Form Integration
```typescript
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
const formSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
});
type FormData = z.infer<typeof formSchema>;
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
password: "",
},
});
```
### 2. Form Field Component Pattern
```typescript
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Enter your email" {...field} />
</FormControl>
<FormDescription>
We'll use this to send you updates
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
```
## State Management Patterns
### 1. Component State
```typescript
// Local state for UI interactions
const [isOpen, setIsOpen] = useState(false);
const [selectedItems, setSelectedItems] = useState<string[]>([]);
// Derived state
const hasSelection = selectedItems.length > 0;
const allSelected = selectedItems.length === items.length;
```
### 2. Context for Shared State
```typescript
// Theme context
const ThemeContext = createContext<{
theme: Theme;
setTheme: (theme: Theme) => void;
} | null>(null);
// Sidebar context
const SidebarContext = createContext<{
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
isMobile: boolean;
} | null>(null);
```
### 3. URL State Management
```typescript
// For filters, search, pagination
const [searchParams, setSearchParams] = useSearchParams();
const currentPage = Number(searchParams.get('page')) || 1;
const searchQuery = searchParams.get('q') || '';
const updateFilters = (filters: Record<string, string>) => {
const newParams = new URLSearchParams(searchParams);
Object.entries(filters).forEach(([key, value]) => {
if (value) {
newParams.set(key, value);
} else {
newParams.delete(key);
}
});
setSearchParams(newParams);
};
```
## Performance Optimization
### 1. Code Splitting
```typescript
// Lazy loading for routes
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
// Wrap in Suspense
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
```
### 2. Memoization
```typescript
// Expensive calculations
const processedData = useMemo(() => {
return data.map(item => ({
...item,
computed: expensiveComputation(item)
}));
}, [data]);
// Callback memoization
const handleItemClick = useCallback((id: string) => {
setSelectedItems(prev =>
prev.includes(id)
? prev.filter(item => item !== id)
: [...prev, id]
);
}, []);
// Component memoization
const MemoizedTableRow = memo(TableRow);
```
### 3. Virtual Scrolling (for large lists)
```typescript
import { FixedSizeList as List } from 'react-window';
const VirtualizedTable = ({ items }: { items: any[] }) => {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
<TableRow data={items[index]} />
</div>
);
return (
<List
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</List>
);
};
```
## Testing Patterns
### 1. Component Testing
```typescript
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './button';
describe('Button', () => {
it('renders with correct variant styles', () => {
render(<Button variant="destructive">Delete</Button>);
const button = screen.getByRole('button');
expect(button).toHaveClass('bg-destructive');
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
```
### 2. Accessibility Testing
```typescript
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
it('should not have accessibility violations', async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
```
This implementation guide provides the foundation for building consistent, accessible, and performant applications using the Backstage Design System.

View File

@@ -0,0 +1,980 @@
# Backstage Style Component Templates
## Core Page Templates
### 1. Dashboard Page Template
```typescript
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardAction } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { MoreHorizontal, Plus, Search, Filter, Download } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
interface DashboardData {
id: string;
name: string;
status: "active" | "inactive" | "pending";
value: number;
lastUpdated: string;
}
export default function DashboardPage() {
const [searchTerm, setSearchTerm] = useState("");
const [data, setData] = useState<DashboardData[]>([
// Sample data
{ id: "1", name: "Project Alpha", status: "active", value: 12450, lastUpdated: "2 hours ago" },
{ id: "2", name: "Project Beta", status: "pending", value: 8900, lastUpdated: "1 day ago" },
{ id: "3", name: "Project Gamma", status: "inactive", value: 3200, lastUpdated: "3 days ago" },
]);
const filteredData = data.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const statusVariant = (status: string) => {
switch (status) {
case "active": return "default";
case "pending": return "secondary";
case "inactive": return "outline";
default: return "secondary";
}
};
return (
<div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
<p className="text-muted-foreground">
Manage your projects and monitor performance
</p>
</div>
<div className="flex items-center space-x-2">
<Button variant="outline" size="sm">
<Download className="mr-2 h-4 w-4" />
Export
</Button>
<Button size="sm">
<Plus className="mr-2 h-4 w-4" />
Add Project
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Projects</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">24</div>
<p className="text-xs text-muted-foreground">
+2 from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Projects</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">18</div>
<p className="text-xs text-muted-foreground">
+4 from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Value</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$24,550</div>
<p className="text-xs text-muted-foreground">
+12% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Completion Rate</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">89%</div>
<p className="text-xs text-muted-foreground">
+3% from last month
</p>
</CardContent>
</Card>
</div>
{/* Data Table */}
<Card>
<CardHeader>
<CardTitle>Projects</CardTitle>
<CardAction>
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search projects..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 w-[300px]"
/>
</div>
<Button variant="outline" size="sm">
<Filter className="mr-2 h-4 w-4" />
Filter
</Button>
</div>
</CardAction>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead>Value</TableHead>
<TableHead>Last Updated</TableHead>
<TableHead className="w-[70px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredData.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>
<Badge variant={statusVariant(item.status)}>
{item.status}
</Badge>
</TableCell>
<TableCell>${item.value.toLocaleString()}</TableCell>
<TableCell className="text-muted-foreground">
{item.lastUpdated}
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuItem className="text-red-600">
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
);
}
```
### 2. Form Page Template
```typescript
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { toast } from "@/components/ui/use-toast";
const formSchema = z.object({
title: z.string().min(2, "Title must be at least 2 characters"),
description: z.string().min(10, "Description must be at least 10 characters"),
category: z.string().min(1, "Please select a category"),
priority: z.enum(["low", "medium", "high"]),
isPublic: z.boolean().default(false),
tags: z.array(z.string()).optional(),
email: z.string().email("Invalid email address"),
});
type FormData = z.infer<typeof formSchema>;
export default function FormPage() {
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
description: "",
category: "",
priority: "medium",
isPublic: false,
tags: [],
email: "",
},
});
function onSubmit(values: FormData) {
toast({
title: "Form submitted successfully!",
description: "Your data has been saved.",
});
console.log(values);
}
return (
<div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight">Create New Item</h2>
<p className="text-muted-foreground">
Fill out the form below to create a new item
</p>
</div>
</div>
<div className="grid gap-4 md:grid-cols-3">
{/* Main Form */}
<div className="md:col-span-2">
<Card>
<CardHeader>
<CardTitle>Item Details</CardTitle>
<CardDescription>
Provide the basic information for your new item
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="Enter a title" {...field} />
</FormControl>
<FormDescription>
This will be the display name for your item.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Enter a description"
className="min-h-[100px]"
{...field}
/>
</FormControl>
<FormDescription>
Provide a detailed description of your item.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem>
<FormLabel>Category</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="web">Web Development</SelectItem>
<SelectItem value="mobile">Mobile Development</SelectItem>
<SelectItem value="design">Design</SelectItem>
<SelectItem value="marketing">Marketing</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Enter email address" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Priority Level</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-row space-x-6"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="low" id="low" />
<Label htmlFor="low">Low</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="medium" id="medium" />
<Label htmlFor="medium">Medium</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="high" id="high" />
<Label htmlFor="high">High</Label>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isPublic"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
Make Public
</FormLabel>
<FormDescription>
Allow others to view this item publicly.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<div className="flex gap-3">
<Button type="submit">Create Item</Button>
<Button type="button" variant="outline">
Save as Draft
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
{/* Sidebar Info */}
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Help</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Need help? Check out our documentation or contact support.
</p>
<div className="mt-4 space-y-2">
<Button variant="outline" size="sm" className="w-full">
View Documentation
</Button>
<Button variant="outline" size="sm" className="w-full">
Contact Support
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Quick Stats</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Total Items</span>
<span className="text-sm font-medium">247</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">This Month</span>
<span className="text-sm font-medium">18</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground">Published</span>
<span className="text-sm font-medium">156</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
}
```
### 3. Settings Page Template
```typescript
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Bell, Shield, User, Palette, Globe } from "lucide-react";
export default function SettingsPage() {
const [notifications, setNotifications] = useState({
email: true,
push: false,
marketing: true,
});
const [theme, setTheme] = useState("system");
return (
<div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight">Settings</h2>
<p className="text-muted-foreground">
Manage your account settings and preferences
</p>
</div>
</div>
<Tabs defaultValue="general" className="space-y-4">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="general" className="flex items-center gap-2">
<User className="h-4 w-4" />
General
</TabsTrigger>
<TabsTrigger value="notifications" className="flex items-center gap-2">
<Bell className="h-4 w-4" />
Notifications
</TabsTrigger>
<TabsTrigger value="appearance" className="flex items-center gap-2">
<Palette className="h-4 w-4" />
Appearance
</TabsTrigger>
<TabsTrigger value="privacy" className="flex items-center gap-2">
<Shield className="h-4 w-4" />
Privacy
</TabsTrigger>
<TabsTrigger value="advanced" className="flex items-center gap-2">
<Globe className="h-4 w-4" />
Advanced
</TabsTrigger>
</TabsList>
<TabsContent value="general" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
<CardDescription>
Update your personal information and profile details
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center space-x-4">
<Avatar className="h-20 w-20">
<AvatarImage src="/avatars/01.png" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>
<div className="space-y-2">
<Button variant="outline" size="sm">
Change Avatar
</Button>
<p className="text-sm text-muted-foreground">
JPG, GIF or PNG. 1MB max.
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="firstName">First Name</Label>
<Input id="firstName" defaultValue="John" />
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Last Name</Label>
<Input id="lastName" defaultValue="Doe" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" defaultValue="john@example.com" />
</div>
<div className="space-y-2">
<Label htmlFor="bio">Bio</Label>
<Input id="bio" defaultValue="Software developer and designer" />
</div>
<Button>Save Changes</Button>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="notifications" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Notification Preferences</CardTitle>
<CardDescription>
Choose what notifications you want to receive
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-base">Email Notifications</Label>
<p className="text-sm text-muted-foreground">
Receive notifications via email
</p>
</div>
<Switch
checked={notifications.email}
onCheckedChange={(checked) =>
setNotifications(prev => ({ ...prev, email: checked }))
}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-base">Push Notifications</Label>
<p className="text-sm text-muted-foreground">
Receive push notifications on your devices
</p>
</div>
<Switch
checked={notifications.push}
onCheckedChange={(checked) =>
setNotifications(prev => ({ ...prev, push: checked }))
}
/>
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-base">Marketing Emails</Label>
<p className="text-sm text-muted-foreground">
Receive emails about new features and updates
</p>
</div>
<Switch
checked={notifications.marketing}
onCheckedChange={(checked) =>
setNotifications(prev => ({ ...prev, marketing: checked }))
}
/>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="appearance" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Theme Preferences</CardTitle>
<CardDescription>
Customize the appearance of your interface
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label>Theme</Label>
<Select value={theme} onValueChange={setTheme}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Choose your preferred theme or sync with your system
</p>
</div>
<Separator />
<div className="space-y-2">
<Label>Language</Label>
<Select defaultValue="en">
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="en">English</SelectItem>
<SelectItem value="zh"></SelectItem>
<SelectItem value="es">Español</SelectItem>
<SelectItem value="fr">Français</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="privacy" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Privacy Settings</CardTitle>
<CardDescription>
Control your privacy and data sharing preferences
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-base">Profile Visibility</Label>
<p className="text-sm text-muted-foreground">
Make your profile visible to other users
</p>
</div>
<Switch defaultChecked />
</div>
<Separator />
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-base">Analytics</Label>
<p className="text-sm text-muted-foreground">
Help us improve by sharing usage analytics
</p>
</div>
<Switch defaultChecked />
</div>
<Separator />
<div className="space-y-4">
<Label className="text-base">Data Export</Label>
<p className="text-sm text-muted-foreground">
Download a copy of your data
</p>
<Button variant="outline">Request Data Export</Button>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="advanced" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Advanced Settings</CardTitle>
<CardDescription>
Advanced configuration options for power users
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label>API Access</Label>
<div className="flex items-center space-x-2">
<Input
readOnly
value="sk-1234567890abcdef"
className="font-mono text-sm"
/>
<Button variant="outline" size="sm">
Copy
</Button>
<Button variant="outline" size="sm">
Regenerate
</Button>
</div>
<p className="text-sm text-muted-foreground">
Use this API key to access our services programmatically
</p>
</div>
<Separator />
<div className="space-y-4">
<Label className="text-base text-red-600">Danger Zone</Label>
<div className="border border-red-200 rounded-lg p-4 space-y-4">
<div>
<h4 className="font-medium">Delete Account</h4>
<p className="text-sm text-muted-foreground">
Permanently delete your account and all data
</p>
</div>
<Button variant="destructive" size="sm">
Delete Account
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}
```
## Component Building Blocks
### 1. Data Display Card
```typescript
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
interface DataCardProps {
title: string;
value: string | number;
description?: string;
trend?: "up" | "down" | "neutral";
trendValue?: string;
action?: React.ReactNode;
}
export function DataCard({
title,
value,
description,
trend,
trendValue,
action
}: DataCardProps) {
const trendColor = {
up: "text-green-600",
down: "text-red-600",
neutral: "text-muted-foreground",
};
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
{action}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{(description || trendValue) && (
<p className={`text-xs ${trend ? trendColor[trend] : 'text-muted-foreground'}`}>
{trendValue && <span>{trendValue} </span>}
{description}
</p>
)}
</CardContent>
</Card>
);
}
```
### 2. Action Header
```typescript
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Search, Plus, Filter, Download } from "lucide-react";
interface ActionHeaderProps {
title: string;
description?: string;
searchPlaceholder?: string;
onSearch?: (value: string) => void;
actions?: Array<{
label: string;
icon?: React.ReactNode;
variant?: "default" | "outline" | "secondary";
onClick?: () => void;
}>;
}
export function ActionHeader({
title,
description,
searchPlaceholder,
onSearch,
actions = []
}: ActionHeaderProps) {
return (
<div className="flex flex-col space-y-4 md:flex-row md:items-center md:justify-between md:space-y-0">
<div>
<h2 className="text-3xl font-bold tracking-tight">{title}</h2>
{description && (
<p className="text-muted-foreground">{description}</p>
)}
</div>
<div className="flex items-center space-x-2">
{searchPlaceholder && onSearch && (
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder={searchPlaceholder}
onChange={(e) => onSearch(e.target.value)}
className="pl-8 w-[300px]"
/>
</div>
)}
{actions.map((action, index) => (
<Button
key={index}
variant={action.variant || "default"}
size="sm"
onClick={action.onClick}
>
{action.icon}
{action.label}
</Button>
))}
</div>
</div>
);
}
```
### 3. Status Badge
```typescript
import { Badge } from "@/components/ui/badge";
import { cva, type VariantProps } from "class-variance-authority";
const statusVariants = cva("", {
variants: {
status: {
active: "bg-green-100 text-green-800 hover:bg-green-100/80",
inactive: "bg-gray-100 text-gray-800 hover:bg-gray-100/80",
pending: "bg-yellow-100 text-yellow-800 hover:bg-yellow-100/80",
error: "bg-red-100 text-red-800 hover:bg-red-100/80",
success: "bg-emerald-100 text-emerald-800 hover:bg-emerald-100/80",
warning: "bg-amber-100 text-amber-800 hover:bg-amber-100/80",
},
},
defaultVariants: {
status: "inactive",
},
});
interface StatusBadgeProps extends VariantProps<typeof statusVariants> {
children: React.ReactNode;
className?: string;
}
export function StatusBadge({ status, children, className }: StatusBadgeProps) {
return (
<Badge variant="secondary" className={statusVariants({ status, className })}>
{children}
</Badge>
);
}
```
### 4. Layout Wrapper
```typescript
interface PageLayoutProps {
children: React.ReactNode;
title: string;
description?: string;
actions?: React.ReactNode;
}
export function PageLayout({ children, title, description, actions }: PageLayoutProps) {
return (
<div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight">{title}</h2>
{description && (
<p className="text-muted-foreground">{description}</p>
)}
</div>
{actions && <div className="flex items-center space-x-2">{actions}</div>}
</div>
{children}
</div>
);
}
```
These templates provide a solid foundation for building Backstage-style applications with consistent patterns, proper TypeScript types, and responsive design.

View File

@@ -0,0 +1,683 @@
# Admin Dashboard Example
This example demonstrates how to build a comprehensive admin dashboard using the Backstage Design System.
## Project Structure
```
admin-dashboard/
├── src/
│ ├── components/
│ │ ├── ui/ # Base UI components
│ │ ├── layout/ # Layout components
│ │ ├── dashboard/ # Dashboard-specific components
│ │ └── forms/ # Form components
│ ├── pages/
│ │ ├── Dashboard.tsx # Main dashboard page
│ │ ├── Users.tsx # User management
│ │ ├── Analytics.tsx # Analytics page
│ │ └── Settings.tsx # Settings page
│ ├── hooks/ # Custom hooks
│ ├── lib/ # Utilities
│ └── types/ # TypeScript types
```
## Main Dashboard Page
```typescript
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Progress } from "@/components/ui/progress";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
BarChart3,
Users,
TrendingUp,
DollarSign,
Search,
Filter,
Download,
Plus,
Eye,
Edit,
Trash2
} from "lucide-react";
interface DashboardStats {
totalUsers: number;
activeUsers: number;
revenue: number;
growth: number;
}
interface RecentActivity {
id: string;
user: string;
action: string;
timestamp: string;
status: "success" | "warning" | "error";
}
interface TopPerformer {
id: string;
name: string;
email: string;
sales: number;
target: number;
performance: number;
}
export default function AdminDashboard() {
const [stats, setStats] = useState<DashboardStats>({
totalUsers: 0,
activeUsers: 0,
revenue: 0,
growth: 0,
});
const [recentActivity, setRecentActivity] = useState<RecentActivity[]>([]);
const [topPerformers, setTopPerformers] = useState<TopPerformer[]>([]);
const [searchTerm, setSearchTerm] = useState("");
const [selectedTab, setSelectedTab] = useState("overview");
// Simulate data loading
useEffect(() => {
const loadData = () => {
setStats({
totalUsers: 12543,
activeUsers: 8432,
revenue: 245780,
growth: 12.5,
});
setRecentActivity([
{
id: "1",
user: "John Doe",
action: "Created new project",
timestamp: "2 minutes ago",
status: "success",
},
{
id: "2",
user: "Jane Smith",
action: "Updated user profile",
timestamp: "5 minutes ago",
status: "success",
},
{
id: "3",
user: "Bob Johnson",
action: "Failed login attempt",
timestamp: "10 minutes ago",
status: "error",
},
{
id: "4",
user: "Alice Brown",
action: "Exported data",
timestamp: "15 minutes ago",
status: "warning",
},
]);
setTopPerformers([
{
id: "1",
name: "Sarah Wilson",
email: "sarah@example.com",
sales: 45000,
target: 50000,
performance: 90,
},
{
id: "2",
name: "Mike Chen",
email: "mike@example.com",
sales: 42000,
target: 45000,
performance: 93,
},
{
id: "3",
name: "Emma Davis",
email: "emma@example.com",
sales: 38000,
target: 40000,
performance: 95,
},
]);
};
loadData();
}, []);
const getStatusBadge = (status: RecentActivity["status"]) => {
const variants = {
success: "default",
warning: "secondary",
error: "destructive",
} as const;
return variants[status] || "secondary";
};
return (
<div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight">Admin Dashboard</h2>
<p className="text-muted-foreground">
Welcome back! Here's what's happening with your platform.
</p>
</div>
<div className="flex items-center space-x-2">
<Button variant="outline" size="sm">
<Download className="mr-2 h-4 w-4" />
Export
</Button>
<Button size="sm">
<Plus className="mr-2 h-4 w-4" />
Add User
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalUsers.toLocaleString()}</div>
<p className="text-xs text-muted-foreground">
+180 from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Users</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.activeUsers.toLocaleString()}</div>
<p className="text-xs text-muted-foreground">
+{((stats.activeUsers / stats.totalUsers) * 100).toFixed(1)}% active rate
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Revenue</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">${stats.revenue.toLocaleString()}</div>
<p className="text-xs text-muted-foreground">
+{stats.growth}% from last month
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Growth Rate</CardTitle>
<BarChart3 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.growth}%</div>
<p className="text-xs text-muted-foreground">
Above target by 2.5%
</p>
</CardContent>
</Card>
</div>
{/* Main Content Tabs */}
<Tabs value={selectedTab} onValueChange={setSelectedTab} className="space-y-4">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="users">Users</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="reports">Reports</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
{/* Recent Activity */}
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{recentActivity.map((activity) => (
<div key={activity.id} className="flex items-center space-x-4">
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">
{activity.user}
</p>
<p className="text-sm text-muted-foreground">
{activity.action}
</p>
</div>
<div className="flex flex-col items-end space-y-1">
<Badge variant={getStatusBadge(activity.status)}>
{activity.status}
</Badge>
<p className="text-xs text-muted-foreground">
{activity.timestamp}
</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Top Performers */}
<Card>
<CardHeader>
<CardTitle>Top Performers</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{topPerformers.map((performer) => (
<div key={performer.id} className="space-y-2">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">{performer.name}</p>
<p className="text-xs text-muted-foreground">
{performer.email}
</p>
</div>
<div className="text-right">
<p className="text-sm font-medium">
${performer.sales.toLocaleString()}
</p>
<p className="text-xs text-muted-foreground">
Target: ${performer.target.toLocaleString()}
</p>
</div>
</div>
<Progress value={performer.performance} className="h-2" />
<p className="text-xs text-muted-foreground">
{performer.performance}% of target
</p>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="users" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>User Management</CardTitle>
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search users..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 w-[300px]"
/>
</div>
<Button variant="outline" size="sm">
<Filter className="mr-2 h-4 w-4" />
Filter
</Button>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
<TableHead>Last Active</TableHead>
<TableHead className="w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">John Doe</TableCell>
<TableCell>john@example.com</TableCell>
<TableCell>Admin</TableCell>
<TableCell>
<Badge>Active</Badge>
</TableCell>
<TableCell>2 hours ago</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Button variant="ghost" size="sm">
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" className="text-red-600">
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Jane Smith</TableCell>
<TableCell>jane@example.com</TableCell>
<TableCell>Editor</TableCell>
<TableCell>
<Badge variant="secondary">Inactive</Badge>
</TableCell>
<TableCell>1 day ago</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Button variant="ghost" size="sm">
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" className="text-red-600">
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="analytics" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>User Growth</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[200px] flex items-center justify-center text-muted-foreground">
Chart Component Here
<br />
(Integrate with Chart.js, Recharts, or similar)
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Revenue Trends</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[200px] flex items-center justify-center text-muted-foreground">
Chart Component Here
<br />
(Integrate with Chart.js, Recharts, or similar)
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="reports" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Generate Reports</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid gap-4 md:grid-cols-3">
<Button variant="outline" className="h-24 flex flex-col items-center justify-center">
<Users className="h-8 w-8 mb-2" />
User Report
</Button>
<Button variant="outline" className="h-24 flex flex-col items-center justify-center">
<DollarSign className="h-8 w-8 mb-2" />
Revenue Report
</Button>
<Button variant="outline" className="h-24 flex flex-col items-center justify-center">
<BarChart3 className="h-8 w-8 mb-2" />
Analytics Report
</Button>
</div>
<div className="text-center">
<p className="text-sm text-muted-foreground">
Select a report type to generate detailed insights
</p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}
```
## Layout Component with Sidebar
```typescript
"use client";
import { useState } from "react";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Home,
Users,
BarChart3,
Settings,
FileText,
Shield,
HelpCircle,
LogOut,
ChevronUp,
} from "lucide-react";
const navigationItems = [
{
title: "Overview",
items: [
{ title: "Dashboard", icon: Home, href: "/" },
{ title: "Analytics", icon: BarChart3, href: "/analytics" },
],
},
{
title: "Management",
items: [
{ title: "Users", icon: Users, href: "/users" },
{ title: "Reports", icon: FileText, href: "/reports" },
{ title: "Security", icon: Shield, href: "/security" },
],
},
{
title: "System",
items: [
{ title: "Settings", icon: Settings, href: "/settings" },
{ title: "Help", icon: HelpCircle, href: "/help" },
],
},
];
interface AdminLayoutProps {
children: React.ReactNode;
}
export function AdminLayout({ children }: AdminLayoutProps) {
return (
<SidebarProvider defaultOpen={true}>
<div className="flex min-h-screen">
<Sidebar className="border-r">
<SidebarHeader>
<div className="flex items-center gap-2 px-2 py-1">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<Home className="h-4 w-4" />
</div>
<span className="font-semibold">Admin Panel</span>
</div>
</SidebarHeader>
<SidebarContent>
{navigationItems.map((section) => (
<SidebarGroup key={section.title}>
<SidebarGroupLabel>{section.title}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{section.items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.href} className="flex items-center gap-2">
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
))}
</SidebarContent>
<SidebarFooter>
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton className="h-12">
<Avatar className="h-8 w-8">
<AvatarImage src="/avatars/admin.png" />
<AvatarFallback>AD</AvatarFallback>
</Avatar>
<div className="flex flex-col items-start text-sm">
<span className="font-medium">Admin User</span>
<span className="text-xs text-muted-foreground">
admin@example.com
</span>
</div>
<ChevronUp className="ml-auto h-4 w-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem>
<Settings className="mr-2 h-4 w-4" />
Account Settings
</DropdownMenuItem>
<DropdownMenuItem>
<HelpCircle className="mr-2 h-4 w-4" />
Support
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-red-600">
<LogOut className="mr-2 h-4 w-4" />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
<div className="flex-1 flex flex-col">
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex h-14 items-center px-4">
<SidebarTrigger className="mr-4" />
<div className="flex-1" />
<div className="flex items-center space-x-2">
<Button variant="outline" size="sm">
Notifications
</Button>
</div>
</div>
</header>
<main className="flex-1">
{children}
</main>
</div>
</div>
</SidebarProvider>
);
}
```
## Key Features
### 1. **Responsive Design**
- Mobile-first approach with collapsible sidebar
- Adaptive grid layouts for different screen sizes
- Touch-friendly interactions on mobile devices
### 2. **Real-time Data**
- Live activity feed with status indicators
- Performance metrics with progress bars
- Automatic data refresh capabilities
### 3. **Advanced Filtering & Search**
- Global search functionality
- Advanced filtering options
- URL-based state management for bookmarkable views
### 4. **Role-based Access Control**
- Different permission levels for different user types
- Conditional UI rendering based on user roles
- Secure routing and API access patterns
### 5. **Data Visualization**
- Integration points for chart libraries
- Progress indicators and status displays
- Trend analysis and reporting features
### 6. **Accessibility Features**
- Full keyboard navigation support
- Screen reader compatibility
- High contrast mode support
- Focus management for modal interactions
This admin dashboard provides a solid foundation for building comprehensive management interfaces with the Backstage Design System, featuring modern React patterns, TypeScript safety, and enterprise-grade functionality.

View File

@@ -0,0 +1,519 @@
# Backstage Design System Implementation Guide
## Core Architecture Overview
The Backstage Design System is built on modern React patterns and industry-standard libraries:
### Technology Stack
- **React 18+** with TypeScript
- **Radix UI Primitives** for accessible, headless components
- **Tailwind CSS** for utility-first styling
- **Class Variance Authority (CVA)** for type-safe component variants
- **React Hook Form** with Zod validation
- **Lucide React** for consistent iconography
### Key Design Principles
1. **Accessibility First**: Built on Radix UI primitives with ARIA support
2. **Type Safety**: Full TypeScript coverage with strict typing
3. **Composability**: Components designed for flexible composition
4. **Consistency**: Unified design tokens and patterns
5. **Performance**: Optimized for bundle size and runtime performance
## Component Architecture Patterns
### 1. Compound Components
Components are designed with multiple sub-components for maximum flexibility:
```typescript
// Card component structure
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
<CardAction>Action buttons</CardAction>
</CardHeader>
<CardContent>
Main content
</CardContent>
<CardFooter>
Footer content
</CardFooter>
</Card>
```
### 2. Variant-Based Styling
Using CVA for type-safe, predictable component variants:
```typescript
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
```
### 3. Data Attributes for Styling
Consistent use of data attributes for component identification and styling:
```typescript
<div
data-slot="card"
className="bg-card text-card-foreground rounded-lg border"
>
<div data-slot="card-header">
{/* Header content */}
</div>
</div>
```
## Theme System Implementation
### 1. Color Architecture
#### Brand Colors (11-step scale)
```css
:root {
--brand-050: #F5F1F0; /* 最浅背景色 */
--brand-100: #E9DDDB; /* Hover background / light tint */
--brand-200: #D6BEBB; /* Soft background */
--brand-300: #B78F8A; /* Subtle brand tint */
--brand-400: #94635E; /* Hover / outline */
--brand-500: #6A4040; /* Primary brand color */
--brand-600: #5B3535; /* Active / pressed */
--brand-700: #4C2D2D; /* Stronger contrast */
--brand-800: #3E2525; /* Deep background */
--brand-900: #2E1C1C; /* Text on light */
--brand-950: #1C1010; /* Text on dark / darkest tone */
}
```
#### Semantic Color Tokens
```css
:root {
/* === Light Mode === */
--background: hsl(48, 33%, 98%);
--foreground: hsl(0, 25%, 33%);
--card: hsl(48, 33%, 98%);
--card-foreground: hsl(0, 25%, 33%);
--popover: hsl(48, 33%, 98%);
--popover-foreground: hsl(0, 25%, 33%);
--primary: hsl(222, 28%, 14%);
--primary-foreground: hsl(48, 33%, 98%);
--secondary: hsl(210, 25%, 96%);
--secondary-foreground: hsl(0, 25%, 33%);
--muted: hsl(210, 25%, 96%);
--muted-foreground: hsl(215, 9%, 46%);
--accent: hsl(210, 25%, 96%);
--accent-foreground: hsl(0, 25%, 33%);
--destructive: hsl(0, 82%, 67%);
--destructive-foreground: hsl(48, 33%, 98%);
--border: hsl(0, 14%, 94%);
--input: hsl(214, 27%, 91%);
--ring: hsl(0, 25%, 33%);
}
[data-theme='dark'] {
/* === Dark Mode === */
--background: hsl(0, 25%, 6%);
--foreground: hsl(48, 33%, 98%);
--card: hsl(0, 25%, 6%);
--card-foreground: hsl(48, 33%, 98%);
--popover: hsl(0, 25%, 6%);
--popover-foreground: hsl(48, 33%, 98%);
--primary: hsl(48, 33%, 98%);
--primary-foreground: hsl(0, 25%, 33%);
--secondary: hsl(0, 22%, 14%);
--secondary-foreground: hsl(48, 33%, 98%);
--muted: hsl(0, 22%, 14%);
--muted-foreground: hsl(0, 20%, 75%);
--accent: hsl(0, 22%, 14%);
--accent-foreground: hsl(48, 33%, 98%);
--destructive: hsl(0, 82%, 67%);
--destructive-foreground: hsl(48, 33%, 98%);
--border: hsl(0, 22%, 14%);
--input: hsl(0, 22%, 14%);
--ring: hsl(0, 25%, 33%);
}
```
### 2. Font Families
```css
/* Import custom fonts */
@font-face {
font-family: 'Instrument Serif';
src: url('./references/InstrumentSerif-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Instrument Serif';
src: url('./references/InstrumentSerif-Italic.ttf') format('truetype');
font-weight: 400;
font-style: italic;
}
/* Typography Rules */
/* Paragraph and text content */
p, .text-body, .text-content {
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
/* Headings */
h1, h2, h3, h4, h5, h6,
.heading-1, .heading-2, .heading-3, .heading-4, .heading-5, .heading-6 {
font-family: 'Instrument Serif', Georgia, 'Times New Roman', serif;
font-weight: 400;
}
/* Prepositions in headings should use italic */
h1 .preposition, h2 .preposition, h3 .preposition,
h4 .preposition, h5 .preposition, h6 .preposition,
.heading-1 .preposition, .heading-2 .preposition, .heading-3 .preposition,
.heading-4 .preposition, .heading-5 .preposition, .heading-6 .preposition {
font-style: italic;
}
```
### 3. Typography Scale
```css
.text-xxs { font-size: 8px; } /* Micro text */
.text-xs { font-size: 10px; } /* Caption */
.text-sm { font-size: 12px; } /* Secondary text */
.text-base { font-size: 14px; } /* Default body */
.text-lg { font-size: 16px; } /* Paragraph */
.display-xl { font-size: 60px; } /* Hero headings */
```
### 4. Spacing System (4px base unit)
```css
.space-1 { margin: 4px; } /* XS */
.space-2 { margin: 8px; } /* SM */
.space-3 { margin: 12px; } /* MD */
.space-4 { margin: 16px; } /* LG - Component spacing */
.space-6 { margin: 24px; } /* Standard spacing */
.space-8 { margin: 32px; } /* Section padding */
.space-16 { margin: 64px; } /* Large sections */
```
## Responsive Design Strategy
### 1. Breakpoint System
```css
/* Mobile-first approach */
sm: '640px', /* Small devices */
md: '768px', /* Tablets */
lg: '1024px', /* Laptops */
xl: '1280px', /* Desktops */
2xl: '1536px' /* Large screens */
```
### 2. Component Responsiveness
```typescript
// Example: Responsive grid
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Cards automatically adapt */}
</div>
// Custom mobile hook
const isMobile = useIsMobile(); // 768px breakpoint
```
### 3. Sidebar Adaptation
```typescript
// Desktop: Collapsible sidebar
// Mobile: Transforms to sheet/drawer
{isMobile ? (
<Sheet open={openMobile} onOpenChange={setOpenMobile}>
<SheetContent side="left" className="p-0">
<SidebarContent />
</SheetContent>
</Sheet>
) : (
<Sidebar className={cn(
"transition-all duration-300",
state === "collapsed" ? "w-[48px]" : "w-[256px]"
)}>
<SidebarContent />
</Sidebar>
)}
```
## Accessibility Implementation
### 1. ARIA Patterns
```typescript
// Proper ARIA labeling
<button
aria-label="Close dialog"
aria-expanded={isOpen}
aria-controls="dialog-content"
>
<X className="h-4 w-4" />
</button>
// Form accessibility
<label htmlFor="email" className="sr-only">
Email address
</label>
<input
id="email"
type="email"
aria-describedby="email-description"
aria-invalid={hasError}
/>
<div id="email-description" className="text-sm text-muted-foreground">
We'll never share your email
</div>
```
### 2. Keyboard Navigation
```typescript
// Custom keyboard event handling
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
if (event.key === 'Enter' || event.key === ' ') {
onActivate();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [onClose, onActivate]);
```
### 3. Focus Management
```typescript
// Focus trap for modals
import { FocusTrap } from '@radix-ui/react-focus-trap';
<FocusTrap asChild>
<div className="modal-content">
{/* Modal content with proper focus order */}
</div>
</FocusTrap>
```
## Form Architecture
### 1. React Hook Form Integration
```typescript
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
const formSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
});
type FormData = z.infer<typeof formSchema>;
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
password: "",
},
});
```
### 2. Form Field Component Pattern
```typescript
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Enter your email" {...field} />
</FormControl>
<FormDescription>
We'll use this to send you updates
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
```
## State Management Patterns
### 1. Component State
```typescript
// Local state for UI interactions
const [isOpen, setIsOpen] = useState(false);
const [selectedItems, setSelectedItems] = useState<string[]>([]);
// Derived state
const hasSelection = selectedItems.length > 0;
const allSelected = selectedItems.length === items.length;
```
### 2. Context for Shared State
```typescript
// Theme context
const ThemeContext = createContext<{
theme: Theme;
setTheme: (theme: Theme) => void;
} | null>(null);
// Sidebar context
const SidebarContext = createContext<{
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
isMobile: boolean;
} | null>(null);
```
### 3. URL State Management
```typescript
// For filters, search, pagination
const [searchParams, setSearchParams] = useSearchParams();
const currentPage = Number(searchParams.get('page')) || 1;
const searchQuery = searchParams.get('q') || '';
const updateFilters = (filters: Record<string, string>) => {
const newParams = new URLSearchParams(searchParams);
Object.entries(filters).forEach(([key, value]) => {
if (value) {
newParams.set(key, value);
} else {
newParams.delete(key);
}
});
setSearchParams(newParams);
};
```
## Performance Optimization
### 1. Code Splitting
```typescript
// Lazy loading for routes
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
// Wrap in Suspense
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
```
### 2. Memoization
```typescript
// Expensive calculations
const processedData = useMemo(() => {
return data.map(item => ({
...item,
computed: expensiveComputation(item)
}));
}, [data]);
// Callback memoization
const handleItemClick = useCallback((id: string) => {
setSelectedItems(prev =>
prev.includes(id)
? prev.filter(item => item !== id)
: [...prev, id]
);
}, []);
// Component memoization
const MemoizedTableRow = memo(TableRow);
```
### 3. Virtual Scrolling (for large lists)
```typescript
import { FixedSizeList as List } from 'react-window';
const VirtualizedTable = ({ items }: { items: any[] }) => {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
<TableRow data={items[index]} />
</div>
);
return (
<List
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</List>
);
};
```
## Testing Patterns
### 1. Component Testing
```typescript
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './button';
describe('Button', () => {
it('renders with correct variant styles', () => {
render(<Button variant="destructive">Delete</Button>);
const button = screen.getByRole('button');
expect(button).toHaveClass('bg-destructive');
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
```
### 2. Accessibility Testing
```typescript
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
it('should not have accessibility violations', async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
```
This implementation guide provides the foundation for building consistent, accessible, and performant applications using the Backstage Design System.