| name | new-component |
| description | Scaffold a new accessible frontend component (shadcn/Radix/CVA/Tailwind) plus a test stub, following Butler's UI and a11y conventions |
| disable-model-invocation | true |
New Component
Scaffold a new @butler/frontend component that already follows the project stack
(shadcn-style + Radix UI Slot + class-variance-authority + Tailwind v4) and the
mandated accessibility conventions, together with a co-located test stub that matches
the repo's Testing Library pattern.
The generated files pass pnpm --filter @butler/frontend lint and type-check as-is.
Arguments
A PascalCase component name (e.g., StatCard, MemberBadge, BookingBanner).
Derive the kebab-case slug from it (StatCard → stat-card).
Steps
- From the argument, compute:
Name — the PascalCase name (e.g., StatCard)
slug — kebab-case (e.g., stat-card)
- Create the component at
packages/frontend/src/components/<slug>.tsx from the
Component template below, replacing __Name__ and __slug__.
- Create the test stub at
packages/frontend/src/__tests__/<slug>.test.tsx from the
Test template below (the repo keeps tests under src/__tests__/, importing the
component with a .js ESM extension — match that, do not invent a sibling test file).
- Choose the right semantic root element for what you're building and adjust the
template (see Accessibility checklist). Do not leave a
<div> where a semantic
element fits — Butler requires semantic HTML over generic containers.
- Run
pnpm --filter @butler/frontend lint and pnpm --filter @butler/frontend type-check
to confirm the scaffold is clean, then fill in the real markup/variants.
Component template
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "radix-ui";
import { cn } from "../lib/utils.js";
const __Name__Variants = cva(
"relative rounded-lg outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
{
variants: {
variant: {
default: "border border-border bg-card text-card-foreground",
muted: "border border-transparent bg-muted text-muted-foreground",
},
size: {
sm: "p-3 text-sm",
default: "p-4",
lg: "p-6",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
type __Name__Props = React.ComponentProps<"section"> &
VariantProps<typeof __Name__Variants> & {
asChild?: boolean;
};
function __Name__({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: __Name__Props) {
const Comp = asChild ? Slot.Root : "section";
return (
<Comp
data-slot="__slug__"
data-variant={variant}
data-size={size}
className={cn(__Name__Variants({ variant, size, className }))}
{...props}
/>
);
}
export { __Name__, __Name__Variants };
export type { __Name__Props };
Test template
import { describe, it, expect, afterEach } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { __Name__ } from "../components/__slug__.js";
describe("__Name__", () => {
afterEach(() => {
cleanup();
});
it("renders its children", () => {
render(<__Name__ aria-label="example __slug__">content</__Name__>);
expect(screen.getByText("content")).toBeDefined();
});
it("exposes an accessible name", () => {
render(<__Name__ aria-label="example __slug__">content</__Name__>);
expect(screen.getByLabelText("example __slug__")).toBeDefined();
});
it("applies the requested variant", () => {
render(
<__Name__ aria-label="m" variant="muted">
x
</__Name__>,
);
expect(screen.getByLabelText("m").dataset.variant).toBe("muted");
});
});
Accessibility checklist (must hold for the finished component)
- Semantic root element (
button, nav, section, ul, article, …) — never a
bare <div>/<span> where a semantic element fits.
- Accessible name on every interactive/landmark element (visible text,
aria-label,
or aria-labelledby).
- Keyboard operable: prefer native interactive elements (
<button>, <a>) so
Enter/Space/Tab work for free; if you must use a non-native control, add tabIndex,
role, and key handlers.
- Visible focus: the
focus-visible:ring-* classes in the template base must survive
any restyle.
- Icons are
aria-hidden="true" unless they carry meaning (then label them).
Notes
- Use relative
.js ESM imports (../lib/utils.js, ../components/x.js) like the
rest of the app code — the @/ alias is configured for the app bundle but not for
Vitest, so @/-imported components are not testable. Only vendored components/ui/**
(coverage-excluded) uses @/.
- Components are React 19 function components (no
forwardRef); type props with
React.ComponentProps<"el"> and merge VariantProps, mirroring
components/ui/button.tsx.
- Keep one component per file; export the component and its
cva variants.