128 lines
4 KiB
Text
128 lines
4 KiB
Text
---
|
|
import { navItems, site } from '../lib/site';
|
|
|
|
const currentPath = Astro.url.pathname;
|
|
const current =
|
|
currentPath === '/' || currentPath.endsWith('/') || /\.[^/]+$/.test(currentPath)
|
|
? currentPath
|
|
: `${currentPath}/`;
|
|
|
|
// Exact match for the current page; section match (descendant URLs) for
|
|
// ancestor links. `aria-current="page"` is reserved for the exact page,
|
|
// `"true"` indicates an ancestor section.
|
|
function currentState(href: string): 'page' | 'true' | undefined {
|
|
if (current === href) return 'page';
|
|
if (href !== '/' && current.startsWith(href)) return 'true';
|
|
return undefined;
|
|
}
|
|
|
|
// Header shows nav items except Home and footer-only entries. RSS lives as a
|
|
// dedicated icon link to the right of the nav.
|
|
const headerNavItems = navItems.filter((item) => item.href !== '/' && !item.footerOnly);
|
|
---
|
|
|
|
<a class="skip-link" href="#content">Skip to content</a>
|
|
<header class="site-header">
|
|
<a class="site-title" href="/" aria-current={currentState('/')}>{site.name}</a>
|
|
<div class="header-actions">
|
|
<nav class="site-nav" aria-label="Primary">
|
|
{
|
|
headerNavItems.map((item) => (
|
|
<a href={item.href} aria-current={currentState(item.href)}>
|
|
{item.label}
|
|
</a>
|
|
))
|
|
}
|
|
</nav>
|
|
<a class="rss-link" href="/rss.xml" aria-label="RSS feed">
|
|
<svg
|
|
class="rss-icon"
|
|
viewBox="0 0 24 24"
|
|
width="18"
|
|
height="18"
|
|
aria-hidden="true"
|
|
focusable="false"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
d="M6.18 17.82a2.18 2.18 0 1 1-4.36 0 2.18 2.18 0 0 1 4.36 0ZM2 9.86v3.13a8.97 8.97 0 0 1 9.01 9.01h3.13A12.1 12.1 0 0 0 2 9.86Zm0-5.86V7.1A14.92 14.92 0 0 1 16.9 22H20A17.9 17.9 0 0 0 2 4Z"
|
|
></path>
|
|
</svg>
|
|
<span class="sr-only">RSS feed</span>
|
|
</a>
|
|
<button
|
|
id="theme-switcher"
|
|
class="theme-switcher"
|
|
type="button"
|
|
aria-label="Dark theme"
|
|
aria-pressed="false"
|
|
>
|
|
<span class="sr-only">Toggle theme</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<script is:inline data-theme-script>
|
|
// Co-located with the button so the initial aria state is set as soon as the
|
|
// button parses, avoiding a flash of the wrong icon. The theme itself is
|
|
// already on <html> from theme-init.js in <head>.
|
|
(function () {
|
|
var root = document.documentElement;
|
|
var switcher = document.getElementById('theme-switcher');
|
|
if (!switcher) return;
|
|
|
|
// Keep in sync with --color-bg in global.css and theme-init.js.
|
|
var THEME_BG = { light: '#fbfaf7', dark: '#151514' };
|
|
var themeColorMetas = document.querySelectorAll('meta[name="theme-color"]');
|
|
|
|
function sync(theme) {
|
|
switcher.setAttribute('aria-pressed', String(theme === 'dark'));
|
|
switcher.setAttribute(
|
|
'title',
|
|
theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'
|
|
);
|
|
for (var i = 0; i < themeColorMetas.length; i += 1) {
|
|
themeColorMetas[i].setAttribute('content', THEME_BG[theme]);
|
|
}
|
|
}
|
|
sync(root.dataset.theme === 'dark' ? 'dark' : 'light');
|
|
|
|
var reduced = matchMedia('(prefers-reduced-motion: reduce)');
|
|
switcher.addEventListener('click', function () {
|
|
var next = root.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
try {
|
|
localStorage.setItem('theme', next);
|
|
} catch (e) {}
|
|
var run = function () {
|
|
root.dataset.theme = next;
|
|
root.style.colorScheme = next;
|
|
sync(next);
|
|
};
|
|
if (!reduced.matches && typeof document.startViewTransition === 'function') {
|
|
document.startViewTransition(run);
|
|
} else {
|
|
run();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<style>
|
|
.rss-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-block-size: 44px;
|
|
min-inline-size: 44px;
|
|
color: inherit;
|
|
line-height: 0;
|
|
transition: color 150ms ease;
|
|
}
|
|
.rss-link:hover,
|
|
.rss-link:focus-visible {
|
|
color: var(--color-link-hover);
|
|
}
|
|
.rss-icon {
|
|
display: block;
|
|
}
|
|
</style>
|