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