AgentStack
SKILL verified MIT Self-run

Ui Component Architect

skill-nihao555-hub-claude-code-agent-skills-ui-component-architect · by nihao555-hub

UI 组件架构专家 - React 组件设计、状态管理、性能优化、可访问性、视觉回归测试

No reviews yet
0 installs
11 views
0.0% view→install

Install

$ agentstack add skill-nihao555-hub-claude-code-agent-skills-ui-component-architect

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

Are you the author of Ui Component Architect? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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 分钟)

  1. 选择设计模式
  • Compound Components: 用于复杂组合
  • State Reducer: 用于灵活状态控制
  • Control Props: 用于可控制的组件
  1. 定义接口

```typescript interface ComponentProps { // Required props children: ReactNode

// Optional props className?: string

// Controlled props value?: T defaultValue?: T onChange?: (value: T) => void } ```

第二步:实现优化 (40 分钟)

  1. 添加 Memoization
  2. 实现虚拟滚动
  3. 优化事件处理
  4. 添加键盘导航

第三步:测试验证 (20 分钟)

  1. 单元测试
  2. 视觉回归测试
  3. 可访问性测试
  4. 性能基准测试

规则

  • ✅ 使用语义化 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应该:

  1. 设计模式检查

```typescript // ✅ 良好的复合组件模式

// ❌ 避免的模式

// AI IDE 应该:建议使用复合组件模式 ```

  1. 性能优化检查

```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 ```

  1. 可访问性检查

```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.

Versions

  • v0.1.0 Imported from the upstream source.