Skip to content

Commit 768f44f

Browse files
committed
fix(ui): stabilize global header layering and first-paint behavior across locales
1 parent 24c4faa commit 768f44f

9 files changed

Lines changed: 134 additions & 74 deletions

File tree

src/app/[locale]/layout.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import {
99
import { Inter, JetBrains_Mono, Space_Grotesk } from 'next/font/google';
1010
import { notFound } from 'next/navigation';
1111
import { SiteFooter } from '@/components/common/site-footer';
12+
import {
13+
SiteHeaderShell,
14+
type HeaderNavItem,
15+
} from '@/components/common/site-header-shell';
1216
import '../globals.css';
1317

1418
const inter = Inter({
@@ -62,6 +66,16 @@ export default async function LocaleLayout({
6266

6367
setRequestLocale(locale);
6468
const messages = await getMessages();
69+
const tHeader = await getTranslations({ locale, namespace: 'Header' });
70+
71+
const header = {
72+
logoAlt: tHeader('header.logoAlt'),
73+
city: tHeader('header.city'),
74+
registerLabel: tHeader('header.registerLabel'),
75+
registerAria: tHeader('header.registerAria'),
76+
};
77+
78+
const nav = tHeader.raw('navigation') as HeaderNavItem[];
6579

6680
return (
6781
<html lang={locale}>
@@ -70,7 +84,8 @@ export default async function LocaleLayout({
7084
>
7185
<NextIntlClientProvider messages={messages}>
7286
<div className="flex flex-col min-h-screen">
73-
<main className="flex-grow">{children}</main>
87+
<SiteHeaderShell header={header} nav={nav} />
88+
<main className="relative z-0 flex-grow">{children}</main>
7489
<SiteFooter />
7590
</div>
7691
</NextIntlClientProvider>

src/app/[locale]/page.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,10 @@ export default async function Home({
1212
const { locale } = await params;
1313
setRequestLocale(locale);
1414

15-
const t_header = await getTranslations({ locale, namespace: 'Header' });
1615
const t_home = await getTranslations({ locale, namespace: 'Home' });
1716
const t_gallery = await getTranslations({ locale, namespace: 'Gallery' });
1817

1918
const copy: HomeLandingCopy = {
20-
header: {
21-
logoAlt: t_header('header.logoAlt'),
22-
city: t_header('header.city'),
23-
registerLabel: t_header('header.registerLabel'),
24-
registerAria: t_header('header.registerAria'),
25-
},
26-
nav: t_header.raw('navigation'),
2719
hero: {
2820
phase: t_home('hero.phase'),
2921
title: t_home('hero.title'),
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { MobileMenu } from '@/components/home/mobile-menu';
5+
import { SiteHeader } from '@/components/home/site-header';
6+
7+
export type HeaderCopy = {
8+
logoAlt: string;
9+
city: string;
10+
registerLabel: string;
11+
registerAria: string;
12+
};
13+
14+
export type HeaderNavItem = {
15+
label: string;
16+
href: string;
17+
};
18+
19+
type SiteHeaderShellProps = {
20+
header: HeaderCopy;
21+
nav: HeaderNavItem[];
22+
};
23+
24+
export function SiteHeaderShell({ header, nav }: SiteHeaderShellProps) {
25+
const [isMenuOpen, setIsMenuOpen] = useState(false);
26+
27+
const toggleMenu = () => {
28+
setIsMenuOpen((prev) => !prev);
29+
};
30+
31+
return (
32+
<>
33+
<SiteHeader
34+
header={header}
35+
nav={nav}
36+
isMenuOpen={isMenuOpen}
37+
toggleMenu={toggleMenu}
38+
/>
39+
<MobileMenu nav={nav} isOpen={isMenuOpen} toggleMenu={toggleMenu} />
40+
</>
41+
);
42+
}
43+

src/components/home/hero-ticker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ type HeroTickerProps = {
66

77
export function HeroTicker({ ticker }: HeroTickerProps) {
88
return (
9-
<div className="absolute bottom-0 z-50 w-full overflow-hidden border-t border-white/10 bg-white/[0.03] py-2">
9+
<div className="absolute bottom-0 z-40 w-full overflow-hidden border-t border-white/10 bg-white/[0.03] py-2">
1010
<div className="font-mono-tech animate-ticker flex w-[200%] whitespace-nowrap text-[8px] tracking-[0.5em] text-white/30 uppercase">
1111
<span className="px-12 text-google-blue">{ticker.item1}</span>
1212
<span className="px-12 text-google-red">{ticker.item2}</span>

src/components/home/home-landing.tsx

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
'use client';
22

33
import Image from 'next/image';
4-
import { useState } from 'react';
54
import { HeroBottomHud } from './hero-bottom-hud';
65
import { HeroContent } from './hero-content';
76
import { HeroTicker } from './hero-ticker';
8-
import { SiteHeader } from './site-header';
9-
import { MobileMenu } from './mobile-menu';
107
import GalleryGrid from '../gallery/gallery-grid';
118
import { SectionHeader } from '../common/section-header';
129
import type { HomeLandingCopy } from './types';
@@ -17,25 +14,8 @@ type HomeLandingProps = {
1714
};
1815

1916
export function HomeLanding({ copy }: HomeLandingProps) {
20-
const [isMenuOpen, setIsMenuOpen] = useState(false);
21-
const toggleMenu = () => {
22-
setIsMenuOpen(!isMenuOpen);
23-
};
24-
2517
return (
2618
<>
27-
<SiteHeader
28-
header={copy.header}
29-
nav={copy.nav}
30-
isMenuOpen={isMenuOpen}
31-
toggleMenu={toggleMenu}
32-
/>
33-
<MobileMenu
34-
nav={copy.nav}
35-
isOpen={isMenuOpen}
36-
toggleMenu={toggleMenu}
37-
/>
38-
3919
{/* Global Grid Background */}
4020
<div className="fixed inset-0 -z-10 bg-background">
4121
<div className="blueprint-subgrid absolute inset-0" />

src/components/home/locale-switcher.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function LocaleSwitcher() {
1313
href={pathname}
1414
locale={nextLocale}
1515
aria-label={`Switch language to ${nextLocale.toUpperCase()}`}
16-
className="group relative inline-flex items-center gap-2 overflow-hidden px-4 py-2.5 transition-all hover:bg-white/5"
16+
className="group relative inline-flex items-center gap-2 overflow-hidden rounded-sm border border-white/25 bg-black/20 px-4 py-2.5 backdrop-blur-sm transition-all hover:border-white/45 hover:bg-white/5"
1717
>
1818
<div className="quad-border-tr absolute inset-0 opacity-35" />
1919
<div className="quad-border-bl absolute inset-0 opacity-35" />

src/components/home/mobile-menu.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
import { motion, AnimatePresence } from 'framer-motion';
44
import { Link } from '@/i18n/navigation';
5-
import type { HomeLandingCopy } from './types';
65
import { useEffect } from 'react';
76

7+
type HeaderNavItem = {
8+
label: string;
9+
href: string;
10+
};
11+
812
type MobileMenuProps = {
9-
nav: HomeLandingCopy['nav'];
13+
nav: HeaderNavItem[];
1014
isOpen: boolean;
1115
toggleMenu: () => void;
1216
};
@@ -37,7 +41,7 @@ export function MobileMenu({ nav, isOpen, toggleMenu }: MobileMenuProps) {
3741
animate="visible"
3842
exit="hidden"
3943
variants={menuVariants}
40-
className="fixed top-[80px] right-0 bottom-0 z-[99] w-full bg-[#050505] py-6 px-4 flex flex-col overflow-y-auto md:hidden"
44+
className="fixed top-[80px] right-0 bottom-0 z-[110] flex w-full flex-col overflow-y-auto bg-[#050505] px-4 py-6 md:hidden"
4145
>
4246
<div className="flex-grow flex flex-col items-center gap-8 mt-0"> {/* Adjusted margin-top */}
4347
<motion.nav className="flex flex-col items-center gap-8">

src/components/home/site-header.tsx

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,64 +5,100 @@ import Image from 'next/image';
55
import { useState, useEffect } from 'react';
66
import { Menu, X } from 'lucide-react';
77
import { LocaleSwitcher } from './locale-switcher';
8-
import type { HomeLandingCopy } from './types';
8+
9+
type HeaderCopy = {
10+
logoAlt: string;
11+
city: string;
12+
registerLabel: string;
13+
registerAria: string;
14+
};
15+
16+
type HeaderNavItem = {
17+
label: string;
18+
href: string;
19+
};
920

1021
type SiteHeaderProps = {
11-
header: HomeLandingCopy['header'];
12-
nav: HomeLandingCopy['nav'];
22+
header: HeaderCopy;
23+
nav: HeaderNavItem[];
1324
isMenuOpen: boolean;
1425
toggleMenu: () => void;
1526
};
1627

1728
export function SiteHeader({ header, nav, isMenuOpen, toggleMenu }: SiteHeaderProps) {
18-
const [scrolled, setScrolled] = useState(false);
29+
const [scrolled, setScrolled] = useState(true);
1930

2031
useEffect(() => {
2132
const handleScroll = () => {
2233
setScrolled(window.scrollY > 20);
2334
};
35+
handleScroll();
2436
window.addEventListener('scroll', handleScroll);
2537
return () => window.removeEventListener('scroll', handleScroll);
2638
}, []);
2739

2840
return (
2941
<header
30-
className={`fixed top-0 left-0 z-50 flex w-full items-center justify-between px-5 py-6 md:px-10 md:py-8 transition-all duration-300 ${scrolled ? 'bg-[#050505]/90 backdrop-blur-md border-b border-white/5' : 'bg-transparent'}`}>
31-
<div className="flex items-center gap-3">
42+
className={`fixed top-0 left-0 z-[100] w-full border-b border-white/5 px-5 py-6 backdrop-blur-md transition-all duration-300 md:px-10 md:py-8 ${scrolled ? 'bg-[#050505]/90' : 'bg-[#050505]/55'}`}
43+
>
44+
<div className="flex w-full items-center justify-between md:hidden">
3245
<Link href="/">
3346
<div className="relative h-auto w-auto">
34-
<Image src="/assets/images/logos/devfest-2026.svg" width={220} height={28} alt={header.logoAlt}
35-
className="object-contain" />
47+
<Image
48+
src="/assets/images/logos/devfest-2026.svg"
49+
width={220}
50+
height={28}
51+
alt={header.logoAlt}
52+
className="h-auto w-[200px] object-contain"
53+
/>
3654
</div>
3755
</Link>
38-
</div>
39-
40-
<nav className="hidden items-center gap-10 md:flex">
41-
{nav.map((item, index) => {
42-
const colors = ['blue', 'red', 'yellow', 'green'];
43-
const colorClass = `hover:text-google-${colors[index % colors.length]}`;
44-
const opacityClass = index === 0 ? 'text-white' : 'text-white/50';
45-
46-
return (
47-
<Link
48-
key={item.href}
49-
className={`font-mono-tech text-[12px] tracking-[0.32em] ${opacityClass} ${colorClass} uppercase transition-colors`}
50-
href={item.href}
51-
>
52-
{item.label}
53-
</Link>
54-
);
55-
})}
56-
</nav>
5756

58-
<div className="flex items-center gap-6">
59-
<LocaleSwitcher />
60-
<div className="md:hidden">
57+
<div className="flex items-center gap-3">
58+
<LocaleSwitcher />
6159
<button onClick={toggleMenu} className="text-white">
6260
{isMenuOpen ? <X size={28} /> : <Menu size={28} />}
6361
</button>
6462
</div>
6563
</div>
64+
65+
<div className="hidden w-full items-center justify-between md:flex">
66+
<div className="flex items-center gap-3">
67+
<Link href="/">
68+
<div className="relative h-auto w-auto">
69+
<Image
70+
src="/assets/images/logos/devfest-2026.svg"
71+
width={220}
72+
height={28}
73+
alt={header.logoAlt}
74+
className="h-auto w-[220px] object-contain"
75+
/>
76+
</div>
77+
</Link>
78+
</div>
79+
80+
<nav className="flex items-center gap-10">
81+
{nav.map((item, index) => {
82+
const colors = ['blue', 'red', 'yellow', 'green'];
83+
const colorClass = `hover:text-google-${colors[index % colors.length]}`;
84+
const opacityClass = index === 0 ? 'text-white' : 'text-white/50';
85+
86+
return (
87+
<Link
88+
key={item.href}
89+
className={`font-mono-tech text-[12px] tracking-[0.32em] ${opacityClass} ${colorClass} uppercase transition-colors`}
90+
href={item.href}
91+
>
92+
{item.label}
93+
</Link>
94+
);
95+
})}
96+
</nav>
97+
98+
<div className="flex items-center gap-6">
99+
<LocaleSwitcher />
100+
</div>
101+
</div>
66102
</header>
67103
);
68104
}

src/components/home/types.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,4 @@
11
export type HomeLandingCopy = {
2-
header: {
3-
logoAlt: string;
4-
city: string;
5-
registerLabel: string;
6-
registerAria: string;
7-
};
8-
nav: {
9-
label: string;
10-
href: string;
11-
}[];
122
hero: {
133
phase: string;
144
title: string;

0 commit comments

Comments
 (0)