Build the Astro site UI
This commit is contained in:
parent
e5a219499e
commit
f27e9ec3fd
84 changed files with 3510 additions and 1949 deletions
128
src/components/Header.astro
Normal file
128
src/components/Header.astro
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue