Shared layout

Docs Surfaces

Shared layout

Chrome that every page inherits: the layout shell, theme switch, command palette, and footer.

Scope

This document covers shared cross-page UI behavior:

  • layout shell
  • navbar and header actions
  • page-template adaptation
  • shared footer

Base Layout

Main file: src/layouts/Layout.astro

Responsibilities:

  • owns the HTML shell for most routes
  • sets canonical, OG, and Twitter metadata
  • exposes optional RSS and oEmbed discovery links
  • mounts the shared section navbar by default
  • mounts the shared theme dropdown
  • avoids mounting third-party analytics scripts

Theme behavior:

  • runs before paint with an inline script
  • reads localStorage.theme
  • falls back to prefers-color-scheme
  • applies html.dark
  • stores the current selection in html[data-theme-setting]

Implementation lives in src/layouts/Layout.astro.

Important design choice:

  • the navbar is section-anchor based, not route-aware

Behavior:

  • default links target:
    • #projects-section
    • #writing-section
    • #moods-section
  • nav labels are rewritten into per-character spans
  • scrolling updates the active section
  • smooth scrolling is handled in client code
  • IntersectionObserver switches the nav between horizontal and vertical modes based on hero visibility
  • the active indicator is animated only in vertical mode

Header actions:

  • Layout.astro owns the theme dropdown
  • individual pages can inject extra buttons into [data-header-actions]
  • Layout.astro exposes a small registration surface for GSAP header-button animation

Page Template Adaptation

Main file: src/layouts/Page.astro

Purpose:

  • reuse the same base layout for document-style pages such as /privacy

How it adapts the shared nav:

  • adds body.page-template-active
  • keeps only the first nav item
  • renames that item to buxx.me
  • rewires it to /
  • removes the active indicator
  • removes extra links and separators

This keeps the global chrome but changes the navigation contract from section scrolling to home navigation.

File: src/features/home/ui/Footer.astro

Behavior:

  • static footer
  • exposes /privacy
  • exposes the GitHub source repository

The privacy page is therefore linked from:

  • the global footer
  • the mood notify panel

Implementation Summary

  • shared UI concerns are centralized in Layout.astro
  • content pages reuse the same shell and mutate the nav through Page.astro
  • the shared layout is optimized for the home page first, then adapted for document-style pages

Motion Vocabulary

Declared at :root in src/styles/globals.css. One curve family, one duration scale, site-wide:

Token Value
--ease cubic-bezier(0.2, 0, 0, 1)
--ease-out cubic-bezier(0.23, 1, 0.32, 1)
--ease-in-out cubic-bezier(0.77, 0, 0.175, 1)
--dur-press 110ms
--dur-fast 130ms
--dur-base 190ms
--dur-enter 240ms

The scale was adopted from the portal, which was the only part of the site that had one; src/styles/portal.css now aliases its --portal-* names to these so its existing rules keep reading their own vocabulary.

Rule: new motion uses a token. A literal curve needs a comment saying why it is not one.

--expo-out is deliberately outside this scale — it is a linear() easing for the 1.5s theme wipe, a different register from UI motion. src/styles/home-reveal.css likewise owns its own --reveal-ease by design.

Adoption follow-ups

The adoption pass replaced literals only where the value matched a token exactly and the site was enumerated in plan 022. Left for a later pass:

Exact matches, mechanically safe — these can become tokens with no change in rendering:

  • src/features/components/ui/OnThisPage.astro:72--ease-out
  • src/pages/privacy.astro:334--ease-out
  • src/components/CommandPalette.astro:1554-1556--ease
  • src/features/admin/ui/AnalyticsCharts.tsx:158--ease (inline style, React island)

src/styles/code-box.css:11 and src/styles/listening.css:655 already read var(--ease-out, …) with a literal fallback; that form is deliberate for stylesheets that may mount outside their owning subtree.

Near-misses needing a judgement call — each is an “ease-out with a long tail” that is not --ease-out. Collapsing them blind would change how things feel, so each site needs its own decision (is this meant to be the standard ease-out, or is the curve deliberate?):

Curve Uses Notable homes
cubic-bezier(0.16, 1, 0.3, 1) 31 globals, TimelineWheel, 404, view transitions
cubic-bezier(0.2, 0.8, 0.2, 1) 12
cubic-bezier(0.4, 0, 0.2, 1) 9 Material’s standard curve
cubic-bezier(0.32, 0.72, 0, 1) 8 ProjectStack entrance
cubic-bezier(0.22, 1, 0.36, 1) 8 (+2 unspaced) SiteWordmark, hero cards, GitHubContributions
cubic-bezier(0.25, 1, 0.3, 1) 6 blog.css, SiteWordmark
cubic-bezier(0.45, 0, 0.2, 1) 4
cubic-bezier(0.2, 0.7, 0.2, 1) 3 (+2 unspaced variant)

Overshoot curves (0.25, 1.22, 0.45, 1.04, 0.25, 1.18, 0.45, 1.04, 0.34, 1.56, 0.64, 1, 0.22, 1.2, 0.4, 1) are character, not drift — they are not candidates for the token set.

Also worth a pass: the same curve is spelled both with and without spaces (cubic-bezier(0.22,1,0.36,1) vs cubic-bezier(0.22, 1, 0.36, 1)), which defeats grep-based auditing.