progress
This commit is contained in:
1 parent
49850fe485
commit
39bb0f480c
10 files changed
+248
-117
No files matched your search
+161
-52
@@ -17,89 +17,172 @@ const contactHref = `${home}#contact`;
|
||||
const path = Astro.url.pathname.replace(/\/$/, '') || '/';
|
||||
const isCurrent = (href: string) => href.split('#')[0] === path && !href.includes('#');
|
||||
---
|
||||
<header class="header" data-header>
|
||||
<div class="container header__inner">
|
||||
<a class="header__home" href={home} aria-label={ui.nav.homeLabel}><Wordmark inline /></a>
|
||||
<header class="header" data-header data-tone="paper">
|
||||
<div class="container header__shell">
|
||||
<div class="header__bar hud-frame">
|
||||
<a class="header__home" href={home} aria-label={ui.nav.homeLabel}>
|
||||
<Wordmark inline class="header__logo header__logo--paper" />
|
||||
<Wordmark inline light class="header__logo header__logo--night" />
|
||||
</a>
|
||||
|
||||
<nav class="header__nav" aria-label="Primary" id="primary-nav">
|
||||
<ul class="header__list" role="list">
|
||||
{items.map((item) => (
|
||||
<li><a href={item.href} aria-current={isCurrent(item.href) ? 'page' : undefined}>{item.label}</a></li>
|
||||
))}
|
||||
<li class="header__contact-mobile"><a href={contactHref}>{ui.nav.contact}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<nav class="header__nav hud-frame" aria-label="Primary" id="primary-nav">
|
||||
<ul class="header__list" role="list">
|
||||
{items.map((item) => {
|
||||
const current = isCurrent(item.href);
|
||||
return (
|
||||
<li><a class:list={[{ 'hud-corners': current }]} href={item.href} aria-current={current ? 'page' : undefined}>{item.label}</a></li>
|
||||
);
|
||||
})}
|
||||
<li class="header__contact-mobile"><a href={contactHref}>{ui.nav.contact}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<a class="button button--small header__contact" href={contactHref}>{ui.nav.contact}</a>
|
||||
<a class="header__contact" href={contactHref}>{ui.nav.contact}</a>
|
||||
|
||||
<button class="header__toggle" type="button" aria-expanded="false" aria-controls="primary-nav" data-nav-toggle>
|
||||
<span data-label-open>{ui.nav.menu}</span>
|
||||
<span data-label-close hidden>{ui.nav.close}</span>
|
||||
</button>
|
||||
<button class="header__toggle" type="button" aria-expanded="false" aria-controls="primary-nav" data-nav-toggle>
|
||||
<span data-label-open>{ui.nav.menu}</span>
|
||||
<span data-label-close hidden>{ui.nav.close}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
/* The header is a transparent sticky strip; the bar floats inside it with room on every side
|
||||
for the corner crosses. Only the bar takes pointer events, so the margin never blocks clicks. */
|
||||
.header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: var(--paper);
|
||||
border-bottom: var(--hairline) solid var(--line);
|
||||
pointer-events: none;
|
||||
}
|
||||
.header__inner {
|
||||
.header__shell {
|
||||
padding-block: var(--space-3);
|
||||
}
|
||||
.header__bar {
|
||||
pointer-events: auto;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
min-height: calc(var(--header-height) - 2 * var(--space-3));
|
||||
color: var(--ink-base);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--step-micro);
|
||||
font-weight: var(--weight-medium);
|
||||
line-height: 1;
|
||||
letter-spacing: var(--track-caps);
|
||||
text-transform: uppercase;
|
||||
transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
|
||||
}
|
||||
/* The glass sits on a pseudo-element rather than the bar itself: a backdrop-filter on the bar
|
||||
would make it a backdrop root, and the mobile panel inside it could then blur nothing. */
|
||||
.header__bar::before,
|
||||
.header__nav[data-open]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background: var(--glass-solid);
|
||||
transition: background-color var(--dur) var(--ease);
|
||||
}
|
||||
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
|
||||
.header__bar::before,
|
||||
.header__nav[data-open]::before {
|
||||
background: var(--glass);
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
|
||||
backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
|
||||
}
|
||||
}
|
||||
|
||||
/* Over a night section the bar takes the night tone. */
|
||||
.header[data-tone='night'] { --hud-line: var(--hud-line-on-night); }
|
||||
.header[data-tone='night'] .header__bar { color: var(--ink-on-night); }
|
||||
.header[data-tone='night'] .header__bar::before,
|
||||
.header[data-tone='night'] .header__nav[data-open]::before { background: var(--glass-solid-on-night); }
|
||||
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
|
||||
.header[data-tone='night'] .header__bar::before,
|
||||
.header[data-tone='night'] .header__nav[data-open]::before { background: var(--glass-on-night); }
|
||||
}
|
||||
|
||||
.header__home {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--gap);
|
||||
min-height: var(--header-height);
|
||||
}
|
||||
.header__home { text-decoration: none; }
|
||||
.header__nav { margin-left: auto; }
|
||||
.header__list {
|
||||
display: flex;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
.header__list a {
|
||||
padding-inline: var(--space-4);
|
||||
text-decoration: none;
|
||||
font-weight: var(--weight-regular);
|
||||
padding-block: var(--space-2);
|
||||
border-bottom: var(--hairline) solid transparent;
|
||||
transition: border-color var(--dur-fast) var(--ease);
|
||||
}
|
||||
.header__home :global(.header__logo--night),
|
||||
.header[data-tone='night'] .header__home :global(.header__logo--paper) { display: none; }
|
||||
.header[data-tone='night'] .header__home :global(.header__logo--night) { display: block; }
|
||||
|
||||
/* Cells: every item is a full-height cell, divided from its neighbour by one frame line. */
|
||||
.header__nav { margin-left: auto; display: flex; }
|
||||
/* On desktop the nav is just a run of cells inside the bar: no frame of its own. */
|
||||
.header__nav.hud-frame { border: 0; }
|
||||
.header__nav.hud-frame::after { content: none; }
|
||||
.header__list { display: flex; }
|
||||
.header__list li { display: flex; border-left: var(--line) solid var(--frame-color); }
|
||||
.header__list a,
|
||||
.header__contact,
|
||||
.header__toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline: var(--space-4);
|
||||
text-decoration: none;
|
||||
transition: background-color var(--dur-fast) var(--ease);
|
||||
}
|
||||
.header__list a:hover,
|
||||
.header__list a[aria-current='page'] { border-bottom-color: currentColor; }
|
||||
.header__contact { margin-left: var(--space-6); }
|
||||
.header__contact-mobile { display: none; }
|
||||
.header__toggle:hover { background: color-mix(in srgb, currentColor var(--tint-hover), transparent); }
|
||||
.header__list a[aria-current='page'] { background: color-mix(in srgb, currentColor var(--tint-active), transparent); }
|
||||
.header__contact {
|
||||
border-left: var(--line) solid var(--frame-color);
|
||||
background: var(--ink-base);
|
||||
color: var(--paper);
|
||||
}
|
||||
.header__contact:hover { background: var(--ink-muted-base); }
|
||||
.header[data-tone='night'] .header__contact { background: var(--paper); color: var(--night); }
|
||||
.header[data-tone='night'] .header__contact:hover { background: var(--paper-deep); }
|
||||
.header__list .header__contact-mobile { display: none; }
|
||||
.header__toggle {
|
||||
display: none;
|
||||
font-weight: var(--weight-medium);
|
||||
padding: var(--space-2) 0;
|
||||
border-left: var(--line) solid var(--frame-color);
|
||||
text-transform: inherit;
|
||||
letter-spacing: inherit;
|
||||
}
|
||||
|
||||
/* Focus ring drawn inside the cell, so it never collides with the frame lines. */
|
||||
.header__bar :focus-visible {
|
||||
outline-offset: calc(-1 * var(--space-1));
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* A panel left open across a resize to desktop must not paint a second glass layer. */
|
||||
@media (min-width: 48rem) {
|
||||
.header__nav[data-open]::before { content: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 47.99rem) {
|
||||
.header__contact { display: none; }
|
||||
.header__contact-mobile { display: block; }
|
||||
.header__toggle { display: inline-block; }
|
||||
.header__list .header__contact-mobile { display: flex; }
|
||||
.header__toggle { display: flex; margin-left: auto; }
|
||||
.header__nav {
|
||||
position: absolute;
|
||||
inset: 100% 0 auto 0;
|
||||
top: calc(100% + var(--space-3));
|
||||
left: calc(-1 * var(--line));
|
||||
right: calc(-1 * var(--line));
|
||||
display: none;
|
||||
padding: var(--space-5) var(--gutter) var(--space-7);
|
||||
background: var(--paper);
|
||||
border-bottom: var(--hairline) solid var(--line);
|
||||
}
|
||||
/* As the mobile panel it is a full .hud-frame: border and corner crosses. */
|
||||
.header__nav.hud-frame { border: var(--line) solid var(--frame-color); }
|
||||
.header__nav.hud-frame::after { content: ''; }
|
||||
.header__nav[data-open] { display: block; }
|
||||
.header__list { flex-direction: column; gap: 0; }
|
||||
.header__list li + li { border-top: var(--hairline) solid var(--line); }
|
||||
.header__list { flex-direction: column; }
|
||||
.header__list li { border-left: 0; }
|
||||
.header__list li + li { border-top: var(--line) solid var(--frame-color); }
|
||||
.header__list a {
|
||||
display: block;
|
||||
flex: 1;
|
||||
padding-block: var(--space-4);
|
||||
font-size: var(--step-h3);
|
||||
letter-spacing: var(--track-h3);
|
||||
border-bottom: 0;
|
||||
font-size: var(--step-small);
|
||||
}
|
||||
.header__list a[aria-current='page'] { font-weight: var(--weight-medium); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -120,4 +203,30 @@ const isCurrent = (href: string) => href.split('#')[0] === path && !href.include
|
||||
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') set(false); });
|
||||
nav.addEventListener('click', (e) => { if ((e.target as HTMLElement).closest('a')) set(false); });
|
||||
}
|
||||
|
||||
/* Tone: watch a one-pixel line through the middle of the bar and switch to night while a
|
||||
.night section crosses it. An IntersectionObserver, so nothing runs on scroll. */
|
||||
const sections = document.querySelectorAll<HTMLElement>('.night');
|
||||
if (header && sections.length && 'IntersectionObserver' in window) {
|
||||
const under = new Set<Element>();
|
||||
let observer: IntersectionObserver | undefined;
|
||||
const watch = () => {
|
||||
observer?.disconnect();
|
||||
under.clear();
|
||||
const mid = Math.round(header.offsetHeight / 2);
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) under.add(entry.target); else under.delete(entry.target);
|
||||
}
|
||||
header.dataset.tone = under.size ? 'night' : 'paper';
|
||||
}, { rootMargin: `-${mid}px 0px -${window.innerHeight - mid - 1}px 0px` });
|
||||
sections.forEach((section) => observer!.observe(section));
|
||||
};
|
||||
watch();
|
||||
let resize: number | undefined;
|
||||
window.addEventListener('resize', () => {
|
||||
window.clearTimeout(resize);
|
||||
resize = window.setTimeout(watch, 150);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
Reference in new issue
Block a user