Two files, one repository
TryGhost/Ghost ships 2 formats across 4 indexed files. The question worth asking is whether the second one says anything the first does not.
CompareAGENTS.md ↔ CLAUDE.md
| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 58 | 19 | 0% |
| Commands | 3 | 37 | 5 | 7% |
| Section tags | 8 | 6 | 2 | 50% |
What each file covers
Sections
0 shared · 58 only in A · 19 only in B- − AGENTS.md
- − Package Manager
- − Monorepo Structure
- − ghost/* - Core Ghost packages
- − apps/* - React-based UI applications
- − koenig/* - Ghost editor (Koenig) packages
- − packages/* - Shared workspace libraries
- − e2e/ - End-to-end tests
- − Common Commands
- − Development
- − Building
- − Testing
- − Unit tests (from root)
- − Ghost core tests (from ghost/core/)
- − These run on sqlite with no extra services. The Redis/MinIO/S3 adapter suites
- − probe for their service and auto-skip when it's down (run `pnpm dev:storage`
- − etc. to exercise them); they always run in CI, which starts the services.
- − E2E browser tests (from root)
- − Running a single test
- − Watch a single DB-backed file (integration/e2e) — the default test:watch only
- − covers unit tests, so point it at the DB config explicitly:
- − Ember Admin tests (from the repository root)
- − Run one Ember Admin test file. Paths are relative to apps/ember-admin.
- − The explicit `1` supplies the numeric value required by the test script's
- − trailing `--parallel` option before additional Ember Exam arguments.
- − Linting
- − Database
- − Docker
- − How `pnpm dev` works
- − Start Ghost backend, Admin, Portal, and Docker services
- − Add optional public apps (comments-ui, sodo-search, signup-form, admin-toolbar)
- − Develop the Koenig editor against Ghost Admin (adds a koenig-lexical rebuild
- − watcher + preview server; Admin loads the editor from your local build)
- − With optional services (uses Docker Compose file composition)
- − Everything available
- − Architecture Patterns
- − Admin Apps Integration (Micro-Frontend)
- − Public Apps Integration
- − i18n Architecture
- − Build Dependencies (Nx)
- − CSS Architecture
- − TailwindCSS v4 Setup
- − Entry Point
- − Shade Styles
- − Critical Rule: Embedded Apps Must NOT Import Shade Independently
- − Public Apps
- − Legacy Apps
- − Code Guidelines
- − Commit Messages
- − ESLint Config
- − When Working on Admin UI
- − When Working on Public UI
- − When Working on Backend
- − Design System Usage
- − Analytics (Tinybird)
- − Troubleshooting
- − Build Issues
- − Test Issues
- + Shade — Agent guide
- + Core assumptions
- + The five layers
- + Decision flow: where does new code go?
- + When to ADD to Shade vs keep local
- + Conventions
- + File names
- + Component file structure
- + Storybook titles
- + Tokens & dark mode
- + Required states for components
- + ShadCN guardrails
- + Build, test, dev
- + Testing expectations
- + Anti-patterns (don't do these)
- + Commit & PR conventions
- + Acceptance checklist (component)
- + Repo layout
- + Human docs
Commands
3 shared · 37 only in A · 5 only in B- − pnpm run setup
- − pnpm dev
- − pnpm build:clean
- − pnpm test:watch
- − pnpm test:integration
- − pnpm test:e2e
- − pnpm test:all
- − pnpm test:single test/unit/path/to/test.test.js
- − pnpm exec vitest -c vitest.config.db.ts test/integration/path/to/test.test.js
- − pnpm nx run ghost-admin:test
- − pnpm nx run ghost-admin:test -- 1 --file-path=tests/acceptance/editor/publish-flow-test.js
- − pnpm knex-migrator migrate
- − pnpm reset:data
- − pnpm reset:data:empty
- − pnpm docker:build
- − pnpm docker:clean
- − pnpm docker:down
- − pnpm dev:public
- − pnpm dev:lexical
- − pnpm dev:analytics
- − pnpm dev:storage
- − pnpm dev:stripe
- − pnpm dev:full
- − pnpm dev:all
- − pnpm --filter @tryghost/i18n translate
- − pnpm --filter @tryghost/i18n lint:translations
- − pnpm fix
- − pnpm nx reset
- − pnpm
- − pnpm-workspace.yaml
- − pnpm add
- − node --conditions=source --import=tsx
- − tsc
- − node
- − pnpm setup
- − pnpm --filter @tryghost/i18n test
- − eslint-plugin-ghost: catalog:
- + pnpm dlx shadcn@latest add <component-name>
- + pnpm storybook
- + pnpm build-storybook
- + pnpm test
- + pnpm test:types
- pnpm build
- pnpm test:unit
- pnpm lint
Section tags
8 shared · 6 only in A · 2 only in B- − setup
- − testing-strategy
- − security
- − dependencies
- − database
- − monorepo
- + do-not
- + agent-behaviour
- build
- test
- lint-format
- code-style
- architecture
- git-pr
- ui
- docs
Line diff
TryGhost/Ghost · AGENTS.md
@@ −1 @@
1# AGENTS.md
2
3This file provides guidance to AI Agents when working with code in this repository.
4
5## Package Manager
6
7**Always use `pnpm` for all commands.** This repository uses pnpm workspaces, not npm.
8
9Shared dependency versions are pinned in `pnpm-workspace.yaml` under `catalog:` and referenced as `"pkg": "catalog:"` (or `catalog:<name>` for named catalogs). `catalogMode` is `strict`, so `pnpm add` routes new deps into the catalog automatically — don't inline the version.
10
11## Monorepo Structure
12
13Ghost is a pnpm + Nx monorepo with four workspace groups:
14
15### ghost/* - Core Ghost packages
16- **ghost/core** - Main Ghost application (Node.js/Express backend)
17 - Core server: `ghost/core/core/server/`
18 - Frontend rendering: `ghost/core/core/frontend/`
19
20### apps/* - React-based UI applications
21Two categories of apps:
22
23**Admin Apps** (embedded in Ghost Admin):
24- `ember-admin` - Ember.js admin client (legacy, being migrated to React)
25- `admin` - The consolidated React admin shell, organized by domain (`src/{analytics,members,posts,tags,comments,automations,...}`)
26- `admin-x-settings`, `activitypub` - Settings and ActivityPub integration (route-composed into `admin`)
27- Built with Vite + React + `@tanstack/react-query`
28
29**Public Apps** (served to site visitors):
30- `portal`, `comments-ui`, `signup-form`, `sodo-search`, `announcement-bar`
31- Built as UMD bundles, loaded via CDN in site themes
32
33**Foundation Libraries**:
34- `admin-x-framework` - Shared API hooks, routing, utilities
35- `admin-x-design-system` - Legacy design system (being phased out)
36- `shade` - New design system (shadcn/ui + Radix UI + react-hook-form + zod)
37
38### koenig/* - Ghost editor (Koenig) packages
39Merged from the former TryGhost/Koenig repo with full git history:
40
41- **koenig-lexical** - The Lexical-based rich text editor UI. Bundled into
42 Ghost Admin at build time (`apps/ember-admin` copies its UMD build into admin
43 assets; `apps/admin` imports it directly)
44- **kg-*** - Editor support packages: server-side renderers and converters
45 consumed by `ghost/core` (kg-default-nodes, kg-lexical-html-renderer,
46 kg-html-to-lexical, ...) plus frontend helpers (kg-unsplash-selector)
47
48All Koenig packages resolve via `workspace:` — nothing in dev, CI, or the
49release archive installs them from npm. They are published to npm for
50external consumers only, automatically as part of the Ghost release lane
51(see `publish_koenig_packages` in ci.yml).
52
53**Zero-build dev via the `source` export condition.** The `kg-*` libraries
54consumed by `ghost/core` (and `packages/parse-email-address`) declare a `source`
55condition in their `package.json` `exports` that points at the raw
56`src/*.ts`, listed *before* `types`/`import`/`require`:
57
58```jsonc
59".": {
60 "source": "./src/index.ts", // dev/test: read raw TS
61 "types": "./build/esm/index.d.ts",
62 "import": "./build/esm/index.js",
63 "require": "./build/cjs/index.js" // prod/published: compiled JS
64}
65```
66
67`ghost/core`'s dev runner (`nodemon.json`: `node --conditions=source --import=tsx`)
68and its Vitest configs (`resolve.conditions: ['source', 'node']` +
69`--import tsx --conditions=source`) activate this condition, so a source change
70in a `kg-*` package is picked up with **no `tsc` rebuild**. Production and the
71published npm tarball run plain `node`, which ignores `source` and uses
72`build/` — and `src/` is excluded from each package's `files` array, so it is
73never shipped. When adding a new backend-consumed TS workspace package, copy
74this `exports` shape (see `packages/parse-email-address`) so it works build-free
75in dev from day one; keep the `^build` graph for `tsc`/type-checking and prod.
76
77### packages/* - Shared workspace libraries
78Backend and shared libraries consumed via `workspace:` — not published to npm:
79
80- **i18n** - Centralized internationalization for all apps
81- **parse-email-address** - Email address parsing (see the `source` export
82 condition above)
83- **adapters/** - Adapter base classes (`adapter-base-*`: scheduling, storage,
84 SSO, redirects, route settings)
85- **custom-field-types**, **testing** - Shared field-type definitions and test
86 helpers
87- **_template** - Scaffold for new packages; excluded from the workspace
88
89### e2e/ - End-to-end tests
90- Playwright-based E2E tests with Docker container isolation
91- See `e2e/CLAUDE.md` for detailed testing guidance
92
93## Common Commands
94
95### Development
96```bash
97corepack enable pnpm # Enable corepack to use the correct pnpm version
98pnpm run setup # First-time setup (installs deps + submodules + builds workspace packages)
99pnpm dev # Start development (Docker backend + host frontend dev servers)
100```
101
102> **Fresh worktree / first run — run `pnpm setup` before anything else.** It installs deps and syncs submodules. `pnpm fix` does a clean reinstall if anything misbehaves after a branch switch.
103
104### Building
105```bash
106pnpm build # Build all packages (Nx handles dependencies)
107pnpm build:clean # Clean build artifacts and rebuild
108```
109
110### Testing
111```bash
112# Unit tests (from root)
113pnpm test:unit # Run all unit tests in all packages
114pnpm test:watch # Watch mode — unified Vitest watcher (ghost/core + all apps)
115
116# Ghost core tests (from ghost/core/)
117cd ghost/core
118pnpm test:unit # Unit tests only (Vitest, run once)
119pnpm test:watch # Watch mode — ghost/core unit tests only
120pnpm test:integration # Integration tests
121pnpm test:e2e # Server-side e2e suites (webhooks/server/frontend/api) — not browser
122pnpm test:all # All test types
123
124# These run on sqlite with no extra services. The Redis/MinIO/S3 adapter suites
125# probe for their service and auto-skip when it's down (run `pnpm dev:storage`
126# etc. to exercise them); they always run in CI, which starts the services.
127
128# E2E browser tests (from root)
129pnpm test:e2e # Run e2e/ Playwright tests
130
131# Running a single test
132cd ghost/core
133pnpm test:single test/unit/path/to/test.test.js # routes test/unit/* → unit config, test/* → DB config
134
135# Watch a single DB-backed file (integration/e2e) — the default test:watch only
136# covers unit tests, so point it at the DB config explicitly:
137pnpm exec vitest -c vitest.config.db.ts test/integration/path/to/test.test.js
138
139# Ember Admin tests (from the repository root)
140pnpm nx run ghost-admin:test
141
142# Run one Ember Admin test file. Paths are relative to apps/ember-admin.
143# The explicit `1` supplies the numeric value required by the test script's
144# trailing `--parallel` option before additional Ember Exam arguments.
145pnpm nx run ghost-admin:test -- 1 --file-path=tests/acceptance/editor/publish-flow-test.js
146```
147
148> **Always run Ember Admin tests through Nx.** Running `ember test` or
149> `ember exam` directly from `apps/ember-admin` skips the dependency build
150> graph and commonly fails in fresh worktrees with missing outputs such as
151> `koenig-lexical.umd.js`, `@tryghost/admin-x-framework/hooks`, or
152> `@tryghost/kg-converters`. For focused runs, use Ember Exam's `--file-path`
153> as shown above rather than appending `--filter` to the package script.
154
155### Linting
156```bash
157pnpm lint # Lint all packages
158cd ghost/core && pnpm lint # Lint Ghost core (server, shared, frontend, tests)
159cd apps/ember-admin && pnpm lint # Lint Ember admin
160```
161
162### Database
163```bash
164pnpm knex-migrator migrate # Run database migrations
165pnpm reset:data # Reset database with test data (1000 members, 100 posts) (requires pnpm dev running)
166pnpm reset:data:empty # Reset database with no data (requires pnpm dev running)
167```
168
169### Docker
170```bash
171pnpm docker:build # Build Docker images
172pnpm docker:clean # Stop containers, remove volumes and local images
173pnpm docker:down # Stop containers
174```
175
176### How `pnpm dev` works
177
178The `pnpm dev` command uses a **hybrid Docker + host development** setup:
179
180**What runs in Docker:**
181- Ghost Core backend (with hot-reload via mounted source)
182- MySQL, Redis, Mailpit
183- Caddy gateway/reverse proxy
184
185**What runs on host by default:**
186- Admin, legacy Ember admin, Portal, and foundation library dev watchers
187- Optional public UMD app watchers can be added when needed
188
189**Setup:**
190```bash
191# Start Ghost backend, Admin, Portal, and Docker services
192pnpm dev
193
194# Add optional public apps (comments-ui, sodo-search, signup-form, admin-toolbar)
195pnpm dev:public
196
197# Develop the Koenig editor against Ghost Admin (adds a koenig-lexical rebuild
198# watcher + preview server; Admin loads the editor from your local build)
199pnpm dev:lexical
200
201# With optional services (uses Docker Compose file composition)
202pnpm dev:analytics # Include Tinybird analytics
203pnpm dev:storage # Include MinIO S3-compatible object storage
204pnpm dev:stripe # Include Stripe webhook forwarding
205pnpm dev:full # Include analytics, storage, Stripe, and public app watchers
206
207# Everything available
208pnpm dev:all #
209```
210
211**Accessing Services:**
212- Ghost: `http://localhost:2368` (database: `ghost_dev`)
213- Mailpit UI: `http://localhost:8025` (email testing)
214- MySQL: `localhost:3306`
215- Redis: `localhost:6379`
216- Tinybird: `http://localhost:7181` (when analytics enabled)
217- MinIO Console: `http://localhost:9001` (when storage enabled)
218- MinIO S3 API: `http://localhost:9000` (when storage enabled)
219
220## Architecture Patterns
221
222### Admin Apps Integration (Micro-Frontend)
223
224**Build Process:**
2251. Admin-x React apps build to `apps/*/dist` using Vite
2262. `apps/ember-admin/lib/asset-delivery` copies them to `ghost/core/core/built/admin/assets/*`
2273. Ghost admin serves from `/ghost/assets/{app-name}/{app-name}.js`
228
229**Runtime Loading:**
230- Ember admin uses `AdminXComponent` to dynamically import React apps
231- React components wrapped in Suspense with error boundaries
232- Apps receive config via `additionalProps()` method
233
234### Public Apps Integration
235
236- Built as UMD bundles to `apps/*/umd/*.min.js`
237- Loaded via `<script>` tags in theme templates (injected by `{{ghost_head}}`)
238- Configuration passed via data attributes
239
240### i18n Architecture
241
242**Centralized Translations:**
243- Single source: `packages/i18n/locales/{locale}/{namespace}.json`
244- Namespaces: `ghost`, `portal`, `signup-form`, `comments`, `search`
245- 60+ supported locales
246- Context descriptions: `packages/i18n/locales/context.json` — every key must have a non-empty description
247
248**Translation Workflow:**
249```bash
250pnpm --filter @tryghost/i18n translate # Extract keys from source, update all locale files + context.json
251pnpm --filter @tryghost/i18n lint:translations # Validate interpolation variables across locales
252```
253
254`translate` is run as part of `pnpm --filter @tryghost/i18n test`. In CI, it fails if translation keys or `context.json` are out of date (`failOnUpdate: process.env.CI`). Always run `pnpm --filter @tryghost/i18n translate` after adding or changing `t()` calls.
255
256**Rules for Translation Keys:**
2571. **Never split sentences across multiple `t()` calls.** Translators cannot reorder words across separate keys. Instead, use `@doist/react-interpolate` to embed React elements (links, bold, etc.) within a single translatable string.
2582. **Always provide context descriptions.** When adding a new key, add a description in `context.json` explaining where the string appears and what it does. CI will reject empty descriptions.
2593. **Use interpolation for dynamic values.** Ghost uses `{variable}` syntax: `t('Welcome back, {name}!', {name: firstname})`
2604. **Use `<tag>` syntax for inline elements.** Combined with `@doist/react-interpolate`: `t('Click <a>here</a> to retry')` with `mapping={{ a: <a href="..." /> }}`
261
262**Correct pattern (using Interpolate):**
263```jsx
264import Interpolate from '@doist/react-interpolate';
265
266<Interpolate
267 mapping={{ a: <a href={link} /> }}
268 string={t('Could not sign in. <a>Click here to retry</a>')}
269/>
270```
271
272**Incorrect pattern (split sentences):**
273```jsx
274// BAD: translators cannot reorder "Click here to retry" relative to the first sentence
275{t('Could not sign in.')} <a href={link}>{t('Click here to retry')}</a>
276```
277
278See `apps/portal/src/components/pages/email-receiving-faq.js` for a canonical example of correct `Interpolate` usage.
279
280### Build Dependencies (Nx)
281
282Critical build order (Nx handles automatically):
2831. `shade` + `admin-x-design-system` build
2842. `admin-x-framework` builds (depends on #1)
2853. Admin apps build (depend on #2)
2864. `apps/ember-admin` builds (depends on #3, copies via asset-delivery)
2875. `ghost/core` serves admin build
288
289## CSS Architecture
290
291### TailwindCSS v4 Setup
292
293Ghost Admin uses **TailwindCSS v4** via the `@tailwindcss/vite` plugin. CSS processing is centralized — only `apps/admin/vite.config.ts` loads the `@tailwindcss/vite` plugin. All embedded React apps (activitypub, admin-x-settings, admin-x-design-system) are scanned from this single entry point.
294
295### Entry Point
296
297`apps/admin/src/index.css` is the main CSS entry point. It contains:
298- `@source` directives that scan class usage in shade, activitypub, admin-x-settings, admin-x-design-system, and kg-unsplash-selector
299- `@import "@tryghost/shade/styles.css"` which loads the Shade design system styles
300
301### Shade Styles
302
303`apps/shade/styles.css` uses **unlayered** Tailwind imports:
304```css
305@import "tailwindcss/theme.css";
306@import "./preflight.css";
307@import "tailwindcss/utilities.css";
308@import "tw-animate-css";
309@import "./tailwind.theme.css";
310```
311
312**Why unlayered:** Ember's legacy CSS (`.flex`, `.hidden`, etc.) is unlayered. If Tailwind utilities were in a `@layer`, they would lose to Ember's unlayered CSS in the cascade. Keeping both unlayered means source order determines specificity.
313
314Theme tokens/variants/animations are defined in CSS (`apps/shade/tailwind.theme.css` + runtime vars in `styles.css`), so there is no JS `@config` bridge in the Admin runtime lane. `tw-animate-css` is the v4 replacement for `tailwindcss-animate`.
315
316### Critical Rule: Embedded Apps Must NOT Import Shade Independently
317
318Apps consumed via `@source` (activitypub, admin-x-settings) must **NOT** import `@tryghost/shade/styles.css` in their own CSS. Doing so causes duplicate Tailwind utilities and cascade conflicts. All Tailwind CSS is generated once via the admin entry point.
319
320### Public Apps
321
322Public-facing apps (`comments-ui`, `signup-form`, `sodo-search`, `portal`, `announcement-bar`) remain on **TailwindCSS v3**. They are built as UMD bundles for CDN distribution and are independent of the admin CSS pipeline.
323
324### Legacy Apps
325
326`admin-x-design-system` and `admin-x-settings` are consumed via `@source` in admin's centralized v4 pipeline for production, and both packages build with CSS-first Tailwind v4 setup.
327
328## Code Guidelines
329
330### Commit Messages
331When the user asks you to create a commit or draft a commit message, load and follow the `commit` skill from `.agents/skills/commit`.
332
333### ESLint Config
334Source of truth: two internal config packages — [`@internal/cfg-eslint`](configs/eslint/index.mjs) (shared rule atoms + the `nodeLibConfig` factory for Node libs) and [`@internal/cfg-eslint-react`](configs/eslint-react/index.mjs) (the `reactAppConfig` factory for every `apps/*` workspace). Both factories are synchronous and have full JSDoc with `@example`s; hover the call site in your editor. Consume them by name — declare the package as a `workspace:*` devDependency.
335
336Minimal example for a new admin React app (`apps/new-feature/eslint.config.js`):
337
338```js
339import {reactAppConfig} from '@internal/cfg-eslint-react';
340export default reactAppConfig({
341 tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`,
342 shadeRestricted: true
343});
344```
345
346Conventions:
347- **Rules are `'error'` or `'off'` — never `'warn'`.** Warnings get ignored and pollute output. Applies to every workspace covered by the factories above + the standalones; `e2e/` has its own setup (see [e2e/CLAUDE.md](e2e/CLAUDE.md)) and currently still uses warn-level Playwright rules — a separate cleanup.
348- **Params prefixed `legacy*`** (`legacyTailwindV3ConfigPath`, `legacyJsTsSplit`) are escape hatches for migrations that haven't shipped yet. Intentional and visible — PRs to remove them are scoped.
349- **Standalone configs** (`ghost/core`, `apps/ember-admin`, `apps/admin-toolbar`) exist because their rule sets genuinely don't fit a factory — read the file directly. They import shared atoms (`correctnessRules`, `nodeLibRules`, `localFilenamesPlugin`, `strictLinterOptions`) from `@internal/cfg-eslint`.
350- **Plugin deps**: a workspace must declare every eslint plugin its config resolves. Two cases:
351 - *Factory consumers* only import a factory, which supplies its plugins as objects from the config package — so they need just the config package (`@internal/cfg-eslint` / `@internal/cfg-eslint-react`) as a `workspace:*` devDependency, not the individual plugins.
352 - *Hand-rolled configs* (the standalones above, plus the inline configs in `koenig/kg-*` and `e2e/`) `import` plugins directly, so each must list those plugins in its own `devDependencies` — most commonly `eslint-plugin-ghost: catalog:`. Don't rely on the root hoisting a plugin for you; there are no eslint plugins left in the root `package.json` (only `eslint` itself and `globals`, which the root config uses).
353 - Exception: Tailwind — a workspace that uses it must list `tailwindcss` as its own (dev)Dependency regardless (the settings-based resolver requires it locally), and the legacy v3 apps pin `eslint-plugin-tailwindcss` via `catalog:tailwind3`.
354
355### When Working on Admin UI
356- **New features:** Build in React in `apps/admin` (domain folders under `src/`)
357- **Use:** `admin-x-framework` for API hooks (`useBrowse`, `useEdit`, etc.)
358- **Use:** `shade` design system for new components (not admin-x-design-system)
359- **Translations:** Add to `packages/i18n/locales/en/ghost.json`
360
361### When Working on Public UI
362- **Edit:** `apps/portal`, `apps/comments-ui`, etc.
363- **Translations:** Separate namespaces (`portal.json`, `comments.json`)
364- **Build:** UMD bundles for CDN distribution
365
366### When Working on Backend
367- **Core logic:** `ghost/core/core/server/`
368- **Database Schema:** `ghost/core/core/server/data/schema/`
369- **API routes:** `ghost/core/core/server/api/`
370- **Services:** `ghost/core/core/server/services/`
371- **Models:** `ghost/core/core/server/models/`
372- **Frontend & theme rendering:** `ghost/core/core/frontend/`
373
374### Design System Usage
375- **New components:** Use `shade` (shadcn/ui-inspired)
376- **Legacy:** `admin-x-design-system` (being phased out, avoid for new work)
377
378### Analytics (Tinybird)
379- **Local development:** `pnpm dev:analytics` (starts Tinybird + MySQL)
380- **Config:** Add Tinybird config to `ghost/core/config.development.json`
381- **Scripts:** `ghost/core/core/server/data/tinybird/scripts/`
382- **Datafiles:** `ghost/core/core/server/data/tinybird/`
383
384## Troubleshooting
385
386### Build Issues
387```bash
388pnpm fix # Clean cache + node_modules + reinstall
389pnpm build:clean # Clean build artifacts
390pnpm nx reset # Reset Nx cache
391```
392
393### Test Issues
394- **E2E failures:** Check `e2e/CLAUDE.md` for debugging tips
395- **Docker issues:** `pnpm docker:clean && pnpm docker:build`
396
TryGhost/Ghost · apps/shade/AGENTS.md
@@ +1 @@
1# Shade — Agent guide
2
3Canonical, 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.**
4
5## Core assumptions
6
7- **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 ```ts
11 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 {PostShareModal} from '@tryghost/shade/posts-stats';
16 import {cn} from '@tryghost/shade/utils';
17 import {ShadeApp} from '@tryghost/shade/app';
18 ```
19- Inside Shade itself, use the `@/` alias for cross-file imports.
20
21## The five layers
22
23| Layer | Path | Use when | Examples |
24|---|---|---|---|
25| **Tokens** | `theme-variables.css`, `tailwind.theme.css` | You need a colour, size, duration, radius | `--background`, `--text-base`, `--radius-md` |
26| **Primitives** | `src/components/primitives/` | You need layout structure | `Stack`, `Inline`, `Box`, `Grid`, `Container`, `Text` |
27| **Components** | `src/components/ui/` | You need a generic, accessible UI control | `Button`, `Input`, `Dialog`, `Tabs`, `Card`, `DropdownMenu` |
28| **Recipes** | `src/components/ui/<name>.ts` | Several components share the same visual rule (chrome, focus, density) | `inputSurface` |
29| **Patterns** | `src/components/patterns/` | The shape is product-specific and recurs across Admin | `PageHeader`, `Filters`, `KpiCard`, `GhAreaChart` |
30
31Plus two additional barrels:
32
33- **`page-templates/`** (`src/components/page-templates/`) — top-level page wrappers (`ListPage` today). Composes Patterns + Components + Primitives. Imported via `@tryghost/shade/page-templates`.
34- **`posts-stats/`** (`src/components/posts-stats/`) — transitional layer for components shared between `apps/posts` and `apps/stats` until those merge. Don't generalise it. Imported via `@tryghost/shade/posts-stats`.
35
36## Decision flow: where does new code go?
37
38When building a new UI shape, walk this top-to-bottom and stop at the first match.
39
401. **Is it just a colour, size, radius, duration?** → **Token**. Add to `theme-variables.css` (semantic) or `tailwind.theme.css` (`@theme` raw).
412. **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.
423. **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.
434. **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/`.
445. **Does it know about Ghost (KPIs, members, posts, newsletters, analytics)?** → **Pattern**.
45
46Quick gut check: **generic name → Component; Ghost-shaped name → Pattern.** `Button` is web-y; `KpiCard` is Ghost-y.
47
48## When to ADD to Shade vs keep local
49
50The default is to **keep code local first**. Premature design system additions lock in the wrong API and every consumer pays when you change it.
51
52Promote to Shade only when **all** are true:
53
541. **Reused at least twice in different surfaces.** Not "we might reuse this" — actual second use.
552. **It's generic.** A `<MembersTable>` that's just `<Table>` with three pre-set columns is not a Shade thing; it belongs in the app.
563. **The shape has settled.** Slots and composition have been stable across both local copies for at least one iteration cycle.
574. **It has a generic name.** `PageHeader`, `KpiCard`, `PostShareModal`. Not `MembersFilterBar` or `PostAnalyticsHero` — those name a single surface and will date.
585. **The API is slots, not props.** 3–6 named subcomponents (`.Title`, `.Actions`, `.Body`), not a `<ListPage title="..." onAdd={...} columns={...} />` prop bag.
596. **State stays with the consumer.** No `useQuery`, no routing, no app-context reads inside Shade.
60
61Fail any of these? Keep it local. Build it again somewhere else first, then promote.
62
63## Conventions
64
65### File names
66
67- Files: kebab-case (`dropdown-menu.tsx`) — matches ShadCN CLI output.
68- Components: PascalCase exports (`DropdownMenu`).
69- Hooks, functions, variables: camelCase.
70
71### Component file structure
72
73- One `<name>.tsx` per component (or compound family).
74- Sibling `<name>.stories.tsx` is required.
75- Use `cn()` to merge classes (`@tryghost/shade/utils` for consumers, `@/lib/utils` inside Shade).
76- Use `cva()` for variants. Forward and merge `className` so consumers can extend without wrapping.
77- For multi-region components, expose compound subcomponents (`.Title`, `.Actions`, …) — not a prop bag.
78
79### Storybook titles
80
81| Layer | Title prefix |
82|---|---|
83| Primitive | `Primitives / <Name>` |
84| Component | `Components / <Name>` |
85| Recipe | `Recipes / <Name>` |
86| Pattern | `Patterns / <Name>` |
87| Posts–Stats interim | `Posts–Stats / <Name>` |
88| Token gallery | `Tokens / <Topic>` |
89
90Use `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.
91
92### Tokens & dark mode
93
94- Use **semantic tokens** (`bg-background`, `text-foreground`, `border-border-default`, `var(--surface-elevated)`) — these flip in dark mode automatically.
95- Never hard-code hex or `hsl()` values, even temporarily.
96- Don't write `dark:` Tailwind variants for colour. The tokens do that. (Exceptions: assets like logos/illustrations.)
97- Inside stylesheets, use `var(--token)` directly. Don't wrap in `hsl()` — the variables already contain `hsl(…)`.
98- New tokens go in `apps/shade/theme-variables.css` (semantic + dark-mode overrides) or `apps/shade/tailwind.theme.css` (raw `@theme`).
99
100### Required states for components
101
102Every 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.
103
104For form controls, drive chrome through the `inputSurface` recipe — don't roll your own focus ring.
105
106## ShadCN guardrails
107
108Most new components start from a ShadCN install:
109
110```bash
111pnpm dlx shadcn@latest add <component-name>
112```
113
114- **Never overwrite an existing Shade component** when the CLI prompts. Choose "No".
115- Run on a fresh branch before installing.
116- If the component already exists, generate into a scratch repo and manually port the parts you want.
117- 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.
118- Use the `@` alias for internal imports (e.g. `@/lib/utils`).
119
120## Build, test, dev
121
122| Command | Purpose |
123|---|---|
124| `pnpm storybook` | Run Storybook locally (visual verification) |
125| `pnpm build` | Type declarations + Vite library build to `es/` |
126| `pnpm build-storybook` | Static Storybook export |
127| `pnpm test` | Type-check + Vitest with coverage |
128| `pnpm test:unit` | Unit tests only |
129| `pnpm test:types` | TS type-check only |
130| `pnpm lint` | ESLint (src + tests, `tailwindcss/*` rules enabled) |
131
132Always run `pnpm lint` before committing.
133
134## Testing expectations
135
136Formal testing strategy is TBD. Interim rules:
137
138- Vitest + Testing Library + jsdom.
139- Location: `test/unit/**/*.test.(ts|tsx|js)`.
140- Use `test/unit/utils/test-utils.tsx`'s `render` helper when a wrapper is needed.
141- For new UI components, prioritise comprehensive Storybook stories; add focused unit tests where they pay off (hooks, utils, logic-heavy parts).
142- No strict coverage threshold yet — just run `pnpm test` locally and keep it green.
143
144## Anti-patterns (don't do these)
145
146- **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.
147- **Don't import from the root `@tryghost/shade` barrel.** Use layer-specific subpaths.
148- **Don't add `dark:` variants for colour.** Use semantic tokens.
149- **Don't add product-specific props to a generic Component.** Extract a Pattern wrapper.
150- **Don't put `useQuery` or app-context reads inside a Pattern.** Patterns are layout/composition contracts. Bring-your-own state.
151- **Don't rename ShadCN-generated files** purely for casing.
152- **Don't generalise `posts-stats/`.** It's named for one specific historical situation and goes away on its own.
153- **Don't create new top-level CSS files.** Tokens live in `theme-variables.css` and `tailwind.theme.css`.
154- **Don't add migration / setup / install instructions to component docs.** Shade is admin-only and already wired up.
155
156## Commit & PR conventions
157
158Commit messages are the release notes.
159
160```
161Added Avatar component
162
163ref https://linear.app/ghost/issue/DES-1234/avatar
164
165Builds on Radix Avatar with a size variant scale and a fallback initials slot.
166```
167
168- **Line 1**: ≤ 80 chars, past tense. Starts with one of: `Fixed`, `Changed`, `Updated`, `Improved`, `Added`, `Removed`, `Reverted`, `Moved`, `Released`, `Bumped`, `Cleaned`.
169- **Line 2**: blank.
170- **Line 3**: magic word (`ref`, `closes`, `fixes`) + space + **full Linear URL**. Not `ref:` (no colon).
171- **Line 4+**: explain the **why**, not the what.
172- Dependency bumps: focus the message on user-visible changes.
173
174PRs: describe the change, link the Linear issue, include screenshots or GIFs for any UI change, update or add stories.
175
176## Acceptance checklist (component)
177
178Before marking a component done:
179
180- [ ] Lives in the right layer (re-read **Decision flow** above)
181- [ ] `className` forwarded and merged with `cn()`
182- [ ] Default, hover, focus-visible, disabled all work and are visible in the story
183- [ ] No hex values, no `bg-gray-200`-style raw palette utilities for UI chrome — semantic tokens only
184- [ ] No product-specific props on a generic Component
185- [ ] Story covers variants + states with one-line "when to use" descriptions
186- [ ] `pnpm lint`, `pnpm test`, and Storybook all clean
187
188## Repo layout
189
190```
191apps/shade/
192├── theme-variables.css Runtime semantic tokens + dark mode
193├── tailwind.theme.css Tailwind @theme raw catalogue
194├── .storybook/ Storybook config (preview.tsx controls sort order)
195└── src/
196 ├── components/
197 │ ├── primitives/ Layout primitives
198 │ ├── ui/ Generic controls + recipes
199 │ ├── patterns/ Product compositions
200 │ └── posts-stats/ Interim Posts ↔ Stats shared
201 ├── docs/ MDX + token showcase stories
202 │ ├── showcase/ Internal-only token display components
203 │ └── tokens/ Token visual stories
204 ├── hooks/ Generic React hooks
205 ├── lib/ Utilities (cn, formatters, chart helpers)
206 └── providers/ Context providers
207```
208
209Entrypoint barrels (`components.ts`, `primitives.ts`, `patterns.ts`) re-export from the matching folder.
210
211## Human docs
212
213The 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.
214
@@ −1 +1 @@
1−# AGENTS.md
1+# Shade — Agent guide
22
3−This file provides guidance to AI Agents when working with code in this repository.
3+Canonical, 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.**
44
5−## Package Manager
5+## Core assumptions
66
7−**Always use `pnpm` for all commands.** This repository uses pnpm workspaces, not npm.
7+- **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+ ```ts
11+ 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 {PostShareModal} from '@tryghost/shade/posts-stats';
16+ import {cn} from '@tryghost/shade/utils';
17+ import {ShadeApp} from '@tryghost/shade/app';
18+ ```
19+- Inside Shade itself, use the `@/` alias for cross-file imports.
820
9−Shared dependency versions are pinned in `pnpm-workspace.yaml` under `catalog:` and referenced as `"pkg": "catalog:"` (or `catalog:<name>` for named catalogs). `catalogMode` is `strict`, so `pnpm add` routes new deps into the catalog automatically — don't inline the version.
21+## The five layers
1022
11−## Monorepo Structure
23+| Layer | Path | Use when | Examples |
24+|---|---|---|---|
25+| **Tokens** | `theme-variables.css`, `tailwind.theme.css` | You need a colour, size, duration, radius | `--background`, `--text-base`, `--radius-md` |
26+| **Primitives** | `src/components/primitives/` | You need layout structure | `Stack`, `Inline`, `Box`, `Grid`, `Container`, `Text` |
27+| **Components** | `src/components/ui/` | You need a generic, accessible UI control | `Button`, `Input`, `Dialog`, `Tabs`, `Card`, `DropdownMenu` |
28+| **Recipes** | `src/components/ui/<name>.ts` | Several components share the same visual rule (chrome, focus, density) | `inputSurface` |
29+| **Patterns** | `src/components/patterns/` | The shape is product-specific and recurs across Admin | `PageHeader`, `Filters`, `KpiCard`, `GhAreaChart` |
1230
13−Ghost is a pnpm + Nx monorepo with four workspace groups:
31+Plus two additional barrels:
1432
15−### ghost/* - Core Ghost packages
16−- **ghost/core** - Main Ghost application (Node.js/Express backend)
17− - Core server: `ghost/core/core/server/`
18− - Frontend rendering: `ghost/core/core/frontend/`
33+- **`page-templates/`** (`src/components/page-templates/`) — top-level page wrappers (`ListPage` today). Composes Patterns + Components + Primitives. Imported via `@tryghost/shade/page-templates`.
34+- **`posts-stats/`** (`src/components/posts-stats/`) — transitional layer for components shared between `apps/posts` and `apps/stats` until those merge. Don't generalise it. Imported via `@tryghost/shade/posts-stats`.
1935
20−### apps/* - React-based UI applications
21−Two categories of apps:
36+## Decision flow: where does new code go?
2237
23−**Admin Apps** (embedded in Ghost Admin):
24−- `ember-admin` - Ember.js admin client (legacy, being migrated to React)
25−- `admin` - The consolidated React admin shell, organized by domain (`src/{analytics,members,posts,tags,comments,automations,...}`)
26−- `admin-x-settings`, `activitypub` - Settings and ActivityPub integration (route-composed into `admin`)
27−- Built with Vite + React + `@tanstack/react-query`
38+When building a new UI shape, walk this top-to-bottom and stop at the first match.
2839
29−**Public Apps** (served to site visitors):
30−- `portal`, `comments-ui`, `signup-form`, `sodo-search`, `announcement-bar`
31−- Built as UMD bundles, loaded via CDN in site themes
40+1. **Is it just a colour, size, radius, duration?** → **Token**. Add to `theme-variables.css` (semantic) or `tailwind.theme.css` (`@theme` raw).
41+2. **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.
42+3. **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.
43+4. **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/`.
44+5. **Does it know about Ghost (KPIs, members, posts, newsletters, analytics)?** → **Pattern**.
3245
33−**Foundation Libraries**:
34−- `admin-x-framework` - Shared API hooks, routing, utilities
35−- `admin-x-design-system` - Legacy design system (being phased out)
36−- `shade` - New design system (shadcn/ui + Radix UI + react-hook-form + zod)
46+Quick gut check: **generic name → Component; Ghost-shaped name → Pattern.** `Button` is web-y; `KpiCard` is Ghost-y.
3747
38−### koenig/* - Ghost editor (Koenig) packages
39−Merged from the former TryGhost/Koenig repo with full git history:
48+## When to ADD to Shade vs keep local
4049
41−- **koenig-lexical** - The Lexical-based rich text editor UI. Bundled into
42− Ghost Admin at build time (`apps/ember-admin` copies its UMD build into admin
43− assets; `apps/admin` imports it directly)
44−- **kg-*** - Editor support packages: server-side renderers and converters
45− consumed by `ghost/core` (kg-default-nodes, kg-lexical-html-renderer,
46− kg-html-to-lexical, ...) plus frontend helpers (kg-unsplash-selector)
50+The default is to **keep code local first**. Premature design system additions lock in the wrong API and every consumer pays when you change it.
4751
48−All Koenig packages resolve via `workspace:` — nothing in dev, CI, or the
49−release archive installs them from npm. They are published to npm for
50−external consumers only, automatically as part of the Ghost release lane
51−(see `publish_koenig_packages` in ci.yml).
52+Promote to Shade only when **all** are true:
5253
53−**Zero-build dev via the `source` export condition.** The `kg-*` libraries
54−consumed by `ghost/core` (and `packages/parse-email-address`) declare a `source`
55−condition in their `package.json` `exports` that points at the raw
56−`src/*.ts`, listed *before* `types`/`import`/`require`:
54+1. **Reused at least twice in different surfaces.** Not "we might reuse this" — actual second use.
55+2. **It's generic.** A `<MembersTable>` that's just `<Table>` with three pre-set columns is not a Shade thing; it belongs in the app.
56+3. **The shape has settled.** Slots and composition have been stable across both local copies for at least one iteration cycle.
57+4. **It has a generic name.** `PageHeader`, `KpiCard`, `PostShareModal`. Not `MembersFilterBar` or `PostAnalyticsHero` — those name a single surface and will date.
58+5. **The API is slots, not props.** 3–6 named subcomponents (`.Title`, `.Actions`, `.Body`), not a `<ListPage title="..." onAdd={...} columns={...} />` prop bag.
59+6. **State stays with the consumer.** No `useQuery`, no routing, no app-context reads inside Shade.
5760
58−```jsonc
59−".": {
60− "source": "./src/index.ts", // dev/test: read raw TS
61− "types": "./build/esm/index.d.ts",
62− "import": "./build/esm/index.js",
63− "require": "./build/cjs/index.js" // prod/published: compiled JS
64−}
65−```
61+Fail any of these? Keep it local. Build it again somewhere else first, then promote.
6662
67−`ghost/core`'s dev runner (`nodemon.json`: `node --conditions=source --import=tsx`)
68−and its Vitest configs (`resolve.conditions: ['source', 'node']` +
69−`--import tsx --conditions=source`) activate this condition, so a source change
70−in a `kg-*` package is picked up with **no `tsc` rebuild**. Production and the
71−published npm tarball run plain `node`, which ignores `source` and uses
72−`build/` — and `src/` is excluded from each package's `files` array, so it is
73−never shipped. When adding a new backend-consumed TS workspace package, copy
74−this `exports` shape (see `packages/parse-email-address`) so it works build-free
75−in dev from day one; keep the `^build` graph for `tsc`/type-checking and prod.
63+## Conventions
7664
77−### packages/* - Shared workspace libraries
78−Backend and shared libraries consumed via `workspace:` — not published to npm:
65+### File names
7966
80−- **i18n** - Centralized internationalization for all apps
81−- **parse-email-address** - Email address parsing (see the `source` export
82− condition above)
83−- **adapters/** - Adapter base classes (`adapter-base-*`: scheduling, storage,
84− SSO, redirects, route settings)
85−- **custom-field-types**, **testing** - Shared field-type definitions and test
86− helpers
87−- **_template** - Scaffold for new packages; excluded from the workspace
67+- Files: kebab-case (`dropdown-menu.tsx`) — matches ShadCN CLI output.
68+- Components: PascalCase exports (`DropdownMenu`).
69+- Hooks, functions, variables: camelCase.
8870
89−### e2e/ - End-to-end tests
90−- Playwright-based E2E tests with Docker container isolation
91−- See `e2e/CLAUDE.md` for detailed testing guidance
71+### Component file structure
9272
93−## Common Commands
73+- One `<name>.tsx` per component (or compound family).
74+- Sibling `<name>.stories.tsx` is required.
75+- Use `cn()` to merge classes (`@tryghost/shade/utils` for consumers, `@/lib/utils` inside Shade).
76+- Use `cva()` for variants. Forward and merge `className` so consumers can extend without wrapping.
77+- For multi-region components, expose compound subcomponents (`.Title`, `.Actions`, …) — not a prop bag.
9478
95−### Development
96−```bash
97−corepack enable pnpm # Enable corepack to use the correct pnpm version
98−pnpm run setup # First-time setup (installs deps + submodules + builds workspace packages)
99−pnpm dev # Start development (Docker backend + host frontend dev servers)
100−```
79+### Storybook titles
10180
102−> **Fresh worktree / first run — run `pnpm setup` before anything else.** It installs deps and syncs submodules. `pnpm fix` does a clean reinstall if anything misbehaves after a branch switch.
81+| Layer | Title prefix |
82+|---|---|
83+| Primitive | `Primitives / <Name>` |
84+| Component | `Components / <Name>` |
85+| Recipe | `Recipes / <Name>` |
86+| Pattern | `Patterns / <Name>` |
87+| Posts–Stats interim | `Posts–Stats / <Name>` |
88+| Token gallery | `Tokens / <Topic>` |
10389
104−### Building
105−```bash
106−pnpm build # Build all packages (Nx handles dependencies)
107−pnpm build:clean # Clean build artifacts and rebuild
108−```
90+Use `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.
10991
110−### Testing
111−```bash
112−# Unit tests (from root)
113−pnpm test:unit # Run all unit tests in all packages
114−pnpm test:watch # Watch mode — unified Vitest watcher (ghost/core + all apps)
92+### Tokens & dark mode
11593
116−# Ghost core tests (from ghost/core/)
117−cd ghost/core
118−pnpm test:unit # Unit tests only (Vitest, run once)
119−pnpm test:watch # Watch mode — ghost/core unit tests only
120−pnpm test:integration # Integration tests
121−pnpm test:e2e # Server-side e2e suites (webhooks/server/frontend/api) — not browser
122−pnpm test:all # All test types
94+- Use **semantic tokens** (`bg-background`, `text-foreground`, `border-border-default`, `var(--surface-elevated)`) — these flip in dark mode automatically.
95+- Never hard-code hex or `hsl()` values, even temporarily.
96+- Don't write `dark:` Tailwind variants for colour. The tokens do that. (Exceptions: assets like logos/illustrations.)
97+- Inside stylesheets, use `var(--token)` directly. Don't wrap in `hsl()` — the variables already contain `hsl(…)`.
98+- New tokens go in `apps/shade/theme-variables.css` (semantic + dark-mode overrides) or `apps/shade/tailwind.theme.css` (raw `@theme`).
12399
124−# These run on sqlite with no extra services. The Redis/MinIO/S3 adapter suites
125−# probe for their service and auto-skip when it's down (run `pnpm dev:storage`
126−# etc. to exercise them); they always run in CI, which starts the services.
100+### Required states for components
127101
128−# E2E browser tests (from root)
129−pnpm test:e2e # Run e2e/ Playwright tests
102+Every 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.
130103
131−# Running a single test
132−cd ghost/core
133−pnpm test:single test/unit/path/to/test.test.js # routes test/unit/* → unit config, test/* → DB config
104+For form controls, drive chrome through the `inputSurface` recipe — don't roll your own focus ring.
134105
135−# Watch a single DB-backed file (integration/e2e) — the default test:watch only
136−# covers unit tests, so point it at the DB config explicitly:
137−pnpm exec vitest -c vitest.config.db.ts test/integration/path/to/test.test.js
106+## ShadCN guardrails
138107
139−# Ember Admin tests (from the repository root)
140−pnpm nx run ghost-admin:test
108+Most new components start from a ShadCN install:
141109
142−# Run one Ember Admin test file. Paths are relative to apps/ember-admin.
143−# The explicit `1` supplies the numeric value required by the test script's
144−# trailing `--parallel` option before additional Ember Exam arguments.
145−pnpm nx run ghost-admin:test -- 1 --file-path=tests/acceptance/editor/publish-flow-test.js
146−```
147−
148−> **Always run Ember Admin tests through Nx.** Running `ember test` or
149−> `ember exam` directly from `apps/ember-admin` skips the dependency build
150−> graph and commonly fails in fresh worktrees with missing outputs such as
151−> `koenig-lexical.umd.js`, `@tryghost/admin-x-framework/hooks`, or
152−> `@tryghost/kg-converters`. For focused runs, use Ember Exam's `--file-path`
153−> as shown above rather than appending `--filter` to the package script.
154−
155−### Linting
156110 ```bash
157−pnpm lint # Lint all packages
158−cd ghost/core && pnpm lint # Lint Ghost core (server, shared, frontend, tests)
159−cd apps/ember-admin && pnpm lint # Lint Ember admin
111+pnpm dlx shadcn@latest add <component-name>
160112 ```
161113
162−### Database
163−```bash
164−pnpm knex-migrator migrate # Run database migrations
165−pnpm reset:data # Reset database with test data (1000 members, 100 posts) (requires pnpm dev running)
166−pnpm reset:data:empty # Reset database with no data (requires pnpm dev running)
167−```
114+- **Never overwrite an existing Shade component** when the CLI prompts. Choose "No".
115+- Run on a fresh branch before installing.
116+- If the component already exists, generate into a scratch repo and manually port the parts you want.
117+- 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.
118+- Use the `@` alias for internal imports (e.g. `@/lib/utils`).
168119
169−### Docker
170−```bash
171−pnpm docker:build # Build Docker images
172−pnpm docker:clean # Stop containers, remove volumes and local images
173−pnpm docker:down # Stop containers
174−```
120+## Build, test, dev
175121
176−### How `pnpm dev` works
122+| Command | Purpose |
123+|---|---|
124+| `pnpm storybook` | Run Storybook locally (visual verification) |
125+| `pnpm build` | Type declarations + Vite library build to `es/` |
126+| `pnpm build-storybook` | Static Storybook export |
127+| `pnpm test` | Type-check + Vitest with coverage |
128+| `pnpm test:unit` | Unit tests only |
129+| `pnpm test:types` | TS type-check only |
130+| `pnpm lint` | ESLint (src + tests, `tailwindcss/*` rules enabled) |
177131
178−The `pnpm dev` command uses a **hybrid Docker + host development** setup:
132+Always run `pnpm lint` before committing.
179133
180−**What runs in Docker:**
181−- Ghost Core backend (with hot-reload via mounted source)
182−- MySQL, Redis, Mailpit
183−- Caddy gateway/reverse proxy
134+## Testing expectations
184135
185−**What runs on host by default:**
186−- Admin, legacy Ember admin, Portal, and foundation library dev watchers
187−- Optional public UMD app watchers can be added when needed
136+Formal testing strategy is TBD. Interim rules:
188137
189−**Setup:**
190−```bash
191−# Start Ghost backend, Admin, Portal, and Docker services
192−pnpm dev
138+- Vitest + Testing Library + jsdom.
139+- Location: `test/unit/**/*.test.(ts|tsx|js)`.
140+- Use `test/unit/utils/test-utils.tsx`'s `render` helper when a wrapper is needed.
141+- For new UI components, prioritise comprehensive Storybook stories; add focused unit tests where they pay off (hooks, utils, logic-heavy parts).
142+- No strict coverage threshold yet — just run `pnpm test` locally and keep it green.
193143
194−# Add optional public apps (comments-ui, sodo-search, signup-form, admin-toolbar)
195−pnpm dev:public
144+## Anti-patterns (don't do these)
196145
197−# Develop the Koenig editor against Ghost Admin (adds a koenig-lexical rebuild
198−# watcher + preview server; Admin loads the editor from your local build)
199−pnpm dev:lexical
146+- **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.
147+- **Don't import from the root `@tryghost/shade` barrel.** Use layer-specific subpaths.
148+- **Don't add `dark:` variants for colour.** Use semantic tokens.
149+- **Don't add product-specific props to a generic Component.** Extract a Pattern wrapper.
150+- **Don't put `useQuery` or app-context reads inside a Pattern.** Patterns are layout/composition contracts. Bring-your-own state.
151+- **Don't rename ShadCN-generated files** purely for casing.
152+- **Don't generalise `posts-stats/`.** It's named for one specific historical situation and goes away on its own.
153+- **Don't create new top-level CSS files.** Tokens live in `theme-variables.css` and `tailwind.theme.css`.
154+- **Don't add migration / setup / install instructions to component docs.** Shade is admin-only and already wired up.
200155
201−# With optional services (uses Docker Compose file composition)
202−pnpm dev:analytics # Include Tinybird analytics
203−pnpm dev:storage # Include MinIO S3-compatible object storage
204−pnpm dev:stripe # Include Stripe webhook forwarding
205−pnpm dev:full # Include analytics, storage, Stripe, and public app watchers
156+## Commit & PR conventions
206157
207−# Everything available
208−pnpm dev:all #
209−```
158+Commit messages are the release notes.
210159
211−**Accessing Services:**
212−- Ghost: `http://localhost:2368` (database: `ghost_dev`)
213−- Mailpit UI: `http://localhost:8025` (email testing)
214−- MySQL: `localhost:3306`
215−- Redis: `localhost:6379`
216−- Tinybird: `http://localhost:7181` (when analytics enabled)
217−- MinIO Console: `http://localhost:9001` (when storage enabled)
218−- MinIO S3 API: `http://localhost:9000` (when storage enabled)
219−
220−## Architecture Patterns
221−
222−### Admin Apps Integration (Micro-Frontend)
223−
224−**Build Process:**
225−1. Admin-x React apps build to `apps/*/dist` using Vite
226−2. `apps/ember-admin/lib/asset-delivery` copies them to `ghost/core/core/built/admin/assets/*`
227−3. Ghost admin serves from `/ghost/assets/{app-name}/{app-name}.js`
228−
229−**Runtime Loading:**
230−- Ember admin uses `AdminXComponent` to dynamically import React apps
231−- React components wrapped in Suspense with error boundaries
232−- Apps receive config via `additionalProps()` method
233−
234−### Public Apps Integration
235−
236−- Built as UMD bundles to `apps/*/umd/*.min.js`
237−- Loaded via `<script>` tags in theme templates (injected by `{{ghost_head}}`)
238−- Configuration passed via data attributes
239−
240−### i18n Architecture
241−
242−**Centralized Translations:**
243−- Single source: `packages/i18n/locales/{locale}/{namespace}.json`
244−- Namespaces: `ghost`, `portal`, `signup-form`, `comments`, `search`
245−- 60+ supported locales
246−- Context descriptions: `packages/i18n/locales/context.json` — every key must have a non-empty description
247−
248−**Translation Workflow:**
249−```bash
250−pnpm --filter @tryghost/i18n translate # Extract keys from source, update all locale files + context.json
251−pnpm --filter @tryghost/i18n lint:translations # Validate interpolation variables across locales
252160 ```
161+Added Avatar component
253162
254−`translate` is run as part of `pnpm --filter @tryghost/i18n test`. In CI, it fails if translation keys or `context.json` are out of date (`failOnUpdate: process.env.CI`). Always run `pnpm --filter @tryghost/i18n translate` after adding or changing `t()` calls.
163+ref https://linear.app/ghost/issue/DES-1234/avatar
255164
256−**Rules for Translation Keys:**
257−1. **Never split sentences across multiple `t()` calls.** Translators cannot reorder words across separate keys. Instead, use `@doist/react-interpolate` to embed React elements (links, bold, etc.) within a single translatable string.
258−2. **Always provide context descriptions.** When adding a new key, add a description in `context.json` explaining where the string appears and what it does. CI will reject empty descriptions.
259−3. **Use interpolation for dynamic values.** Ghost uses `{variable}` syntax: `t('Welcome back, {name}!', {name: firstname})`
260−4. **Use `<tag>` syntax for inline elements.** Combined with `@doist/react-interpolate`: `t('Click <a>here</a> to retry')` with `mapping={{ a: <a href="..." /> }}`
261−
262−**Correct pattern (using Interpolate):**
263−```jsx
264−import Interpolate from '@doist/react-interpolate';
265−
266−<Interpolate
267− mapping={{ a: <a href={link} /> }}
268− string={t('Could not sign in. <a>Click here to retry</a>')}
269−/>
165+Builds on Radix Avatar with a size variant scale and a fallback initials slot.
270166 ```
271167
272−**Incorrect pattern (split sentences):**
273−```jsx
274−// BAD: translators cannot reorder "Click here to retry" relative to the first sentence
275−{t('Could not sign in.')} <a href={link}>{t('Click here to retry')}</a>
276−```
168+- **Line 1**: ≤ 80 chars, past tense. Starts with one of: `Fixed`, `Changed`, `Updated`, `Improved`, `Added`, `Removed`, `Reverted`, `Moved`, `Released`, `Bumped`, `Cleaned`.
169+- **Line 2**: blank.
170+- **Line 3**: magic word (`ref`, `closes`, `fixes`) + space + **full Linear URL**. Not `ref:` (no colon).
171+- **Line 4+**: explain the **why**, not the what.
172+- Dependency bumps: focus the message on user-visible changes.
277173
278−See `apps/portal/src/components/pages/email-receiving-faq.js` for a canonical example of correct `Interpolate` usage.
174+PRs: describe the change, link the Linear issue, include screenshots or GIFs for any UI change, update or add stories.
279175
280−### Build Dependencies (Nx)
176+## Acceptance checklist (component)
281177
282−Critical build order (Nx handles automatically):
283−1. `shade` + `admin-x-design-system` build
284−2. `admin-x-framework` builds (depends on #1)
285−3. Admin apps build (depend on #2)
286−4. `apps/ember-admin` builds (depends on #3, copies via asset-delivery)
287−5. `ghost/core` serves admin build
178+Before marking a component done:
288179
289−## CSS Architecture
180+- [ ] Lives in the right layer (re-read **Decision flow** above)
181+- [ ] `className` forwarded and merged with `cn()`
182+- [ ] Default, hover, focus-visible, disabled all work and are visible in the story
183+- [ ] No hex values, no `bg-gray-200`-style raw palette utilities for UI chrome — semantic tokens only
184+- [ ] No product-specific props on a generic Component
185+- [ ] Story covers variants + states with one-line "when to use" descriptions
186+- [ ] `pnpm lint`, `pnpm test`, and Storybook all clean
290187
291−### TailwindCSS v4 Setup
188+## Repo layout
292189
293−Ghost Admin uses **TailwindCSS v4** via the `@tailwindcss/vite` plugin. CSS processing is centralized — only `apps/admin/vite.config.ts` loads the `@tailwindcss/vite` plugin. All embedded React apps (activitypub, admin-x-settings, admin-x-design-system) are scanned from this single entry point.
294−
295−### Entry Point
296−
297−`apps/admin/src/index.css` is the main CSS entry point. It contains:
298−- `@source` directives that scan class usage in shade, activitypub, admin-x-settings, admin-x-design-system, and kg-unsplash-selector
299−- `@import "@tryghost/shade/styles.css"` which loads the Shade design system styles
300−
301−### Shade Styles
302−
303−`apps/shade/styles.css` uses **unlayered** Tailwind imports:
304−```css
305−@import "tailwindcss/theme.css";
306−@import "./preflight.css";
307−@import "tailwindcss/utilities.css";
308−@import "tw-animate-css";
309−@import "./tailwind.theme.css";
310190 ```
311−
312−**Why unlayered:** Ember's legacy CSS (`.flex`, `.hidden`, etc.) is unlayered. If Tailwind utilities were in a `@layer`, they would lose to Ember's unlayered CSS in the cascade. Keeping both unlayered means source order determines specificity.
313−
314−Theme tokens/variants/animations are defined in CSS (`apps/shade/tailwind.theme.css` + runtime vars in `styles.css`), so there is no JS `@config` bridge in the Admin runtime lane. `tw-animate-css` is the v4 replacement for `tailwindcss-animate`.
315−
316−### Critical Rule: Embedded Apps Must NOT Import Shade Independently
317−
318−Apps consumed via `@source` (activitypub, admin-x-settings) must **NOT** import `@tryghost/shade/styles.css` in their own CSS. Doing so causes duplicate Tailwind utilities and cascade conflicts. All Tailwind CSS is generated once via the admin entry point.
319−
320−### Public Apps
321−
322−Public-facing apps (`comments-ui`, `signup-form`, `sodo-search`, `portal`, `announcement-bar`) remain on **TailwindCSS v3**. They are built as UMD bundles for CDN distribution and are independent of the admin CSS pipeline.
323−
324−### Legacy Apps
325−
326−`admin-x-design-system` and `admin-x-settings` are consumed via `@source` in admin's centralized v4 pipeline for production, and both packages build with CSS-first Tailwind v4 setup.
327−
328−## Code Guidelines
329−
330−### Commit Messages
331−When the user asks you to create a commit or draft a commit message, load and follow the `commit` skill from `.agents/skills/commit`.
332−
333−### ESLint Config
334−Source of truth: two internal config packages — [`@internal/cfg-eslint`](configs/eslint/index.mjs) (shared rule atoms + the `nodeLibConfig` factory for Node libs) and [`@internal/cfg-eslint-react`](configs/eslint-react/index.mjs) (the `reactAppConfig` factory for every `apps/*` workspace). Both factories are synchronous and have full JSDoc with `@example`s; hover the call site in your editor. Consume them by name — declare the package as a `workspace:*` devDependency.
335−
336−Minimal example for a new admin React app (`apps/new-feature/eslint.config.js`):
337−
338−```js
339−import {reactAppConfig} from '@internal/cfg-eslint-react';
340−export default reactAppConfig({
341− tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`,
342− shadeRestricted: true
343−});
191+apps/shade/
192+├── theme-variables.css Runtime semantic tokens + dark mode
193+├── tailwind.theme.css Tailwind @theme raw catalogue
194+├── .storybook/ Storybook config (preview.tsx controls sort order)
195+└── src/
196+ ├── components/
197+ │ ├── primitives/ Layout primitives
198+ │ ├── ui/ Generic controls + recipes
199+ │ ├── patterns/ Product compositions
200+ │ └── posts-stats/ Interim Posts ↔ Stats shared
201+ ├── docs/ MDX + token showcase stories
202+ │ ├── showcase/ Internal-only token display components
203+ │ └── tokens/ Token visual stories
204+ ├── hooks/ Generic React hooks
205+ ├── lib/ Utilities (cn, formatters, chart helpers)
206+ └── providers/ Context providers
344207 ```
345208
346−Conventions:
347−- **Rules are `'error'` or `'off'` — never `'warn'`.** Warnings get ignored and pollute output. Applies to every workspace covered by the factories above + the standalones; `e2e/` has its own setup (see [e2e/CLAUDE.md](e2e/CLAUDE.md)) and currently still uses warn-level Playwright rules — a separate cleanup.
348−- **Params prefixed `legacy*`** (`legacyTailwindV3ConfigPath`, `legacyJsTsSplit`) are escape hatches for migrations that haven't shipped yet. Intentional and visible — PRs to remove them are scoped.
349−- **Standalone configs** (`ghost/core`, `apps/ember-admin`, `apps/admin-toolbar`) exist because their rule sets genuinely don't fit a factory — read the file directly. They import shared atoms (`correctnessRules`, `nodeLibRules`, `localFilenamesPlugin`, `strictLinterOptions`) from `@internal/cfg-eslint`.
350−- **Plugin deps**: a workspace must declare every eslint plugin its config resolves. Two cases:
351− - *Factory consumers* only import a factory, which supplies its plugins as objects from the config package — so they need just the config package (`@internal/cfg-eslint` / `@internal/cfg-eslint-react`) as a `workspace:*` devDependency, not the individual plugins.
352− - *Hand-rolled configs* (the standalones above, plus the inline configs in `koenig/kg-*` and `e2e/`) `import` plugins directly, so each must list those plugins in its own `devDependencies` — most commonly `eslint-plugin-ghost: catalog:`. Don't rely on the root hoisting a plugin for you; there are no eslint plugins left in the root `package.json` (only `eslint` itself and `globals`, which the root config uses).
353− - Exception: Tailwind — a workspace that uses it must list `tailwindcss` as its own (dev)Dependency regardless (the settings-based resolver requires it locally), and the legacy v3 apps pin `eslint-plugin-tailwindcss` via `catalog:tailwind3`.
209+Entrypoint barrels (`components.ts`, `primitives.ts`, `patterns.ts`) re-export from the matching folder.
354210
355−### When Working on Admin UI
356−- **New features:** Build in React in `apps/admin` (domain folders under `src/`)
357−- **Use:** `admin-x-framework` for API hooks (`useBrowse`, `useEdit`, etc.)
358−- **Use:** `shade` design system for new components (not admin-x-design-system)
359−- **Translations:** Add to `packages/i18n/locales/en/ghost.json`
211+## Human docs
360212
361−### When Working on Public UI
362−- **Edit:** `apps/portal`, `apps/comments-ui`, etc.
363−- **Translations:** Separate namespaces (`portal.json`, `comments.json`)
364−- **Build:** UMD bundles for CDN distribution
365−
366−### When Working on Backend
367−- **Core logic:** `ghost/core/core/server/`
368−- **Database Schema:** `ghost/core/core/server/data/schema/`
369−- **API routes:** `ghost/core/core/server/api/`
370−- **Services:** `ghost/core/core/server/services/`
371−- **Models:** `ghost/core/core/server/models/`
372−- **Frontend & theme rendering:** `ghost/core/core/frontend/`
373−
374−### Design System Usage
375−- **New components:** Use `shade` (shadcn/ui-inspired)
376−- **Legacy:** `admin-x-design-system` (being phased out, avoid for new work)
377−
378−### Analytics (Tinybird)
379−- **Local development:** `pnpm dev:analytics` (starts Tinybird + MySQL)
380−- **Config:** Add Tinybird config to `ghost/core/config.development.json`
381−- **Scripts:** `ghost/core/core/server/data/tinybird/scripts/`
382−- **Datafiles:** `ghost/core/core/server/data/tinybird/`
383−
384−## Troubleshooting
385−
386−### Build Issues
387−```bash
388−pnpm fix # Clean cache + node_modules + reinstall
389−pnpm build:clean # Clean build artifacts
390−pnpm nx reset # Reset Nx cache
391−```
392−
393−### Test Issues
394−- **E2E failures:** Check `e2e/CLAUDE.md` for debugging tips
395−- **Docker issues:** `pnpm docker:clean && pnpm docker:build`
213+The 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.
396214
