docs: add screenshots for all themes and hero styles
16
README.md
@@ -117,6 +117,22 @@ theme: studio # or: minimal, editorial, classic, noir, monograph, botanica
|
|||||||
<td><img src="docs/screenshots/theme-editorial-home.png" width="400" alt="Editorial theme" /></td>
|
<td><img src="docs/screenshots/theme-editorial-home.png" width="400" alt="Editorial theme" /></td>
|
||||||
<td><img src="docs/screenshots/theme-classic-home.png" width="400" alt="Classic theme" /></td>
|
<td><img src="docs/screenshots/theme-classic-home.png" width="400" alt="Classic theme" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><strong>Noir</strong><br><em>Darkroom analog, film noir</em></td>
|
||||||
|
<td align="center"><strong>Monograph</strong><br><em>Typographic, book-like</em></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="docs/screenshots/theme-noir-home.png" width="400" alt="Noir theme" /></td>
|
||||||
|
<td><img src="docs/screenshots/theme-monograph-home.png" width="400" alt="Monograph theme" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><strong>Botanica</strong><br><em>Organic, nature-inspired</em></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="docs/screenshots/theme-botanica-home.png" width="400" alt="Botanica theme" /></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
→ **[Theming Guide](docs/theming.md)**
|
→ **[Theming Guide](docs/theming.md)**
|
||||||
|
|||||||
BIN
docs/screenshots/hero-fullbleed-home.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/screenshots/hero-minimal-home.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/screenshots/hero-mosaic-home.png
Normal file
|
After Width: | Height: | Size: 684 KiB |
BIN
docs/screenshots/hero-split-home.png
Normal file
|
After Width: | Height: | Size: 920 KiB |
BIN
docs/screenshots/hero-stacked-home.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/screenshots/hero-typographic-home.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
docs/screenshots/theme-botanica-grid.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/screenshots/theme-botanica-home.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 786 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 900 KiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 757 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 905 KiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 873 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 1.9 MiB |
BIN
docs/screenshots/theme-monograph-grid.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/screenshots/theme-monograph-home.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/screenshots/theme-noir-grid.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/screenshots/theme-noir-home.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 905 KiB After Width: | Height: | Size: 1.9 MiB |
@@ -26,6 +26,41 @@ That's it. Your entire gallery switches to the minimal theme.
|
|||||||
|
|
||||||
Default is `studio` if no theme is specified.
|
Default is `studio` if no theme is specified.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><strong>Studio</strong><br><em>Leica-inspired, red dot</em></td>
|
||||||
|
<td align="center"><strong>Minimal</strong><br><em>Swiss brutalist, fullbleed hero</em></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="screenshots/theme-studio-home.png" width="400" alt="Studio theme" /></td>
|
||||||
|
<td><img src="screenshots/theme-minimal-home.png" width="400" alt="Minimal theme" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><strong>Editorial</strong><br><em>Cinematic magazine, warm charcoal</em></td>
|
||||||
|
<td align="center"><strong>Classic</strong><br><em>Gilded gallery, gold accents</em></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="screenshots/theme-editorial-home.png" width="400" alt="Editorial theme" /></td>
|
||||||
|
<td><img src="screenshots/theme-classic-home.png" width="400" alt="Classic theme" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><strong>Noir</strong><br><em>Darkroom analog, film noir</em></td>
|
||||||
|
<td align="center"><strong>Monograph</strong><br><em>Typographic, book-like</em></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="screenshots/theme-noir-home.png" width="400" alt="Noir theme" /></td>
|
||||||
|
<td><img src="screenshots/theme-monograph-home.png" width="400" alt="Monograph theme" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><strong>Botanica</strong><br><em>Organic, nature-inspired</em></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="screenshots/theme-botanica-home.png" width="400" alt="Botanica theme" /></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
### What makes each theme unique
|
### What makes each theme unique
|
||||||
|
|
||||||
**Studio** (default) — Leica-inspired with a red nav dot, passepartout photo frames, film grain, and a split hero layout. Clean editorial feel with Playfair Display headings.
|
**Studio** (default) — Leica-inspired with a red nav dot, passepartout photo frames, film grain, and a split hero layout. Clean editorial feel with Playfair Display headings.
|
||||||
@@ -112,6 +147,29 @@ Controls the homepage hero layout:
|
|||||||
- **`typographic`** — no hero image; massive centered title with numbered album navigation list (used by Monograph)
|
- **`typographic`** — no hero image; massive centered title with numbered album navigation list (used by Monograph)
|
||||||
- **`mosaic`** — asymmetric multi-image grid with frosted-glass title overlay centered on top
|
- **`mosaic`** — asymmetric multi-image grid with frosted-glass title overlay centered on top
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><strong>Split</strong></td>
|
||||||
|
<td align="center"><strong>Fullbleed</strong></td>
|
||||||
|
<td align="center"><strong>Minimal</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="screenshots/hero-split-home.png" width="280" alt="Split Hero" /></td>
|
||||||
|
<td><img src="screenshots/hero-fullbleed-home.png" width="280" alt="Fullbleed Hero" /></td>
|
||||||
|
<td><img src="screenshots/hero-minimal-home.png" width="280" alt="Minimal Hero" /></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><strong>Stacked</strong></td>
|
||||||
|
<td align="center"><strong>Typographic</strong></td>
|
||||||
|
<td align="center"><strong>Mosaic</strong></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><img src="screenshots/hero-stacked-home.png" width="280" alt="Stacked Hero" /></td>
|
||||||
|
<td><img src="screenshots/hero-typographic-home.png" width="280" alt="Typographic Hero" /></td>
|
||||||
|
<td><img src="screenshots/hero-mosaic-home.png" width="280" alt="Mosaic Hero" /></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
### `grid.layout`
|
### `grid.layout`
|
||||||
|
|
||||||
Controls the photo grid layout on album pages:
|
Controls the photo grid layout on album pages:
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ import { spawn, type ChildProcess } from 'child_process';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const THEMES = ['studio', 'minimal', 'editorial', 'classic'];
|
const THEMES = ['studio', 'minimal', 'editorial', 'classic', 'noir', 'monograph', 'botanica'];
|
||||||
const BASE_URL = 'http://localhost:3000';
|
const BASE_URL = 'http://localhost:3000';
|
||||||
const GRID_PATH = '/deutschland/kloster-chorin-2024'; // public subpage with photos
|
const GRID_PATH = '/deutschland/kloster-chorin'; // public subpage with photos
|
||||||
const OUTPUT_DIR = path.join(process.cwd(), 'docs', 'screenshots');
|
const OUTPUT_DIR = path.join(process.cwd(), 'docs', 'screenshots');
|
||||||
const GALLERY_YAML = path.join(process.cwd(), 'content', 'gallery.yaml');
|
const SETTINGS_YAML = path.join(process.cwd(), 'content', 'settings.yaml');
|
||||||
const VIEWPORT = { width: 1440, height: 900 };
|
const VIEWPORT = { width: 1440, height: 900 };
|
||||||
const IMAGE_LOAD_TIMEOUT = 8000; // ms to wait for images after networkidle
|
const IMAGE_LOAD_TIMEOUT = 8000; // ms to wait for images after networkidle
|
||||||
|
|
||||||
@@ -31,8 +31,8 @@ async function main() {
|
|||||||
// Ensure output directory exists
|
// Ensure output directory exists
|
||||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||||
|
|
||||||
// Read original gallery.yaml to restore later
|
// Read original settings.yaml to restore later
|
||||||
const originalYaml = fs.readFileSync(GALLERY_YAML, 'utf8');
|
const originalYaml = fs.readFileSync(SETTINGS_YAML, 'utf8');
|
||||||
|
|
||||||
// Dynamic import for Playwright (may not be installed globally)
|
// Dynamic import for Playwright (may not be installed globally)
|
||||||
const { chromium } = await import('playwright');
|
const { chromium } = await import('playwright');
|
||||||
@@ -43,9 +43,9 @@ async function main() {
|
|||||||
console.log(`\n🎨 Theme: ${theme}`);
|
console.log(`\n🎨 Theme: ${theme}`);
|
||||||
console.log('─'.repeat(50));
|
console.log('─'.repeat(50));
|
||||||
|
|
||||||
// Update gallery.yaml with the theme
|
// Update settings.yaml with the theme
|
||||||
const yamlWithTheme = setThemeInYaml(originalYaml, theme);
|
const yamlWithTheme = setThemeInSettingsYaml(originalYaml, theme);
|
||||||
fs.writeFileSync(GALLERY_YAML, yamlWithTheme);
|
fs.writeFileSync(SETTINGS_YAML, yamlWithTheme);
|
||||||
|
|
||||||
// Start dev server
|
// Start dev server
|
||||||
console.log(' ⏳ Starting dev server...');
|
console.log(' ⏳ Starting dev server...');
|
||||||
@@ -89,13 +89,62 @@ async function main() {
|
|||||||
await context.close();
|
await context.close();
|
||||||
|
|
||||||
// Stop dev server
|
// Stop dev server
|
||||||
server.kill('SIGTERM');
|
if (server.pid) {
|
||||||
await new Promise((resolve) => server.on('close', resolve));
|
process.kill(-server.pid, 'SIGTERM');
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const timeout = setTimeout(resolve, 3000);
|
||||||
|
server.on('close', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log(' 🛑 Server stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
const HERO_STYLES = ['split', 'fullbleed', 'minimal', 'stacked', 'typographic', 'mosaic'];
|
||||||
|
for (const hero of HERO_STYLES) {
|
||||||
|
console.log(`\n🖼 Hero Style: ${hero} (Theme: studio)`);
|
||||||
|
console.log('─'.repeat(50));
|
||||||
|
|
||||||
|
const yamlWithTheme = setThemeInSettingsYaml(originalYaml, 'studio');
|
||||||
|
const yamlWithHero = setHeroStyleInSettingsYaml(yamlWithTheme, hero);
|
||||||
|
fs.writeFileSync(SETTINGS_YAML, yamlWithHero);
|
||||||
|
|
||||||
|
console.log(' ⏳ Starting dev server...');
|
||||||
|
const server = startDevServer();
|
||||||
|
await waitForServer(BASE_URL, 15000);
|
||||||
|
console.log(' ✅ Server ready');
|
||||||
|
|
||||||
|
const context = await browser.newContext({ viewport: VIEWPORT });
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
console.log(' 📸 Capturing homepage hero...');
|
||||||
|
await page.goto(BASE_URL, { waitUntil: 'networkidle' });
|
||||||
|
await waitForImages(page);
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(OUTPUT_DIR, `hero-${hero}-home.png`),
|
||||||
|
fullPage: false,
|
||||||
|
});
|
||||||
|
console.log(` ✅ hero-${hero}-home.png`);
|
||||||
|
|
||||||
|
await context.close();
|
||||||
|
|
||||||
|
if (server.pid) {
|
||||||
|
process.kill(-server.pid, 'SIGTERM');
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const timeout = setTimeout(resolve, 3000);
|
||||||
|
server.on('close', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
console.log(' 🛑 Server stopped');
|
console.log(' 🛑 Server stopped');
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Always restore original gallery.yaml
|
// Always restore original settings.yaml
|
||||||
fs.writeFileSync(GALLERY_YAML, originalYaml);
|
fs.writeFileSync(SETTINGS_YAML, originalYaml);
|
||||||
console.log('\n✅ Restored original gallery.yaml');
|
console.log('\n✅ Restored original gallery.yaml');
|
||||||
await browser.close();
|
await browser.close();
|
||||||
}
|
}
|
||||||
@@ -111,14 +160,35 @@ async function main() {
|
|||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
function setThemeInYaml(yaml: string, theme: string): string {
|
/**
|
||||||
// Replace existing theme line or insert after the header comment
|
* Replace the theme preset in settings.yaml.
|
||||||
if (/^theme:\s*.+$/m.test(yaml)) {
|
* Handles both `theme: preset` shorthand and `theme:\n preset: "x"` block.
|
||||||
return yaml.replace(/^theme:\s*.+$/m, `theme: ${theme}`);
|
*/
|
||||||
|
function setThemeInSettingsYaml(yaml: string, theme: string): string {
|
||||||
|
// Handle ` preset: "studio"` or ` preset: studio` (block format)
|
||||||
|
if (/^ preset:\s*/m.test(yaml)) {
|
||||||
|
return yaml.replace(/^ preset:\s*.+$/m, ` preset: "${theme}"`);
|
||||||
}
|
}
|
||||||
// Insert theme after the header comments
|
// Handle `theme: studio` shorthand
|
||||||
const marker = '# Secrets (API key, server URL) stay in .env.local.';
|
if (/^theme:\s*\w+\s*$/m.test(yaml)) {
|
||||||
return yaml.replace(marker, `${marker}\n\ntheme: ${theme}`);
|
return yaml.replace(/^theme:\s*\w+\s*$/m, `theme: ${theme}`);
|
||||||
|
}
|
||||||
|
// Fallback: insert block after the theme comment
|
||||||
|
const marker = '# Presets: studio, minimal, editorial, classic, noir, monograph, botanica';
|
||||||
|
if (yaml.includes(marker)) {
|
||||||
|
return yaml.replace(marker, `${marker}\ntheme: ${theme}`);
|
||||||
|
}
|
||||||
|
return yaml + `\ntheme: ${theme}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the heroStyle in settings.yaml.
|
||||||
|
*/
|
||||||
|
function setHeroStyleInSettingsYaml(yaml: string, style: string): string {
|
||||||
|
if (/^ heroStyle:\s*/m.test(yaml)) {
|
||||||
|
return yaml.replace(/^ heroStyle:\s*.+$/m, ` heroStyle: "${style}"`);
|
||||||
|
}
|
||||||
|
return yaml;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startDevServer(): ChildProcess {
|
function startDevServer(): ChildProcess {
|
||||||
@@ -126,6 +196,7 @@ function startDevServer(): ChildProcess {
|
|||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
stdio: 'pipe',
|
stdio: 'pipe',
|
||||||
env: { ...process.env },
|
env: { ...process.env },
|
||||||
|
detached: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Suppress server output unless DEBUG is set
|
// Suppress server output unless DEBUG is set
|
||||||
@@ -196,8 +267,8 @@ main().catch((err) => {
|
|||||||
console.error('❌ Screenshot generation failed:', err);
|
console.error('❌ Screenshot generation failed:', err);
|
||||||
// Try to restore gallery.yaml on error
|
// Try to restore gallery.yaml on error
|
||||||
try {
|
try {
|
||||||
const original = fs.readFileSync(GALLERY_YAML + '.bak', 'utf8');
|
const original = fs.readFileSync(SETTINGS_YAML + '.bak', 'utf8');
|
||||||
fs.writeFileSync(GALLERY_YAML, original);
|
fs.writeFileSync(SETTINGS_YAML, original);
|
||||||
} catch {
|
} catch {
|
||||||
// Backup may not exist
|
// Backup may not exist
|
||||||
}
|
}
|
||||||
|
|||||||