Dark and Light Theme Support for the Viewer Shell
Summary
Add a persistent System / Light / Dark preference to the trusted agent-pages viewer shell. The preference will style the HTML toolbar, Markdown renderer, version menu, and viewer error pages without altering published HTML or weakening the iframe sandbox.
Published HTML remains responsible for its own palette. The viewer will clearly communicate that boundary and document how publishers can make artifacts follow the operating-system theme with color-scheme and prefers-color-scheme.
Goals and Success Criteria
- The server-owned interface supports light, dark, and system-selected colors.
- The default is System, following
prefers-color-scheme. - A visitor can change the preference from both the HTML toolbar and Markdown header.
- The choice persists across viewer pages and versions.
- The control remains usable without JavaScript.
- The selected shell theme never rewrites, filters, or injects styles into published HTML.
- Existing sandbox, CSP, raw-content fidelity, version pinning, and public API behavior remain intact.
- Light and dark palettes meet WCAG AA contrast for normal viewer text and controls.
Implementation Changes
Theme preference and server behavior
-
Define the supported theme values as
system,light, anddark. -
Resolve the viewer theme from the
agent_pages_themecookie:- Missing or invalid cookie:
system. lightordark: explicit override.- The server must ignore invalid values rather than reflecting them into HTML.
- Missing or invalid cookie:
-
Add a public navigational endpoint:
GET /viewer/theme/{theme}?next=/p/{slug} -
Accept only the three theme values.
-
Validate
nextas a local absolute path beginning with/p/, with no scheme, host, backslash, or control characters. Return400for an invalid target so the endpoint cannot become an open redirect. -
For
lightanddark, return a303redirect and setagent_pages_themewith:Path=/pMax-Age=31536000HttpOnlySameSite=LaxSecurewhenPUBLIC_BASE_URLuses HTTPS
-
For
system, delete the cookie using the same path and redirect rules. -
Add
Vary: Cookieto server-rendered viewer responses whose markup or metadata depends on the preference. Do not add it to immutable raw HTML responses. -
Centralize theme resolution and theme-option URL construction so Markdown, HTML host, and error rendering use identical behavior.
Templates and controls
-
Render the resolved preference on the shared root element:
<html lang="en" data-theme="system"> -
Add
<meta name="color-scheme" content="light dark">. -
Render theme-color metadata from the server:
- Explicit light or dark: one matching
theme-colorvalue. - System: separate light and dark
theme-colorentries using media queries.
- Explicit light or dark: one matching
-
Create a shared, script-free theme-menu partial using
<details>and immutable links for System, Light, and Dark. -
Display the current selection in the summary, for example
Theme: Dark. -
Mark the selected option semantically with
aria-current="true"and visible styling. -
Place the theme menu:
- In the HTML toolbar between the version selector and “Open fullscreen.”
- In a compact controls row beneath or beside the Markdown title.
- On styled viewer error pages when a safe return path is available.
-
Use text labels rather than relying on sun/moon icons alone. The control’s accessible description must explain: “Changes the agent-pages viewer. Published HTML controls its own theme.”
-
Keep the existing 48px HTML toolbar. On narrow screens, preserve version, theme, and fullscreen controls while allowing the document title to truncate first.
CSS theme system
-
Replace
:root { color-scheme: light; }with a light/dark-capable token system. -
Keep the current light palette as the baseline.
-
Add this dark palette:
--bg: #111318; --surface: #1b1f27; --text: #f3f4f6; --muted: #aab2bf; --border: #343b48; --link: #8ab4ff; --code-bg: #252b35; -
Apply the dark tokens when:
data-theme="dark"is explicit.data-theme="system"andprefers-color-scheme: darkmatches.
-
Ensure
data-theme="light"remains light even when the operating system is dark. -
Set the CSS
color-schemeproperty to the resolved scheme so browser-native controls and scrollbars match. -
Update dropdown shadows, hover states, focus outlines, code blocks, tables, borders, and error surfaces for both palettes.
-
Add clear
:focus-visiblestyles for theme options and existing toolbar controls. -
Do not apply
filter: invert()or other forced transformations to the iframe.
Published HTML boundary and author guidance
-
Leave raw HTML response bodies byte-for-byte unchanged.
-
Do not add theme query parameters to iframe or fullscreen URLs.
-
Do not read the shell preference inside raw routes.
-
Keep raw CSP unchanged, including
sandbox,script-src 'none', andframe-ancestors 'self'. -
Keep the iframe’s empty
sandbox=""; do not add script or same-origin permissions. -
Document that an explicit viewer preference cannot override arbitrary sandboxed HTML. Artifact CSS media queries continue to follow the browser/operating-system preference, not the agent-pages shell override.
-
Add an authoring example:
:root { color-scheme: light dark; } @media (prefers-color-scheme: dark) { :root { /* artifact-owned dark palette */ } } -
Update the README, architecture documentation, decision log, and durable agent guidance to distinguish:
- Server-owned theme support for the toolbar, Markdown, and errors.
- Publisher-owned theme support inside HTML artifacts.
-
Preserve the existing remote-image policy and privacy warning.
Security and Compatibility
- Keep
script-src 'none'andform-action 'none'on viewer-shell CSP. Theme changes use ordinary navigation links, not scripts or forms. - The preference cookie is non-secret and contains only a validated enum value.
- API and MCP authentication cookies or tokens are not introduced.
- No database schema, R2 objects, REST schemas, MCP tools, or published-page metadata change.
- Existing canonical and immutable page URLs remain unchanged.
- “Open fullscreen” continues to target the resolved immutable raw version and has no server-owned toolbar.
Public Interface Changes
- Add
GET /viewer/theme/{theme}?next=<local-viewer-path>. - Add the
agent_pages_themepreference cookie described above. - Server-rendered viewer HTML gains
data-theme, color-scheme metadata, andVary: Cookie. - Raw HTML endpoints and all
/v1responses remain unchanged.
Test Plan
- Verify a viewer request without a cookie resolves to
system. - Verify valid light and dark requests set the correct cookie attributes and return a
303to the local viewer path. - Verify selecting system deletes the preference cookie.
- Verify invalid theme values are rejected and invalid
nextvalues cannot redirect off-site. - Verify explicit light/dark cookies render the matching
data-themeand metadata on HTML, Markdown, and styled error pages. - Verify system mode contains the light/dark media-aware metadata and CSS behavior.
- Verify the theme menu appears in both viewer layouts, marks the selected option, and generates safe return URLs.
- Verify viewer responses include
Vary: Cookiewhile raw HTML does not. - Verify shell CSP still contains
script-src 'none'andform-action 'none'. - Verify raw HTML content and CSP are identical regardless of the preference cookie.
- Preserve all existing iframe sandbox, fullscreen version pinning, Markdown sanitization, R2-fetch, and HTML-error tests.
- Run the complete pytest suite.
- Perform visual QA at desktop and mobile sizes for all three preferences:
- Toolbar and dropdown contrast.
- Markdown typography, code, tables, links, and error pages.
- Fixed-light HTML remains fixed light while the surrounding toolbar follows the viewer preference.
- Theme-aware HTML follows the operating-system theme as authored.
Assumptions
- Theme support remains entirely server-rendered and script-free.
- System is the default and recommended preference.
- The server does not claim to force a theme inside sandboxed HTML.
- The existing Jinja frontend remains the correct architecture; no React or frontend build pipeline is added.
- The toolbar height remains 48px.