Install
$ agentstack add skill-nihao555-hub-claude-code-agent-skills-ui-component-architect ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
UI Component Architect
使用此技能当你需要进行 React 组件架构设计、实现复杂 UI 状态管理、优化渲染性能、确保可访问性合规、或建立视觉回归测试流程。
目标
产出完整的 UI 组件架构方案,包括组件设计模式、状态管理策略、性能优化技术、可访问性指南和测试策略。
核心能力(基于 Claude Code源码)
1. React 组件设计模式
Compound Components Pattern
// 基于 Claude Code的 Message 组件系统
interface MessageContextValue {
messageId: string
role: 'user' | 'assistant'
isExpanded: boolean
}
const MessageContext = createContext(null)
function Message({ children, messageId, role }: MessageProps) {
const [isExpanded, setIsExpanded] = useState(true)
const contextValue = useMemo(() => ({
messageId,
role,
isExpanded,
}), [messageId, role, isExpanded])
return (
{children}
)
}
function MessageHeader() {
const context = useContext(MessageContext)
if (!context) throw new Error('MessageHeader must be used within Message')
return (
{context.role}
{/* toggle expand */}}>
{context.isExpanded ? '▼' : '▶'}
)
}
function MessageContent({ children }: { children: ReactNode }) {
const context = useContext(MessageContext)
if (!context) throw new Error('MessageContent must be used within Message')
if (!context.isExpanded) return null
return (
{children}
)
}
// 使用方式
State Reducer Pattern
type MessageAction =
| { type: 'EXPAND' }
| { type: 'COLLAPSE' }
| { type: 'TOGGLE' }
| { type: 'SET_CONTENT'; content: string }
interface MessageState {
isExpanded: boolean
content: string
isLoading: boolean
}
function messageReducer(state: MessageState, action: MessageAction): MessageState {
switch (action.type) {
case 'EXPAND':
return { ...state, isExpanded: true }
case 'COLLAPSE':
return { ...state, isExpanded: false }
case 'TOGGLE':
return { ...state, isExpanded: !state.isExpanded }
case 'SET_CONTENT':
return { ...state, content: action.content }
default:
return state
}
}
function MessageWithReducer({ initialState }: MessageProps) {
const [state, dispatch] = useReducer(messageReducer, initialState)
return (
{/* children */}
)
}
Control Props Pattern
interface AccordionProps {
expandedIndex?: number // controlled
defaultExpandedIndex?: number // uncontrolled
onChange?: (index: number) => void
children: ReactNode
}
function Accordion({
expandedIndex: controlledIndex,
defaultExpandedIndex = 0,
onChange,
children,
}: AccordionProps) {
const [uncontrolledIndex, setUncontrolledIndex] = useState(defaultExpandedIndex)
// Determine if controlled or uncontrolled
const isControlled = controlledIndex !== undefined
const expandedIndex = isControlled ? controlledIndex : uncontrolledIndex
const handleChange = useCallback((index: number) => {
if (!isControlled) {
setUncontrolledIndex(index)
}
onChange?.(index)
}, [isControlled, onChange])
return (
{children}
)
}
2. 高性能渲染优化
Virtual Scrolling
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualMessageList({ messages, containerRef }: MessageListProps) {
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => containerRef.current,
estimateSize: () => 100, // 估计每个项目高度
overscan: 5, // 预渲染前后各 5 个项目
})
const virtualItems = virtualizer.getVirtualItems()
return (
{virtualItems.map((virtualRow) => (
))}
)
}
Memoization Strategy
// Expensive computation memoization
const MemoizedMarkdown = memo(function Markdown({ content }: { content: string }) {
// Parse markdown (expensive)
const parsed = useMemo(() => parseMarkdown(content), [content])
// Syntax highlight code blocks (very expensive)
const highlighted = useMemo(() => {
return highlightCodeBlocks(parsed)
}, [parsed])
return
}, (prevProps, nextProps) => {
// Custom comparison
return prevProps.content === nextProps.content
})
// Event handler memoization
function MessageActions({ messageId, onArchive, onDelete }: ActionsProps) {
const handleArchive = useCallback(() => {
onArchive(messageId)
}, [messageId, onArchive])
const handleDelete = useCallback(() => {
onDelete(messageId)
}, [messageId, onDelete])
return (
<>
Archive
Delete
)
}
Windowing for Large Lists
function FixedSizeList({
height,
itemCount,
itemSize,
renderItem,
}: FixedSizeListProps) {
const [scrollTop, setScrollTop] = useState(0)
const visibleStart = Math.floor(scrollTop / itemSize)
const visibleEnd = Math.min(
itemCount,
visibleStart + Math.ceil(height / itemSize)
)
const items = []
for (let i = visibleStart; i
{renderItem(i)}
)
}
return (
setScrollTop(e.currentTarget.scrollTop)}
>
{items}
)
}
3. 状态管理策略
Zustand for Global State
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
interface AppState {
// Session state
sessionId: string
turnCount: number
// UI state
sidebarOpen: boolean
theme: 'light' | 'dark'
// Actions
actions: {
setSidebarOpen: (open: boolean) => void
toggleTheme: () => void
incrementTurn: () => void
}
}
export const useAppStore = create()(
subscribeWithSelector((set, get) => ({
// Initial state
sessionId: generateSessionId(),
turnCount: 0,
sidebarOpen: true,
theme: 'light',
// Actions
actions: {
setSidebarOpen: (open) => set({ sidebarOpen: open }),
toggleTheme: () => set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light'
})),
incrementTurn: () => set((state) => ({
turnCount: state.turnCount + 1
})),
},
}))
)
// Usage in components
function Sidebar() {
const { sidebarOpen, actions } = useAppStore()
return (
actions.setSidebarOpen(!sidebarOpen)}>
Toggle
)
}
Jotai for Atomic State
import { atom, useAtom } from 'jotai'
// Atomic state
const messageIdsAtom = atom([])
const messagesMapAtom = atom>(new Map())
// Derived state
const messageCountAtom = atom(
(get) => get(messageIdsAtom).length
)
// Async state
const currentUserAtom = atom(async () => {
return fetchCurrentUser()
})
// Write-only atom
const addMessageAtom = atom(
null,
(get, set, message: Message) => {
set(messageIdsAtom, [...get(messageIdsAtom), message.id])
set(messagesMapAtom, new Map([
...get(messagesMapAtom),
[message.id]: message
]))
}
)
function MessageList() {
const [messageIds] = useAtom(messageIdsAtom)
const [messagesMap] = useAtom(messagesMapAtom)
const [, addMessage] = useAtom(addMessageAtom)
return (
{messageIds.map(id => (
))}
)
}
4. 可访问性(A11y)实现
Semantic HTML & ARIA
interface DialogProps {
isOpen: boolean
onClose: () => void
title: string
children: ReactNode
}
function Dialog({ isOpen, onClose, title, children }: DialogProps) {
// Trap focus inside dialog
useEffect(() => {
if (!isOpen) return
const dialog = document.querySelector('[role="dialog"]')
const focusableElements = dialog?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const firstFocusable = focusableElements?.[0]
const lastFocusable = focusableElements?.[focusableElements.length - 1]
function handleTabKey(e: KeyboardEvent) {
if (e.key !== 'Tab') return
if (e.shiftKey && document.activeElement === firstFocusable) {
e.preventDefault()
lastFocusable?.focus()
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
e.preventDefault()
firstFocusable?.focus()
}
}
document.addEventListener('keydown', handleTabKey)
return () => document.removeEventListener('keydown', handleTabKey)
}, [isOpen])
if (!isOpen) return null
return (
e.target === e.currentTarget && onClose()}
>
{title}
×
{children}
)
}
Keyboard Navigation
function Menu({ items, onSelect }: MenuProps) {
const [focusedIndex, setFocusedIndex] = useState(0)
function handleKeyDown(e: React.KeyboardEvent) {
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setFocusedIndex((prev) => (prev + 1) % items.length)
break
case 'ArrowUp':
e.preventDefault()
setFocusedIndex((prev) => (prev - 1 + items.length) % items.length)
break
case 'Enter':
case ' ':
e.preventDefault()
onSelect(items[focusedIndex])
break
case 'Escape':
onClose?.()
break
}
}
return (
{items.map((item, index) => (
onSelect(item)}
>
{item.label}
))}
)
}
5. 视觉回归测试
Percy Integration
import percySnapshot from '@percy/playwright'
import { test, expect } from '@playwright/test'
test.describe('Message Component', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/messages')
})
test('renders user message correctly', async ({ page }) => {
const message = page.getByTestId('message-user')
await expect(message).toBeVisible()
await percySnapshot(page, 'User Message - Default')
})
test('renders assistant message with markdown', async ({ page }) => {
const message = page.getByTestId('message-assistant')
await expect(message).toContainText('# Hello')
await percySnapshot(page, 'Assistant Message - Markdown')
})
test('handles long content gracefully', async ({ page }) => {
await page.evaluate(() => {
window.postMessage({
type: 'ADD_MESSAGE',
content: 'A'.repeat(10000)
})
})
await percySnapshot(page, 'Message - Long Content')
})
})
Chromatic Integration
// .storybook/main.ts
module.exports = {
stories: ['../src/**/*.stories.@(ts|tsx)'],
addons: [
'@storybook/addon-essentials',
'@chromatic-com/storybook',
],
}
// Message.stories.tsx
export default {
component: Message,
title: 'Components/Message',
decorators: [
(Story) => (
),
],
}
export const UserMessage = {
args: {
role: 'user',
content: 'Hello, world!',
},
}
export const AssistantMessageWithCode = {
args: {
role: 'assistant',
content: `Here's some code:
\`\`\`typescript
const greeting = 'Hello!'
console.log(greeting)
\`\`\``,
},
}
export const LoadingState = {
args: {
role: 'assistant',
isLoading: true,
},
}
工作流程
第一步:组件设计 (20 分钟)
- 选择设计模式
- Compound Components: 用于复杂组合
- State Reducer: 用于灵活状态控制
- Control Props: 用于可控制的组件
- 定义接口
```typescript interface ComponentProps { // Required props children: ReactNode
// Optional props className?: string
// Controlled props value?: T defaultValue?: T onChange?: (value: T) => void } ```
第二步:实现优化 (40 分钟)
- 添加 Memoization
- 实现虚拟滚动
- 优化事件处理
- 添加键盘导航
第三步:测试验证 (20 分钟)
- 单元测试
- 视觉回归测试
- 可访问性测试
- 性能基准测试
规则
- ✅ 使用语义化 HTML
- ✅ 实现完整的键盘导航
- ✅ 添加适当的 ARIA 属性
- ✅ 使用 Memoization 优化渲染
- ✅ 实现错误边界
- ❌ 不要忽略可访问性
- ❌ 不要在渲染中进行昂贵计算
- ❌ 不要忘记清理副作用
- ❌ 不要直接修改 props
输出格式
📊 组件架构图
展示组件层次结构和数据流。
📝 实现代码
提供完整的组件实现,包括优化和测试。
🔍 测试报告
包含视觉回归测试结果和可访问性审计报告。
⚠️ 性能分析
| 指标 | 优化前 | 优化后 | 改进 | |------|--------|--------|------| | 首次渲染 | X ms | Y ms | -Z% | | 重渲染次数 | X | Y | -Z% | | FPS | X | Y | +Z% |
交付物清单
最终报告应包含:
- [ ] 组件设计文档
- [ ] 完整实现代码
- [ ] Storybook故事
- [ ] 视觉回归测试配置
- [ ] 可访问性审计报告
- [ ] 性能优化报告
- [ ] 使用文档
🤖 AI IDE 开发指南
本部分指导 AI IDE 按照 Claude Code的最佳实践开发 UI 组件系统。
AI IDE 开发前检查
设计系统验证
必须检查:
- Design Token定义完整
- 颜色系统一致
- 间距系统统一
- 字体系统清晰
警告信号:
- 硬编码的颜色值
- 不一致的间距
- 混用的单位 (px/rem/em)
可访问性准备
必须存在:
- 键盘导航支持
- Focus管理
- ARIA标签
- 屏幕阅读器测试
AI IDE 开发中指导
实时组件质量监控
当开发 UI 组件时,AI IDE应该:
- 设计模式检查
```typescript // ✅ 良好的复合组件模式
// ❌ 避免的模式
// AI IDE 应该:建议使用复合组件模式 ```
- 性能优化检查
```typescript // ✅ 正确的memoization const ExpensiveComponent = memo(({ data }) => { const processed = useMemo(() => process(data), [data]) return {processed} }, (prev, next) => prev.data === next.data)
// ❌ 避免的模式 function ExpensiveComponent({ data }) { const processed = process(data) // 每次都重新计算! return {processed} } // AI IDE 应该:建议添加 useMemo 和 memo ```
- 可访问性检查
```typescript // ✅ 完整的键盘支持 function Menu({ items }) { const [focused, setFocused] = useState(0)
return (
{items.map((item, i) => (
{item.label}
))}
) }
// ❌ 缺少键盘支持 function Menu({ items }) { return (
{items.map(item => {item})}
) } // AI IDE 应该:建议添加键盘导航和ARIA ```
视觉回归自动化
AI IDE应该提供:
- 自动截图: 在 PR 时自动运行 Percy/Chromatic
- 差异检测: 高亮显示视觉变化
- 基线管理: 轻松更新视觉基线
- 审批流程: 设计师审查视觉变化
AI IDE 完成后验证
UI 组件健康度评分卡
设计模式 (0-10分):
- 模式选择合适
- 组合性强
- 可扩展性好
性能 (0-10分):
- 渲染效率高
- 内存占用低
- 无内存泄漏
可访问性 (0-10分):
- WCAG 2.1 AA合规
- 键盘导航完整
- 屏幕阅读器友好
测试覆盖 (0-10分):
- 单元测试完整
- 视觉回归正常
- E2E测试通过
总分评级:
- 90-100: Excellent (生产就绪)
- 75-89: Good (少量优化)
- 60-74: Fair (需要改进)
- // 子组件错误会导致整个应用崩溃
}
// AI IDE 应该:建议添加 ErrorBoundary
// ❌ 内存泄漏
useEffect(() => {
const subscription = eventBus.subscribe(handler)
// 没有返回清理函数
}, [])
// AI IDE 应该:提醒添加清理逻辑
// ❌ 直接修改props
function Child({ list }) {
list.push(newItem) // 直接修改props!
return {list}
}
// AI IDE 应该:立即警告并使用不可变更新
🟡 中等风险(建议优化)
// ⚠️ 过度渲染
function Parent() {
const [count, setCount] = useState(0)
return // count变化导致Child重渲染
}
// AI IDE 应该:建议拆分状态或使用memo
// ⚠️ 内联对象创建
function Component() {
return // 每次渲染创建新对象
}
// AI IDE 应该:建议提取到组件外或使用useMemo
🟢 低风险(可选改进)
// 💡 可以优化的模式
const className = isActive ? 'active' : 'inactive'
// AI IDE 可以建议:使用classnames库处理复杂条件
AI IDE 代码审查检查清单
在 PR/MR阶段,AI IDE应该自动检查:
设计模式
- [ ] 选择了合适的组件模式
- [ ] 组合性优于配置
- [ ] 关注点分离清晰
- [ ] 单一职责原则
性能优化
- [ ] 使用React.memo优化
- [ ] useMemo/useCallback正确
- [ ] 虚拟滚动实现
- [ ] 无不必要的重渲染
可访问性
- [ ] 语义化HTML
- [ ] ARIA属性完整
- [ ] 键盘导航支持
- [ ] Focus管理正确
- [ ] 颜色对比度达标
测试
- [ ] 单元测试覆盖
- [ ] 视觉回归基线
- [ ] 可访问性测试
- [ ] 跨浏览器测试
AI IDE 集成实现示例
interface UIComponentRule {
id: string
description: string
severity: 'error' | 'warning' | 'info'
check: (componentFile: SourceFile) => Violation[]
fix?: (violation: Violation) => Fix
}
const rules: UIComponentRule[] = [
{
id: 'UI-001',
description: 'Interactive elements must have keyboard support',
severity: 'error',
check: (file) => {
const interactiveWithoutKeyboard = findInteractiveElementsWithoutKeyboard(file)
if (interactiveWithoutKeyboard.length > 0) {
return [{
message: 'Interactive element without keyboard support',
location: interactiveWithoutKeyboard.map(e => e.location)
}]
}
return []
},
fix: () => ({ type: 'add_keyboard_handler' })
},
{
id: 'UI-002',
description: 'Must use semantic HTML elements',
severity: 'error',
check: (file) => {
const nonSemanticDivs = findNonSemanticDivs(file)
if (nonSemanticDivs.length > 0) {
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [nihao555-hub](https://github.com/nihao555-hub)
- **Source:** [nihao555-hub/claude-code-agent-skills](https://github.com/nihao555-hub/claude-code-agent-skills)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.