# Theming System — Feature Plan Make the app themeable so photographers with different aesthetic preferences can fully customize the look without touching component code. ## Current State The CSS architecture is already 80% of the way there: - **Design tokens** in `tokens.css` — colors, fonts, radii, transitions, spacing - **Dark/light toggle** via `[data-theme]` attribute - All components reference tokens exclusively (`var(--text-primary)`, `var(--font-serif)`, etc.) **What's NOT themeable yet:** - The "Studio/Leica" aesthetic is hardcoded into `globals.css` (lines 607–783) — red dot, film grain, passepartout borders, uppercase titles, sharp corners - Header glass effect colors are hardcoded (`rgba(26, 26, 26, 0.9)`) - Font imports are hardcoded in `tokens.css` - The OG image generator has hardcoded colors --- ## Proposed Architecture ### Theme as a YAML config Add a `theme` section to `gallery.yaml`. Photographers configure their visual identity alongside their gallery structure — no CSS editing required. ```yaml # gallery.yaml theme: editorial # built-in preset (or "studio", "minimal", "classic") # OR fully custom: theme: preset: minimal # start from a base accent: "#2563eb" # brand color fonts: heading: "Inter" # Google Fonts name body: "Inter" caption: "JetBrains Mono" radius: 8 # border-radius in px (0 = sharp) photoFrame: none # "none" | "passepartout" | "shadow" grain: false # film grain overlay headerDot: false # red dot in nav heroStyle: split # "split" | "fullbleed" | "minimal" ``` ### How it works ``` gallery.yaml (theme config) │ ▼ getConfig() ← reads theme key │ ▼ RootLayout ← injects CSS custom properties as inline style on │ + conditionally loads Google Fonts ▼ tokens.css ← contains fallback defaults only │ ▼ globals.css ← component styles use var() tokens conditional classes for optional features ``` ### Implementation Layers #### Layer 1: Extract theme tokens from config **Files:** `lib/config.ts`, `app/layout.tsx` 1. Add `ThemeConfig` type to `config.ts`: ```ts interface ThemeConfig { accent: string; fonts: { heading: string; body: string; caption: string }; radius: number; photoFrame: 'none' | 'passepartout' | 'shadow'; grain: boolean; headerDot: boolean; heroStyle: 'split' | 'fullbleed' | 'minimal'; } ``` 2. In `layout.tsx`, inject theme values as CSS custom properties: ```tsx ``` 3. Dynamically build the Google Fonts `` URL from the font names. #### Layer 2: Make component CSS conditional **Files:** `globals.css`, `tokens.css` 1. Move the "Studio-Inspired Defaults" block (lines 607–783) into a separate file `themes/studio.css` 2. Create `themes/minimal.css`, `themes/classic.css`, `themes/editorial.css` with different aesthetic overrides 3. The base `globals.css` becomes theme-agnostic — clean, neutral component styles 4. Conditional features controlled by data attributes: ```css /* Only show if theme.grain is true */ [data-grain="true"] .photo-grid__item::after { /* grain overlay */ } /* Only show if theme.headerDot is true */ [data-header-dot="true"] .header__nav::before { /* red dot */ } /* Passepartout frame */ [data-photo-frame="passepartout"] .photo-grid__item { padding: 8px; border: 1px solid ... } /* Shadow frame */ [data-photo-frame="shadow"] .photo-grid__item { box-shadow: 0 2px 20px rgba(0,0,0,0.3); } ``` #### Layer 3: Built-in presets Ship 4 presets that photographers can pick with a single word: | Preset | Aesthetic | Accent | Fonts | Radius | Frame | Grain | |--------|-----------|--------|-------|--------|-------|-------| | `studio` | Current Leica look | `#e60012` | Playfair+DM Sans | 0 | passepartout | ✓ | | `minimal` | Clean, no distractions | `#666` | Inter | 0 | none | ✗ | | `editorial` | Magazine feel | `#1a1a1a` | Libre Baskerville+Source Sans | 4 | shadow | ✗ | | `classic` | Warm, inviting | `#8b6914` | Cormorant+Lato | 8 | none | ✗ | Presets are just default values — every property can be overridden individually. #### Layer 4: OG image theming Update `/api/og/route.tsx` to read accent color and fonts from config, replacing the hardcoded `#0a0a0a` / `#d4af37`. --- ## File Changes Summary | File | Change | |------|--------| | `lib/config.ts` | Add `ThemeConfig` type + parsing from YAML | | `app/layout.tsx` | Inject CSS vars inline, dynamic Google Fonts link | | `app/tokens.css` | Tokens become fallback defaults only | | `app/globals.css` | Remove hardcoded "Studio" overrides, use data attributes | | `themes/studio.css` | [NEW] Extracted studio/Leica aesthetic | | `themes/minimal.css` | [NEW] Clean minimal preset | | `themes/editorial.css` | [NEW] Magazine preset | | `themes/classic.css` | [NEW] Warm classic preset | | `app/api/og/route.tsx` | Read accent color from config | | `content/gallery.yaml.example` | Add `theme:` section | | `README.md` | Document theme customization | --- ## Migration Path **Zero breaking changes.** If no `theme` key exists in `gallery.yaml`, the app defaults to `studio` (current look). Existing deployments see no visual change. --- ## Not In Scope (Future) - **Live theme preview** — a `/theme-editor` page where you tweak and see changes in real time - **Custom CSS injection** — a `customCss` field in YAML for power users to inject arbitrary CSS - **Per-album themes** — different themes for different subpages - **Background texture/pattern** options beyond grain