.agents/skills/shade-component-decision/SKILL.md
Before adding anything to Shade, walk the decision flow and the promotion checklist. The default is keep code local first — premature design system additions lock in the wrong API.
| Layer | Lives in | Examples |
|---|---|---|
| Token | theme-variables.css, tailwind.theme.css | --background, --text-base, --radius-md |
| Primitive | src/components/primitives/ | Stack, Inline, Box, Grid, Container, Text |
| Component | src/components/ui/ | Button, Input, Dialog, Tabs, Card |
| Recipe | src/components/ui/<name>.ts (no JSX) | inputSurface |
| Pattern | src/components/patterns/ | PageHeader, KpiCard, Filters, GhAreaChart |
Plus two additional barrels:
page-templates/ (@tryghost/shade/page-templates) — top-level page wrappers (ListPage today). Composes Patterns + Components + Primitives. If you're building a new shape that wraps a whole admin page, this is where it goes. See shade-page-templates.posts-stats/ (@tryghost/shade/posts-stats) — transitional layer for components shared between apps/posts and apps/stats until those merge. Don't add to it unless the file is genuinely posts-or-stats-only and shared between both apps.theme-variables.css (semantic) or tailwind.theme.css (raw @theme).src/components/ui/.Quick gut check: generic name → Component; Ghost-shaped name → Pattern. Button is web-y; KpiCard is Ghost-y.
Each layer can use anything below it. The reverse is forbidden.
<MembersTable> that's just <Table> with three preset columns belongs in the app, not Shade.PageHeader, KpiCard, PostShareModal — not MembersFilterBar or PostAnalyticsHero..Title, .Actions, .Body) — not <ListPage title="..." onAdd={...} columns={...} />.useQuery, no routing, no app-context reads inside Shade.Fail any of these? Keep it local. Build it again somewhere else first, then promote.
| Tempting | Actually | Why |
|---|---|---|
Add a variant="kpi" to Card | New Pattern KpiCard | Product-specific shape, generic Component shouldn't know about it |
Add <MembersFilterBar> to patterns | Keep local in apps/admin-x-settings | Single-surface name |
| Add a one-off class string as a recipe | Inline it in the one component | Recipes are for shared rules across ≥ 2 components |
Add a useQuery-driven <MembersList> to patterns | Keep local — patterns are state-free | Bring-your-own state |
Wrap a <div className="flex gap-3"> as a new primitive | Use Inline gap="md" | Already covered |
shade-new-component skill for the file/story/state checklist.recipeClasses + recipe() shape (see apps/shade/src/components/ui/input-surface.ts).apps/shade/theme-variables.css (semantic + dark-mode override) or apps/shade/tailwind.theme.css (raw @theme). Never an ad-hoc CSS variable in a component file.Full rules: apps/shade/AGENTS.md. Human-facing: Storybook → Overview / Layers.