All components
Navigation
Art Header
Sticky bilingual journal header with bold nav and locale switcher.
components/art/site-header
Hugo Lin
Source
Copy the files below into your project.
apps/web/components/art/site-header.tsx
import { getTranslations } from "next-intl/server";
import { Link } from "@/lib/art/navigation";
import { LocaleSwitcher } from "@/components/art/locale-switcher";
import { MobileNav } from "@/components/art/mobile-nav";
import { NavLinks } from "@/components/art/nav-links";
export async function SiteHeader() {
const tr = await getTranslations();
return (
<header className="bg-art-bento-bg sticky top-0 z-50 pt-[max(0.75rem,env(safe-area-inset-top))]">
<div className="relative flex items-center justify-between gap-4 px-4 pb-3 md:px-6">
<Link
href="/"
className="flex items-center gap-2 font-bold tracking-tight"
>
<span className="text-art-bento-ink text-lg md:text-xl">{tr("brand")}</span>
</Link>
<MobileNav />
<nav className="hidden flex-wrap items-center justify-end gap-3 md:flex md:gap-6">
<ul className="flex flex-wrap items-center gap-3 text-sm font-bold md:gap-5 md:text-base">
<NavLinks variant="desktop" />
</ul>
<LocaleSwitcher />
</nav>
</div>
</header>
);
}apps/web/components/art/nav-links.tsx
"use client";
import { useTranslations } from "next-intl";
import { Link } from "@/lib/art/navigation";
import { NAV_ITEMS } from "@/lib/art/nav";
type Props = {
variant: "desktop" | "mobile";
onNavigate?: () => void;
};
const linkClass = {
desktop: "text-art-bento-ink hover:underline",
mobile: "text-art-bento-ink flex min-h-11 items-center hover:underline",
} as const;
export function NavLinks({ variant, onNavigate }: Props) {
const t = useTranslations("nav");
const className = linkClass[variant];
return (
<>
{NAV_ITEMS.map((item) => (
<li key={item.labelKey}>
{item.kind === "internal" ? (
<Link href={item.href} onClick={onNavigate} className={className}>
{t(item.labelKey)}
</Link>
) : (
<a
href={item.href}
onClick={onNavigate}
className={className}
target="_blank"
rel="noopener noreferrer"
>
{t(item.labelKey)}
</a>
)}
</li>
))}
</>
);
}apps/web/components/art/locale-switcher.tsx
'use client'
import NextLink from 'next/link'
import { usePathname as useNextPathname } from 'next/navigation'
import clsx from 'clsx'
import { artHref, stripArtLocalePrefix } from '@/lib/art/navigation'
import {
ART_MOUNTS,
artBaseFromPathname,
artLocales,
type ArtLocale,
} from '@/lib/art/routing'
function localeFromPath(pathname: string): ArtLocale {
for (const base of ART_MOUNTS) {
const match = pathname.match(new RegExp(`^${base}/(en|zh)(?:/|$)`))
if (match?.[1] === 'zh') return 'zh'
if (match?.[1] === 'en') return 'en'
}
return 'en'
}
export function LocaleSwitcher() {
const fullPath = useNextPathname()
const pathname = stripArtLocalePrefix(fullPath)
const locale = localeFromPath(fullPath)
const base = artBaseFromPathname(fullPath)
return (
<div
className="border-art-bento-ink flex gap-0 border-2 text-xs font-bold uppercase"
role="group"
aria-label="Language"
>
{artLocales.map((loc) => (
<NextLink
key={loc}
href={artHref(loc, pathname, base)}
hrefLang={loc === 'zh' ? 'zh-Hant' : 'en'}
className={clsx(
'px-2 py-1 transition-colors',
locale === loc
? 'bg-art-bento-ink text-art-bento-bg'
: 'hover:bg-art-bento-ink/10 text-art-bento-ink',
)}
>
{loc === 'zh' ? '中' : 'EN'}
</NextLink>
))}
</div>
)
}