add-component
Scaffold a React component with Vitest tests (TDD-first)
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Scaffold a React component with Vitest tests (TDD-first)
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Scaffold a REST API endpoint with hexagonal layers and integration tests
Scaffold a React component with Vitest tests (TDD-first)
Scaffold a new Shell extension with hexagonal layers and TDD
Add a sub-feature within an existing Shell extension
Scaffold a SQL migration with RLS policy, rollback plan, and test seed data
Scaffold a Tauri IPC command with full round-trip (Rust handler, TS types, React hook, tests)
| name | add-component |
| description | Scaffold a React component with Vitest tests (TDD-first) |
| user_invocable | true |
Scaffold a new React component inside a feature's ui/ layer, test-first.
Before creating anything, search for an existing solution:
src/features/*/ui/ for a component that
already covers the need. If one exists, extend or compose -- do not duplicate.@base-ui/react for built-in primitives:
Dialog, Menu, Tooltip, Popover, Switch, Tabs, Select, Checkbox, Slider,
Accordion, Collapsible, Progress, Toggle, AlertDialog, NumberField, etc.
Wrap and style these rather than building from scratch.@tanstack/react-table v8 for headless table/grid
needs. Compose with Grove styling rather than hand-rolling table logic.If an existing primitive or component covers the need, STOP and recommend composition instead of creation. Only proceed if nothing fits.
Prerequisites: This skill references packages that may need to be installed:
class-variance-authority — for style variants (install: pnpm add class-variance-authority)@base-ui/react — for UI primitives (install: pnpm add @base-ui/react)motion + motion-plus — for animations (see .claude/rules/motion.md for setup)Skip checks for packages not yet installed.
Collect these before writing any code:
| Field | Description |
|---|---|
| Component name | PascalCase (e.g., PersonaCard, JourneyTimeline) |
| Parent feature | Feature directory name (e.g., personas, journeys) |
| Purpose | One sentence describing what it renders and why |
| Props interface | TypeScript props with JSDoc comments |
| Variants | CVA variants (size, intent, state) if applicable |
| Interactive behavior | Click, hover, keyboard, drag-and-drop |
| Data source | Which application-layer hook or store provides data |
| Accessibility | ARIA roles, keyboard navigation, focus management |
Create src/features/<feature>/ui/<Name>.test.tsx.
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { ComponentName } from "./<Name>";
describe("<Name>", () => {
it("renders with required props", () => {
render(<ComponentName label="Test" />);
expect(screen.getByTestId("<name>")).toBeInTheDocument();
});
it("handles user interaction", async () => {
const user = userEvent.setup();
const onAction = vi.fn();
render(<ComponentName label="Test" onAction={onAction} />);
await user.click(screen.getByTestId("<name>-action"));
expect(onAction).toHaveBeenCalledOnce();
});
it("renders variant correctly", () => {
render(<ComponentName label="Test" variant="primary" />);
const el = screen.getByTestId("<name>");
expect(el.className).toContain("primary");
});
it("handles empty/loading state", () => {
render(<ComponentName label="Test" loading />);
expect(screen.getByTestId("<name>-loading")).toBeInTheDocument();
});
});
data-testid for selectors (Testing Library default)userEvent over fireEvent for realistic interactionswaitFor for async state updatespnpm test -- <Name>
All tests must FAIL. If any pass, the test is not testing new behavior -- fix it.
Create src/features/<feature>/ui/<Name>.tsx.
import { cva, type VariantProps } from "class-variance-authority";
import { type ComponentProps } from "react";
/* ------------------------------------------------------------------ */
/* Styles (CVA) */
/* ------------------------------------------------------------------ */
const nameStyles = cva(
[
// Base styles -- design tokens only
"grove-component-base",
],
{
variants: {
variant: {
default: "grove-component-default",
primary: "grove-component-primary",
},
size: {
sm: "grove-component-sm",
md: "grove-component-md",
lg: "grove-component-lg",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
},
);
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
interface NameProps
extends VariantProps<typeof nameStyles>,
Omit<ComponentProps<"div">, "className"> {
/** Visible label text */
label: string;
/** Optional loading state */
loading?: boolean;
/** Callback on primary action */
onAction?: () => void;
}
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
export function Name({
label,
variant,
size,
loading = false,
onAction,
ref,
...rest
}: NameProps) {
if (loading) {
return (
<div data-testid="<name>-loading" aria-busy="true">
Loading...
</div>
);
}
return (
<div
ref={ref}
data-testid="<name>"
className={nameStyles({ variant, size })}
{...rest}
>
<span>{label}</span>
{onAction && (
<button
type="button"
data-testid="<name>-action"
onClick={onAction}
>
Action
</button>
)}
</div>
);
}
| Rule | Detail |
|---|---|
| Design tokens only | --grove-* custom properties. Never raw hex, px, or colors |
| CVA for variants | All visual variants through class-variance-authority |
data-testid | Root element and every interactive child |
ref as prop | Pass ref as a regular prop -- never use forwardRef |
| No manual memoization | React Compiler handles it -- no useMemo, useCallback, React.memo |
useReducedMotion() gate | Wrap every animation with reduced-motion check |
| Base UI primitives | Use @base-ui/react for dialogs, menus, tooltips, etc. |
| Hexagonal imports | Import from application layer only, never from domain or adapters |
| No cross-feature imports | Components may not import from other features |
import { motion, useReducedMotion } from "motion/react";
function AnimatedComponent({ children }: { children: React.ReactNode }) {
const prefersReduced = useReducedMotion();
return (
<motion.div
initial={prefersReduced ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={prefersReduced ? undefined : { opacity: 0, y: -8 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
{children}
</motion.div>
);
}
.grove-component-base {
padding: var(--grove-space-3);
border-radius: var(--grove-radius-md);
font-family: var(--grove-font-sans);
font-size: var(--grove-font-size-sm);
color: var(--grove-text-primary);
background: var(--grove-surface-base);
border: 1px solid var(--grove-border-default);
box-shadow: var(--grove-shadow-sm);
transition: box-shadow 0.15s ease;
}
.grove-component-base:focus-visible {
outline: 2px solid var(--grove-accent);
outline-offset: 2px;
}
pnpm test
All tests must pass. If any fail, fix the component -- not the tests.
If the component will be consumed outside its feature (rare -- most are feature-internal), add a barrel export:
// src/features/<feature>/index.ts
export { Name } from "./ui/<Name>";
| File | Purpose |
|---|---|
src/features/<feature>/ui/ | Component location |
src/features/<feature>/application/ | Hooks and stores the component consumes |
src/features/<feature>/domain/ | Types the hooks depend on |
src/features/shared/ | Shared utilities and types |
package.json | Check available dependencies |
src/app.css | Global --grove-* token definitions |
Before declaring done:
src/features/<feature>/ui/<Name>.test.tsxsrc/features/<feature>/ui/<Name>.tsxpnpm test passesdata-testid on root and interactive elements--grove-* design tokens in CSS (no raw values)ref passed as prop (no forwardRef)useMemo / useCallback / React.memouseReducedMotion() gates all animations@base-ui/react used for interactive primitivespnpm check passes (TypeScript + ESLint)