Section chip is a button: tapping it goes back to the top of the section it names
Changes
@@@ -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-soliddisplay: 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) => {
@@ -33,7 +33,7 @@ const here: [string, string] = path.startsWith("/resume") ? ["Resume", "fa-solid
@@ -33,7 +33,7 @@ const here: [string, string] = path.startsWith("/resume") ? ["Resume", "fa-solid
</span>
</span>
<span class="nav__name">Alex Frison</span>
<span class="nav__name">Alex Frison</span>
</a>
</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">
<nav class="nav__links" aria-label="Primary">
<ul>
<ul>
@@ -141,10 +141,14 @@ const here: [string, string] = path.startsWith("/resume") ? ["Resume", "fa-solid
@@ -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;
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);
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);
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__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); }
.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; } }
@keyframes whereIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@media (max-width: 920px) { .nav__where { margin-left: auto; } }
@media (max-width: 920px) { .nav__where { margin-left: auto; } }
@@ -345,13 +349,24 @@ const here: [string, string] = path.startsWith("/resume") ? ["Resume", "fa-solid
@@ -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.
// 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.
// 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'] };
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) => {
const setWhere = (id: string) => {
if (!where || !whereIcon || !whereTxt) return;
if (!where || !whereIcon || !whereTxt) return;
+ whereId = id;
const src = anchors.find((a) => a.dataset.nav === 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 icon = src ? src.querySelector('i')!.className : (extraWhere[id]?.[1] || 'fa-solid fa-id-card');
const label = src ? (src.textContent || '').trim() : (extraWhere[id]?.[0] || 'Summary');
const label = src ? (src.textContent || '').trim() : (extraWhere[id]?.[0] || 'Summary');
if (whereTxt.textContent === label) return;
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');
where.classList.remove('is-swap'); void where.offsetWidth; where.classList.add('is-swap');
};
};
const movePill = (a: HTMLAnchorElement | null) => {
const movePill = (a: HTMLAnchorElement | null) => {