diff --git a/llm-docs/html-dark-mode-architecture.md b/llm-docs/html-dark-mode-architecture.md index cf52c96b526..03fd96e3e7e 100644 --- a/llm-docs/html-dark-mode-architecture.md +++ b/llm-docs/html-dark-mode-architecture.md @@ -25,7 +25,7 @@ The core model: a page ships **one presentation or two**. When it ships two, the - `false` — dark mode exists; the default is light. - `true` — dark mode exists; the default is dark. -It gates the toggle (`src/project/types/website/website-navigation.ts:356-362`), the before-body script (`src/format/html/format-html.ts:351`, template written at `:530`), and the body class (`format-html-bootstrap.ts:1073-1078`). Its inputs: `darkModeDefaultMetadata()` (`format-html-info.ts:46-70`) reads key order of `theme:` then `brand:`; `darkModeDefault()` (`:71-85`) adds a fallback on `format.render.brand?.dark`. +It gates the toggle (`src/project/types/website/website-navigation.ts:356-362`), the before-body script (`src/format/html/format-html.ts:351`, template written at `:530`), and the body class (`format-html-bootstrap.ts:1073-1078`). Its input is `darkModeDefault()` (`format-html-info.ts:56-66`): it reads `theme:` key order first, then `brand:` key order only when `format.render.brand?.dark` is loaded, and falls back to `false` for a loaded dark brand with no `brand:` key order. `darkModeDefaultMetadata()` (`:49-54`) reads the same key orders from metadata alone, without the loaded-brand check; it exists for `render-contexts.ts`, which calls it before the brand is loaded. ## Configuration surface @@ -53,7 +53,7 @@ Brand SASS layers become one more bundle with a `dark` half (`src/core/sass/bran ### Precedence between `theme.dark` and brand dark -- **Default mode:** `theme:` key order wins over `brand:` key order (`format-html-info.ts:46-70`). A unified `_brand.yml` carries no preference and defaults to light (`:81-82`). +- **Default mode:** `theme:` key order wins over `brand:` key order (`format-html-info.ts:56-66`). A unified `_brand.yml` carries no preference and defaults to light (`:63`). - **Variable values:** SASS `!default` order. Brand normally sits before the theme, so the theme overrides brand. An explicit `brand` marker in the theme list (`theme: {dark: [cyborg, brand]}`) reverses this (`pandoc-html.ts:91-137`, `format-html-scss.ts:216-219`). Pinned by `tests/integration/playwright/tests/html-dark-mode.spec.ts` and `html-dark-mode-defaultlight.spec.ts`. ### `respect-user-color-scheme` @@ -115,6 +115,7 @@ To read the active mode: the `body` class, or the `rel` of `link.quarto-color-sc | Situation | Rendered result | Trap | |---|---|---| | Light-only `_brand.yml` | Full `quarto-color-scheme`/`quarto-color-alternate` link set, all `data-mode="light"`, **no toggle, no before-body script** | An "alternate link exists" probe reports two modes; there is one. Cause: stylesheets key off `project.resolveBrand()` (always split) while the toggle keys off `format.render.brand.dark` (`render-contexts.ts:574-585`). Fixture: `tests/docs/axe-scan/sites/brand-light-only` | +| Dark brand file holding only comments | Warning, file ignored; without a `theme.dark` the page has a single light mode and no toggle, with `theme: {light, dark}` the theme's dark mode stays | `brand:` metadata still lists `dark:`; anything reading its key order must check the loaded brand (`format.render.brand.dark`) | | `theme: {light: cosmo, dark: flatly}` | Alternate link with `data-mode="light"` | A `[data-mode="dark"]` probe reports one mode; there are two | | `theme: darkly` | One link, `data-mode="dark"`, no `quarto-color-scheme` class, `body.quarto-light` | Visually dark page that every declaration-level signal calls light | | `theme: none` / `html+minimal` + brand dark | Untested edge: `brandSassFormatExtras` still contributes a bootstrap bundle (`pandoc-html.ts:284-294`) | Possible alternate stylesheets with no JS to manage them | @@ -127,7 +128,7 @@ To read the active mode: the `body` class, or the `rel` of `link.quarto-color-sc | `kTheme` = `"theme"` | `src/config/constants.ts:635` | | `kBrand` = `"brand"` | `src/config/constants.ts:123` | | `kRespectUserColorScheme` | `src/config/constants.ts:156` | -| `formatDarkMode`, `darkModeDefault`, `darkModeDefaultMetadata` | `src/format/html/format-html-info.ts:33-85` | +| `formatDarkMode`, `darkModeDefault`, `darkModeDefaultMetadata` | `src/format/html/format-html-info.ts:33-78` | | `brandHasDarkMode`, `splitUnifiedBrand` | `src/core/brand/brand.ts:545-590`, `:773-805` | | `cssHasDarkModeSentinel` | `src/core/pandoc/css.ts:45-47` | | `attribForThemeStyle` (link attributes) | `src/command/render/pandoc-html.ts:658-679` | diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index 2466935c636..a5de09dd42d 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -68,6 +68,7 @@ All changes included in 1.11: - ([#15118](https://github.com/quarto-dev/quarto-cli/pull/15118)): Fix Google Scholar metadata missing the first and last page of bibliography entries, and giving wrong pages for a citation `page` written with an en-dash or as a page list. - ([#15125](https://github.com/quarto-dev/quarto-cli/pull/15125)): Fix Google Scholar metadata failing the render with a CSL JSON (`.json`) bibliography. - ([#15128](https://github.com/quarto-dev/quarto-cli/pull/15128)): Fix Google Scholar metadata missing references with a YAML bibliography not wrapped in `---` delimiters. +- ([#15131](https://github.com/quarto-dev/quarto-cli/issues/15131)): Fix the light/dark toggle appearing, with no dark styles to switch to, when the `dark` brand file holds only comments and is ignored. ### `pdf` diff --git a/src/format/html/format-html-info.ts b/src/format/html/format-html-info.ts index 3626390779b..d973e6b170f 100644 --- a/src/format/html/format-html-info.ts +++ b/src/format/html/format-html-info.ts @@ -43,43 +43,36 @@ export function formatDarkMode(format: Format): boolean | undefined { // a dark theme or brand // 2. later, after brand is loaded, we'll know whether unified brand enabled dark mode // in practice, we only know +// Metadata alone cannot tell whether a brand file was ignored (e.g. it only +// holds comments), so darkModeDefault checks `brand:` key order against the +// loaded brand. export function darkModeDefaultMetadata( metadata: Metadata, ): boolean | undefined { - if (metadata[kTheme] && typeof metadata[kTheme] === "object") { - const keys = Object.keys(metadata[kTheme]); - if (keys.includes("dark")) { - if (keys[0] === "dark") { - return true; - } else { - return false; - } - } + return darkModeFromKeyOrder(metadata[kTheme]) ?? + darkModeFromKeyOrder(metadata[kBrand]); +} + +export function darkModeDefault(format: Format): boolean | undefined { + const fromTheme = darkModeFromKeyOrder(format.metadata?.[kTheme]); + if (fromTheme !== undefined) { + return fromTheme; } - if (metadata[kBrand] && typeof metadata[kBrand] === "object") { - const keys = Object.keys(metadata[kBrand]); - if (keys.includes("dark")) { - if (keys[0] === "dark") { - return true; - } else { - return false; - } - } + if (format.render.brand?.dark) { + // a unified brand has no author preference but it can have dark mode + return darkModeFromKeyOrder(format.metadata?.[kBrand]) ?? false; } return undefined; } -export function darkModeDefault(format: Format): boolean | undefined { - const metadata = format.metadata; - if (metadata !== undefined) { - const dmdm = darkModeDefaultMetadata(metadata); - if (dmdm !== undefined) { - return dmdm; + +// true/false when `dark` is a key (true when it is listed first), +// undefined when there is no `dark` key +function darkModeFromKeyOrder(value: unknown): boolean | undefined { + if (value && typeof value === "object") { + const keys = Object.keys(value); + if (keys.includes("dark")) { + return keys[0] === "dark"; } } - const brand = format.render.brand; - if (brand && brand.dark) { - // unified brand has no author preference but it can have dark mode - return false; - } return undefined; } diff --git a/tests/docs/smoke-all/dark-mode/comment-only-dark-brand-with-theme.yml b/tests/docs/smoke-all/dark-mode/comment-only-dark-brand-with-theme.yml new file mode 100644 index 00000000000..9344aa54cac --- /dev/null +++ b/tests/docs/smoke-all/dark-mode/comment-only-dark-brand-with-theme.yml @@ -0,0 +1,4 @@ +# A dark brand file that only holds comments, e.g. a commented-out template. +# color: +# background: "#222222" +# foreground: "#eeeeee" diff --git a/tests/docs/smoke-all/dark-mode/comment-only-dark-brand.yml b/tests/docs/smoke-all/dark-mode/comment-only-dark-brand.yml new file mode 100644 index 00000000000..9344aa54cac --- /dev/null +++ b/tests/docs/smoke-all/dark-mode/comment-only-dark-brand.yml @@ -0,0 +1,4 @@ +# A dark brand file that only holds comments, e.g. a commented-out template. +# color: +# background: "#222222" +# foreground: "#eeeeee" diff --git a/tests/docs/smoke-all/dark-mode/comment-only-dark-only-brand.yml b/tests/docs/smoke-all/dark-mode/comment-only-dark-only-brand.yml new file mode 100644 index 00000000000..9344aa54cac --- /dev/null +++ b/tests/docs/smoke-all/dark-mode/comment-only-dark-only-brand.yml @@ -0,0 +1,4 @@ +# A dark brand file that only holds comments, e.g. a commented-out template. +# color: +# background: "#222222" +# foreground: "#eeeeee" diff --git a/tests/docs/smoke-all/dark-mode/default-dark-brand-order.qmd b/tests/docs/smoke-all/dark-mode/default-dark-brand-order.qmd new file mode 100644 index 00000000000..84ebe4f95c3 --- /dev/null +++ b/tests/docs/smoke-all/dark-mode/default-dark-brand-order.qmd @@ -0,0 +1,15 @@ +--- +title: "Dark brand listed first defaults to dark" +format: html +brand: + dark: slate-brand.yml + light: united-brand.yml +_quarto: + tests: + html: + ensureHtmlElements: + - ['body.quarto-dark', 'link.quarto-color-alternate'] + - [] +--- + +Both brand files are valid and `dark` is listed first, so the page defaults to dark mode. diff --git a/tests/docs/smoke-all/dark-mode/no-dark-mode-comment-only-dark-only-brand.qmd b/tests/docs/smoke-all/dark-mode/no-dark-mode-comment-only-dark-only-brand.qmd new file mode 100644 index 00000000000..4b39725697a --- /dev/null +++ b/tests/docs/smoke-all/dark-mode/no-dark-mode-comment-only-dark-only-brand.qmd @@ -0,0 +1,21 @@ +--- +title: "No dark mode when the only dark brand file is ignored" +format: html +brand: + dark: comment-only-dark-only-brand.yml +_quarto: + tests: + html: + noErrors: true + printsMessage: + level: WARN + regex: 'comment-only-dark-only-brand\.yml has no content and will be ignored' + ensureHtmlElements: + - ['body.quarto-light'] + - ['body.quarto-dark', 'link.quarto-color-alternate'] + ensureFileRegexMatches: + - [] + - ['quarto-color-scheme-toggle'] +--- + +The dark brand file is the only dark source and holds only comments, so it is ignored and the page has a single light mode. diff --git a/tests/docs/smoke-all/dark-mode/no-toggle-comment-only-dark-brand.qmd b/tests/docs/smoke-all/dark-mode/no-toggle-comment-only-dark-brand.qmd new file mode 100644 index 00000000000..4692d3f9c90 --- /dev/null +++ b/tests/docs/smoke-all/dark-mode/no-toggle-comment-only-dark-brand.qmd @@ -0,0 +1,23 @@ +--- +title: "No color-scheme toggle when the dark brand file is ignored" +format: html +brand: + light: united-brand.yml + dark: comment-only-dark-brand.yml +_quarto: + tests: + html: + noErrors: true + printsMessage: + level: WARN + regex: 'comment-only-dark-brand\.yml has no content and will be ignored' + ensureHtmlElements: + - [] + - ["link.quarto-color-alternate"] + ensureFileRegexMatches: + - [] + - + - 'quarto-color-scheme-toggle' +--- + +A comment-only dark brand file is ignored, so the page has a single light mode and must not emit any toggle markup or toggle script. diff --git a/tests/docs/smoke-all/dark-mode/toggle-theme-dark-with-ignored-dark-brand.qmd b/tests/docs/smoke-all/dark-mode/toggle-theme-dark-with-ignored-dark-brand.qmd new file mode 100644 index 00000000000..5643ef82fbe --- /dev/null +++ b/tests/docs/smoke-all/dark-mode/toggle-theme-dark-with-ignored-dark-brand.qmd @@ -0,0 +1,26 @@ +--- +title: "Theme dark mode kept when the dark brand file is ignored" +format: + html: + theme: + light: flatly + dark: darkly +brand: + light: united-brand.yml + dark: comment-only-dark-brand-with-theme.yml +_quarto: + tests: + html: + noErrors: true + printsMessage: + level: WARN + regex: 'comment-only-dark-brand-with-theme\.yml has no content and will be ignored' + ensureHtmlElements: + - ['link.quarto-color-alternate', 'body.quarto-light'] + - [] + ensureFileRegexMatches: + - ['quarto-color-scheme-toggle'] + - [] +--- + +The theme supplies the dark mode, so the toggle stays even though the dark brand file is ignored. diff --git a/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/.gitignore b/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/.gitignore new file mode 100644 index 00000000000..0b3bf772477 --- /dev/null +++ b/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/.gitignore @@ -0,0 +1,5 @@ +/.quarto/ +*.html +*.json +site_libs/ +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/_quarto.yml b/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/_quarto.yml new file mode 100644 index 00000000000..2f896b1572b --- /dev/null +++ b/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/_quarto.yml @@ -0,0 +1,16 @@ +project: + type: website + output-dir: . + +website: + title: "Website" + navbar: + left: + - href: index.qmd + text: Home + +brand: + light: light-brand.yml + dark: dark-brand.yml + +format: html diff --git a/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/dark-brand.yml b/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/dark-brand.yml new file mode 100644 index 00000000000..2eba6aa400c --- /dev/null +++ b/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/dark-brand.yml @@ -0,0 +1,2 @@ +# color: +# background: "#222222" diff --git a/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/index.qmd b/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/index.qmd new file mode 100644 index 00000000000..41878ce48dc --- /dev/null +++ b/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/index.qmd @@ -0,0 +1,15 @@ +--- +title: "Website" +_quarto: + tests: + html: + noErrors: true + printsMessage: + level: WARN + regex: 'dark-brand\.yml has no content and will be ignored' + ensureHtmlElements: + - ['nav.navbar'] + - ['.quarto-color-scheme-toggle', 'link.quarto-color-alternate'] +--- + +The project dark brand file holds only comments and is ignored, so the navbar has no color-scheme toggle. diff --git a/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/light-brand.yml b/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/light-brand.yml new file mode 100644 index 00000000000..6c7e7536edf --- /dev/null +++ b/tests/docs/smoke-all/website/color-scheme-toggle-ignored-dark-brand/light-brand.yml @@ -0,0 +1,2 @@ +color: + primary: red