From abed8d0e286c55612286f2eab131435f66be0851 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Tue, 18 Aug 2026 07:48:32 +0100 Subject: [PATCH 01/13] Moved shared Shade guidance into contributor docs (#30010) Make Shade's Storybook pages the human-readable source of truth for shared contribution, testing, and review guidance. Keep `apps/shade/AGENTS.md` focused on routing, execution constraints, and recurring agent mistakes, and keep it super short. Update Shade skills and layer-specific docs so they no longer treat the agent file as a competing source of truth. --- .../skills/shade-component-decision/SKILL.md | 3 +- .../shade-dropdown-surface-contract/SKILL.md | 3 +- .agents/skills/shade-new-component/SKILL.md | 2 +- .agents/skills/shade-page-templates/SKILL.md | 3 +- .agents/skills/shade-shadcn-install/SKILL.md | 3 +- .github/workflows/ci.yml | 6 +- apps/shade/AGENTS.md | 243 +++--------------- apps/shade/README.md | 1 + apps/shade/src/docs/component-contracts.mdx | 2 +- apps/shade/src/docs/contributing.mdx | 66 +++-- apps/shade/src/docs/introduction.mdx | 2 +- apps/shade/src/docs/layers.mdx | 2 +- apps/shade/src/docs/patterns-guide.mdx | 2 +- apps/shade/src/docs/primitives-guide.mdx | 2 +- apps/shade/src/docs/recipes-guide.mdx | 2 +- scripts/test/ci-path-filters.test.js | 29 ++- 16 files changed, 120 insertions(+), 251 deletions(-) diff --git a/.agents/skills/shade-component-decision/SKILL.md b/.agents/skills/shade-component-decision/SKILL.md index 49cbeb2b172..fb5b70283d5 100644 --- a/.agents/skills/shade-component-decision/SKILL.md +++ b/.agents/skills/shade-component-decision/SKILL.md @@ -70,4 +70,5 @@ Each layer can use anything **below** it. The reverse is forbidden. ## Source of truth -Full rules: `apps/shade/AGENTS.md`. Human-facing: Storybook → Overview / Layers. +Storybook → Overview / Layers owns the layer model and promotion rules. +Overview / Contributing owns the implementation requirements. diff --git a/.agents/skills/shade-dropdown-surface-contract/SKILL.md b/.agents/skills/shade-dropdown-surface-contract/SKILL.md index d2008e1827d..5567d3fe628 100644 --- a/.agents/skills/shade-dropdown-surface-contract/SKILL.md +++ b/.agents/skills/shade-dropdown-surface-contract/SKILL.md @@ -72,4 +72,5 @@ Hover/active/selected state tokens (`--interactive-hover`, `--button-hover`, `-- ## Source of truth -`apps/shade/AGENTS.md` (Tokens & dark mode → Dropdown surface contract). Storybook → Tokens / Tokens Guide. +The `DropdownMenu`, `Select`, and `Popover` component files define the current +surface contract. Storybook → Tokens / Tokens Guide explains the token model. diff --git a/.agents/skills/shade-new-component/SKILL.md b/.agents/skills/shade-new-component/SKILL.md index 3f8d1be58bb..196e9ead511 100644 --- a/.agents/skills/shade-new-component/SKILL.md +++ b/.agents/skills/shade-new-component/SKILL.md @@ -114,4 +114,4 @@ See `shade-tokens-not-hex` and `shade-no-dark-variants`. ## Source of truth -`apps/shade/AGENTS.md`. Human docs: Storybook → Overview / Contributing. +Storybook → Overview / Contributing and the component's stories. diff --git a/.agents/skills/shade-page-templates/SKILL.md b/.agents/skills/shade-page-templates/SKILL.md index 1416a307d99..1a79a0b7559 100644 --- a/.agents/skills/shade-page-templates/SKILL.md +++ b/.agents/skills/shade-page-templates/SKILL.md @@ -94,4 +94,5 @@ If you're tempted to force a non-list shape into `ListPage`, stop and check whet ## Source of truth -`apps/shade/AGENTS.md`. Human docs: Storybook → Page Templates / Page Types. +Storybook → Page Templates / Page Types and the `ListPage` and `PageHeader` +stories. diff --git a/.agents/skills/shade-shadcn-install/SKILL.md b/.agents/skills/shade-shadcn-install/SKILL.md index 3fb440fcc48..9d5e91e8bc8 100644 --- a/.agents/skills/shade-shadcn-install/SKILL.md +++ b/.agents/skills/shade-shadcn-install/SKILL.md @@ -58,4 +58,5 @@ Raw ShadCN output is not Shade-quality yet. Do all of these: ## Source of truth -`apps/shade/AGENTS.md`, Storybook → Overview / Contributing. +Storybook → Overview / Contributing. This skill adds the agent-specific safety +steps for running the destructive ShadCN CLI. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba606ade48e..33729ee04b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,6 +155,7 @@ jobs: - 'scripts/**' docs: - '**/*.md' + - '**/*.mdx' - '.agents/**' - '.claude/**' - '.github/workflows/ci.yml' @@ -191,8 +192,9 @@ jobs: - '!koenig/*/test/**' # Documentation does not affect Ghost runtime behaviour, even # when it lives inside a project root. Keep this after every - # positive pattern so micromatch cannot add Markdown files back. + # positive pattern so micromatch cannot add docs files back. - '!**/*.md' + - '!**/*.mdx' unit-test-globals: - 'vitest.config.mjs' core-unit-test-globals: @@ -200,6 +202,7 @@ jobs: - 'ghost/core/test/utils/vitest-*.ts' any-code: - '!**/*.md' + - '!**/*.mdx' - '!.devcontainer/**' - '!.vscode/**' - *renovate_only @@ -211,6 +214,7 @@ jobs: # added here as their conventions are confirmed. e2e: - '!**/*.md' + - '!**/*.mdx' - '!.devcontainer/**' - '!.vscode/**' - '!ghost/core/test/**' diff --git a/apps/shade/AGENTS.md b/apps/shade/AGENTS.md index 1ceca43a18d..c7484346492 100644 --- a/apps/shade/AGENTS.md +++ b/apps/shade/AGENTS.md @@ -1,208 +1,35 @@ -# Shade — Agent guide - -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.** - -## Core assumptions - -- **Shade is the default source for Ghost Admin UI.** Reach for it first. If a usable primitive, component, recipe, or pattern exists, use it. -- **Shade is admin-only.** Don't generate install instructions, stylesheet imports, or `ShadeApp` setup snippets — every admin app is already wired up. -- **Imports come from layer-specific subpaths**, never the root barrel: - ```ts - import {Stack, Inline, Box, Grid, Container, Text} from '@tryghost/shade/primitives'; - import {Button, Input, Dialog} from '@tryghost/shade/components'; - import {PageHeader, KpiCard, Filters} from '@tryghost/shade/patterns'; - import {ListPage} from '@tryghost/shade/page-templates'; - import {cn} from '@tryghost/shade/utils'; - import {ShadeApp} from '@tryghost/shade/app'; - ``` -- Inside Shade itself, use the `@/` alias for cross-file imports. - -## The five layers - -| Layer | Path | Use when | Examples | -|---|---|---|---| -| **Tokens** | `theme-variables.css`, `tailwind.theme.css` | You need a colour, size, duration, radius | `--background`, `--text-base`, `--radius-md` | -| **Primitives** | `src/components/primitives/` | You need layout structure | `Stack`, `Inline`, `Box`, `Grid`, `Container`, `Text` | -| **Components** | `src/components/ui/` | You need a generic, accessible UI control | `Button`, `Input`, `Dialog`, `Tabs`, `Card`, `DropdownMenu` | -| **Recipes** | `src/components/ui/.ts` | Several components share the same visual rule (chrome, focus, density) | `inputSurface` | -| **Patterns** | `src/components/patterns/` | The shape is product-specific and recurs across Admin | `PageHeader`, `Filters`, `KpiCard`, `GhAreaChart` | - -Plus one additional barrel: - -- **`page-templates/`** (`src/components/page-templates/`) — top-level page wrappers (`ListPage` today). Composes Patterns + Components + Primitives. Imported via `@tryghost/shade/page-templates`. - -## Decision flow: where does new code go? - -When building a new UI shape, walk this top-to-bottom and stop at the first match. - -1. **Is it just a colour, size, radius, duration?** → **Token**. Add to `theme-variables.css` (semantic) or `tailwind.theme.css` (`@theme` raw). -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. -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. -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/`. -5. **Does it know about Ghost (KPIs, members, posts, newsletters, analytics)?** → **Pattern**. - -Quick gut check: **generic name → Component; Ghost-shaped name → Pattern.** `Button` is web-y; `KpiCard` is Ghost-y. - -## When to ADD to Shade vs keep local - -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. - -Promote to Shade only when **all** are true: - -1. **Reused at least twice in different surfaces.** Not "we might reuse this" — actual second use. -2. **It's generic.** A `` that's just `` with three pre-set columns is not a Shade thing; it belongs in the app. -3. **The shape has settled.** Slots and composition have been stable across both local copies for at least one iteration cycle. -4. **It has a generic name.** `PageHeader`, `KpiCard`, `Filters`. Not `MembersFilterBar` or `PostAnalyticsHero` — those name a single surface and will date. -5. **The API is slots, not props.** 3–6 named subcomponents (`.Title`, `.Actions`, `.Body`), not a `` prop bag. -6. **State stays with the consumer.** No `useQuery`, no routing, no app-context reads inside Shade. - -Fail any of these? Keep it local. Build it again somewhere else first, then promote. - -## Conventions - -### File names - -- Files: kebab-case (`dropdown-menu.tsx`) — matches ShadCN CLI output. -- Components: PascalCase exports (`DropdownMenu`). -- Hooks, functions, variables: camelCase. - -### Component file structure - -- One `.tsx` per component (or compound family). -- Sibling `.stories.tsx` is required. -- Use `cn()` to merge classes (`@tryghost/shade/utils` for consumers, `@/lib/utils` inside Shade). -- Use `cva()` for variants. Forward and merge `className` so consumers can extend without wrapping. -- For multi-region components, expose compound subcomponents (`.Title`, `.Actions`, …) — not a prop bag. - -### Storybook titles - -| Layer | Title prefix | -|---|---| -| Primitive | `Primitives / ` | -| Component | `Components / ` | -| Recipe | `Recipes / ` | -| Pattern | `Patterns / ` | -| Token gallery | `Tokens / ` | - -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. - -### Tokens & dark mode - -- Use **semantic tokens** (`bg-background`, `text-foreground`, `border-border-default`, `var(--surface-elevated)`) — these flip in dark mode automatically. -- Never hard-code hex or `hsl()` values, even temporarily. -- Don't write `dark:` Tailwind variants for colour. The tokens do that. (Exceptions: assets like logos/illustrations.) -- Inside stylesheets, use `var(--token)` directly. Don't wrap in `hsl()` — the variables already contain `hsl(…)`. -- New tokens go in `apps/shade/theme-variables.css` (semantic + dark-mode overrides) or `apps/shade/tailwind.theme.css` (raw `@theme`). - -### Required states for components - -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. - -For form controls, drive chrome through the `inputSurface` recipe — don't roll your own focus ring. - -## ShadCN guardrails - -Most new components start from a ShadCN install: - -```bash -pnpm dlx shadcn@latest add -``` - -- **Never overwrite an existing Shade component** when the CLI prompts. Choose "No". -- Run on a fresh branch before installing. -- If the component already exists, generate into a scratch repo and manually port the parts you want. -- 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/` into the story. -- Use the `@` alias for internal imports (e.g. `@/lib/utils`). - -## Build, test, dev - -| Command | Purpose | -|---|---| -| `pnpm storybook` | Run Storybook locally (visual verification) | -| `pnpm build` | Type declarations + Vite library build to `es/` | -| `pnpm build-storybook` | Static Storybook export | -| `pnpm test` | Type-check + Vitest with coverage | -| `pnpm test:unit` | Unit tests only | -| `pnpm test:types` | TS type-check only | -| `pnpm lint` | ESLint (src + tests, `tailwindcss/*` rules enabled) | - -Always run `pnpm lint` before committing. - -## Testing expectations - -Formal testing strategy is TBD. Interim rules: - -- Vitest + Testing Library + jsdom. -- Location: `test/unit/**/*.test.(ts|tsx|js)`. -- Use `test/unit/utils/test-utils.tsx`'s `render` helper when a wrapper is needed. -- For new UI components, prioritise comprehensive Storybook stories; add focused unit tests where they pay off (hooks, utils, logic-heavy parts). -- No strict coverage threshold yet — just run `pnpm test` locally and keep it green. - -## Anti-patterns (don't do these) - -- **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. -- **Don't import from the root `@tryghost/shade` barrel.** Use layer-specific subpaths. -- **Don't add `dark:` variants for colour.** Use semantic tokens. -- **Don't add product-specific props to a generic Component.** Extract a Pattern wrapper. -- **Don't put `useQuery` or app-context reads inside a Pattern.** Patterns are layout/composition contracts. Bring-your-own state. -- **Don't rename ShadCN-generated files** purely for casing. -- **Don't create new top-level CSS files.** Tokens live in `theme-variables.css` and `tailwind.theme.css`. -- **Don't add migration / setup / install instructions to component docs.** Shade is admin-only and already wired up. - -## Commit & PR conventions - -Commit messages are the release notes. - -``` -Added Avatar component - -ref https://linear.app/ghost/issue/DES-1234/avatar - -Builds on Radix Avatar with a size variant scale and a fallback initials slot. -``` - -- **Line 1**: ≤ 80 chars, past tense. Starts with one of: `Fixed`, `Changed`, `Updated`, `Improved`, `Added`, `Removed`, `Reverted`, `Moved`, `Released`, `Bumped`, `Cleaned`. -- **Line 2**: blank. -- **Line 3**: magic word (`ref`, `closes`, `fixes`) + space + **full Linear URL**. Not `ref:` (no colon). -- **Line 4+**: explain the **why**, not the what. -- Dependency bumps: focus the message on user-visible changes. - -PRs: describe the change, link the Linear issue, include screenshots or GIFs for any UI change, update or add stories. - -## Acceptance checklist (component) - -Before marking a component done: - -- [ ] Lives in the right layer (re-read **Decision flow** above) -- [ ] `className` forwarded and merged with `cn()` -- [ ] Default, hover, focus-visible, disabled all work and are visible in the story -- [ ] No hex values, no `bg-gray-200`-style raw palette utilities for UI chrome — semantic tokens only -- [ ] No product-specific props on a generic Component -- [ ] Story covers variants + states with one-line "when to use" descriptions -- [ ] `pnpm lint`, `pnpm test`, and Storybook all clean - -## Repo layout - -``` -apps/shade/ -├── theme-variables.css Runtime semantic tokens + dark mode -├── tailwind.theme.css Tailwind @theme raw catalogue -├── .storybook/ Storybook config (preview.tsx controls sort order) -└── src/ - ├── components/ - │ ├── primitives/ Layout primitives - │ ├── ui/ Generic controls + recipes - │ └── patterns/ Product compositions - ├── docs/ MDX + token showcase stories - │ ├── showcase/ Internal-only token display components - │ └── tokens/ Token visual stories - ├── hooks/ Generic React hooks - ├── lib/ Utilities (cn, formatters, chart helpers) - └── providers/ Context providers -``` - -Entrypoint barrels (`components.ts`, `primitives.ts`, `patterns.ts`) re-export from the matching folder. - -## Human docs - -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. +# Shade agent guidance + +Read the human documentation before changing Shade: + +- [`README.md`](./README.md) covers integration, development, and package tests. +- [Introduction](./src/docs/introduction.mdx) and + [Layers](./src/docs/layers.mdx) explain the design system and where code + belongs. +- [Contributing](./src/docs/contributing.mdx) defines implementation, Storybook, + testing, and review requirements. +- Review the [common anti-patterns](./src/docs/contributing.mdx#common-anti-patterns) + before making a change. +- [Component contracts](./src/docs/component-contracts.mdx), + [patterns](./src/docs/patterns-guide.mdx), + [primitives](./src/docs/primitives-guide.mdx), + [recipes](./src/docs/recipes-guide.mdx), and + [tokens](./src/docs/tokens.mdx) provide layer-specific guidance. + +## Required workflow + +- Use the repository Shade skills for the relevant task. In particular, use + `shade-component-decision` before adding a component or pattern, and follow + `shade-new-component` for the acceptance checklist. +- Import from layer-specific Shade subpaths, never the root barrel. Inside + Shade, use the `@/` alias for cross-file imports. +- Never overwrite an existing component when using the ShadCN CLI. Follow the + `shade-shadcn-install` skill. +- Do not import `@tryghost/shade/styles.css` or add another `ShadeApp` wrapper + in an embedded Admin app; Admin owns the shared CSS and application wrapper. +- Use semantic tokens rather than raw colours or colour `dark:` variants, and + use Shade primitives for layout-only wrappers. +- Run `pnpm lint` and `pnpm test`. Visually verify changed UI and stories in + Storybook. +- Update the Storybook human documentation when a shared Shade convention + changes; do not make this file the only source. diff --git a/apps/shade/README.md b/apps/shade/README.md index 2ba15c08ff3..646a4b15cb8 100644 --- a/apps/shade/README.md +++ b/apps/shade/README.md @@ -68,6 +68,7 @@ Local docs with Storybook: - `pnpm storybook` — run Storybook and view docs under `src/docs/` - `pnpm build-storybook` — build a static export +- `pnpm build` — build the package and its type declarations ## Test diff --git a/apps/shade/src/docs/component-contracts.mdx b/apps/shade/src/docs/component-contracts.mdx index 12edca3bc6a..8fcf3951a77 100644 --- a/apps/shade/src/docs/component-contracts.mdx +++ b/apps/shade/src/docs/component-contracts.mdx @@ -23,7 +23,7 @@ If a component starts collecting workflow-specific props, **stop and extract a P - The control is generic enough to live on any page (Members, Settings, Stats, the post editor). - The name sounds like the open web, not Ghost. `Button`, `Dialog`, `Tabs` — yes. `MembersList`, `PostHeader` — no, that's a Pattern. -Browse the sidebar for the live inventory and APIs. Full rules: [Layers](?path=/docs/overview-layers--docs). Agent rules: `apps/shade/AGENTS.md`. +Browse the sidebar for the live inventory and APIs. Full rules: [Layers](?path=/docs/overview-layers--docs) and [Contributing](?path=/docs/overview-contributing--docs). ## Example: good vs. bad props diff --git a/apps/shade/src/docs/contributing.mdx b/apps/shade/src/docs/contributing.mdx index 68e72b6c1a5..ec030f8db98 100644 --- a/apps/shade/src/docs/contributing.mdx +++ b/apps/shade/src/docs/contributing.mdx @@ -6,21 +6,25 @@ import { Meta } from '@storybook/addon-docs/blocks'; # Contributing -

Conventions for adding to Shade. The decision tree for *which layer* something belongs in is on the [Layers](?path=/docs/overview-layers--docs) page. The agent-facing rule source is `apps/shade/AGENTS.md`.

+

Conventions for adding to Shade. The decision tree for *which layer* something belongs in is on the [Layers](?path=/docs/overview-layers--docs) page.

## File layout ``` -apps/shade/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 +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. @@ -81,6 +85,21 @@ Guardrails: - 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) or `apps/shade/tailwind.theme.css` (raw `@theme`). Don't introduce ad-hoc CSS variables in component files. +## Common anti-patterns + +- Don't import from the root `@tryghost/shade` barrel. Use the layer-specific + subpaths shown above. +- Don't import Shade's stylesheet or add another `ShadeApp` wrapper 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](?path=/docs/overview-layers--docs) for the promotion rules. + ## Acceptance checklist Before merging a component: @@ -93,22 +112,21 @@ Before merging a component: - [ ] No product-specific props on a generic control - [ ] `pnpm lint`, `pnpm test`, and Storybook all clean -## Commits & PRs - -Commit message format (full rules in `apps/shade/AGENTS.md`): +## Testing -``` -Added Avatar component +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. -ref https://linear.app/ghost/issue/DES-1234/avatar +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. -Builds on Radix Avatar with a size variant scale and a fallback initials slot. -``` - -- 1st line: ≤ 80 chars, past tense, starts with one of: Fixed / Changed / Updated / Improved / Added / Removed / Reverted / Moved / Released / Bumped / Cleaned. -- 3rd line: magic word (`ref`, `closes`, `fixes`) + full Linear URL. -- 4th+: the **why**, not the what. +## Commits & PRs -PRs: screenshots or GIFs for any UI change; link the Linear issue; update or add stories. +Follow the repository's [contribution workflow](https://github.com/TryGhost/Ghost/blob/main/docs/contributing/workflow.md) +for commits and pull requests. For Shade UI changes, also include screenshots +or a GIF and update or add the relevant stories. diff --git a/apps/shade/src/docs/introduction.mdx b/apps/shade/src/docs/introduction.mdx index d2cc8eb8425..adfb8aa7467 100644 --- a/apps/shade/src/docs/introduction.mdx +++ b/apps/shade/src/docs/introduction.mdx @@ -29,6 +29,6 @@ import {Stack} from '@tryghost/shade/primitives'; If a control exists, use it. If it doesn't and you'll reuse it, [Layers](?path=/docs/overview-layers--docs) tells you which folder to add it to. -For AI-assisted work, see `apps/shade/AGENTS.md` — the canonical rules for where new code goes. +See **[Contributing](?path=/docs/overview-contributing--docs)** for the implementation and review checklist. diff --git a/apps/shade/src/docs/layers.mdx b/apps/shade/src/docs/layers.mdx index 020366c3aa3..4008221719f 100644 --- a/apps/shade/src/docs/layers.mdx +++ b/apps/shade/src/docs/layers.mdx @@ -50,6 +50,6 @@ When in doubt: **keep it local**, build it again somewhere else, then promote. - **Browse the galleries.** The sidebar lists every primitive, component, recipe, and pattern with live examples. - **For implementation rules** (where new files go, naming, ShadCN guardrails, commit format): see [Contributing](?path=/docs/overview-contributing--docs). -- **For agent-facing rules**: see `apps/shade/AGENTS.md` — the canonical decision flow for AI-assisted work. +- **For implementation and review rules**: see [Contributing](?path=/docs/overview-contributing--docs). diff --git a/apps/shade/src/docs/patterns-guide.mdx b/apps/shade/src/docs/patterns-guide.mdx index 1a502e6287d..981edbdf036 100644 --- a/apps/shade/src/docs/patterns-guide.mdx +++ b/apps/shade/src/docs/patterns-guide.mdx @@ -25,7 +25,7 @@ import { Meta } from '@storybook/addon-docs/blocks'; If you find `useQuery` inside a pattern, the boundary is wrong — bring-your-own state. -Promotion rules and the full decision flow: [Layers](?path=/docs/overview-layers--docs). Agent rules: `apps/shade/AGENTS.md`. +Promotion rules and the full decision flow: [Layers](?path=/docs/overview-layers--docs). ## Example diff --git a/apps/shade/src/docs/primitives-guide.mdx b/apps/shade/src/docs/primitives-guide.mdx index 0fbcebf1cf9..c76a1d12605 100644 --- a/apps/shade/src/docs/primitives-guide.mdx +++ b/apps/shade/src/docs/primitives-guide.mdx @@ -25,7 +25,7 @@ Use semantic gaps (`gap="md"`), not raw numbers (`gap-4`). Map: see [Tokens / Sp - Put product logic inside a primitive. If a wrapper is starting to know about Ghost data, it's a Pattern. - Wrap content in a bare `
` carrying only `flex / grid / gap` utilities — that's exactly what primitives replace. -Full rules and decision flow: [Layers](?path=/docs/overview-layers--docs). Agent rules: `apps/shade/AGENTS.md`. +Full rules and decision flow: [Layers](?path=/docs/overview-layers--docs). ## Example diff --git a/apps/shade/src/docs/recipes-guide.mdx b/apps/shade/src/docs/recipes-guide.mdx index a4263e23d0b..93acb0d120a 100644 --- a/apps/shade/src/docs/recipes-guide.mdx +++ b/apps/shade/src/docs/recipes-guide.mdx @@ -40,7 +40,7 @@ export function recipe(mode: 'a' | 'b' = 'a') { Function for the easy path, atoms (the `as const` object) for unusual cases where consumers need to compose a subset. Tokens everywhere underneath. -Full rules: [Layers](?path=/docs/overview-layers--docs). Agent rules: `apps/shade/AGENTS.md`. +Full rules: [Layers](?path=/docs/overview-layers--docs). ## Example diff --git a/scripts/test/ci-path-filters.test.js b/scripts/test/ci-path-filters.test.js index 9e9f846b0e3..a156db32f57 100644 --- a/scripts/test/ci-path-filters.test.js +++ b/scripts/test/ci-path-filters.test.js @@ -12,17 +12,32 @@ async function getFilterPatterns(filterName) { } describe('CI path filters', () => { - it('excludes Markdown after all positive core patterns', async () => { + it('excludes Markdown and MDX after all positive core patterns', async () => { const patterns = await getFilterPatterns('core'); - const markdownExclusion = patterns.indexOf('!**/*.md'); const positivePatterns = patterns .map((pattern, index) => ({pattern, index})) .filter(({pattern}) => !pattern.startsWith('!')); - assert.notStrictEqual(markdownExclusion, -1); - assert.ok( - positivePatterns.every(({index}) => index < markdownExclusion), - 'Markdown exclusions must follow positive patterns so later patterns cannot add docs back' - ); + for (const exclusion of ['!**/*.md', '!**/*.mdx']) { + const exclusionIndex = patterns.indexOf(exclusion); + assert.notStrictEqual(exclusionIndex, -1); + assert.ok( + positivePatterns.every(({index}) => index < exclusionIndex), + `${exclusion} must follow positive patterns so later patterns cannot add docs back` + ); + } + }); + + it('treats Markdown and MDX as documentation rather than code', async () => { + const docs = await getFilterPatterns('docs'); + const anyCode = await getFilterPatterns('any-code'); + const e2e = await getFilterPatterns('e2e'); + + assert.ok(docs.includes('**/*.md')); + assert.ok(docs.includes('**/*.mdx')); + assert.ok(anyCode.includes('!**/*.md')); + assert.ok(anyCode.includes('!**/*.mdx')); + assert.ok(e2e.includes('!**/*.md')); + assert.ok(e2e.includes('!**/*.mdx')); }); }); From 9b1b61d662fbabe9b84bbd1bb76458fb8f73055b Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Tue, 18 Aug 2026 08:58:31 +0200 Subject: [PATCH 02/13] =?UTF-8?q?=F0=9F=8E=A8=20Refined=20the=20React=20ta?= =?UTF-8?q?g=20details=20screen=20(#29934)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changed - moved View posts and Delete tag into a tag actions menu - added the INTERNAL badge for internal tags - replaced the native color input with Shade’s color picker - refined advanced-section typography and tag image actions - introduced a responsive 2:1 resource-editor layout - reorganized the core tag fields for the wider left card - made advanced settings a one-at-a-time sidebar accordion with Meta data open by default and vertical form/preview composition ## Why The React tag details screen needed clearer action hierarchy, more consistent controls, and a layout that separates everyday tag data from advanced metadata while remaining responsive. ## Validation - tag detail acceptance tests: 28/28 passed - Admin typecheck passed - focused ESLint passed - full Admin acceptance suite: 441/443 passed; the two unrelated settings failures both passed when rerun independently (22/22) - visually checked the responsive 2:1 layout and every advanced-settings panel on the local Admin screen ref https://linear.app/ghost/issue/PLA-342/design-refinements --- .../admin/src/tags/detail/tag-color-field.tsx | 101 ++-- .../admin/src/tags/detail/tag-detail-form.tsx | 433 +++++++++--------- .../src/tags/detail/tag-detail-previews.tsx | 20 +- .../detail/tag-detail.acceptance.test.tsx | 113 ++++- apps/admin/src/tags/detail/tag-detail.tsx | 44 +- .../admin/src/tags/detail/tag-image-field.tsx | 15 +- .../pages/admin/tags/tag-editor-page.ts | 10 + 7 files changed, 432 insertions(+), 304 deletions(-) diff --git a/apps/admin/src/tags/detail/tag-color-field.tsx b/apps/admin/src/tags/detail/tag-color-field.tsx index 975ce45be42..849e314aeeb 100644 --- a/apps/admin/src/tags/detail/tag-color-field.tsx +++ b/apps/admin/src/tags/detail/tag-color-field.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {InputGroup, InputGroupAddon, InputGroupInput, InputGroupText, Label} from '@tryghost/shade/components'; +import {InputGroup, InputGroupAddon, InputGroupInput, InputGroupText, Label, Popover, PopoverContent, PopoverTrigger} from '@tryghost/shade/components'; +import {ColorPicker, ColorPickerTrigger} from '@tryghost/shade/patterns'; interface TagColorFieldProps { value: string; @@ -13,7 +14,7 @@ const HEX_COLOR_REGEX = /#[0-9A-Fa-f]{6}$/; /** * The tag accent colour control, arranged like Ember's `.input-color`: one - * bordered control with the swatch (the native colour-picker trigger) on the + * bordered control with the colour-picker trigger on the * left, a static `#` prefix, and the hex text input. Ports `tag-form.js` * `updateAccentColor` — immediate normalization keeps the form draft in sync * before keyboard saves, with the same error copy for a malformed hex value. @@ -21,6 +22,7 @@ const HEX_COLOR_REGEX = /#[0-9A-Fa-f]{6}$/; const TagColorField: React.FC = ({value, disabled, errorId, onChange, onError}) => { const [text, setText] = React.useState(value.replace(/^#/, '')); const lastValueRef = React.useRef(value); + const allowPickerChanges = React.useRef(false); // Adopt external changes (initial load, a background refetch) without // clobbering in-progress typing on unrelated re-renders. @@ -60,50 +62,57 @@ const TagColorField: React.FC = ({value, disabled, errorId, }; return ( -
- - - -
e.stopPropagation()} - > - { - setText(e.target.value.replace(/^#/, '')); - applyColor(e.target.value); - }} - /> -
- # -
- applyColor(e.target.value)} - onChange={(e) => { - setText(e.target.value); - applyColor(e.target.value); - }} - /> -
-
+ allowPickerChanges.current = false}> +
+ + + + + + + # + + applyColor(e.target.value)} + onChange={(e) => { + setText(e.target.value); + applyColor(e.target.value); + }} + /> + +
+ +
allowPickerChanges.current = true} + onKeyDownCapture={() => allowPickerChanges.current = true} + onPointerDownCapture={() => allowPickerChanges.current = true} + > + { + if (allowPickerChanges.current) { + setText(color.replace(/^#/, '')); + applyColor(color); + } + }} + /> +
+
+
); }; diff --git a/apps/admin/src/tags/detail/tag-detail-form.tsx b/apps/admin/src/tags/detail/tag-detail-form.tsx index 4e33c215ebf..7410a7d2052 100644 --- a/apps/admin/src/tags/detail/tag-detail-form.tsx +++ b/apps/admin/src/tags/detail/tag-detail-form.tsx @@ -1,8 +1,8 @@ import React from 'react'; import TagColorField from './tag-color-field'; import TagImageField from './tag-image-field'; -import {Accordion, AccordionContent, AccordionItem, AccordionTrigger, Card, CardContent, CodeEditor, FieldError, Input, Label, Textarea} from '@tryghost/shade/components'; -import {Stack} from '@tryghost/shade/primitives'; +import {Accordion, AccordionContent, AccordionItem, AccordionTrigger, Card, CardContent, CodeEditor, FieldError, Input, Label, Tabs, TabsContent, TabsList, TabsTrigger, Textarea} from '@tryghost/shade/components'; +import {Grid, Inline, Stack} from '@tryghost/shade/primitives'; import {DESCRIPTION_MAX_LENGTH, FACEBOOK_DESCRIPTION_RECOMMENDED_LENGTH, FACEBOOK_TITLE_RECOMMENDED_LENGTH, META_DESCRIPTION_RECOMMENDED_LENGTH, META_TITLE_RECOMMENDED_LENGTH, X_DESCRIPTION_RECOMMENDED_LENGTH, X_TITLE_RECOMMENDED_LENGTH, charLength, getBlogDomain, getSeoDescription, getSeoTitle, getSeoUrl, getSlugUrlPreview, validateTagField} from './tag-detail-edit'; import {FacebookCardPreview, SeoPreview, XCardPreview} from './tag-detail-previews'; import {cn, formatNumber} from '@tryghost/shade/utils'; @@ -28,21 +28,12 @@ const UsedCharacters: React.FC<{value: string; limit: number; prefix: 'Maximum' const used = charLength(value); return (

- {prefix}: {formatNumber(limit)} characters. You’ve used{' '} + {prefix}: {formatNumber(limit)} characters. You’ve used{' '} limit ? 'text-destructive' : 'text-state-success')}>{formatNumber(used)}

); }; -const SectionTrigger: React.FC<{title: string; description: string}> = ({title, description}) => ( - - - {title} - {description} - - -); - const TagDetailForm: React.FC = ({draft, errors, blogUrl, disabled, onChange, onFieldError, onImageBusyChange, onImageUploadPendingChange}) => { const {data: settingsData} = useBrowseSettings({}); const siteTitle = getSettingValue(settingsData?.settings ?? [], 'title') ?? ''; @@ -61,15 +52,15 @@ const TagDetailForm: React.FC = ({draft, errors, blogUrl, di const blogDomain = getBlogDomain(blogUrl); return ( -
- {/* Card 1 mirrors Ember's main form block; card 2 groups the - collapsible sections — the member detail screen's card idiom. */} - + + {/* The main form and advanced settings collapse into one column + below the medium breakpoint. */} + -
-
-
-
+ + + + = ({draft, errors, blogUrl, di onBlur={() => validateOnBlur('name')} onChange={e => onChange({name: e.target.value})} /> -
+ = ({draft, errors, blogUrl, di onChange={accentColor => onChange({accentColor})} onError={message => onFieldError('accentColor', message)} /> -
-
+ + {errors.name} {errors.accentColor}

Start with # to create internal tags.{' '} Learn more

-
- -
- - validateOnBlur('slug')} - onChange={e => onChange({slug: e.target.value})} - /> -

{getSlugUrlPreview(draft.slug, blogUrl)}

- {errors.slug} -
- -
- -