mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-10 15:03:32 -07:00
website: redesign part 21
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user