---
import MenuIcon from './MenuIcon.astro';
const base = import.meta.env.BASE_URL;
const currentPath = Astro.url.pathname;
const links = [
{ href: base, label: 'Overview' },
{ href: `${base}install/`, label: 'Install' },
{ href: `${base}docs/`, label: 'Docs' },
{ href: `${base}changelog/`, label: 'Changelog' },
{ href: `${base}compare/`, label: 'Compare' },
{ href: `${base}privacy/`, label: 'Privacy' },
{ href: `${base}terms/`, label: 'Terms' },
];
const isActive = (href: string) => {
if (href === base) {
return currentPath === base || currentPath === `${base}index.html`;
}
return currentPath.startsWith(href);
};
---
<header class="site-header navbar sticky top-0 z-40 h-auto min-h-16 border-b border-base-300 bg-base-100/88 backdrop-blur-md">
<div class="container header-inner flex w-full min-w-0 flex-wrap items-center justify-between gap-x-4 min-h-16 py-2.5">
<a class="brand inline-flex flex-none items-center gap-2.5 no-underline" href={base} aria-label="grr home">
<img src={`${base}logo.svg`} alt="" width="256" height="256" class="h-[30px] w-[30px]" />
<span class="brand-wordmark text-[19px] leading-6">grr</span>
</a>
<nav class="primary-nav hidden flex-1 md:flex" aria-label="Primary navigation">
{
links.map((link) => (
<a class:list={{ active: isActive(link.href) }} href={link.href} aria-current={isActive(link.href) ? 'page' : undefined}>
{link.label}
</a>
))
}
</nav>
<div class="flex flex-none items-center gap-2">
{/* The search entry point. The overlay itself is mounted once by
BaseLayout; the handler is document-delegated on [data-search-open],
so this trigger survives the ClientRouter's header swaps. Without JS
the button is hidden by CSS (html[data-search-ready]) and the
<noscript> link to the docs index is the fallback route in. */}
<div class="search" data-search-trigger>
<button
type="button"
class="search-trigger"
data-search-open
aria-label="Search the site"
aria-haspopup="dialog"
title="Search (press /)"
>
<span class="search-icon" aria-hidden="true"></span>
<span class="search-trigger-label" aria-hidden="true">Search</span>
<kbd class="search-trigger-kbd" aria-hidden="true">/</kbd>
</button>
<script is:inline data-astro-rerun>document.documentElement.dataset.searchReady = '';</script>
<noscript>
<a class="search-noscript" href={`${base}docs/`}>Docs index</a>
</noscript>
</div>
<button class="theme-toggle" type="button" data-theme-toggle aria-label="Toggle color theme" aria-pressed="false" title="Toggle color theme">
<svg class="icon-moon" viewBox="0 0 48 48" width="20" height="20" aria-hidden="true" focusable="false" shape-rendering="crispEdges">
<g fill="currentColor">
<rect x="24" y="4" width="16" height="4"></rect>
<rect x="16" y="8" width="24" height="4"></rect>
<rect x="12" y="12" width="20" height="4"></rect>
<rect x="12" y="16" width="16" height="4"></rect>
<rect x="8" y="20" width="16" height="12"></rect>
<rect x="12" y="32" width="16" height="4"></rect>
<rect x="12" y="36" width="20" height="4"></rect>
<rect x="16" y="40" width="24" height="4"></rect>
<rect x="24" y="44" width="16" height="4"></rect>
</g>
</svg>
<svg class="icon-sun" viewBox="0 0 48 48" width="20" height="20" aria-hidden="true" focusable="false" shape-rendering="crispEdges">
<g fill="currentColor">
<rect x="16" y="16" width="16" height="16"></rect>
<rect x="20" y="0" width="8" height="8"></rect>
<rect x="20" y="40" width="8" height="8"></rect>
<rect x="0" y="20" width="8" height="8"></rect>
<rect x="40" y="20" width="8" height="8"></rect>
<rect x="36" y="4" width="8" height="8"></rect>
<rect x="4" y="4" width="8" height="8"></rect>
<rect x="36" y="36" width="8" height="8"></rect>
<rect x="4" y="36" width="8" height="8"></rect>
</g>
</svg>
</button>
<a class="btn btn-primary btn-sm hidden items-center sm:inline-flex" href="https://github.com/debanjanbasu/grr-cli" target="_blank" rel="noreferrer">
GitHub <span aria-hidden="true">↗</span>
</a>
{/* DaisyUI opens this on :focus-within, so the trigger only has to be
focusable. It was a <div role="button">: a fake button is announced
as one but does nothing on Enter/Space, and the dropdown panel added
its own tabindex, so keyboard users landed on a non-interactive <ul>.
A real <button> is focusable and activatable natively, and the panel
no longer needs a tab stop of its own. */}
<div class="dropdown dropdown-end md:hidden" data-nav-disclosure>
<button
type="button"
class="btn btn-ghost btn-square min-h-12 min-w-12"
aria-label="Open the navigation menu"
aria-haspopup="menu"
aria-expanded="false"
aria-controls="mobile-nav-menu"
data-nav-toggle
>
<MenuIcon />
</button>
<nav id="mobile-nav-menu" aria-label="Mobile navigation">
<ul class="dropdown-content menu z-50 mt-2 w-56 border border-base-300 bg-base-200 p-2 shadow-lg">
{
links.map((link) => (
<li>
<a class:list={['min-h-12', { 'menu-active': isActive(link.href) }]} href={link.href} aria-current={isActive(link.href) ? 'page' : undefined}>
{link.label}
</a>
</li>
))
}
</ul>
</nav>
</div>
</div>
</div>
</header>
{/*
The mobile nav is a DaisyUI focus-within dropdown: focusing the trigger opens
it, and it closes when focus leaves the widget. aria-expanded has to mirror
that real state, so this delegated listener flips it on focus in and out of
the disclosure. It runs once per document (guarded) and is attached to the
document rather than the button, so the header ClientRouter swaps in after a
client-side navigation keeps working without re-binding.
*/}
<script is:inline>
(function () {
if (window.__grrMobileNavBound) return;
window.__grrMobileNavBound = true;
var setExpanded = function (widget, open) {
var toggle = widget.querySelector('[data-nav-toggle]');
if (toggle) toggle.setAttribute('aria-expanded', String(open));
};
document.addEventListener('focusin', function (event) {
var widget = event.target && event.target.closest ? event.target.closest('[data-nav-disclosure]') : null;
if (widget) setExpanded(widget, true);
});
document.addEventListener('focusout', function (event) {
var widget = event.target && event.target.closest ? event.target.closest('[data-nav-disclosure]') : null;
if (widget && !widget.contains(event.relatedTarget)) setExpanded(widget, false);
});
})();
</script>