From 1bb54a7a30d92f72c433923b1b21cfd4c72d0f70 Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 8 Jun 2026 09:29:10 +0100 Subject: [PATCH 1/4] fix(theme-color): emit media-scoped dark variant for iOS Safari (#7606) The theme-color meta only had a single light value rendered server-side; dark mode was applied purely by JS (skin_variants.ts) after page load. iOS Safari colors the address bar at parse time and does not reliably repaint when JS mutates the meta later, so dark-mode iPhone users kept a white address bar above a dark toolbar (the green Chromium Playwright test masked this because Chrome does honor the dynamic update). Emit a prefers-color-scheme media-scoped pair server-side so the correct color is chosen at first paint without JS: - Add SkinColors.darkToolbarColor() (reuses toolbarColorForTokens). - Expose enableDarkMode via getPublicSettings so the templates can gate the dark variant on it (no dark variant when dark mode can't be reached). - Apply to both pad.html and timeslider.html. - updateThemeColorMeta now updates every theme-color meta so a manual #options-darkmode toggle still wins over the media scoping on desktop/Android. - Backend + frontend tests updated to assert the media-scoped pair and the enableDarkMode-off case. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/node/utils/Settings.ts | 5 ++- src/node/utils/SkinColors.ts | 15 ++++++++ src/static/js/skin_variants.ts | 26 +++++++------ src/templates/pad.html | 16 +++++--- src/templates/timeslider.html | 6 ++- src/tests/backend/specs/specialpages.ts | 36 ++++++++++++++---- .../specs/theme_color_dark_mode.spec.ts | 37 ++++++++++++------- 7 files changed, 100 insertions(+), 41 deletions(-) diff --git a/src/node/utils/Settings.ts b/src/node/utils/Settings.ts index 4dd34a616ba..1878e744fdb 100644 --- a/src/node/utils/Settings.ts +++ b/src/node/utils/Settings.ts @@ -369,7 +369,7 @@ export type SettingsType = { from: string | null; auth: {user: string; pass: string} | null; }, - getPublicSettings: () => Pick, + getPublicSettings: () => Pick, } const settings: SettingsType = { @@ -871,6 +871,9 @@ const settings: SettingsType = { title: settings.title, skinName: settings.skinName, skinVariants: settings.skinVariants, + // Needed so pad.html / timeslider.html only emit the dark theme-color + // variant when dark mode can actually be reached client-side (#7606). + enableDarkMode: settings.enableDarkMode, enablePadWideSettings: settings.enablePadWideSettings, enablePluginPadOptions: settings.enablePluginPadOptions, privacyBanner: getPublicPrivacyBanner(), diff --git a/src/node/utils/SkinColors.ts b/src/node/utils/SkinColors.ts index 74b9bedbcfe..5698ea9a3c0 100644 --- a/src/node/utils/SkinColors.ts +++ b/src/node/utils/SkinColors.ts @@ -14,3 +14,18 @@ export const configuredToolbarColor = ( if (skinName !== 'colibris') return null; return toolbarColorForTokens((skinVariants || '').split(/\s+/).filter(Boolean)); }; + +// The toolbar color colibris auto-switches to on a dark-OS client (pad.ts +// forces 'super-dark-toolbar' when enableDarkMode is on and the system is in +// dark mode). Templates emit this in a `media="(prefers-color-scheme: dark)"` +// theme-color meta so iOS Safari — which colors the address bar at parse time +// and does not reliably repaint when JS mutates the tag later — picks the +// right color at first paint instead of staying on the light baseline +// (issue #7606). Returns null for non-colibris skins, matching +// configuredToolbarColor. +export const darkToolbarColor = ( + skinName: string | undefined | null, +): string | null => { + if (skinName !== 'colibris') return null; + return toolbarColorForTokens(['super-dark-toolbar']); +}; diff --git a/src/static/js/skin_variants.ts b/src/static/js/skin_variants.ts index 22c055bf817..cf193cd7de8 100644 --- a/src/static/js/skin_variants.ts +++ b/src/static/js/skin_variants.ts @@ -7,19 +7,21 @@ const containers = ['editor', 'background', 'toolbar']; const colors = ['super-light', 'light', 'dark', 'super-dark']; // Keep in sync with the toolbar the user actually -// sees. The server emits a baseline derived from settings.skinVariants, but -// pad.ts may flip the toolbar to super-dark on first paint (enableDarkMode -// + prefers-color-scheme:dark + no localStorage white-mode override) and -// the user can toggle via #options-darkmode. Without this, dark-mode users -// keep the light meta and see a white address bar above a dark toolbar -// (issue #7606 follow-up). Color resolution lives in skin_toolbar_colors so -// the server-rendered baseline and the client updates share one source of -// truth — Qodo flagged the prior duplicated table as a drift hazard. +// sees. The server emits a media-scoped light + dark pair so iOS Safari picks +// the right color at first paint (issue #7606); on desktop/Android the user +// can still override the system scheme via #options-darkmode. When that +// happens we point EVERY theme-color meta at the chosen color so the explicit +// choice wins regardless of which media query the browser is currently +// matching — otherwise toggling light while the OS is dark (or vice versa) +// would update a tag the browser is ignoring. Color resolution lives in +// skin_toolbar_colors so the server-rendered baseline and the client updates +// share one source of truth — Qodo flagged the prior duplicated table as a +// drift hazard. const updateThemeColorMeta = (newClasses: string[]) => { - const meta = document.querySelector('meta[name="theme-color"]'); - if (!meta) return; - meta.setAttribute('content', - toolbarColorForTokens(newClasses.join(' ').split(/\s+/).filter(Boolean))); + const metas = document.querySelectorAll('meta[name="theme-color"]'); + if (!metas.length) return; + const color = toolbarColorForTokens(newClasses.join(' ').split(/\s+/).filter(Boolean)); + metas.forEach((meta) => { meta.setAttribute('content', color); }); }; // add corresponding classes when config change diff --git a/src/templates/pad.html b/src/templates/pad.html index 151334a4ec3..58429144c30 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -7,11 +7,16 @@ && req.acceptsLanguages(Object.keys(langs))) || 'en'; var renderDir = (langs[renderLang] && langs[renderLang].direction === 'rtl') ? 'rtl' : 'ltr'; // theme-color matches the configured toolbar so mobile address bars don't - // paint a mismatched system color above the toolbar on first paint. We do - // not emit a prefers-color-scheme: dark variant: the client-side dark-mode - // auto-switch is gated on enableDarkMode, matchMedia, and a localStorage - // white-mode override, none of which a media query can express. + // paint a mismatched system color above the toolbar on first paint. We emit + // a prefers-color-scheme: dark variant scoped with the `media` attribute so + // iOS Safari — which colors the address bar at parse time and does not + // reliably repaint when JS mutates the tag later — picks the dark toolbar + // color at first paint on a dark-OS client (issue #7606). The dark variant + // is only emitted when enableDarkMode is on, since that is what gates the + // client-side auto-switch; the manual #options-darkmode toggle and the + // localStorage white-mode override are still handled by skin_variants.ts. var configuredColor = skinColors.configuredToolbarColor(settings.skinName, settings.skinVariants); + var darkColor = settings.enableDarkMode ? skinColors.darkToolbarColor(settings.skinName) : null; %> @@ -49,7 +54,8 @@ - <% if (configuredColor) { %><% } %> + <% if (configuredColor) { %> media="(prefers-color-scheme: light)"<% } %>><% } %> + <% if (darkColor) { %><% } %> <% e.begin_block("styles"); %> diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index 0e9eebb8f27..04f1ce04fe7 100644 --- a/src/templates/timeslider.html +++ b/src/templates/timeslider.html @@ -5,6 +5,9 @@ && req.acceptsLanguages(Object.keys(langs))) || 'en'; var renderDir = (langs[renderLang] && langs[renderLang].direction === 'rtl') ? 'rtl' : 'ltr'; var themeColor = skinColors.configuredToolbarColor(settings.skinName, settings.skinVariants); + // See pad.html: emit a media-scoped dark variant so iOS Safari picks the + // dark toolbar color at first paint on a dark-OS client (issue #7606). + var darkThemeColor = settings.enableDarkMode ? skinColors.darkToolbarColor(settings.skinName) : null; %> @@ -39,7 +42,8 @@ - <% if (themeColor) { %><% } %> + <% if (themeColor) { %> media="(prefers-color-scheme: light)"<% } %>><% } %> + <% if (darkThemeColor) { %><% } %> <% e.begin_block("timesliderStyles"); %> diff --git a/src/tests/backend/specs/specialpages.ts b/src/tests/backend/specs/specialpages.ts index 17b7c49a86a..77a8ef3dc4b 100644 --- a/src/tests/backend/specs/specialpages.ts +++ b/src/tests/backend/specs/specialpages.ts @@ -60,19 +60,36 @@ describe(__filename, function () { beforeEach(function () { backups.skinName = settings.skinName; backups.skinVariants = settings.skinVariants; + backups.enableDarkMode = settings.enableDarkMode; }); afterEach(function () { settings.skinName = backups.skinName; settings.skinVariants = backups.skinVariants; + settings.enableDarkMode = backups.enableDarkMode; }); - it('pad page emits theme-color matching the configured colibris toolbar', async function () { + it('pad page emits a light baseline and a media-scoped dark variant', async function () { + // Issue #7606: iOS Safari colors the address bar at parse time and does + // not reliably repaint when JS mutates the meta later, so the dark + // toolbar color must be selectable at first paint via a media query. settings.skinName = 'colibris'; settings.skinVariants = 'super-light-toolbar super-light-editor light-background'; + settings.enableDarkMode = true; + const res = await agent.get('/p/testpad').expect(200); + assert.match(res.text, + //); + assert.match(res.text, + //); + }); + + it('pad page omits the dark variant when enableDarkMode is off', async function () { + // With dark mode disabled the client never auto-switches, so the address + // bar should stay on the unscoped light baseline. + settings.skinName = 'colibris'; + settings.skinVariants = 'super-light-toolbar super-light-editor light-background'; + settings.enableDarkMode = false; const res = await agent.get('/p/testpad').expect(200); assert.match(res.text, //); - // No media-query variants — runtime dark-mode also depends on localStorage, - // which a server-rendered media query cannot account for. assert.doesNotMatch(res.text, /prefers-color-scheme/); }); @@ -80,7 +97,7 @@ describe(__filename, function () { settings.skinName = 'colibris'; settings.skinVariants = 'dark-toolbar dark-editor dark-background'; const res = await agent.get('/p/testpad').expect(200); - assert.match(res.text, //); + assert.match(res.text, //); - assert.doesNotMatch(res.text, /prefers-color-scheme/); + assert.match(res.text, + //); + assert.match(res.text, + //); }); it('timeslider page omits theme-color for non-colibris skins', async function () { diff --git a/src/tests/frontend-new/specs/theme_color_dark_mode.spec.ts b/src/tests/frontend-new/specs/theme_color_dark_mode.spec.ts index 0393b913bde..4e3783d7b65 100644 --- a/src/tests/frontend-new/specs/theme_color_dark_mode.spec.ts +++ b/src/tests/frontend-new/specs/theme_color_dark_mode.spec.ts @@ -1,16 +1,26 @@ import {expect, test, Page} from '@playwright/test'; import {goToNewPad} from '../helper/padHelper'; -const themeColor = (page: Page) => - page.locator('meta[name="theme-color"]').getAttribute('content'); +// Issue #7606: the server emits a media-scoped pair of theme-color metas so +// iOS Safari can pick the right address-bar color at first paint without JS. +// Read each by its media attribute; the browser applies whichever matches the +// active color scheme. +const lightThemeColor = (page: Page) => + page.locator('meta[name="theme-color"][media="(prefers-color-scheme: light)"]') + .getAttribute('content'); +const darkThemeColor = (page: Page) => + page.locator('meta[name="theme-color"][media="(prefers-color-scheme: dark)"]') + .getAttribute('content'); test.describe('light color scheme', () => { test.use({colorScheme: 'light'}); test('theme-color meta tracks the dark-mode toggle', async ({page}) => { await goToNewPad(page); - // Server emits the light baseline derived from settings.skinVariants. - expect(await themeColor(page)).toBe('#ffffff'); + // First paint: light baseline is active, and the dark variant is present + // so a dark-OS client would have rendered #485365 without any JS. + expect(await lightThemeColor(page)).toBe('#ffffff'); + expect(await darkThemeColor(page)).toBe('#485365'); await page.locator('button[data-l10n-id="pad.toolbar.settings.title"]').click(); await expect(page.locator('#theme-toggle-row')).toBeVisible(); @@ -18,25 +28,24 @@ test.describe('light color scheme', () => { // Colibris styles the native checkbox via a sibling label; click the label // so the toggle fires the real change event the production code listens on. await page.locator('label[for="options-darkmode"]').click(); - // pad.ts forces super-dark-toolbar (#485365) regardless of the configured - // light skinVariants, so the meta must follow the client-applied class. - await expect.poll(() => themeColor(page)).toBe('#485365'); + // The explicit toggle points every theme-color meta at the dark toolbar + // color, so the address bar goes dark even though the OS is in light mode. + await expect.poll(() => lightThemeColor(page)).toBe('#485365'); await page.locator('label[for="options-darkmode"]').click(); - await expect.poll(() => themeColor(page)).toBe('#ffffff'); + await expect.poll(() => lightThemeColor(page)).toBe('#ffffff'); }); }); test.describe('dark color scheme', () => { test.use({colorScheme: 'dark'}); - test('theme-color meta follows the auto dark-mode switch on dark-OS clients', + test('theme-color meta is dark at first paint on dark-OS clients', async ({page}) => { await goToNewPad(page); - // pad.ts auto-switches to super-dark-toolbar when enableDarkMode is on, - // matchMedia(prefers-color-scheme:dark) matches, and no localStorage - // white-mode override is set. The meta must follow the applied class — - // this is the case stffen reported on issue #7606. - await expect.poll(() => themeColor(page)).toBe('#485365'); + // The media-scoped dark variant is what fixes stffen's iPhone: it is + // present and dark before any JS runs, so iOS Safari colors the address + // bar correctly at parse time (issue #7606). + await expect.poll(() => darkThemeColor(page)).toBe('#485365'); }); }); From 0975c410f1c1cfd725f76267f3c541d483d43e88 Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 8 Jun 2026 09:43:02 +0100 Subject: [PATCH 2/4] fix(theme-color): prevent the light-mode flash on dark-OS load (#7606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The theme-color meta fix corrects the address-bar tint, but dark-OS users still saw the whole page painted light before the JS bundle ran and applied the dark skin classes in postAceInit — a visible flash on every browser, not just the mobile address bar. Add a tiny blocking inline script in , before the stylesheet, that applies the dark skin classes to synchronously during parse when the client is in dark mode (matchMedia + no localStorage white-mode override). The condition mirrors pad.ts's auto-switch, which still runs on init to wire up the #options-darkmode toggle and theme the editor iframes (those don't exist yet at parse time). Gated on the same enableDarkMode + colibris check as the dark theme-color variant. Applied to pad.html and timeslider.html. Verified in Chromium: at domcontentloaded a dark-OS client's already carries super-dark-editor/dark-background/super-dark-toolbar (no flash), and a light-OS client is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/templates/pad.html | 25 +++++++++ src/templates/timeslider.html | 20 ++++++++ src/tests/backend/specs/specialpages.ts | 51 +++++++++++++++++++ .../specs/theme_color_dark_mode.spec.ts | 11 ++++ 4 files changed, 107 insertions(+) diff --git a/src/templates/pad.html b/src/templates/pad.html index 58429144c30..9911f39b079 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -58,6 +58,31 @@ <% if (darkColor) { %><% } %> + <% if (darkColor) { %> + + <% } %> + <% e.begin_block("styles"); %> diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index 04f1ce04fe7..032a4f2c985 100644 --- a/src/templates/timeslider.html +++ b/src/templates/timeslider.html @@ -45,6 +45,26 @@ <% if (themeColor) { %> media="(prefers-color-scheme: light)"<% } %>><% } %> <% if (darkThemeColor) { %><% } %> + <% if (darkThemeColor) { %> + + <% } %> <% e.begin_block("timesliderStyles"); %> diff --git a/src/tests/backend/specs/specialpages.ts b/src/tests/backend/specs/specialpages.ts index 77a8ef3dc4b..c88e7f6e6c3 100644 --- a/src/tests/backend/specs/specialpages.ts +++ b/src/tests/backend/specs/specialpages.ts @@ -127,4 +127,55 @@ describe(__filename, function () { assert.doesNotMatch(res.text, /theme-color/); }); }); + + describe('dark-mode flash prevention', function () { + const backups:MapArrayType = {}; + beforeEach(function () { + backups.skinName = settings.skinName; + backups.enableDarkMode = settings.enableDarkMode; + }); + afterEach(function () { + settings.skinName = backups.skinName; + settings.enableDarkMode = backups.enableDarkMode; + }); + + const PREPAINT = "classList.add('super-dark-editor', 'dark-background', 'super-dark-toolbar')"; + + it('pad page inlines the pre-paint dark-mode script before the stylesheet', async function () { + // Issue #7606: without this, dark-OS users see the page painted light + // until the JS bundle runs and flips the skin classes. The inline script + // must come before pad.css so the dark classes are on at first + // paint. + settings.skinName = 'colibris'; + settings.enableDarkMode = true; + const res = await agent.get('/p/testpad').expect(200); + const scriptIdx = res.text.indexOf(PREPAINT); + const cssIdx = res.text.indexOf('css/pad.css'); + assert(scriptIdx !== -1, 'expected the pre-paint dark-mode script in the pad page'); + assert(cssIdx !== -1 && scriptIdx < cssIdx, + 'pre-paint dark-mode script must come before pad.css so it applies before first paint'); + }); + + it('timeslider page inlines the pre-paint dark-mode script', async function () { + settings.skinName = 'colibris'; + settings.enableDarkMode = true; + const res = await agent.get('/p/testpad/timeslider?embed=1').expect(200); + assert(res.text.includes(PREPAINT), + 'expected the pre-paint dark-mode script in the timeslider page'); + }); + + it('omits the pre-paint script when dark mode is disabled', async function () { + settings.skinName = 'colibris'; + settings.enableDarkMode = false; + const res = await agent.get('/p/testpad').expect(200); + assert(!res.text.includes(PREPAINT)); + }); + + it('omits the pre-paint script for non-colibris skins', async function () { + settings.skinName = 'no-skin'; + settings.enableDarkMode = true; + const res = await agent.get('/p/testpad').expect(200); + assert(!res.text.includes(PREPAINT)); + }); + }); }); diff --git a/src/tests/frontend-new/specs/theme_color_dark_mode.spec.ts b/src/tests/frontend-new/specs/theme_color_dark_mode.spec.ts index 4e3783d7b65..056d232e986 100644 --- a/src/tests/frontend-new/specs/theme_color_dark_mode.spec.ts +++ b/src/tests/frontend-new/specs/theme_color_dark_mode.spec.ts @@ -48,4 +48,15 @@ test.describe('dark color scheme', () => { // bar correctly at parse time (issue #7606). await expect.poll(() => darkThemeColor(page)).toBe('#485365'); }); + + test('page paints dark without a light flash on dark-OS clients', + async ({page}) => { + await goToNewPad(page); + // The inline pre-paint script in adds the dark skin classes to + // before the stylesheet paints, so a dark-OS user never sees the + // light page (issue #7606). Asserting the class is present confirms the + // dark skin is applied; the backend test verifies the script is ordered + // before pad.css so it takes effect at first paint. + await expect(page.locator('html')).toHaveClass(/super-dark-editor/); + }); }); From 90008f2cf9f0afa5e497834d43d7bf2b24559ef6 Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 8 Jun 2026 09:49:46 +0100 Subject: [PATCH 3/4] docs(changelog): note the dark-mode address-bar + flash fix (#7606) Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ead1143007c..f4729a5061e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ A defence-in-depth pass across the API, token, export, and deployment surfaces: - **URL-encode pad names in the admin 'Open' button and recent pads (#7865 / #7895).** Pad names are `encodeURIComponent`-d in the admin `PadPage` Open href and the colibris recent-pads href, and `decodeURIComponent`-d when read back from the URL pathname; legacy URL-encoded recent-pads names are normalised before re-encoding to prevent double-encoding (`%2F` → `%252F`). The admin Open `window.open` gains `noopener,noreferrer`. - **OIDC — fix broken `OIDCAdapter` flows (#7837).** Repairs the adapter flows and widens the storage type to include `string` for the `userCode` index; adds regression tests. - **Accessibility — dialog titles/descriptions and a missing l10n key (#7835 / #7836).** Adds the `index.code` key referenced by `index.html` but never defined (which produced a "Couldn't find translation key" console error on the landing page), and gives every admin `@radix-ui/react-dialog` `Dialog.Content` a `Dialog.Title` and `Dialog.Description` (visually hidden where there's no visible heading), silencing Radix's a11y warnings. A new backend spec fails CI if any `data-l10n-id` in `src/templates/*.html` is missing from `en.json`. +- **Dark mode — fix the white address bar and the light-flash on load (#7909, issue #7606).** Dark-mode users still saw a white mobile address bar above the dark toolbar, and the whole page flashed light before going dark. Both came from rendering the light state server-side and switching to dark only after the JS bundle ran: iOS Safari reads `theme-color` at parse time and doesn't reliably repaint on a later JS mutation, and the page painted light before the bundle applied the dark skin classes. The server now emits a `prefers-color-scheme`-scoped `theme-color` pair so the address bar is correct at first paint, plus a small blocking `` script that applies the dark skin classes before the stylesheet paints. Both are gated on `enableDarkMode` (default on) and the colibris skin; `pad.ts` still runs on init to wire up the `#options-darkmode` toggle (which now updates every `theme-color` meta) and theme the editor iframes. Applies to the pad and timeslider views. ### Internal / contributor-facing From 6f7b07b12cd2db36b9a18fada0e64ae5f6b86111 Mon Sep 17 00:00:00 2001 From: John McLear Date: Mon, 8 Jun 2026 10:04:06 +0100 Subject: [PATCH 4/4] fix(theme-color): guard pre-paint script on #skinvariantsbuilder; ignore updater state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address PR review: - Copilot: the inline pre-paint dark-mode script must skip the auto-dark switch on the #skinvariantsbuilder hash, matching pad.ts — otherwise it forces super-dark classes on a dark-OS client and fights the variants builder UI. Added the guard to pad.html and timeslider.html and a backend assertion so it can't regress. - Qodo: ignore var/update-state.json (runtime updater cache) so the server run that regenerates it can't dirty the tree or be committed accidentally. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/templates/pad.html | 3 +++ src/templates/timeslider.html | 1 + src/tests/backend/specs/specialpages.ts | 4 ++++ var/.gitignore | 1 + 4 files changed, 9 insertions(+) diff --git a/src/templates/pad.html b/src/templates/pad.html index 9911f39b079..6bbc359c6ef 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -68,6 +68,9 @@ // and theme the editor iframes (those don't exist yet at parse time). (function () { try { + // The skin-variants builder lets the user pick variants by hand; pad.ts + // skips the auto-dark switch there, so we must too. + if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') return; if (localStorage.getItem('ep_darkMode') === 'false') return; if (!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)) return; diff --git a/src/templates/timeslider.html b/src/templates/timeslider.html index 032a4f2c985..f070fd73f54 100644 --- a/src/templates/timeslider.html +++ b/src/templates/timeslider.html @@ -51,6 +51,7 @@ // so dark-OS users don't see a white flash on load (issue #7606). (function () { try { + if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') return; if (localStorage.getItem('ep_darkMode') === 'false') return; if (!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)) return; diff --git a/src/tests/backend/specs/specialpages.ts b/src/tests/backend/specs/specialpages.ts index c88e7f6e6c3..343b29f23eb 100644 --- a/src/tests/backend/specs/specialpages.ts +++ b/src/tests/backend/specs/specialpages.ts @@ -154,6 +154,10 @@ describe(__filename, function () { assert(scriptIdx !== -1, 'expected the pre-paint dark-mode script in the pad page'); assert(cssIdx !== -1 && scriptIdx < cssIdx, 'pre-paint dark-mode script must come before pad.css so it applies before first paint'); + // Must skip the auto-dark switch on the skin-variants builder, matching + // pad.ts (so it can't fight the builder UI on a dark-OS client). + assert(res.text.includes('#skinvariantsbuilder'), + 'pre-paint script must guard against the #skinvariantsbuilder hash like pad.ts'); }); it('timeslider page inlines the pre-paint dark-mode script', async function () { diff --git a/var/.gitignore b/var/.gitignore index d75cb9e42b2..32742afc343 100644 --- a/var/.gitignore +++ b/var/.gitignore @@ -3,3 +3,4 @@ minified* installed_plugins.json dirty.db rusty.db +update-state.json