Offer a dark theme on dash and admin - #382
Conversation
This comment has been minimized.
This comment has been minimized.
9a7498a to
7e8df7f
Compare
|
Fixed the Rather than just fixing the one site, I added the sweep that would have caught it. Folded into the original commit rather than stacked on top, per the repo's commit guidance. 118 tests pass; typecheck, lint and build clean. |
This comment has been minimized.
This comment has been minimized.
|
CI is green. One note on the way there:
It is a known race rather than a new one: the autouse fixture in that file already carries a comment describing this exact symptom (dated 2026-09-06) and calls Filed as https://app.clickup.com/t/123zgec2ry0 rather than fixed here, since a harder reset would not address it — the race is with a live daemon, not with leftover state. |
7e8df7f to
0050e0a
Compare
|
Fixed — and this one was a better catch than the last, because it is the same bug class Both Discord links carried an inline Verified in a browser with You also identified precisely why my sweep missed it, so I extended it rather than only fixing the two sites: the page sweep now also rejects a named colour in a style object ( Folded into the same commit again. |
This comment has been minimized.
This comment has been minimized.
0050e0a to
a792654
Compare
This comment has been minimized.
This comment has been minimized.
a792654 to
a81d7c1
Compare
This comment has been minimized.
This comment has been minimized.
a81d7c1 to
6fb814e
Compare
This comment has been minimized.
This comment has been minimized.
The console has only ever had the light palette, which makes it the odd one out next to the map — the map has themed since it borrowed dash's whole vocabulary, and the data explorer has answered the OS preference all along. Someone working across the three at night meets a white page on two of them. The mechanism is the map's, with the two themes the other way round. The map carries dark on its base selector and light behind the attribute, because dark is its default and the default must never be the theme that flashes on first paint; light is the default here, so light stays on the bare selector and dark is what the attribute buys. The palette is the map's dark column wholesale rather than a new one: the brand guide already records the two as counterparts, and the estate's own §10 says drift is its main failure mode. The control has three states rather than the map's two. `system` stamps no attribute and lets a prefers-color-scheme block answer, so the OS preference is honoured with no JavaScript and keeps working when the OS changes its mind mid-session; light and dark override it. That also means the data explorer, which has behaved that way since it was written, can gain a switch without regressing anyone. Three things the diff does not explain on its own: - Recharts paints from props, not from the cascade, so `var()` cannot reach it. The chart chrome moves into a second, hand-kept copy of the tokens, and a test asserts the two agree. AnomalyPage was already passing `stroke="var(--border)"` into SVG presentation attributes, where CSS does not resolve var() at all — those axes were drawing at the initial value, and now draw. - Dark's accent is a bright sky blue, so the white that inks the primary button and the sidebar mark drops to about 1.8:1 there. Both now take `--accent-ink`, near-black on dark. The map still inks `#fff` on the same fill; that is its bug to fix, not one to copy. - The pre-paint script is a file, not an inline tag. Every page vhost sends `script-src 'self'`, which does not cover inline code, while the dev server sends no CSP — an inline version works in every local check and silently never runs once deployed. A colour written into a page is pinned to one theme, and no token block covers a chart's props or an inline style, so the pages get their own sweep. It catches both shapes this change had: an Area whose stroke follows the palette while its fill does not, and a `color: "white"` sitting on a .btn-primary, where an inline style outranks the class rule and quietly undoes --accent-ink. Leaflet needs three rules rather than one. Its tiles take a filter, since OSM does not theme; its container takes the sunk tier; and its own attribution and zoom controls are hardcoded white and #333 in leaflet.css, so on a dark card they read as two bright chips stuck to the corners until they are told otherwise. The switch is three icons rather than three words: Feather's sun, monitor and moon, ordered light to dark with the neutral between them, hand-inlined in the same house style as the sidebar's set (24-unit box, no fill, 2-unit round-capped stroke in currentColor) since no icon package is installed and three glyphs do not justify adding one. Drawing in currentColor is what lets the active segment tint itself from the same rule that sets its ink. A glyph names nothing on its own, so each button carries an aria-label and a matching title, and the svg is aria-hidden so the name is not read twice. Choosing role=radiogroup over three independent toggles is what obliges the keyboard half: the set is announced as one control with three options, so it is one tab stop rather than three (a roving tabIndex on whichever is checked) and the arrows move within it, selecting as they go. Home and End reach the ends. Everything else passes through — without preventDefault the arrows scroll the dropdown while the selection moves underneath. Verified in a browser across all three settings: system follows the OS and follows it changing, an explicit choice overrides it and survives a reload, and the tooltip, grid and axes take their themed values. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6fb814e to
0af853c
Compare
|
Claude finished @Babissimo's task in 2m 14s —— View job Review: dark theme for dash and admin (PR #382)
This Verified correct:
No blocking issues found, and no new issues introduced by this push. This PR is in good shape. |
Adds a light/dark/system appearance control to
dash.retina.fmandadmin.retina.fm(one bundle, one change), modelled on the map's theming andtaking the map's dark palette rather than inventing a sixth.
The data explorer is the other half of the request and lands separately, since
it is a different app with no code in common: #383.
How it works
:rootkeeps today's light tokens and dark arrives behind[data-theme="dark"]— the opposite way round from the map, which carries dark on its base selector.
Same reasoning both times: the attribute is stamped from JavaScript, so
whichever theme depends on it is the one that can flash the other on first
paint, and that must never be the default. The guide makes light the default for
anything that explains or configures.
The control has three states rather than the map's two. System stamps no
attribute at all and lets a
prefers-color-schemeblock answer, so the OSpreference needs no JavaScript and keeps working when the OS changes mid-session;
light and dark override it via
:root:not([data-theme="light"]). It lives in theexisting header dropdown.
Three things worth a reviewer's attention
Recharts cannot read the cascade. It paints from props, so the chart chrome
is a second, hand-kept copy of the tokens in
utils/chartTheme.ts;chartTheme.test.tsasserts the two agree. Related:AnomalyPagewas alreadypassing
stroke="var(--border)"into SVG presentation attributes, where CSSdoes not resolve
var()— those axes have been drawing at the initial value,and now draw.
--accent-inkis new. Dark's accent is a bright sky blue, so thewhitethat inked the primary button and the sidebar mark drops to about 1.8:1 there.
Both now take a token: white on light, near-black on dark. The map still inks
#fffon the same fill — that is a real contrast bug there, left alone ratherthan copied.
The pre-paint script is a file, not an inline tag. Every page vhost sends
script-src 'self'(deploy/nginx/snippets/security-headers-page.conf), whichdoes not cover inline code, while the dev server sends no CSP at all — so an
inline version works in every local check and silently never runs once deployed.
public/theme-boot.jsis outside the module graph by necessity, so a testasserts it uses the same storage key and attribute as
ThemeContext.Two smaller things:
vite.config.jsgainscss: trueso the token tests canread
App.css?raw(vitest stubs CSS imports with an empty string otherwise,the raw query included), and
NetworkHealthPage's node markers deliberatelykeep their light values — they are drawn onto OSM tiles, which do not theme.
Verification
typecheck,lintandbuildclean; 95 tests pass (62 before, 33 newcovering the preference, the attribute, OS-following, storage failure, the two
dark blocks agreeing, the chart palette and the boot script).
Checked in a real browser across all three settings: system follows the OS and
follows it changing, an explicit light beats a dark OS, an explicit choice
survives a reload, and the tooltip/grid/axes take themed values
(
background: rgb(19, 34, 64),border: rgba(100, 180, 255, 0.28),color: rgb(226, 232, 240)).Not verified, and worth a look after deploy: anything behind auth — the
sidebar, tables and the
NetworkHealthPageLeaflet map under the new dark tilefilter — since there is no backend locally to log into.
Follow-ups
The brand guide in
claude-sharednow describes the estate inaccurately: itcalls the map "the only surface that themes". Worth a small PR there.
🤖 Generated with Claude Code