Rework the theme selector ahead of the design refresh - #3259
Conversation
84e1d85 to
8f97202
Compare
|
Pull request environment is available at https://stoctodocspr3259.z22.web.core.windows.net. You can view the ephemeral environment status in Octopus Deploy. This environment will be automatically deprovisioned when the pull request is closed, or after 7 days of inactivity. |
Move theme handling onto a single contract in src/lib/theme.ts, shared by the pre-paint inline script, the client controller and the stylesheets. <html> now always carries data-theme="light" or "dark" (never absent, never "system"), so incoming design token stylesheets can key off the attribute with no :root fallback. data-theme-preference records the raw choice, including "system", ready for a three-position control. Fixes along the way: - The two switchers each cached their own state, so toggling one left the other stale. Both now read the same attribute. - The knob position came from a JS-applied class, so it painted on the light side and slid across on every load. It is now driven by html[data-theme]. - tabindex="-1" on the input plus tabindex="0" on the label made the focusable element a bare <label> with no role, name or state. The input is focusable again and the deprecated keyCode handler is gone. - A localStorage throw in the head script left the site permanently light. - color-scheme is set per theme, so native controls and scrollbars follow. - OS preference changes are followed until the user picks a side; other tabs stay in sync via the storage event. The controller moves from public/ into src/ so Astro bundles, minifies and dedupes it, and an astro:after-swap handler keeps it working if <ClientRouter /> is ever added. Adds tests/theme.spec.ts (7 tests) and PLAYWRIGHT_PORT so the suite can run beside a dev server. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moving to a native checkbox meant only Space toggled it - a checkbox ignores Enter. The switch role treats Enter as an optional second activation key and the control supported it before, so bind it explicitly on the input. Tests now press each key twice to check it toggles both ways. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Route the Enter handler through box.click() so the change listener stays the only writer of theme state, instead of flipping box.checked separately. - Scope the dark slider rule to .theme-switcher so a future .switch-slider elsewhere does not inherit it. - Use auto-retrying toHaveAttribute assertions in the theme spec rather than one-shot getAttribute reads. - Document that theme:change does not fire at boot, and that the src/pages/report/*.astro pages render their own <html> and sit outside the data-theme contract. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#3261 added the token CSS, which defines its custom properties only under [data-theme='light'] and [data-theme='dark'] with nothing at :root, and added a server-rendered data-theme="light" on <html> so the attribute is present even without scripting. Both are kept: ThemeScript.astro resolves the real value before first paint on top of that default. - Point the HtmlHead token comment at ThemeScript.astro rather than HEADER_SCRIPTS, which no longer holds the theme script. - Spell out in theme.ts why the attribute has to be unconditional. - Add a javaScriptEnabled: false test so the server-rendered default cannot be dropped again without a failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3d27ca5 to
cd273a3
Compare
| import { devices } from '@playwright/test'; | ||
|
|
||
| // Override with PLAYWRIGHT_PORT to run the suite alongside a dev server. | ||
| const port = Number(process.env.PLAYWRIGHT_PORT ?? 3000); |
There was a problem hiding this comment.
so we can run tests while dev server is running
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| webServer: { | ||
| command: 'npm run preview', | ||
| port: 3000, | ||
| command: `npm run preview -- --port ${port}`, |
There was a problem hiding this comment.
What's up with this change here? Did the previous port key in the config object not work?
There was a problem hiding this comment.
Added it so we can run the test while running the dev server, rather than being hard coded to 3000
There was a problem hiding this comment.
yeah but it still has port on the line below.
And would this also work?
webServer: {
command: 'npm run preview',
port: port,
}|
I don't profess to understand the nitty-gritty detail of css and root selectors vs other things, but at a high level it all seems sound and reasonable. I noticed though on the staging site, when switching between light and dark mode, the color changes on the header items now "pulse" in a way that they didn't before. To experience this, go to this page: https://octopus.com/docs/infrastructure/accounts/openid-connect on the main site, and toggle between light and dark a few times quickly. Then do the same on the staging site: https://stoctodocspr3259.z22.web.core.windows.net/docs/infrastructure/accounts/openid-connect and note how all the header text such as "OpenID Connect" animates differently. While personally I think it's kind of weird looking, It isn't unacceptable -- we could choose to accept the new color changes. But if we can fix them instead, we should |
Rebuilds theme management so it is a solid base for the design refresh, and reconciles it with the design token CSS that landed in #3261.
Rebased onto current main. #3261 arrived while this was open and touched the same two lines; see "Relationship to #3261" below for how they fit together.
The contract the token stylesheets rely on
<html>always carriesdata-theme="light"ordata-theme="dark"— never absent, never"system".layouts/Default.astrorenderslightas the server default andThemeScript.astroresolves the real value before first paint.lightTheme.css/darkTheme.cssdefine their custom properties only under[data-theme="light"]/[data-theme="dark"]and nothing at:root. A page with no attribute resolves no colour tokens at all. That is why the server default from Consume design tokens as CSS custom properties #3261 is kept and is now covered by ajavaScriptEnabled: falsetest.<html>also carriesdata-theme-preference(light/dark/system). Nothing styles off it; it is there so a future three-position control renders correctly.theme:changefires on<html>with{ theme, preference }. It does not fire at boot — readdata-themefor the current value and listen for later changes.src/lib/theme.ts. Change them there and the pre-paint script, controller and tests all follow.Specificity note for the variable migration:
vars.cssuseshtml[data-theme='dark'](0,1,1) and the tokens use[data-theme='dark'](0,1,0), so existing variables win regardless of load order. Tokens sit underneath until each name is replaced.Functional change:
color-schemevars.cssnow setscolor-scheme: lightat:rootandcolor-scheme: darkunderhtml[data-theme='dark']. This tells the browser to render native UI to match the theme, so it reaches beyond the toggle and is the one change here with blast radius across the site.What visibly changes in dark mode: the main document scrollbar and code-block horizontal scrollbars render dark instead of light. Nothing styles those, so the UA default applied and a light scrollbar sat against a dark page. Confirmed in a real browser — this is the intended improvement.
What does not change, because it is already fully styled:
background: transparent,color: var(--color-text), themed placeholder, themed container. The text caret followscolor-scheme, which improves it.main.csshas custom::-webkit-scrollbarrules with a themed thumb; custom styling wins.bodysetsbackground-color: var(--color-base-primary)explicitly, so the UA canvas default never shows.No
<select>or date inputs exist in the docs templates. Autofill backgrounds will also follow the theme, which is the same behaviour as the rest of octopus.com.Bugs fixed
isDarkModeat construction, so toggling the mobile one left the header one stale. Resize past 1130px and it showed the wrong position; clicking it then setdata-themeto the value it already had..dark-modeclass, so in dark mode it painted on the light side and slid across after hydration. Now driven byhtml[data-theme], so it is correct on the first frame.tabindex="-1"on the input plustabindex="0"on the label meant the focusable element was a bare<label>— no role, name or state exposed. The realinput[role=switch]is focusable again, the deprecatedkeyCodehandler is gone, and the focus ring is drawn viainput:focus-visible + label.localStoragewas unguarded. A throw in the head script (Safari private mode, cookie-blocked iframes) killed the IIFE and left the site permanently light regardless of OS preference.click()so thechangelistener stays the only writer of state. Both keys are tested.Also
storageevent.public/intosrc/scripts/, so Astro bundles, minifies and dedupes it instead of shipping an unhashed public file loaded viais:inline.HEADER_SCRIPTSstring inconfig.tsintoThemeScript.astro, rendered first in<head>— before every stylesheet link, so the parser is not waiting on a CSS download to run it. It staysis:inlinedeliberately; it must not be bundled or deferred.astro:after-swaphandler so the switcher keeps working if<ClientRouter />is ever added.src/components/ThemeSwitcher.astro, used by bothHeader.astroandMobileMenu.astro.Relationship to #3261
#3261 moved
data-themefrom<body>to<html>, the same direction as this branch, and added the token CSS. The rebase resolved cleanly because the two touched different halves of the change: #3261 added the attribute to the<html>line, this branch only removed it from<body>.The
data-theme="light"default from #3261 is preserved and is now load-bearing for the reason described above. A comment #3261 left pointing atHEADER_SCRIPTSfor the inline theme script was updated, since that script now lives inThemeScript.astro.Testing
tests/theme.spec.ts— 9 tests, all passing:localStoragethrowsdata-themestill present with scripting disabled, so the tokens resolvepnpm buildpasses (2697 pages).playwright.config.tsnow honoursPLAYWRIGHT_PORTso the suite can run alongside a dev server on 3000.Not touched:
class="initial"on<html>has no matching CSS anywhere, but I could not prove nothing external reads it.I have also manually tested the above bugs and can confirm they are resolved, i found one trivial thing that was fixed being pressing enter on the toggle when tabbing through ui
🤖 Generated with Claude Code