Skip to main content

Component Architecture

This document explains how the component layer is organized: what is shared, what is feature-specific, how composition works, and where new components should go.

For the concrete catalog of existing surfaces, text primitives, and selection guidance, see the Design system reference.

Component layering

Shared primitives vs feature components

Shared primitives (src/components/ + src/components/layout/ + src/components/text/)

These are reusable across multiple routes. They define the site's shared visual language:

PrimitivePurposeConsumers
PageFrameRoute-level scaffold with background image and responsive containerBlog, BlogPost, CV, Climbing, Photography, PhotographyCategory
BackgroundPaperFull-bleed scenic backdrop with optional shell overlayHome, NotFound, also used internally by PageFrame
SectionCardViewport-triggered reveal card for content sectionsBlog, Climbing, Photography, PhotographyCategory
CVSectionCardExtended section card with CV-specific surface treatmentAll CV section wrappers
SectionPanelFlat inset panel for nested dense dataClimbingAnalytics, CVGitHubSection, AnimatedContentList panel mode
SectionHeadingOverline + title + subtitle compositionBlog, Photography, Climbing, CV sections
ContentCardBase frosted card surface (no animation)BlogHero, BlogPostCard, GitHub sections
AnimatedContentCardViewport-triggered fade-in card with optional tiltCV repeated-card lists (via AnimatedContentList)
AnimatedContentListViewport-triggered card-list wrapper with tilt and surface options5 CV list renderers
AnimatedSlideListPrimary controlled repeated-item animation primitiveCV tab panels and other controlled reveal lists
SkillsChipListSkill/tool chip wrap listCV about, experience, education, coding, story sections
TabPanelCollapsible tab panel wrapperCV experience, education, coding, volunteering
TextCanonical semantic text primitive (role × tone × context)Site-wide
TypewriterTextAnimated semantic text compositionCV about, home/IDE-adjacent text reveals

Feature components (src/components/{feature}/)

These belong to a specific route or feature area. They compose shared primitives but add domain-specific logic:

Intentional design-system exceptions

Two subsystems intentionally bypass the shared design-system primitives. These are not drift — they are purpose-built alternative design languages:

SubsystemComponentsWhy it differs
Home faux-VS Code heroTerminalHeroContent + src/components/ide/*Simulates a desktop IDE chrome; uses its own token file (vscodeTokens.ts), custom window controls, and terminal typewriter — none of this belongs in the shared card/section system
CV story modeCVStoryViewer, CVStorySectionRenderer, CVStoryProgressFull-screen continuous-scroll narrative with per-section reveals, scroll progress tracking, and deterministic active-section labels; bounded through UnsafeTypography

Blog uses the shared Text component with prose roles (proseParagraph, proseHeading, etc.) and is not a design-system exception. Photography uses Text with tone="inverse" and context="overlay" and is not a design-system exception.

Rule: Do not attempt to "normalize" the IDE hero or CV story mode into the shared primitive system. They exist for a reason.

Composition patterns

Pages compose; components render

Pages are declarative assemblers. They:

  • Import data via hooks
  • Arrange sections using layout primitives
  • Pass data down as props
  • Own orchestration timing (section delays, stagger offsets)

Components are reusable renderers. They:

  • Accept data via props
  • Handle their own presentation and interaction state (tabs, accordions, hover)
  • Do not fetch data or own routing concerns
  • Do not contain page-level orchestration logic

Card composition chain

The most common composition pattern for content sections:

Animated list variants

ComponentTriggerAnimationUse case
AnimatedContentListViewport intersection per cardZoom-in with optional tiltSpecialized CV card lists
AnimatedSlideListBoolean in propPrimary controlled stagger revealTab/accordion expanded content

Hook → page → component data flow

Component ownership boundaries

What shared components should NOT do

  • Fetch data or call hooks that fetch data
  • Know about specific routes or route parameters
  • Own page-level orchestration timing
  • Contain page-specific business logic
  • Hardcode content strings

What feature components CAN do

  • Compose shared primitives with domain-specific props
  • Own feature-specific interaction state (tab selection, drawer toggle)
  • Define feature-specific rendering logic (e.g., how a CV entry's detail panel works)
  • Import from their own data hooks

What pages should do

  • Wire up data hooks
  • Declare section ordering and delay timing
  • Compose feature sections into the page scaffold
  • Own page-specific state (filters, search, expanded mode)

Safe extension points

Adding a new shared component

  1. Check the Design system reference first — the pattern you need may already exist
  2. Place it in src/components/ (top-level) or src/components/layout/ (layout primitive)
  3. Accept controlled props; avoid internal state unless it's presentation-only (hover, focus)
  4. Use existing motion primitives from src/motion/components.tsx for animation — do not create parallel wrappers
  5. Use Text roles from src/components/text/ — do not style raw Typography for standard roles
  6. Style via the existing style builder system or inline sx from theme tokens

Adding a new feature component

  1. Place it in src/components/{feature}/ alongside related components
  2. Compose shared primitives (cards, lists, headings) as the rendering surface
  3. Keep orchestration logic in the consuming page, not in the feature component
  4. If the component needs motion, wrap with an existing motion primitive — do not define new variants inline

Adding a new page

  1. Create the page in src/pages/
  2. Add route metadata to src/constants/siteRoutes.ts
  3. Add the <Route> in App.tsx
  4. Use PageFrame as the scaffold unless the page needs full-bleed treatment (then use BackgroundPaper)
  5. Compose sections from shared layout primitives (SectionCard, SectionHeading)
  6. Wire data through a hook in src/hooks/
  7. Follow existing motion patterns — viewport-triggered reveals with stagger timing

Further reading