Back to Ghost

System Architecture

apps/shade/src/docs/architecture.mdx

6.36.08.0 KB
Original Source

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

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

System Architecture

<p className="excerpt">Shade is built as a layered system, with each layer building upon the previous to create a complete design system. This document outlines how these layers work together.</p>

Read This First (60 seconds)

If you are not sure where code belongs, use this:

  • tokens: visual values only (color, type, radius, motion)
  • primitives: layout structure only (Stack, Inline, Box, Container, Grid, Text)
  • components: reusable generic controls (button, input, dialog)
  • patterns: repeated product workflows (filters, resource flows)
  • app: app shell and transitional domain exports
  • utils: design-system-safe generic helpers only

Fast decision rule:

  • If it changes spacing/alignment/layout, use primitives.
  • If it is a generic control, use components.
  • If it encodes product workflow, use patterns.

File Structure

/apps/shade/
├── src/
│   ├── components/
│   │   ├── ui/           # Base ShadCN components
│   │   ├── layout/       # Composed, reusable layout components
│   │   └── features/     # Feature-specific, higher-level components
│   ├── hooks/            # Custom React hooks
│   ├── providers/        # Context providers
│   ├── lib/ds-utils.ts   # DS-safe utilities
│   ├── lib/app-utils.ts  # Transitional domain utilities
│   └── docs/             # Documentation
├── .storybook/          # Storybook configuration
├── styles.css           # Tailwind imports + runtime variables
└── tailwind.theme.css   # CSS-first token definitions (@theme)

Export Entrypoints

Shade now uses layered package entrypoints:

  • @tryghost/shade/tokens and @tryghost/shade/tokens.css
  • @tryghost/shade/primitives
  • @tryghost/shade/components
  • @tryghost/shade/patterns
  • @tryghost/shade/app
  • @tryghost/shade/utils

The root @tryghost/shade entrypoint remains a compatibility lane during migration, but new code should import from a layer-specific subpath. The root entrypoint now keeps compatibility for DS layers only (tokens, primitives, components, patterns). app and utils are intentionally subpath-only.

Layer Ownership Rules

  • tokens: token definitions and helpers only (CSS variables, token names, token lookup helpers).
  • primitives: structure/layout building blocks with no product/domain behavior.
  • components: reusable UI controls and visual assets/icons.
  • patterns: feature-level reusable compositions and interaction rules.
  • app: app shell/provider/context APIs and transitional domain exports.
  • utils: DS-safe helpers, generic hooks, and third-party namespaces.

Human and AI Decision Protocol

Run this order before implementing UI:

  1. Choose the layer (tokens -> primitives -> components -> patterns).
  2. Reuse existing API first.
  3. If no fit, keep code local and add it to Shade only after repeated use.
  4. Keep shared APIs product-agnostic.

Stop and re-evaluate when:

  • a shared component needs workflow-specific props
  • a layout abstraction starts carrying business logic
  • ownership between components and patterns is unclear

When to add code to Shade:

  • Keep code local first, then add it to Shade when it is reused across surfaces.
  • Add to primitives when the abstraction is structural.
  • Add to components when the abstraction is a reusable control.
  • Add to patterns when repeated product workflows emerge.
  • Do not place domain/product helpers in utils; route them through app during migration and then toward app-local/pattern ownership.

Primitive Composition Rules and Guarantees

Primitive composition has these fixed rules:

  • Primitive set: Stack, Inline, Box, Container, Grid, Text.
  • Spacing API: semantic steps none | xs | sm | md | lg | xl | 2xl.
  • Text depth: minimal Text primitive with compatibility heading wrappers (H1-H4, HTable).
  • Migration scope: apps/shade/src/components/layout/* only.
  • Compatibility policy: keep existing @tryghost/shade/primitives exports available during migration and deprecate without hard removals.

Component Rules and Guarantees Layer

This layer keeps shared control APIs explicit and stable in components/ui:

  • Define explicit API and state rules for Wave A controls (Button, Input, Field, Select, Tabs, Table, Dialog, DropdownMenu, Card).
  • Keep shared controls product-agnostic.
  • Preserve compatibility through deprecation notes, not hard removals.

Component Types

Base Components (UI)

Located in /src/components/ui/:

  • Built on ShadCN/UI primitives
  • Use RadixUI for accessibility
  • Follow composable pattern
  • Examples: Button, Input, Card

Layout Components (Composed)

Located in /src/components/layout/:

  • Compose base UI into reusable layout primitives (Page, Header, Heading, ViewHeader)
  • Shared across surfaces; no product logic
  • Follow same patterns as base components

Feature Components

Located in /src/components/features/:

  • Higher-level, feature-specific compositions (e.g., PostShareModal, SourceTabs)
  • May bundle multiple UI and layout parts for a specific workflow
  • Still reusable across apps when the feature exists

Dependencies

Core Dependencies

  • React: UI library
  • TailwindCSS: Utility-first styling
  • ShadCN/UI: Component primitives
  • RadixUI: Accessible primitives
  • Lucide: Default icon system

⚠️ Note: When using third-party libraries that might have naming conflicts (like Recharts), we alias all exports (e.g., export * as Recharts from "recharts").

Component Implementation

Naming Conventions

  • Filenames: ShadCN-generated files keep kebab-case; file-scoped components live alongside their stories
  • Components: PascalCase
  • Functions/vars/types: camelCase

⚠️ Note: ShadCN/UI CLI creates files in kebab-case. We accept this inconsistency due to case-sensitivity issues.

Component Structure

tsx
// Basic component structure
import { cn } from "@/lib/utils"
import { cva } from "class-variance-authority"

const componentVariants = cva(
    "base-styles",
    {
        variants: {
            variant: {
                default: "variant-styles",
                // Add variants...
            }
        },
        defaultVariants: {
            variant: "default"
        }
    }
)

export interface ComponentProps {
    className?: string;
    // Other props...
}

export function Component({ className, ...props }: ComponentProps) {
    return (
        <div className={cn(componentVariants(), className)} {...props} />
    )
}

Component API

  • Use RadixUI primitives for accessibility
  • Expose RadixUI props when available
  • Always include className prop
  • Use Class Variance Authority for variants

Icons

Shade defaults to using Lucide icons. Since all exports are aliased as LucideIcon in index.ts, the way to use icons in apps happens through referencing the LucideIcon package:

tsx
import { LucideIcon } from 'lucide-react';

<LucideIcon.Search size={16} />

ShadCN/UI Configuration

Key configuration in components.json:

json
{
    "style": "new-york",
    "baseColor": "gray",
    "cssVariables": true
}

Best Practices

Component Design

  • Create composable, not configurable components
  • Keep components focused and single-purpose
  • Maintain consistent prop patterns
  • Document all variants and props

Styling

  • Use design tokens via Tailwind classes sourced from CSS-first @theme tokens
  • Always merge className prop using cn utility
  • Maintain dark mode support
  • Follow responsive design patterns

Implementation Flow

  1. Start with ShadCN/UI

    • Install via CLI if available
    • Customize implementation
    • Add/modify variants
  2. Add Documentation

    • Create stories
    • Document variants
    • Show usage examples
  3. Export Component

    ts
    // index.ts
    export * from './components/ui/component';
    
</div>