import { Meta } from '@storybook/addon-docs/blocks';
Conventions for adding to Shade. The decision tree for *which layer* something belongs in is on the [Layers](?path=/docs/overview-layers--docs) page.
apps/shade/
├── .storybook/ Storybook configuration
├── theme-variables.css Semantic tokens and dark-mode values
├── tailwind.theme.css Tailwind theme mappings and raw tokens
└── src/
├── components/
│ ├── ui/ Generic controls + recipes
│ ├── primitives/ Layout primitives (Stack, Inline, …)
│ ├── patterns/ Product compositions (PageHeader, KpiCard, …)
│ └── page-templates/ Top-level page wrappers (ListPage)
├── docs/ MDX + showcase stories rendered in Storybook
├── hooks/ Generic React hooks
├── lib/ Utilities (cn, formatters, chart helpers)
└── providers/ Context providers
Each entrypoint barrel (components.ts, primitives.ts, patterns.ts, page-templates.ts) re-exports from its folder.
| What | Convention | Example |
|---|---|---|
| File names | kebab-case | dropdown-menu.tsx |
| Component identifiers | PascalCase | DropdownMenu |
| Hooks, functions, variables | camelCase | useFocusTrap, formatNumber |
| Storybook titles | layer prefix | Components / Button, Patterns / PageHeader, Recipes / inputSurface |
Always forward and merge className with cn(...). Use cva() for variants.
Always use the layer-specific subpath:
import {Button} from '@tryghost/shade/components';
import {Stack} from '@tryghost/shade/primitives';
import {PageHeader} from '@tryghost/shade/patterns';
import {cn} from '@tryghost/shade/utils';Inside Shade itself, use the @/ alias for cross-file imports.
Every component ships with <name>.stories.tsx next to it:
titlefollows the layer convention (table above).tags: ['autodocs']so docs render.- A short
parameters.docs.description.component. - One story per important variant/state, each with a one-line
parameters.docs.description.storyexplaining when to use it.
Stories are the gallery. Many small focused stories beat one prose-heavy story.
Most new components start from a ShadCN install:
pnpm dlx shadcn@latest add <component-name>Guardrails:
- Never overwrite an existing Shade component when the CLI prompts. Choose "No".
- Run on a fresh branch so the CLI's diff is clean.
- If the component already exists, generate into a scratch repo and port the parts you actually want.
- After integrating: replace any raw colour/spacing with semantic tokens; ensure default / hover / focus-visible / disabled all work; trim props that hint at a specific surface.
- Reference semantic tokens (
bg-background,text-foreground,border-border-default,--surface-elevated). Never hard-code hex values. - Don't write
dark:variants for colour — semantic tokens flip automatically. Exceptions: assets like logos and illustrations. - New tokens go in
apps/shade/theme-variables.css(semantic) orapps/shade/tailwind.theme.css(raw@theme). Don't introduce ad-hoc CSS variables in component files.
- Don't import from the root
@tryghost/shadebarrel. Use the layer-specific subpaths shown above. - Don't import Shade's stylesheet or add another
ShadeAppwrapper in an embedded Admin app. Admin owns both centrally. - Don't use raw colours or colour
dark:variants. Use semantic tokens. - Don't add product-specific props or application state to a generic component or pattern.
- Don't add a component without its sibling story and required interactive states.
- Don't let the ShadCN CLI overwrite an existing Shade component.
- Don't add something to Shade before it has demonstrated reuse. See Layers for the promotion rules.
Before merging a component:
- [ ] Lives in the right layer (see Layers)
- [ ]
classNameforwarded and merged withcn() - [ ] All states work: default, hover, focus-visible, disabled
- [ ] Semantic tokens only; no hex values, no
bg-gray-200-style raw utilities for UI chrome - [ ] Story covers variants + states with one-line "when to use" descriptions
- [ ] No product-specific props on a generic control
- [ ]
pnpm lint,pnpm test, and Storybook all clean
Shade uses Vitest, Testing Library, and jsdom. Unit tests live under
test/unit/; use test/unit/utils/test-utils.tsx when a test needs the shared
render wrapper.
For a new UI component, make the Storybook stories cover its important variants and states. Add focused unit tests when they provide useful coverage for hooks, utilities, or logic-heavy behaviour. Shade does not currently enforce a package-specific coverage threshold.
Follow the repository's contribution workflow for commits and pull requests. For Shade UI changes, also include screenshots or a GIF and update or add the relevant stories.