|
| 1 | +--- |
| 2 | +name: Shade component decision |
| 3 | +description: Decide which Shade layer (Token, Primitive, Component, Recipe, Pattern) a new piece of UI belongs in — and whether it should be added to Shade at all. Trigger when creating new files in apps/shade/src/components, or proposing to add a new Shade component. |
| 4 | +autoTrigger: |
| 5 | + - fileEdit: "apps/shade/src/components/**/*.{ts,tsx}" |
| 6 | +--- |
| 7 | + |
| 8 | +# Shade — pick the right layer (and decide whether to add to Shade at all) |
| 9 | + |
| 10 | +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. |
| 11 | + |
| 12 | +## The five layers |
| 13 | + |
| 14 | +| Layer | Lives in | Examples | |
| 15 | +|---|---|---| |
| 16 | +| **Token** | `theme-variables.css`, `tailwind.theme.css` | `--background`, `--text-base`, `--radius-md` | |
| 17 | +| **Primitive** | `src/components/primitives/` | `Stack`, `Inline`, `Box`, `Grid`, `Container`, `Text` | |
| 18 | +| **Component** | `src/components/ui/` | `Button`, `Input`, `Dialog`, `Tabs`, `Card` | |
| 19 | +| **Recipe** | `src/components/ui/<name>.ts` (no JSX) | `inputSurface` | |
| 20 | +| **Pattern** | `src/components/patterns/` | `PageHeader`, `ListPage`, `KpiCard`, `Filters` | |
| 21 | + |
| 22 | +Plus two additional barrels: |
| 23 | + |
| 24 | +- **`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`. |
| 25 | +- **`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. |
| 26 | + |
| 27 | +## Decision flow (top-to-bottom, stop at first match) |
| 28 | + |
| 29 | +1. **Just a colour, size, radius, duration?** → **Token**. Add to `theme-variables.css` (semantic) or `tailwind.theme.css` (raw `@theme`). |
| 30 | +2. **Layout-only (spacing, alignment, structure)?** → **Primitive**. Reuse an existing one; only add a new one if the structural shape is genuinely novel. |
| 31 | +3. **Generic, accessible UI control with no Ghost-specific knowledge?** → **Component**. Reuse first; only add if it doesn't exist and the promotion checklist below passes. |
| 32 | +4. **The same chrome / focus / density rule shared across ≥ 2 components?** → **Recipe**. A class-string function (no JSX, no React) next to the components in `src/components/ui/`. |
| 33 | +5. **Knows about Ghost (KPIs, members, posts, newsletters, analytics)?** → **Pattern**. |
| 34 | + |
| 35 | +**Quick gut check: generic name → Component; Ghost-shaped name → Pattern.** `Button` is web-y; `KpiCard` is Ghost-y. |
| 36 | + |
| 37 | +## Allowed dependencies (one-way) |
| 38 | + |
| 39 | +Each layer can use anything **below** it. The reverse is forbidden. |
| 40 | + |
| 41 | +- A Button can't reach into a Pattern. |
| 42 | +- A Primitive can't import a Component. |
| 43 | +- A Recipe is pure CSS classes — no React, no JSX. |
| 44 | + |
| 45 | +## Promotion checklist — add to Shade only if ALL are true |
| 46 | + |
| 47 | +1. **Reused at least twice in different surfaces.** Actual second use, not "we might reuse this." |
| 48 | +2. **It's generic.** A `<MembersTable>` that's just `<Table>` with three preset columns belongs in the app, not Shade. |
| 49 | +3. **The shape has settled.** Slots and composition have been stable across both local copies for at least one iteration. |
| 50 | +4. **It has a generic name.** `PageHeader`, `KpiCard`, `PostShareModal` — not `MembersFilterBar` or `PostAnalyticsHero`. |
| 51 | +5. **The API is slots, not props.** 3–6 named subcomponents (`.Title`, `.Actions`, `.Body`) — not `<ListPage title="..." onAdd={...} columns={...} />`. |
| 52 | +6. **State stays with the consumer.** No `useQuery`, no routing, no app-context reads inside Shade. |
| 53 | + |
| 54 | +**Fail any of these? Keep it local.** Build it again somewhere else first, then promote. |
| 55 | + |
| 56 | +## Common misclassifications |
| 57 | + |
| 58 | +| Tempting | Actually | Why | |
| 59 | +|---|---|---| |
| 60 | +| Add a `variant="kpi"` to `Card` | New Pattern `KpiCard` | Product-specific shape, generic Component shouldn't know about it | |
| 61 | +| Add `<MembersFilterBar>` to patterns | Keep local in `apps/admin-x-settings` | Single-surface name | |
| 62 | +| Add a one-off class string as a recipe | Inline it in the one component | Recipes are for shared rules across ≥ 2 components | |
| 63 | +| Add a `useQuery`-driven `<MembersList>` to patterns | Keep local — patterns are state-free | Bring-your-own state | |
| 64 | +| Wrap a `<div className="flex gap-3">` as a new primitive | Use `Inline gap="md"` | Already covered | |
| 65 | + |
| 66 | +## When you've decided |
| 67 | + |
| 68 | +- New **Component** or **Pattern** → also see the `shade-new-component` skill for the file/story/state checklist. |
| 69 | +- New **Recipe** → no JSX, follow the `recipeClasses` + `recipe()` shape (see `apps/shade/src/components/ui/input-surface.ts`). |
| 70 | +- New **Token** → add to `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. |
| 71 | + |
| 72 | +## Source of truth |
| 73 | + |
| 74 | +Full rules: `apps/shade/AGENTS.md`. Human-facing: Storybook → Overview / Layers. |
0 commit comments