seedproject-web/app/src/components/Header.astro
Carlos Arias 1559ce017d chore: scaffold SeedProject base (Phase 1)
Clean-room copy of the reusable engines from comiida, with all
instance data, secrets, dependencies, and build output excluded:
- app/         Astro theme skeleton (no comiida blog posts; hero image -> placeholder)
- api/         SeedProject PHP framework (no vendor/.env/config.php)
- content-pipeline/  engine only (scripts/admin/prompts; empty runtime state)
- astroagent.config.json + app/.astroagent/skills

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYHWLHihq3v9nxNwoPCKSn
2026-07-04 22:53:10 +00:00

200 lines
6.7 KiB
Text

---
import Icon from "./Icon.astro";
import { SITE } from "../lib/blog-data.js";
const nav = [
{ to: "/", label: "Home" },
{ to: "/blog", label: "Writing" },
{ to: "/about", label: "About" },
{ to: "/services", label: "Services" },
{ to: "/contact", label: "Contact" },
];
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="flex items-center gap-2">
<span class={`font-serif text-xl font-semibold tracking-tight ${isHome ? "home-header-brand" : ""}`}>{SITE.name}</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={`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>
<button
type="button"
data-theme-toggle
aria-label="Toggle dark mode"
class={iconClass}
>
<span data-theme-icon="moon"><Icon name="moon" class="h-4 w-4" /></span>
<span data-theme-icon="sun" hidden><Icon name="sun" class="h-4 w-4" /></span>
</button>
<button
type="button"
data-menu-toggle
aria-label="Menu"
aria-controls="mobile-menu"
aria-expanded="false"
class={`${iconClass} md:hidden`}
>
<span data-menu-icon="menu"><Icon name="menu" class="h-4 w-4" /></span>
<span data-menu-icon="x" hidden><Icon name="x" class="h-4 w-4" /></span>
</button>
</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>
<div id="mobile-menu" data-menu-panel class="border-t border-border/60 bg-background text-foreground md:hidden" hidden>
<nav class="mx-auto flex max-w-6xl flex-col gap-1 px-5 py-3" aria-label="Mobile navigation">
{
nav.map((item) => {
const exact = item.to === "/";
const active = exact ? current === "/" : current.startsWith(item.to);
return (
<a
href={item.to}
class={`rounded-md px-2 py-2 text-sm hover:bg-muted hover:text-foreground ${active ? "text-foreground" : "text-muted-foreground"}`}
aria-current={active ? "page" : undefined}
>
{item.label}
</a>
);
})
}
</nav>
</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 menuToggle = document.querySelector("[data-menu-toggle]");
const menuPanel = document.querySelector("[data-menu-panel]");
const menuIcon = document.querySelector('[data-menu-icon="menu"]');
const closeIcon = document.querySelector('[data-menu-icon="x"]');
const themeToggle = document.querySelector("[data-theme-toggle]");
const moonIcon = document.querySelector('[data-theme-icon="moon"]');
const sunIcon = document.querySelector('[data-theme-icon="sun"]');
const homeHeader = document.querySelector("[data-home-header]");
const setPanelOpen = () => {
if (!homeHeader) return;
const open = !searchPanel.hidden || !menuPanel.hidden;
homeHeader.dataset.panelOpen = String(open);
};
const setMenu = (open) => {
menuPanel.hidden = !open;
menuIcon.hidden = open;
closeIcon.hidden = !open;
menuToggle?.setAttribute("aria-expanded", String(open));
setPanelOpen();
};
const setSearch = (open) => {
searchPanel.hidden = !open;
searchToggle?.setAttribute("aria-expanded", String(open));
if (open) searchInput?.focus();
setPanelOpen();
};
const setThemeIcon = () => {
const dark = document.documentElement.classList.contains("dark");
moonIcon.hidden = dark;
sunIcon.hidden = !dark;
};
setThemeIcon();
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);
});
menuToggle?.addEventListener("click", () => setMenu(menuPanel.hidden));
document.addEventListener("keydown", (event) => {
if (event.key !== "Escape") return;
if (!searchPanel.hidden) {
setSearch(false);
searchToggle?.focus();
}
if (!menuPanel.hidden) {
setMenu(false);
menuToggle?.focus();
}
});
themeToggle?.addEventListener("click", () => {
const next = !document.documentElement.classList.contains("dark");
document.documentElement.classList.toggle("dark", next);
localStorage.setItem("theme", next ? "dark" : "light");
setThemeIcon();
});
</script>