apps/shade/src/docs/contributing.mdx
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Contributing" /> <div className="sb-doc">Use this sequence before writing code:
tokens, primitives, components, patterns).If unsure between components and patterns:
components for generic controlspatterns for product workflow composition/apps/shade/
.storybook/ # Storybook config
src/
components/
ui/ # Base ShadCN/UI primitives
layout/ # Composed, reusable layout components
features/ # Feature-specific, higher-level components
docs/ # System docs rendered in Storybook
For new code, import from a layer-specific Shade subpath instead of the root barrel:
@tryghost/shade/tokens (+ @tryghost/shade/tokens.css for CSS token consumption)@tryghost/shade/primitives@tryghost/shade/components@tryghost/shade/patterns@tryghost/shade/app@tryghost/shade/utilsUse the root @tryghost/shade entrypoint only for DS-layer compatibility (tokens/primitives/components/patterns).
Do not import utils or app symbols from root.
tokens.primitives.components.patterns.app.utils design-system-safe only (generic helpers/hooks + third-party namespaces).utils; place them in app-local code or transitional app exports.Stack, Inline, Box, Container, Grid, and Text over anonymous wrappers that only carry layout utility classes.apps/shade/src/components/layout/*.@tryghost/shade/primitives consumers compatible during migration; do not remove legacy exports during compatibility windows.none | xs | sm | md | lg | xl | 2xl) for primitive composition APIs.apps/shade/src/components/ui/* must be product-agnostic.default, hover, focus-visible, disabled.patterns wrapper instead of growing the base control API.When an AI agent proposes or changes a shared component, include:
Scope (purpose + non-goals)Public API (props, slots, variants)States (default, hover, focus-visible, disabled at minimum)Compatibility notes (what changed, what remains stable)dropdown-menu.tsx (ShadCN-generated files keep kebab-case)
dropdown-menu.tsx
dropdown-menu.stories.tsx
# optional extras below
dropdown-menu.mdx
dropdown-menu.meta.json
fixtures/dropdown-menu.fixtures.ts
Components / <Name> (e.g., Components / Button)Layout / <Name> (e.g., Layout / Page)Features / <Name> (e.g., Features / Post Share Modal)/apps/shade/src/components/ui//apps/shade/src/components/layout//apps/shade/src/components/features/Each documented component requires:
/apps/shade/src/components/ui/button.tsx # Component (source)
/apps/shade/src/components/ui/button.stories.tsx # Stories (CSF)
/apps/shade/src/components/ui/button.mdx # (optional) component docs (MDX)
# Optional metadata & fixtures
/apps/shade/src/components/ui/button.meta.json # Machine-readable metadata
/apps/shade/src/components/ui/fixtures/button.fixtures.ts
We document components through concise stories that show real use cases. Each story should have a one‑line description explaining when to use that variant/state. Prefer multiple focused stories over long prose.
Required for every component:
*.stories.tsx using CSF with:
title under the right section (Components / Layout / Features)tags: ['autodocs']parameters.docs.description.componentparameters.docs.description.storyExample:
import type {Meta, StoryObj} from '@storybook/react-vite';
import {Button} from './button';
const meta: Meta<typeof Button> = {
title: 'Components / Button',
component: Button,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component: 'Reusable button for actions. Choose variant/size by context.'
}
}
}
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Default: Story = {
args: {children: 'Continue'},
parameters: {
docs: {description: {story: 'Primary call‑to‑action with default styling.'}}
}
};
export const Destructive: Story = {
args: {variant: 'destructive', children: 'Delete'},
parameters: {
docs: {description: {story: 'Use for dangerous or irreversible actions.'}}
}
};
Install Component
npx shadcn@latest add button
Customize Implementation
⚠️ Always use the
cnutility to combine classNames andcvafor component variants. This ensures consistent class merging and variant handling across the design system.
import { cn } from "@/lib/utils"
import { cva, type VariantProps } from "class-variance-authority"
// Define variants using cva
const buttonVariants = cva(
// Base styles applied to all variants
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
{
variants: {
variant: {
solid: "bg-primary text-primary-foreground hover:bg-primary/90",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground"
},
size: {
sm: "h-9 px-3",
md: "h-10 px-4",
lg: "h-11 px-8"
}
},
defaultVariants: {
variant: "solid",
size: "md"
}
}
);
// Extract variant props type
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
// Use cn to combine variants with custom classes
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
}
Key Points:
cn to merge component variants with custom className propscva for type-safe variant combinationsVariantProps typeWhen a component has multiple meaningful regions (title, meta, actions, etc.), prefer a compound subcomponents API over many props. This keeps the API composable and flexible.
Example (Header):
function Header(props) { /* ... */ }
function HeaderTitle(props) { /* ... */ }
function HeaderMeta(props) { /* ... */ }
function HeaderActions(props) { /* ... */ }
Header.Title = HeaderTitle;
Header.Meta = HeaderMeta;
Header.Actions = HeaderActions;
export {Header};
// Usage
<Header>
<Header.Title>Members</Header.Title>
<Header.Meta>42,102 total</Header.Meta>
<Header.Actions><Button>New member</Button></Header.Actions>
</Header>
Tips:
className.File Structure
# Layout component
src/components/layout/
├── custom-component.tsx
├── custom-component.stories.tsx
└── custom-component.mdx
# Feature component
src/components/features/
├── custom-component.tsx
├── custom-component.stories.tsx
└── custom-component.mdx
Implementation Pattern
import { cn } from "@/lib/utils"
interface CustomComponentProps {
className?: string;
}
export function CustomComponent({ className }: CustomComponentProps) {
return (
<div className={cn("base-styles", className)}>
</div>
);
}
For each component:
*.stories.tsx includes: overview + key variants and states*.meta.json present and validnpx shadcn@latest add <name> prompts — choose “No”.git checkout -b chore/shadcn-add-<name>.yarn lint, yarn test, and verify in Storybook.@/lib/utils.className with cn(...) and prefer CVA for variants when useful.Refer to AGENTS.md at the repository root for additional, repo-specific guidance used by both humans and agents. It covers: