diff --git a/app/globals.css b/app/globals.css index 2eeb26a..33c01fa 100644 --- a/app/globals.css +++ b/app/globals.css @@ -57,7 +57,8 @@ .main { max-width: var(--max-width); margin: 0 auto; - padding: 104px 24px 96px; /* 56px header + 48px original padding */ + padding: 104px 24px 96px; + /* 56px header + 48px original padding */ } /* Simple CSS Page Fade-In (Replaces Framer Motion) */ @@ -70,6 +71,7 @@ opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); @@ -80,7 +82,8 @@ .main:has(.hero), .main:has(.about) { max-width: none; - padding-top: 56px; /* Still need space for header */ + padding-top: 56px; + /* Still need space for header */ padding-left: 0; padding-right: 0; padding-bottom: 0; @@ -179,6 +182,95 @@ opacity: 1; } +/* ── Hero: Fullbleed Layout ─────────────────────── */ +.hero--fullbleed { + position: relative; + min-height: calc(100vh - 56px); + overflow: hidden; +} + +/* HeroCarousel images fill the entire container */ +.hero--fullbleed>span, +.hero--fullbleed>img { + position: absolute !important; + inset: 0; + width: 100% !important; + height: 100% !important; + object-fit: cover; +} + +.hero__fullbleed-overlay { + position: absolute; + inset: 0; + z-index: 2; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 64px 40px; + /* Multi-stop for contrast on any image brightness */ + background: + radial-gradient(ellipse at center, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0.0) 70%), + linear-gradient(to bottom, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.15) 40%, rgba(0, 0, 0, 0.45) 100%); +} + +.hero--fullbleed .hero__title { + color: #fff; + font-size: 4rem; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7), 0 8px 48px rgba(0, 0, 0, 0.6); +} + +.hero--fullbleed .hero__subtitle { + color: rgba(255, 255, 255, 0.92); + margin-bottom: 32px; + text-shadow: 0 1px 6px rgba(0, 0, 0, 0.7); +} + +.hero--fullbleed .hero__nav { + background: rgba(0, 0, 0, 0.38); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + padding: 14px 28px; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.hero--fullbleed .hero__nav-link { + color: rgba(255, 255, 255, 0.7); + text-decoration-color: rgba(255, 255, 255, 0.25); +} + +.hero--fullbleed .hero__nav-link:hover { + color: #fff; + text-decoration-color: #fff; +} + +/* ── Hero: Minimal Layout ───────────────────────── */ +.hero--minimal { + display: flex; + align-items: center; + justify-content: center; + min-height: calc(100vh - 56px); + padding: 64px 40px; +} + +.hero--minimal .hero__content { + text-align: center; + max-width: 500px; +} + +.hero--minimal .hero__title { + font-size: 4.5rem; + font-weight: 300; + letter-spacing: 0.08em; + margin-bottom: 12px; +} + +.hero--minimal .hero__subtitle { + margin-bottom: 48px; +} + /* ── Hero: Stacked Layout ───────────────────────── */ .hero--stacked { display: flex; diff --git a/app/page.tsx b/app/page.tsx index aac4cf8..b6195cb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -174,9 +174,56 @@ export default async function HomePage() { ); } - // ── Default: split / fullbleed / minimal (existing layouts) ──── + // ── Minimal: pure text, no image, simple centered ────────────── + if (heroStyle === 'minimal') { + return ( +
+
+ +

{config.siteTitle}

+
+ {config.siteSubtitle && ( + +

{config.siteSubtitle}

+
+ )} + + + +
+
+ ); + } + + // ── Fullbleed: full-viewport image + centered overlay ─────────── + if (heroStyle === 'fullbleed') { + return ( +
+ +
+ +

{config.siteTitle}

+
+ {config.siteSubtitle && ( + +

{config.siteSubtitle}

+
+ )} + + + +
+
+ ); + } + + // ── Split: left text panel, right image (default) ─────────────── return ( -
+
{/* ── Left Panel ──────────────────────────────── */}
diff --git a/components/DevToolbar.tsx b/components/DevToolbar.tsx index 04264c5..3ef4c65 100644 --- a/components/DevToolbar.tsx +++ b/components/DevToolbar.tsx @@ -7,6 +7,9 @@ * * Hero styles and grid layouts require server re-render, so * those sections show a copyable YAML snippet instead. + * + * Layout: tabbed panel (Theme / Grid / Server) anchored bottom-right, + * away from Next.js debug toolbar which sits bottom-left. */ 'use client'; @@ -17,6 +20,9 @@ const PRESETS = ['studio', 'minimal', 'editorial', 'classic', 'noir', 'monograph const FRAMES = ['none', 'passepartout', 'shadow']; const HERO_STYLES = ['split', 'fullbleed', 'minimal', 'stacked', 'typographic', 'mosaic']; const GRID_LAYOUTS = ['masonry', 'uniform', 'showcase', 'filmstrip', 'editorial-flow']; +const ASPECT_RATIOS = ['1', '3/2', '2/3', '4/3', '16/9', 'auto']; + +type Tab = 'theme' | 'grid' | 'server'; // Font stacks for each preset (must match THEME_PRESETS in config.ts) const PRESET_FONTS: Record = { @@ -53,13 +59,17 @@ function setVar(name: string, value: string) { export function DevToolbar() { const [open, setOpen] = useState(false); + const [tab, setTab] = useState('theme'); const [preset, setPreset] = useState(''); const [frame, setFrame] = useState(''); const [theme, setTheme] = useState(''); const [grain, setGrain] = useState(''); + const [exifOnHover, setExifOnHover] = useState(true); const [copiedYaml, setCopiedYaml] = useState(''); const [gridCols, setGridCols] = useState(3); const [gridGap, setGridGap] = useState(12); + const [aspectRatio, setAspectRatio] = useState('1'); + const [gridLayout, setGridLayout] = useState('masonry'); // Read initial values from DOM useLayoutEffect(() => { @@ -71,8 +81,12 @@ export function DevToolbar() { const style = getComputedStyle(document.documentElement); const cols = parseInt(style.getPropertyValue('--grid-columns') || '3', 10); const gap = parseInt(style.getPropertyValue('--grid-gap') || '12', 10); + const ar = style.getPropertyValue('--grid-aspect-ratio').trim() || '1'; + const layout = getAttr('data-grid-layout') || 'masonry'; if (!isNaN(cols)) setGridCols(cols); if (!isNaN(gap)) setGridGap(gap); + setAspectRatio(ar); + setGridLayout(layout); }, []); const switchPreset = useCallback((p: string) => { @@ -140,6 +154,17 @@ export function DevToolbar() { }); }, []); + const updateAspectRatio = useCallback((ar: string) => { + setAspectRatio(ar); + document.querySelectorAll('.photo-grid').forEach((el) => { + el.style.setProperty('--grid-aspect-ratio', ar); + }); + }, []); + + const toggleExifOnHover = useCallback(() => { + setExifOnHover((prev) => !prev); + }, []); + const copyYaml = useCallback((section: string, yaml: string) => { navigator.clipboard.writeText(yaml); setCopiedYaml(section); @@ -147,9 +172,17 @@ export function DevToolbar() { }, []); const currentYaml = `theme: - preset: ${preset} - photoFrame: ${frame} - grain: ${grain === 'true'}`; + preset: "${preset}" + photoFrame: "${frame}" + grain: ${grain === 'true'} + +grid: + columns: ${gridCols} + gap: ${gridGap} + aspectRatio: "${aspectRatio}" + layout: "${gridLayout}" + +exifOnHover: ${exifOnHover}`; // Keyboard shortcut: Ctrl/Cmd + Shift + T useEffect(() => { @@ -165,20 +198,20 @@ export function DevToolbar() { return ( <> - {/* ── Floating Toggle Button ─────────────────────── */} + {/* ── Floating Toggle Button — bottom-right ── */} - {/* ── Panel ──────────────────────────────────────── */} + {/* ── Panel — anchored bottom-right ────────── */} {open && (
{/* ── Header ──────────────────────────── */}
- {/* ── Preset ──────────────────────────── */} -
- -
+ {/* ── Tab Bar ─────────────────────────── */} +
+ {(['theme', 'grid', 'server'] as Tab[]).map((t) => ( + + ))} +
- {/* ── Dark / Light ────────────────────── */} -
- -
+ {/* ── Tab Content ──────────────────────── */} +
- {/* ── Photo Frame ─────────────────────── */} -
- -
+ {/* ── THEME TAB ── */} + {tab === 'theme' && ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ )} - {/* ── Grain ───────────────────────────── */} -
- -
+ {/* ── GRID TAB ── */} + {tab === 'grid' && ( +
+
+ + updateGridCols(Number(e.target.value))} + style={{ width: '100%', accentColor: '#e4e4e7', marginTop: 6 }} + /> +
+
+ + updateGridGap(Number(e.target.value))} + style={{ width: '100%', accentColor: '#e4e4e7', marginTop: 6 }} + /> +
+
+ + +
+
+ )} - {/* ── Grid Controls ───────────────────── */} -
-
- - updateGridCols(Number(e.target.value))} - style={{ flex: 1, accentColor: '#a855f7' }} - /> -
-
- - updateGridGap(Number(e.target.value))} - style={{ flex: 1, accentColor: '#a855f7' }} - /> -
-
- - {/* ── Hero Style (info only) ──────────── */} -
- { - copyYaml('hero', `theme:\n heroStyle: ${h}`); - }} - /> - {copiedYaml === 'hero' && } -
- - {/* ── Grid Layout (info only) ─────────── */} -
- { - copyYaml('layout', `grid:\n layout: ${l}`); - }} - /> - {copiedYaml === 'layout' && } -
+ {/* ── SERVER TAB ── */} + {tab === 'server' && ( +
+
+ + copyYaml('hero', `theme:\n heroStyle: "${h}"`)} + /> + {copiedYaml === 'hero' && } +
+
+ + { + setGridLayout(l); + document.documentElement.setAttribute('data-grid-layout', l); + copyYaml('layout', `grid:\n layout: "${l}"`); + }} + /> + {copiedYaml === 'layout' && } +
+

+ These settings require a server re-render. Click to copy the YAML snippet, then paste into{' '} + content/settings.yaml. +

+
+ )} +
{/* ── Export YAML ──────────────────────── */}
@@ -342,12 +413,12 @@ export function DevToolbar() { onClick={() => copyYaml('all', currentYaml)} style={{ width: '100%', - padding: '10px 0', + padding: '9px 0', borderRadius: 10, border: 'none', background: - copiedYaml === 'all' ? '#22c55e' : 'linear-gradient(135deg, #a855f7, #6366f1)', - color: '#fff', + copiedYaml === 'all' ? '#16a34a' : 'rgba(255,255,255,0.92)', + color: copiedYaml === 'all' ? '#fff' : '#111', fontWeight: 600, fontSize: 12, cursor: 'pointer', @@ -366,28 +437,50 @@ export function DevToolbar() { /* ── Sub-components ───────────────────────────────── */ -function Section({ title, children }: { title: string; children: React.ReactNode }) { +function Label({ children }: { children: React.ReactNode }) { return ( -
-
- {title} -
+
{children}
); } +function ToggleButton({ on, onClick }: { on: boolean; onClick: () => void }) { + return ( + + ); +} + const pillStyle: React.CSSProperties = { - padding: '6px 12px', + padding: '5px 11px', borderRadius: 8, fontSize: 11, fontWeight: 500, @@ -406,7 +499,7 @@ function PillGrid({ onSelect: (item: string) => void; }) { return ( -
+
{items.map((item) => (