2026-02-14 00:59:57 +01:00
|
|
|
/**
|
2026-02-14 10:30:27 +01:00
|
|
|
* Typed configuration loader.
|
|
|
|
|
* Secrets come from env vars; gallery structure comes from content/gallery.yaml.
|
2026-02-14 00:59:57 +01:00
|
|
|
*/
|
|
|
|
|
|
2026-02-14 10:30:27 +01:00
|
|
|
import fs from 'fs';
|
|
|
|
|
import path from 'path';
|
|
|
|
|
import yaml from 'js-yaml';
|
2026-02-14 11:47:26 +01:00
|
|
|
import { env } from './env';
|
2026-02-14 10:30:27 +01:00
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
// ── Env helpers (now handled by lib/env.ts via Zod) ───────────
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-02-14 10:30:27 +01:00
|
|
|
// ── Types ──────────────────────────────────────────────────────
|
|
|
|
|
|
2026-02-14 00:59:57 +01:00
|
|
|
/** A subpage groups multiple Immich albums under one URL path. */
|
|
|
|
|
export interface SubpageConfig {
|
|
|
|
|
name: string;
|
|
|
|
|
slug: string;
|
|
|
|
|
albumIds: string[];
|
2026-02-14 10:30:27 +01:00
|
|
|
/** Optional password — if set, visitors must authenticate to view. */
|
|
|
|
|
password?: string;
|
|
|
|
|
/** Per-subpage grid overrides (merged with global grid config). */
|
|
|
|
|
grid?: Partial<GridConfig>;
|
2026-02-14 00:59:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
/** Footer configuration for social links. */
|
|
|
|
|
export interface FooterConfig {
|
|
|
|
|
name?: string;
|
|
|
|
|
instagram?: string;
|
|
|
|
|
email?: string;
|
|
|
|
|
website?: string;
|
|
|
|
|
}
|
|
|
|
|
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
/** Theme configuration — controls the visual identity of the gallery. */
|
|
|
|
|
export interface ThemeConfig {
|
|
|
|
|
preset: string;
|
|
|
|
|
accent: string;
|
|
|
|
|
fonts: { heading: string; body: string; caption: string };
|
|
|
|
|
radius: number;
|
|
|
|
|
photoFrame: 'none' | 'passepartout' | 'shadow';
|
|
|
|
|
grain: boolean;
|
|
|
|
|
headerDot: boolean;
|
feat: add noir, monograph, botanica presets + stacked/typographic/mosaic hero styles + showcase/filmstrip/editorial-flow gallery layouts
- 3 new theme presets: noir (darkroom analog), monograph (typographic book), botanica (organic nature)
- 3 new hero styles: stacked (fullbleed + thumbnail strip), typographic (text-only), mosaic (multi-image grid)
- 3 new gallery layouts: showcase (featured + grid), filmstrip (horizontal scroll), editorial-flow (alternating pairs)
- Extended config types, validation, and CSS tokens for all new options
- Updated theming docs with new presets table, descriptions, and example configs
2026-02-15 21:58:31 +01:00
|
|
|
heroStyle: 'split' | 'fullbleed' | 'minimal' | 'stacked' | 'typographic' | 'mosaic';
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Theme Presets ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const THEME_PRESETS: Record<string, ThemeConfig> = {
|
|
|
|
|
studio: {
|
|
|
|
|
preset: 'studio',
|
|
|
|
|
accent: '#e60012',
|
|
|
|
|
fonts: { heading: 'Playfair Display', body: 'DM Sans', caption: 'EB Garamond' },
|
|
|
|
|
radius: 0,
|
|
|
|
|
photoFrame: 'passepartout',
|
|
|
|
|
grain: true,
|
|
|
|
|
headerDot: true,
|
|
|
|
|
heroStyle: 'split',
|
|
|
|
|
},
|
|
|
|
|
minimal: {
|
|
|
|
|
preset: 'minimal',
|
feat: restyle themes to be visually distinct
- Minimal: Swiss Brutalist — fullbleed hero, true B&W, zero-gap grid, Geist font
- Editorial: Cinematic Magazine — warm charcoal, Bodoni Moda serif, slow transitions
- Classic: Gilded Gallery — gold accents, Cinzel headings, passepartout, ornamental dividers
Updated config presets, per-theme color palettes, ~550 lines of scoped CSS overrides,
data-preset attribute on html element, responsive breakpoints, screenshots, and docs.
2026-02-15 17:17:25 +01:00
|
|
|
accent: '#000000',
|
|
|
|
|
fonts: { heading: 'Geist', body: 'Geist', caption: 'IBM Plex Mono' },
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
radius: 0,
|
|
|
|
|
photoFrame: 'none',
|
|
|
|
|
grain: false,
|
|
|
|
|
headerDot: false,
|
feat: restyle themes to be visually distinct
- Minimal: Swiss Brutalist — fullbleed hero, true B&W, zero-gap grid, Geist font
- Editorial: Cinematic Magazine — warm charcoal, Bodoni Moda serif, slow transitions
- Classic: Gilded Gallery — gold accents, Cinzel headings, passepartout, ornamental dividers
Updated config presets, per-theme color palettes, ~550 lines of scoped CSS overrides,
data-preset attribute on html element, responsive breakpoints, screenshots, and docs.
2026-02-15 17:17:25 +01:00
|
|
|
heroStyle: 'fullbleed',
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
},
|
|
|
|
|
editorial: {
|
|
|
|
|
preset: 'editorial',
|
feat: restyle themes to be visually distinct
- Minimal: Swiss Brutalist — fullbleed hero, true B&W, zero-gap grid, Geist font
- Editorial: Cinematic Magazine — warm charcoal, Bodoni Moda serif, slow transitions
- Classic: Gilded Gallery — gold accents, Cinzel headings, passepartout, ornamental dividers
Updated config presets, per-theme color palettes, ~550 lines of scoped CSS overrides,
data-preset attribute on html element, responsive breakpoints, screenshots, and docs.
2026-02-15 17:17:25 +01:00
|
|
|
accent: '#8B2500',
|
|
|
|
|
fonts: { heading: 'Bodoni Moda', body: 'Newsreader', caption: 'Spectral' },
|
|
|
|
|
radius: 0,
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
photoFrame: 'shadow',
|
|
|
|
|
grain: false,
|
|
|
|
|
headerDot: false,
|
|
|
|
|
heroStyle: 'split',
|
|
|
|
|
},
|
|
|
|
|
classic: {
|
|
|
|
|
preset: 'classic',
|
feat: restyle themes to be visually distinct
- Minimal: Swiss Brutalist — fullbleed hero, true B&W, zero-gap grid, Geist font
- Editorial: Cinematic Magazine — warm charcoal, Bodoni Moda serif, slow transitions
- Classic: Gilded Gallery — gold accents, Cinzel headings, passepartout, ornamental dividers
Updated config presets, per-theme color palettes, ~550 lines of scoped CSS overrides,
data-preset attribute on html element, responsive breakpoints, screenshots, and docs.
2026-02-15 17:17:25 +01:00
|
|
|
accent: '#c49a3c',
|
|
|
|
|
fonts: { heading: 'Cinzel', body: 'Crimson Pro', caption: 'Crimson Pro' },
|
|
|
|
|
radius: 12,
|
|
|
|
|
photoFrame: 'passepartout',
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
grain: false,
|
feat: restyle themes to be visually distinct
- Minimal: Swiss Brutalist — fullbleed hero, true B&W, zero-gap grid, Geist font
- Editorial: Cinematic Magazine — warm charcoal, Bodoni Moda serif, slow transitions
- Classic: Gilded Gallery — gold accents, Cinzel headings, passepartout, ornamental dividers
Updated config presets, per-theme color palettes, ~550 lines of scoped CSS overrides,
data-preset attribute on html element, responsive breakpoints, screenshots, and docs.
2026-02-15 17:17:25 +01:00
|
|
|
headerDot: true,
|
|
|
|
|
heroStyle: 'minimal',
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
},
|
feat: add noir, monograph, botanica presets + stacked/typographic/mosaic hero styles + showcase/filmstrip/editorial-flow gallery layouts
- 3 new theme presets: noir (darkroom analog), monograph (typographic book), botanica (organic nature)
- 3 new hero styles: stacked (fullbleed + thumbnail strip), typographic (text-only), mosaic (multi-image grid)
- 3 new gallery layouts: showcase (featured + grid), filmstrip (horizontal scroll), editorial-flow (alternating pairs)
- Extended config types, validation, and CSS tokens for all new options
- Updated theming docs with new presets table, descriptions, and example configs
2026-02-15 21:58:31 +01:00
|
|
|
noir: {
|
|
|
|
|
preset: 'noir',
|
|
|
|
|
accent: '#ff6b35',
|
|
|
|
|
fonts: { heading: 'Libre Baskerville', body: 'Source Sans 3', caption: 'Space Mono' },
|
|
|
|
|
radius: 0,
|
|
|
|
|
photoFrame: 'passepartout',
|
|
|
|
|
grain: true,
|
|
|
|
|
headerDot: false,
|
|
|
|
|
heroStyle: 'fullbleed',
|
|
|
|
|
},
|
|
|
|
|
monograph: {
|
|
|
|
|
preset: 'monograph',
|
|
|
|
|
accent: '#333333',
|
|
|
|
|
fonts: { heading: 'Instrument Serif', body: 'Inter', caption: 'IBM Plex Mono' },
|
|
|
|
|
radius: 0,
|
|
|
|
|
photoFrame: 'none',
|
|
|
|
|
grain: false,
|
|
|
|
|
headerDot: false,
|
|
|
|
|
heroStyle: 'typographic',
|
|
|
|
|
},
|
|
|
|
|
botanica: {
|
|
|
|
|
preset: 'botanica',
|
|
|
|
|
accent: '#4a7c59',
|
|
|
|
|
fonts: { heading: 'Cormorant Garamond', body: 'Nunito Sans', caption: 'Inconsolata' },
|
|
|
|
|
radius: 8,
|
|
|
|
|
photoFrame: 'none',
|
|
|
|
|
grain: false,
|
|
|
|
|
headerDot: true,
|
|
|
|
|
heroStyle: 'split',
|
|
|
|
|
},
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
};
|
|
|
|
|
|
2026-02-14 10:30:27 +01:00
|
|
|
/** Raw YAML structure (before validation). */
|
|
|
|
|
interface GalleryYaml {
|
|
|
|
|
hero?: string | string[];
|
|
|
|
|
albums?: string[];
|
|
|
|
|
exifOnHover?: boolean;
|
2026-02-15 23:07:04 +01:00
|
|
|
map?: boolean;
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
theme?:
|
feat: add noir, monograph, botanica presets + stacked/typographic/mosaic hero styles + showcase/filmstrip/editorial-flow gallery layouts
- 3 new theme presets: noir (darkroom analog), monograph (typographic book), botanica (organic nature)
- 3 new hero styles: stacked (fullbleed + thumbnail strip), typographic (text-only), mosaic (multi-image grid)
- 3 new gallery layouts: showcase (featured + grid), filmstrip (horizontal scroll), editorial-flow (alternating pairs)
- Extended config types, validation, and CSS tokens for all new options
- Updated theming docs with new presets table, descriptions, and example configs
2026-02-15 21:58:31 +01:00
|
|
|
| string
|
|
|
|
|
| {
|
|
|
|
|
preset?: string;
|
|
|
|
|
accent?: string;
|
|
|
|
|
fonts?: { heading?: string; body?: string; caption?: string };
|
|
|
|
|
radius?: number;
|
|
|
|
|
photoFrame?: string;
|
|
|
|
|
grain?: boolean;
|
|
|
|
|
headerDot?: boolean;
|
|
|
|
|
heroStyle?: string;
|
|
|
|
|
};
|
2026-02-14 10:30:27 +01:00
|
|
|
grid?: {
|
|
|
|
|
columns?: number;
|
|
|
|
|
gap?: number;
|
|
|
|
|
aspectRatio?: string;
|
|
|
|
|
layout?: string;
|
|
|
|
|
};
|
2026-02-14 11:47:26 +01:00
|
|
|
footer?: {
|
|
|
|
|
name?: string;
|
|
|
|
|
instagram?: string;
|
|
|
|
|
email?: string;
|
|
|
|
|
website?: string;
|
|
|
|
|
};
|
2026-03-05 16:52:01 +01:00
|
|
|
subpages?:
|
|
|
|
|
| Record<string, string[] | Array<string | Record<string, string>>>
|
|
|
|
|
| Array<{
|
2026-02-14 10:30:27 +01:00
|
|
|
name: string;
|
2026-03-05 16:52:01 +01:00
|
|
|
albums: Array<string | Record<string, string>>;
|
2026-02-14 10:30:27 +01:00
|
|
|
password?: string;
|
|
|
|
|
grid?: {
|
|
|
|
|
columns?: number;
|
|
|
|
|
gap?: number;
|
|
|
|
|
aspectRatio?: string;
|
|
|
|
|
layout?: string;
|
|
|
|
|
};
|
|
|
|
|
}>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── YAML loading ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function loadGalleryYaml(): GalleryYaml {
|
|
|
|
|
const yamlPath = path.join(process.cwd(), 'content', 'gallery.yaml');
|
|
|
|
|
|
|
|
|
|
if (!fs.existsSync(yamlPath)) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Gallery config not found: ${yamlPath}\n` +
|
feat: add noir, monograph, botanica presets + stacked/typographic/mosaic hero styles + showcase/filmstrip/editorial-flow gallery layouts
- 3 new theme presets: noir (darkroom analog), monograph (typographic book), botanica (organic nature)
- 3 new hero styles: stacked (fullbleed + thumbnail strip), typographic (text-only), mosaic (multi-image grid)
- 3 new gallery layouts: showcase (featured + grid), filmstrip (horizontal scroll), editorial-flow (alternating pairs)
- Extended config types, validation, and CSS tokens for all new options
- Updated theming docs with new presets table, descriptions, and example configs
2026-02-15 21:58:31 +01:00
|
|
|
`Copy content/gallery.yaml.example to content/gallery.yaml and add your album IDs.`,
|
2026-02-14 10:30:27 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const raw = fs.readFileSync(yamlPath, 'utf8');
|
|
|
|
|
const data = yaml.load(raw) as GalleryYaml;
|
|
|
|
|
|
|
|
|
|
if (!data || typeof data !== 'object') {
|
|
|
|
|
throw new Error('gallery.yaml is empty or invalid');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── UUID validation ────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
|
|
|
|
|
|
function validateUuid(id: string, context: string): string {
|
|
|
|
|
const trimmed = id.trim();
|
|
|
|
|
if (!UUID_REGEX.test(trimmed)) {
|
|
|
|
|
throw new Error(`Invalid UUID in ${context}: "${trimmed}"`);
|
|
|
|
|
}
|
|
|
|
|
return trimmed;
|
2026-02-14 00:59:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-14 10:30:27 +01:00
|
|
|
// ── Slugify ────────────────────────────────────────────────────
|
|
|
|
|
|
2026-02-14 00:59:57 +01:00
|
|
|
/**
|
2026-02-14 10:30:27 +01:00
|
|
|
* Convert an album name into a URL-friendly slug.
|
|
|
|
|
* "Kloster Chorin" → "kloster-chorin"
|
2026-02-14 00:59:57 +01:00
|
|
|
*/
|
2026-02-14 10:30:27 +01:00
|
|
|
export function slugify(name: string): string {
|
|
|
|
|
return name
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.normalize('NFD')
|
|
|
|
|
.replace(/[\u0300-\u036f]/g, '') // strip diacritics (ü→u, é→e)
|
2026-02-14 11:47:26 +01:00
|
|
|
.replace(/[^a-z0-9]+/g, '-') // non-alphanumeric → hyphens
|
|
|
|
|
.replace(/^-+|-+$/g, ''); // trim leading/trailing hyphens
|
2026-02-14 10:30:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Config type ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/** Grid layout configuration. */
|
|
|
|
|
export interface GridConfig {
|
|
|
|
|
columns: number;
|
|
|
|
|
gap: number;
|
|
|
|
|
aspectRatio: string;
|
feat: add noir, monograph, botanica presets + stacked/typographic/mosaic hero styles + showcase/filmstrip/editorial-flow gallery layouts
- 3 new theme presets: noir (darkroom analog), monograph (typographic book), botanica (organic nature)
- 3 new hero styles: stacked (fullbleed + thumbnail strip), typographic (text-only), mosaic (multi-image grid)
- 3 new gallery layouts: showcase (featured + grid), filmstrip (horizontal scroll), editorial-flow (alternating pairs)
- Extended config types, validation, and CSS tokens for all new options
- Updated theming docs with new presets table, descriptions, and example configs
2026-02-15 21:58:31 +01:00
|
|
|
layout: 'masonry' | 'uniform' | 'showcase' | 'filmstrip' | 'editorial-flow';
|
2026-02-14 00:59:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-14 10:30:27 +01:00
|
|
|
export interface AppConfig {
|
|
|
|
|
immich: { apiUrl: string; apiKey: string };
|
|
|
|
|
albums: string[];
|
|
|
|
|
standaloneAlbums: string[];
|
|
|
|
|
subpages: SubpageConfig[];
|
|
|
|
|
siteTitle: string;
|
|
|
|
|
siteSubtitle: string;
|
|
|
|
|
/** One or more hero image asset IDs for the homepage carousel. */
|
|
|
|
|
heroImages: string[];
|
|
|
|
|
/** Show EXIF (camera/lens) on photo grid hover. Default: true. */
|
|
|
|
|
exifOnHover: boolean;
|
|
|
|
|
/** Photo grid layout configuration. */
|
|
|
|
|
grid: GridConfig;
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
/** Theme configuration. */
|
|
|
|
|
theme: ThemeConfig;
|
2026-02-14 11:47:26 +01:00
|
|
|
/** Footer social links config. */
|
|
|
|
|
footer: FooterConfig | null;
|
2026-02-15 23:07:04 +01:00
|
|
|
/** Show the /map page. Default: false. */
|
|
|
|
|
map: boolean;
|
2026-03-05 16:52:01 +01:00
|
|
|
/** Explicit album name overrides from gallery.yaml. */
|
|
|
|
|
albumOverrides: Record<string, string>;
|
2026-02-14 10:30:27 +01:00
|
|
|
cacheTtl: number;
|
|
|
|
|
rateLimitRpm: number;
|
|
|
|
|
}
|
|
|
|
|
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
// ── Theme resolution ───────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const VALID_PHOTO_FRAMES = ['none', 'passepartout', 'shadow'];
|
feat: add noir, monograph, botanica presets + stacked/typographic/mosaic hero styles + showcase/filmstrip/editorial-flow gallery layouts
- 3 new theme presets: noir (darkroom analog), monograph (typographic book), botanica (organic nature)
- 3 new hero styles: stacked (fullbleed + thumbnail strip), typographic (text-only), mosaic (multi-image grid)
- 3 new gallery layouts: showcase (featured + grid), filmstrip (horizontal scroll), editorial-flow (alternating pairs)
- Extended config types, validation, and CSS tokens for all new options
- Updated theming docs with new presets table, descriptions, and example configs
2026-02-15 21:58:31 +01:00
|
|
|
const VALID_HERO_STYLES = ['split', 'fullbleed', 'minimal', 'stacked', 'typographic', 'mosaic'];
|
|
|
|
|
const VALID_LAYOUTS = ['masonry', 'uniform', 'showcase', 'filmstrip', 'editorial-flow'];
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
|
|
|
|
|
function resolveTheme(raw?: GalleryYaml['theme']): ThemeConfig {
|
|
|
|
|
// No theme key → default preset
|
|
|
|
|
if (!raw) return { ...THEME_PRESETS.studio };
|
|
|
|
|
|
|
|
|
|
// String shorthand: `theme: minimal`
|
|
|
|
|
if (typeof raw === 'string') {
|
|
|
|
|
const preset = THEME_PRESETS[raw];
|
|
|
|
|
if (!preset) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Unknown theme preset "${raw}". Valid presets: ${Object.keys(THEME_PRESETS).join(', ')}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return { ...preset };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Object: merge with base preset
|
|
|
|
|
const baseName = raw.preset ?? 'studio';
|
|
|
|
|
const base = THEME_PRESETS[baseName];
|
|
|
|
|
if (!base) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Unknown theme preset "${baseName}". Valid presets: ${Object.keys(THEME_PRESETS).join(', ')}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
preset: baseName,
|
|
|
|
|
accent: raw.accent ?? base.accent,
|
|
|
|
|
fonts: {
|
|
|
|
|
heading: raw.fonts?.heading ?? base.fonts.heading,
|
|
|
|
|
body: raw.fonts?.body ?? base.fonts.body,
|
|
|
|
|
caption: raw.fonts?.caption ?? base.fonts.caption,
|
|
|
|
|
},
|
|
|
|
|
radius: raw.radius ?? base.radius,
|
|
|
|
|
photoFrame: VALID_PHOTO_FRAMES.includes(raw.photoFrame ?? '')
|
|
|
|
|
? (raw.photoFrame as ThemeConfig['photoFrame'])
|
|
|
|
|
: base.photoFrame,
|
|
|
|
|
grain: raw.grain ?? base.grain,
|
|
|
|
|
headerDot: raw.headerDot ?? base.headerDot,
|
|
|
|
|
heroStyle: VALID_HERO_STYLES.includes(raw.heroStyle ?? '')
|
|
|
|
|
? (raw.heroStyle as ThemeConfig['heroStyle'])
|
|
|
|
|
: base.heroStyle,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Build a Google Fonts URL for the theme's fonts.
|
|
|
|
|
* Deduplicates fonts and includes key weights.
|
|
|
|
|
*/
|
|
|
|
|
export function getGoogleFontsUrl(theme: ThemeConfig): string {
|
|
|
|
|
const weights = '300;400;500;600';
|
|
|
|
|
const families = [...new Set([theme.fonts.heading, theme.fonts.body, theme.fonts.caption])];
|
|
|
|
|
const params = families.map((f) => `family=${f.replace(/ /g, '+')}:wght@${weights}`).join('&');
|
|
|
|
|
return `https://fonts.googleapis.com/css2?${params}&display=swap`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 10:30:27 +01:00
|
|
|
// ── Config builder ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
let _config: AppConfig | null = null;
|
|
|
|
|
|
|
|
|
|
export function getConfig(): AppConfig {
|
|
|
|
|
if (_config) return _config;
|
|
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
const apiUrl = env.IMMICH_API_URL;
|
|
|
|
|
const apiKey = env.IMMICH_API_KEY;
|
2026-02-14 10:30:27 +01:00
|
|
|
const gallery = loadGalleryYaml();
|
2026-02-14 00:59:57 +01:00
|
|
|
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
// Resolve theme: string shorthand → preset, object → merged with preset base
|
|
|
|
|
const theme = resolveTheme(gallery.theme);
|
|
|
|
|
|
2026-03-05 16:52:01 +01:00
|
|
|
const albumOverrides: Record<string, string> = {};
|
|
|
|
|
|
|
|
|
|
function processAlbumEntry(entry: string | Record<string, string>, context: string): string {
|
|
|
|
|
if (typeof entry === 'string') {
|
|
|
|
|
return validateUuid(entry, context);
|
|
|
|
|
}
|
|
|
|
|
// Map format: { "uuid": "Custom Name" }
|
|
|
|
|
const [uuid, name] = Object.entries(entry)[0];
|
|
|
|
|
const validatedUuid = validateUuid(uuid, context);
|
|
|
|
|
albumOverrides[validatedUuid] = name;
|
|
|
|
|
return validatedUuid;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 10:30:27 +01:00
|
|
|
// Parse standalone albums
|
2026-03-05 16:52:01 +01:00
|
|
|
const standaloneAlbumIds = (gallery.albums ?? []).map((entry) =>
|
|
|
|
|
processAlbumEntry(entry, 'gallery.yaml albums'),
|
2026-02-14 11:47:26 +01:00
|
|
|
);
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-03-05 16:52:01 +01:00
|
|
|
if (standaloneAlbumIds.length === 0 && (!gallery.subpages || (Array.isArray(gallery.subpages) ? gallery.subpages.length === 0 : Object.keys(gallery.subpages).length === 0))) {
|
2026-02-14 10:30:27 +01:00
|
|
|
throw new Error('gallery.yaml must define at least one album or subpage');
|
2026-02-14 00:59:57 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-14 10:30:27 +01:00
|
|
|
// Parse subpages
|
2026-03-05 16:52:01 +01:00
|
|
|
let subpages: SubpageConfig[] = [];
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(gallery.subpages)) {
|
|
|
|
|
// Original list format
|
|
|
|
|
subpages = gallery.subpages.map((sp) => {
|
|
|
|
|
if (!sp.name || !sp.albums || sp.albums.length === 0) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Subpage "${sp.name || '(unnamed)'}" must have a name and at least one album`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
name: sp.name,
|
|
|
|
|
slug: slugify(sp.name),
|
|
|
|
|
albumIds: sp.albums.map((entry) => processAlbumEntry(entry, `subpage "${sp.name}"`)),
|
|
|
|
|
password: sp.password,
|
|
|
|
|
...(sp.grid
|
|
|
|
|
? {
|
|
|
|
|
grid: {
|
|
|
|
|
...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}),
|
|
|
|
|
...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}),
|
|
|
|
|
...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}),
|
|
|
|
|
...(sp.grid.layout != null
|
|
|
|
|
? {
|
|
|
|
|
layout: (VALID_LAYOUTS.includes(sp.grid.layout)
|
|
|
|
|
? sp.grid.layout
|
|
|
|
|
: 'masonry') as GridConfig['layout'],
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
} else if (gallery.subpages) {
|
|
|
|
|
// New map format: { "Subpage Name": ["uuid1", { "uuid2": "Name" }] } OR { "Subpage Name": { password: "...", albums: [...] } }
|
|
|
|
|
subpages = Object.entries(gallery.subpages).map(([name, value]) => {
|
|
|
|
|
// If it's an array, it's just albums
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
return {
|
|
|
|
|
name,
|
|
|
|
|
slug: slugify(name),
|
|
|
|
|
albumIds: value.map((entry) => processAlbumEntry(entry, `subpage "${name}"`)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If it's an object, it might have password, grid, albums
|
|
|
|
|
const sp = value as any;
|
|
|
|
|
const albumEntries = sp.albums || [];
|
|
|
|
|
return {
|
|
|
|
|
name,
|
|
|
|
|
slug: slugify(name),
|
|
|
|
|
albumIds: albumEntries.map((entry: any) => processAlbumEntry(entry, `subpage "${name}"`)),
|
|
|
|
|
password: sp.password,
|
|
|
|
|
...(sp.grid
|
|
|
|
|
? {
|
|
|
|
|
grid: {
|
|
|
|
|
...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}),
|
|
|
|
|
...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}),
|
|
|
|
|
...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}),
|
|
|
|
|
...(sp.grid.layout != null
|
|
|
|
|
? {
|
|
|
|
|
layout: (VALID_LAYOUTS.includes(sp.grid.layout)
|
|
|
|
|
? sp.grid.layout
|
|
|
|
|
: 'masonry') as GridConfig['layout'],
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-02-14 00:59:57 +01:00
|
|
|
|
|
|
|
|
// Collect ALL album IDs (standalone + subpage) — deduplicated
|
|
|
|
|
const subpageAlbumIds = new Set(subpages.flatMap((sp) => sp.albumIds));
|
|
|
|
|
const allAlbumIds = [...new Set([...standaloneAlbumIds, ...subpageAlbumIds])];
|
|
|
|
|
|
2026-02-14 10:30:27 +01:00
|
|
|
_config = {
|
2026-02-14 00:59:57 +01:00
|
|
|
immich: {
|
|
|
|
|
apiUrl: `${apiUrl}/api`,
|
|
|
|
|
apiKey,
|
|
|
|
|
},
|
|
|
|
|
albums: allAlbumIds,
|
|
|
|
|
standaloneAlbums: standaloneAlbumIds.filter((id) => !subpageAlbumIds.has(id)),
|
|
|
|
|
subpages,
|
2026-02-14 11:47:26 +01:00
|
|
|
siteTitle: env.SITE_TITLE,
|
|
|
|
|
siteSubtitle: env.SITE_SUBTITLE,
|
2026-02-14 10:30:27 +01:00
|
|
|
heroImages: gallery.hero
|
2026-02-14 11:47:26 +01:00
|
|
|
? (Array.isArray(gallery.hero) ? gallery.hero : [gallery.hero]).map((id) =>
|
feat: add noir, monograph, botanica presets + stacked/typographic/mosaic hero styles + showcase/filmstrip/editorial-flow gallery layouts
- 3 new theme presets: noir (darkroom analog), monograph (typographic book), botanica (organic nature)
- 3 new hero styles: stacked (fullbleed + thumbnail strip), typographic (text-only), mosaic (multi-image grid)
- 3 new gallery layouts: showcase (featured + grid), filmstrip (horizontal scroll), editorial-flow (alternating pairs)
- Extended config types, validation, and CSS tokens for all new options
- Updated theming docs with new presets table, descriptions, and example configs
2026-02-15 21:58:31 +01:00
|
|
|
validateUuid(id, 'gallery.yaml hero'),
|
|
|
|
|
)
|
2026-02-14 10:30:27 +01:00
|
|
|
: [],
|
|
|
|
|
exifOnHover: gallery.exifOnHover !== false,
|
|
|
|
|
grid: {
|
|
|
|
|
columns: gallery.grid?.columns ?? 3,
|
|
|
|
|
gap: gallery.grid?.gap ?? 12,
|
|
|
|
|
aspectRatio: gallery.grid?.aspectRatio ?? '1',
|
feat: add noir, monograph, botanica presets + stacked/typographic/mosaic hero styles + showcase/filmstrip/editorial-flow gallery layouts
- 3 new theme presets: noir (darkroom analog), monograph (typographic book), botanica (organic nature)
- 3 new hero styles: stacked (fullbleed + thumbnail strip), typographic (text-only), mosaic (multi-image grid)
- 3 new gallery layouts: showcase (featured + grid), filmstrip (horizontal scroll), editorial-flow (alternating pairs)
- Extended config types, validation, and CSS tokens for all new options
- Updated theming docs with new presets table, descriptions, and example configs
2026-02-15 21:58:31 +01:00
|
|
|
layout: (VALID_LAYOUTS.includes(gallery.grid?.layout ?? '')
|
|
|
|
|
? gallery.grid!.layout
|
|
|
|
|
: 'masonry') as GridConfig['layout'],
|
2026-02-14 10:30:27 +01:00
|
|
|
},
|
feat: theming system, documentation, and photo grid regression tests
- Add configurable theme system with 4 presets (studio, minimal, editorial, classic)
- Theme controls fonts, colors, accent, photo frames, grain, and header dot
- Resolve themes from gallery.yaml: string shorthand or full object override
- Inject theme as CSS custom properties on <html> element
- Dynamic Google Fonts loading based on theme config
- OG image route reads accent/background from theme
- Refactor Studio overrides into conditional CSS via data-attributes
Documentation:
- Add docs/gallery-config.md with comprehensive configuration reference
- Add docs/theming.md with preset reference and custom theme guide
- Slim README to link to detailed docs
Fix CSS regression:
- Fix passepartout frame overflow:visible breaking next/image fill rendering
- Remove duplicate .photo-grid__item block that stripped overflow:hidden
Testing:
- Add e2e/photo-grid.spec.ts with 3 regression tests:
- Grid items render with real dimensions
- overflow:hidden CSS guard (catches the exact bug fixed)
- Images load with real pixel data (not blurhash)
Tooling:
- Add scripts/screenshots.ts for automated theme screenshot generation
- Handles dev server lifecycle, theme cycling, FadeIn + lazy image handling
2026-02-15 12:21:02 +01:00
|
|
|
theme,
|
2026-02-14 11:47:26 +01:00
|
|
|
footer: gallery.footer
|
|
|
|
|
? {
|
feat: add noir, monograph, botanica presets + stacked/typographic/mosaic hero styles + showcase/filmstrip/editorial-flow gallery layouts
- 3 new theme presets: noir (darkroom analog), monograph (typographic book), botanica (organic nature)
- 3 new hero styles: stacked (fullbleed + thumbnail strip), typographic (text-only), mosaic (multi-image grid)
- 3 new gallery layouts: showcase (featured + grid), filmstrip (horizontal scroll), editorial-flow (alternating pairs)
- Extended config types, validation, and CSS tokens for all new options
- Updated theming docs with new presets table, descriptions, and example configs
2026-02-15 21:58:31 +01:00
|
|
|
name: gallery.footer.name,
|
|
|
|
|
instagram: gallery.footer.instagram,
|
|
|
|
|
email: gallery.footer.email,
|
|
|
|
|
website: gallery.footer.website,
|
|
|
|
|
}
|
2026-02-14 11:47:26 +01:00
|
|
|
: null,
|
2026-02-15 23:07:04 +01:00
|
|
|
map: gallery.map === true,
|
2026-03-05 16:52:01 +01:00
|
|
|
albumOverrides,
|
2026-02-14 11:47:26 +01:00
|
|
|
cacheTtl: env.CACHE_TTL * 1000,
|
|
|
|
|
rateLimitRpm: env.RATE_LIMIT_RPM,
|
2026-02-14 00:59:57 +01:00
|
|
|
};
|
|
|
|
|
|
2026-02-14 10:30:27 +01:00
|
|
|
return _config;
|
2026-02-14 00:59:57 +01:00
|
|
|
}
|