- name
- 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.
- autoTrigger
- [{"fileEdit":"apps/shade/src/components/**/*.{ts,tsx}"}]
# 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.
在 GitHub 查看