mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-18 22:04:47 -07:00
website: snap
This commit is contained in:
@@ -139,7 +139,6 @@
|
||||
<aside id="aside">
|
||||
<div id="explorer" hidden></div>
|
||||
<div id="chart" hidden></div>
|
||||
<div id="table" hidden></div>
|
||||
</aside>
|
||||
<footer>
|
||||
<fieldset id="frame-selectors">
|
||||
|
||||
@@ -114,7 +114,7 @@ export function init(selected) {
|
||||
function startPolling() {
|
||||
stopPolling();
|
||||
poll();
|
||||
pollInterval = setInterval(poll, 5_000);
|
||||
pollInterval = setInterval(poll, 1_000);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
|
||||
@@ -388,7 +388,7 @@ export function initOptions() {
|
||||
summary.append(node.name);
|
||||
|
||||
const count = window.document.createElement("small");
|
||||
count.textContent = `(${node.count.toLocaleString("en-us")})`;
|
||||
count.textContent = `[${node.count.toLocaleString("en-us")}]`;
|
||||
summary.append(count);
|
||||
|
||||
let built = false;
|
||||
|
||||
@@ -29,7 +29,7 @@ export function initPrice(brk) {
|
||||
}
|
||||
|
||||
poll();
|
||||
setInterval(poll, 5_000);
|
||||
setInterval(poll, 1_000);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
!document.hidden && poll();
|
||||
});
|
||||
|
||||
@@ -19,16 +19,33 @@ export function onChange(callback) {
|
||||
return () => callbacks.delete(callback);
|
||||
}
|
||||
|
||||
const themeButton = /** @type {HTMLButtonElement | null} */ (
|
||||
document.getElementById("theme-button")
|
||||
);
|
||||
let running = false;
|
||||
|
||||
/** @param {boolean} value */
|
||||
function setDark(value) {
|
||||
if (dark === value) return;
|
||||
if (running || dark === value) return;
|
||||
dark = value;
|
||||
running = true;
|
||||
if (themeButton) themeButton.disabled = true;
|
||||
const swap = () => {
|
||||
apply(value);
|
||||
callbacks.forEach((cb) => cb());
|
||||
};
|
||||
if (document.startViewTransition) document.startViewTransition(swap);
|
||||
else swap();
|
||||
document.documentElement.classList.add("no-transitions");
|
||||
const restore = () => {
|
||||
document.documentElement.classList.remove("no-transitions");
|
||||
running = false;
|
||||
if (themeButton) themeButton.disabled = false;
|
||||
};
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(swap).finished.finally(restore);
|
||||
} else {
|
||||
swap();
|
||||
requestAnimationFrame(restore);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {boolean} isDark */
|
||||
@@ -53,4 +70,4 @@ function invert() {
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("theme-button")?.addEventListener("click", invert);
|
||||
themeButton?.addEventListener("click", invert);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/**
|
||||
* @param {number} [fill]
|
||||
* @returns {{ el: HTMLAnchorElement, topFace: HTMLDivElement, rightFace: HTMLDivElement, leftFace: HTMLDivElement }}
|
||||
*/
|
||||
export function createCubeAnchor(fill = 1) {
|
||||
const el = document.createElement("a");
|
||||
@@ -9,7 +8,6 @@ export function createCubeAnchor(fill = 1) {
|
||||
|
||||
/**
|
||||
* @param {number} [fill]
|
||||
* @returns {{ el: HTMLDivElement, topFace: HTMLDivElement, rightFace: HTMLDivElement, leftFace: HTMLDivElement }}
|
||||
*/
|
||||
export function createCubeDiv(fill = 1) {
|
||||
const el = document.createElement("div");
|
||||
|
||||
@@ -1,43 +1,38 @@
|
||||
@property --pulse-mix {
|
||||
syntax: "<number>";
|
||||
inherits: true;
|
||||
initial-value: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--cube-size: 4.5rem;
|
||||
--iso-scale: calc(sqrt(3) / 2);
|
||||
--cube-empty-alpha: 0.4;
|
||||
--cube-ease: 50ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--face-step: 0.033;
|
||||
}
|
||||
|
||||
@keyframes cube-pulse {
|
||||
0%, 100% { filter: brightness(1); }
|
||||
50% { filter: brightness(1.1); }
|
||||
}
|
||||
|
||||
.cube {
|
||||
--cube-width: calc(var(--cube-size) * 2 * var(--iso-scale));
|
||||
--cube-width: calc(var(--cube-size) * 2 * var(--iso-scale));
|
||||
--cube-height: calc(var(--cube-size) * 2);
|
||||
|
||||
--face-base-light: var(--light-gray);
|
||||
--face-base-dark: var(--dark-gray);
|
||||
--face-bottom-base: var(--border-color);
|
||||
|
||||
/* Light mode darkens from base, dark mode lightens. The asymmetry
|
||||
can't be encoded in a single base, so each face picks its branch
|
||||
from the matching flat base. State classes swap the bases only,
|
||||
the per-face pattern stays. */
|
||||
--cube-face-base: var(--border-color);
|
||||
--face-top: light-dark(
|
||||
var(--face-base-light),
|
||||
oklch(from var(--face-base-dark) calc(l + var(--face-step) * 2) c h)
|
||||
var(--cube-face-base),
|
||||
oklch(from var(--cube-face-base) calc(l + var(--face-step) * 2) c h)
|
||||
);
|
||||
--face-right: light-dark(
|
||||
oklch(from var(--face-base-light) calc(l - var(--face-step) * 2) c h),
|
||||
var(--face-base-dark)
|
||||
oklch(from var(--cube-face-base) calc(l - var(--face-step) * 2) c h),
|
||||
var(--cube-face-base)
|
||||
);
|
||||
--face-left: light-dark(
|
||||
oklch(from var(--face-base-light) calc(l - var(--face-step)) c h),
|
||||
oklch(from var(--face-base-dark) calc(l + var(--face-step)) c h)
|
||||
oklch(from var(--cube-face-base) calc(l - var(--face-step)) c h),
|
||||
oklch(from var(--cube-face-base) calc(l + var(--face-step)) c h)
|
||||
);
|
||||
--face-bottom: oklch(
|
||||
from var(--cube-face-base) calc(l - var(--face-step) * 3) c h
|
||||
);
|
||||
--face-bottom: oklch(from var(--face-bottom-base) calc(l - var(--face-step) * 3) c h);
|
||||
|
||||
--is-full: round(down, var(--fill), 1);
|
||||
--is-full: round(down, var(--fill), 1);
|
||||
--is-empty: round(down, calc(1 - var(--fill)), 1);
|
||||
|
||||
flex-shrink: 0;
|
||||
@@ -49,38 +44,43 @@
|
||||
overflow: visible;
|
||||
text-decoration: none;
|
||||
color: var(--color);
|
||||
transition: color var(--cube-ease);
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover {
|
||||
/* Hover/active styling is gated on the anchor tag: only <a> cubes
|
||||
(confirmed blocks) react to pointer state. Plain <div> projected
|
||||
previews stay visually inert. --face-color-base is resolved lazily,
|
||||
so the orange override below feeds the face declarations too. */
|
||||
&:is(a):hover,
|
||||
&:is(a):active,
|
||||
&.selected {
|
||||
color: var(--background-color);
|
||||
--face-base-light: var(--dark-gray);
|
||||
--face-base-dark: var(--light-gray);
|
||||
--face-bottom-base: var(--inv-border-color);
|
||||
--face-color-base: var(--inv-border-color);
|
||||
--face-top: var(--face-color-base);
|
||||
--face-right: oklch(
|
||||
from var(--face-color-base) calc(l - var(--face-step) * 2) c h
|
||||
);
|
||||
--face-left: oklch(
|
||||
from var(--face-color-base) calc(l - var(--face-step)) c h
|
||||
);
|
||||
--face-bottom: oklch(
|
||||
from var(--face-color-base) calc(l - var(--face-step) * 3) c h
|
||||
);
|
||||
}
|
||||
|
||||
/* Color states: override --face-* directly with a fixed
|
||||
darken-from-base derivation so the cube renders identically in
|
||||
light and dark mode (no theme-flip on the colored faces). */
|
||||
&:active,
|
||||
&.selected { color: var(--black); --face-color-base: var(--orange); }
|
||||
&.next { --face-color-base: var(--cyan); }
|
||||
&.projected{ --face-color-base: var(--off-color); }
|
||||
|
||||
&:active,
|
||||
&.selected,
|
||||
&.next,
|
||||
&.projected {
|
||||
--face-top: var(--face-color-base);
|
||||
--face-right: oklch(from var(--face-color-base) calc(l - var(--face-step) * 2) c h);
|
||||
--face-left: oklch(from var(--face-color-base) calc(l - var(--face-step)) c h);
|
||||
--face-bottom: oklch(from var(--face-color-base) calc(l - var(--face-step) * 3) c h);
|
||||
&:is(a):active,
|
||||
&.selected {
|
||||
color: var(--black);
|
||||
--face-color-base: var(--orange);
|
||||
}
|
||||
|
||||
&.next,
|
||||
&.projected {
|
||||
animation: cube-pulse 2.5s ease-in-out infinite;
|
||||
animation: cube-pulse 4s ease-in-out infinite;
|
||||
--cube-face-base: color-mix(
|
||||
in oklch,
|
||||
var(--border-color),
|
||||
var(--background-color) calc(var(--pulse-mix) * 100%)
|
||||
);
|
||||
}
|
||||
|
||||
/* visibility (not color:transparent) so child <img> hides too */
|
||||
@@ -94,10 +94,11 @@
|
||||
box-sizing: border-box;
|
||||
width: var(--cube-size);
|
||||
height: var(--cube-size);
|
||||
transform:
|
||||
translateY(50%)
|
||||
var(--face-orient)
|
||||
translate(calc(var(--cube-size) * var(--face-x)), calc(var(--cube-size) * var(--face-y)))
|
||||
transform: translateY(50%) var(--face-orient)
|
||||
translate(
|
||||
calc(var(--cube-size) * var(--face-x)),
|
||||
calc(var(--cube-size) * var(--face-y))
|
||||
)
|
||||
scale(var(--face-scale-x, 1), var(--face-scale-y));
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -107,53 +108,51 @@
|
||||
.liquid,
|
||||
.glass {
|
||||
will-change: background-color;
|
||||
transition: background-color var(--cube-ease);
|
||||
}
|
||||
.liquid {
|
||||
background: var(--face-color);
|
||||
opacity: calc(1 - var(--is-empty));
|
||||
--face-scale-y: calc(var(--iso-scale) * var(--fill));
|
||||
--face-scale-y: calc(var(--iso-scale) * var(--fill));
|
||||
--face-stack-shift: calc(var(--iso-scale) * (1 - var(--fill)));
|
||||
}
|
||||
.glass {
|
||||
background: oklch(from var(--face-color) l c h / var(--cube-empty-alpha));
|
||||
--face-scale-y: calc(var(--iso-scale) * (1 - var(--fill)));
|
||||
--face-scale-y: calc(var(--iso-scale) * (1 - var(--fill)));
|
||||
--face-stack-shift: 0;
|
||||
}
|
||||
.glass.top {
|
||||
opacity: calc(1 - var(--is-full));
|
||||
}
|
||||
|
||||
.face-text {
|
||||
--face-scale-y: var(--iso-scale);
|
||||
--face-scale-y: var(--iso-scale);
|
||||
--face-stack-shift: 0;
|
||||
pointer-events: none;
|
||||
padding: 0.1rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 450;
|
||||
}
|
||||
.face-text.top,
|
||||
.face-text.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.face-text.top {
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.face-text.right {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.face-text p {
|
||||
margin: 0;
|
||||
|
||||
&.top {
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
&.right {
|
||||
justify-content: space-between;
|
||||
}
|
||||
&.left {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.top,
|
||||
.bottom {
|
||||
--face-orient: rotate(30deg) skewX(-30deg);
|
||||
--face-orient: rotate(30deg) skewX(-30deg);
|
||||
--face-scale-y: var(--iso-scale);
|
||||
}
|
||||
.right,
|
||||
@@ -164,7 +163,6 @@
|
||||
.rear-right {
|
||||
--face-orient: rotate(30deg) skewX(30deg);
|
||||
}
|
||||
|
||||
.top,
|
||||
.rear-right {
|
||||
--face-y: calc(var(--face-stack-shift) - var(--iso-scale));
|
||||
@@ -179,13 +177,42 @@
|
||||
.bottom {
|
||||
--face-y: 0;
|
||||
}
|
||||
|
||||
.top { --face-color: var(--face-top); --face-x: 0; }
|
||||
.bottom { --face-color: var(--face-bottom); --face-x: 1; }
|
||||
.right { --face-color: var(--face-right); --face-x: 1; }
|
||||
.left { --face-color: var(--face-left); --face-x: 0; }
|
||||
.rear-right { --face-color: var(--face-left); --face-x: 1; }
|
||||
.rear-left { --face-color: var(--face-top); --face-x: 1; --face-scale-x: -1; }
|
||||
/* Top liquid face slides as fill drops, animating the surface level. */
|
||||
.liquid.top { --face-x: calc(1 - var(--fill)); }
|
||||
.top {
|
||||
--face-color: var(--face-top);
|
||||
--face-x: 0;
|
||||
}
|
||||
.bottom {
|
||||
--face-color: var(--face-bottom);
|
||||
--face-x: 1;
|
||||
}
|
||||
.right {
|
||||
--face-color: var(--face-right);
|
||||
--face-x: 1;
|
||||
}
|
||||
.left {
|
||||
--face-color: var(--face-left);
|
||||
--face-x: 0;
|
||||
}
|
||||
.rear-right {
|
||||
--face-color: var(--face-left);
|
||||
--face-x: 1;
|
||||
}
|
||||
.rear-left {
|
||||
--face-color: var(--face-top);
|
||||
--face-x: 1;
|
||||
--face-scale-x: -1;
|
||||
}
|
||||
.liquid.top {
|
||||
--face-x: calc(1 - var(--fill));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cube-pulse {
|
||||
0%,
|
||||
100% {
|
||||
--pulse-mix: 0.5;
|
||||
}
|
||||
50% {
|
||||
--pulse-mix: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ const MONTHS = [
|
||||
"Dec",
|
||||
];
|
||||
|
||||
/** @type {HTMLElement} */ let explorerEl;
|
||||
/** @type {HTMLDivElement} */ let chainEl;
|
||||
/** @type {HTMLDivElement} */ let scrollEl;
|
||||
/** @type {HTMLDivElement} */ let confirmedEl;
|
||||
/** @type {HTMLDivElement} */ let projectedEl;
|
||||
/** @type {HTMLDivElement} */ let blocksEl;
|
||||
/** @type {HTMLAnchorElement | null} */ let selectedCube = null;
|
||||
/** @type {IntersectionObserver} */ let olderEdgeObserver;
|
||||
/** @type {(block: BlockInfoV1) => void} */ let onSelect = () => {};
|
||||
@@ -37,7 +37,7 @@ const MONTHS = [
|
||||
|
||||
/** @type {Map<BlockHash, BlockInfoV1>} */
|
||||
const blocksByHash = new Map();
|
||||
/** @type {Array<{ el: HTMLDivElement, topFace: HTMLDivElement, rightFace: HTMLDivElement, leftFace: HTMLDivElement }>} */
|
||||
/** @type {ReturnType<typeof createProjectedCube>[]} */
|
||||
const projectedCubes = [];
|
||||
|
||||
let newestHeight = -1;
|
||||
@@ -61,6 +61,7 @@ export function initChain(parent, callbacks) {
|
||||
onCubeClick = callbacks.onCubeClick;
|
||||
onTip = callbacks.onTip;
|
||||
onGenesis = callbacks.onGenesis;
|
||||
explorerEl = parent;
|
||||
|
||||
chainEl = document.createElement("div");
|
||||
chainEl.id = "chain";
|
||||
@@ -72,17 +73,12 @@ export function initChain(parent, callbacks) {
|
||||
);
|
||||
|
||||
scrollEl = document.createElement("div");
|
||||
scrollEl.classList.add("chain-scroll");
|
||||
scrollEl.classList.add("scroll");
|
||||
chainEl.append(scrollEl);
|
||||
|
||||
projectedEl = document.createElement("div");
|
||||
projectedEl.classList.add("projected");
|
||||
projectedEl.hidden = true;
|
||||
scrollEl.append(projectedEl);
|
||||
|
||||
confirmedEl = document.createElement("div");
|
||||
confirmedEl.classList.add("confirmed");
|
||||
scrollEl.append(confirmedEl);
|
||||
blocksEl = document.createElement("div");
|
||||
blocksEl.classList.add("blocks");
|
||||
scrollEl.append(blocksEl);
|
||||
|
||||
olderEdgeObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
@@ -140,7 +136,10 @@ export async function goToCube(hashOrHeight, { silent } = {}) {
|
||||
selectCube(cube, { scroll: "smooth", silent });
|
||||
return;
|
||||
}
|
||||
for (const cube of confirmedEl.children) cube.classList.add("skeleton");
|
||||
for (const cube of blocksEl.children) {
|
||||
if (!cube.classList.contains("projected")) cube.classList.add("skeleton");
|
||||
}
|
||||
explorerEl.classList.add("loading");
|
||||
let startHash;
|
||||
try {
|
||||
const height = await resolveHeight(hashOrHeight);
|
||||
@@ -149,6 +148,7 @@ export async function goToCube(hashOrHeight, { silent } = {}) {
|
||||
try {
|
||||
startHash = await loadInitial(null);
|
||||
} catch (_) {
|
||||
explorerEl.classList.remove("loading");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -156,14 +156,12 @@ export async function goToCube(hashOrHeight, { silent } = {}) {
|
||||
scroll: "instant",
|
||||
silent,
|
||||
});
|
||||
explorerEl.classList.remove("loading");
|
||||
}
|
||||
|
||||
export async function poll() {
|
||||
if (!reachedTip) return;
|
||||
brk
|
||||
.getMempoolBlocks()
|
||||
.then(renderProjected)
|
||||
.catch((e) => console.error("mempool poll:", e));
|
||||
pollProjected();
|
||||
try {
|
||||
const blocks = await brk.getBlocksV1();
|
||||
appendNewerBlocks(blocks);
|
||||
@@ -172,16 +170,32 @@ export async function poll() {
|
||||
}
|
||||
}
|
||||
|
||||
function pollProjected() {
|
||||
return brk
|
||||
.getMempoolBlocks()
|
||||
.then(renderProjected)
|
||||
.catch((e) => console.error("mempool poll:", e));
|
||||
}
|
||||
|
||||
/** @param {BlockHash | Height | null} [hashOrHeight] */
|
||||
function findCube(hashOrHeight) {
|
||||
if (hashOrHeight == null) {
|
||||
return reachedTip && newestHeight >= 0
|
||||
? /** @type {HTMLAnchorElement | null} */ (confirmedEl.lastElementChild)
|
||||
: null;
|
||||
return reachedTip && newestHeight >= 0 ? newestConfirmedCube() : null;
|
||||
}
|
||||
const attr = typeof hashOrHeight === "number" ? "height" : "hash";
|
||||
return /** @type {HTMLAnchorElement | null} */ (
|
||||
confirmedEl.querySelector(`[data-${attr}="${hashOrHeight}"]`)
|
||||
blocksEl.querySelector(`[data-${attr}="${hashOrHeight}"]`)
|
||||
);
|
||||
}
|
||||
|
||||
function firstProjectedCube() {
|
||||
return projectedCubes[0]?.el ?? null;
|
||||
}
|
||||
|
||||
function newestConfirmedCube() {
|
||||
const firstProj = firstProjectedCube();
|
||||
return /** @type {HTMLAnchorElement | null} */ (
|
||||
firstProj ? firstProj.previousElementSibling : blocksEl.lastElementChild
|
||||
);
|
||||
}
|
||||
|
||||
@@ -193,20 +207,21 @@ function clear() {
|
||||
loadingNewer = false;
|
||||
reachedTip = false;
|
||||
selectedCube = null;
|
||||
confirmedEl.innerHTML = "";
|
||||
blocksEl.innerHTML = "";
|
||||
projectedCubes.length = 0;
|
||||
olderEdgeObserver.disconnect();
|
||||
}
|
||||
|
||||
function observeOldestEdge() {
|
||||
olderEdgeObserver.disconnect();
|
||||
const oldest = confirmedEl.firstElementChild;
|
||||
const oldest = blocksEl.firstElementChild;
|
||||
if (oldest) olderEdgeObserver.observe(oldest);
|
||||
}
|
||||
|
||||
/** @param {BlockInfoV1[]} blocks */
|
||||
function appendNewerBlocks(blocks) {
|
||||
if (!blocks.length) return false;
|
||||
const anchor = confirmedEl.lastElementChild;
|
||||
const anchor = newestConfirmedCube();
|
||||
const anchorRect = anchor?.getBoundingClientRect();
|
||||
for (let i = blocks.length - 1; i >= 0; i--) {
|
||||
const b = blocks[i];
|
||||
@@ -215,7 +230,7 @@ function appendNewerBlocks(blocks) {
|
||||
}
|
||||
newestHeight = Math.max(newestHeight, blocks[0].height);
|
||||
newestTimestamp = blocks[0].timestamp;
|
||||
refreshProjectedIntervals();
|
||||
refreshProjected();
|
||||
if (anchor && anchorRect) {
|
||||
const r = anchor.getBoundingClientRect();
|
||||
scrollEl.scrollTop += r.top - anchorRect.top;
|
||||
@@ -240,6 +255,9 @@ async function loadInitial(height) {
|
||||
observeOldestEdge();
|
||||
|
||||
if (!reachedTip) await loadNewer();
|
||||
// Await the projected cubes so the layout is complete before the caller
|
||||
// scrolls to the tip; otherwise they load late and push the tip out of view.
|
||||
else await pollProjected();
|
||||
return blocks[0].id;
|
||||
}
|
||||
|
||||
@@ -278,8 +296,10 @@ async function loadNewer() {
|
||||
try {
|
||||
const prevNewest = newestHeight;
|
||||
const blocks = await brk.getBlocksV1FromHeight(newestHeight + LOOKAHEAD);
|
||||
if (!appendNewerBlocks(blocks) || newestHeight === prevNewest)
|
||||
if (!appendNewerBlocks(blocks) || newestHeight === prevNewest) {
|
||||
reachedTip = true;
|
||||
await pollProjected();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("explorer loadNewer:", e);
|
||||
}
|
||||
@@ -354,99 +374,111 @@ function setConfirmedInterval(cube) {
|
||||
|
||||
/** @param {HTMLAnchorElement} cube */
|
||||
function prependConfirmed(cube) {
|
||||
const next = /** @type {HTMLElement | null} */ (
|
||||
confirmedEl.firstElementChild
|
||||
);
|
||||
confirmedEl.prepend(cube);
|
||||
if (next) setConfirmedInterval(next);
|
||||
const oldFirst = /** @type {HTMLElement | null} */ (blocksEl.firstElementChild);
|
||||
blocksEl.insertBefore(cube, oldFirst);
|
||||
if (oldFirst) setConfirmedInterval(oldFirst);
|
||||
}
|
||||
|
||||
/** @param {HTMLAnchorElement} cube */
|
||||
function appendConfirmed(cube) {
|
||||
confirmedEl.append(cube);
|
||||
blocksEl.insertBefore(cube, firstProjectedCube());
|
||||
setConfirmedInterval(cube);
|
||||
}
|
||||
|
||||
/** @param {MempoolBlock[]} blocks */
|
||||
function renderProjected(blocks) {
|
||||
const want = Math.min(blocks.length, PROJECTED_LIMIT);
|
||||
projectedEl.hidden = want === 0;
|
||||
|
||||
while (projectedCubes.length > want) {
|
||||
const last = projectedCubes.pop();
|
||||
if (last) last.el.remove();
|
||||
}
|
||||
while (projectedCubes.length < want) {
|
||||
const cube = createProjectedCube(projectedCubes.length);
|
||||
const cube = createProjectedCube();
|
||||
projectedCubes.push(cube);
|
||||
projectedEl.append(cube.el);
|
||||
blocksEl.append(cube.el);
|
||||
}
|
||||
for (let i = 0; i < want; i++)
|
||||
updateProjectedCube(projectedCubes[i], blocks[i], i);
|
||||
refreshProjectedIntervals();
|
||||
for (let i = 0; i < want; i++) updateProjectedCube(projectedCubes[i], blocks[i]);
|
||||
refreshProjected();
|
||||
}
|
||||
|
||||
/** @param {number} index */
|
||||
function createProjectedCube(index) {
|
||||
const { el, topFace, rightFace, leftFace } = createCubeDiv(0);
|
||||
el.classList.add("projected");
|
||||
if (index === 0) el.classList.add("next");
|
||||
return { el, topFace, rightFace, leftFace };
|
||||
function createProjectedCube() {
|
||||
const cube = createCubeDiv();
|
||||
cube.el.classList.add("projected");
|
||||
|
||||
const date = document.createTextNode("");
|
||||
const hh = document.createTextNode("");
|
||||
const mm = document.createTextNode("");
|
||||
const dateP = document.createElement("p");
|
||||
dateP.append(date);
|
||||
const timeP = document.createElement("p");
|
||||
timeP.append(hh, span(":", "dim"), mm);
|
||||
cube.topFace.append(dateP, timeP);
|
||||
|
||||
const txs = document.createTextNode("");
|
||||
const txsUnit = document.createTextNode("");
|
||||
const txsP = document.createElement("p");
|
||||
txsP.append(txs);
|
||||
const txsUnitP = document.createElement("p");
|
||||
txsUnitP.classList.add("dim");
|
||||
txsUnitP.append(txsUnit);
|
||||
cube.rightFace.append(txsP, txsUnitP);
|
||||
|
||||
const median = document.createTextNode("");
|
||||
const rangeLo = document.createTextNode("");
|
||||
const rangeHi = document.createTextNode("");
|
||||
const medianP = document.createElement("p");
|
||||
medianP.append(span("~", "dim"), median);
|
||||
const rangeP = document.createElement("p");
|
||||
rangeP.append(rangeLo, span("-", "dim"), rangeHi);
|
||||
const unitP = document.createElement("p");
|
||||
unitP.classList.add("dim");
|
||||
unitP.textContent = "sat/vB";
|
||||
cube.leftFace.append(medianP, rangeP, unitP);
|
||||
|
||||
return {
|
||||
...cube,
|
||||
parts: { date, hh, mm, txs, txsUnit, median, rangeLo, rangeHi },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ el: HTMLDivElement, topFace: HTMLDivElement, rightFace: HTMLDivElement, leftFace: HTMLDivElement }} cube
|
||||
* @param {ReturnType<typeof createProjectedCube>} cube
|
||||
* @param {MempoolBlock} block
|
||||
* @param {number} index
|
||||
*/
|
||||
function updateProjectedCube(cube, block, index) {
|
||||
const fill = Math.min(1, block.blockVSize / 1_000_000);
|
||||
cube.el.style.setProperty("--fill", String(fill));
|
||||
|
||||
cube.topFace.textContent = "";
|
||||
const label = document.createElement("p");
|
||||
label.textContent = index === 0 ? "next" : `+${index}`;
|
||||
cube.topFace.append(label);
|
||||
|
||||
cube.rightFace.textContent = "";
|
||||
const txs = document.createElement("p");
|
||||
txs.textContent = block.nTx.toLocaleString();
|
||||
const txsUnit = document.createElement("p");
|
||||
txsUnit.classList.add("dim");
|
||||
txsUnit.textContent = block.nTx === 1 ? "tx" : "txs";
|
||||
cube.rightFace.append(txs, txsUnit);
|
||||
|
||||
cube.leftFace.textContent = "";
|
||||
const median = document.createElement("p");
|
||||
median.append(span("~", "dim"), formatFeeRate(block.medianFee));
|
||||
const range = document.createElement("p");
|
||||
range.append(
|
||||
formatFeeRate(block.feeRange[0]),
|
||||
span("-", "dim"),
|
||||
formatFeeRate(block.feeRange[6]),
|
||||
function updateProjectedCube(cube, block) {
|
||||
cube.el.style.setProperty(
|
||||
"--fill",
|
||||
String(Math.min(1, block.blockVSize / 1_000_000)),
|
||||
);
|
||||
const unit = document.createElement("p");
|
||||
unit.classList.add("dim");
|
||||
unit.textContent = "sat/vB";
|
||||
cube.leftFace.append(median, range, unit);
|
||||
const p = cube.parts;
|
||||
p.txs.nodeValue = block.nTx.toLocaleString();
|
||||
p.txsUnit.nodeValue = block.nTx === 1 ? "tx" : "txs";
|
||||
p.median.nodeValue = formatFeeRate(block.medianFee);
|
||||
p.rangeLo.nodeValue = formatFeeRate(block.feeRange[0]);
|
||||
p.rangeHi.nodeValue = formatFeeRate(block.feeRange[6]);
|
||||
}
|
||||
|
||||
function refreshProjectedIntervals() {
|
||||
function refreshProjected() {
|
||||
if (!projectedCubes.length || !newestTimestamp) return;
|
||||
const elapsed = Math.max(0, Math.floor(Date.now() / 1000) - newestTimestamp);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const elapsed = Math.max(0, now - newestTimestamp);
|
||||
for (let i = 0; i < projectedCubes.length; i++) {
|
||||
const interval = TARGET_BLOCK_SECONDS * i + elapsed;
|
||||
projectedCubes[i].el.style.setProperty(
|
||||
"--block-interval",
|
||||
String(interval),
|
||||
);
|
||||
const cube = projectedCubes[i];
|
||||
const interval = i === 0 ? elapsed : TARGET_BLOCK_SECONDS;
|
||||
cube.el.style.setProperty("--block-interval", String(interval));
|
||||
const ts = now + i * TARGET_BLOCK_SECONDS;
|
||||
const [hh, mm] = formatHHMM(ts);
|
||||
cube.parts.date.nodeValue = formatShortDate(ts);
|
||||
cube.parts.hh.nodeValue = hh;
|
||||
cube.parts.mm.nodeValue = mm;
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {"tip" | "gen"} label @param {string} href @param {string} title @param {() => void} handler */
|
||||
function createEdgeLink(label, href, title, handler) {
|
||||
const a = document.createElement("a");
|
||||
a.classList.add("chain-edge", label);
|
||||
a.classList.add("edge", label);
|
||||
a.href = href;
|
||||
a.title = title;
|
||||
a.textContent = label;
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chain-scroll {
|
||||
.scroll {
|
||||
padding: 0 var(--main-padding);
|
||||
scrollbar-width: none;
|
||||
|
||||
@container aside (max-width: 767px) {
|
||||
padding-bottom: 1rem;
|
||||
overflow-x: auto;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
@container aside (min-width: 768px) {
|
||||
@@ -29,9 +29,76 @@
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.blocks {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
|
||||
@container aside (max-width: 767px) {
|
||||
flex-direction: row-reverse;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.cube {
|
||||
--cube-fall-off: pow(
|
||||
clamp(
|
||||
0,
|
||||
(var(--block-interval, 600) - var(--min-block-interval)) /
|
||||
(var(--max-block-interval) - var(--min-block-interval)),
|
||||
1
|
||||
),
|
||||
0.7
|
||||
);
|
||||
--block-gap: calc(
|
||||
var(--min-gap) + var(--cube-fall-off) *
|
||||
(var(--max-gap) - var(--min-gap))
|
||||
);
|
||||
|
||||
& + & {
|
||||
margin-bottom: var(--block-gap);
|
||||
|
||||
@container aside (max-width: 767px) {
|
||||
margin-bottom: 0;
|
||||
margin-right: var(--block-gap);
|
||||
}
|
||||
}
|
||||
|
||||
.face-text .height {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: normal;
|
||||
}
|
||||
.face-text .fees {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.face-text .pool {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.1em;
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chain-edge {
|
||||
.edge {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -54,98 +121,14 @@
|
||||
height: var(--main-padding);
|
||||
padding-left: calc(var(--main-padding) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.gen {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.projected,
|
||||
.confirmed {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
@container aside (max-width: 767px) {
|
||||
flex-direction: row-reverse;
|
||||
&.tip {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cube {
|
||||
--cube-fall-off: pow(
|
||||
clamp(
|
||||
0,
|
||||
(var(--block-interval, 600) - var(--min-block-interval)) /
|
||||
(var(--max-block-interval) - var(--min-block-interval)),
|
||||
1
|
||||
),
|
||||
0.7
|
||||
);
|
||||
--block-gap: calc(
|
||||
var(--min-gap) + var(--cube-fall-off) * (var(--max-gap) - var(--min-gap))
|
||||
);
|
||||
|
||||
& + & {
|
||||
margin-bottom: var(--block-gap);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
width: 1px;
|
||||
height: var(--block-gap);
|
||||
background: var(--border-color);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@container aside (max-width: 767px) {
|
||||
margin-bottom: 0;
|
||||
margin-right: var(--block-gap);
|
||||
|
||||
&::before {
|
||||
top: 50%;
|
||||
left: auto;
|
||||
right: calc(-1 * var(--block-gap));
|
||||
width: var(--block-gap);
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.face-text .height {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: normal;
|
||||
}
|
||||
.face-text .fees {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.face-text .pool {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.1em;
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
&.gen {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ button {
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
text-transform: inherit;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
transition: opacity 200ms ease;
|
||||
|
||||
/* Held invisible while the chain rebuilds and scrolls to the target,
|
||||
then faded in so the layout settling isn't visible. */
|
||||
&.loading {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dim {
|
||||
opacity: 0.5;
|
||||
@@ -356,8 +363,16 @@
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
|
||||
/* #explorer supplies only vertical padding on mobile. */
|
||||
@container aside (max-width: 767px) {
|
||||
padding: 0 var(--main-padding);
|
||||
}
|
||||
|
||||
/* Full padding, halved on the side facing the chain so its halved
|
||||
right padding and this halved left padding form one gutter. */
|
||||
@container aside (min-width: 768px) {
|
||||
overflow-y: auto;
|
||||
padding: var(--main-padding);
|
||||
padding-left: calc(var(--main-padding) / 2);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user