nav: add a Learn tab pointing at the teaching site, and drop the scroll-spy underline - tracking the active section as you scrolled read as clunky, so the underline is hover-only now; also refresh the public CV
build-and-deploy / build (push) Failing after 15m10s
build-and-deploy / build (push) Failing after 15m10s
This commit is contained in:
Binary file not shown.
+16
-12
@@ -2,16 +2,15 @@
|
|||||||
import ThemeToggle from "./ThemeToggle.astro";
|
import ThemeToggle from "./ThemeToggle.astro";
|
||||||
import { site } from "../data/site";
|
import { site } from "../data/site";
|
||||||
|
|
||||||
// `id` ties a nav link to a homepage section so the Motion layer animates its
|
// Plain links. Hover shows the underline; there is no scroll-spy - tracking the
|
||||||
// underline as that section scrolls through (scroll-spy). Projects/Blog navigate
|
// active section as you scrolled read as clunky, so the underline is hover-only.
|
||||||
// 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 = [
|
const links = [
|
||||||
{ label: "About", href: "/#about", id: "about" },
|
{ label: "About", href: "/#about" },
|
||||||
{ label: "Skills", href: "/#skills", id: "skills" },
|
{ label: "Skills", href: "/#skills" },
|
||||||
{ label: "Projects", href: "/projects/", id: "projects" },
|
{ label: "Projects", href: "/projects/" },
|
||||||
{ label: "Blog", href: "/blog/", id: "writing" },
|
{ label: "Blog", href: "/blog/" },
|
||||||
{ label: "Contact", href: "/#contact", id: "contact" },
|
{ label: "Contact", href: "/#contact" },
|
||||||
|
{ label: "Learn", href: "https://learn.bztmon.com", external: true },
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,7 +22,11 @@ const links = [
|
|||||||
|
|
||||||
<nav class="nav__links" aria-label="Primary">
|
<nav class="nav__links" aria-label="Primary">
|
||||||
{links.map((l) => (
|
{links.map((l) => (
|
||||||
<a href={l.href} data-nav-id={l.id}>
|
<a
|
||||||
|
href={l.href}
|
||||||
|
target={l.external ? "_blank" : undefined}
|
||||||
|
rel={l.external ? "noopener noreferrer" : undefined}
|
||||||
|
>
|
||||||
{l.label}<span class="nav__ul" aria-hidden="true"></span>
|
{l.label}<span class="nav__ul" aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
@@ -95,8 +98,9 @@ const links = [
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
/* Active-section / hover underline - scaleX scrubbed by the Motion layer
|
/* Hover underline - scaleX sprung by the Motion layer, growing from the
|
||||||
(spring). transform-origin left so it grows from the start of the label. */
|
start of the label. No scroll-spy: it tracked the active section as you
|
||||||
|
scrolled, which read as clunky on a page this short. */
|
||||||
.nav__ul {
|
.nav__ul {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
+3
-55
@@ -92,73 +92,21 @@ function run(): void {
|
|||||||
|
|
||||||
function initNav(): void {
|
function initNav(): void {
|
||||||
const links = Array.from(
|
const links = Array.from(
|
||||||
document.querySelectorAll<HTMLAnchorElement>(".nav__links a[data-nav-id]"),
|
document.querySelectorAll<HTMLAnchorElement>(".nav__links a"),
|
||||||
);
|
);
|
||||||
if (!links.length) return;
|
if (!links.length) return;
|
||||||
|
|
||||||
const spring = { type: "spring", stiffness: 380, damping: 30 } as const;
|
const spring = { type: "spring", stiffness: 380, damping: 30 } as const;
|
||||||
let activeIdx = -1;
|
|
||||||
|
|
||||||
// `fromRight` flips the grow/collapse origin so the underline animates in the
|
const under = (a: HTMLAnchorElement, on: boolean): void => {
|
||||||
// direction of travel: scrolling down it grows from the left (moves right);
|
|
||||||
// scrolling back up it grows from the right (moves left).
|
|
||||||
const under = (a: HTMLAnchorElement, on: boolean, fromRight = false): void => {
|
|
||||||
const ul = a.querySelector<HTMLElement>(".nav__ul");
|
const ul = a.querySelector<HTMLElement>(".nav__ul");
|
||||||
if (!ul) return;
|
if (!ul) return;
|
||||||
ul.style.transformOrigin = fromRight ? "100% 50%" : "0% 50%";
|
|
||||||
animate(ul, { scaleX: on ? 1 : 0 }, spring);
|
animate(ul, { scaleX: on ? 1 : 0 }, spring);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Links that map to a section ON THIS PAGE. Projects/Blog point at the homepage
|
|
||||||
// #projects / #writing sections so every nav item gets the underline; on their
|
|
||||||
// own pages those sections don't exist and they simply drop out of the spy.
|
|
||||||
const sectioned = links.filter(
|
|
||||||
(a) => a.dataset.navId && document.getElementById(a.dataset.navId),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const a of links) {
|
for (const a of links) {
|
||||||
a.addEventListener("pointerenter", () => under(a, true));
|
a.addEventListener("pointerenter", () => under(a, true));
|
||||||
// On leave, keep the underline only if this link IS the active scroll-spy
|
a.addEventListener("pointerleave", () => under(a, false));
|
||||||
// section. Guard on activeIdx >= 0 so that when nothing is active - e.g. the
|
|
||||||
// Projects/Blog pages, where none of these sections exist and activeIdx stays
|
|
||||||
// -1 - a hovered tab doesn't stick lit via the -1 === indexOf(non-member) -1.
|
|
||||||
a.addEventListener("pointerleave", () =>
|
|
||||||
under(a, activeIdx >= 0 && sectioned.indexOf(a) === activeIdx),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Light up the link whose section is currently being read, recomputed from
|
|
||||||
// scroll position so it tracks BOTH directions. Every change re-asserts ALL
|
|
||||||
// underlines (active → 1, the rest → 0) so none can be left stranded, and the
|
|
||||||
// grow/collapse origin follows the scroll direction.
|
|
||||||
if (sectioned.length) {
|
|
||||||
const setActive = (): void => {
|
|
||||||
const vh = window.innerHeight;
|
|
||||||
const ref = vh * 0.45; // a section whose top has passed this line is "active"
|
|
||||||
let idx = -1;
|
|
||||||
sectioned.forEach((a, i) => {
|
|
||||||
const sec = document.getElementById(a.dataset.navId as string);
|
|
||||||
if (sec && sec.getBoundingClientRect().top <= ref) idx = i;
|
|
||||||
});
|
|
||||||
// At the very bottom the last (often short) section may never push its top
|
|
||||||
// above the line - force it so the final item lights up.
|
|
||||||
if (window.scrollY + vh >= document.documentElement.scrollHeight - 4) {
|
|
||||||
idx = sectioned.length - 1;
|
|
||||||
}
|
|
||||||
if (idx === activeIdx) return;
|
|
||||||
const backward = idx < activeIdx; // scrolling up → travel left
|
|
||||||
activeIdx = idx;
|
|
||||||
// Coherent directional slide: the activating underline grows from its
|
|
||||||
// trailing edge while the others collapse toward the leading edge, so the
|
|
||||||
// whole motion follows the scroll direction (down → right, up → left)
|
|
||||||
// instead of the active/old pair diverging.
|
|
||||||
sectioned.forEach((a, i) => {
|
|
||||||
const on = i === idx;
|
|
||||||
under(a, on, on ? backward : !backward);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
scroll(setActive);
|
|
||||||
setActive();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user