From 2389632812e7ee7162bcd379b3795b9b007d19c2 Mon Sep 17 00:00:00 2001 From: nym21 Date: Sun, 7 Jun 2026 13:10:23 +0200 Subject: [PATCH] website: redesign part 21 --- website_next/home/style.css | 5 +- website_next/learn/charts/highlight.js | 9 ++-- website_next/learn/charts/index.js | 5 +- website_next/learn/charts/renderer.js | 6 +++ website_next/learn/charts/style.css | 36 ++++++++++--- website_next/learn/cohorts.js | 14 +++++ website_next/learn/contents/style.css | 4 +- website_next/learn/data.js | 25 +++++++++ website_next/learn/groups.js | 11 ++++ website_next/learn/scroll-spy.js | 72 ++++++++++++-------------- website_next/learn/style.css | 6 +-- website_next/styles/variables.css | 2 - 12 files changed, 133 insertions(+), 62 deletions(-) diff --git a/website_next/home/style.css b/website_next/home/style.css index 1ccbe2501..1f6d49d30 100644 --- a/website_next/home/style.css +++ b/website_next/home/style.css @@ -23,11 +23,12 @@ main.home { padding: 0.75rem 1rem; border-radius: 0.3125rem; color: var(--white); - background: var(--dark-gray); + background: var(--gray); text-decoration: none; &:hover { - background: var(--gray); + color: var(--black); + background: var(--white); } &:active { diff --git a/website_next/learn/charts/highlight.js b/website_next/learn/charts/highlight.js index bce9320a2..0dad1627f 100644 --- a/website_next/learn/charts/highlight.js +++ b/website_next/learn/charts/highlight.js @@ -9,17 +9,18 @@ export function createSeriesHighlight(items, menu) { /** @param {number} index */ function scrollToItem(index) { + const margin = Number.parseFloat(getComputedStyle(menu).paddingLeft); const itemRect = items[index].getBoundingClientRect(); const menuRect = menu.getBoundingClientRect(); - if (itemRect.left < menuRect.left) { + if (itemRect.left < menuRect.left + margin) { menu.scrollBy({ - left: itemRect.left - menuRect.left, + left: itemRect.left - menuRect.left - margin, behavior: "smooth", }); - } else if (itemRect.right > menuRect.right) { + } else if (itemRect.right > menuRect.right - margin) { menu.scrollBy({ - left: itemRect.right - menuRect.right, + left: itemRect.right - menuRect.right + margin, behavior: "smooth", }); } diff --git a/website_next/learn/charts/index.js b/website_next/learn/charts/index.js index 48f07d788..b22a11f19 100644 --- a/website_next/learn/charts/index.js +++ b/website_next/learn/charts/index.js @@ -23,6 +23,7 @@ import { FALLBACK_VIEWBOX_HEIGHT, VIEWBOX_WIDTH } from "./viewbox.js"; /** @param {Chart} chart */ export function createChart(chart) { const figure = document.createElement("figure"); + const plot = document.createElement("div"); const svg = createSvgElement("svg"); const controls = document.createElement("footer"); const chartControls = document.createElement("div"); @@ -35,6 +36,7 @@ export function createChart(chart) { const { legend, menu, items, readout } = createLegend(chart); figure.dataset.chart = "series"; + plot.dataset.chart = "plot"; figure.dataset.timeframe = currentTimeframe; figure.dataset.view = currentView; figure.dataset.scale = currentScale; @@ -83,7 +85,8 @@ export function createChart(chart) { chartControls.append(viewControl, scaleControl); timeControls.append(timeframeControl, createFullscreenButton(figure)); controls.append(chartControls, timeControls); - figure.append(legend, svg, controls, status); + plot.append(svg, status); + figure.append(legend, plot, controls); onChartVisibility(figure, { show: renderer.resume, hide: renderer.suspend, diff --git a/website_next/learn/charts/renderer.js b/website_next/learn/charts/renderer.js index 499b8dd01..9bb68f71f 100644 --- a/website_next/learn/charts/renderer.js +++ b/website_next/learn/charts/renderer.js @@ -104,6 +104,10 @@ export function createChartRenderer({ async function loadCurrent() { const id = (loadId += 1); + const loadingTimer = setTimeout(() => { + if (id === loadId && active) status.textContent = "Loading"; + }, 250); + svg.setAttribute("aria-busy", "true"); try { @@ -119,6 +123,7 @@ export function createChartRenderer({ console.error(error); status.textContent = "Chart unavailable"; } finally { + clearTimeout(loadingTimer); if (id === loadId) svg.removeAttribute("aria-busy"); } } @@ -141,6 +146,7 @@ export function createChartRenderer({ group.replaceChildren(); highlight.clearNodes(); scrubber?.clear(); + status.textContent = ""; svg.removeAttribute("aria-busy"); } diff --git a/website_next/learn/charts/style.css b/website_next/learn/charts/style.css index 05d1417c9..2ae22578c 100644 --- a/website_next/learn/charts/style.css +++ b/website_next/learn/charts/style.css @@ -10,6 +10,26 @@ main.learn { cursor: crosshair; overflow: visible; touch-action: pan-y; + transition: opacity 150ms ease; + } + + svg[aria-busy="true"] { + opacity: 0.25; + } + + > div[data-chart="plot"] { + position: relative; + } + + p[role="status"] { + position: absolute; + inset: 0; + display: grid; + place-items: center; + margin: 0; + color: var(--white); + text-transform: uppercase; + pointer-events: none; } p[role="status"]:empty { @@ -69,15 +89,15 @@ main.learn { } label:hover span { - color: var(--white); - background: var(--dark-gray); - } - - label:has(:checked) span { color: var(--black); background: var(--white); } + label:has(:checked):not(:hover) span { + color: var(--black); + background: var(--gray); + } + label:active span { color: var(--black); background: var(--orange); @@ -101,8 +121,8 @@ main.learn { cursor: pointer; &:hover { - color: var(--white); - background: var(--dark-gray); + color: var(--black); + background: var(--white); } &[aria-pressed="true"] { @@ -255,7 +275,7 @@ main.learn { } [data-scrubber="guide"] { - stroke: var(--light-gray); + stroke: var(--white); stroke-dasharray: 2 4; vector-effect: non-scaling-stroke; } diff --git a/website_next/learn/cohorts.js b/website_next/learn/cohorts.js index 608924700..f2860966c 100644 --- a/website_next/learn/cohorts.js +++ b/website_next/learn/cohorts.js @@ -3,6 +3,7 @@ import { createCohortSeriesFromKeys, } from "./cohort-series.js"; import { + addressableTypes, ageRanges, amountRanges, classes, @@ -11,6 +12,19 @@ import { } from "./groups.js"; import { colors } from "../utils/colors.js"; +export const exposedSupplySeries = createCohortSeries([ + { + label: "Exposed", + color: colors.orange, + metric: (client) => client.series.addrs.exposed.supply.all.btc, + }, +]); + +export const exposedSupplyTypeSeries = createCohortSeriesFromKeys( + addressableTypes, + (key) => (client) => client.series.addrs.exposed.supply[key].btc, +); + export const termSeries = createCohortSeries([ { label: "STH", diff --git a/website_next/learn/contents/style.css b/website_next/learn/contents/style.css index 0dfa8f2a0..d33fc669d 100644 --- a/website_next/learn/contents/style.css +++ b/website_next/learn/contents/style.css @@ -67,8 +67,8 @@ main.learn { } &:hover { - color: var(--white); - background-color: var(--dark-gray); + color: var(--black); + background-color: var(--white); } &[aria-current="location"] { diff --git a/website_next/learn/data.js b/website_next/learn/data.js index 1a4e3da28..ac1642e0d 100644 --- a/website_next/learn/data.js +++ b/website_next/learn/data.js @@ -24,6 +24,8 @@ import { ageSeries, classSeries, epochSeries, + exposedSupplySeries, + exposedSupplyTypeSeries, termSeries, typeSeries, utxoBalanceSeries, @@ -92,6 +94,29 @@ export const sections = [ ], }, }, + { + title: "Exposed", + description: + "Shows BTC held by addresses whose public key is already visible on-chain. This can happen because the address type exposes the key directly, or because coins were spent from that address before.", + chart: { + title: "Exposed supply", + unit: units.btc, + defaultType: lineType, + series: exposedSupplySeries, + }, + children: [ + { + title: "Type", + description: + "Splits exposed supply by address type. This shows which script formats account for the visible-public-key supply.", + chart: { + title: "Exposed supply by type", + unit: units.btc, + series: exposedSupplyTypeSeries, + }, + }, + ], + }, { title: "Term", description: diff --git a/website_next/learn/groups.js b/website_next/learn/groups.js index 176f7d3db..bc7b02443 100644 --- a/website_next/learn/groups.js +++ b/website_next/learn/groups.js @@ -54,6 +54,17 @@ export const spendableTypes = /** @type {const} */ ([ ["Empty", "empty"], ]); +export const addressableTypes = /** @type {const} */ ([ + ["P2PK65", "p2pk65"], + ["P2PK33", "p2pk33"], + ["P2PKH", "p2pkh"], + ["P2SH", "p2sh"], + ["P2WPKH", "p2wpkh"], + ["P2WSH", "p2wsh"], + ["P2TR", "p2tr"], + ["P2A", "p2a"], +]); + export const outputTypes = /** @type {const} */ ([ ["P2PK65", "p2pk65"], ["P2PK33", "p2pk33"], diff --git a/website_next/learn/scroll-spy.js b/website_next/learn/scroll-spy.js index 7797f4866..564831696 100644 --- a/website_next/learn/scroll-spy.js +++ b/website_next/learn/scroll-spy.js @@ -1,17 +1,11 @@ -const thresholds = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]; - /** @param {HTMLElement} main */ export function initScrollSpy(main) { const nav = /** @type {HTMLElement} */ (main.querySelector("nav")); const sections = [...main.querySelectorAll("section[id]")]; const sectionStates = sections.map((section) => ({ section, - children: [...section.querySelectorAll(":scope > section")], - intersecting: false, + firstChild: section.querySelector(":scope > section"), })); - const stateBySection = new Map( - sectionStates.map((state) => [state.section, state]), - ); const links = new Map( [...main.querySelectorAll('nav a[href^="#"]')].map((link) => [ link.getAttribute("href"), @@ -21,25 +15,29 @@ export function initScrollSpy(main) { /** @type {string | null} */ let current = null; + let scheduled = false; - /** @param {Element} section */ - function getVisibleHeight(section) { - const rect = section.getBoundingClientRect(); - return Math.max( - 0, - Math.min(rect.bottom, window.innerHeight) - Math.max(rect.top, 0), - ); + function getViewportTop() { + return Number.parseFloat(getComputedStyle(main).getPropertyValue("--offset")); } - /** @param {{ section: Element, children: Element[] }} state */ - function getOwnVisibleHeight(state) { - let height = getVisibleHeight(state.section); + /** + * @param {Element} section + * @param {Element | null} firstChild + */ + function getOwnVisibleHeight(section, firstChild) { + const sectionRect = section.getBoundingClientRect(); + const childRect = firstChild?.getBoundingClientRect(); + const top = Math.max(sectionRect.top, getViewportTop()); + const bottom = Math.min( + childRect?.top ?? sectionRect.bottom, + window.innerHeight, + ); - for (const child of state.children) { - height -= getVisibleHeight(child); - } - - return Math.max(0, height); + return Math.max( + 0, + bottom - top, + ); } /** @param {string} hash */ @@ -79,14 +77,12 @@ export function initScrollSpy(main) { } function getCurrentSection() { - /** @type {{ section: Element, children: Element[] } | undefined} */ + /** @type {{ section: Element, firstChild: Element | null } | undefined} */ let currentState; let currentHeight = 0; for (const state of sectionStates) { - if (!state.intersecting) continue; - - const height = getOwnVisibleHeight(state); + const height = getOwnVisibleHeight(state.section, state.firstChild); if (height > currentHeight) { currentState = state; @@ -104,21 +100,17 @@ export function initScrollSpy(main) { if (section) setCurrentHash(`#${section.id}`); } - const observer = new IntersectionObserver( - (entries) => { - for (const entry of entries) { - const state = /** @type {{ intersecting: boolean }} */ ( - stateBySection.get(entry.target) - ); - state.intersecting = entry.isIntersecting; - } + function scheduleUpdate() { + if (scheduled) return; + scheduled = true; + requestAnimationFrame(() => { + scheduled = false; update(); - }, - { - threshold: thresholds, - }, - ); + }); + } - for (const section of sections) observer.observe(section); + window.addEventListener("scroll", scheduleUpdate, { passive: true }); + main.addEventListener("pageactive", scheduleUpdate); + scheduleUpdate(); } diff --git a/website_next/learn/style.css b/website_next/learn/style.css index 377c5a674..aea9b016f 100644 --- a/website_next/learn/style.css +++ b/website_next/learn/style.css @@ -136,7 +136,7 @@ main.learn { h1 { z-index: 3; padding-bottom: var(--heading-padding-bottom); - border-bottom: 1px solid var(--dark-gray); + border-bottom: 1px solid var(--gray); font-size: 3rem; a::before { @@ -148,7 +148,7 @@ main.learn { z-index: 2; padding-top: var(--topic-padding-top); padding-bottom: var(--heading-padding-bottom); - border-bottom: 1px dashed var(--dark-gray); + border-bottom: 1px dashed var(--gray); font-size: var(--topic-font-size); a::before { @@ -160,7 +160,7 @@ main.learn { z-index: 1; padding-top: var(--detail-padding-top); padding-bottom: var(--detail-padding-bottom); - border-bottom: 1px dotted var(--dark-gray); + border-bottom: 1px dotted var(--gray); font-size: var(--detail-font-size); a::before { diff --git a/website_next/styles/variables.css b/website_next/styles/variables.css index ff42027cb..2716e3c95 100644 --- a/website_next/styles/variables.css +++ b/website_next/styles/variables.css @@ -3,9 +3,7 @@ --white: oklch(95% 0 0); --dark-white: oklch(92.5% 0 0); - --light-gray: oklch(90% 0 0); --gray: oklch(55% 0 0); - --dark-gray: oklch(20% 0 0); --light-black: oklch(17.5% 0 0); --black: oklch(15% 0 0); --red: oklch(0.607 0.241 26.328);