mcpField

ui-builder

github.com/eddiebe147设计与 UI54 次安装
代码仓库

关于 ui-builder

ui-builder 是面向 UI 开发的技能,提供 React、Tailwind CSS 和组件库相关模式,涵盖响应式设计、无障碍、性能与常见界面组件。

主要功能

  1. 01移动优先的响应式设计
  2. 02语义化 HTML 与无障碍实践
  3. 03Tailwind CSS 布局和样式模式
  4. 04常见组件与表单模式
  5. 05动画、加载状态和深色模式

适用场景

  1. 01构建适配多种屏幕的页面
  2. 02实现可键盘操作的界面
  3. 03创建表单、导航和常见组件
  4. 04为界面添加加载状态或深色模式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/eddiebe147/claude-settings --skill ui-builder

安装后在 Claude Code 中输入/ui-builder确认已加载。

详细介绍

UI/UX 构建技能

概述

此技能可帮助你使用现代 React 模式、Tailwind CSS 和 Shadcn/ui 等组件库,构建美观、易于访问且响应迅速的用户界面。

核心原则

1. 移动端优先设计
  • 从移动端布局开始
  • 使用响应式断点:sm(640px)、md(768px)、lg(1024px)、xl(1280px)
  • 在多种屏幕尺寸下进行测试
2. 无障碍访问
  • 使用语义化 HTML
  • 在需要时添加 ARIA 标签
  • 确保键盘导航正常工作
  • 保持颜色对比度
  • 支持屏幕阅读器
3. 性能
  • 延迟加载大型组件
  • 优化图片
  • 尽可能使用 CSS 动画代替 JS
  • 尽量减少重新渲染
4. 一致性
  • 使用设计令牌(颜色、间距、排版)
  • 遵循既定模式
  • 保持视觉层级

Tailwind CSS 模式

布局
// Flex layouts
<div className="flex items-center justify-between gap-4">
  <div>Left</div>
  <div>Right</div>
</div>

// Grid layouts
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  <div>Card 1</div>
  <div>Card 2</div>
  <div>Card 3</div>
</div>

// Container
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
  Content
</div>

// Centering
<div className="flex items-center justify-center min-h-screen">
  <div>Centered</div>
</div>
响应式设计
// Mobile first
<div className="text-sm md:text-base lg:text-lg">
  Responsive text
</div>

// Hide/show by breakpoint
<div className="hidden md:block">
  Desktop only
</div>

<div className="block md:hidden">
  Mobile only
</div>

// Responsive padding/margin
<div className="p-4 md:p-6 lg:p-8">
  Responsive padding
</div>
颜色和主题
// Background colors
<div className="bg-white dark:bg-gray-900">
  Content
</div>

// Text colors
<p className="text-gray-900 dark:text-white">
  Text
</p>

// Hover states
<button className="bg-blue-500 hover:bg-blue-600 transition-colors">
  Hover me
</button>

// Focus states
<input className="border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200" />
间距
// Margin/Padding scale: 0, 1(4px), 2(8px), 4(16px), 6(24px), 8(32px), 12(48px), 16(64px)
<div className="p-4">Padding 16px</div>
<div className="mt-8 mb-4">Margin top 32px, bottom 16px</div>
<div className="space-y-4">Vertical spacing between children</div>
<div className="gap-6">Gap between flex/grid items</div>

常见组件模式

按钮
// components/ui/button.tsx
import { cn } from '@/lib/utils'

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'default' | 'outline' | 'ghost' | 'destructive'
  size?: 'sm' | 'md' | 'lg'
}

export function Button({
  className,
  variant = 'default',
  size = 'md',
  ...props
}: ButtonProps) {
  return (
    <button
      className={cn(
        'inline-flex items-center justify-center rounded-md font-medium transition-colors',
        'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
        'disabled:opacity-50 disabled:pointer-events-none',
        {
          'bg-blue-600 text-white hover:bg-blue-700': variant === 'default',
          'border border-gray-300 bg-white hover:bg-gray-50': variant === 'outline',
          'hover:bg-gray-100': variant === 'ghost',
          'bg-red-600 text-white hover:bg-red-700': variant === 'destructive',
        },
        {
          'h-8 px-3 text-sm': size === 'sm',
          'h-10 px-4': size === 'md',
          'h-12 px-6 text-lg': size === 'lg',
        },
        className
      )}
      {...props}
    />
  )
}
卡片
// components/ui/card.tsx
export function Card({ children, className }: { children: React.ReactNode; className?: string }) {
  return (
    <div className={cn('rounded-lg border bg-white p-6 shadow-sm', className)}>
      {children}
    </div>
  )
}

export function CardHeader({ children }: { children: React.ReactNode }) {
  return <div className="mb-4">{children}</div>
}

export function CardTitle({ children }: { children: React.ReactNode }) {
  return <h3 className="text-lg font-semibold">{children}</h3>
}

export function CardContent({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>
}
输入框
// components/ui/input.tsx
export function Input({ className, ...props }: React.InputHTMLAttributes<HTMLInputElement>) {
  return (
    <input
      className={cn(
        'flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2',
        'text-sm placeholder:text-gray-400',
        'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent',
        'disabled:cursor-not-allowed disabled:opacity-50',
        className
      )}
      {...props}
    />
  )
}
模态框/对话框
'use client'
import { useEffect } from 'react'
import { X } from 'lucide-react'

interface DialogProps {
  open: boolean
  onClose: () => void
  title?: string
  children: React.ReactNode
}

export function Dialog({ open, onClose, title, children }: DialogProps) {
  useEffect(() => {
    if (open) {
      document.body.style.overflow = 'hidden'
    } else {
      document.body.style.overflow = 'unset'
    }
    return () => {
      document.body.style.overflow = 'unset'
    }
  }, [open])

  if (!open) return null

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center">
      {/* Backdrop */}
      <div
        className="absolute inset-0 bg-black/50 backdrop-blur-sm"
        onClick={onClose}
      />

      {/* Dialog */}
      <div className="relative z-50 w-full max-w-lg rounded-lg bg-white p-6 shadow-xl">
        {title && (
          <div className="mb-4 flex items-center justify-between">
            <h2 className="text-xl font-semibold">{title}</h2>
            <button
              onClick={onClose}
              className="rounded-md p-1 hover:bg-gray-100"
            >
              <X className="h-5 w-5" />
            </button>
          </div>
        )}
        {children}
      </div>
    </div>
  )
}
下拉菜单
'use client'
import { useState, useRef, useEffect } from 'react'

export function DropdownMenu({ trigger, children }: {
  trigger: React.ReactNode
  children: React.ReactNode
}) {
  const [open, setOpen] = useState(false)
  const ref = useRef<HTMLDivElement>(null)

  useEffect(() => {
    function handleClickOutside(event: MouseEvent) {
      if (ref.current && !ref.current.contains(event.target as Node)) {
        setOpen(false)
      }
    }
    document.addEventListener('mousedown', handleClickOutside)
    return () => document.removeEventListener('mousedown', handleClickOutside)
  }, [])

  return (
    <div className="relative" ref={ref}>
      <div onClick={() => setOpen(!open)}>{trigger}</div>
      {open && (
        <div className="absolute right-0 mt-2 w-56 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5">
          <div className="py-1">{children}</div>
        </div>
      )}
    </div>
  )
}

export function DropdownMenuItem({ children, onClick }: {
  children: React.ReactNode
  onClick?: () => void
}) {
  return (
    <button
      className="block w-full px-4 py-2 text-left text-sm hover:bg-gray-100"
      onClick={onClick}
    >
      {children}
    </button>
  )
}
表单
// components/forms/contact-form.tsx
'use client'
import { useState } from 'react'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'

export function ContactForm() {
  const [loading, setLoading] = useState(false)
  const [errors, setErrors] = useState<Record<string, string>>({})

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    setLoading(true)
    setErrors({})

    const formData = new FormData(e.currentTarget)
    const email = formData.get('email') as string
    const message = formData.get('message') as string

    // Validation
    const newErrors: Record<string, string> = {}
    if (!email) newErrors.email = 'Email is required'
    if (!message) newErrors.message = 'Message is required'

    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors)
      setLoading(false)
      return
    }

    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, message }),
      })

      if (!response.ok) throw new Error('Failed to send')

      // Success
      e.currentTarget.reset()
      alert('Message sent!')
    } catch (error) {
      setErrors({ submit: 'Failed to send message' })
    } finally {
      setLoading(false)
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div>
        <label htmlFor="email" className="block text-sm font-medium mb-1">
          Email
        </label>
        <Input
          id="email"
          name="email"
          type="email"
          placeholder="[email protected]"
        />
        {errors.email && (
          <p className="mt-1 text-sm text-red-600">{errors.email}</p>
        )}
      </div>

      <div>
        <label htmlFor="message" className="block text-sm font-medium mb-1">
          Message
        </label>
        <textarea
          id="message"
          name="message"
          rows={4}
          className="w-full rounded-md border border-gray-300 px-3 py-2"
          placeholder="Your message..."
        />
        {errors.message && (
          <p className="mt-1 text-sm text-red-600">{errors.message}</p>
        )}
      </div>

      {errors.submit && (
        <p className="text-sm text-red-600">{errors.submit}</p>
      )}

      <Button type="submit" disabled={loading}>
        {loading ? 'Sending...' : 'Send Message'}
      </Button>
    </form>
  )
}

动画

Tailwind 过渡效果
// Hover effects
<div className="transition-all duration-200 hover:scale-105">
  Hover to scale
</div>

// Opacity fade
<div className="transition-opacity duration-300 opacity-0 hover:opacity-100">
  Fade in
</div>

// Transform
<div className="transition-transform duration-200 hover:translate-x-2">
  Slide on hover
</div>
Framer Motion
'use client'
import { motion } from 'framer-motion'

// Fade in
<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  transition={{ duration: 0.5 }}
>
  Fades in
</motion.div>

// Slide in
<motion.div
  initial={{ x: -100, opacity: 0 }}
  animate={{ x: 0, opacity: 1 }}
  transition={{ duration: 0.5 }}
>
  Slides in
</motion.div>

// Stagger children
<motion.div
  initial="hidden"
  animate="visible"
  variants={{
    visible: {
      transition: {
        staggerChildren: 0.1
      }
    }
  }}
>
  {items.map(item => (
    <motion.div
      key={item.id}
      variants={{
        hidden: { opacity: 0, y: 20 },
        visible: { opacity: 1, y: 0 }
      }}
    >
      {item.title}
    </motion.div>
  ))}
</motion.div>

图标

Lucide React
import { Search, User, Settings, ChevronRight } from 'lucide-react'

// Basic usage
<Search className="h-5 w-5" />

// With color
<User className="h-5 w-5 text-blue-600" />

// In button
<button className="flex items-center gap-2">
  <Settings className="h-4 w-4" />
  Settings
</button>

加载状态

骨架屏
export function Skeleton({ className }: { className?: string }) {
  return (
    <div className={cn('animate-pulse rounded-md bg-gray-200', className)} />
  )
}

// Usage
<div className="space-y-4">
  <Skeleton className="h-12 w-full" />
  <Skeleton className="h-12 w-full" />
  <Skeleton className="h-12 w-3/4" />
</div>
加载指示器
export function Spinner({ className }: { className?: string }) {
  return (
    <div
      className={cn(
        'animate-spin rounded-full border-2 border-gray-300 border-t-blue-600',
        'h-8 w-8',
        className
      )}
    />
  )
}

深色模式

设置
// app/providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </ThemeProvider>
  )
}

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}
主题切换
'use client'
import { useTheme } from 'next-themes'
import { Moon, Sun } from 'lucide-react'

export function ThemeToggle() {
  const { theme, setTheme } = useTheme()

  return (
    <button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      className="rounded-md p-2 hover:bg-gray-100 dark:hover:bg-gray-800"
    >
      <Sun className="h-5 w-5 dark:hidden" />
      <Moon className="hidden h-5 w-5 dark:block" />
    </button>
  )
}
深色模式样式
// Tailwind dark mode
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
  Dark mode aware
</div>

// tailwind.config.js
module.exports = {
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        background: 'hsl(var(--background))',
        foreground: 'hsl(var(--foreground))',
      }
    }
  }
}

最佳实践

无障碍访问检查清单
  • 使用语义化 HTML(header、nav、main、footer、article、section)
  • 为图片添加替代文本
  • 确保颜色对比度充足(普通文本为 4.5:1)
  • 支持键盘导航(Tab、Enter、Escape)
  • 在需要时添加 ARIA 标签
  • 使用 focus-visible 设置键盘焦点样式
  • 使用屏幕阅读器进行测试
性能检查清单
  • 使用 next/image 处理图片
  • 延迟加载视口中不可见的组件
  • 尽量减少客户端 JavaScript
  • 使用 CSS 动画代替 JS
  • 实现加载骨架屏
  • 优化字体加载
  • 使用适当的图片格式(WebP、AVIF)
响应式设计检查清单
  • 采用移动端优先的设计
  • 在多种屏幕尺寸下进行测试
  • 使用响应式图片
  • 实现便于触控的点击目标(最小为 44x44px)
  • 测试横屏和竖屏
  • 处理移动设备上的安全区域
  • 考虑折叠屏设备

实用函数

// lib/utils.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

常见布局

仪表盘布局
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex h-screen">
      {/* Sidebar */}
      <aside className="w-64 border-r bg-gray-50">
        <nav className="p-4">Sidebar</nav>
      </aside>

      {/* Main content */}
      <div className="flex-1 overflow-auto">
        <header className="border-b bg-white px-6 py-4">
          Header
        </header>
        <main className="p-6">{children}</main>
      </div>
    </div>
  )
}
落地页主视觉区
export function Hero() {
  return (
    <section className="flex min-h-screen items-center justify-center bg-gradient-to-b from-blue-50 to-white">
      <div className="container px-4 text-center">
        <h1 className="mb-6 text-5xl font-bold tracking-tight md:text-6xl lg:text-7xl">
          Your Amazing Product
        </h1>
        <p className="mx-auto mb-8 max-w-2xl text-lg text-gray-600 md:text-xl">
          A compelling description that explains what your product does
        </p>
        <div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
          <Button size="lg">Get Started</Button>
          <Button size="lg" variant="outline">Learn More</Button>
        </div>
      </div>
    </section>
  )
}

何时使用此技能

在以下情况下调用此技能:

  • 构建新的 UI 组件
  • 创建布局和页面结构
  • 实现响应式设计
  • 添加动画和过渡效果
  • 处理表单和输入框
  • 实现深色模式
  • 确保无障碍访问
  • 优化 UI 性能
  • 创建加载状态
  • 构建导航组件

评论

支持 Markdown