website_next: part 7

This commit is contained in:
nym21
2026-07-06 18:52:51 +02:00
parent 4d7212fb63
commit 046c624abf
10 changed files with 201 additions and 127 deletions
-28
View File
@@ -35,34 +35,6 @@ export function createFeeRateRange(feeRates) {
});
}
/**
* @param {number} index
* @param {number} count
*/
export function getFeeRateStopByRank(index, count) {
const percentile = count === 1 ? 100 : 100 - (index / (count - 1)) * 100;
for (let stopIndex = 0; stopIndex < FEE_RATE_PERCENTILES.length; stopIndex += 1) {
if (percentile <= FEE_RATE_PERCENTILES[stopIndex]) {
return FEE_RATE_STOPS[stopIndex];
}
}
return FEE_RATE_STOPS[FEE_RATE_STOPS.length - 1];
}
/**
* @param {number} feeRate
* @param {number[]} ranges
*/
export function getFeeRateStop(feeRate, ranges) {
for (let index = 0; index < ranges.length; index += 1) {
if (feeRate <= ranges[index]) return FEE_RATE_STOPS[index];
}
return FEE_RATE_STOPS[FEE_RATE_STOPS.length - 1];
}
/**
* @param {number} feeRate
* @param {number[]} ranges
+1 -15
View File
@@ -1,8 +1,4 @@
import {
createFeeRateRange,
FEE_RATE_STOPS,
getFeeRateStopByRank,
} from "../fee-rates.js";
import { createFeeRateRange } from "../fee-rates.js";
/**
* @param {BlockPreviewTransaction[]} transactions
@@ -11,14 +7,6 @@ export function createPreviewFeeRange(transactions) {
return createFeeRateRange(transactions.map(({ feeRate }) => feeRate));
}
/**
* @param {number} index
* @param {number} count
*/
export function getFeeBucket(index, count) {
return getFeeRateStopByRank(index, count);
}
/**
* @param {BlockPreviewTransaction[]} transactions
*/
@@ -34,5 +22,3 @@ export function orderTransactions(transactions) {
* @property {number} weight
* @property {number} feeRate
*/
export { FEE_RATE_STOPS as FEE_BUCKETS };
@@ -0,0 +1,113 @@
import {
appendLegendListItem,
createLegendItem,
createLegendList,
} from "../../../legend/index.js";
import { formatNumber } from "../format.js";
const VERSION_FILTERS = /** @type {const} */ ([1, 2, 3]);
/** @param {number} version */
export function getVersionKey(version) {
return String(version);
}
/**
* @param {BlockPreviewTransaction[]} transactions
* @returns {Map<number, number>}
*/
function countVersions(transactions) {
const counts = new Map();
for (const transaction of transactions) {
counts.set(transaction.version, (counts.get(transaction.version) ?? 0) + 1);
}
return counts;
}
/**
* @param {HTMLElement} heatmap
* @returns {Map<string, HTMLElement[]>}
*/
function groupCells(heatmap) {
const groups = new Map();
const cells = /** @type {HTMLElement[]} */ ([
...heatmap.querySelectorAll("[data-heatmap-cell]"),
]);
for (const cell of cells) {
const key = /** @type {string} */ (cell.dataset.heatmapCell);
let group = groups.get(key);
if (group === undefined) {
group = [];
groups.set(key, group);
}
group.push(cell);
}
return groups;
}
/**
* @param {HTMLButtonElement} button
* @param {boolean} active
*/
function setActive(button, active) {
button.setAttribute("aria-pressed", String(active));
button.toggleAttribute("data-muted", !active);
}
/**
* @param {HTMLButtonElement} button
*/
function setPending(button) {
button.disabled = true;
button.setAttribute("aria-pressed", "false");
button.removeAttribute("data-muted");
}
/**
* @param {BlockPreviewTransaction[]} transactions
* @param {HTMLElement | null} heatmap
* @param {Object} [options]
* @param {boolean} [options.pending]
*/
export function createVersionFilters(transactions, heatmap, options = {}) {
const list = createLegendList({ scroll: true });
const counts = countVersions(transactions);
const cells = heatmap === null ? new Map() : groupCells(heatmap);
const pending = options.pending === true;
for (const version of VERSION_FILTERS) {
const count = counts.get(version) ?? 0;
const key = getVersionKey(version);
const { button, value } = createLegendItem({
label: `tx v${version}`,
color: "var(--white)",
ariaLabel: `Transaction version ${version}`,
});
value.textContent = pending ? "..." : formatNumber(count);
if (pending) {
setPending(button);
} else {
button.addEventListener("click", () => {
const active = button.getAttribute("aria-pressed") !== "true";
setActive(button, active);
for (const cell of cells.get(key) ?? []) {
cell.toggleAttribute("data-muted", !active);
}
});
setActive(button, true);
}
appendLegendListItem(list, button);
}
return list;
}
/** @typedef {import("./fees.js").BlockPreviewTransaction} BlockPreviewTransaction */
+34 -28
View File
@@ -3,21 +3,17 @@ import { formatFeeRate } from "../../../utils/fee-rate.js";
import { formatWeight, MAX_BLOCK_WEIGHT } from "../format.js";
import { getFeeRateColor } from "../fee-rates.js";
import { loadBlockPreview } from "./data.js";
import { createPreviewFeeRange, getFeeBucket, orderTransactions } from "./fees.js";
import { createPreviewLegend } from "./legend.js";
import { createPreviewFeeRange, orderTransactions } from "./fees.js";
import { createVersionFilters, getVersionKey } from "./filters.js";
/**
* @param {BlockPreviewTransaction} transaction
* @param {number[]} ranges
* @param {number} index
* @param {number} count
*/
function createPreviewItem(transaction, ranges, index, count) {
const bucket = getFeeBucket(index, count);
function createPreviewItem(transaction, ranges) {
return {
color: getFeeRateColor(transaction.feeRate, ranges),
group: bucket.label,
group: getVersionKey(transaction.version),
weight: transaction.weight,
title: [
transaction.txid,
@@ -28,34 +24,42 @@ function createPreviewItem(transaction, ranges, index, count) {
};
}
/**
* @param {HTMLElement} body
* @param {HTMLElement} filters
*/
function createFigure(body, filters) {
const figure = document.createElement("figure");
const caption = document.createElement("figcaption");
const title = document.createElement("h5");
figure.dataset.blockPreviewFigure = "";
caption.dataset.blockPreviewLegend = "";
title.append("Filters");
caption.append(title, filters);
figure.append(caption, body);
return figure;
}
/**
* @param {HTMLElement} content
* @param {BlockPreviewTransaction[]} transactions
*/
function renderPreview(content, transactions) {
const figure = document.createElement("figure");
const caption = document.createElement("figcaption");
const title = document.createElement("h5");
const ordered = orderTransactions(transactions);
const ranges = createPreviewFeeRange(ordered);
const items = ordered.map((transaction, index) => {
return createPreviewItem(transaction, ranges, index, ordered.length);
const items = ordered.map((transaction) => {
return createPreviewItem(transaction, ranges);
});
const heatmap = createHeatmap(items, {
origin: "bottom",
shape: "square",
capacity: MAX_BLOCK_WEIGHT,
columns: 84,
});
figure.dataset.blockPreviewFigure = "";
caption.dataset.blockPreviewLegend = "";
title.append("Fees");
caption.append(title, createPreviewLegend(ranges));
figure.append(
caption,
createHeatmap(items, {
origin: "bottom",
shape: "square",
capacity: MAX_BLOCK_WEIGHT,
columns: 84,
}),
);
content.replaceChildren(figure);
content.replaceChildren(createFigure(heatmap, createVersionFilters(ordered, heatmap)));
}
/**
@@ -67,7 +71,9 @@ function renderStatus(content, status) {
p.dataset.blockPreviewStatus = status;
p.textContent = status;
content.replaceChildren(p);
content.replaceChildren(createFigure(p, createVersionFilters([], null, {
pending: true,
})));
}
/**
@@ -1,29 +0,0 @@
import {
appendLegendListItem,
createLegendItem,
createLegendList,
} from "../../../legend/index.js";
import { formatFeeRate } from "../../../utils/fee-rate.js";
import { FEE_BUCKETS } from "./fees.js";
/**
* @param {number[]} ranges
*/
export function createPreviewLegend(ranges) {
const list = createLegendList({ scroll: true });
for (let index = 0; index < FEE_BUCKETS.length; index += 1) {
const bucket = FEE_BUCKETS[index];
const { button, value } = createLegendItem({
label: bucket.label,
color: bucket.color,
ariaLabel: `${bucket.label} fee rate percentile`,
detail: "sat/vB",
});
value.textContent = formatFeeRate(ranges[index]);
appendLegendListItem(list, button);
}
return list;
}
@@ -23,6 +23,10 @@
}
[data-block-preview-status] {
display: grid;
aspect-ratio: 1;
margin: 0;
place-items: center;
color: var(--gray);
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
@@ -1,48 +1,36 @@
#block-details section[data-group="preview"] {
[data-block-meta] {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.5rem;
grid-template-columns: minmax(0, 1fr) repeat(3, max-content);
gap: 1.5rem;
min-width: 0;
}
[data-block-meta-item] {
display: grid;
gap: 0.125rem;
display: flex;
gap: 0.5rem;
align-items: baseline;
justify-content: space-between;
min-width: 0;
> span {
color: var(--block-meta-color);
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
flex: 0 0 auto;
color: var(--gray);
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
text-transform: uppercase;
}
strong {
min-width: 0;
overflow-wrap: anywhere;
color: var(--block-meta-color);
color: var(--white);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-strong);
line-height: var(--line-height-sm);
text-align: right;
white-space: nowrap;
}
}
[data-block-meta-item="block"] {
--block-meta-color: var(--cyan);
}
[data-block-meta-item="tx"] {
--block-meta-color: var(--orange);
}
[data-block-meta-item="input"] {
--block-meta-color: var(--yellow);
}
[data-block-meta-item="output"] {
--block-meta-color: var(--red);
}
}
@media (max-width: 48rem) {
+26 -2
View File
@@ -25,6 +25,31 @@ function resolveCapacityCells(cells, capacity, columns) {
}));
}
/**
* @template {CapacityCell} Cell
* @param {readonly Cell[]} cells
* @param {number} capacity
* @param {number} columns
*/
function fitCapacityCells(cells, capacity, columns) {
let resolvedCells = resolveCapacityCells(cells, capacity, columns);
let layouts = packCells(resolvedCells, columns, columns);
while (layouts === null) {
const largest = Math.max(...resolvedCells.map(({ span }) => span));
if (largest <= 1) break;
resolvedCells = resolvedCells.map((cell) => ({
...cell,
span: cell.span === largest ? largest - 1 : cell.span,
}));
layouts = packCells(resolvedCells, columns, columns);
}
return { resolvedCells, layouts };
}
/**
* @template {CapacityCell} Cell
* @param {readonly Cell[]} cells
@@ -32,8 +57,7 @@ function resolveCapacityCells(cells, capacity, columns) {
* @param {number} columns
*/
export function createSquareLayout(cells, capacity, columns) {
const resolvedCells = resolveCapacityCells(cells, capacity, columns);
const layouts = packCells(resolvedCells, columns);
const { resolvedCells, layouts } = fitCapacityCells(cells, capacity, columns);
return { columns, resolvedCells, layouts: /** @type {NonNullable<typeof layouts>} */ (layouts) };
}
+1
View File
@@ -126,6 +126,7 @@ export function createHeatmap(items, options = {}) {
}));
map.dataset.heatmap = options.origin ?? "";
if (options.shape != null) map.dataset.heatmapShape = options.shape;
map.append(...cells.map(({ node }) => node));
observeLayout(
map,
+9
View File
@@ -7,12 +7,21 @@
overflow: hidden;
}
[data-heatmap-shape="square"] {
aspect-ratio: 1;
}
[data-heatmap-cell] {
position: absolute;
left: var(--x);
width: var(--size);
height: var(--size);
background: var(--color);
transition: opacity 150ms;
&[data-muted] {
opacity: 0.12;
}
}
[data-heatmap="bottom"] > [data-heatmap-cell] {