Back to Ghost

Contributing

apps/shade/src/docs/contributing.mdx

6.58.05.3 KB
Original Source

import { Meta } from '@storybook/addon-docs/blocks';

<Meta title="Overview / Contributing" /> <div className="sb-doc">

Contributing

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

File layout

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.

Naming

WhatConventionExample
File nameskebab-casedropdown-menu.tsx
Component identifiersPascalCaseDropdownMenu
Hooks, functions, variablescamelCaseuseFocusTrap, formatNumber
Storybook titleslayer prefixComponents / Button, Patterns / PageHeader, Recipes / inputSurface

Always forward and merge className with cn(...). Use cva() for variants.

Imports

Always use the layer-specific subpath:

ts
import {Button} from '@tryghost/shade/components';
import {Stack} from '@tryghost/shade/primitives';
import {PageHeader} from '@tryghost/shade/patterns';
import {cn} from '@tryghost/shade/utils';

Inside Shade itself, use the @/ alias for cross-file imports.

Stories

Every component ships with <name>.stories.tsx next to it:

  • title follows the layer convention (table above).
  • tags: ['autodocs'] so docs render.
  • A short parameters.docs.description.component.
  • One story per important variant/state, each with a one-line parameters.docs.description.story explaining when to use it.

Stories are the gallery. Many small focused stories beat one prose-heavy story.

ShadCN

Most new components start from a ShadCN install:

bash
pnpm dlx shadcn@latest add <component-name>

Guardrails:

  • Never overwrite an existing Shade component when the CLI prompts. Choose "No".
  • Run on a fresh branch so the CLI's diff is clean.
  • If the component already exists, generate into a scratch repo and port the parts you actually want.
  • After integrating: replace any raw colour/spacing with semantic tokens; ensure default / hover / focus-visible / disabled all work; trim props that hint at a specific surface.

Tokens & dark mode

  • Reference semantic tokens (bg-background, text-foreground, border-border-default, --surface-elevated). Never hard-code hex values.
  • Don't write dark: variants for colour — semantic tokens flip automatically. Exceptions: assets like logos and illustrations.
  • New tokens go in apps/shade/theme-variables.css (semantic) 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 for the promotion rules.

Acceptance checklist

Before merging a component:

  • Lives in the right layer (see Layers)
  • className forwarded and merged with cn()
  • All states work: default, hover, focus-visible, disabled
  • Semantic tokens only; no hex values, no bg-gray-200-style raw utilities for UI chrome
  • Story covers variants + states with one-line "when to use" descriptions
  • No product-specific props on a generic control
  • pnpm lint, pnpm test, and Storybook all clean

Testing

Shade uses Vitest, Testing Library, and jsdom. Unit tests live under test/unit/; use test/unit/utils/test-utils.tsx when a test needs the shared render wrapper.

For a new UI component, make the Storybook stories cover its important variants and states. Add focused unit tests when they provide useful coverage for hooks, utilities, or logic-heavy behaviour. Shade does not currently enforce a package-specific coverage threshold.

Commits & PRs

Follow the repository's contribution workflow for commits and pull requests. For Shade UI changes, also include screenshots or a GIF and update or add the relevant stories.

</div>