;
+// 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() {
+ 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;