ui: separate mainnet from testnets in the chain nav
`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
This commit is contained in:
@@ -5,6 +5,18 @@
|
||||
* 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
|
||||
@@ -22,15 +34,16 @@ 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} has not produced a block yet.`
|
||||
return `${chain.display_name}${kind} has not produced a block yet.`
|
||||
case 'unreachable':
|
||||
return `${chain.display_name}'s node is not answering; the last known standings are shown.`
|
||||
return `${chain.display_name}${kind} — its node is not answering; the last known standings are shown.`
|
||||
case 'syncing':
|
||||
return `${chain.display_name}'s node is still importing history.`
|
||||
return `${chain.display_name}${kind} — its node is still importing history.`
|
||||
default:
|
||||
return chain.display_name
|
||||
return `${chain.display_name}${kind}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,33 +60,46 @@ export function ChainSwitcher({
|
||||
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">
|
||||
<span className="eyebrow chains-label">Chains</span>
|
||||
<div className="chains-list">
|
||||
{navigable.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 on the site.
|
||||
return (
|
||||
<Link
|
||||
key={chain.genesis ?? chain.id}
|
||||
to={href({ chain: chain.id, window: windowName })}
|
||||
className={isActive ? 'chain-chip is-active' : '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>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
+16
-1
@@ -276,11 +276,19 @@ a {
|
||||
.chains {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
gap: 14px 28px;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 0 18px;
|
||||
}
|
||||
|
||||
/* One labelled group per kind of network. The gap between groups is wider than
|
||||
the gap inside one, so the split reads before either label does. */
|
||||
.chain-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.chains-label {
|
||||
flex: none;
|
||||
}
|
||||
@@ -1386,3 +1394,10 @@ tr.mine .share-bar > i {
|
||||
padding-left: 10px;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* A testnet. Dashed rather than a colour, and a second channel behind the group
|
||||
heading rather than the only one — a single visual difference reads as
|
||||
decoration to anyone who has not been told what it means. */
|
||||
.chain-chip.is-testnet {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user