openvibers/afresume Commits

Section chip is a button: tapping it goes back to the top of the section it names

OP OpenVibers committed 26 Sept 2026 · 0c498cbe16 · 1 file, +19 −4

Same smooth scroll and header offset as the nav links (the page top for the summary and on pages
without sections), instant under reduced motion; labelled "Back to the top of <section>". While the
chip is hidden at the top of the page it is not clickable or focusable.

Changes

ddiff --git a/src/components/Nav.astro b/src/components/Nav.astro
iindex 654f19f..7f99560 100644
---- a/src/components/Nav.astro
++++ b/src/components/Nav.astro
@@@ -33,7 +33,7 @@ const here: [string, string] = path.startsWith("/resume") ? ["Resume", "fa-solid
</span>
<span class="nav__name">Alex Frison</span>
</a>
-- <span class="nav__where" id="nav-where" aria-live="polite"><i class={here[1]} aria-hidden="true"></i><span class="nav__where-txt">{here[0]}</span></span>
++ <button type="button" class="nav__where" id="nav-where" aria-label={`Back to the top of ${here[0]}`}><i class={here[1]} aria-hidden="true"></i><span class="nav__where-txt" aria-live="polite">{here[0]}</span></button>
<nav class="nav__links" aria-label="Primary">
<ul>
@@@ -141,10 +141,14 @@ const here: [string, string] = path.startsWith("/resume") ? ["Resume", "fa-solid
display: inline-flex; align-items: center; gap: 8px; height: 32px; padding: 0 12px 0 10px; border-radius: 999px; flex: none;
background: rgba(var(--accent-rgb), .1); border: 1px solid rgba(var(--accent-rgb), .25);
font: 500 .72rem var(--font-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--fg);
-- opacity: 0; transform: translateX(-6px); transition: opacity .35s var(--ease), transform .35s var(--ease); overflow: hidden;
++ opacity: 0; transform: translateX(-6px); overflow: hidden; margin: 0; cursor: pointer; appearance: none;
++ /* Hidden until you scroll: then not clickable or focusable either (visibility flips after the fade). */
++ visibility: hidden; transition: opacity .35s var(--ease), transform .35s var(--ease), border-color .25s var(--ease), background .25s var(--ease), visibility 0s linear .35s;
}
++ .nav__where:hover { border-color: rgba(var(--accent-rgb), .55); background: rgba(var(--accent-rgb), .16); }
++ .nav__where:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.nav__where i { color: var(--accent); font-size: .8rem; }
-- .nav.is-scrolled .nav__where { opacity: 1; transform: none; }
++ .nav.is-scrolled .nav__where { opacity: 1; transform: none; visibility: visible; transition-delay: 0s; }
.nav__where.is-swap .nav__where-txt, .nav__where.is-swap i { animation: whereIn .45s var(--ease); }
@keyframes whereIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@media (max-width: 920px) { .nav__where { margin-left: auto; } }
@@@ -345,13 +349,24 @@ const here: [string, string] = path.startsWith("/resume") ? ["Resume", "fa-solid
// Sections that aren't nav items still get a readout of their own.
// The hero ('top', read as '') is the summary: who, what and the pitch. "Profile" is #about's.
const extraWhere: Record<string, [string, string]> = { '': ['Summary', 'fa-solid fa-id-card'], about: ['Profile', 'fa-solid fa-user'], metrics: ['At a glance', 'fa-solid fa-chart-simple'] };
++ // The chip is a button: it takes you back to the top of the section it names (the page's top
++ // on pages without sections), with the same smooth scroll and header offset as the nav links.
++ let whereId = '';
++ where?.addEventListener('click', () => {
++ const target = whereId ? document.getElementById(whereId) : null;
++ const lenis = (window as any).__lenis;
++ const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
++ if (lenis) lenis.scrollTo(target || 0, { offset: target ? -80 : 0, duration: 1.1, immediate: reduced });
++ else window.scrollTo({ top: target ? target.getBoundingClientRect().top + scrollY - 80 : 0, behavior: reduced ? 'auto' : 'smooth' });
++ });
const setWhere = (id: string) => {
if (!where || !whereIcon || !whereTxt) return;
++ whereId = id;
const src = anchors.find((a) => a.dataset.nav === id);
const icon = src ? src.querySelector('i')!.className : (extraWhere[id]?.[1] || 'fa-solid fa-id-card');
const label = src ? (src.textContent || '').trim() : (extraWhere[id]?.[0] || 'Summary');
if (whereTxt.textContent === label) return;
-- whereIcon.className = icon; whereTxt.textContent = label;
++ whereIcon.className = icon; whereTxt.textContent = label; where.setAttribute('aria-label', `Back to the top of ${label}`);
where.classList.remove('is-swap'); void where.offsetWidth; where.classList.add('is-swap');
};
const movePill = (a: HTMLAnchorElement | null) => {