apps/shade/src/docs/layers.mdx
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Overview / Layers" /> <div className="sb-doc">| Layer | What it is | Lives in | Examples |
|---|---|---|---|
| Tokens | Named visual values (colour, size, duration). No markup. | theme-variables.css, tailwind.theme.css | --background, --text-base, --radius-md |
| Primitives | Layout vocabulary — rows, columns, padding, gaps. No domain meaning. | src/components/primitives/ | Stack, Inline, Box, Grid, Container, Text |
| Components | Generic, accessible UI controls. Same on every page. | src/components/ui/ | Button, Input, Dialog, Tabs, Card |
| Recipes | Shared visual rules (class-string functions). No JSX. | src/components/ui/<name>.ts | inputSurface — chrome shared by Input, Textarea, Select trigger |
| Patterns | Product-shaped compositions. Know Ghost. | src/components/patterns/ | PageHeader, ListPage, Filters, KpiCard, GhAreaChart |
Each layer is allowed to use anything below it. The reverse is forbidden: a Button can't reach into a Pattern, a Primitive can't import a Component.
Ask: what is this thing doing?
Stack, Inline, Box, Grid, Container).components/ui/).components/ui/).components/patterns/<name>.tsx).Quick gut check: generic name → component; Ghost-shaped name → pattern. Button is web-y. KpiCard is Ghost-y.
The default is to keep code local first. A component used in one place doesn't belong in a design system.
Promote to Shade when:
PageHeader, KpiCard, PostShareModal describe concepts. MembersFilterBar names a single surface and will date the moment that surface changes..Title, .Actions, .Body) — not a prop bag.useQuery inside a pattern. No app context reads. Bring-your-own state.When in doubt: keep it local, build it again somewhere else, then promote.
The posts-stats/ folder is an interim home for components shared between the Posts and Stats apps until they merge. Don't generalise it — that category goes away on its own.
apps/shade/AGENTS.md — the canonical decision flow for AI-assisted work.