From 90b3b51c482e0fd9304dbb5cd43553277ea41d1c Mon Sep 17 00:00:00 2001 From: nym21 Date: Tue, 9 Jun 2026 16:12:50 +0200 Subject: [PATCH] website: redesign part 28 --- website_next/learn/charts/intersection.js | 2 +- website_next/learn/charts/scrubber/index.js | 28 +++- website_next/learn/charts/style.css | 9 +- website_next/learn/contents/index.js | 1 - website_next/learn/contents/style.css | 89 +++++-------- website_next/learn/hash-links.js | 8 +- website_next/learn/index.js | 4 +- website_next/learn/scroll-spy.js | 135 ++++++++++++-------- website_next/learn/style.css | 16 --- website_next/styles/fonts.css | 3 +- 10 files changed, 153 insertions(+), 142 deletions(-) diff --git a/website_next/learn/charts/intersection.js b/website_next/learn/charts/intersection.js index fbf3b4e9d..7480f0bc6 100644 --- a/website_next/learn/charts/intersection.js +++ b/website_next/learn/charts/intersection.js @@ -7,7 +7,7 @@ const observer = new IntersectionObserver( } }, { - rootMargin: "800px 0px", + rootMargin: "400px 0px", }, ); diff --git a/website_next/learn/charts/scrubber/index.js b/website_next/learn/charts/scrubber/index.js index 08acdda28..5b021bc31 100644 --- a/website_next/learn/charts/scrubber/index.js +++ b/website_next/learn/charts/scrubber/index.js @@ -82,8 +82,12 @@ export function createScrubber(svg, readout, highlight, format) { let height = 0; let stepCount = 0; let currentStep = -1; - let currentPoints = getPointsAtStep(0); + /** @type {ChartPoint[]} */ + let currentPoints = []; let rect = svg.getBoundingClientRect(); + let pointerX = 0; + let pointerY = 0; + let pointerFrame = 0; group.dataset.scrubber = "root"; shade.dataset.scrubber = "shade"; @@ -151,7 +155,13 @@ export function createScrubber(svg, readout, highlight, format) { update(1, undefined, false); } + function cancelPointerUpdate() { + if (pointerFrame) cancelAnimationFrame(pointerFrame); + pointerFrame = 0; + } + function clear() { + cancelPointerUpdate(); series = []; markers = []; currentStep = -1; @@ -191,20 +201,30 @@ export function createScrubber(svg, readout, highlight, format) { /** @param {PointerEvent} event */ function updateFromPointer(event) { - const x = ((event.clientX - rect.left) / rect.width) * VIEWBOX_WIDTH; - const y = ((event.clientY - rect.top) / rect.height) * height; + pointerX = event.clientX; + pointerY = event.clientY; + if (pointerFrame) return; - update(x / VIEWBOX_WIDTH, y); + pointerFrame = requestAnimationFrame(() => { + pointerFrame = 0; + + const x = ((pointerX - rect.left) / rect.width) * VIEWBOX_WIDTH; + const y = ((pointerY - rect.top) / rect.height) * height; + + update(x / VIEWBOX_WIDTH, y); + }); } svg.addEventListener("pointerenter", measure); svg.addEventListener("pointermove", updateFromPointer); svg.addEventListener("pointerleave", () => { + cancelPointerUpdate(); highlight.clearPreview(); hide(); }); svg.addEventListener("focus", () => update(1)); svg.addEventListener("blur", () => { + cancelPointerUpdate(); highlight.clearPreview(); hide(); }); diff --git a/website_next/learn/charts/style.css b/website_next/learn/charts/style.css index 21fb84f39..e9fd81503 100644 --- a/website_next/learn/charts/style.css +++ b/website_next/learn/charts/style.css @@ -1,14 +1,13 @@ main.learn { figure[data-chart="series"] { --chart-plot-height: 20rem; - --chart-placeholder-height: calc(var(--chart-plot-height) + 4rem); + --chart-reserved-ui-height: 6rem; + min-height: calc( + var(--chart-plot-height) + var(--chart-reserved-ui-height) + ); line-height: 1; - &:empty { - min-height: var(--chart-placeholder-height); - } - svg { display: block; width: 100%; diff --git a/website_next/learn/contents/index.js b/website_next/learn/contents/index.js index 7c31aa2d0..bc0486d13 100644 --- a/website_next/learn/contents/index.js +++ b/website_next/learn/contents/index.js @@ -10,7 +10,6 @@ function createContentsItem(section, path) { const children = section.children ?? []; const sectionPath = [...path, section.title]; - if (section.numbered === false) item.dataset.numbered = "false"; anchor.href = `#${createPathId(sectionPath)}`; anchor.append(section.title); diff --git a/website_next/learn/contents/style.css b/website_next/learn/contents/style.css index 95f3668e6..cbd1338f8 100644 --- a/website_next/learn/contents/style.css +++ b/website_next/learn/contents/style.css @@ -1,8 +1,11 @@ main.learn { > nav { --nav-offset: calc(var(--offset) + 2rem); + --line-gap: 0.5rem; + --line-gutter: 1.25rem; + --line-inset: 0.5rem; + --line-step: 1rem; - counter-reset: content-theme; position: sticky; top: 0; max-height: 100dvh; @@ -11,6 +14,7 @@ main.learn { padding-left: 0.5rem; overflow: auto; overscroll-behavior: contain; + scroll-snap-type: y proximity; color: var(--gray); font-size: var(--font-size-xs); line-height: var(--line-height-xs); @@ -27,22 +31,30 @@ main.learn { } a { + --line-width: calc(var(--line-indent) + var(--line-gutter)); + + position: relative; display: block; + scroll-snap-align: center; scroll-margin-block: var(--offset); color: inherit; text-decoration: none; + border-radius: 0.25rem; margin-block: -0.25rem; - margin-inline: -0.5rem 1rem; + margin-inline: calc(-1 * var(--line-indent)) 1rem; padding: 0.25rem; - padding-inline-start: 0.5rem; + padding-inline-start: calc(var(--line-width) + var(--line-gap)); - &::before { + &::after { + content: ""; + position: absolute; + top: 50%; + left: var(--line-inset); + width: calc(var(--line-width) - var(--line-inset)); + translate: 0 -50%; + border-block-start: 1px solid currentColor; opacity: 0.5; - text-transform: none; - } - - &:is(:hover, :active) { - border-radius: 0.25rem; + pointer-events: none; } &[aria-current="location"] { @@ -61,63 +73,22 @@ main.learn { } ol ol { + --line-indent: var(--line-step); + margin-block-start: 0.25rem; - margin-inline-start: 1rem; + margin-inline-start: var(--line-step); + } + + ol ol ol { + --line-indent: calc(var(--line-step) * 2); } ol ol ol ol { - margin-inline-start: 0.5rem; + --line-indent: calc(var(--line-step) * 3); } > ol { - > li { - counter-reset: content-topic; - - &:not([data-numbered="false"]) { - counter-increment: content-theme; - } - - > a::before { - content: counter(content-theme, upper-roman) ". "; - } - - &[data-numbered="false"] > a::before { - content: "I. "; - visibility: hidden; - } - - > ol { - > li { - counter-increment: content-topic; - counter-reset: content-detail; - - > a::before { - content: counter(content-topic) ". "; - } - - > ol { - > li { - counter-increment: content-detail; - counter-reset: content-subtopic; - - > a::before { - content: counter(content-detail, lower-alpha) ". "; - } - - > ol { - > li { - counter-increment: content-subtopic; - - > a::before { - content: counter(content-subtopic, lower-alpha) ". "; - } - } - } - } - } - } - } - } + --line-indent: 0rem; } } } diff --git a/website_next/learn/hash-links.js b/website_next/learn/hash-links.js index bed318c46..4f4e92d75 100644 --- a/website_next/learn/hash-links.js +++ b/website_next/learn/hash-links.js @@ -28,8 +28,11 @@ function scrollToCurrentHash(main, behavior) { if (target) scrollToTarget(target, behavior); } -/** @param {HTMLElement} main */ -export function initHashLinks(main) { +/** + * @param {HTMLElement} main + * @param {(hash: string) => void} onHashNavigate + */ +export function initHashLinks(main, onHashNavigate) { main.addEventListener("click", (event) => { if (!isPlainLeftClick(event)) return; @@ -44,6 +47,7 @@ export function initHashLinks(main) { if (!target) return; event.preventDefault(); + onHashNavigate(url.hash); scrollToTarget(target, "smooth"); if (url.hash !== window.location.hash) { diff --git a/website_next/learn/index.js b/website_next/learn/index.js index 717bc9bf3..82b4520b5 100644 --- a/website_next/learn/index.js +++ b/website_next/learn/index.js @@ -45,7 +45,7 @@ export function createLearnPage() { } main.append(createContents(sections), article); - initHashLinks(main); - initScrollSpy(main); + const navigateToHash = initScrollSpy(main); + initHashLinks(main, navigateToHash); return main; } diff --git a/website_next/learn/scroll-spy.js b/website_next/learn/scroll-spy.js index f6fea37b5..76fa4ec25 100644 --- a/website_next/learn/scroll-spy.js +++ b/website_next/learn/scroll-spy.js @@ -2,10 +2,6 @@ export function initScrollSpy(main) { const nav = /** @type {HTMLElement} */ (main.querySelector("nav")); const sections = [...main.querySelectorAll("section[id]")]; - const sectionStates = sections.map((section) => ({ - section, - firstChild: section.querySelector(":scope > section"), - })); const links = new Map( [...main.querySelectorAll('nav a[href^="#"]')].map((link) => [ link.getAttribute("href"), @@ -15,39 +11,25 @@ export function initScrollSpy(main) { /** @type {string | null} */ let current = null; + /** @type {string | null} */ + let navigatingTo = null; + let alignNavToTop = true; let scheduled = false; function getViewportTop() { return Number.parseFloat(getComputedStyle(main).scrollPaddingTop); } - /** - * @param {Element} section - * @param {Element | null} firstChild - * @param {number} viewportTop - */ - function getOwnVisibleHeight(section, firstChild, viewportTop) { - const sectionRect = section.getBoundingClientRect(); - const childRect = firstChild?.getBoundingClientRect(); - const top = Math.max(sectionRect.top, viewportTop); - const bottom = Math.min( - childRect?.top ?? sectionRect.bottom, - window.innerHeight, - ); - - return Math.max( - 0, - bottom - top, - ); - } - /** @param {string} hash */ function getLink(hash) { return /** @type {HTMLAnchorElement} */ (links.get(hash)); } - /** @param {HTMLElement} link */ - function scrollLinkIntoNav(link) { + /** + * @param {HTMLElement} link + * @param {ScrollBehavior} behavior + */ + function scrollLinkIntoNav(link, behavior) { const style = getComputedStyle(nav); const top = Number.parseFloat(style.paddingTop); const bottom = Number.parseFloat(style.paddingBottom); @@ -55,55 +37,97 @@ export function initScrollSpy(main) { const linkRect = link.getBoundingClientRect(); if (linkRect.top < navRect.top + top) { - nav.scrollBy({ top: linkRect.top - navRect.top - top }); - } - - if (linkRect.bottom > navRect.bottom - bottom) { - nav.scrollBy({ top: linkRect.bottom - navRect.bottom + bottom }); + nav.scrollBy({ + top: linkRect.top - navRect.top - top, + behavior, + }); + } else if (linkRect.bottom > navRect.bottom - bottom) { + nav.scrollBy({ + top: linkRect.bottom - navRect.bottom + bottom, + behavior, + }); } } + /** + * @param {HTMLElement} link + * @param {ScrollBehavior} behavior + */ + function scrollLinkToNavTop(link, behavior) { + const top = Number.parseFloat(getComputedStyle(nav).paddingTop); + const navRect = nav.getBoundingClientRect(); + const linkRect = link.getBoundingClientRect(); + + nav.scrollBy({ + top: linkRect.top - navRect.top - top, + behavior, + }); + } + + function stopHashNavigation() { + navigatingTo = null; + } + /** @param {string} hash */ - function setCurrentHash(hash) { + function selectHash(hash) { if (hash === current) return; if (current) getLink(current).removeAttribute("aria-current"); const link = getLink(hash); link.setAttribute("aria-current", "location"); - scrollLinkIntoNav(link); - - history.replaceState(null, "", hash); current = hash; } + /** @param {string} hash */ + function syncHash(hash) { + if (hash === current) return; + + selectHash(hash); + const link = getLink(hash); + if (alignNavToTop) { + scrollLinkToNavTop(link, "auto"); + alignNavToTop = false; + } else { + scrollLinkIntoNav(link, "auto"); + } + history.replaceState(null, "", hash); + } + + /** @param {string} hash */ + function navigateToHash(hash) { + navigatingTo = hash; + selectHash(hash); + scrollLinkIntoNav(getLink(hash), "smooth"); + } + function getCurrentSection() { - /** @type {{ section: Element, firstChild: Element | null } | undefined} */ - let currentState; - let currentHeight = 0; + let currentSection = sections[0]; const viewportTop = getViewportTop(); - for (const state of sectionStates) { - const height = getOwnVisibleHeight( - state.section, - state.firstChild, - viewportTop, - ); + for (const section of sections) { + if (section.getBoundingClientRect().top > viewportTop) break; - if (height > currentHeight) { - currentState = state; - currentHeight = height; - } + currentSection = section; } - return currentState?.section; + return currentSection; } function update() { if (main.hidden) return; const section = getCurrentSection(); - if (section) setCurrentHash(`#${section.id}`); + if (!section) return; + + const hash = `#${section.id}`; + if (navigatingTo) { + selectHash(hash); + if (hash === navigatingTo) navigatingTo = null; + return; + } + + syncHash(hash); } function scheduleUpdate() { @@ -117,6 +141,15 @@ export function initScrollSpy(main) { } window.addEventListener("scroll", scheduleUpdate, { passive: true }); - main.addEventListener("pageactive", scheduleUpdate); + window.addEventListener("scrollend", () => { + stopHashNavigation(); + scheduleUpdate(); + }, { passive: true }); + main.addEventListener("pageactive", () => { + stopHashNavigation(); + alignNavToTop = true; + scheduleUpdate(); + }); scheduleUpdate(); + return navigateToHash; } diff --git a/website_next/learn/style.css b/website_next/learn/style.css index a8a36c44b..f7a8bcb4a 100644 --- a/website_next/learn/style.css +++ b/website_next/learn/style.css @@ -146,10 +146,6 @@ main.learn { padding-bottom: var(--heading-padding-bottom); border-bottom: 1px solid var(--gray); font-size: 3rem; - - a::before { - content: counter(theme, upper-roman) ". "; - } } > h2 { @@ -158,10 +154,6 @@ main.learn { padding-bottom: var(--heading-padding-bottom); border-bottom: 1px dashed var(--gray); font-size: var(--topic-font-size); - - a::before { - content: counter(topic) ". "; - } } > h3 { @@ -170,10 +162,6 @@ main.learn { padding-bottom: var(--detail-padding-bottom); border-bottom: 1px dotted var(--gray); font-size: var(--detail-font-size); - - a::before { - content: counter(detail, lower-alpha) ". "; - } } > h4 { @@ -182,10 +170,6 @@ main.learn { padding-bottom: var(--subtopic-padding-bottom); border-bottom: 1px dotted var(--gray); font-size: var(--subtopic-font-size); - - a::before { - content: counter(subtopic, lower-alpha) ". "; - } } > p { diff --git a/website_next/styles/fonts.css b/website_next/styles/fonts.css index a2989d984..9c160da2f 100644 --- a/website_next/styles/fonts.css +++ b/website_next/styles/fonts.css @@ -44,7 +44,8 @@ html { h1, h2, -h3 { +h3, +h4 { font-family: var(--font-serif); font-weight: 400; }