Blocks 高阶组件
贡献 Blocks 高阶组件的详细指南
概述
Blocks 是高阶组件,包含复合组件、特效组件、业务组件等。它们通常由多个基础组件组合而成,提供完整的功能模块。
创建 Block 组件
1. 生成组件模板
npm run registry:create <block-name> --type block生成的目录结构:
registry/ssp/block/<block-name>/
├── index.tsx # 组件源代码
├── index.json # 组件注册信息
├── example.tsx # 组件示例
└── README.mdx # 组件文档2. 配置依赖
Blocks 通常依赖多个基础组件和外部库:
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "block-name",
"type": "registry:block",
"dependencies": [
"framer-motion",
"lucide-react"
],
"registryDependencies": [
"button",
"card",
"input"
]
}3. 实现组件
复合组件示例
import * as React from "react"
import { Search, X } from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"
import { Button } from "@/registry/ssp/ui/button"
import { Input } from "@/registry/ssp/ui/input"
import { Card } from "@/registry/ssp/ui/card"
import { cn } from "@/lib/utils"
export interface SearchDialogProps {
isOpen: boolean
onClose: () => void
onSearch: (query: string) => void
placeholder?: string
results?: Array<{
id: string
title: string
description?: string
}>
}
export function SearchDialog({
isOpen,
onClose,
onSearch,
placeholder = "Search...",
results = []
}: SearchDialogProps) {
const [query, setQuery] = React.useState("")
const handleSearch = (value: string) => {
setQuery(value)
onSearch(value)
}
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
className="fixed inset-0 bg-black/50 z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
{/* Dialog */}
<motion.div
className="fixed top-20 left-1/2 w-full max-w-2xl z-50"
initial={{ opacity: 0, y: -20, x: "-50%" }}
animate={{ opacity: 1, y: 0, x: "-50%" }}
exit={{ opacity: 0, y: -20, x: "-50%" }}
>
<Card className="mx-4 p-4">
<div className="flex items-center gap-2 mb-4">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
value={query}
onChange={(e) => handleSearch(e.target.value)}
placeholder={placeholder}
className="border-0 focus-visible:ring-0"
autoFocus
/>
<Button
variant="ghost"
size="sm"
onClick={onClose}
>
<X className="h-4 w-4" />
</Button>
</div>
{results.length > 0 && (
<div className="space-y-2">
{results.map((result) => (
<div
key={result.id}
className="p-2 rounded hover:bg-muted cursor-pointer"
>
<div className="font-medium">{result.title}</div>
{result.description && (
<div className="text-sm text-muted-foreground">
{result.description}
</div>
)}
</div>
))}
</div>
)}
</Card>
</motion.div>
</>
)}
</AnimatePresence>
)
}特效组件示例
import * as React from "react"
import { motion } from "framer-motion"
import { cn } from "@/lib/utils"
export interface GlowCardProps
extends React.HTMLAttributes<HTMLDivElement> {
glowColor?: string
intensity?: "low" | "medium" | "high"
}
export function GlowCard({
className,
glowColor = "blue",
intensity = "medium",
children,
...props
}: GlowCardProps) {
const intensityMap = {
low: "0.3",
medium: "0.6",
high: "0.9"
}
return (
<motion.div
className={cn(
"relative rounded-lg border bg-card p-6",
"before:absolute before:inset-0 before:rounded-lg before:p-[1px]",
"before:bg-gradient-to-r before:from-transparent before:via-current before:to-transparent",
"before:opacity-0 hover:before:opacity-100",
"before:transition-opacity before:duration-500",
className
)}
style={{
"--glow-color": `hsl(var(--${glowColor}))`,
filter: `drop-shadow(0 0 20px hsla(var(--${glowColor}) / ${intensityMap[intensity]}))`,
} as React.CSSProperties}
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }}
{...props}
>
{children}
</motion.div>
)
}4. 创建示例
import * as React from "react"
import { SearchDialog } from "./index"
import { Button } from "@/registry/ssp/ui/button"
const mockResults = [
{ id: "1", title: "React Hooks", description: "Learn about React Hooks" },
{ id: "2", title: "TypeScript", description: "TypeScript fundamentals" },
{ id: "3", title: "Next.js", description: "Next.js framework guide" },
]
export default function Example() {
const [isOpen, setIsOpen] = React.useState(false)
const [results, setResults] = React.useState([])
const handleSearch = (query: string) => {
if (query.trim()) {
const filtered = mockResults.filter(item =>
item.title.toLowerCase().includes(query.toLowerCase())
)
setResults(filtered)
} else {
setResults([])
}
}
return (
<div>
<Button onClick={() => setIsOpen(true)}>
Open Search Dialog
</Button>
<SearchDialog
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSearch={handleSearch}
results={results}
placeholder="Search documentation..."
/>
</div>
)
}5. 编写文档
---
title: Block Name
---
## 安装
<RegistryDownload filename="block-name.json" />
## 用法
import Example from "@/registry/ssp/block/block-name/example";
<ExampleCode content={`_code_autogenerated_`}>
<Example />
</ExampleCode>
## 特性
- 功能特性 1
- 功能特性 2
- 功能特性 3
## API
### Props
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| prop1 | string | - | 属性描述 |
| prop2 | boolean | false | 属性描述 |
## 样式定制
### CSS 变量
```css
.block-name {
--block-color: hsl(var(--primary));
--block-radius: var(--radius);
}主题扩展
如需自定义主题,可以扩展以下 CSS 变量:
{
"cssVars": {
"--block-custom": "value"
}
}
## Block 类型
### 1. 复合组件
多个基础组件的组合,提供完整的功能模块:
- 搜索对话框
- 数据表格
- 表单组合
- 导航菜单
### 2. 特效组件
带有动画和特效的组件:
- 发光卡片
- 粒子背景
- 渐变边框
- 动态图标
### 3. 业务组件
特定业务场景的组件:
- 用户头像
- 状态指示器
- 进度追踪
- 评分组件
### 4. 布局组件
页面布局相关的组件:
- 网格布局
- 瀑布流
- 侧边栏
- 面包屑
## 最佳实践
### 1. 组件设计原则
- **单一职责**:每个 Block 专注解决一个特定问题
- **可组合性**:与其他组件良好配合
- **可定制性**:提供足够的配置选项
- **性能优化**:避免不必要的重渲染
### 2. API 设计
```typescript
// 好的 API 设计
interface GoodBlockProps {
// 核心功能属性
data: DataType[]
onAction: (item: DataType) => void
// 可选配置
variant?: "default" | "compact"
size?: "sm" | "md" | "lg"
// 样式定制
className?: string
style?: CSSProperties
// 行为配置
disabled?: boolean
loading?: boolean
}3. 性能优化
// 使用 memo 避免不必要的渲染
export const ExpensiveBlock = React.memo(({ data, ...props }) => {
// 组件实现
})
// 使用 useMemo 缓存计算结果
const processedData = React.useMemo(() => {
return data.map(processItem)
}, [data])
// 使用 useCallback 稳定函数引用
const handleClick = React.useCallback((id: string) => {
onItemClick(id)
}, [onItemClick])4. 可访问性
// 提供适当的 ARIA 属性
<div
role="dialog"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Dialog Title</h2>
<p id="dialog-description">Dialog description</p>
</div>
// 支持键盘导航
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose()
}
}5. 错误边界
// 为复杂组件提供错误边界
export function BlockWithErrorBoundary(props: BlockProps) {
return (
<ErrorBoundary fallback={<BlockErrorFallback />}>
<Block {...props} />
</ErrorBoundary>
)
}测试
为 Block 组件编写全面的测试:
import { render, screen, fireEvent } from '@testing-library/react'
import { SearchDialog } from './search-dialog'
describe('SearchDialog', () => {
it('renders when open', () => {
render(
<SearchDialog
isOpen={true}
onClose={jest.fn()}
onSearch={jest.fn()}
/>
)
expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument()
})
it('calls onSearch when typing', () => {
const onSearch = jest.fn()
render(
<SearchDialog
isOpen={true}
onClose={jest.fn()}
onSearch={onSearch}
/>
)
fireEvent.change(screen.getByPlaceholderText('Search...'), {
target: { value: 'test query' }
})
expect(onSearch).toHaveBeenCalledWith('test query')
})
})下一步
完成 Block 开发后,查看 文档生成与同步 了解如何发布组件。