Tawny
All components

Navigation

Art Header

Sticky bilingual journal header with bold nav and locale switcher.

components/art/site-header

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>
  )
}