# COMPONENT_ARCHITECTURE.md ## 1. Principles 1. **Server components by default** (Astro components, zero client JS). Client behaviour only via the five behaviour modules bound by `data-*` attributes. 2. **Components receive typed props from content + locale; they never fetch or read files.** Layouts do the data plumbing. 3. **One component, both themes, both locales** — no `HeroNo.astro`, no `CardDark.astro`. Locale comes in as a prop bundle; theme is pure CSS. 4. Naming and states follow the Phase 1 component inventory (DESIGN_SYSTEM §5) 1:1 — design system and component tree share vocabulary. ## 2. Component tree ``` layouts/ Base html/head/meta, Seo, ThemeScript(inline), fonts preload, SkipLink, Header, , Footer, behaviour-module script tags (deferred) Page Base + page-title block + prose container (about/contact/cv/colophon) Home Base + section scaffolding with reveal data-attributes CaseStudy Base + case-study scaffold (header block, MiniToc, section renderer) components/core/ Header nav links (slug-map-driven), LangSwitch, ThemeToggle, SplitCvButton; states: default/scrolled via data-scrolled (observer module) MobileNav overlay panel (template in DOM, hidden; nav.ts toggles, traps focus) Footer channels, CV links, language repeat, colophon link Seo per-page meta, hreflang pairs (slug map), OG, JSON-LD builders ThemeScript inline no-flash theme init (the one permitted inline script, CSP-hashed) SkipLink first focusable element components/ui/ (design-system atoms — dumb, prop-driven) Chip(kind: fact|status|stack, status?) Button(variant: primary|secondary|ghost) SplitCvButton(locale, cvMeta) LangSwitch(currentPageId, locale) ThemeToggle Card(interactive?) SectionLabel(number, text) TraceMotif(animated?) FramedImage(media, loading) Tooltip(delay 600ms) components/home/ Hero(profile) ProofStrip(4 tiles from profile/skills data) SkillsGrid(skillGroups) ProjectCards(featured projects, homelab slim card) ExperienceTimeline(entries) AboutTeaser(profile) ContactBand(profile) components/case-study/ CsHeader(project) TldrBox(project.tldr) MiniToc(sections, sticky ≥1200) SectionRenderer(section) — markdown → prose DecisionList(decisions) ArchDiagram(diagram, locale labels) Gallery(media) + Lightbox host NextPrev(project order) ``` ## 3. Behaviour modules (the entire client JS surface) | Module | Binds to | Responsibility | Budget | |---|---|---|---| | `theme.ts` | ThemeToggle + inline init | read/persist preference, `data-theme` swap, icon morph class | ~1 kB | | `nav.ts` | Header/MobileNav | overlay open/close, focus trap, scroll lock, Esc; header `data-scrolled` flag | ~2 kB | | `observer.ts` | `[data-reveal]`, `[data-spy]` | shared IntersectionObserver: reveal-once classes, scroll-spy for nav/MiniToc, timeline draw trigger | ~2 kB | | `lightbox.ts` | Gallery | open/close/navigate, focus trap, key handling, swipe | ~3 kB | | `form.ts` | Contact form | client validation, honeypot timing, POST to relay, A9 success/error panels, copy-to-clipboard buttons | ~3 kB | Rules: modules are independent (no shared state beyond `localStorage` keys), fail-silent (feature simply absent if errored), loaded `defer`, total ≤ 15 kB gzip — well under the 40 kB budget with margin for the language-hint logic (~1 kB, lives in `nav.ts`). ## 4. Contracts between design tokens and components - Components use Tailwind utilities mapped to tokens; **no raw hex/px in component code** (lint-guarded where feasible). - State styling via data-attributes (`data-scrolled`, `data-open`, `data-revealed`, `data-status="active"`) so behaviour modules never touch inline styles — CSS owns all appearance. - Motion classes come exclusively from `motion.css` (token durations + reduced-motion layer) — a component never declares its own transition values. ## 5. Reuse & variant policy - Case-study template is one component set; JobTrack vs InboxIntel differ only in content. Homelab's `capability` template reuses CsHeader/SectionRenderer, omits TldrBox/MiniToc/DecisionList. - The 404, hint bar, and colophon reuse existing atoms only — no bespoke components for one-offs unless an atom can't express it (guard against component sprawl in a one-person codebase).