website: redesign part 28

This commit is contained in:
nym21
2026-06-09 16:12:50 +02:00
parent 5966ab05e4
commit 90b3b51c48
10 changed files with 153 additions and 142 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ const observer = new IntersectionObserver(
}
},
{
rootMargin: "800px 0px",
rootMargin: "400px 0px",
},
);
+24 -4
View File
@@ -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();
});
+4 -5
View File
@@ -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%;
-1
View File
@@ -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);
+30 -59
View File
@@ -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;
}
}
}
+6 -2
View File
@@ -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) {
+2 -2
View File
@@ -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;
}
+84 -51
View File
@@ -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;
}
-16
View File
@@ -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 {
+2 -1
View File
@@ -44,7 +44,8 @@ html {
h1,
h2,
h3 {
h3,
h4 {
font-family: var(--font-serif);
font-weight: 400;
}