ux(nav): unify navbar palette, localize, fix locale-aware metadata
Some checks failed
CI / Web — typecheck + lint + build (push) Failing after 36s
CI / Scripts — Python smoke (push) Failing after 5s
CI / Web — npm audit (push) Failing after 37s
CI / Retrieval — golden set (Recall@5 + MRR) (push) Failing after 5s

Visual audit found several issues in the navbar / header layer; this pass
addresses the cluster.

- AuthBar: localize sign in/out + dev-disabled message; switch matrix-green
  (#00ff9c) to gold (#e0c080) to match the magazine palette.
- NavLink (new): client component reading usePathname to mark the active
  section with aria-current="page" and a gold underline+fill, so the user
  knows where they are inside the entity hub.
- Locale toggle: bump font to 12px, widen the pill, lift the active state's
  contrast. Hard reload on switch (window.location.reload) instead of
  router.refresh — that is what makes the <title>, <html lang> and og:locale
  actually update on toggle (router.refresh kept stale layout metadata).
- Root layout: convert static `export const metadata` to generateMetadata()
  so title/description/OG/twitter all read the cookie per request. Drop the
  duplicate floating LocaleToggle at bottom-left; the navbar carries it now.
- BureauNav: drop the cyan-on-home / gold-on-bureau split (palette mismatch
  with SiteHeader); single gold tone; localize "home"/"case files" + aria
  labels; mount AuthBar inside the bar so sub-pages get a sign-in surface.
- Pages using BureauNav: drop the standalone <AuthBar /> below it (now
  duplicated). Affected: /bureau, /c/[slug], /h/[hypothesisId], /jobs/[id].
- Lift nav-link rest text from #9aa6b8 to #cbd2dd for WCAG-AA on near-black.

Verified live: title/lang/og:locale switch with cookie; active link gold
indicator on /sightings; "Entrar"/"Sign in" localizes; no double AuthBar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Luiz Gustavo 2026-05-28 17:50:10 -03:00
parent 912758e18a
commit 2ea350e283
10 changed files with 216 additions and 128 deletions

View file

@ -4,7 +4,6 @@
* Reader-facing list of every assembled narrative. No detective surfacing, * Reader-facing list of every assembled narrative. No detective surfacing,
* no artefact dumps. Just stories with hooks. * no artefact dumps. Just stories with hooks.
*/ */
import { AuthBar } from "@/components/auth-bar";
import { BureauNav } from "@/components/bureau-nav"; import { BureauNav } from "@/components/bureau-nav";
import { CaseLibrary } from "@/components/case-library"; import { CaseLibrary } from "@/components/case-library";
import { getLocale } from "@/components/locale-toggle"; import { getLocale } from "@/components/locale-toggle";
@ -18,7 +17,6 @@ export default async function BureauPage() {
return ( return (
<div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]"> <div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]">
<BureauNav locale={locale} crumbs={[{ label: locale === "en" ? "case files" : "casos" }]} /> <BureauNav locale={locale} crumbs={[{ label: locale === "en" ? "case files" : "casos" }]} />
<AuthBar />
<div className="mx-auto max-w-5xl px-4 py-6 pt-4"> <div className="mx-auto max-w-5xl px-4 py-6 pt-4">
<header className="mb-6 border-b border-[rgba(224,192,128,0.32)] pb-4"> <header className="mb-6 border-b border-[rgba(224,192,128,0.32)] pb-4">
<h1 className="font-mono text-3xl text-[#e0c080]"> <h1 className="font-mono text-3xl text-[#e0c080]">

View file

@ -10,7 +10,6 @@ import { readFile } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { MarkdownBody } from "@/components/markdown-body"; import { MarkdownBody } from "@/components/markdown-body";
import { AuthBar } from "@/components/auth-bar";
import { BureauNav } from "@/components/bureau-nav"; import { BureauNav } from "@/components/bureau-nav";
import { getLocale } from "@/components/locale-toggle"; import { getLocale } from "@/components/locale-toggle";
import { pickLocaleBody } from "@/lib/bilingual"; import { pickLocaleBody } from "@/lib/bilingual";
@ -156,7 +155,6 @@ export default async function CaseReportPage({
{ label: locale === "en" ? "case files" : "casos", href: "/bureau" }, { label: locale === "en" ? "case files" : "casos", href: "/bureau" },
{ label: title.length > 32 ? title.slice(0, 32) + "…" : title }, { label: title.length > 32 ? title.slice(0, 32) + "…" : title },
]} /> ]} />
<AuthBar />
{/* Full-bleed editorial hero only when a painterly illustration {/* Full-bleed editorial hero only when a painterly illustration
exists for this case. Other cases get the plain title header. */} exists for this case. Other cases get the plain title header. */}

View file

@ -16,7 +16,6 @@ import Link from "next/link";
import { readFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { pgQuery } from "@/lib/retrieval/db"; import { pgQuery } from "@/lib/retrieval/db";
import { AuthBar } from "@/components/auth-bar";
import { BureauNav } from "@/components/bureau-nav"; import { BureauNav } from "@/components/bureau-nav";
import { getLocale } from "@/components/locale-toggle"; import { getLocale } from "@/components/locale-toggle";
import { RedTeamRequestButton } from "@/components/red-team-request-button"; import { RedTeamRequestButton } from "@/components/red-team-request-button";
@ -204,7 +203,6 @@ export default async function HypothesisPage({
{ label: "hypotheses", href: "/bureau#hypotheses" }, { label: "hypotheses", href: "/bureau#hypotheses" },
{ label: hypothesisId }, { label: hypothesisId },
]} /> ]} />
<AuthBar />
<div className="mx-auto max-w-5xl px-4 py-6 pt-4"> <div className="mx-auto max-w-5xl px-4 py-6 pt-4">
{/* Header */} {/* Header */}

View file

@ -17,7 +17,6 @@
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { pgQuery } from "@/lib/retrieval/db"; import { pgQuery } from "@/lib/retrieval/db";
import { AuthBar } from "@/components/auth-bar";
import { BureauNav } from "@/components/bureau-nav"; import { BureauNav } from "@/components/bureau-nav";
import { getLocale } from "@/components/locale-toggle"; import { getLocale } from "@/components/locale-toggle";
import { JobStatusPoller } from "@/components/job-status-poller"; import { JobStatusPoller } from "@/components/job-status-poller";
@ -83,7 +82,6 @@ export default async function JobPage({
{ label: "jobs", href: "/bureau#jobs" }, { label: "jobs", href: "/bureau#jobs" },
{ label: job.job_id.slice(0, 8) }, { label: job.job_id.slice(0, 8) },
]} /> ]} />
<AuthBar />
<div className="mx-auto max-w-5xl px-4 py-6 pt-4"> <div className="mx-auto max-w-5xl px-4 py-6 pt-4">
<div className={`rounded-lg border border-[rgba(127,219,255,0.18)] bg-gradient-to-br ${detectiveBg} to-transparent p-5`}> <div className={`rounded-lg border border-[rgba(127,219,255,0.18)] bg-gradient-to-br ${detectiveBg} to-transparent p-5`}>

View file

@ -2,7 +2,7 @@ import type { Metadata } from "next";
import { JetBrains_Mono, Inter, Fraunces } from "next/font/google"; import { JetBrains_Mono, Inter, Fraunces } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { CommandPalette } from "@/components/command-palette"; import { CommandPalette } from "@/components/command-palette";
import { LocaleToggle, getLocale } from "@/components/locale-toggle"; import { getLocale } from "@/components/locale-toggle";
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
const mono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono" }); const mono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono" });
@ -16,67 +16,90 @@ const fraunces = Fraunces({
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://disclosure.top"; const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://disclosure.top";
export const metadata: Metadata = { // Site-wide metadata is locale-aware: title, description, OG, twitter, html
metadataBase: new URL(SITE_URL), // lang and openGraph.locale must all switch when the cookie toggles. Static
title: { // `export const metadata` is resolved without cookies, so the title would
default: "The Disclosure Bureau — UAP/UFO desclassificado", // stay pinned to whichever language built the page. generateMetadata() runs
template: "%s · The Disclosure Bureau", // per request and can read the cookie via getLocale().
}, export async function generateMetadata(): Promise<Metadata> {
description: const locale = await getLocale();
"122 documentos desclassificados do Departamento de Guerra dos EUA sobre UAP/UFO. " + const t = locale === "en"
"Pilotos, oficiais e físicos relatam o que viram. Avistamentos, testemunhas, " + ? {
"objetos catalogados — arquivos abertos da divulgação.", title: "The Disclosure Bureau — declassified UAP/UFO",
keywords: [ template: "%s · The Disclosure Bureau",
"UAP", "UFO", "ovni", "desclassificado", "war.gov", "Pentagon", desc: "122 declassified US Department of War documents on UAP/UFO. " +
"Kenneth Arnold", "Mantell", "green fireballs", "Sandia", "Pilots, officers and physicists describe what they saw. Sightings, " +
"Project Blue Book", "Robertson Panel", "AATIP", "witnesses, catalogued craft — the open files of disclosure.",
"documentos desclassificados", "avistamento", "testemunha", og_desc: "122 declassified documents. Pilots, officers, physicists describe what they saw.",
"disclosure", "divulgação UFO", tw_desc: "Declassified UAP/UFO files, narrated from the public record.",
], ogLocale: "en_US",
authors: [{ name: "The Disclosure Bureau" }], altLocale: "pt_BR",
openGraph: { }
type: "website", : {
siteName: "The Disclosure Bureau", title: "The Disclosure Bureau — UAP/UFO desclassificado",
title: "The Disclosure Bureau — UAP/UFO desclassificado", template: "%s · The Disclosure Bureau",
description: desc: "122 documentos desclassificados do Departamento de Guerra dos EUA sobre UAP/UFO. " +
"122 documentos desclassificados. Pilotos, oficiais, físicos relatam o que viram.", "Pilotos, oficiais e físicos relatam o que viram. Avistamentos, testemunhas, " +
locale: "pt_BR", "objetos catalogados — arquivos abertos da divulgação.",
alternateLocale: ["en_US"], og_desc: "122 documentos desclassificados. Pilotos, oficiais, físicos relatam o que viram.",
url: SITE_URL, tw_desc: "Arquivos UAP/UFO desclassificados, narrados a partir do registro público.",
images: [{ ogLocale: "pt_BR",
url: `${SITE_URL}/api/static/processing/case-art/green-fireballs-narrative.png`, altLocale: "en_US",
width: 2000, height: 1125, };
alt: "The Disclosure Bureau — UAP/UFO desclassificado",
}], return {
}, metadataBase: new URL(SITE_URL),
twitter: { title: { default: t.title, template: t.template },
card: "summary_large_image", description: t.desc,
title: "The Disclosure Bureau", keywords: [
description: "Arquivos UAP/UFO desclassificados, narrados a partir do registro público.", "UAP", "UFO", "ovni", "desclassificado", "war.gov", "Pentagon",
images: [`${SITE_URL}/api/static/processing/case-art/green-fireballs-narrative.png`], "Kenneth Arnold", "Mantell", "green fireballs", "Sandia",
}, "Project Blue Book", "Robertson Panel", "AATIP",
alternates: { "documentos desclassificados", "avistamento", "testemunha",
canonical: "/", "disclosure", "divulgação UFO",
languages: { "pt-BR": "/", "en-US": "/" }, ],
}, authors: [{ name: "The Disclosure Bureau" }],
robots: { openGraph: {
index: true, type: "website",
follow: true, siteName: "The Disclosure Bureau",
googleBot: { title: t.title,
description: t.og_desc,
locale: t.ogLocale,
alternateLocale: [t.altLocale],
url: SITE_URL,
images: [{
url: `${SITE_URL}/api/static/processing/case-art/green-fireballs-narrative.png`,
width: 2000, height: 1125,
alt: t.title,
}],
},
twitter: {
card: "summary_large_image",
title: "The Disclosure Bureau",
description: t.tw_desc,
images: [`${SITE_URL}/api/static/processing/case-art/green-fireballs-narrative.png`],
},
alternates: {
canonical: "/",
languages: { "pt-BR": "/", "en-US": "/" },
},
robots: {
index: true, index: true,
follow: true, follow: true,
"max-image-preview": "large", googleBot: {
"max-snippet": -1, index: true,
follow: true,
"max-image-preview": "large",
"max-snippet": -1,
},
}, },
}, other: {
other: { "ai:purpose":
// GEO (Generative Engine Optimization) — explicit primary-source statement "Public, citation-linked archive of declassified UAP/UFO documents from the US Department of War. Each case file is grounded in primary-source memos with verbatim quotes and bbox-cropped imagery.",
// so retrieval-augmented assistants understand what the site is. "ai:license": "Documents are US Government works in the public domain; site narrative © Disclosure Bureau, CC-BY 4.0.",
"ai:purpose": },
"Public, citation-linked archive of declassified UAP/UFO documents from the US Department of War. Each case file is grounded in primary-source memos with verbatim quotes and bbox-cropped imagery.", };
"ai:license": "Documents are US Government works in the public domain; site narrative © Disclosure Bureau, CC-BY 4.0.", }
},
};
export default async function RootLayout({ children }: { children: React.ReactNode }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {
const locale = await getLocale(); const locale = await getLocale();
@ -105,9 +128,6 @@ export default async function RootLayout({ children }: { children: React.ReactNo
</a> </a>
{children} {children}
<CommandPalette /> <CommandPalette />
<div className="fixed bottom-3 left-3 z-40 opacity-70 hover:opacity-100 transition">
<LocaleToggle current={locale} />
</div>
</body> </body>
</html> </html>
); );

View file

@ -13,10 +13,26 @@ interface Profile {
daily_quota?: number; daily_quota?: number;
} }
export function AuthBar() { const COPY = {
"pt-br": {
sign_in: "Entrar",
sign_out: "Sair",
auth_disabled: "auth: desativada (dev)",
msgs_today: "mensagens hoje",
},
en: {
sign_in: "Sign in",
sign_out: "Sign out",
auth_disabled: "auth: disabled (dev)",
msgs_today: "msgs today",
},
} as const;
export function AuthBar({ locale = "pt-br" }: { locale?: "pt-br" | "en" }) {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
const [profile, setProfile] = useState<Profile | null>(null); const [profile, setProfile] = useState<Profile | null>(null);
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
const t = COPY[locale];
useEffect(() => { useEffect(() => {
if (!isSupabaseConfigured()) { if (!isSupabaseConfigured()) {
@ -45,7 +61,7 @@ export function AuthBar() {
if (!isSupabaseConfigured()) { if (!isSupabaseConfigured()) {
return ( return (
<div className="font-mono text-[10px] text-[#5a6678] uppercase tracking-widest"> <div className="font-mono text-[10px] text-[#5a6678] uppercase tracking-widest">
auth: disabled (dev) {t.auth_disabled}
</div> </div>
); );
} }
@ -54,10 +70,10 @@ export function AuthBar() {
return ( return (
<Link <Link
href="/auth/signin" href="/auth/signin"
className="font-mono text-xs px-3 py-1 border border-[rgba(0,255,156,0.32)] className="font-mono text-[12px] px-3 py-1 border border-[rgba(224,192,128,0.35)]
text-[#00ff9c] hover:bg-[rgba(0,255,156,0.08)] rounded" text-[#e0c080] hover:bg-[rgba(224,192,128,0.08)] rounded-full transition-colors"
> >
sign in {t.sign_in}
</Link> </Link>
); );
} }
@ -69,17 +85,17 @@ export function AuthBar() {
<div>{profile.display_name ?? user.email}</div> <div>{profile.display_name ?? user.email}</div>
<div> <div>
${(profile.total_cost_usd ?? 0).toFixed(2)} / ${(profile.budget_cap_usd ?? 0).toFixed(2)} ·{" "} ${(profile.total_cost_usd ?? 0).toFixed(2)} / ${(profile.budget_cap_usd ?? 0).toFixed(2)} ·{" "}
{profile.daily_used ?? 0}/{profile.daily_quota ?? 0} msgs today {profile.daily_used ?? 0}/{profile.daily_quota ?? 0} {t.msgs_today}
</div> </div>
</div> </div>
)} )}
<form action="/auth/signout" method="POST"> <form action="/auth/signout" method="POST">
<button <button
type="submit" type="submit"
className="font-mono text-[10px] px-2 py-1 text-[#8896aa] hover:text-[#ff3344] className="font-mono text-[11px] px-2.5 py-1 text-[#9aa6b8] hover:text-[#ff3344]
border border-transparent hover:border-[#ff3344] rounded" border border-transparent hover:border-[#ff3344] rounded-full transition-colors"
> >
sign out {t.sign_out}
</button> </button>
</form> </form>
</div> </div>

View file

@ -1,16 +1,18 @@
/** /**
* BureauNav top navigation bar present on every bureau page. * BureauNav top navigation bar present on every bureau sub-page.
* *
* Solves "I'm stuck on this page, there's no way back" UX. Shows: * Solves "I'm stuck on this page, there's no way back" UX. Shows:
* - "← Home" (always /) * - "← Home" (always /)
* - "🔎 Bureau" (always /bureau) * - "Bureau" (always /bureau)
* - Breadcrumb trail of current page (passed via props) * - Breadcrumb trail of current page (passed via props)
* - Locale toggle + auth pill on the right
* *
* Server component. No client-side history needed because the parent links * Visually harmonised with SiteHeader (single gold palette, no cyan); labels
* are deterministic (every bureau page has a known parent in the hierarchy). * localised.
*/ */
import Link from "next/link"; import Link from "next/link";
import { ArrowLeft, Home } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { AuthBar } from "./auth-bar";
import { LocaleToggle, type Locale } from "./locale-toggle"; import { LocaleToggle, type Locale } from "./locale-toggle";
export interface Crumb { export interface Crumb {
@ -20,49 +22,56 @@ export interface Crumb {
href?: string; href?: string;
} }
export function BureauNav({ crumbs, locale }: { crumbs: Crumb[]; locale?: Locale }) { const COPY = {
"pt-br": { home: "home", bureau: "casos", aria_home: "Voltar para a home", aria_bureau: "Listagem de casos" },
en: { home: "home", bureau: "case files", aria_home: "Back to home", aria_bureau: "Case file library" },
} as const;
export function BureauNav({ crumbs, locale = "pt-br" }: { crumbs: Crumb[]; locale?: Locale }) {
const t = COPY[locale];
return ( return (
<nav className="sticky top-0 z-30 bg-[#0a0e1a]/95 backdrop-blur border-b border-[rgba(224,192,128,0.18)]"> <nav
<div className="mx-auto max-w-6xl px-4 py-2 flex items-center gap-3 flex-wrap text-[12px] font-mono"> aria-label={locale === "en" ? "Breadcrumb" : "Trilha"}
className="sticky top-0 z-30 bg-[#0a0e1a]/95 backdrop-blur border-b border-[rgba(224,192,128,0.18)]"
>
<div className="mx-auto max-w-6xl px-4 py-2.5 flex items-center gap-3 flex-wrap text-[12px] font-mono">
<Link <Link
href="/" href="/"
className="inline-flex items-center gap-1 px-2 py-1 rounded border border-[rgba(127,219,255,0.30)] text-[#7fdbff] hover:bg-[rgba(127,219,255,0.08)]" className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border border-[rgba(224,192,128,0.30)] text-[#cbd2dd] hover:text-[#e0c080] hover:bg-[rgba(224,192,128,0.08)] transition-colors"
aria-label="Voltar para a home" aria-label={t.aria_home}
> >
<ArrowLeft size={12} /> <ArrowLeft size={12} aria-hidden="true" />
<Home size={12} /> <span>{t.home}</span>
<span>home</span>
</Link> </Link>
<Link <Link
href="/bureau" href="/bureau"
className="inline-flex items-center gap-1 px-2 py-1 rounded border border-[#e0c080] text-[#e0c080] hover:bg-[rgba(224,192,128,0.08)]" className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border border-[rgba(224,192,128,0.50)] text-[#e0c080] hover:bg-[rgba(224,192,128,0.08)] transition-colors"
aria-label="Hub do bureau" aria-label={t.aria_bureau}
> >
🔎 <span>bureau</span> <span>{t.bureau}</span>
</Link> </Link>
{crumbs.length > 0 && ( {crumbs.length > 0 && (
<div className="text-[11px] text-[#5a6678] flex items-center gap-1.5 flex-wrap"> <div className="text-[11px] text-[#5a6678] flex items-center gap-1.5 flex-wrap">
<span className="mx-1">/</span> <span className="mx-1" aria-hidden="true">/</span>
{crumbs.map((c, i) => ( {crumbs.map((c, i) => (
<span key={`${c.label}-${i}`} className="inline-flex items-center gap-1.5"> <span key={`${c.label}-${i}`} className="inline-flex items-center gap-1.5">
{c.href ? ( {c.href ? (
<Link href={c.href} className="hover:text-[#7fdbff]">{c.label}</Link> <Link href={c.href} className="hover:text-[#e0c080] transition-colors">{c.label}</Link>
) : ( ) : (
<span className="text-[#e7ecf3]">{c.label}</span> <span className="text-[#e7ecf3]" aria-current="page">{c.label}</span>
)} )}
{i < crumbs.length - 1 && <span>/</span>} {i < crumbs.length - 1 && <span aria-hidden="true">/</span>}
</span> </span>
))} ))}
</div> </div>
)} )}
{locale && ( <span className="ml-auto inline-flex items-center gap-2">
<span className="ml-auto"> <LocaleToggle current={locale} variant="navbar" />
<LocaleToggle current={locale} variant="navbar" /> <AuthBar locale={locale} />
</span> </span>
)}
</div> </div>
</nav> </nav>
); );

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import { useRouter } from "next/navigation";
import { Globe } from "lucide-react"; import { Globe } from "lucide-react";
import type { Locale } from "./locale-toggle"; import type { Locale } from "./locale-toggle";
@ -8,30 +7,54 @@ export function LocaleToggleClient({
current, current,
variant = "retro", variant = "retro",
}: { current: Locale; variant?: "retro" | "navbar" }) { }: { current: Locale; variant?: "retro" | "navbar" }) {
const router = useRouter();
function setLocale(next: Locale) { function setLocale(next: Locale) {
if (next === current) return; if (next === current) return;
document.cookie = `locale=${next}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`; document.cookie = `locale=${next}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`;
router.refresh(); // Hard reload, not router.refresh(): the latter re-fetches the route's
// server tree but Next.js can keep a stale <title>/<meta> because metadata
// is rendered up in the layout tree which isn't always re-invalidated.
// A full navigation re-runs generateMetadata against the new cookie, so
// the tab title, OG tags, and html lang attribute all switch together.
window.location.reload();
} }
if (variant === "navbar") { if (variant === "navbar") {
// Gold-amber magazine theme to match SiteHeader / BureauNav. // Gold-amber magazine theme to match SiteHeader / BureauNav.
// The active locale fills with gold; the inactive one stays muted text.
// Sized to be unmistakably readable in the navbar — the previous 11px pill
// disappeared visually next to the other links.
return ( return (
<div className="inline-flex items-center gap-0.5 font-mono text-[11px] uppercase tracking-wider border border-[rgba(224,192,128,0.30)] rounded-full overflow-hidden"> <div
<span className="pl-2.5 pr-1 text-[#5a6678]"><Globe size={11} /></span> role="group"
aria-label="Language"
className="inline-flex items-center font-mono text-[12px] uppercase tracking-wider border border-[rgba(224,192,128,0.35)] rounded-full overflow-hidden"
>
<span className="pl-2.5 pr-1.5 py-1 text-[#9aa6b8]" aria-hidden="true">
<Globe size={12} />
</span>
<button <button
type="button"
onClick={() => setLocale("pt-br")} onClick={() => setLocale("pt-br")}
aria-pressed={current === "pt-br"} aria-pressed={current === "pt-br"}
className={`px-2 py-1 transition-colors ${current === "pt-br" ? "bg-[#e0c080] text-[#0a0e1a] font-semibold" : "text-[#9aa6b8] hover:text-[#e0c080]"}`} aria-label="Português (Brasil)"
className={`px-2.5 py-1 transition-colors ${
current === "pt-br"
? "bg-[#e0c080] text-[#0a0e1a] font-semibold"
: "text-[#cbd2dd] hover:text-[#e0c080] hover:bg-[rgba(224,192,128,0.08)]"
}`}
> >
PT PT
</button> </button>
<button <button
type="button"
onClick={() => setLocale("en")} onClick={() => setLocale("en")}
aria-pressed={current === "en"} aria-pressed={current === "en"}
className={`px-2 py-1 transition-colors ${current === "en" ? "bg-[#e0c080] text-[#0a0e1a] font-semibold" : "text-[#9aa6b8] hover:text-[#e0c080]"}`} aria-label="English"
className={`px-2.5 py-1 transition-colors ${
current === "en"
? "bg-[#e0c080] text-[#0a0e1a] font-semibold"
: "text-[#cbd2dd] hover:text-[#e0c080] hover:bg-[rgba(224,192,128,0.08)]"
}`}
> >
EN EN
</button> </button>
@ -43,12 +66,14 @@ export function LocaleToggleClient({
<div className="inline-flex items-center gap-1 font-mono text-[10px] uppercase tracking-widest border border-[rgba(0,255,156,0.32)] rounded"> <div className="inline-flex items-center gap-1 font-mono text-[10px] uppercase tracking-widest border border-[rgba(0,255,156,0.32)] rounded">
<span className="px-2 py-1 text-[#5a6678]"><Globe size={11} /></span> <span className="px-2 py-1 text-[#5a6678]"><Globe size={11} /></span>
<button <button
type="button"
onClick={() => setLocale("en")} onClick={() => setLocale("en")}
className={`px-2 py-1 ${current === "en" ? "bg-[rgba(0,255,156,0.12)] text-[#00ff9c]" : "text-[#8896aa] hover:text-[#7fdbff]"}`} className={`px-2 py-1 ${current === "en" ? "bg-[rgba(0,255,156,0.12)] text-[#00ff9c]" : "text-[#8896aa] hover:text-[#7fdbff]"}`}
> >
EN EN
</button> </button>
<button <button
type="button"
onClick={() => setLocale("pt-br")} onClick={() => setLocale("pt-br")}
className={`px-2 py-1 ${current === "pt-br" ? "bg-[rgba(0,255,156,0.12)] text-[#00ff9c]" : "text-[#8896aa] hover:text-[#7fdbff]"}`} className={`px-2 py-1 ${current === "pt-br" ? "bg-[rgba(0,255,156,0.12)] text-[#00ff9c]" : "text-[#8896aa] hover:text-[#7fdbff]"}`}
> >

View file

@ -0,0 +1,33 @@
"use client";
/**
* NavLink primary-nav link with an active-page indicator.
*
* Reads usePathname to decide whether this link points at the active route,
* then renders a visible gold underline plus a brighter text colour and
* sets `aria-current="page"` for screen readers. A link counts as active for
* its exact path AND for descendant paths (e.g. /sightings is active when
* on /sightings/EV-1947-), so deep pages still highlight their section.
*/
import Link from "next/link";
import { usePathname } from "next/navigation";
export function NavLink({ href, label }: { href: string; label: string }) {
const pathname = usePathname() || "/";
const isActive =
pathname === href || (href !== "/" && pathname.startsWith(`${href}/`));
return (
<Link
href={href}
aria-current={isActive ? "page" : undefined}
className={
isActive
? "px-2.5 py-1.5 rounded text-[#e0c080] bg-[rgba(224,192,128,0.10)] border-b-2 border-[#e0c080] -mb-[2px] transition-colors"
: "px-2.5 py-1.5 rounded text-[#cbd2dd] hover:text-[#e0c080] hover:bg-[rgba(224,192,128,0.06)] border-b-2 border-transparent -mb-[2px] transition-colors"
}
>
{label}
</Link>
);
}

View file

@ -7,19 +7,23 @@
import Link from "next/link"; import Link from "next/link";
import { AuthBar } from "./auth-bar"; import { AuthBar } from "./auth-bar";
import { LocaleToggle } from "./locale-toggle"; import { LocaleToggle } from "./locale-toggle";
import { NavLink } from "./nav-link";
export function SiteHeader({ locale }: { locale: "pt-br" | "en" }) { export function SiteHeader({ locale }: { locale: "pt-br" | "en" }) {
return ( return (
<header className="border-b border-[rgba(224,192,128,0.15)]"> <header className="border-b border-[rgba(224,192,128,0.15)]">
<div className="mx-auto max-w-7xl px-4 md:px-8 py-4 flex items-center justify-between gap-4 flex-wrap"> <div className="mx-auto max-w-7xl px-4 md:px-8 py-4 flex items-center justify-between gap-4 flex-wrap">
<Link href="/" className="flex items-center gap-2 group"> <Link href="/" className="flex items-center gap-2 group" aria-label="The Disclosure Bureau — home">
<span className="text-[#e0c080] text-lg"></span> <span className="text-[#e0c080] text-lg" aria-hidden="true"></span>
<span className="font-display text-xl text-[#e7ecf3] group-hover:text-[#e0c080] transition-colors"> <span className="font-display text-xl text-[#e7ecf3] group-hover:text-[#e0c080] transition-colors">
The Disclosure Bureau The Disclosure Bureau
</span> </span>
</Link> </Link>
<nav className="flex items-center gap-1 text-[12px] font-mono"> <nav
aria-label={locale === "en" ? "Primary" : "Principal"}
className="flex items-center gap-1 text-[13px] font-mono"
>
<NavLink href="/sightings" label={locale === "en" ? "Sightings" : "Avistamentos"} /> <NavLink href="/sightings" label={locale === "en" ? "Sightings" : "Avistamentos"} />
<NavLink href="/witnesses" label={locale === "en" ? "Witnesses" : "Testemunhas"} /> <NavLink href="/witnesses" label={locale === "en" ? "Witnesses" : "Testemunhas"} />
<NavLink href="/objects" label={locale === "en" ? "Craft" : "Objetos"} /> <NavLink href="/objects" label={locale === "en" ? "Craft" : "Objetos"} />
@ -27,21 +31,10 @@ export function SiteHeader({ locale }: { locale: "pt-br" | "en" }) {
<NavLink href="/operations" label={locale === "en" ? "Programs" : "Programas"} /> <NavLink href="/operations" label={locale === "en" ? "Programs" : "Programas"} />
<NavLink href="/bureau" label={locale === "en" ? "Case files" : "Casos"} /> <NavLink href="/bureau" label={locale === "en" ? "Case files" : "Casos"} />
<NavLink href="/search" label={locale === "en" ? "Search" : "Busca"} /> <NavLink href="/search" label={locale === "en" ? "Search" : "Busca"} />
<span className="ml-1"><LocaleToggle current={locale} variant="navbar" /></span> <span className="ml-2"><LocaleToggle current={locale} variant="navbar" /></span>
<span className="ml-1"><AuthBar /></span> <span className="ml-2"><AuthBar locale={locale} /></span>
</nav> </nav>
</div> </div>
</header> </header>
); );
} }
function NavLink({ href, label }: { href: string; label: string }) {
return (
<Link
href={href}
className="px-2.5 py-1.5 rounded text-[#9aa6b8] hover:text-[#e0c080] hover:bg-[rgba(224,192,128,0.06)] transition-colors"
>
{label}
</Link>
);
}