169 lines
5.9 KiB
Markdown
169 lines
5.9 KiB
Markdown
# 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 <html>
|
||
│ + conditionally loads Google Fonts <link>
|
||
▼
|
||
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
|
||
<html style={{
|
||
'--accent': theme.accent,
|
||
'--font-serif': `'${theme.fonts.heading}', Georgia, serif`,
|
||
'--font-sans': `'${theme.fonts.body}', system-ui, sans-serif`,
|
||
'--radius-sm': `${theme.radius}px`,
|
||
'--radius-md': `${theme.radius * 1.5}px`,
|
||
}}>
|
||
```
|
||
|
||
3. Dynamically build the Google Fonts `<link>` 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
|