AgentSkillsCN

New Component

新组件

SKILL.md

New Component Skill

Create a new React component following the project's conventions and best practices.

Trigger

Use this skill when: user says "create component", "add component", "new component", or "build a [something] component"

Workflow

Step 1: Determine Component Type

  • UI Componentcomponents/ui/ (shadcn-style, reusable primitives)
  • Feature Componentcomponents/[feature]/ (feature-specific)
  • Layout Componentcomponents/layout/ (page layouts, headers, footers)
  • Marketing Componentcomponents/marketing/ (landing page sections)

Step 2: Determine Client/Server

  • Server Component (default): No interactivity, can fetch data directly
  • Client Component: Has state, effects, event handlers → add "use client"

Step 3: Create Component File

Basic Component Template:

tsx
interface [ComponentName]Props {
  // Define props with TypeScript
  title?: string;
  children?: React.ReactNode;
}

export function [ComponentName]({ title, children }: [ComponentName]Props) {
  return (
    <div className="">
      {title && <h2>{title}</h2>}
      {children}
    </div>
  );
}

Client Component Template:

tsx
"use client";

import { useState } from "react";

interface [ComponentName]Props {
  defaultValue?: string;
  onChange?: (value: string) => void;
}

export function [ComponentName]({ defaultValue = "", onChange }: [ComponentName]Props) {
  const [value, setValue] = useState(defaultValue);

  const handleChange = (newValue: string) => {
    setValue(newValue);
    onChange?.(newValue);
  };

  return (
    <div className="">
      {/* Interactive content */}
    </div>
  );
}

Data-Fetching Component Template:

tsx
import { createClient } from "@/supabase/server";

interface [ComponentName]Props {
  userId?: string;
}

export async function [ComponentName]({ userId }: [ComponentName]Props) {
  const supabase = await createClient();

  const { data, error } = await supabase
    .from("table")
    .select("*")
    .eq("user_id", userId);

  if (error) {
    return <div>Error loading data</div>;
  }

  return (
    <div>
      {data?.map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

Step 4: Add Props Interface

Always define a TypeScript interface for props:

tsx
interface [ComponentName]Props {
  // Required props
  id: string;

  // Optional props with defaults
  variant?: "default" | "outline" | "ghost";
  size?: "sm" | "md" | "lg";

  // Callback props
  onClick?: () => void;
  onChange?: (value: string) => void;

  // Children
  children?: React.ReactNode;

  // HTML attributes passthrough
  className?: string;
}

Step 5: Use Tailwind + cn() for Styling

tsx
import { cn } from "@/lib/utils";

export function [ComponentName]({ className, variant = "default" }: Props) {
  return (
    <div
      className={cn(
        "base-styles-here",
        variant === "outline" && "border border-input",
        className
      )}
    >
      {/* content */}
    </div>
  );
}

Step 6: Add to Index Export (if in subdirectory)

Create or update components/[category]/index.ts:

tsx
export { [ComponentName] } from "./[component-name]";

Step 7: Write Tests

Create __tests__/components/[component-name].test.tsx:

tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import { [ComponentName] } from "@/components/[path]/[component-name]";

describe("[ComponentName]", () => {
  it("renders correctly", () => {
    render(<[ComponentName] />);
    expect(screen.getByRole("...")).toBeInTheDocument();
  });

  it("handles click events", async () => {
    const onClick = vi.fn();
    const user = userEvent.setup();

    render(<[ComponentName] onClick={onClick} />);
    await user.click(screen.getByRole("button"));

    expect(onClick).toHaveBeenCalled();
  });

  it("applies custom className", () => {
    render(<[ComponentName] className="custom-class" />);
    expect(screen.getByRole("...")).toHaveClass("custom-class");
  });
});

File Naming Conventions

  • Component files: kebab-case.tsx (e.g., user-avatar.tsx)
  • Component names: PascalCase (e.g., UserAvatar)
  • Test files: kebab-case.test.tsx

Checklist

  • Component file created
  • Props interface defined with TypeScript
  • "use client" added if needed
  • Tailwind + cn() for styling
  • Index export updated
  • Tests written
  • Build passes: pnpm build