chore: consolidate dev-docs and plans into docs/
This commit is contained in:
168
docs/dev/theming-system.md
Normal file
168
docs/dev/theming-system.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user