diff --git a/web/src/components/WormholeStage.tsx b/web/src/components/WormholeStage.tsx index aa493c1..e0b973f 100644 --- a/web/src/components/WormholeStage.tsx +++ b/web/src/components/WormholeStage.tsx @@ -27,9 +27,12 @@ import { useEffect, useMemo, useRef, useState } from 'react' +import { Link } from 'react-router-dom' + import { fetchWormhole } from '../api/rest' import type { WormholePulse } from '../api/socket' import { shortAddress, tokens } from '../lib/format' +import { href } from '../lib/routes' import { useObserverInstance } from '../lib/observer-context' const HEIGHT = 210 @@ -169,6 +172,19 @@ function ease(t: number): number { return t * t * (3 - 2 * t) } +/** + * A block height, as a link to it where there is a chain to link into. + * + * Falls back to plain text rather than a dead link when the chain is not yet + * known: `href` would render `//block/43466`, which looks clickable and goes + * nowhere. The number is the same either way, so nothing is lost. + */ +function BlockLink({ chain, height }: { chain: string | null; height: number }) { + const label = `#${height.toLocaleString('en-US')}` + if (chain === null) return <>{label} + return {label} +} + export function WormholeStage({ chain, decimals, @@ -449,90 +465,107 @@ export function WormholeStage({ const { cx, cy } = geo return ( -
- - {/* The mouth. Three ellipses of the one hue at descending wash weight — +
+ {/* The ref is on the figure rather than the panel: in the two-column + layout the SVG's width is its *column's* width, and measuring the + panel would size the mouth for space the console is occupying. */} +
+ + {/* The mouth. Three ellipses of the one hue at descending wash weight — a throat seen edge-on, not a second series. */} - - - - - - {/* The notes already inside, turning slowly. Drawn before the marks in - flight so an arriving note passes in front of the population it is - about to join. */} - {stock?.shells.map((shell, si) => ( - - - {shell.marks.map((m, mi) => ( - - ))} - - - ))} - - {particles.current.map((_, i) => ( - { - pool.current[i] = node - }} + + - ))} - + + -
- {last === null ? ( - Watching for the next block. - ) : ( - <> - #{last.height.toLocaleString('en-US')} - - {last.notesIn.toLocaleString('en-US')} in - {last.notesIn > 0 && ` · ${tokens(last.amountIn, decimals, 1)} ${symbol}`} - - - {last.notesOut.toLocaleString('en-US')} out - {last.notesOut > 0 && ` · ${tokens(last.amountOut, decimals, 1)} ${symbol}`} - - - )} - {/* Outside the block branch on purpose: the held population is drawn + {/* The notes already inside, turning slowly. Drawn before the marks in + flight so an arriving note passes in front of the population it is + about to join. */} + {stock?.shells.map((shell, si) => ( + + + {shell.marks.map((m, mi) => ( + + ))} + + + ))} + + {particles.current.map((_, i) => ( + { + pool.current[i] = node + }} + cx={cx} + cy={cy} + r={2} + opacity={0} + /> + ))} + + +
+ {last === null ? ( + Watching for the next block. + ) : ( + <> + #{last.height.toLocaleString('en-US')} + + {last.notesIn.toLocaleString('en-US')} in + {last.notesIn > 0 && ` · ${tokens(last.amountIn, decimals, 1)} ${symbol}`} + + + {last.notesOut.toLocaleString('en-US')} out + {last.notesOut > 0 && ` · ${tokens(last.amountOut, decimals, 1)} ${symbol}`} + + + )} + {/* Outside the block branch on purpose: the held population is drawn from the moment the page loads, and a reader arriving between blocks would otherwise see forty-five turning marks with nothing saying what they are. */} - {stock !== null && outstanding !== null && ( - - {outstanding.toLocaleString('en-US')} notes held - {stock.per > 1 && ` · one mark per ${stock.per.toLocaleString('en-US')}`} - - )} - {reduced && motion off} + {stock !== null && outstanding !== null && ( + + {outstanding.toLocaleString('en-US')} notes held + {stock.per > 1 && ` · one mark per ${stock.per.toLocaleString('en-US')}`} + + )} + {reduced && motion off} +
{/* What the marks were. The stage on its own is a dot travelling inward, @@ -554,7 +587,7 @@ export function WormholeStage({ return (
  • - {repeat ? '' : `#${line.height.toLocaleString('en-US')}`} + {repeat ? '' : } {kind.glyph} {kind.label} @@ -562,10 +595,19 @@ export function WormholeStage({ {tokens(line.amount, decimals, 4)} {symbol} - {shortAddress(line.to)} + + {chain === null ? ( + shortAddress(line.to) + ) : ( + + {shortAddress(line.to)} + + )} + {line.elided > 0 && ( - - +{line.elided.toLocaleString('en-US')} more in this block + + +{line.elided.toLocaleString('en-US')} more in{' '} + )}
  • diff --git a/web/src/index.css b/web/src/index.css index fa29016..2837c41 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1048,6 +1048,36 @@ tr.mine .share-bar > i { padding: 6px 0 2px; } +/* Side by side once there is room for both. + * + * Stacked, the console pushes the panel past a phone screen's height and the + * mouth and the lines describing it cannot be read at the same time — which is + * the entire point of having both. The breakpoint is where the console still + * fits its widest line (a height, a kind, an amount and an address) without + * wrapping; below it they stack, and the figure takes the full width back. */ +@media (min-width: 1100px) { + .stage-wrap { + display: grid; + /* 400px is not a taste: it is what one line needs — the height, kind and + amount columns plus a shortened address and three gaps come to ~354px, + and the column's own padding takes the rest. Measured at 360px every + line wrapped to two, which is worse than stacking. The breakpoint is + where that column can exist without starving the figure. */ + grid-template-columns: minmax(0, 1fr) clamp(400px, 36%, 470px); + padding: 6px 0 0; + } + + .stage-log { + border-left: var(--rule); + /* Stretched to the figure's height so the panel has a straight bottom edge + whatever the console is holding. Scrolls rather than growing: the buffer + is fixed, but a wrapped line must not lengthen the panel. */ + height: 100%; + overflow-y: auto; + padding-top: 10px; + } +} + .stage { display: block; } @@ -1181,19 +1211,21 @@ tr.mine .share-bar > i { .stage-log-height { color: var(--text-muted); - min-width: 66px; + min-width: 62px; } /* Direction repeats what the mark did, in a glyph, so a line and its dot can be matched without reading the label. Same reasoning as the flow chart: the encoding is geometry, never a second hue — every kind wears the one colour. */ .stage-log-kind { - min-width: 72px; + min-width: 66px; color: var(--data-bright); } .stage-log-amount { - min-width: 132px; + /* Sized to the widest real amount rather than generously: the column has to + survive being squeezed into the console's own column at 1000px. */ + min-width: 104px; color: var(--text-primary); } @@ -1201,6 +1233,33 @@ tr.mine .share-bar > i { color: var(--text-muted); } +/* Console links: the block a note landed in, and the account it credited. + * + * Underlined on hover rather than tinted, following `a.ticker-height`: the + * columns already carry meaning through colour — muted for the height and the + * address, bright for the kind — and recolouring a link on hover would make it + * momentarily read as a different column. The dotted rule is the affordance; + * it costs no colour at all. */ +.stage-log a { + text-decoration: underline; + text-decoration-style: dotted; + text-underline-offset: 3px; + text-decoration-color: var(--border-strong); +} + +.stage-log a:hover { + color: var(--data-bright); + text-decoration-color: var(--data-bright); +} + +/* The count of what is not shown. It fits on the line in the wide layout and + wraps in the narrow column, so it carries the height column's indent either + way — wrapped and flush left it reads as a new record rather than as a note + about the one above it. */ +.stage-log-more { + padding-left: 62px; +} + /* A meter: one ratio against its limit. * * The track is the same hue at wash weight rather than a neutral grey, so the