AgentSkillsCN

frontend-builder

生成高质量的 React/Next.js 组件。 适用于 UI 设计、组件开发以及前端项目中的各类场景。支持 TypeScript、Tailwind CSS、无障碍访问优化以及性能调优。 触发关键词:“组件”、“UI”、“表单”、“页面”。

SKILL.md
--- frontmatter
name: frontend-builder
description: |
  高品質なReact/Next.jsコンポーネントを生成。
  UI作成、コンポーネント実装、フロントエンド開発時に使用。
  TypeScript、Tailwind CSS、アクセシビリティ、パフォーマンス最適化対応。
  「コンポーネント」「UI」「フォーム」「画面」で発動。
allowed-tools: Read, Write, Bash

フロントエンドビルダースキル

生成規約

コンポーネント構造

code
src/components/
├── ui/              # 汎用UIコンポーネント(Button, Input等)
├── features/        # 機能単位のコンポーネント
├── layouts/         # レイアウトコンポーネント
└── forms/           # フォーム関連コンポーネント

ファイル命名規則

  • コンポーネント: PascalCase(例: UserProfile.tsx
  • フック: camelCase + use prefix(例: useUserProfile.ts
  • ユーティリティ: camelCase(例: formatDate.ts

必須チェックリスト

品質基準

  • TypeScript strict mode準拠
  • Props型定義は必ず明示
  • デフォルトpropsを設定
  • export default ではなく named export を使用

アクセシビリティ(a11y)

  • aria-label / aria-labelledby を適切に設定
  • role属性を必要に応じて設定
  • キーボード操作対応(tabIndex, onKeyDown)
  • フォーカス管理(focus-visible)
  • カラーコントラスト比 4.5:1以上

パフォーマンス

  • 不要な再レンダリングを防止(memo, useMemo, useCallback)
  • 重いコンポーネントは遅延ロード(dynamic import)
  • 画像は next/image を使用

レスポンシブ

  • モバイルファースト設計
  • Tailwindのブレークポイント活用(sm, md, lg, xl)

コンポーネントテンプレート

基本コンポーネント(shadcn/ui風)

tsx
import { type FC, type ComponentPropsWithoutRef, forwardRef } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const buttonVariants = cva(
  // ベーススタイル
  'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
        outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
        link: 'text-primary underline-offset-4 hover:underline',
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm: 'h-9 rounded-md px-3 text-sm',
        lg: 'h-11 rounded-md px-8 text-lg',
        icon: 'h-10 w-10',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
);

interface ButtonProps
  extends ComponentPropsWithoutRef<'button'>,
    VariantProps<typeof buttonVariants> {
  isLoading?: boolean;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, isLoading = false, disabled, children, ...props }, ref) => {
    return (
      <button
        ref={ref}
        className={cn(buttonVariants({ variant, size, className }))}
        disabled={disabled || isLoading}
        aria-busy={isLoading}
        {...props}
      >
        {isLoading ? (
          <span className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
        ) : null}
        {children}
      </button>
    );
  }
);
Button.displayName = 'Button';

フォームコンポーネント(React Hook Form + Zod)

tsx
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@/components/ui/Button';

const formSchema = z.object({
  email: z
    .string()
    .min(1, 'メールアドレスを入力してください')
    .email('有効なメールアドレスを入力してください'),
  password: z
    .string()
    .min(1, 'パスワードを入力してください')
    .min(8, 'パスワードは8文字以上で入力してください'),
});

type FormValues = z.infer<typeof formSchema>;

interface LoginFormProps {
  onSubmit: (data: FormValues) => Promise<void>;
}

export const LoginForm: FC<LoginFormProps> = ({ onSubmit }) => {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      email: '',
      password: '',
    },
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate className="space-y-4">
      <div className="space-y-2">
        <label htmlFor="email" className="text-sm font-medium">
          メールアドレス
        </label>
        <input
          id="email"
          type="email"
          autoComplete="email"
          className={cn(
            'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
            'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
            errors.email && 'border-destructive'
          )}
          aria-invalid={!!errors.email}
          aria-describedby={errors.email ? 'email-error' : undefined}
          {...register('email')}
        />
        {errors.email && (
          <p id="email-error" role="alert" className="text-sm text-destructive">
            {errors.email.message}
          </p>
        )}
      </div>

      <div className="space-y-2">
        <label htmlFor="password" className="text-sm font-medium">
          パスワード
        </label>
        <input
          id="password"
          type="password"
          autoComplete="current-password"
          className={cn(
            'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
            'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
            errors.password && 'border-destructive'
          )}
          aria-invalid={!!errors.password}
          aria-describedby={errors.password ? 'password-error' : undefined}
          {...register('password')}
        />
        {errors.password && (
          <p id="password-error" role="alert" className="text-sm text-destructive">
            {errors.password.message}
          </p>
        )}
      </div>

      <Button type="submit" className="w-full" isLoading={isSubmitting}>
        {isSubmitting ? 'ログイン中...' : 'ログイン'}
      </Button>
    </form>
  );
};

データ取得コンポーネント(Server Component)

tsx
// app/users/page.tsx
import { Suspense } from 'react';
import { UserList } from '@/components/features/UserList';
import { UserListSkeleton } from '@/components/features/UserListSkeleton';

export default function UsersPage() {
  return (
    <div className="container mx-auto py-8">
      <h1 className="text-2xl font-bold mb-6">ユーザー一覧</h1>
      <Suspense fallback={<UserListSkeleton />}>
        <UserList />
      </Suspense>
    </div>
  );
}

// components/features/UserList.tsx
async function getUsers() {
  const res = await fetch('/api/users', { next: { revalidate: 60 } });
  if (!res.ok) throw new Error('Failed to fetch users');
  return res.json();
}

export async function UserList() {
  const users = await getUsers();
  
  return (
    <ul className="space-y-4">
      {users.map((user) => (
        <li key={user.id} className="p-4 border rounded-lg">
          <p className="font-medium">{user.name}</p>
          <p className="text-sm text-muted-foreground">{user.email}</p>
        </li>
      ))}
    </ul>
  );
}

カスタムフック

tsx
import { useState, useCallback } from 'react';

interface UseDisclosureReturn {
  isOpen: boolean;
  onOpen: () => void;
  onClose: () => void;
  onToggle: () => void;
}

export function useDisclosure(initialState = false): UseDisclosureReturn {
  const [isOpen, setIsOpen] = useState(initialState);

  const onOpen = useCallback(() => setIsOpen(true), []);
  const onClose = useCallback(() => setIsOpen(false), []);
  const onToggle = useCallback(() => setIsOpen((prev) => !prev), []);

  return { isOpen, onOpen, onClose, onToggle };
}

パフォーマンス最適化

メモ化パターン

tsx
import { memo, useMemo, useCallback } from 'react';

// コンポーネントのメモ化
export const ExpensiveList = memo(function ExpensiveList({ items }: Props) {
  // 計算結果のメモ化
  const sortedItems = useMemo(
    () => items.sort((a, b) => a.value - b.value),
    [items]
  );

  // コールバックのメモ化
  const handleClick = useCallback((id: string) => {
    console.log('Clicked:', id);
  }, []);

  return (
    <ul>
      {sortedItems.map((item) => (
        <li key={item.id} onClick={() => handleClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
});

遅延ロード

tsx
import dynamic from 'next/dynamic';
import { Skeleton } from '@/components/ui/Skeleton';

// 重いコンポーネントを遅延ロード
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <Skeleton className="h-64 w-full" />,
  ssr: false,
});

// ページレベルの遅延ロード
const AdminPage = dynamic(() => import('./pages/Admin'));

画像最適化

tsx
import Image from 'next/image';

export function Avatar({ src, alt }: Props) {
  return (
    <Image
      src={src}
      alt={alt}
      width={40}
      height={40}
      className="rounded-full"
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,..."
    />
  );
}

生成後の自動検証

  1. TypeScript型チェック

    bash
    npx tsc --noEmit
    
  2. ESLint

    bash
    npx eslint [生成ファイル] --fix
    
  3. アクセシビリティチェック

    • aria属性の存在確認
    • role属性の妥当性確認

ユーティリティ関数

cn(クラス名結合)

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

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