`Quantus` and `Heisenberg` sat side by side, identically styled. One is the real
network. Somebody reading a balance, a reward or a transfer had no way to tell
from the chrome whether the number meant anything, and Planck makes it two
testnets against one mainnet.
Two labelled groups rather than a styling difference alone, because a lone
visual difference reads as decoration to anyone who has not been told what it
means:
MAINNET [ Quantus 329 ] TESTNETS [ Planck 110 ] [ Heisenberg 4 ]
The dashed border on a testnet chip is a second channel behind the heading
rather than the only one, and every chip's tooltip now says outright that a
testnet's balances and rewards are not real.
No backend work: `ChainInfo.mainnet` has been on the wire since the registry was
written and the frontend simply ignored it. It stays operator-asserted and is
never inferred from the name — a chain publishing itself as "Quantus Staging
Mainnet" must not collect the heading by having the word in its title.
A chain list with no mainnet in it renders one `TESTNETS` group rather than an
empty `MAINNET` heading.
Closes #5
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
106 lines
4.4 KiB
TypeScript
106 lines
4.4 KiB
TypeScript
/**
|
|
* The chain nav.
|
|
*
|
|
* Only the chains this observer holds an RPC endpoint for, ordered by node
|
|
* count descending — the busiest is the one most people came for, and the order
|
|
* is a real fact about the network rather than a config ordering.
|
|
*
|
|
* **Mainnet and testnets are separated and labelled.** Side by side and
|
|
* identically styled they had equal weight, and somebody reading a balance, a
|
|
* reward or a transfer had no way to tell from the chrome whether the number
|
|
* meant anything. Two headings say which is which without a legend and without
|
|
* the reader having to know a convention; the dashed border on a testnet chip
|
|
* is a second channel, because one visual difference alone reads as decoration
|
|
* to anybody who has not been told what it means.
|
|
*
|
|
* `mainnet` is operator-asserted in the `[[chains]]` entry and never inferred
|
|
* from the name — a chain publishing itself as "Quantus Staging Mainnet" must
|
|
* not collect the heading by having the word in its title.
|
|
*
|
|
* The rest are **not advertised**. They were listed as disabled chips for a
|
|
* while, on the reasoning that they are part of the network and hiding them
|
|
* misrepresents its shape. In practice that put five dead chips above the
|
|
* standings on every page, four of which had never had anything behind them,
|
|
* and the one live chain was the smallest thing in the row. Nothing is
|
|
* blocked: `/:chain/...` still resolves for any chain the backend knows, and
|
|
* the API still reports every one of them with its `tracking`. This is only
|
|
* about what the page offers unprompted.
|
|
*/
|
|
|
|
import { Link } from 'react-router-dom'
|
|
|
|
import type { ChainInfo } from '../api/generated/ChainInfo'
|
|
import type { Window as WindowName } from '../api/generated/Window'
|
|
import { href } from '../lib/routes'
|
|
|
|
function reason(chain: ChainInfo): string {
|
|
const kind = chain.mainnet ? '' : ' (a testnet — its balances and rewards are not real)'
|
|
switch (chain.status) {
|
|
case 'awaiting':
|
|
return `${chain.display_name}${kind} has not produced a block yet.`
|
|
case 'unreachable':
|
|
return `${chain.display_name}${kind} — its node is not answering; the last known standings are shown.`
|
|
case 'syncing':
|
|
return `${chain.display_name}${kind} — its node is still importing history.`
|
|
default:
|
|
return `${chain.display_name}${kind}`
|
|
}
|
|
}
|
|
|
|
export function ChainSwitcher({
|
|
chains,
|
|
active,
|
|
window: windowName,
|
|
}: {
|
|
chains: ChainInfo[]
|
|
active: string | null
|
|
/** Carried across the switch: changing chain is not a reason to change span. */
|
|
window: WindowName
|
|
}) {
|
|
const navigable = chains.filter((chain) => chain.tracking === 'full')
|
|
if (navigable.length === 0) return null
|
|
|
|
const groups: { label: string; chains: ChainInfo[] }[] = [
|
|
{ label: 'Mainnet', chains: navigable.filter((c) => c.mainnet) },
|
|
{ label: 'Testnets', chains: navigable.filter((c) => !c.mainnet) },
|
|
].filter((g) => g.chains.length > 0)
|
|
|
|
return (
|
|
<nav className="chains" aria-label="Chain">
|
|
{groups.map((group) => (
|
|
<div key={group.label} className="chain-group">
|
|
<span className="eyebrow chains-label">{group.label}</span>
|
|
<div className="chains-list">
|
|
{group.chains.map((chain) => {
|
|
const isActive = chain.id === active
|
|
// A link, not a button: a chain is a page with an address, and
|
|
// the chip is how most people reach it — so it has to survive a
|
|
// middle click and a right-click-copy like any other link.
|
|
return (
|
|
<Link
|
|
key={chain.genesis ?? chain.id}
|
|
to={href({ chain: chain.id, window: windowName })}
|
|
className={
|
|
['chain-chip', isActive ? 'is-active' : '', chain.mainnet ? '' : 'is-testnet']
|
|
.filter(Boolean)
|
|
.join(' ') || 'chain-chip'
|
|
}
|
|
aria-current={isActive ? 'page' : undefined}
|
|
title={reason(chain)}
|
|
>
|
|
<span className="chain-name">{chain.display_name}</span>
|
|
{chain.node_count !== null && (
|
|
<span className="chain-nodes" title={`${chain.node_count} nodes on telemetry`}>
|
|
{chain.node_count}
|
|
</span>
|
|
)}
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</nav>
|
|
)
|
|
}
|