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:
| Primitive | Purpose | Consumers |
|---|---|---|
PageFrame | Route-level scaffold with background image and responsive container | Blog, BlogPost, CV, Climbing, Photography, PhotographyCategory |
BackgroundPaper | Full-bleed scenic backdrop with optional shell overlay | Home, NotFound, also used internally by PageFrame |
SectionCard | Viewport-triggered reveal card for content sections | Blog, Climbing, Photography, PhotographyCategory |
CVSectionCard | Extended section card with CV-specific surface treatment | All CV section wrappers |
SectionPanel | Flat inset panel for nested dense data | ClimbingAnalytics, CVGitHubSection, AnimatedContentList panel mode |
SectionHeading | Overline + title + subtitle composition | Blog, Photography, Climbing, CV sections |
ContentCard | Base frosted card surface (no animation) | BlogHero, BlogPostCard, GitHub sections |
AnimatedContentCard | Viewport-triggered fade-in card with optional tilt | CV repeated-card lists (via AnimatedContentList) |
AnimatedContentList | Viewport-triggered card-list wrapper with tilt and surface options | 5 CV list renderers |
AnimatedSlideList | Primary controlled repeated-item animation primitive | CV tab panels and other controlled reveal lists |
SkillsChipList | Skill/tool chip wrap list | CV about, experience, education, coding, story sections |
TabPanel | Collapsible tab panel wrapper | CV experience, education, coding, volunteering |
Text | Canonical semantic text primitive (role × tone × context) | Site-wide |
TypewriterText | Animated semantic text composition | CV 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:
| Subsystem | Components | Why it differs |
|---|---|---|
| Home faux-VS Code hero | TerminalHeroContent + 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 mode | CVStoryViewer, CVStorySectionRenderer, CVStoryProgress | Full-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
| Component | Trigger | Animation | Use case |
|---|---|---|---|
AnimatedContentList | Viewport intersection per card | Zoom-in with optional tilt | Specialized CV card lists |
AnimatedSlideList | Boolean in prop | Primary controlled stagger reveal | Tab/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
- Check the Design system reference first — the pattern you need may already exist
- Place it in
src/components/(top-level) orsrc/components/layout/(layout primitive) - Accept controlled props; avoid internal state unless it's presentation-only (hover, focus)
- Use existing motion primitives from
src/motion/components.tsxfor animation — do not create parallel wrappers - Use
Textroles fromsrc/components/text/— do not style rawTypographyfor standard roles - Style via the existing style builder system or inline
sxfrom theme tokens
Adding a new feature component
- Place it in
src/components/{feature}/alongside related components - Compose shared primitives (cards, lists, headings) as the rendering surface
- Keep orchestration logic in the consuming page, not in the feature component
- If the component needs motion, wrap with an existing motion primitive — do not define new variants inline
Adding a new page
- Create the page in
src/pages/ - Add route metadata to
src/constants/siteRoutes.ts - Add the
<Route>inApp.tsx - Use
PageFrameas the scaffold unless the page needs full-bleed treatment (then useBackgroundPaper) - Compose sections from shared layout primitives (
SectionCard,SectionHeading) - Wire data through a hook in
src/hooks/ - Follow existing motion patterns — viewport-triggered reveals with stagger timing
Further reading
- Design system reference — concrete catalog of surfaces, text roles, and selection guide
- Motion architecture — how animation primitives compose with components
- Page choreography — how pages assemble and sequence their sections
- Agent guide — operational rules for safe component extension