feat(ui): two columns for the stage, and a console that links out
Stacked, the mouth and the lines describing it could not be read at the same time, which is the whole point of having both. They sit side by side once there is room: the figure takes the space left over and the console takes a fixed column. The breakpoint is measured rather than chosen. At 1000px the console came out 360px wide and *every* line wrapped to two — worse than stacking — because one line needs about 354px for its height, kind and amount columns plus a shortened address and three gaps. The column's minimum is 400px and the breakpoint is where that can exist without starving the figure. The console now links out. A block height goes to the block, an address to the account, and the count of what is not shown names its block instead of saying "this block" — so the line that tells you 897 notes went unlisted is also the way to go and read them. Links fall back to plain text before the chain is known, because `href` would otherwise render `//block/43466`, which looks clickable and goes nowhere. They are marked with a dotted underline rather than a colour, following `a.ticker-height`: the console's columns already carry meaning through colour — muted for the height and address, bright for the kind — and tinting a link would make it momentarily read as a different column. Refs #17 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <Link to={href({ chain, block: String(height) })}>{label}</Link>
|
||||
}
|
||||
|
||||
export function WormholeStage({
|
||||
chain,
|
||||
decimals,
|
||||
@@ -449,90 +465,107 @@ export function WormholeStage({
|
||||
const { cx, cy } = geo
|
||||
|
||||
return (
|
||||
<div ref={wrap} className="stage-wrap">
|
||||
<svg
|
||||
className="chart stage"
|
||||
width={width}
|
||||
height={HEIGHT}
|
||||
viewBox={`0 0 ${width} ${HEIGHT}`}
|
||||
role="img"
|
||||
aria-label={[
|
||||
outstanding === null
|
||||
? 'The wormhole.'
|
||||
: `The wormhole, holding ${outstanding} notes committed and not yet spent.`,
|
||||
last === null
|
||||
? 'Waiting for the next block.'
|
||||
: `Block ${last.height} committed ${last.notesIn} notes and released ${last.notesOut}.`,
|
||||
].join(' ')}
|
||||
>
|
||||
{/* The mouth. Three ellipses of the one hue at descending wash weight —
|
||||
<div className="stage-wrap">
|
||||
{/* 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. */}
|
||||
<div ref={wrap} className="stage-figure">
|
||||
<svg
|
||||
className="chart stage"
|
||||
width={width}
|
||||
height={HEIGHT}
|
||||
viewBox={`0 0 ${width} ${HEIGHT}`}
|
||||
role="img"
|
||||
aria-label={[
|
||||
outstanding === null
|
||||
? 'The wormhole.'
|
||||
: `The wormhole, holding ${outstanding} notes committed and not yet spent.`,
|
||||
last === null
|
||||
? 'Waiting for the next block.'
|
||||
: `Block ${last.height} committed ${last.notesIn} notes and released ${last.notesOut}.`,
|
||||
].join(' ')}
|
||||
>
|
||||
{/* The mouth. Three ellipses of the one hue at descending wash weight —
|
||||
a throat seen edge-on, not a second series. */}
|
||||
<ellipse className="stage-rim stage-rim-3" cx={cx} cy={cy} rx={rx} ry={ry} />
|
||||
<ellipse className="stage-rim stage-rim-2" cx={cx} cy={cy} rx={rx * 0.64} ry={ry * 0.64} />
|
||||
<ellipse className="stage-rim stage-rim-1" cx={cx} cy={cy} rx={rx * 0.33} ry={ry * 0.33} />
|
||||
<ellipse className="stage-core" cx={cx} cy={cy} rx={rx * 0.11} ry={ry * 0.11} />
|
||||
|
||||
{/* 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) => (
|
||||
<g key={si} transform={`translate(${cx} ${cy}) scale(1 ${geo.squash})`}>
|
||||
<g
|
||||
className="stage-shell"
|
||||
style={{
|
||||
animationDuration: `${shell.seconds}s`,
|
||||
animationDirection: shell.reverse ? 'reverse' : 'normal',
|
||||
}}
|
||||
>
|
||||
{shell.marks.map((m, mi) => (
|
||||
<circle className="stage-held" key={mi} cx={m.x} cy={m.y} r={m.r} />
|
||||
))}
|
||||
</g>
|
||||
</g>
|
||||
))}
|
||||
|
||||
{particles.current.map((_, i) => (
|
||||
<circle
|
||||
key={i}
|
||||
className="stage-note"
|
||||
ref={(node) => {
|
||||
pool.current[i] = node
|
||||
}}
|
||||
<ellipse className="stage-rim stage-rim-3" cx={cx} cy={cy} rx={rx} ry={ry} />
|
||||
<ellipse
|
||||
className="stage-rim stage-rim-2"
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={2}
|
||||
opacity={0}
|
||||
rx={rx * 0.64}
|
||||
ry={ry * 0.64}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
<ellipse
|
||||
className="stage-rim stage-rim-1"
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
rx={rx * 0.33}
|
||||
ry={ry * 0.33}
|
||||
/>
|
||||
<ellipse className="stage-core" cx={cx} cy={cy} rx={rx * 0.11} ry={ry * 0.11} />
|
||||
|
||||
<div className="stage-caption">
|
||||
{last === null ? (
|
||||
<span className="stage-idle">Watching for the next block.</span>
|
||||
) : (
|
||||
<>
|
||||
<strong>#{last.height.toLocaleString('en-US')}</strong>
|
||||
<span>
|
||||
{last.notesIn.toLocaleString('en-US')} in
|
||||
{last.notesIn > 0 && ` · ${tokens(last.amountIn, decimals, 1)} ${symbol}`}
|
||||
</span>
|
||||
<span>
|
||||
{last.notesOut.toLocaleString('en-US')} out
|
||||
{last.notesOut > 0 && ` · ${tokens(last.amountOut, decimals, 1)} ${symbol}`}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{/* 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) => (
|
||||
<g key={si} transform={`translate(${cx} ${cy}) scale(1 ${geo.squash})`}>
|
||||
<g
|
||||
className="stage-shell"
|
||||
style={{
|
||||
animationDuration: `${shell.seconds}s`,
|
||||
animationDirection: shell.reverse ? 'reverse' : 'normal',
|
||||
}}
|
||||
>
|
||||
{shell.marks.map((m, mi) => (
|
||||
<circle className="stage-held" key={mi} cx={m.x} cy={m.y} r={m.r} />
|
||||
))}
|
||||
</g>
|
||||
</g>
|
||||
))}
|
||||
|
||||
{particles.current.map((_, i) => (
|
||||
<circle
|
||||
key={i}
|
||||
className="stage-note"
|
||||
ref={(node) => {
|
||||
pool.current[i] = node
|
||||
}}
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={2}
|
||||
opacity={0}
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
|
||||
<div className="stage-caption">
|
||||
{last === null ? (
|
||||
<span className="stage-idle">Watching for the next block.</span>
|
||||
) : (
|
||||
<>
|
||||
<strong>#{last.height.toLocaleString('en-US')}</strong>
|
||||
<span>
|
||||
{last.notesIn.toLocaleString('en-US')} in
|
||||
{last.notesIn > 0 && ` · ${tokens(last.amountIn, decimals, 1)} ${symbol}`}
|
||||
</span>
|
||||
<span>
|
||||
{last.notesOut.toLocaleString('en-US')} out
|
||||
{last.notesOut > 0 && ` · ${tokens(last.amountOut, decimals, 1)} ${symbol}`}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{/* 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 && (
|
||||
<span className="stage-idle">
|
||||
{outstanding.toLocaleString('en-US')} notes held
|
||||
{stock.per > 1 && ` · one mark per ${stock.per.toLocaleString('en-US')}`}
|
||||
</span>
|
||||
)}
|
||||
{reduced && <span className="stage-idle">motion off</span>}
|
||||
{stock !== null && outstanding !== null && (
|
||||
<span className="stage-idle">
|
||||
{outstanding.toLocaleString('en-US')} notes held
|
||||
{stock.per > 1 && ` · one mark per ${stock.per.toLocaleString('en-US')}`}
|
||||
</span>
|
||||
)}
|
||||
{reduced && <span className="stage-idle">motion off</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* What the marks were. The stage on its own is a dot travelling inward,
|
||||
@@ -554,7 +587,7 @@ export function WormholeStage({
|
||||
return (
|
||||
<li key={line.id}>
|
||||
<span className="stage-log-height">
|
||||
{repeat ? '' : `#${line.height.toLocaleString('en-US')}`}
|
||||
{repeat ? '' : <BlockLink chain={chain} height={line.height} />}
|
||||
</span>
|
||||
<span className={`stage-log-kind stage-log-${line.kind}`}>
|
||||
{kind.glyph} {kind.label}
|
||||
@@ -562,10 +595,19 @@ export function WormholeStage({
|
||||
<span className="stage-log-amount">
|
||||
{tokens(line.amount, decimals, 4)} {symbol}
|
||||
</span>
|
||||
<span className="stage-log-to">{shortAddress(line.to)}</span>
|
||||
<span className="stage-log-to">
|
||||
{chain === null ? (
|
||||
shortAddress(line.to)
|
||||
) : (
|
||||
<Link to={href({ chain, account: line.to })} title={line.to}>
|
||||
{shortAddress(line.to)}
|
||||
</Link>
|
||||
)}
|
||||
</span>
|
||||
{line.elided > 0 && (
|
||||
<span className="stage-idle">
|
||||
+{line.elided.toLocaleString('en-US')} more in this block
|
||||
<span className="stage-idle stage-log-more">
|
||||
+{line.elided.toLocaleString('en-US')} more in{' '}
|
||||
<BlockLink chain={chain} height={line.height} />
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user