Files
manycoretech--aholo-viewer/website/src/components/site/Header.astro
T
2026-07-13 13:18:05 +08:00

204 lines
7.5 KiB
Plaintext

---
import LanguageSwitch from './LanguageSwitch.astro';
import ThemeToggle from './ThemeToggle.astro';
import { dictionary } from '../../i18n/dictionary.js';
import { localizedPath } from '../../i18n/routes.js';
import type { Locale } from '../../i18n/locales.js';
import { navItems } from '../../utils/navigation.js';
interface Props {
lang: Locale;
currentPath: string;
}
const { lang, currentPath } = Astro.props;
const t = dictionary[lang];
const githubUrl = 'https://github.com/manycoretech/aholo-viewer';
const hasDrawerContext = Astro.slots.has('default');
const drawerLabel = lang === 'zh-CN' ? '移动端导航' : 'Mobile navigation';
const openMenuLabel = lang === 'zh-CN' ? '打开导航' : 'Open navigation';
const closeMenuLabel = lang === 'zh-CN' ? '关闭导航' : 'Close navigation';
const navLinks = navItems.map(item => {
const href = localizedPath(lang, item.href);
return {
href,
active: item.href === '/' ? currentPath === href : currentPath.startsWith(href),
label: t.nav[item.key],
};
});
---
<header class="site-header">
<button
class="site-menu-toggle"
type="button"
aria-label={openMenuLabel}
aria-controls="site-mobile-drawer"
aria-expanded="false"
title={openMenuLabel}
data-open-label={openMenuLabel}
data-close-label={closeMenuLabel}
data-site-menu-toggle
>
<span class="site-menu-icon" aria-hidden="true"></span>
</button>
<a class="brand" href={localizedPath(lang)} aria-label={t.siteName}>
<img class="brand-logo" src="/aholo-logo.svg" alt="" width="164" height="24" aria-hidden="true" />
</a>
<nav class="site-nav" aria-label="Primary navigation">
{
navLinks.map(item => (
<a class={item.active ? 'is-active' : ''} href={item.href}>
{item.label}
</a>
))
}
</nav>
<div class="site-actions">
<LanguageSwitch lang={lang} currentPath={currentPath} />
<ThemeToggle />
<a class="github-link" href={githubUrl} target="_blank" rel="noreferrer" aria-label="GitHub">
<svg
xmlns="http://www.w3.org/2000/svg"
height="16"
width="16"
viewBox="0 0 496 475"
class="icon-github"
aria-hidden="true"
>
<path
d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"
></path>
</svg>
</a>
</div>
</header>
<div class="site-mobile-drawer-shell" aria-hidden="true" inert data-site-drawer-shell>
<button class="site-drawer-backdrop" type="button" aria-label={closeMenuLabel} data-site-menu-close></button>
<aside id="site-mobile-drawer" class="site-mobile-drawer" aria-label={drawerLabel} tabindex="-1">
<header class="site-drawer-header">
<a class="site-drawer-brand" href={localizedPath(lang)} aria-label={t.siteName}>
<img class="brand-logo" src="/aholo-logo.svg" alt="" width="164" height="24" aria-hidden="true" />
</a>
<button
class="site-drawer-close"
type="button"
aria-label={closeMenuLabel}
title={closeMenuLabel}
data-site-menu-close
>
<span class="site-drawer-close-icon" aria-hidden="true"></span>
</button>
</header>
<nav class="site-drawer-nav" aria-label="Primary navigation">
{
navLinks.map(item => (
<>
<a class={item.active ? 'is-active' : ''} href={item.href}>
{item.label}
</a>
{hasDrawerContext && item.active && (
<div class="site-drawer-nav-context">
<slot />
</div>
)}
</>
))
}
</nav>
</aside>
</div>
<script>
const setupSiteMobileDrawer = () => {
const shell = document.querySelector<HTMLElement>('[data-site-drawer-shell]');
const toggle = document.querySelector<HTMLButtonElement>('[data-site-menu-toggle]');
if (!shell || !toggle || shell.dataset.siteDrawerReady === 'true') {
return;
}
const drawer = shell.querySelector<HTMLElement>('#site-mobile-drawer');
const closeButtons = Array.from(shell.querySelectorAll<HTMLButtonElement>('[data-site-menu-close]'));
const compactQuery = window.matchMedia('(max-width: 900px)');
const openLabel = toggle.dataset.openLabel ?? 'Open navigation';
const closeLabel = toggle.dataset.closeLabel ?? 'Close navigation';
const desktopFocusTarget = document.querySelector<HTMLElement>('.site-header .brand');
if (!drawer) {
return;
}
shell.dataset.siteDrawerReady = 'true';
const releaseDrawerFocus = () => {
if (!(document.activeElement instanceof HTMLElement) || !shell.contains(document.activeElement)) {
return;
}
const focusTarget = compactQuery.matches ? toggle : (desktopFocusTarget ?? toggle);
focusTarget.focus({ preventScroll: true });
if (document.activeElement instanceof HTMLElement && shell.contains(document.activeElement)) {
document.activeElement.blur();
}
};
const setOpen = (open: boolean) => {
if (!open) {
releaseDrawerFocus();
}
shell.toggleAttribute('inert', !open);
shell.setAttribute('aria-hidden', String(!open));
toggle.setAttribute('aria-expanded', String(open));
toggle.setAttribute('aria-label', open ? closeLabel : openLabel);
toggle.title = open ? closeLabel : openLabel;
if (open) {
document.documentElement.dataset.siteMenuOpen = 'true';
drawer.focus({ preventScroll: true });
return;
}
delete document.documentElement.dataset.siteMenuOpen;
};
toggle.addEventListener('click', () => {
setOpen(toggle.getAttribute('aria-expanded') !== 'true');
});
for (const closeButton of closeButtons) {
closeButton.addEventListener('click', () => setOpen(false));
}
drawer.addEventListener('click', event => {
const link = event.target instanceof Element ? event.target.closest('a') : null;
if (link) {
setOpen(false);
}
});
document.addEventListener('keydown', event => {
if (event.key === 'Escape' && toggle.getAttribute('aria-expanded') === 'true') {
setOpen(false);
}
});
compactQuery.addEventListener('change', event => {
if (!event.matches) {
setOpen(false);
}
});
document.addEventListener('astro:before-swap', () => setOpen(false), { once: true });
};
setupSiteMobileDrawer();
</script>