diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5964115..b039e55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,9 @@ on: # PRs. To bump the pin, update FIXTURES_SHA below. env: FIXTURES_REPO: QuantEcon/quantecon-book-theme-fixtures - FIXTURES_SHA: d8ffc17c753ecf45fa25c6062827e1aa9de201b3 + # Includes the announcement-banner demo in the fixtures config (merged in + # fixtures #1), so the preview/visual build exercises the banner. + FIXTURES_SHA: 32763e43b893c1520df8e58c0c6e10d81db7f1ac # Explicit least-privilege permissions: # contents: write — checkout + upload-artifact + nwtgck/actions-netlify diff --git a/.github/workflows/update-snapshots.yml b/.github/workflows/update-snapshots.yml index 3252bfe..aedc713 100644 --- a/.github/workflows/update-snapshots.yml +++ b/.github/workflows/update-snapshots.yml @@ -11,7 +11,7 @@ on: # "Resolve fixtures pin" step. env: FIXTURES_REPO: QuantEcon/quantecon-book-theme-fixtures - FIXTURES_SHA: d8ffc17c753ecf45fa25c6062827e1aa9de201b3 + FIXTURES_SHA: 32763e43b893c1520df8e58c0c6e10d81db7f1ac jobs: # /update-new-snapshots — only creates MISSING snapshots (safe for adding new tests) diff --git a/CHANGELOG.md b/CHANGELOG.md index de193c0..22bfd55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **Dismissible announcement banner** — a new `announcement` theme option renders a notice at the top of every page (HTML allowed, so you can include emphasis and a link to changelog notes). Readers dismiss it with a `×`; the dismissal persists in `localStorage` and is keyed to a hash of the message, so editing the text re-shows the banner for everyone who dismissed the previous one. An optional `announcement_expires` ISO date (`YYYY-MM-DD`) auto-hides the banner after that day — enforced client-side (so it disappears for visitors even without a rebuild) and at build time (an already-expired notice is omitted from the HTML). An invalid expiry date logs a warning and fails open. Two looks are available via `announcement_style`: `bar` (default) — a thin full-width strip that scrolls away below the toolbar — or `callout` — a boxed in-column notice; both adapt to dark mode and RTL. The banner defaults to empty/off, so existing sites are unaffected. The renderer iterates a list of notices internally so per-page announcements can be added additively later (tracked in #403). + ### Documentation - **Developer setup troubleshooting for stale `.nodeenv`** — documented the `nodeenv-version-mismatch` error (an in-repo `.nodeenv/` left over from an older pinned Node.js version) and its fix (`rm -rf .nodeenv` then rebuild), which otherwise blocks `tox` and editable installs locally. Also clarified that `tox` keeps the toolchain fully repo-local (`.tox/`, `.nodeenv/`, `node_modules/` are all git-ignored and regenerated), so nothing is installed into the base/global environment. diff --git a/docs/user/announcements.md b/docs/user/announcements.md new file mode 100644 index 0000000..1133200 --- /dev/null +++ b/docs/user/announcements.md @@ -0,0 +1,99 @@ +# Announcement Banner + +Display a dismissible announcement at the top of every page — useful for +site-wide notices such as a software upgrade, a link to changelog notes, or a +scheduled maintenance window. + +```{contents} +:local: +:depth: 1 +``` + +## Basic usage + +Set the `announcement` option in `html_theme_options`. The value is an HTML +string, so you can include emphasis and a link: + +```yaml +sphinx: + config: + html_theme_options: + announcement: 'We upgraded to Anaconda 2026.06 — see the CHANGELOG.' +``` + +For a `conf.py` project: + +```python +html_theme_options = { + "announcement": 'We upgraded to Anaconda 2026.06 — see the CHANGELOG.', +} +``` + +Leave the option empty (the default) and no banner is shown. + +## Style + +Choose how the banner looks with `announcement_style`: + +```yaml +sphinx: + config: + html_theme_options: + announcement: 'We upgraded to Anaconda 2026.06.' + announcement_style: bar # "bar" (default) or "callout" +``` + +- **`bar`** (default) — a thin, full-width strip with centered text, sitting just + below the toolbar and scrolling away with the page. Discreet; good for standing + notices. +- **`callout`** — a boxed notice in the content column with an accent border. + More prominent; good for a notice you want to stand out. + +Both styles adapt to dark mode and right-to-left layouts, and both are +dismissible. An unrecognized value logs a warning and falls back to `bar`. + +## Dismissal + +Readers can dismiss the banner with the `×` button. The dismissal is remembered +in the browser's `localStorage`, so it stays hidden on future visits. + +Dismissal is keyed to the **content of the message**. When you change the +`announcement` text, the banner re-appears for everyone — including readers who +dismissed the previous message. This means you can reuse the banner for a new +notice without worrying that people who dismissed the last one will miss it. + +## Expiry date + +Add an optional `announcement_expires` date (ISO `YYYY-MM-DD`) to have the +banner disappear automatically. The banner shows **through the end of** that +day, in the reader's local timezone: + +```yaml +sphinx: + config: + html_theme_options: + announcement: 'We upgraded to Anaconda 2026.06 — see the CHANGELOG.' + announcement_expires: "2026-07-01" +``` + +The expiry is enforced two ways, so it works whether or not the site is rebuilt: + +- **In the reader's browser** — the banner hides itself at the end of the expiry + day in the reader's local timezone, even if the published site has not been + rebuilt since. +- **At build time** — once the expiry day is well past (a one-day grace ensures + it has ended in every timezone), the banner is omitted from the generated HTML + entirely. Around the expiry date the markup is still emitted and the per-reader + browser check above governs exactly when it disappears. + +If `announcement_expires` is not a valid `YYYY-MM-DD` date, the build logs a +warning and ignores the expiry (the banner keeps showing) — a typo will never +silently hide an active announcement. + +## Per-page announcements + +Per-page announcements (for example, flagging that a single lecture now uses a +newer library version) are not yet supported. Progress is tracked in +[issue #403](https://github.com/QuantEcon/quantecon-book-theme/issues/403); the +banner is built to accept per-page notices additively, so this can be added +without changing how the site-wide `announcement` option works. diff --git a/docs/user/configuration.md b/docs/user/configuration.md index 0c61c25..1cb7e41 100644 --- a/docs/user/configuration.md +++ b/docs/user/configuration.md @@ -102,6 +102,23 @@ html_theme_options = { This disables the left navigation bar. +## Announcement Banner + +Show a dismissible notice at the top of every page (for example, a software +upgrade with a link to changelog notes): + +```python +html_theme_options = { + ... + "announcement": 'We upgraded to Anaconda 2026.06 — see the CHANGELOG.', + "announcement_expires": "2026-07-01", # optional ISO date; banner auto-hides after this day + ... +} +``` + +See [Announcement Banner](announcements.md) for dismissal behavior and expiry +details. + ## Add Authors Display a list of authors just below the page title: diff --git a/docs/user/index.md b/docs/user/index.md index 436d2e1..1372e60 100644 --- a/docs/user/index.md +++ b/docs/user/index.md @@ -51,6 +51,7 @@ html_theme_options = { :caption: User Guide configuration +announcements layout notebooks launch diff --git a/src/quantecon_book_theme/__init__.py b/src/quantecon_book_theme/__init__.py index 2378854..8910523 100644 --- a/src/quantecon_book_theme/__init__.py +++ b/src/quantecon_book_theme/__init__.py @@ -5,7 +5,7 @@ import hashlib from functools import lru_cache import subprocess -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from docutils import nodes from sphinx.util import logging @@ -223,6 +223,58 @@ def get_relative_time(past_date): return f"{years} year{'s' if years != 1 else ''} ago" +def _parse_iso_date(value): + """Parse a ``YYYY-MM-DD`` string into a ``date``, or return ``None``.""" + if not value: + return None + try: + return datetime.strptime(value.strip(), "%Y-%m-%d").date() + except (ValueError, TypeError): + return None + + +def _build_announcements(config_theme): + """Build the list of announcements to render in the page banner. + + Currently this is a single site-wide announcement (from the ``announcement`` + option), but it returns a *list* so per-page announcements can be appended + additively in future without changing the template or JavaScript (tracked in + GitHub issue #403). Each entry is a dict with: + + - ``html``: the (trusted) HTML message + - ``id``: a short content hash, used to key dismissal in localStorage so an + edited message re-appears for everyone who dismissed the old one + - ``expires_iso``: the ISO expiry date (or ``""``), checked client-side so the + banner disappears for visitors on the date even without a rebuild + + An announcement whose expiry has already passed at build time is omitted + entirely; the client-side check handles expiry that falls between builds. + """ + announcements = [] + message = (config_theme.get("announcement") or "").strip() + if message: + expires = (config_theme.get("announcement_expires") or "").strip() + expires_date = _parse_iso_date(expires) + # Build-time skip is only an optimization: drop a clearly-stale notice so + # it isn't shipped in the HTML at all. Keep it conservative — the expiry + # day ends at different UTC instants across timezones, and the client + # side hides the banner precisely per-reader, so we only skip once the + # date is past for every real-world timezone (a one-day UTC grace + # covers the full UTC-12..UTC+14 range). + today_utc = datetime.now(timezone.utc).date() + if expires_date is not None and today_utc > expires_date + timedelta(days=1): + return announcements + announcement_id = hashlib.sha1(message.encode("utf-8")).hexdigest()[:12] + announcements.append( + { + "html": message, + "id": announcement_id, + "expires_iso": expires if expires_date is not None else "", + } + ) + return announcements + + def _process_languages(config_theme): """Validate and normalize language switcher configuration. @@ -492,6 +544,10 @@ def get_github_src_folder(app): config_theme ) + # Build the announcement banner list (currently site-wide only; the list + # shape leaves room for additive per-page announcements later). + context["announcements"] = _build_announcements(config_theme) + # Make sure the context values are bool blns = [ "theme_use_edit_page_button", @@ -627,6 +683,44 @@ def _string_or_bool(var): return var is None +# Announcement banner styles. "bar" is a thin full-width strip; "callout" is +# the original boxed in-column notice. +_VALID_ANNOUNCEMENT_STYLES = ["bar", "callout"] +_DEFAULT_ANNOUNCEMENT_STYLE = "bar" + + +def validate_announcement(app): + """Validate the announcement options once, at build start. + + Fails open: an unparseable ``announcement_expires`` is dropped (cleared) so a + typo can never silently hide an active announcement, and an unknown + ``announcement_style`` falls back to the default. Warnings are logged so the + misconfiguration surfaces during the build. + """ + theme_options = app.config.html_theme_options + expires = theme_options.get("announcement_expires", "") + if expires and _parse_iso_date(expires) is None: + SPHINX_LOGGER.warning( + "Invalid announcement_expires %r. Expected ISO date YYYY-MM-DD. " + "Ignoring expiry; the announcement will not auto-expire.", + expires, + ) + theme_options["announcement_expires"] = "" + + style = str(theme_options.get("announcement_style", "") or "").strip().lower() + if not style: + style = _DEFAULT_ANNOUNCEMENT_STYLE + elif style not in _VALID_ANNOUNCEMENT_STYLES: + SPHINX_LOGGER.warning( + "Unknown announcement_style %r. Valid styles: %s. Falling back to %r.", + style, + ", ".join(_VALID_ANNOUNCEMENT_STYLES), + _DEFAULT_ANNOUNCEMENT_STYLE, + ) + style = _DEFAULT_ANNOUNCEMENT_STYLE + theme_options["announcement_style"] = style + + # Built-in text color schemes _VALID_COLOR_SCHEMES = ["seoul256", "gruvbox", "none"] @@ -676,6 +770,7 @@ def setup(app): app.connect("html-page-context", add_hub_urls) app.connect("builder-inited", add_plugins_list) + app.connect("builder-inited", validate_announcement) app.connect("builder-inited", validate_color_scheme) app.connect("builder-inited", setup_pygments_css) app.connect("html-page-context", hash_html_assets) diff --git a/src/quantecon_book_theme/assets/scripts/announcement.js b/src/quantecon_book_theme/assets/scripts/announcement.js new file mode 100644 index 0000000..ec6101d --- /dev/null +++ b/src/quantecon_book_theme/assets/scripts/announcement.js @@ -0,0 +1,93 @@ +/** + * Announcement Banner Module + * + * Renders a dismissible announcement bar at the top of the page. The bar is + * server-rendered hidden and revealed here only for rows that survive two + * checks, so the reader never sees a flash of a notice they already dismissed + * or one that has expired: + * + * - Dismissed: each row carries a content hash (`data-announcement-id`). The + * set of dismissed ids is stored in localStorage, so an edited message — + * which produces a new hash — re-appears even for readers who dismissed the + * old one. Dismissal persists across visits until the message changes. + * - Expired: an optional `data-announcement-expires` (YYYY-MM-DD) hides the + * row once the visitor's clock is past the end of that day, so a notice + * disappears on the date even if the site has not been rebuilt. + * + * The bar may hold more than one row; the logic is intentionally n-aware so + * per-page announcements can be added additively later without changes here. + */ + +const STORAGE_KEY = "qe-dismissed-announcements"; +const MAX_REMEMBERED = 20; + +function readDismissed() { + try { + const raw = window.localStorage.getItem(STORAGE_KEY); + const parsed = raw ? JSON.parse(raw) : []; + return Array.isArray(parsed) ? parsed : []; + } catch (e) { + // localStorage unavailable (e.g. private mode) or corrupt value. + return []; + } +} + +function rememberDismissed(id) { + try { + const dismissed = readDismissed().filter((value) => value !== id); + dismissed.push(id); + // Cap the list so it can't grow unbounded over the site's lifetime. + const trimmed = dismissed.slice(-MAX_REMEMBERED); + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed)); + } catch (e) { + // Persisting is best-effort; dismissal still works for this page view. + } +} + +function isExpired(expires) { + if (!expires) return false; + // Parse YYYY-MM-DD into explicit local-time components (unambiguous across + // engines, and not subject to the "date-only string is UTC" parsing rule). + // Expire at the very end of that calendar day in the visitor's timezone. + const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(expires.trim()); + if (!m) return false; // fail open on bad/unsupported format + const endOfDay = new Date(+m[1], +m[2] - 1, +m[3], 23, 59, 59, 999); + if (Number.isNaN(endOfDay.getTime())) return false; + return new Date() > endOfDay; +} + +export function initAnnouncement() { + const bar = document.querySelector(".qe-announcement-bar"); + if (!bar) return; + + const dismissed = readDismissed(); + + bar.querySelectorAll(".qe-announcement").forEach((row) => { + const id = row.getAttribute("data-announcement-id"); + const expires = row.getAttribute("data-announcement-expires"); + + // Drop rows the reader has dismissed or that have expired. + if (dismissed.includes(id) || isExpired(expires)) { + row.remove(); + return; + } + + const closeButton = row.querySelector(".qe-announcement__close"); + if (closeButton) { + closeButton.addEventListener("click", function () { + rememberDismissed(id); + row.remove(); + if (!bar.querySelector(".qe-announcement")) { + bar.setAttribute("hidden", ""); + } + }); + } + }); + + // Reveal the bar only if at least one row survived. + if (bar.querySelector(".qe-announcement")) { + bar.removeAttribute("hidden"); + } else { + bar.remove(); + } +} diff --git a/src/quantecon_book_theme/assets/scripts/index.js b/src/quantecon_book_theme/assets/scripts/index.js index 064f6c4..7126968 100644 --- a/src/quantecon_book_theme/assets/scripts/index.js +++ b/src/quantecon_book_theme/assets/scripts/index.js @@ -19,6 +19,7 @@ import { initPageHeader, initChangelog } from "./page-header.js"; import { initStderrWarnings } from "./stderr-warnings.js"; import { initScrollSpy } from "./scrollspy.js"; import { initLanguageSwitcher } from "./language-switcher.js"; +import { initAnnouncement } from "./announcement.js"; document.addEventListener("DOMContentLoaded", function () { // Load feather icon set @@ -54,4 +55,7 @@ document.addEventListener("DOMContentLoaded", function () { // Initialize language switcher initLanguageSwitcher(); + + // Initialize the dismissible announcement banner + initAnnouncement(); }); diff --git a/src/quantecon_book_theme/assets/styles/_announcement.scss b/src/quantecon_book_theme/assets/styles/_announcement.scss new file mode 100644 index 0000000..b5e2e90 --- /dev/null +++ b/src/quantecon_book_theme/assets/styles/_announcement.scss @@ -0,0 +1,210 @@ +/* +----------------------------------- +ANNOUNCEMENT BANNER +Dismissible notice rendered near the top of the page. Two selectable styles +(set via the `announcement_style` theme option): + + --bar thin full-width strip, discrete, scrolls away (default) + --callout boxed in-column notice (the original style) + +Both share the same markup, JS, and global dismissal; only the look differs. +The bar holds one or more stacked rows (n-aware for future per-page notices). +----------------------------------- +*/ + +@use "colors"; + +/* Shared structure ----------------------------------------------------- */ +.qe-announcement-bar { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin: 0 0 1.5rem 0; + + // Honour the server-rendered `hidden` attribute even though the bar is a + // flex container (announcement.js removes it once rows are evaluated). + &[hidden] { + display: none; + } +} + +.qe-announcement { + display: flex; + align-items: center; + font-family: "Source Sans Pro", sans-serif; + line-height: 1.4; + + &__text { + // Drop the bottom margin from a trailing paragraph if the message is rich. + > :last-child { + margin-bottom: 0; + } + } + + &__close { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + background: transparent; + border: none; + cursor: pointer; + line-height: 1; + transition: color 0.15s ease; + } +} + +/* Style: callout — boxed in-column notice (original style) -------------- */ +.qe-announcement-bar--callout .qe-announcement { + align-items: flex-start; + gap: 0.75rem; + padding: 0.6rem 0.9rem; + background-color: #eaf4fb; + border: 1px solid #cfe6f7; + border-left: 4px solid colors.$primary; + border-radius: 4px; + font-size: 0.95rem; + color: #1a3a4d; + + .qe-announcement__text { + flex: 1; + + a { + color: colors.$primary; + text-decoration: underline; + } + } + + .qe-announcement__close { + color: #5a7184; + + &:hover { + color: #1a3a4d; + } + + &:focus-visible { + outline: 2px solid colors.$primary; + outline-offset: 2px; + } + + svg { + width: 16px; + height: 16px; + } + } +} + +body.dark-theme .qe-announcement-bar--callout .qe-announcement { + background-color: #1e2a38; + border-color: #2f4356; + border-left-color: #5fa8e0; + color: #d4e6f4; + + .qe-announcement__text a { + color: #6cb6ff; + } + + .qe-announcement__close { + color: #9bb4c8; + + &:hover { + color: #ffffff; + } + } +} + +// RTL: move the accent border to the right edge. +[dir="rtl"] .qe-announcement-bar--callout .qe-announcement { + border-left: 1px solid #cfe6f7; + border-right: 4px solid colors.$primary; +} + +body.dark-theme[dir="rtl"] .qe-announcement-bar--callout .qe-announcement, +[dir="rtl"] body.dark-theme .qe-announcement-bar--callout .qe-announcement { + border-left-color: #2f4356; + border-right-color: #5fa8e0; +} + +/* Style: bar — thin full-width strip (default) ------------------------- */ +.qe-announcement-bar--bar { + // Break out of the centered content column to span the viewport width. + // .qe-page is centered in the window when the sidebar drawer is closed (its + // default), so this aligns the strip to the window edges. + width: 100vw; + margin-left: calc(50% - 50vw); + margin-right: calc(50% - 50vw); + // Sit flush beneath the fixed toolbar (50px tall + its 1px bottom border) by + // absorbing the gap that .qe-page's 4rem top padding would otherwise leave, + // so the strip reads as a second tier of the header. It scrolls away with + // the page. + margin-top: calc(51px - 4rem); + margin-bottom: 1.5rem; + gap: 0; + + .qe-announcement { + position: relative; + justify-content: center; + padding: 0.25rem 2.75rem; // thin strip; horizontal room clears the close + background-color: #f3f4f6; + border-bottom: 1px solid #e3e5ea; + font-size: 0.82rem; + color: #3a3a3a; + } + + .qe-announcement__text { + flex: 0 1 auto; + text-align: center; + + a { + color: colors.$primary; + text-decoration: underline; + } + } + + .qe-announcement__close { + position: absolute; + right: 1rem; + top: 50%; + transform: translateY(-50%); + color: #7a7a7a; + + &:hover { + color: #1a1a1a; + } + + &:focus-visible { + outline: 2px solid colors.$primary; + outline-offset: 2px; + } + + svg { + width: 14px; + height: 14px; + } + } +} + +body.dark-theme .qe-announcement-bar--bar .qe-announcement { + background-color: #1b2130; + border-bottom-color: #313a52; + color: #c8cbd6; + + .qe-announcement__text a { + color: #6cb6ff; + } +} + +body.dark-theme .qe-announcement-bar--bar .qe-announcement__close { + color: #8a92a6; + + &:hover { + color: #ffffff; + } +} + +// RTL: move the close button to the left edge. +[dir="rtl"] .qe-announcement-bar--bar .qe-announcement__close { + right: auto; + left: 1rem; +} diff --git a/src/quantecon_book_theme/assets/styles/index.scss b/src/quantecon_book_theme/assets/styles/index.scss index 370a93b..8043992 100644 --- a/src/quantecon_book_theme/assets/styles/index.scss +++ b/src/quantecon_book_theme/assets/styles/index.scss @@ -34,6 +34,7 @@ All @forward rules must be at the top before any other rules. @forward "sidebar"; @forward "page"; @forward "content"; +@forward "announcement"; @forward "admonitions"; @forward "footnotes"; @forward "stderr"; diff --git a/src/quantecon_book_theme/theme/quantecon_book_theme/layout.html b/src/quantecon_book_theme/theme/quantecon_book_theme/layout.html index e033647..6147e7a 100644 --- a/src/quantecon_book_theme/theme/quantecon_book_theme/layout.html +++ b/src/quantecon_book_theme/theme/quantecon_book_theme/layout.html @@ -183,6 +183,23 @@
+ {# Announcement banner — hidden until announcement.js decides which + rows survive the dismissed/expired check (avoids a flash). #} + {%- if announcements %} + + {%- endif %} +
{# Determine sticky TOC and autoexpand settings once #} diff --git a/src/quantecon_book_theme/theme/quantecon_book_theme/theme.conf b/src/quantecon_book_theme/theme/quantecon_book_theme/theme.conf index 377228e..cf1f6bf 100644 --- a/src/quantecon_book_theme/theme/quantecon_book_theme/theme.conf +++ b/src/quantecon_book_theme/theme/quantecon_book_theme/theme.conf @@ -3,6 +3,9 @@ inherit = pydata_sphinx_theme stylesheet = styles/quantecon-book-theme.css [options] +announcement = +announcement_expires = +announcement_style = bar authors = binderhub_url = qetheme_code_style = True diff --git a/tests/test_announcement.py b/tests/test_announcement.py new file mode 100644 index 0000000..7b22c07 --- /dev/null +++ b/tests/test_announcement.py @@ -0,0 +1,235 @@ +""" +Tests for the dismissible announcement banner. + +Verifies that: +- The `announcement` / `announcement_expires` options are registered in theme.conf +- The SCSS partial, JS module, and layout markup are wired in +- `_parse_iso_date` parses ISO dates and rejects bad input +- `_build_announcements` builds the render list, hashes content for dismissal, + and skips announcements that have already expired at build time +- `validate_announcement` fails open on an invalid expiry date +""" + +from datetime import datetime, timedelta, timezone +from pathlib import Path +from unittest.mock import MagicMock + +from quantecon_book_theme import ( + _build_announcements, + _parse_iso_date, + validate_announcement, +) + +# Paths +THEME_DIR = Path("src/quantecon_book_theme/theme/quantecon_book_theme") +ASSETS_DIR = Path("src/quantecon_book_theme/assets") + + +def _future_date(days=365): + return (datetime.now(timezone.utc).date() + timedelta(days=days)).isoformat() + + +def _past_date(days=1): + return (datetime.now(timezone.utc).date() - timedelta(days=days)).isoformat() + + +class TestAnnouncementThemeOption: + """The options must be registered in theme.conf so Sphinx accepts them.""" + + def test_announcement_option_exists(self): + content = (THEME_DIR / "theme.conf").read_text() + assert "announcement =" in content + + def test_announcement_expires_option_exists(self): + content = (THEME_DIR / "theme.conf").read_text() + assert "announcement_expires =" in content + + def test_announcement_style_option_defaults_to_bar(self): + content = (THEME_DIR / "theme.conf").read_text() + lines = content.splitlines() + matching = [ + line for line in lines if line.strip().startswith("announcement_style =") + ] + assert len(matching) == 1 + assert matching[0].strip() == "announcement_style = bar" + + +class TestAnnouncementAssets: + """The SCSS, JS, and template pieces must be present and wired together.""" + + def test_scss_partial_exists(self): + content = (ASSETS_DIR / "styles" / "_announcement.scss").read_text() + assert ".qe-announcement" in content + + def test_scss_partial_is_forwarded(self): + content = (ASSETS_DIR / "styles" / "index.scss").read_text() + assert '@forward "announcement"' in content + + def test_scss_has_dark_mode_styles(self): + content = (ASSETS_DIR / "styles" / "_announcement.scss").read_text() + assert "body.dark-theme .qe-announcement" in content + + def test_scss_has_rtl_styles(self): + content = (ASSETS_DIR / "styles" / "_announcement.scss").read_text() + assert '[dir="rtl"] .qe-announcement' in content + + def test_js_module_exists(self): + content = (ASSETS_DIR / "scripts" / "announcement.js").read_text() + assert "qe-dismissed-announcements" in content + assert "export function initAnnouncement" in content + + def test_js_module_is_imported(self): + content = (ASSETS_DIR / "scripts" / "index.js").read_text() + assert 'from "./announcement.js"' in content + assert "initAnnouncement()" in content + + def test_layout_renders_announcement_loop(self): + content = (THEME_DIR / "layout.html").read_text() + assert "qe-announcement-bar" in content + assert "data-announcement-id" in content + assert "data-announcement-expires" in content + + def test_layout_renders_style_modifier(self): + content = (THEME_DIR / "layout.html").read_text() + assert "qe-announcement-bar--" in content + assert "theme_announcement_style" in content + + def test_scss_defines_both_styles(self): + content = (ASSETS_DIR / "styles" / "_announcement.scss").read_text() + assert ".qe-announcement-bar--bar" in content + assert ".qe-announcement-bar--callout" in content + + +class TestParseIsoDate: + """_parse_iso_date should parse YYYY-MM-DD and reject everything else.""" + + def test_valid_date(self): + assert _parse_iso_date("2026-07-01").isoformat() == "2026-07-01" + + def test_whitespace_is_stripped(self): + assert _parse_iso_date(" 2026-07-01 ").isoformat() == "2026-07-01" + + def test_empty_returns_none(self): + assert _parse_iso_date("") is None + assert _parse_iso_date(None) is None + + def test_invalid_format_returns_none(self): + assert _parse_iso_date("July 1 2026") is None + assert _parse_iso_date("2026/07/01") is None + assert _parse_iso_date("not-a-date") is None + + +class TestBuildAnnouncements: + """_build_announcements turns config into the list the template renders.""" + + def test_no_message_is_empty(self): + assert _build_announcements({}) == [] + assert _build_announcements({"announcement": ""}) == [] + assert _build_announcements({"announcement": " "}) == [] + + def test_message_produces_one_entry(self): + result = _build_announcements({"announcement": "Hello world"}) + assert len(result) == 1 + assert result[0]["html"] == "Hello world" + assert result[0]["expires_iso"] == "" + assert result[0]["id"] # a non-empty content hash + + def test_id_is_stable_for_same_message(self): + a = _build_announcements({"announcement": "Same message"})[0]["id"] + b = _build_announcements({"announcement": "Same message"})[0]["id"] + assert a == b + + def test_id_changes_when_message_changes(self): + a = _build_announcements({"announcement": "Message A"})[0]["id"] + b = _build_announcements({"announcement": "Message B"})[0]["id"] + assert a != b + + def test_future_expiry_is_kept(self): + future = _future_date() + result = _build_announcements( + {"announcement": "Upgrade notice", "announcement_expires": future} + ) + assert len(result) == 1 + assert result[0]["expires_iso"] == future + + def test_clearly_past_expiry_is_skipped_at_build_time(self): + # Past the one-day UTC grace (covers all timezones) -> omitted entirely. + result = _build_announcements( + {"announcement": "Stale notice", "announcement_expires": _past_date(3)} + ) + assert result == [] + + def test_yesterday_expiry_is_kept_at_build_time(self): + # Within the one-day grace: it may still be the expiry day for some + # reader's timezone, so the notice is kept in the HTML and the client + # side hides it precisely per-reader. + yesterday = _past_date(1) + result = _build_announcements( + {"announcement": "Ended yesterday (UTC)", "announcement_expires": yesterday} + ) + assert len(result) == 1 + assert result[0]["expires_iso"] == yesterday + + def test_today_expiry_is_kept(self): + today = datetime.now(timezone.utc).date().isoformat() + result = _build_announcements( + {"announcement": "Ends today", "announcement_expires": today} + ) + assert len(result) == 1 + + def test_invalid_expiry_is_ignored_not_expired(self): + # _build_announcements treats an unparseable date as no expiry (fail open). + result = _build_announcements( + {"announcement": "Keep showing", "announcement_expires": "bad-date"} + ) + assert len(result) == 1 + assert result[0]["expires_iso"] == "" + + +class TestValidateAnnouncement: + """validate_announcement fails open: a bad date is cleared, not the message.""" + + def _make_app(self, theme_options): + app = MagicMock() + app.config.html_theme_options = theme_options + return app + + def test_invalid_date_is_cleared(self): + app = self._make_app( + {"announcement": "Notice", "announcement_expires": "July 1"} + ) + validate_announcement(app) + assert app.config.html_theme_options["announcement_expires"] == "" + # The message itself is untouched. + assert app.config.html_theme_options["announcement"] == "Notice" + + def test_valid_date_is_preserved(self): + app = self._make_app({"announcement_expires": "2026-07-01"}) + validate_announcement(app) + assert app.config.html_theme_options["announcement_expires"] == "2026-07-01" + + def test_empty_date_is_left_alone(self): + app = self._make_app({"announcement_expires": ""}) + validate_announcement(app) + assert app.config.html_theme_options["announcement_expires"] == "" + + def test_valid_style_is_preserved(self): + for style in ("bar", "callout"): + app = self._make_app({"announcement_style": style}) + validate_announcement(app) + assert app.config.html_theme_options["announcement_style"] == style + + def test_style_is_case_insensitive(self): + app = self._make_app({"announcement_style": "Callout"}) + validate_announcement(app) + assert app.config.html_theme_options["announcement_style"] == "callout" + + def test_unknown_style_falls_back_to_bar(self): + app = self._make_app({"announcement_style": "fancy"}) + validate_announcement(app) + assert app.config.html_theme_options["announcement_style"] == "bar" + + def test_missing_style_defaults_to_bar(self): + app = self._make_app({"announcement": "Notice"}) + validate_announcement(app) + assert app.config.html_theme_options["announcement_style"] == "bar" diff --git a/tests/visual/__snapshots__/desktop-chrome/admonitions-header.png b/tests/visual/__snapshots__/desktop-chrome/admonitions-header.png index 886afb3..363c9c3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/admonitions-header.png and b/tests/visual/__snapshots__/desktop-chrome/admonitions-header.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/admonitions-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/admonitions-sidebar.png index a7f5afa..e652a70 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/admonitions-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/admonitions-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/admonitions.png b/tests/visual/__snapshots__/desktop-chrome/admonitions.png index 57a6011..6a65211 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/admonitions.png and b/tests/visual/__snapshots__/desktop-chrome/admonitions.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/bold-text.png b/tests/visual/__snapshots__/desktop-chrome/bold-text.png index eb1e87c..ecc68c7 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/bold-text.png and b/tests/visual/__snapshots__/desktop-chrome/bold-text.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/code-block.png b/tests/visual/__snapshots__/desktop-chrome/code-block.png index 06dfa33..07fc7b3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/code-block.png and b/tests/visual/__snapshots__/desktop-chrome/code-block.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/code-blocks-header.png b/tests/visual/__snapshots__/desktop-chrome/code-blocks-header.png index 886afb3..363c9c3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/code-blocks-header.png and b/tests/visual/__snapshots__/desktop-chrome/code-blocks-header.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/code-blocks-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/code-blocks-sidebar.png index 8ff2018..4dd0087 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/code-blocks-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/code-blocks-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/code-blocks.png b/tests/visual/__snapshots__/desktop-chrome/code-blocks.png index 7eb1f5b..b7c4907 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/code-blocks.png and b/tests/visual/__snapshots__/desktop-chrome/code-blocks.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/cross-references-header.png b/tests/visual/__snapshots__/desktop-chrome/cross-references-header.png index 886afb3..363c9c3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/cross-references-header.png and b/tests/visual/__snapshots__/desktop-chrome/cross-references-header.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/cross-references-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/cross-references-sidebar.png index ab63e9b..1006a77 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/cross-references-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/cross-references-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/cross-references.png b/tests/visual/__snapshots__/desktop-chrome/cross-references.png index 94841e5..623476c 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/cross-references.png and b/tests/visual/__snapshots__/desktop-chrome/cross-references.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/dark-mode.png b/tests/visual/__snapshots__/desktop-chrome/dark-mode.png index 00afa27..3205f3f 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/dark-mode.png and b/tests/visual/__snapshots__/desktop-chrome/dark-mode.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/definition-list.png b/tests/visual/__snapshots__/desktop-chrome/definition-list.png index dc44e04..a05617c 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/definition-list.png and b/tests/visual/__snapshots__/desktop-chrome/definition-list.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/definition-lists-header.png b/tests/visual/__snapshots__/desktop-chrome/definition-lists-header.png index 886afb3..363c9c3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/definition-lists-header.png and b/tests/visual/__snapshots__/desktop-chrome/definition-lists-header.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/definition-lists-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/definition-lists-sidebar.png index 01bb10e..6e1ca66 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/definition-lists-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/definition-lists-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/definition-lists.png b/tests/visual/__snapshots__/desktop-chrome/definition-lists.png index 385c6b8..d71f7d9 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/definition-lists.png and b/tests/visual/__snapshots__/desktop-chrome/definition-lists.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/exercises-header.png b/tests/visual/__snapshots__/desktop-chrome/exercises-header.png index 886afb3..363c9c3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/exercises-header.png and b/tests/visual/__snapshots__/desktop-chrome/exercises-header.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/exercises-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/exercises-sidebar.png index 4500450..8074c1b 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/exercises-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/exercises-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/exercises.png b/tests/visual/__snapshots__/desktop-chrome/exercises.png index 5c9c0fc..e448aee 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/exercises.png and b/tests/visual/__snapshots__/desktop-chrome/exercises.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/figures-header.png b/tests/visual/__snapshots__/desktop-chrome/figures-header.png index 886afb3..363c9c3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/figures-header.png and b/tests/visual/__snapshots__/desktop-chrome/figures-header.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/figures-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/figures-sidebar.png index 69214fd..c6f836f 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/figures-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/figures-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/figures.png b/tests/visual/__snapshots__/desktop-chrome/figures.png index 2f26bf4..60e94d1 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/figures.png and b/tests/visual/__snapshots__/desktop-chrome/figures.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/fstring-interpolation.png b/tests/visual/__snapshots__/desktop-chrome/fstring-interpolation.png index 49b3e8a..7edcf79 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/fstring-interpolation.png and b/tests/visual/__snapshots__/desktop-chrome/fstring-interpolation.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/glossary.png b/tests/visual/__snapshots__/desktop-chrome/glossary.png index 88c70b2..ed46cd5 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/glossary.png and b/tests/visual/__snapshots__/desktop-chrome/glossary.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/intro-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/intro-sidebar.png index d6eb6ad..0af0ec1 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/intro-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/intro-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/intro.png b/tests/visual/__snapshots__/desktop-chrome/intro.png index 8e3a7c3..182615b 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/intro.png and b/tests/visual/__snapshots__/desktop-chrome/intro.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/italic-text.png b/tests/visual/__snapshots__/desktop-chrome/italic-text.png index eb1e87c..ecc68c7 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/italic-text.png and b/tests/visual/__snapshots__/desktop-chrome/italic-text.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/long-page-header.png b/tests/visual/__snapshots__/desktop-chrome/long-page-header.png index 886afb3..363c9c3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/long-page-header.png and b/tests/visual/__snapshots__/desktop-chrome/long-page-header.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/long-page-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/long-page-sidebar.png index dbe7688..6b1f187 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/long-page-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/long-page-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/long-page.png b/tests/visual/__snapshots__/desktop-chrome/long-page.png index 098e535..85d9a38 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/long-page.png and b/tests/visual/__snapshots__/desktop-chrome/long-page.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/math-equation.png b/tests/visual/__snapshots__/desktop-chrome/math-equation.png index b1aba8a..55d178b 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/math-equation.png and b/tests/visual/__snapshots__/desktop-chrome/math-equation.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/math-header.png b/tests/visual/__snapshots__/desktop-chrome/math-header.png index 886afb3..363c9c3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/math-header.png and b/tests/visual/__snapshots__/desktop-chrome/math-header.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/math-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/math-sidebar.png index 5a8adf2..59402c0 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/math-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/math-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/math.png b/tests/visual/__snapshots__/desktop-chrome/math.png index 31ac943..fb8b85b 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/math.png and b/tests/visual/__snapshots__/desktop-chrome/math.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/prob-matrix-header.png b/tests/visual/__snapshots__/desktop-chrome/prob-matrix-header.png index 886afb3..363c9c3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/prob-matrix-header.png and b/tests/visual/__snapshots__/desktop-chrome/prob-matrix-header.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/prob-matrix-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/prob-matrix-sidebar.png index 65230af..7fc180e 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/prob-matrix-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/prob-matrix-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/proofs-header.png b/tests/visual/__snapshots__/desktop-chrome/proofs-header.png index 886afb3..363c9c3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/proofs-header.png and b/tests/visual/__snapshots__/desktop-chrome/proofs-header.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/proofs-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/proofs-sidebar.png index 619f22f..40d0e56 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/proofs-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/proofs-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/proofs.png b/tests/visual/__snapshots__/desktop-chrome/proofs.png index 204d0e5..260455c 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/proofs.png and b/tests/visual/__snapshots__/desktop-chrome/proofs.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/tables-header.png b/tests/visual/__snapshots__/desktop-chrome/tables-header.png index 886afb3..363c9c3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/tables-header.png and b/tests/visual/__snapshots__/desktop-chrome/tables-header.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/tables-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/tables-sidebar.png index 2a2bf07..4670ea8 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/tables-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/tables-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/tables.png b/tests/visual/__snapshots__/desktop-chrome/tables.png index 4aecc5d..884c22b 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/tables.png and b/tests/visual/__snapshots__/desktop-chrome/tables.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/toc-deep-nesting-header.png b/tests/visual/__snapshots__/desktop-chrome/toc-deep-nesting-header.png index 886afb3..363c9c3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/toc-deep-nesting-header.png and b/tests/visual/__snapshots__/desktop-chrome/toc-deep-nesting-header.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/toc-deep-nesting-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/toc-deep-nesting-sidebar.png index 0b570bd..276421d 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/toc-deep-nesting-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/toc-deep-nesting-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/toc-deep-nesting.png b/tests/visual/__snapshots__/desktop-chrome/toc-deep-nesting.png index 2d359fb..08da7f7 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/toc-deep-nesting.png and b/tests/visual/__snapshots__/desktop-chrome/toc-deep-nesting.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/typography-header.png b/tests/visual/__snapshots__/desktop-chrome/typography-header.png index 886afb3..363c9c3 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/typography-header.png and b/tests/visual/__snapshots__/desktop-chrome/typography-header.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/typography-sidebar.png b/tests/visual/__snapshots__/desktop-chrome/typography-sidebar.png index fb816c6..1d275af 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/typography-sidebar.png and b/tests/visual/__snapshots__/desktop-chrome/typography-sidebar.png differ diff --git a/tests/visual/__snapshots__/desktop-chrome/typography.png b/tests/visual/__snapshots__/desktop-chrome/typography.png index 7f77782..e5b621f 100644 Binary files a/tests/visual/__snapshots__/desktop-chrome/typography.png and b/tests/visual/__snapshots__/desktop-chrome/typography.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/admonitions-header.png b/tests/visual/__snapshots__/mobile-chrome/admonitions-header.png index ca3f151..26335d9 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/admonitions-header.png and b/tests/visual/__snapshots__/mobile-chrome/admonitions-header.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/admonitions-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/admonitions-sidebar.png index 90b03ca..778f57f 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/admonitions-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/admonitions-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/admonitions.png b/tests/visual/__snapshots__/mobile-chrome/admonitions.png index 98d6c15..c7d9dab 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/admonitions.png and b/tests/visual/__snapshots__/mobile-chrome/admonitions.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/code-blocks-header.png b/tests/visual/__snapshots__/mobile-chrome/code-blocks-header.png index ca3f151..26335d9 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/code-blocks-header.png and b/tests/visual/__snapshots__/mobile-chrome/code-blocks-header.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/code-blocks-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/code-blocks-sidebar.png index bcafc27..82cd98e 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/code-blocks-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/code-blocks-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/code-blocks.png b/tests/visual/__snapshots__/mobile-chrome/code-blocks.png index 699d49f..9f872ac 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/code-blocks.png and b/tests/visual/__snapshots__/mobile-chrome/code-blocks.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/cross-references-header.png b/tests/visual/__snapshots__/mobile-chrome/cross-references-header.png index ca3f151..26335d9 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/cross-references-header.png and b/tests/visual/__snapshots__/mobile-chrome/cross-references-header.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/cross-references-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/cross-references-sidebar.png index 799f21d..8012a56 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/cross-references-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/cross-references-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/cross-references.png b/tests/visual/__snapshots__/mobile-chrome/cross-references.png index 1fc8a93..3416299 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/cross-references.png and b/tests/visual/__snapshots__/mobile-chrome/cross-references.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/dark-mode.png b/tests/visual/__snapshots__/mobile-chrome/dark-mode.png index edac089..246b509 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/dark-mode.png and b/tests/visual/__snapshots__/mobile-chrome/dark-mode.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/definition-list.png b/tests/visual/__snapshots__/mobile-chrome/definition-list.png index b4d1560..bbd11f2 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/definition-list.png and b/tests/visual/__snapshots__/mobile-chrome/definition-list.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/definition-lists-header.png b/tests/visual/__snapshots__/mobile-chrome/definition-lists-header.png index ca3f151..26335d9 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/definition-lists-header.png and b/tests/visual/__snapshots__/mobile-chrome/definition-lists-header.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/definition-lists-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/definition-lists-sidebar.png index 669db51..cab9189 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/definition-lists-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/definition-lists-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/definition-lists.png b/tests/visual/__snapshots__/mobile-chrome/definition-lists.png index 206be96..0dcd49c 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/definition-lists.png and b/tests/visual/__snapshots__/mobile-chrome/definition-lists.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/exercises-header.png b/tests/visual/__snapshots__/mobile-chrome/exercises-header.png index ca3f151..26335d9 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/exercises-header.png and b/tests/visual/__snapshots__/mobile-chrome/exercises-header.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/exercises-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/exercises-sidebar.png index b21a39f..6140d59 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/exercises-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/exercises-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/exercises.png b/tests/visual/__snapshots__/mobile-chrome/exercises.png index d3285c3..7c9d60b 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/exercises.png and b/tests/visual/__snapshots__/mobile-chrome/exercises.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/figures-header.png b/tests/visual/__snapshots__/mobile-chrome/figures-header.png index ca3f151..26335d9 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/figures-header.png and b/tests/visual/__snapshots__/mobile-chrome/figures-header.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/figures-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/figures-sidebar.png index c62e43f..3dc3571 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/figures-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/figures-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/figures.png b/tests/visual/__snapshots__/mobile-chrome/figures.png index d6f8b77..28da75e 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/figures.png and b/tests/visual/__snapshots__/mobile-chrome/figures.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/glossary.png b/tests/visual/__snapshots__/mobile-chrome/glossary.png index c0d59c1..5ba8ce3 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/glossary.png and b/tests/visual/__snapshots__/mobile-chrome/glossary.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/intro-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/intro-sidebar.png index d37057c..3bedf75 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/intro-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/intro-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/intro.png b/tests/visual/__snapshots__/mobile-chrome/intro.png index ea61dec..7dff2b4 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/intro.png and b/tests/visual/__snapshots__/mobile-chrome/intro.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/long-page-header.png b/tests/visual/__snapshots__/mobile-chrome/long-page-header.png index ca3f151..26335d9 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/long-page-header.png and b/tests/visual/__snapshots__/mobile-chrome/long-page-header.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/long-page-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/long-page-sidebar.png index 34ec566..22f67c4 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/long-page-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/long-page-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/long-page.png b/tests/visual/__snapshots__/mobile-chrome/long-page.png index 5b3fc7e..5b3de5e 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/long-page.png and b/tests/visual/__snapshots__/mobile-chrome/long-page.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/math-equation.png b/tests/visual/__snapshots__/mobile-chrome/math-equation.png index aac2156..dd1fba4 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/math-equation.png and b/tests/visual/__snapshots__/mobile-chrome/math-equation.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/math-header.png b/tests/visual/__snapshots__/mobile-chrome/math-header.png index ca3f151..26335d9 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/math-header.png and b/tests/visual/__snapshots__/mobile-chrome/math-header.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/math-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/math-sidebar.png index 0686341..7aff895 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/math-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/math-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/math.png b/tests/visual/__snapshots__/mobile-chrome/math.png index 9ca8192..b11cde3 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/math.png and b/tests/visual/__snapshots__/mobile-chrome/math.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/prob-matrix-header.png b/tests/visual/__snapshots__/mobile-chrome/prob-matrix-header.png index ca3f151..26335d9 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/prob-matrix-header.png and b/tests/visual/__snapshots__/mobile-chrome/prob-matrix-header.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/prob-matrix-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/prob-matrix-sidebar.png index 1c46899..590b11f 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/prob-matrix-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/prob-matrix-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/proofs-header.png b/tests/visual/__snapshots__/mobile-chrome/proofs-header.png index ca3f151..26335d9 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/proofs-header.png and b/tests/visual/__snapshots__/mobile-chrome/proofs-header.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/proofs-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/proofs-sidebar.png index e9b7a11..ce1cc3b 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/proofs-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/proofs-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/proofs.png b/tests/visual/__snapshots__/mobile-chrome/proofs.png index 040f0ef..8060701 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/proofs.png and b/tests/visual/__snapshots__/mobile-chrome/proofs.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/tables-header.png b/tests/visual/__snapshots__/mobile-chrome/tables-header.png index ca3f151..26335d9 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/tables-header.png and b/tests/visual/__snapshots__/mobile-chrome/tables-header.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/tables-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/tables-sidebar.png index 96153f6..dbf8fbc 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/tables-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/tables-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/tables.png b/tests/visual/__snapshots__/mobile-chrome/tables.png index b5183bd..4f7793c 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/tables.png and b/tests/visual/__snapshots__/mobile-chrome/tables.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/toc-deep-nesting-header.png b/tests/visual/__snapshots__/mobile-chrome/toc-deep-nesting-header.png index ca3f151..26335d9 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/toc-deep-nesting-header.png and b/tests/visual/__snapshots__/mobile-chrome/toc-deep-nesting-header.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/toc-deep-nesting-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/toc-deep-nesting-sidebar.png index 584a36c..a980ed7 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/toc-deep-nesting-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/toc-deep-nesting-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/toc-deep-nesting.png b/tests/visual/__snapshots__/mobile-chrome/toc-deep-nesting.png index d9cf7ea..ad78a64 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/toc-deep-nesting.png and b/tests/visual/__snapshots__/mobile-chrome/toc-deep-nesting.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/typography-header.png b/tests/visual/__snapshots__/mobile-chrome/typography-header.png index ca3f151..26335d9 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/typography-header.png and b/tests/visual/__snapshots__/mobile-chrome/typography-header.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/typography-sidebar.png b/tests/visual/__snapshots__/mobile-chrome/typography-sidebar.png index 2f62905..0a0022d 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/typography-sidebar.png and b/tests/visual/__snapshots__/mobile-chrome/typography-sidebar.png differ diff --git a/tests/visual/__snapshots__/mobile-chrome/typography.png b/tests/visual/__snapshots__/mobile-chrome/typography.png index 293cd1c..957fa35 100644 Binary files a/tests/visual/__snapshots__/mobile-chrome/typography.png and b/tests/visual/__snapshots__/mobile-chrome/typography.png differ