From c3506339cdc2e41708abfaf3ec7288b8abf70ebe Mon Sep 17 00:00:00 2001 From: nym21 Date: Tue, 9 Jun 2026 11:26:19 +0200 Subject: [PATCH] website: redesign part 26 --- website_next/index.html | 5 + website_next/learn/charts/controls/style.css | 56 +++----- website_next/learn/charts/format.js | 141 +++++++++++++++---- website_next/learn/charts/index.js | 8 +- website_next/learn/charts/legend/style.css | 4 +- website_next/learn/contents/style.css | 25 ++-- website_next/learn/data/rolling-windows.js | 2 +- website_next/learn/index.js | 2 +- website_next/learn/style.css | 98 ++++++------- website_next/styles/reset.css | 1 + website_next/styles/variables.css | 2 - website_next/utils/event.js | 2 +- 12 files changed, 215 insertions(+), 131 deletions(-) diff --git a/website_next/index.html b/website_next/index.html index 597755411..e0c1d060d 100644 --- a/website_next/index.html +++ b/website_next/index.html @@ -59,6 +59,11 @@ } @media (prefers-reduced-motion: reduce) { + html { + --transition-duration: 0ms; + --reveal-duration: 0ms; + } + body::before { transition: none; } diff --git a/website_next/learn/charts/controls/style.css b/website_next/learn/charts/controls/style.css index d4f54afe4..303155292 100644 --- a/website_next/learn/charts/controls/style.css +++ b/website_next/learn/charts/controls/style.css @@ -40,62 +40,50 @@ main.learn { span { display: block; - padding: 0.25rem; - border-radius: 0.25rem; - color: var(--gray); - } - - label:hover 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); - } - - label:has(:focus-visible) span { - outline: 1px solid var(--orange); - outline-offset: 0.125rem; - } } button[data-chart="fullscreen"] { - padding: 0.25rem; border: 0; - border-radius: 0.25rem; - color: var(--gray); background: none; font: inherit; line-height: inherit; text-transform: uppercase; cursor: pointer; - &:hover { - color: var(--black); - background: var(--white); - } - &[aria-pressed="true"] { color: var(--black); background: var(--green); } + } - &:active { - color: var(--black); - background: var(--orange); - } + :is(label > span, button[data-chart="fullscreen"]) { + padding: 0.25rem; + border-radius: 0.25rem; + color: var(--gray); + } - &:focus-visible { - outline: 1px solid var(--orange); - outline-offset: 0.125rem; - } + :is(label:hover span, button[data-chart="fullscreen"]:hover) { + color: var(--black); + background: var(--white); + } + + :is(label:active span, button[data-chart="fullscreen"]:active) { + color: var(--black); + background: var(--orange); + } + + :is( + label:has(:focus-visible) span, + button[data-chart="fullscreen"]:focus-visible + ) { + outline: 1px solid var(--orange); + outline-offset: 0.125rem; } } } diff --git a/website_next/learn/charts/format.js b/website_next/learn/charts/format.js index 40a694c12..9691b366f 100644 --- a/website_next/learn/charts/format.js +++ b/website_next/learn/charts/format.js @@ -1,45 +1,134 @@ const suffixes = ["M", "B", "T", "P", "E", "Z", "Y"]; -const numberFormats = [0, 1, 2, 3].map( - (digits) => - new Intl.NumberFormat("en-US", { - maximumFractionDigits: digits, - minimumFractionDigits: digits, - }), -); -const percentFormat = new Intl.NumberFormat("en-US", { - maximumFractionDigits: 2, - minimumFractionDigits: 2, -}); +const compactBase = 1_000_000; +const compactStep = 1_000; +const compactMax = 1e27; +const maxLength = 7; +const tinyDigits = [3, 2, 1, 0]; +const smallDigits = [2, 1, 0]; +const mediumDigits = [1, 0]; +const integerDigits = [0]; +const numberFormats = createNumberFormats(true); +const ungroupedNumberFormats = createNumberFormats(false); + +/** @param {boolean} useGrouping */ +function createNumberFormats(useGrouping) { + return [0, 1, 2, 3].map( + (digits) => + new Intl.NumberFormat("en-US", { + maximumFractionDigits: digits, + minimumFractionDigits: digits, + useGrouping, + }), + ); +} /** * @param {number} value * @param {number} digits + * @param {boolean} [useGrouping] */ -function formatNumber(value, digits) { - return numberFormats[digits].format(value); +function formatNumber(value, digits, useGrouping = true) { + return (useGrouping ? numberFormats : ungroupedNumberFormats)[digits].format( + value, + ); } -/** @param {number} value */ -export function formatNumberValue(value) { +/** @param {string} value */ +function parseFormattedNumber(value) { + return Number(value.replaceAll(",", "")); +} + +/** @param {number} index */ +function getCompactFactor(index) { + return compactBase * compactStep ** index; +} + +/** @param {number} absolute */ +function getCompactIndex(absolute) { + return Math.max( + 0, + Math.min( + suffixes.length - 1, + Math.floor(Math.log10(absolute / compactBase) / 3), + ), + ); +} + +/** @param {number} absolute */ +function getPlainDigits(absolute) { + if (absolute < 10) return tinyDigits; + if (absolute < 1_000) return smallDigits; + if (absolute < 10_000) return mediumDigits; + return integerDigits; +} + +/** + * @param {number} value + * @param {number} index + * @param {number} length + * @param {boolean} useGrouping + */ +function formatCompact(value, index, length, useGrouping) { + for (let suffixIndex = index; suffixIndex < suffixes.length; suffixIndex += 1) { + const suffix = suffixes[suffixIndex]; + const scaled = value / getCompactFactor(suffixIndex); + + for (let digits = 3; digits >= 0; digits -= 1) { + const formatted = formatNumber(scaled, digits, useGrouping); + + if ( + Math.abs(parseFormattedNumber(formatted)) >= compactStep && + suffixIndex < suffixes.length - 1 + ) break; + + if (`${formatted}${suffix}`.length <= length) { + return `${formatted}${suffix}`; + } + } + } + + return "Inf."; +} + +/** + * @param {number} value + * @param {number} length + * @param {boolean} useGrouping + */ +function formatPlain(value, length, useGrouping) { + const absolute = Math.abs(value); + + for (const digit of getPlainDigits(absolute)) { + const formatted = formatNumber(value, digit, useGrouping); + + if (formatted.length <= length) return formatted; + } + + return formatCompact(value, 0, length, useGrouping); +} + +/** + * @param {number} value + * @param {number} length + * @param {boolean} [useGrouping] + */ +function formatValue(value, length, useGrouping = true) { if (value === 0) return "0"; const absolute = Math.abs(value); - if (absolute < 10) return formatNumber(value, 3); - if (absolute < 1_000) return formatNumber(value, 2); - if (absolute < 10_000) return formatNumber(value, 1); - if (absolute < 1_000_000) return formatNumber(value, 0); - if (absolute >= 1e27) return "Inf."; + if (absolute >= compactMax) return "Inf."; + if (absolute < compactBase) return formatPlain(value, length, useGrouping); - const log = Math.floor(Math.log10(absolute) - 6); - const suffixIndex = Math.floor(log / 3); - const digits = 3 - (log % 3); - const scaled = value / (1_000_000 * 1_000 ** suffixIndex); + return formatCompact(value, getCompactIndex(absolute), length, useGrouping); +} - return `${formatNumber(scaled, digits)}${suffixes[suffixIndex]}`; +/** @param {number} value */ +export function formatNumberValue(value) { + return formatValue(value, maxLength); } /** @param {number} value */ export function formatPercentValue(value) { - return value === 0 ? "0%" : `${percentFormat.format(value)}%`; + return `${formatValue(value, maxLength - 1, false)}%`; } diff --git a/website_next/learn/charts/index.js b/website_next/learn/charts/index.js index 3032e043a..311d561e9 100644 --- a/website_next/learn/charts/index.js +++ b/website_next/learn/charts/index.js @@ -25,10 +25,12 @@ import { } from "./views.js"; import { FALLBACK_VIEWBOX_HEIGHT, VIEWBOX_WIDTH } from "./viewbox.js"; -/** @param {Chart} chart */ -export function createChart(chart) { +/** + * @param {Chart} chart + * @param {string} chartKey + */ +export function createChart(chart, chartKey) { const figure = document.createElement("figure"); - const chartKey = chart.title; /** @type {ReturnType | undefined} */ let renderer; diff --git a/website_next/learn/charts/legend/style.css b/website_next/learn/charts/legend/style.css index a9728b67f..e8ee39287 100644 --- a/website_next/learn/charts/legend/style.css +++ b/website_next/learn/charts/legend/style.css @@ -19,11 +19,11 @@ main.learn { } time { - color: var(--off-color); + color: var(--gray); } span:is([data-chart="unit"], [data-chart="separator"]) { - color: var(--off-color); + color: var(--gray); } menu { diff --git a/website_next/learn/contents/style.css b/website_next/learn/contents/style.css index 0dcc2e523..95f3668e6 100644 --- a/website_next/learn/contents/style.css +++ b/website_next/learn/contents/style.css @@ -23,7 +23,7 @@ main.learn { } li + li { - margin-top: 0.25rem; + margin-block-start: 0.25rem; } a { @@ -31,11 +31,10 @@ main.learn { scroll-margin-block: var(--offset); color: inherit; text-decoration: none; - margin-right: 1rem; margin-block: -0.25rem; - margin-left: -0.5rem; + margin-inline: -0.5rem 1rem; padding: 0.25rem; - padding-left: 0.5rem; + padding-inline-start: 0.5rem; &::before { opacity: 0.5; @@ -61,6 +60,15 @@ main.learn { } } + ol ol { + margin-block-start: 0.25rem; + margin-inline-start: 1rem; + } + + ol ol ol ol { + margin-inline-start: 0.5rem; + } + > ol { > li { counter-reset: content-topic; @@ -79,9 +87,6 @@ main.learn { } > ol { - margin-top: 0.25rem; - margin-left: 1rem; - > li { counter-increment: content-topic; counter-reset: content-detail; @@ -91,9 +96,6 @@ main.learn { } > ol { - margin-top: 0.25rem; - margin-left: 1rem; - > li { counter-increment: content-detail; counter-reset: content-subtopic; @@ -103,9 +105,6 @@ main.learn { } > ol { - margin-top: 0.25rem; - margin-left: 0.5rem; - > li { counter-increment: content-subtopic; diff --git a/website_next/learn/data/rolling-windows.js b/website_next/learn/data/rolling-windows.js index 5418c5993..da92a6c49 100644 --- a/website_next/learn/data/rolling-windows.js +++ b/website_next/learn/data/rolling-windows.js @@ -1,7 +1,7 @@ import { createCohortSeries } from "./cohort-series.js"; import { colors } from "../../utils/colors.js"; -export const rollingWindows = /** @type {const} */ ([ +const rollingWindows = /** @type {const} */ ([ ["24h", "_24h", colors.sky], ["1w", "_1w", colors.cyan], ["1m", "_1m", colors.blue], diff --git a/website_next/learn/index.js b/website_next/learn/index.js index 58eeae5a3..e5966d9c2 100644 --- a/website_next/learn/index.js +++ b/website_next/learn/index.js @@ -26,7 +26,7 @@ function createSection(section, path = []) { heading.append(anchor); description.append(section.description); element.append(heading, description); - if (section.chart) element.append(createDataChart(section.chart)); + if (section.chart) element.append(createDataChart(section.chart, id)); for (const child of children) { element.append(createSection(child, sectionPath)); diff --git a/website_next/learn/style.css b/website_next/learn/style.css index 93fc0ae1d..a8a36c44b 100644 --- a/website_next/learn/style.css +++ b/website_next/learn/style.css @@ -79,21 +79,58 @@ main.learn { margin-top: 8rem; } - > section > section { - counter-increment: topic; - counter-reset: detail; - scroll-margin-top: var(--offset); + section[id] { + > :is(h1, h2, h3, h4) { + a { + position: relative; + display: inline-block; + color: var(--white); + text-decoration: none; + + &::before { + position: absolute; + top: 50%; + right: 100%; + translate: 0 -50%; + opacity: 0; + user-select: none; + text-decoration: none; + } + + &:hover::before { + opacity: 0.5; + } + + &:hover { + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 0.125em; + } + + &:active { + color: var(--orange); + } + } + } } - > section > section > section { - counter-increment: detail; - counter-reset: subtopic; - scroll-margin-top: var(--offset); - } + > section { + > section { + counter-increment: topic; + counter-reset: detail; + scroll-margin-top: var(--offset); - > section > section > section > section { - counter-increment: subtopic; - scroll-margin-top: var(--offset); + > section { + counter-increment: detail; + counter-reset: subtopic; + scroll-margin-top: var(--offset); + + > section { + counter-increment: subtopic; + scroll-margin-top: var(--offset); + } + } + } } section[id]:not([data-numbered="false"]) { @@ -153,7 +190,7 @@ main.learn { > p { margin-top: 1rem; - color: var(--dark-white); + color: var(--white); font-size: var(--font-size-sm); line-height: var(--line-height-sm); } @@ -164,40 +201,5 @@ main.learn { font-size: var(--font-size-xs); } } - - section[id] { - > :is(h1, h2, h3, h4) { - a { - position: relative; - display: inline-block; - color: var(--white); - text-decoration: none; - - &::before { - position: absolute; - top: 50%; - right: 100%; - translate: 0 -50%; - opacity: 0; - user-select: none; - text-decoration: none; - } - - &:hover::before { - opacity: 0.5; - } - - &:hover { - text-decoration: underline; - text-decoration-thickness: 1px; - text-underline-offset: 0.125em; - } - - &:active { - color: var(--orange); - } - } - } - } } } diff --git a/website_next/styles/reset.css b/website_next/styles/reset.css index 742d679a1..42cf603ba 100644 --- a/website_next/styles/reset.css +++ b/website_next/styles/reset.css @@ -22,6 +22,7 @@ body { /* 7. Inherit fonts for form controls */ input, button, +textarea, select { font: inherit; } diff --git a/website_next/styles/variables.css b/website_next/styles/variables.css index 43f2b6b19..886948ede 100644 --- a/website_next/styles/variables.css +++ b/website_next/styles/variables.css @@ -2,7 +2,6 @@ color-scheme: dark; --white: oklch(95% 0 0); - --dark-white: oklch(92.5% 0 0); --gray: oklch(55% 0 0); --black: oklch(15% 0 0); --red: oklch(0.607 0.241 26.328); @@ -23,7 +22,6 @@ --fuchsia: oklch(0.629 0.294 322.523); --pink: oklch(0.624 0.245 357.444); --rose: oklch(0.6155 0.2495 17.012); - --off-color: var(--gray); --font-size-xs: 0.75rem; --line-height-xs: calc(1 / 0.75); diff --git a/website_next/utils/event.js b/website_next/utils/event.js index 467de69d4..23d522a2e 100644 --- a/website_next/utils/event.js +++ b/website_next/utils/event.js @@ -2,7 +2,7 @@ * @param {Event} event * @param {string} selector */ -export function getEventTarget(event, selector) { +function getEventTarget(event, selector) { const target = event.target; return target instanceof Element ? target.closest(selector) : null;