Skip to main content

Page Choreography

This document explains how each major route is assembled, what motion sequencing it uses, and where page-level orchestration versus shared component concerns live.

For the motion primitive catalog, see Motion architecture. For the component layer, see Component architecture.

Route assembly overview​

Home (/)​

Home presents identity and CV/projects links immediately, alongside a faux-VS Code IDE. The optional, nonmodal preferences panel opens from “Customize appearance & audio”, which acts as a disclosure toggle (aria-expanded/aria-controls). It dismisses after five idle seconds; hover or keyboard focus pauses the timer. Keyboard or assistive-technology activation moves focus into the panel; pointer activation leaves focus on the opener. Escape, click-away, or either dismiss button closes it; keyboard dismissal restores focus to its Customize opener, and header settings remain available. Audio starts only after explicit play. The IDE remains windowed until explicitly expanded; close, minimize, restore, drag, and resize remain available. Off/reduced motion settles the hero immediately and shows a stable terminal transcript.

Assembly​

Entrance sequence​

Page-owned state​

StatePurpose
ideWindowStatenormal / minimized / expanded
ideWindowSizewidth × height for resize
Drag constraintsBound to hero container via heroBoundsRef
Typewriter playingTriggered by HeroMotionPath completion
Scroll transformsheroScale and heroOpacity from scroll progress
Optional customizationuseHomeWelcomeSequence(); opened by the user; five-second idle timer paused during interaction

IDE window interactions​

CV (/cv)​

The CV page has two modes: a default grid layout and an immersive story viewer. Default mode reveals deferred sections on scroll, a known section hash, or the “Explore full CV” button. An explicit reveal focuses Experience; a deep link focuses its target. Resume and contact actions remain visible in About.

Default mode — assembly​

Section layout metadata​

src/pages/cvPageLayout.ts defines per-section placement and motion timing:

SectionDesktop regionDesktop delayDesktop triggerOnViewMobile order
abouttop0msfalse (immediate)0
experiencemain120mstrue1
educationmain240mstrue2
volunteeringmain360mstrue3
githubsidebar120mstrue4
certificatessidebar240mstrue5
codingmain480mstrue6

On mobile, all sections stack vertically with delayMs: 0 and most use triggerOnView: true (viewport-triggered reveal).

Within each section​

Each CVSection* component wraps a CVSectionCard which contains:

  1. SectionHeading (overline + title)
  2. Feature content (list, profile card, GitHub panel, etc.)
  3. Internal AnimatedContentList for repeated entries (with per-item stagger)
  4. Optional TabPanel / AnimatedSlideList for expandable detail

Story mode (?mode=story)​

Activated via query parameter. Replaces the grid layout with CVStoryViewer, a fullscreen scroll container that renders the ordered story chapters from buildCVStoryItems().

Climbing (/climbing)​

Assembly​

  • useFuzzySearch() provides route/location filtering across both route tables
  • initial load shows the Climbing h1, introduction, and “View climbing log” button; analytics and tables reveal on either the existing scroll threshold or the button. The button moves focus to the log, and both searches have persistent labels.
  • once unlocked, the main climbing card enters with the same spring-style card motion used by the deferred CV sections
  • Analytics panels use SectionPanel for dense data display

Photography (/photography)​

Assembly​

  • AlbumCard wraps in MotionTiltCard on desktop for 3D tilt parallax
  • StaggerChildren orchestrates sequential reveal across album cards
  • Image load state tracked to prevent layout shift during loading

Category detail (/photography/:slug)​

  • Album metadata is in normal document flow below the cover; location chips use a native details disclosure.
  • Quilted layout uses MUI ImageList with one column below 600 px, two below 900 px, and four above; desktop spans are clamped to the available columns.
  • Display media uses generated width-descriptor WebP sources; download links retain originals.
  • ImmersiveLightbox is a full-screen overlay with keyboard navigation, download, and close
  • Legacy slug redirects handled in the component for backward compatibility

Blog (/blog)​

Public route rendered as part of the standard route set.

Assembly​

  • BlogHero uses ContentCard with display-scale typography (editorial exception)
  • BlogPostList renders BlogPostCard items in a responsive grid
  • Tag filtering is page-local state driven by useBlogData().tags

Post detail (/blog/:slug)​

  • BlogArticleBody renders typed BlogContentBlock[]: text, code, image, callout, blockquote
  • BlogCodeBlock provides syntax highlighting with copy-to-clipboard
  • Not-found slugs render RouteRecoveryPanel with contextual suggestions

NotFound (/*)​

Assembly​

  • No PageFrame — uses BackgroundPaper directly for full-bleed treatment
  • StaggerChildren + MotionItem animate the text reveal
  • RouteRecoveryPanel provides contextual navigation suggestions based on the attempted path

Page-level vs component-level concerns​

ConcernBelongs at page levelBelongs in component
Section ordering and delay timing✓
delayMs for each section✓ (via layout metadata)
triggerOnView per section✓ (via layout metadata)
Tab/accordion open state✓ (feature component)
Search/filter state✓ (page-local state)
Stagger timing within a section✓ (AnimatedContentList)
Hover/tilt interactions✓ (motion primitives)
Window drag/resize (Home)✓
Story mode scroll progress + active chapter tracking✓ (CVStoryViewer)
Background image selection✓ (passed to PageFrame)
SEO metadata✓ (via useDocumentMetadata())

Choreography rules for new pages​

  1. Use PageFrame as the scaffold unless the page needs a full-bleed background (then use BackgroundPaper)
  2. Wrap sections in MotionSection or SectionCard for viewport-triggered reveals
  3. Define section delays in a layout metadata object (not inline) if the page has multiple sections with staggered timing
  4. Let shared components handle their internal animation — don't override stagger timing from the page
  5. Use SectionHeading for section intros — don't build custom title stacks
  6. Keep page-level state minimal — filters, search, expanded mode — and pass data down as props
  7. Follow the data flow: hook → page → section component → list → card

Further reading​