---
name: tryghost-shade-new-component
description: "Acceptance checklist for adding or editing a Shade component or pattern file — naming, sibling story, className forwarding, cva variants, required states, recipe usage. Trigger when editing files in apps/shade/src/components."
license: "MIT"
metadata:
  decimalai-skill-id: "11d2f0fa-49cb-4b4f-a1b4-83dcbab5cc09"
  decimalai-version-id: "86fb99df-959d-41d3-b7fb-1371203422ef"
  decimalai-version: "2"
  decimalai-body-sha256: "600f806236db7f35740fd8b7cb033efd1b46069c1ab454f5c982a63668440a03"
  decimalai-name: "tryghost/Shade new component"
  decimalai-format: "1"
  decimalai-invocation: "model"
  decimalai-category: "frontend"
  decimalai-source: "https://app.decimal.ai/s/tryghost-shade-new-component@2/SKILL.md"
---
# Shade — new component / pattern checklist

Before marking a Shade component or pattern done, this checklist must pass.

## Files and naming

- File: **kebab-case** (`dropdown-menu.tsx`) — matches ShadCN CLI output. Don't rename for casing.
- Exported identifier: **PascalCase** (`DropdownMenu`).
- Hooks, functions, variables: camelCase.
- Sibling **`<name>.stories.tsx`** is required (same folder).
- A component file exports only components and types, so Fast Refresh can hot-update it (ESLint enforces this). An exported `cva()` config goes in a sibling **`<name>-variants.ts`**; exported hooks, contexts and helpers go in their own sibling module (`sidebar-context.ts`). These siblings belong to the component; they aren't Recipes.
- Inside Shade itself, import via the `@/` alias (`@/lib/utils`, `@/components/ui/input-surface`).

## Storybook title prefix

| Layer         | Title                 |
| ------------- | --------------------- |
| Primitive     | `Primitives / <Name>` |
| Component     | `Components / <Name>` |
| Recipe        | `Recipes / <Name>`    |
| Pattern       | `Patterns / <Name>`   |
| Token gallery | `Tokens / <Topic>`    |

Required on every story file:

- `tags: ['autodocs']`
- `parameters.docs.description.component` — short one-line summary
- Per-story `parameters.docs.description.story` — one-liner explaining when to use that variant
- One story per important variant/state. Many small focused stories > one prose-heavy story.

## Component shape

```ts
// thing-variants.ts
import {cva} from 'class-variance-authority';

const thingVariants = cva('base-classes-here', {
    variants: {
        variant: {default: '...', destructive: '...'},
        size: {default: '...', sm: '...'}
    },
    defaultVariants: {variant: 'default', size: 'default'}
});

export {thingVariants};
```

```tsx
// thing.tsx
import * as React from 'react';
import {type VariantProps} from 'class-variance-authority';
import {thingVariants} from '@/components/ui/thing-variants';
import {cn} from '@/lib/utils';

export interface ThingProps
    extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof thingVariants> {}

const Thing = React.forwardRef<HTMLDivElement, ThingProps>(
    ({className, variant, size, ...props}, ref) => (
        <div
            ref={ref}
            className={cn(thingVariants({variant, size, className}))}
            {...props}
        />
    )
);
Thing.displayName = 'Thing';

export {Thing};
```

Hard requirements:

- `className` forwarded and merged via `cn()` — never overwritten, never dropped. Applies to every component that renders DOM.
- Visual/interactive props only (`variant`, `size`, `loading`). **No workflow props** (`isMembersPage`, `layoutMode`) — those mean you actually need a Pattern wrapper.
- Multi-region components expose compound subcomponents (`.Title`, `.Actions`, `.Body`) — not a prop bag.

When applicable:

- **`forwardRef`** — use when the component renders a single DOM element consumers might need a ref to (most UI controls). Skip for: pure provider/context wrappers, Radix root re-exports that don't render DOM themselves, and components whose `ref` semantics are already handled by a child.
- **`cva()`** — use when the component has variants or stateful class branches (`variant`, `size`, `tone`). Skip for simple single-style components where a `cn(...)` call is clearer.
- **Recipes** (`<name>.ts`, no JSX): no `forwardRef`, no `cva()`, no React. They return class strings.

## Required states (all four)

Every interactive component must work in:

- **default**
- **hover**
- **focus-visible** (use `focus-visible:`, never `focus:`)
- **disabled**

Each state visible in the story. Optional states (active, loading, error, empty) only when they apply.

For form controls, drive chrome through the **`inputSurface` recipe** — don't roll your own border/focus ring. See the `shade-input-surface-recipe` skill.

## Tokens

- No hex values. No `bg-gray-200`-style raw palette utilities for UI chrome.
- No `dark:` variants for colour — semantic tokens handle dark mode.

See `shade-tokens-not-hex` and `shade-no-dark-variants`.

## Before marking done

- [ ] Lives in the right layer (`shade-component-decision`)
- [ ] kebab-case filename, PascalCase export, sibling `<name>.stories.tsx`
- [ ] `className` forwarded and merged with `cn()` (always)
- [ ] `forwardRef` if the component renders DOM consumers might ref
- [ ] `cva()` if the component has variants; `defaultVariants` set
- [ ] The component file exports only components and types; exported variants, hooks and contexts live in sibling modules
- [ ] All four required states work and are visible in the story
- [ ] Semantic tokens only — no hex, no raw greys, no `dark:` colour variants
- [ ] No product-specific props on a generic Component
- [ ] Story has `tags: ['autodocs']`, component description, and one-line per-story descriptions
- [ ] `pnpm lint`, `pnpm test`, and Storybook all clean

## Source of truth

Storybook → Overview / Contributing and the component's stories.
