mcpField

ui-builder

github.com/eddiebe147デザイン・UI54 回インストール
リポジトリ

ui-builder について

ui-builder は、React、Tailwind CSS、コンポーネントライブラリを使った UI 開発向けのスキルです。レスポンシブ設計、アクセシビリティ、パフォーマンス、一般的な UI パターンを扱います。

主な機能

  1. 01モバイルファーストのレスポンシブ設計
  2. 02セマンティック HTML とアクセシビリティの実践
  3. 03Tailwind CSS のレイアウトとスタイルパターン
  4. 04一般的なコンポーネントとフォームのパターン
  5. 05アニメーション、ローディング状態、ダークモード

活用シーン

  1. 01複数の画面サイズに対応するページの構築
  2. 02キーボードで操作できる UI の実装
  3. 03フォーム、ナビゲーション、一般的なコンポーネントの作成
  4. 04UI へのローディング状態やダークモードの追加

インストール

コマンドをコピーしてターミナルで実行します。

$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. パフォーマンス
  • 重いコンポーネントを遅延読み込みする
  • 画像を最適化する
  • 可能な限り JS より CSS アニメーションを使用する
  • 再レンダリングを最小限に抑える
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)
  • 画像に alt テキストを追加する
  • 十分な色のコントラストを確保する(通常のテキストでは 4.5:1)
  • キーボード操作に対応する(Tab、Enter、Escape)
  • 必要な箇所に ARIA ラベルを追加する
  • キーボードフォーカスには focus-visible を使用する
  • スクリーンリーダーでテストする
パフォーマンスチェックリスト
  • 画像には next/image を使用する
  • ビューポートに表示されていないコンポーネントを遅延読み込みする
  • クライアントサイドの JavaScript を最小限に抑える
  • JS より CSS アニメーションを使用する
  • 読み込み時のスケルトンを実装する
  • フォントの読み込みを最適化する
  • 適切な画像形式(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 のパフォーマンスを最適化する
  • 読み込み状態を作成する
  • ナビゲーションコンポーネントを構築する

コメント