Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions news/changelog-1.9.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ All changes included in 1.9:
- ([#13883](https://github.com/quarto-dev/quarto-cli/issues/13883)): Fix unequal top/bottom spacing in simple untitled callouts.
- ([#13900](https://github.com/quarto-dev/quarto-cli/issues/13900)): Warn when `renderings` cell option contains duplicate names. Previously, duplicate names like `[dark, light, dark, light]` would silently use only the last output for each name.
- ([#14065](https://github.com/quarto-dev/quarto-cli/issues/14065)): Fix `SCSSParsingError` when custom SCSS themes contain non-ASCII characters in selectors (e.g., `#présentation`).
- ([#14084](https://github.com/quarto-dev/quarto-cli/issues/14084)): Fix dark mode detection for brand colour overrides with perceptually-dark but low-HWB-blackness colours (e.g. blue `#0000FF`). The dark/light sentinel now uses oklch perceptual lightness instead of HWB blackness, and dual-theme builds inject the sentinel directly from the build pipeline.

### `typst`

Expand Down Expand Up @@ -102,6 +103,7 @@ All changes included in 1.9:
### `revealjs`

- ([#13722](https://github.com/quarto-dev/quarto-cli/issues/13722)): Fix `light-content` / `dark-content` SCSS rules not included in Reveal.js format. (author: @mcanouil)
- ([#14084](https://github.com/quarto-dev/quarto-cli/issues/14084)): Fix dark mode detection for brand colour overrides with perceptually-dark but low-HWB-blackness colours (e.g. blue `#0000FF`). The dark/light sentinel now uses oklch perceptual lightness instead of HWB blackness.

### `ipynb`

Expand Down
49 changes: 49 additions & 0 deletions src/command/render/pandoc-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ export async function resolveSassBundles(
bundle.key = bundle.key + "-dark";
return bundle;
});

// Inject dark/light sentinel comments directly, rather than relying
// on the SCSS blackness() heuristic which fails for perceptually-dark
// colours with low HWB blackness (e.g. blue #0000FF).
// See https://github.com/quarto-dev/quarto-cli/issues/14084
darkBundles.push(darkModeSentinelBundle("dark"));
bundles.push(darkModeSentinelBundle("light"));

const darkTarget = {
name: `${dependency}-dark.min.css`,
bundles: darkBundles as any,
Expand Down Expand Up @@ -195,6 +203,10 @@ export async function resolveSassBundles(
}

hasDarkStyles = true;
} else {
// Single-theme: detect dark/light via WCAG relative luminance,
// which correctly handles saturated colours (unlike HWB blackness).
bundles.push(darkModeSentinelBundle("detect"));
}

for (const target of targets) {
Expand Down Expand Up @@ -654,6 +666,43 @@ async function processCssIntoExtras(
const kVariablesRegex =
/\/\*\! quarto-variables-start \*\/([\S\s]*)\/\*\! quarto-variables-end \*\//g;

// Creates a SassBundle that injects the dark/light mode sentinel comment
// into compiled CSS. For dual-theme builds the mode is known at build time;
// for single-theme builds we detect via oklch perceptual lightness.
function darkModeSentinelBundle(
mode: "dark" | "light" | "detect",
): SassBundle {
let uses = "";
let defaults = "";
let sentinelRules: string;

if (mode === "detect") {
uses = '@use "sass:color";';
defaults = "$body-bg: #fff !default;\n";
sentinelRules = [
'@if (color.channel($body-bg, "lightness", $space: oklch) < 50%) {',
" /*! dark */",
"} @else {",
" /*! light */",
"}",
].join("\n");
} else {
sentinelRules = `/*! ${mode} */`;
}

return {
dependency: "quarto-dark-sentinel",
key: `quarto-${mode}-sentinel`,
quarto: {
uses,
defaults,
functions: "",
mixins: "",
rules: sentinelRules,
},
};
}

// Attributes for the style tag
function attribForThemeStyle(
style: "dark" | "light" | "default",
Expand Down
10 changes: 3 additions & 7 deletions src/resources/formats/html/bootstrap/_bootstrap-rules.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1934,13 +1934,9 @@ code a:any-link {
text-decoration-color: $gray-600;
}

// This is a sentinel value that renderers can use to determine
// whether the theme is dark or light
@if (quarto-color.blackness($body-bg) > $code-block-theme-dark-threshhold) {
/*! dark */
} @else {
/*! light */
}
// The dark/light sentinel comment is now injected by the TypeScript
// compilation pipeline (see pandoc-html.ts) which knows at build time
// whether the target stylesheet is dark or light.

// observable UI element tweaks to support light-mode vs dark-mode
div.observablehq table thead tr th {
Expand Down
9 changes: 4 additions & 5 deletions src/resources/formats/revealjs/quarto.scss
Original file line number Diff line number Diff line change
Expand Up @@ -666,11 +666,10 @@ $panel-sidebar-padding: 0.5em;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#{colorToRGB($linkColor)}" class="bi bi-brush" viewBox="0 0 16 16"><path d="M15.825.12a.5.5 0 0 1 .132.584c-1.53 3.43-4.743 8.17-7.095 10.64a6.067 6.067 0 0 1-2.373 1.534c-.018.227-.06.538-.16.868-.201.659-.667 1.479-1.708 1.74a8.118 8.118 0 0 1-3.078.132 3.659 3.659 0 0 1-.562-.135 1.382 1.382 0 0 1-.466-.247.714.714 0 0 1-.204-.288.622.622 0 0 1 .004-.443c.095-.245.316-.38.461-.452.394-.197.625-.453.867-.826.095-.144.184-.297.287-.472l.117-.198c.151-.255.326-.54.546-.848.528-.739 1.201-.925 1.746-.896.126.007.243.025.348.048.062-.172.142-.38.238-.608.261-.619.658-1.419 1.187-2.069 2.176-2.67 6.18-6.206 9.117-8.104a.5.5 0 0 1 .596.04zM4.705 11.912a1.23 1.23 0 0 0-.419-.1c-.246-.013-.573.05-.879.479-.197.275-.355.532-.5.777l-.105.177c-.106.181-.213.362-.32.528a3.39 3.39 0 0 1-.76.861c.69.112 1.736.111 2.657-.12.559-.139.843-.569.993-1.06a3.122 3.122 0 0 0 .126-.75l-.793-.792zm1.44.026c.12-.04.277-.1.458-.183a5.068 5.068 0 0 0 1.535-1.1c1.9-1.996 4.412-5.57 6.052-8.631-2.59 1.927-5.566 4.66-7.302 6.792-.442.543-.795 1.243-1.042 1.826-.121.288-.214.54-.275.72v.001l.575.575zm-4.973 3.04.007-.005a.031.031 0 0 1-.007.004zm3.582-3.043.002.001h-.002z"/></svg>');
}

// This is a sentinel value that renderers can use to determine
// whether the theme is dark or light
@if (
quarto-color.blackness($backgroundColor) > $code-block-theme-dark-threshhold
) {
// Sentinel: detect dark/light via oklch perceptual lightness.
// Unlike HWB blackness(), this correctly handles perceptually-dark colours
// with low blackness (e.g. blue #0000FF). See #14084.
@if (quarto-color.channel($backgroundColor, "lightness", $space: oklch) < 50%) {
/*! dark */
} @else {
/*! light */
Expand Down
26 changes: 26 additions & 0 deletions tests/docs/playwright/html/dark-brand/issue-14084-dual.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: "Issue 14084 - dual theme with blue dark background"
brand:
color:
background:
light: "white"
dark: "blue"
foreground:
light: "black"
dark: "yellow"
format:
html:
theme:
light:
- cosmo
- brand
dark:
- darkly
- brand
---

## Light and dark content

[This text is visible only in light mode]{.light-content}

[This text is visible only in dark mode]{.dark-content}
17 changes: 17 additions & 0 deletions tests/docs/playwright/html/dark-brand/issue-14084-single.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
title: "Issue 14084 - single dark theme with blue background"
brand:
color:
background: "blue"
foreground: "yellow"
format:
html:
theme:
- darkly
- brand
---

## Single dark theme

This page uses a single dark theme (darkly) with a blue background set via brand.
The body should have class `quarto-dark`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: "Issue 14084 - RevealJS dark theme with blue background"
brand:
color:
background: "blue"
foreground: "yellow"
format:
revealjs:
theme: [dark, brand]
---

## Dark theme with blue background

[This is light content]{.light-content}

[This is dark content]{.dark-content}

Body should have class `quarto-dark` because the theme is dark,
even though blue (#0000FF) has 0% HWB blackness.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title: "Issue 14084 - RevealJS dark theme baseline"
format:
revealjs:
theme: dark
---

## Dark theme baseline

[This is light content]{.light-content}

[This is dark content]{.dark-content}

Body should have class `quarto-dark` for the built-in dark theme.
52 changes: 52 additions & 0 deletions tests/integration/playwright/tests/html-dark-mode-blue-bg.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { test, expect } from '@playwright/test';

// Tests for https://github.com/quarto-dev/quarto-cli/issues/14084
// Brand colour overrides with perceptually-dark but low-HWB-blackness
// colours (like blue #0000FF) break the dark mode sentinel, causing
// data-mode="light" on the dark stylesheet and preventing the
// quarto-dark body class from being applied.

test.describe('Issue 14084: blue background dark mode sentinel', () => {

test('Dual theme with explicit light/dark: toggle applies quarto-dark', async ({ page }) => {
await page.goto('./html/dark-brand/issue-14084-dual.html');

// Should start in light mode
const body = page.locator('body').first();
await expect(body).toHaveClass(/quarto-light/);

// Light content should be visible, dark content hidden
await expect(page.locator('span.light-content').first()).toBeVisible();
await expect(page.locator('span.dark-content').first()).toBeHidden();

// The dark stylesheet link should have data-mode="dark"
const darkLink = page.locator('link#quarto-bootstrap.quarto-color-alternate');
await expect(darkLink).toHaveAttribute('data-mode', 'dark');

// Toggle to dark mode
await page.locator('a.quarto-color-scheme-toggle').click();

// Body should now have quarto-dark
await expect(body).toHaveClass(/quarto-dark/);

// Dark content should be visible, light content hidden
await expect(page.locator('span.dark-content').first()).toBeVisible();
await expect(page.locator('span.light-content').first()).toBeHidden();
});

test('Single dark theme with brand: dark stylesheet has correct data-mode', async ({ page }) => {
// theme: [darkly, brand] with brand background: blue
// Brand auto-creates a light+dark pair (no toggle button, but dual stylesheets).
// The key test: the dark stylesheet link must have data-mode="dark"
// despite blue having 0% HWB blackness.
await page.goto('./html/dark-brand/issue-14084-single.html');

// The dark stylesheet link should have data-mode="dark"
const darkLink = page.locator('link#quarto-bootstrap.quarto-color-alternate');
await expect(darkLink).toHaveAttribute('data-mode', 'dark');

// The light stylesheet link should have data-mode="light"
const lightLink = page.locator('link#quarto-bootstrap.quarto-color-scheme:not(.quarto-color-alternate):not(.quarto-color-scheme-extra)');
await expect(lightLink).toHaveAttribute('data-mode', 'light');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { test, expect } from '@playwright/test';

// Tests for https://github.com/quarto-dev/quarto-cli/issues/14084
// RevealJS: brand colour overrides with perceptually-dark but low-HWB-blackness
// colours (like blue #0000FF) should still produce the correct dark sentinel,
// resulting in body.quarto-dark and dark syntax highlighting.

test.describe('Issue 14084: RevealJS blue background dark mode sentinel', () => {

test('Dark theme with blue brand background: body has quarto-dark', async ({ page }) => {
await page.goto('./revealjs/dark-brand/issue-14084-dark-blue.html');

// Body should have quarto-dark class
const body = page.locator('body');
await expect(body).toHaveClass(/quarto-dark/);

// Dark content should be visible, light content hidden
await expect(page.locator('span.dark-content').first()).toBeVisible();
await expect(page.locator('span.light-content').first()).toBeHidden();

// Syntax highlighting should use the dark stylesheet
const highlightLink = page.locator('link#quarto-text-highlighting-styles');
await expect(highlightLink).toHaveAttribute('href', /quarto-syntax-highlighting-dark/);
});

test('Dark theme baseline: body has quarto-dark', async ({ page }) => {
await page.goto('./revealjs/dark-brand/issue-14084-dark-default.html');

// Body should have quarto-dark class
const body = page.locator('body');
await expect(body).toHaveClass(/quarto-dark/);

// Dark content should be visible, light content hidden
await expect(page.locator('span.dark-content').first()).toBeVisible();
await expect(page.locator('span.light-content').first()).toBeHidden();

// Syntax highlighting should use the dark stylesheet
const highlightLink = page.locator('link#quarto-text-highlighting-styles');
await expect(highlightLink).toHaveAttribute('href', /quarto-syntax-highlighting-dark/);
});
});
Loading