mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-10 06:53:33 -07:00
website: redesign part 28
This commit is contained in:
@@ -7,7 +7,7 @@ const observer = new IntersectionObserver(
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: "800px 0px",
|
||||
rootMargin: "400px 0px",
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -44,7 +44,8 @@ html {
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
h3,
|
||||
h4 {
|
||||
font-family: var(--font-serif);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user