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
This commit is contained in:
ralksta
2026-02-15 21:58:31 +01:00
parent 4e040c28f3
commit 4b80af1c29
6 changed files with 1238 additions and 74 deletions

View File

@@ -1,10 +1,11 @@
/**
* Homepage — Split hero layout.
* Left panel: site title, subtitle, navigation links.
* Right panel: hero image carousel from Immich.
* Homepage — renders different hero layouts based on theme config.
*
* Styles: split, fullbleed, minimal, stacked, typographic, mosaic.
*/
import Link from 'next/link';
import Image from 'next/image';
import { immich } from '@/lib/immich';
import { getConfig } from '@/lib/config';
import { imageUrl, assetPlaceholder } from '@/lib/urls';
@@ -14,6 +15,30 @@ import { FadeIn } from '@/components/FadeIn';
// Render at request time — requires live Immich connection
export const dynamic = 'force-dynamic';
/** Shared nav links for hero sections. */
function HeroNavLinks({
subpages,
albums,
}: {
subpages: { slug: string; name: string }[];
albums: { id: string; slug: string; albumName: string }[];
}) {
return (
<>
{subpages.map((sp) => (
<Link key={sp.slug} href={`/${sp.slug}`} className="hero__nav-link">
{sp.name}
</Link>
))}
{albums.map((album) => (
<Link key={album.id} href={`/${album.slug}`} className="hero__nav-link">
{album.albumName}
</Link>
))}
</>
);
}
export default async function HomePage() {
const config = getConfig();
const [subpages, albums] = await Promise.all([
@@ -33,6 +58,129 @@ export default async function HomePage() {
}),
);
const heroStyle = config.theme.heroStyle;
// ── Typographic: no image, pure text ───────────────────────────
if (heroStyle === 'typographic') {
return (
<div className="hero hero--typographic">
<div className="hero__content">
<FadeIn delay={0}>
<h1 className="hero__title">{config.siteTitle}</h1>
</FadeIn>
{config.siteSubtitle && (
<FadeIn delay={100}>
<p className="hero__subtitle">{config.siteSubtitle}</p>
</FadeIn>
)}
<FadeIn delay={200}>
<div className="hero__divider" />
</FadeIn>
<FadeIn delay={300}>
<nav className="hero__nav hero__nav--indexed">
{subpages.map((sp, i) => (
<Link key={sp.slug} href={`/${sp.slug}`} className="hero__nav-link">
<span className="hero__nav-index">
{String(i + 1).padStart(2, '0')}
</span>
{sp.name}
</Link>
))}
{albums.map((album, i) => (
<Link key={album.id} href={`/${album.slug}`} className="hero__nav-link">
<span className="hero__nav-index">
{String(subpages.length + i + 1).padStart(2, '0')}
</span>
{album.albumName}
</Link>
))}
</nav>
</FadeIn>
</div>
</div>
);
}
// ── Stacked: fullbleed image + text at bottom + thumbnail strip ─
if (heroStyle === 'stacked') {
// Fetch album thumbnails for the strip
const allEntries = [
...subpages.map((sp) => ({ slug: sp.slug, name: sp.name })),
...albums.map((a) => ({ slug: a.slug, name: a.albumName })),
];
return (
<div className="hero hero--stacked">
<div className="hero__stacked-image">
<HeroCarousel images={heroData} />
<div className="hero__stacked-overlay">
<FadeIn delay={0}>
<h1 className="hero__title">{config.siteTitle}</h1>
</FadeIn>
{config.siteSubtitle && (
<FadeIn delay={100}>
<p className="hero__subtitle">{config.siteSubtitle}</p>
</FadeIn>
)}
</div>
</div>
<FadeIn delay={200}>
<nav className="hero__thumbnail-strip">
{allEntries.map((entry) => (
<Link
key={entry.slug}
href={`/${entry.slug}`}
className="hero__thumbnail-item"
>
<span className="hero__thumbnail-label">{entry.name}</span>
</Link>
))}
</nav>
</FadeIn>
</div>
);
}
// ── Mosaic: multi-image grid with frosted title overlay ────────
if (heroStyle === 'mosaic') {
return (
<div className="hero hero--mosaic">
<div className="hero__mosaic-grid">
{heroData.slice(0, 6).map((img, i) => (
<div key={i} className={`hero__mosaic-cell hero__mosaic-cell--${i + 1}`}>
<Image
src={img.src}
alt=""
fill
sizes="(max-width: 640px) 100vw, 50vw"
priority={i < 2}
{...(img.blurDataURL
? { placeholder: 'blur' as const, blurDataURL: img.blurDataURL }
: {})}
/>
</div>
))}
</div>
<div className="hero__mosaic-overlay">
<FadeIn delay={0}>
<h1 className="hero__title">{config.siteTitle}</h1>
</FadeIn>
{config.siteSubtitle && (
<FadeIn delay={100}>
<p className="hero__subtitle">{config.siteSubtitle}</p>
</FadeIn>
)}
<FadeIn delay={200}>
<nav className="hero__nav">
<HeroNavLinks subpages={subpages} albums={albums} />
</nav>
</FadeIn>
</div>
</div>
);
}
// ── Default: split / fullbleed / minimal (existing layouts) ────
return (
<div className="hero">
{/* ── Left Panel ──────────────────────────────── */}
@@ -49,16 +197,7 @@ export default async function HomePage() {
<FadeIn delay={200}>
<nav className="hero__nav">
{subpages.map((sp) => (
<Link key={sp.slug} href={`/${sp.slug}`} className="hero__nav-link">
{sp.name}
</Link>
))}
{albums.map((album) => (
<Link key={album.id} href={`/${album.slug}`} className="hero__nav-link">
{album.albumName}
</Link>
))}
<HeroNavLinks subpages={subpages} albums={albums} />
</nav>
</FadeIn>
</div>