seedproject-web/app/src/components/Header.astro
2026-07-24 00:14:28 +00:00

153 lines
5 KiB
Text

---
import Icon from "./Icon.astro";
import { ThemeToggle } from "@/components/ui/be-ui-theme-toggle";
import { CurvedMenu } from "@/components/ui/curved-menu";
import { SITE } from "../lib/blog-data.js";
const nav = [
{ to: "/services", label: "Services" },
{ to: "/projects", label: "My Projects" },
{ to: "/about", label: "About" },
{ to: "/resume", label: "Resume" },
{ to: "/contact", label: "Contact Me" },
{ to: "/zzz-queue-test", label: "Queue Test" },
];
const current = Astro.url.pathname.replace(/\/$/, "") || "/";
const isHome = current === "/";
const activeClass = isHome ? "home-header-link is-active" : "text-foreground";
const inactiveClass = isHome
? "home-header-link"
: "text-muted-foreground transition-colors hover:text-foreground";
const headerClass = isHome
? "sticky top-0 z-40 border-b border-transparent bg-transparent text-white"
: "sticky top-0 z-40 border-b border-border/60 bg-background/80 backdrop-blur-md";
const iconClass = isHome
? "home-header-action inline-flex h-9 w-9 items-center justify-center rounded-full transition-colors"
: "inline-flex h-9 w-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground";
---
<header class={headerClass} data-home-header={isHome ? "true" : undefined} data-scrolled="false">
<div class="relative mx-auto flex h-16 max-w-6xl items-center justify-between px-5">
<a href="/" class="ca-header-brand flex items-center gap-2.5" aria-label={SITE.name}>
<img
src="/brand/carlos-arias-signature.svg"
alt={SITE.name}
width="2077"
height="520"
class="ca-header-logo h-6 w-auto sm:h-7"
/>
<span class="ca-header-seal" aria-hidden="true">印</span>
</a>
<nav class="hidden items-center gap-8 md:flex" aria-label="Primary navigation">
{
nav.map((item) => {
const exact = item.to === "/";
const active = exact ? current === "/" : current.startsWith(item.to);
return (
<a
href={item.to}
class={`ca-nav-link text-sm ${active ? activeClass : inactiveClass}`}
aria-current={active ? "page" : undefined}
>
{item.label}
</a>
);
})
}
</nav>
<div class="flex items-center gap-1">
<button
type="button"
data-search-toggle
aria-label="Search"
aria-controls="site-search"
aria-expanded="false"
class={iconClass}
>
<Icon name="search" class="h-4 w-4" />
</button>
<a
href="/rss.xml"
aria-label="RSS feed"
class={`${iconClass} hidden sm:inline-flex`}
>
<Icon name="rss" class="h-4 w-4" />
</a>
<!-- React island. Only this component ships JS; the rest of the page
stays static. client:load so the icon is correct before paint. -->
<ThemeToggle
client:load
variant="circle-blur"
start="top-right"
className={iconClass}
iconClassName="h-4 w-4"
/>
<!-- Full-page mobile menu. client:media so desktop ships none of
this JS at all — the island only hydrates below the md breakpoint. -->
<CurvedMenu
client:media="(max-width: 767px)"
items={nav}
current={current}
triggerClassName={`${iconClass} md:hidden`}
/>
</div>
</div>
<div id="site-search" data-search-panel class="border-t border-border/60 bg-background text-foreground" hidden>
<form action="/blog" method="get" class="mx-auto max-w-6xl px-5 py-3">
<input
data-search-input
name="q"
aria-label="Search essays, field notes, interviews"
placeholder="Search essays, field notes, interviews..."
class="w-full border-0 bg-transparent py-2 font-serif text-lg outline-none placeholder:text-muted-foreground"
/>
</form>
</div>
</header>
<script>
const searchToggle = document.querySelector("[data-search-toggle]");
const searchPanel = document.querySelector("[data-search-panel]");
const searchInput = document.querySelector("[data-search-input]");
const homeHeader = document.querySelector("[data-home-header]");
const setPanelOpen = () => {
if (!homeHeader) return;
const open = !searchPanel.hidden;
homeHeader.dataset.panelOpen = String(open);
};
const setSearch = (open) => {
searchPanel.hidden = !open;
searchToggle?.setAttribute("aria-expanded", String(open));
if (open) searchInput?.focus();
setPanelOpen();
};
const setHeaderScrolled = () => {
if (!homeHeader) return;
homeHeader.dataset.scrolled = window.scrollY > 8 ? "true" : "false";
};
setHeaderScrolled();
window.addEventListener("scroll", setHeaderScrolled, { passive: true });
searchToggle?.addEventListener("click", () => {
setSearch(searchPanel.hidden);
});
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return;
if (!searchPanel.hidden) {
setSearch(false);
searchToggle?.focus();
}
});
</script>