Light/dark theme support: dual CSS compilation, toggle runtime, highlight-style, brand pairs (bd-0pic6) - #537
Open
cscheid wants to merge 10 commits into
Open
Light/dark theme support: dual CSS compilation, toggle runtime, highlight-style, brand pairs (bd-0pic6)#537cscheid wants to merge 10 commits into
cscheid wants to merge 10 commits into
Conversation
Design settled 2026-08-14: Q1-compatible rel-swap runtime with the color-scheme improvement (D1a), FOUC avoidance as the hard constraint, epic children A1-E created in braid, integration branch feature/light-dark-theme. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-model-a12bhj1g)
The theme: {light, dark} map now parses BOTH halves. DarkThemeConfig
{themes, theme_locations, suppress_bootstrap, is_default, key_location}
replaces dark_theme_ignored on ThemeConfig; the dark half goes through
the same from_theme_value parsing as the light half (none sentinel,
unknown-theme errors, PandocInlines frontmatter form). is_default
implements Q1's key-order rule (dark written first = dark is the author
default); two new quarto-config materialize tests guard the key-order
preservation that rule depends on. Brand token auto-injection now
applies per variant, and an explicit brand token in the dark list keeps
its position; naming brand in either half without brand: errors.
ResolvedThemeConfig carries the dark variant through resolve().
Zero behavior change by design: Q-14-3 still fires (now keyed off
dark.key_location) and only the light half compiles; both retire in A2
(dual compile). bootstrap_js predicate also updates in A2.
TDD: 10 new/extended quarto-sass unit tests confirmed red before the
implementation; full workspace suite green (12,125 tests).
Part of the light/dark epic (bd-0pic6); plan:
claude-notes/plans/2026-08-14-light-dark-theme-epic.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d-ld-a2-dual-compile-ds10l5wa)
CompileThemeCssStage now compiles the dark half of a theme:
{light, dark} map through the same pure pipeline, via the
ThemeConfig::dark_variant() projection, into a css:theme-dark:<fp>
artifact (quarto/quarto-theme-dark-<fp>.css; styles-dark.css for
single-doc renders). The stage body is refactored into a per-variant
variant_css() helper preserving the exact suppress/fast-path/themed
behavior per variant. The css:theme-dark: key prefix deliberately does
not extend css:theme:, so every existing light-only prefix consumer
keeps working unchanged.
D1a: the darkness-sentinel block in _bootstrap-rules.scss now also
declares :root{color-scheme:light|dark}, so UA-drawn chrome follows
theme darkness — including for single dark themes (theme: darkly) and
for custom-SCSS dark halves detected via $body-bg blackness (the
quarto-web shape). Verified to survive grass minification.
Q-14-3 (interim dark-half-ignored warning) is fully retired: emission,
catalog entry, docs page, and test references removed. BootstrapJsStage
now ships JS iff any variant ships Bootstrap
(ThemeConfig::ships_bootstrap), so {light: none, dark: darkly} keeps
its JS.
Interim state note: both variants are linked as plain stylesheets with
the dark link sorting first, so the light variant wins the cascade and
page appearance is unchanged until A3 (link attributes) and A4 (toggle)
land.
Golden-hash baseline re-captured for the one-rule color-scheme delta
(entry documents the verification). TDD: 5 stage/integration tests
confirmed red first; 12,135 workspace tests green.
Part of the light/dark epic (bd-0pic6); plan:
claude-notes/plans/2026-08-14-light-dark-theme-epic.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-a3-link-emission-ruw9kw4v) Artifact gains link_attribs (order-preserving Vec of HTML attributes for the emitted tag) and link_order (emission sort priority); collect_artifact_urls sorts by (link_order, key) and returns structured LinkedResource entries. Attribute-free entries still render as plain template strings, so custom templates writing $css$ keep working and single-variant output is byte-identical (golden-hash test unchanged). Attributed entries render as maps consumed by new single-line $if(css.href)$ branches in both built-in templates. The theme pair is stored with Q1's exact link contract: light (class=quarto-color-scheme, id=quarto-bootstrap), dark (quarto-color-scheme quarto-color-alternate), and — for author-default-light — a trailing re-link of the same light file with class=quarto-color-scheme-extra so pre-toggle paint and no-JS browsers land on the default variant (the FOUC hard constraint; class replaces so the future toggle's selectors skip it). data-mode comes from each sheet's compiled /*! dark */ sentinel rather than its slot, handling custom-SCSS dark halves. D1a: the full template head now emits <meta name="color-scheme"> when a dark variant exists — author default first, both schemes when respect-user-color-scheme: true (first reader for that key; A4 reuses it for the toggle runtime). TDD: 3 integration tests red first; E2E via real q2 render inspected (link trio + meta byte-shape verified). 12,139 workspace tests green. Part of the light/dark epic (bd-0pic6); plan: claude-notes/plans/2026-08-14-light-dark-theme-epic.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…i2rvs) Adds quarto-color-mode.js, a de-EJS'd port of Q1's before-body toggle script (rel-swap between stylesheet/disabled-stylesheet, body quarto-light/quarto-dark sync from the active sheet's data-mode, root color-scheme sync, localStorage quarto-color-scheme persistence with Q1's key and default/alternate values, respect-user-color-scheme via prefers-color-scheme with explicit-choice-wins, floating top-right fallback toggle folded in from Q1's after-body script). Injected inline as the first child of <body> so initial variant selection runs before first paint (the FOUC hard constraint); configured via data attributes. Deliberate divergences: no Safari scrollbar hack (color-scheme supersedes it), no giscus (no comments support yet). append_color_mode_class grows its default_dark argument (bd-mtzry): author-default-dark maps bake body.quarto-dark. Navbar gains a dark_mode_toggle flag set by NavbarGenerateTransform from the theme config (Q1's formatDarkMode trigger) and rendered as the quarto-navbar-tools slot; folds into general tools: support later (bd-ld-toggle-into-tools-hpae7m9r). bd-l1rx9yzh resolves as a side effect: both .light-content/.dark-content swap rules were already compiled, and the body-class flip now makes them live. Fixes a latent bug found during browser verification: colorToRGBA() was never ported into _bootstrap-functions.scss, so the toggle icons' SVG data-URI fills contained the literal call text (silently invalid inside string interpolation) and the icon was invisible. Ported from Q1's _quarto-functions.scss with a regression test; golden-hash baseline re-captured for the fill delta. TDD: 4 integration tests red first. Browser-verified end-to-end with chrome-devtools MCP: toggle both directions, persistence across reload (restored before paint), content swapping, icon states, no console errors. 12,145 workspace tests green. Part of the light/dark epic (bd-0pic6); plan: claude-notes/plans/2026-08-14-light-dark-theme-epic.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-web-e2e-bzg4o5lc)
Plan-doc update recording the A5 verification. Target corrected per
Carlos: quarto-web is currently a Quarto 1 project (probe findings
recorded in the plan: Q-5-24 alias conflicts incl. an apparent
upstream copy-paste bug, Pandoc-style attr order in the footer vs
qmd's Q-2-3 rule, plus broad Q1-content gaps); the intended large
testbed is the connect-docs project with the posit-docs extension's
theme: {light, dark} map.
Testbed result: 352/352 files rendered, zero errors, zero Q-14-3.
One shared light + one dark fingerprinted artifact deduped across all
pages via site_libs; link trio, color-scheme meta, inline runtime,
and navbar toggle on every page. Browser-verified: toggle applies the
extension's full dark palette (#181c25), persists, and restores.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ht-style-jnb036fz)
First highlight-style reader in Q2. ThemeConfig parses scalar and
{light, dark} forms into per-variant HighlightStyle entries, resolving
adaptive names at parse time: a11y becomes a11y-light/a11y-dark. Pair
halves resolve by role (the quarto-web shape darkens a cosmo base via
custom SCSS, so builtin darkness would mislead); single-variant configs
resolve via BuiltInTheme::is_dark (theme: darkly + a11y -> a11y-dark).
highlight.scss is split into palette-independent structural rules plus
swappable palette files: highlight-default.scss (the original solarized
palette, extracted verbatim) and hand-translated
highlight-a11y-{light,dark}.scss (from Q1's a11y .theme JSONs onto the
tree-sitter hl-* class vocabulary, with palette-level $code-block-bg /
$code-block-color defaults that user themes can still override).
load_highlight_layer(palette) composes structural + palette; unknown
names fall back to default.
Correctness guards: every default-CSS shortcut (stage fast path,
native/wasm compile_with_doc_vars, native/wasm compile_theme_css) now
requires highlight_style.is_none() so a palette never bypasses
assembly or poisons the shared DEFAULT_CSS_CACHE, and the stage
cache_key gained a palette discriminator. Unknown names warn once per
name via new Q-14-5 (catalog entry + docs page). The LSP semantic-token
legend parity test now reads the default palette file.
Follow-up filed: bd-hl-theme-translator-2mdgh4k6 (general .theme
translator for the full Q1 catalog, copy-button color feedback,
sentinel-based single-variant resolution).
TDD: parse/compile/integration tests red first (incl. the
fast-path-bypass case found by writing the no-theme test). E2E via
real q2 render inspected (light css #d91e18 / dark css #ffa07a).
12,158 workspace tests green.
Part of the light/dark epic (bd-0pic6); plan:
claude-notes/plans/2026-08-14-light-dark-theme-epic.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
extract_brand_refs now extracts BOTH halves of a brand: {light:, dark:}
pair (removing the silent light-only TODO from the original brand
port). DarkThemeConfig carries its own brand_ref, with parse-time
fallback to the light brand when the pair has no dark half (Q1's
per-layer fallback semantics).
A dark brand ENABLES dark mode (Q1's enablesDarkMode): when the brand
pair exists without a theme: dark half, the dark variant is synthesized
from the light theme list and flows through the entire established
machinery — dual compilation, attributed links, color-scheme meta, and
the toggle — automatically. The author default falls back to the brand
map's key order; a theme-declared pair's own key order wins when both
maps exist. A dark-only brand ({dark: b.yml}) synthesizes a
default-dark variant with no brand on the light side.
CompileThemeCssStage resolves each variant's brand into its own
ThemeContext, so the dark compile uses the dark brand and cache keys
discriminate via the brand hash already included in cache_key.
Single-variant brand consumers (revealjs, favicon fallback) explicitly
keep the light half.
Scope split: field-level {light, dark} values inside a unified
_brand.yml (Q1's splitUnifiedBrand; needs untagged-enum type surgery in
quarto-brand) filed as bd-unified-brand-split-ep49amad. This commit
delivers the seam bd-v5z8w asked for.
TDD: 6 parse tests + 1 integration test red first (the integration red
pinpointed exactly the shared-ThemeContext gap). E2E via real q2
render inspected: per-variant --bs-primary-rgb values, dark brand
background driving color-scheme:dark. 12,166 workspace tests green.
Part of the light/dark epic (bd-0pic6); plan:
claude-notes/plans/2026-08-14-light-dark-theme-epic.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deletes the never-wired SassBundle/SassBundleDark/SassBundleLayers scaffolding from quarto-sass (ported early from the TS architecture; the epic delivered dark variants through per-variant ThemeConfigs instead — the module doc records the decision). Writes docs/guides/formats/html/themes.qmd (previously a TBD stub): theme basics, the light/dark map form and its key-order default rule, toggle behavior and persistence, respect-user-color-scheme, the color-scheme/light-dark() styling idiom, .light-content/.dark-content, highlight-style (incl. the adaptive a11y pair), and light/dark brands. Anchor ids match the pre-existing links from the brand guide (#dark-mode, #mode-specific-content); rendered with q2 and inspected. Audit notes recorded on bd-36vmz7nk and bd-qmpygp02: the silent DEFAULT_CSS compile-failure fallback now applies per variant, where a failing dark compile ships light CSS under the dark artifact key. 12,158 workspace tests green. Part of the light/dark epic (bd-0pic6); plan: claude-notes/plans/2026-08-14-light-dark-theme-epic.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements full light/dark dark-mode support for
format: htmland website projects — phases A1–A5, B, C, and E of the light/dark epic (bd-0pic6). Design doc:claude-notes/plans/2026-08-14-light-dark-theme-epic.md.What this delivers
renders with both CSS variants compiled, a navbar (or floating) toggle,
prefers-color-schemesupport, localStorage persistence (Q1-compatible key/values), correctbody.quarto-light/quarto-darkclasses,.light-content/.dark-contentswapping, and variant-matched syntax highlighting — with no FOUC.Commits (one per epic phase, TDD throughout)
ThemeConfigparses both halves of the theme map; YAML key order decides the author-default variant (merge key-order preservation guarded by new quarto-config tests).dark_variant()projection intocss:theme-dark:<fp>artifacts (styles-dark.cssfor single-doc). Thecss:theme-dark:key prefix deliberately doesn't extendcss:theme:, so every existing light-only prefix consumer needed zero changes. Every compiled theme now emits:root{color-scheme:…}from the vendored darkness sentinel — an improvement over Q1 (correct native scrollbars/form controls; also benefits single dark themes liketheme: darkly). Interim Q-14-3 retired (emission, catalog, docs page).Artifactgainslink_attribs/link_order; the theme trio is emitted with Q1's exact contract (quarto-color-scheme/quarto-color-alternate/ trailingquarto-color-scheme-extracopy, sharedid="quarto-bootstrap",data-modefrom each sheet's compiled darkness) in FOUC-safe order, plus<meta name="color-scheme">. Single-variant output stays byte-identical (golden-hash test unchanged).quarto-color-mode.js(de-EJS'd port of Q1's before-body script + floating-fallback), body-class baking incl. default-dark, navbar toggle viaNavbar.dark_mode_toggle. Browser verification found and fixed a latent bug:colorToRGBA()was never ported to_bootstrap-functions.scss, leaving toggle icons with a silently-invalid SVG fill (regression test added).site_libs, toggle browser-verified.{light, dark}map, adaptivea11y);highlight.scsssplit into structural rules + swappable palettes with hand-translateda11y-light/a11y-dark; new Q-14-5 warning for unknown names; all default-CSS shortcut paths guarded so palettes can't be bypassed or poison the shared cache.brand: {light, dark}pairs drive per-variant compiles, and a dark brand alone enables full dark mode (synthesized variant). Unblocks bd-v5z8w.SassBundlescaffolding removed;docs/guides/formats/html/themes.qmdwritten (was a stub).Deliberate divergences from Q1 (documented in the plan/design doc)
color-schemeemission +light-dark()as the documented styling idiom (Q1 lacks both; supersedes Q1's Safari scrollbar hack, which is not ported).Out of scope / follow-ups filed
bd-ld-d-preview-hub-t4oxv0hf, design options recorded in the plan..theme-JSON palette translator:bd-hl-theme-translator-2mdgh4k6._brand.ymlfield-level splitting:bd-unified-brand-split-ep49amad.website.tools::bd-ld-toggle-into-tools-hpae7m9r.Verification
cargo xtask verify(including the hub-client/WASM leg) passed twice; WASM leg rebuilt and checked after each phase.color-schemerule; thecolorToRGBAfill fix).🤖 Generated with Claude Code