diff --git a/app/src/components/ThemeSamplePage.js b/app/src/components/ThemeSamplePage.js index 66825e3a..2d669481 100644 --- a/app/src/components/ThemeSamplePage.js +++ b/app/src/components/ThemeSamplePage.js @@ -78,6 +78,10 @@ const Row = ({children}) =>
; +// 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() {
+
+ + setLogLevel(parseInt(e.target.value))} + style={{width: '100%'}} + /> +
+

+ 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 --blue 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. +

diff --git a/app/src/components/ui/ui-layout.cy.js b/app/src/components/ui/ui-layout.cy.js index 75920b40..6b36f97f 100644 --- a/app/src/components/ui/ui-layout.cy.js +++ b/app/src/components/ui/ui-layout.cy.js @@ -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 `` 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( + , + 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 ``, so the whole diff --git a/app/src/themes/tokens.css b/app/src/themes/tokens.css index 5c5df2a8..b2dd9713 100644 --- a/app/src/themes/tokens.css +++ b/app/src/themes/tokens.css @@ -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;