Files
bztmon-site/src/components/Nav.astro
T
2026-08-17 23:21:38 +10:00

152 lines
3.8 KiB
Plaintext

---
import ThemeToggle from "./ThemeToggle.astro";
import { site } from "../data/site";
// `id` ties a nav link to a homepage section so the Motion layer animates its
// underline as that section scrolls through (scroll-spy). Projects/Blog navigate
// to their own pages on click, but on the homepage they track the #projects and
// #writing sections so every nav item gets the underline - in both directions.
const links = [
{ label: "About", href: "/#about", id: "about" },
{ label: "Skills", href: "/#skills", id: "skills" },
{ label: "Projects", href: "/projects/", id: "projects" },
{ label: "Blog", href: "/blog/", id: "writing" },
{ label: "Contact", href: "/#contact", id: "contact" },
];
---
<header class="nav">
<div class="container nav__inner">
<a class="nav__brand mono" href="/" aria-label={`${site.name} - home`}>
<span class="nav__prompt">~/</span><span>{site.handle}</span><span class="nav__caret" aria-hidden="true">▮</span>
</a>
<nav class="nav__links" aria-label="Primary">
{links.map((l) => (
<a href={l.href} data-nav-id={l.id}>
{l.label}<span class="nav__ul" aria-hidden="true"></span>
</a>
))}
</nav>
<div class="nav__actions">
<ThemeToggle />
</div>
</div>
</header>
<style>
.nav {
position: sticky;
top: 0;
z-index: 50;
background: color-mix(in srgb, var(--bg) 82%, transparent);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
}
.nav__inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
height: 4rem;
}
.nav__brand {
display: inline-flex;
align-items: center;
gap: 0.1rem;
font-size: var(--step-0);
font-weight: 600;
color: var(--text);
}
.nav__brand:hover {
text-decoration: none;
color: var(--accent);
}
.nav__prompt {
color: var(--text-faint);
}
.nav__caret {
color: var(--accent);
animation: blink 1.1s steps(1) infinite;
margin-left: 0.1rem;
}
@keyframes blink {
50% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.nav__caret {
animation: none;
}
}
.nav__links {
display: none;
gap: var(--space-5);
font-family: var(--font-mono);
font-size: var(--step--1);
}
.nav__links a {
position: relative;
color: var(--text-dim);
}
.nav__links a:hover {
color: var(--text);
text-decoration: none;
}
/* Active-section / hover underline - scaleX scrubbed by the Motion layer
(spring). transform-origin left so it grows from the start of the label. */
.nav__ul {
position: absolute;
left: 0;
right: 0;
bottom: -0.45rem;
height: 2px;
border-radius: 2px;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
box-shadow: 0 0 8px var(--accent-glow);
transform: scaleX(0);
transform-origin: 0 50%;
pointer-events: none;
}
.nav__actions {
display: flex;
align-items: center;
gap: var(--space-3);
}
@media (min-width: 720px) {
.nav__links {
display: flex;
}
}
/* phones: the tabs live on a compact second row (zero-JS, no hamburger) -
the bar wraps: brand + toggle on row 1, links across row 2 */
@media (max-width: 719px) {
.nav__inner {
height: auto;
flex-wrap: wrap;
row-gap: 0;
padding-block: 0.55rem 0.6rem;
}
.nav__links {
display: flex;
order: 3;
width: 100%;
gap: var(--space-4);
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
font-size: 0.78rem;
padding-top: 0.5rem;
}
.nav__links::-webkit-scrollbar {
display: none;
}
.nav__links a {
flex-shrink: 0;
padding-bottom: 0.15rem;
}
}
</style>