mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
ui: a range slider is painted by the theme, not by the browser
A native `<input type="range">` is drawn by the UA, and with nothing said about it the UA uses its own accent -- a fixed blue no theme can reach. Five are on screen: the log level on Settings, and the duration sliders in the food, water and ration calculators. None had ever been themed; `accent-color` appeared nowhere in the app. Night is where that is not merely inconsistent. The theme exists so a user reading in the dark keeps their adaptation and every token is a red, so the slider was the brightest non-red thing on the page -- on the one control a user drags back and forth while watching it. `accent-color: var(--blue)` for the same reason links take that token: it remaps per theme, so night gets the dim red (#8a2828) and amber the amber (#a86618) with no second rule. Checkboxes, radios and `<progress>` are included because they are painted from the same accent and would have been the next report. `accent-color` reaches the thumb and the FILLED half of the track and nothing else, so on a dark ground the rest stayed the UA's pale slab. `color-scheme: dark` fixes that, scoped to these controls rather than declared on `html` -- on `html` it also governs scrollbars, spinners and every other piece of browser chrome, which is a larger change than this one and worth making deliberately. Tests assert the accent equals `--blue` in each of the four themes, plus a negative: in the monochrome themes it is a resolved color and never `auto`. Without that second one the first four would also pass on an unstyled slider whose accent happened to stringify the same way. Added to the gallery, which is how the monochrome themes get looked at. Its slider is stateful and starts mid-track: the accent only shows on the filled half, so one parked at either end would show almost none of the color the section exists to check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
36d087d489
commit
3f3d46a4c4
3 changed files with 123 additions and 0 deletions
|
|
@ -78,6 +78,10 @@ const Row = ({children}) => <div style={{display: 'flex', gap: 10, flexWrap: 'wr
|
|||
{children}
|
||||
</div>;
|
||||
|
||||
// The labels Settings shows beside its own log-level slider, so the sample reads like the
|
||||
// control it stands in for rather than a bare number.
|
||||
const logLevelNames = ['Critical', 'Error', 'Warning', 'Info', 'Debug'];
|
||||
|
||||
/*
|
||||
* One navigation bar, in one of the twelve colors a user can pick for it.
|
||||
*
|
||||
|
|
@ -149,6 +153,9 @@ export function ThemeSamplePage() {
|
|||
const [wideConfirmOpen, setWideConfirmOpen] = useState(false);
|
||||
const [comments, setComments] = useState(true);
|
||||
const [hotspot, setHotspot] = useState(true);
|
||||
// Draggable, because the accent only shows on the FILLED half of the track -- a slider
|
||||
// parked at either end shows almost none of the color this section is here to check.
|
||||
const [logLevel, setLogLevel] = useState(2);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [realColors, setRealColors] = useState(false);
|
||||
|
|
@ -707,6 +714,26 @@ export function ThemeSamplePage() {
|
|||
<Toggle label='WROL Mode' description='Read-only: disables downloads.'/>
|
||||
<Toggle label='Bluetooth' description='No adapter detected.' disabled/>
|
||||
</div>
|
||||
<div style={{marginTop: 14}}>
|
||||
<label htmlFor='sample_range'>Log Level: {logLevelNames[logLevel]}</label>
|
||||
<input
|
||||
type='range'
|
||||
id='sample_range'
|
||||
min={0}
|
||||
max={4}
|
||||
step={1}
|
||||
value={logLevel}
|
||||
onChange={e => setLogLevel(parseInt(e.target.value))}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</div>
|
||||
<p style={{fontSize: '0.75rem', color: 'var(--muted)', marginTop: 8, marginBottom: 0}}>
|
||||
A range slider is drawn by the browser, not by us — so with nothing said
|
||||
about it the UA paints it in its own fixed blue, which no theme can reach.
|
||||
Check it in night: the accent follows <code>--blue</code> like a link does,
|
||||
and the unfilled half of the track is dark rather than the UA's pale slab.
|
||||
Settings and three of the calculators each have one.
|
||||
</p>
|
||||
</Panel>
|
||||
</Section>
|
||||
|
||||
|
|
|
|||
|
|
@ -2667,6 +2667,58 @@ describe('a card reads as titles, not as a wall of links', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('a range slider is painted by the theme, not by the browser', () => {
|
||||
/*
|
||||
* A native `<input type="range">` is drawn by the UA, and with nothing said about it the
|
||||
* UA uses its own accent -- a fixed blue that no theme can reach. Five of them are on
|
||||
* screen: the log level on Settings, and the duration sliders in the food, water and
|
||||
* ration calculators.
|
||||
*
|
||||
* Night is where this is not merely inconsistent but wrong. The theme exists so a user
|
||||
* reading in the dark keeps their adaptation, and every other token is a red; a slider
|
||||
* arriving in the browser's blue is the brightest non-red thing on the page, on the one
|
||||
* control a user drags back and forth while watching it.
|
||||
*
|
||||
* `accent-color` takes `--blue` for the same reason links do: it remaps per theme, so
|
||||
* night gets the dim red and amber the amber with no second rule.
|
||||
*/
|
||||
const mountSlider = (theme) => cy.mountUI(
|
||||
<input type='range' min={1} max={5} defaultValue={3} data-testid='slider'/>,
|
||||
theme ? {theme} : undefined,
|
||||
);
|
||||
|
||||
themeNames.forEach((theme) => {
|
||||
it(`accents the slider with the theme's own blue in ${theme}`, () => {
|
||||
mountSlider(theme);
|
||||
|
||||
cy.get('[data-testid="slider"]').should(($slider) => {
|
||||
const expected = toRgb(getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--blue'));
|
||||
expect(toRgb(getComputedStyle($slider[0]).accentColor), `accent in ${theme}`)
|
||||
.to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('never leaves it at the UA default, which is what night could not tolerate', () => {
|
||||
/*
|
||||
* The assertions above compare against a token, so they would also pass if the
|
||||
* accent were `auto` and the token happened to resolve to the same string. This
|
||||
* states the failure directly: whatever the slider is accented with, it is a real
|
||||
* color and not the keyword that hands the decision back to the browser.
|
||||
*/
|
||||
monochromeThemes.forEach((theme) => {
|
||||
mountSlider(theme);
|
||||
|
||||
cy.get('[data-testid="slider"]').should(($slider) => {
|
||||
const accent = getComputedStyle($slider[0]).accentColor;
|
||||
expect(accent, `accent in ${theme}`).to.not.equal('auto');
|
||||
expect(() => toRgb(accent), `a resolved color in ${theme}`).to.not.throw();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('a panel wrapped in a link does not read as a link', () => {
|
||||
/*
|
||||
* The dashboard's Status panel is wrapped in a `<Link to='/admin/status'>`, so the whole
|
||||
|
|
|
|||
|
|
@ -318,6 +318,50 @@ a:has(img, svg, .wrolpi-card-icon):hover {
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Native controls the browser paints itself.
|
||||
*
|
||||
* A range slider, a checkbox and a radio are drawn by the UA, and with nothing said about
|
||||
* them the UA uses its own accent -- a fixed blue no theme can reach. Five sliders are on
|
||||
* screen: the log level on Settings, and the duration sliders in the food, water and ration
|
||||
* calculators.
|
||||
*
|
||||
* Night is where that is not merely inconsistent. The theme exists so a user reading in
|
||||
* the dark keeps their adaptation and every token is a red, so a slider arriving in the
|
||||
* browser's blue was the brightest non-red thing on the page -- on a control the user drags
|
||||
* back and forth while watching it.
|
||||
*
|
||||
* `--blue` for the same reason links take it: it remaps per theme, so night gets the dim
|
||||
* red and amber the amber without a second rule.
|
||||
*/
|
||||
input[type="range"],
|
||||
input[type="checkbox"],
|
||||
input[type="radio"],
|
||||
progress {
|
||||
accent-color: var(--blue);
|
||||
}
|
||||
|
||||
/*
|
||||
* The half of a slider's track that is NOT filled, and the tick marks beside it, are drawn
|
||||
* from the UA's own light palette -- `accent-color` reaches the thumb and the filled half
|
||||
* and nothing else. On a dark ground that leaves a pale slab behind the thumb.
|
||||
*
|
||||
* Scoped to the controls rather than declared on `html`: `color-scheme` also governs
|
||||
* scrollbars, spinners and every other piece of browser chrome on the page, which is a
|
||||
* larger change than this and one to make deliberately rather than as a side effect.
|
||||
*/
|
||||
html[data-theme="dark"] input[type="range"],
|
||||
html[data-theme="night"] input[type="range"],
|
||||
html[data-theme="amber"] input[type="range"],
|
||||
html[data-theme="dark"] input[type="checkbox"],
|
||||
html[data-theme="night"] input[type="checkbox"],
|
||||
html[data-theme="amber"] input[type="checkbox"],
|
||||
html[data-theme="dark"] input[type="radio"],
|
||||
html[data-theme="night"] input[type="radio"],
|
||||
html[data-theme="amber"] input[type="radio"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
a:focus-visible {
|
||||
outline: 2px solid var(--blue);
|
||||
outline-offset: 2px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue