Files
observer/web/index.html
rob thijssen 4774b8cfaa
All checks were successful
deploy / build (push) Successful in 1m12s
deploy / deploy-api (push) Has been skipped
deploy / deploy-web (push) Successful in 4s
feat(ui): a light theme, following the browser, with a toggle in the masthead
Dark, light, or whatever the browser asks for. Auto is the default, so a reader
who has never touched it gets their own system's answer rather than ours.

The light palette is selected, not inverted. `#bd8829` carries every magnitude
on this site at 6.6:1 against the warm-black and **2.77:1** against paper — the
validator says so, and 2.77 is under the 3:1 floor a mark has to clear. So light
mode gets its own step of the same bronze, and its `--data-bright` sits *darker*
than `--data`, because emphasis on paper is weight rather than glare. Same for
the washes: a glow at 0.08 alpha on warm-black is a smear at 0.08 on paper, so
the nine colour literals that were still loose in the stylesheet became tokens —
each one was a colour the second theme could not have overridden.

Every value was chosen by running the dataviz validator against the surface it
actually sits on, both modes. The single-hue rule is untouched and still
load-bearing in both: bronze and crimson fail CVD separation as a categorical
pair whichever ground they are on.

`auto` is a preference rather than a third palette. It resolves to a concrete
`light` or `dark` before the stylesheet ever sees it, which is what keeps this to
one definition per palette instead of one per palette per media query — and it
has to resolve before the *first paint*, because anything running after the
bundle loads runs after the page has been painted once, and on a light
preference that is a full-screen flash of warm-black. Hence the inline script,
whose duplication of `lib/theme.ts` is the cheaper of the two costs.

Two things that would otherwise bite: `localStorage` throws rather than returning
null where site data is blocked, so every access is guarded and falls back to
what the browser wants; and `auto` keeps listening, so a machine that turns dark
at sunset does not leave a reader on the daylight palette until they reload.

The toggle shows the state it is in, never the state it would move to — a control
that displays its own destination is why these get guessed at — and its
accessible name carries that state, since the icon cannot.

Checked in a browser, both themes, on the standings, a miner page and the share
chart. Worth recording what that turned up: dark carries 46 text elements under
4.5:1 and light carries 3, each beating its dark counterpart. The gap is
`--text-muted` at 3.7, the deliberate existing value CLAUDE.md has always
documented — not introduced here, and not something to change without deciding
to change the dark design.

Closes #12

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
2026-09-10 12:22:30 +03:00

79 lines
3.2 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>blackbeard.observer — Quantus mining leaderboard</title>
<meta
name="description"
content="Live miner leaderboard and network hashrate for the Quantus blockchain and its Planck testnet. Every author decoded from block headers — no registration, no opt-in."
/>
<meta name="color-scheme" content="dark light" />
<!-- Rewritten by the script below, and again by the toggle. The value here
is only what a reader with no stored preference and no JavaScript
gets. -->
<meta name="theme-color" content="#0d0b09" />
<link rel="icon" href="/sigil.svg" type="image/svg+xml" />
<meta property="og:title" content="blackbeard.observer" />
<meta
property="og:description"
content="Live Quantus mining leaderboard and network hashrate."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://blackbeard.observer/" />
<style>
/* Inline so the background is right on the first paint rather than after
the CSS bundle arrives. Both values, switched by the same attribute the
stylesheet reads — the script below sets it before this rule is ever
applied to anything. */
html {
background: #0d0b09;
}
html[data-theme='light'] {
background: #fbf8f2;
}
</style>
<script>
/* Resolve the theme before the first paint.
*
* This is a deliberate copy of the resolution in `src/lib/theme.ts`, and
* it is here rather than in the bundle because anything that runs after
* the bundle loads runs after the page has been painted once — which on a
* light preference is a full-screen flash of warm-black, and on a dark
* one a flash of white. Seven lines duplicated is the cheaper of the two.
*
* Wrapped because `localStorage` *throws* rather than returning null in a
* browser set to block site data; the browser's own preference is the
* right fallback and is what `auto` would have chosen anyway.
*/
;(function () {
var preference = 'auto'
try {
var stored = localStorage.getItem('blackbeard.theme')
if (stored === 'auto' || stored === 'light' || stored === 'dark') preference = stored
} catch (e) {}
var theme =
preference === 'auto'
? matchMedia('(prefers-color-scheme: light)').matches
? 'light'
: 'dark'
: preference
document.documentElement.setAttribute('data-theme', theme)
document
.querySelector('meta[name="theme-color"]')
.setAttribute('content', theme === 'light' ? '#fbf8f2' : '#0d0b09')
})()
</script>
</head>
<body>
<div id="root"></div>
<noscript>
blackbeard.observer streams the chain over a WebSocket and needs JavaScript. The same data is
available without it from the API at <code>/v1/chains</code>.
</noscript>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>