website: redesign part 21

This commit is contained in:
nym21
2026-06-07 13:10:23 +02:00
parent e0bcdb8105
commit 2389632812
12 changed files with 133 additions and 62 deletions
+3 -2
View File
@@ -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 {
+5 -4
View File
@@ -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",
});
}
+4 -1
View File
@@ -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,
+6
View File
@@ -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");
}
+28 -8
View File
@@ -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;
}
+14
View File
@@ -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",
+2 -2
View File
@@ -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"] {
+25
View File
@@ -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:
+11
View File
@@ -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"],
+32 -40
View File
@@ -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();
}
+3 -3
View File
@@ -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 {
-2
View File
@@ -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);