

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
1# Shade — Agent guide23Canonical, rule-shaped reference for AI-assisted work on Shade and any admin app that consumes it. Storybook docs at `apps/shade/src/docs/` are the human-facing surface (visual, designer-focused). **This file is the source of truth for decisions.**45## Core assumptions67- **Shade is the default source for Ghost Admin UI.** Reach for it first. If a usable primitive, component, recipe, or pattern exists, use it.8- **Shade is admin-only.** Don't generate install instructions, stylesheet imports, or `ShadeApp` setup snippets — every admin app is already wired up.9- **Imports come from layer-specific subpaths**, never the root barrel:10```ts11 import {Stack, Inline, Box, Grid, Container, Text} from '@tryghost/shade/primitives';12 import {Button, Input, Dialog} from '@tryghost/shade/components';13 import {PageHeader, KpiCard, Filters} from '@tryghost/shade/patterns';14 import {ListPage} from '@tryghost/shade/page-templates';15 import {cn} from '@tryghost/shade/utils';16 import {ShadeApp} from '@tryghost/shade/app';17```18- Inside Shade itself, use the `@/` alias for cross-file imports.1920## The five layers2122| Layer | Path | Use when | Examples |23|---|---|---|---|24| **Tokens** | `theme-variables.css`, `tailwind.theme.css` | You need a colour, size, duration, radius | `--background`, `--text-base`, `--radius-md` |25| **Primitives** | `src/components/primitives/` | You need layout structure | `Stack`, `Inline`, `Box`, `Grid`, `Container`, `Text` |26| **Components** | `src/components/ui/` | You need a generic, accessible UI control | `Button`, `Input`, `Dialog`, `Tabs`, `Card`, `DropdownMenu` |27| **Recipes** | `src/components/ui/<name>.ts` | Several components share the same visual rule (chrome, focus, density) | `inputSurface` |28| **Patterns** | `src/components/patterns/` | The shape is product-specific and recurs across Admin | `PageHeader`, `Filters`, `KpiCard`, `GhAreaChart` |2930Plus one additional barrel:3132- **`page-templates/`** (`src/components/page-templates/`) — top-level page wrappers (`ListPage` today). Composes Patterns + Components + Primitives. Imported via `@tryghost/shade/page-templates`.3334## Decision flow: where does new code go?3536When building a new UI shape, walk this top-to-bottom and stop at the first match.37381. **Is it just a colour, size, radius, duration?** → **Token**. Add to `theme-variables.css` (semantic) or `tailwind.theme.css` (`@theme` raw).392. **Is it layout-only (spacing, alignment, structure)?** → **Primitive**. Use an existing one (`Stack`, `Inline`, `Box`, `Grid`, `Container`, `Text`); only add a new one if the structural shape is genuinely novel.403. **Is it a generic, accessible UI control with no Ghost-specific knowledge?** → **Component**. Reuse an existing one in `src/components/ui/`. Only add a new component if it doesn't exist and the rules below pass.414. **Is it the same chrome / focus / density rule shared across ≥ 2 components?** → **Recipe**. A class-string function next to the components in `src/components/ui/`.425. **Does it know about Ghost (KPIs, members, posts, newsletters, analytics)?** → **Pattern**.4344Quick gut check: **generic name → Component; Ghost-shaped name → Pattern.** `Button` is web-y; `KpiCard` is Ghost-y.4546## When to ADD to Shade vs keep local4748The default is to **keep code local first**. Premature design system additions lock in the wrong API and every consumer pays when you change it.4950Promote to Shade only when **all** are true:51521. **Reused at least twice in different surfaces.** Not "we might reuse this" — actual second use.532. **It's generic.** A `<MembersTable>` that's just `<Table>` with three pre-set columns is not a Shade thing; it belongs in the app.543. **The shape has settled.** Slots and composition have been stable across both local copies for at least one iteration cycle.554. **It has a generic name.** `PageHeader`, `KpiCard`, `Filters`. Not `MembersFilterBar` or `PostAnalyticsHero` — those name a single surface and will date.565. **The API is slots, not props.** 3–6 named subcomponents (`.Title`, `.Actions`, `.Body`), not a `<ListPage title="..." onAdd={...} columns={...} />` prop bag.576. **State stays with the consumer.** No `useQuery`, no routing, no app-context reads inside Shade.5859Fail any of these? Keep it local. Build it again somewhere else first, then promote.6061## Conventions6263### File names6465- Files: kebab-case (`dropdown-menu.tsx`) — matches ShadCN CLI output.66- Components: PascalCase exports (`DropdownMenu`).67- Hooks, functions, variables: camelCase.6869### Component file structure7071- One `<name>.tsx` per component (or compound family).72- Sibling `<name>.stories.tsx` is required.73- Use `cn()` to merge classes (`@tryghost/shade/utils` for consumers, `@/lib/utils` inside Shade).74- Use `cva()` for variants. Forward and merge `className` so consumers can extend without wrapping.75- For multi-region components, expose compound subcomponents (`.Title`, `.Actions`, …) — not a prop bag.7677### Storybook titles7879| Layer | Title prefix |80|---|---|81| Primitive | `Primitives / <Name>` |82| Component | `Components / <Name>` |83| Recipe | `Recipes / <Name>` |84| Pattern | `Patterns / <Name>` |85| Token gallery | `Tokens / <Topic>` |8687Use `tags: ['autodocs']`. Add a short `parameters.docs.description.component`. Per-story `parameters.docs.description.story` is a one-liner explaining when to use that variant.8889### Tokens & dark mode9091- Use **semantic tokens** (`bg-background`, `text-foreground`, `border-border-default`, `var(--surface-elevated)`) — these flip in dark mode automatically.92- Never hard-code hex or `hsl()` values, even temporarily.93- Don't write `dark:` Tailwind variants for colour. The tokens do that. (Exceptions: assets like logos/illustrations.)94- Inside stylesheets, use `var(--token)` directly. Don't wrap in `hsl()` — the variables already contain `hsl(…)`.95- New tokens go in `apps/shade/theme-variables.css` (semantic + dark-mode overrides) or `apps/shade/tailwind.theme.css` (raw `@theme`).9697### Required states for components9899Every interactive component must work in **default, hover, focus-visible, disabled** before anything else. Optional states (active, loading, error, empty) are documented when they apply. Each state should be visible in the story.100101For form controls, drive chrome through the `inputSurface` recipe — don't roll your own focus ring.102103## ShadCN guardrails104105Most new components start from a ShadCN install:106107```bash108pnpm dlx shadcn@latest add <component-name>109```110111- **Never overwrite an existing Shade component** when the CLI prompts. Choose "No".112- Run on a fresh branch before installing.113- If the component already exists, generate into a scratch repo and manually port the parts you want.114- After integrating: swap raw colours for semantic tokens, ensure the four required states work, trim any props that hint at a specific surface, copy useful examples from `https://ui.shadcn.com/docs/components/<name>` into the story.115- Use the `@` alias for internal imports (e.g. `@/lib/utils`).116117## Build, test, dev118119| Command | Purpose |120|---|---|121| `pnpm storybook` | Run Storybook locally (visual verification) |122| `pnpm build` | Type declarations + Vite library build to `es/` |123| `pnpm build-storybook` | Static Storybook export |124| `pnpm test` | Type-check + Vitest with coverage |125| `pnpm test:unit` | Unit tests only |126| `pnpm test:types` | TS type-check only |127| `pnpm lint` | ESLint (src + tests, `tailwindcss/*` rules enabled) |128129Always run `pnpm lint` before committing.130131## Testing expectations132133Formal testing strategy is TBD. Interim rules:134135- Vitest + Testing Library + jsdom.136- Location: `test/unit/**/*.test.(ts|tsx|js)`.137- Use `test/unit/utils/test-utils.tsx`'s `render` helper when a wrapper is needed.138- For new UI components, prioritise comprehensive Storybook stories; add focused unit tests where they pay off (hooks, utils, logic-heavy parts).139- No strict coverage threshold yet — just run `pnpm test` locally and keep it green.140141## Anti-patterns (don't do these)142143- **Don't import `@tryghost/shade/styles.css` separately from an embedded admin app.** The admin entry point is the single CSS lane; importing twice causes duplicate utilities and cascade conflicts.144- **Don't import from the root `@tryghost/shade` barrel.** Use layer-specific subpaths.145- **Don't add `dark:` variants for colour.** Use semantic tokens.146- **Don't add product-specific props to a generic Component.** Extract a Pattern wrapper.147- **Don't put `useQuery` or app-context reads inside a Pattern.** Patterns are layout/composition contracts. Bring-your-own state.148- **Don't rename ShadCN-generated files** purely for casing.149- **Don't create new top-level CSS files.** Tokens live in `theme-variables.css` and `tailwind.theme.css`.150- **Don't add migration / setup / install instructions to component docs.** Shade is admin-only and already wired up.151152## Commit & PR conventions153154Commit messages are the release notes.155156```157Added Avatar component158159ref https://linear.app/ghost/issue/DES-1234/avatar160161Builds on Radix Avatar with a size variant scale and a fallback initials slot.162```163164- **Line 1**: ≤ 80 chars, past tense. Starts with one of: `Fixed`, `Changed`, `Updated`, `Improved`, `Added`, `Removed`, `Reverted`, `Moved`, `Released`, `Bumped`, `Cleaned`.165- **Line 2**: blank.166- **Line 3**: magic word (`ref`, `closes`, `fixes`) + space + **full Linear URL**. Not `ref:` (no colon).167- **Line 4+**: explain the **why**, not the what.168- Dependency bumps: focus the message on user-visible changes.169170PRs: describe the change, link the Linear issue, include screenshots or GIFs for any UI change, update or add stories.171172## Acceptance checklist (component)173174Before marking a component done:175176- [ ] Lives in the right layer (re-read **Decision flow** above)177- [ ] `className` forwarded and merged with `cn()`178- [ ] Default, hover, focus-visible, disabled all work and are visible in the story179- [ ] No hex values, no `bg-gray-200`-style raw palette utilities for UI chrome — semantic tokens only180- [ ] No product-specific props on a generic Component181- [ ] Story covers variants + states with one-line "when to use" descriptions182- [ ] `pnpm lint`, `pnpm test`, and Storybook all clean183184## Repo layout185186```187apps/shade/188├── theme-variables.css Runtime semantic tokens + dark mode189├── tailwind.theme.css Tailwind @theme raw catalogue190├── .storybook/ Storybook config (preview.tsx controls sort order)191└── src/192 ├── components/193 │ ├── primitives/ Layout primitives194 │ ├── ui/ Generic controls + recipes195 │ └── patterns/ Product compositions196 ├── docs/ MDX + token showcase stories197 │ ├── showcase/ Internal-only token display components198 │ └── tokens/ Token visual stories199 ├── hooks/ Generic React hooks200 ├── lib/ Utilities (cn, formatters, chart helpers)201 └── providers/ Context providers202```203204Entrypoint barrels (`components.ts`, `primitives.ts`, `patterns.ts`) re-export from the matching folder.205206## Human docs207208The MDX in `src/docs/` and the per-component stories are the **human-facing** surface. They're short, visual, example-driven. If a human-facing rule conflicts with this file, **this file wins** — and that's a sign the MDX needs updating.209
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| TryGhost/GhostAGENTS.md · 55k | AGENTS.md | setupbuildlint-formatstyle+6 | 78/100 | today | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | today | |
| TryGhost/Ghostkoenig/koenig-lexical/AGENTS.md · 55k | AGENTS.md | setuptestarchagent-behaviour | 78/100 | today |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| code-yeongyu/oh-my-openagentpackages/web/AGENTS.md · 68k | AGENTS.md | setupbuildtestlint-format+6 | 100/100 | 13 days ago | |
| mui/material-uiAGENTS.md · 99k | AGENTS.md | setupbuildtestlint-format+9 | 100/100 | 14 days ago | |
| aaif-goose/gooseAGENTS.md · 53k | AGENTS.md | setupbuildtestlint-format+7 | 100/100 | 8 days ago | |
| duckduckgo/content-scope-scriptsspecial-pages/AGENTS.md · 70 | AGENTS.md | buildteststylearch+3 | 100/100 | 14 days ago | |
| deepseek-ai/deepseek-harnessnative/landlock-run/AGENTS.md · 104k | AGENTS.md | setupteststylearch+3 | 100/100 | today | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | today | |
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 201k | AGENTS.md | buildteststylearch+3 | 100/100 | 14 days ago | |
| elastic/elasticsearchx-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/AGENTS.md · 78k | AGENTS.md | buildtestlint-formatstyle+2 | 100/100 | 14 days ago |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/tryghost-ghost-apps-shade-agents)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.