diff --git a/CHANGELOG.md b/CHANGELOG.md index f39d66c..ce6f3c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning][]. ## [Unreleased] +### Added + +- `secondary_accent` theme option for the sidebar captions, which `True` sets to the package’s second brand colour from the website (#73) + ### Changed - brand accents are synced from the website’s `accents.json`. diff --git a/docs/api/theme.md b/docs/api/theme.md index 062e617..c302b77 100644 --- a/docs/api/theme.md +++ b/docs/api/theme.md @@ -32,6 +32,15 @@ The registry name to look the accent and the dropdown’s current entry up under CSS colour for decorative surfaces; link and text shades are derived from it. ``` +```{confval} secondary_accent +:type: str | bool +:default: none + +CSS colour for the sidebar captions; a readable shade is derived from it. +`True` takes the package’s second brand colour from the registry, if it has one. +Without one, the captions keep the text colour. +``` + ```{confval} show_ecosystem_dropdown :type: bool :default: `True` diff --git a/src/scverse_doc/registry.py b/src/scverse_doc/registry.py index 751e01b..05ea99e 100644 --- a/src/scverse_doc/registry.py +++ b/src/scverse_doc/registry.py @@ -57,13 +57,13 @@ def _build_cache(app: Sphinx) -> Path: #: Brand accents from the website, copied by ``scripts/sync_brand_tokens.py``: #: each key lists ``[name, [L, C, H]]`` pairs, primary first. -_ACCENTS: dict[str, str] = { - {"scvi": "scvi-tools"}.get(pkg, pkg): "oklch({} {} {})".format(*colors[0][1]) +_ACCENTS: dict[str, list[str]] = { + {"scvi": "scvi-tools"}.get(pkg, pkg): ["oklch({} {} {})".format(*lch) for _, lch in colors] for pkg, colors in json.loads(files(__package__).joinpath("_accents.json").read_text()).items() } #: The website’s ``--scverse-deep-blue``, used when a package has no accent of its own. -DEFAULT_ACCENT = _ACCENTS.pop("scverse") +DEFAULT_ACCENT = _ACCENTS.pop("scverse")[0] #: Upstream ``category`` -> the ``kind`` recorded here. #: ``core-infrastructure`` is skipped: @@ -109,6 +109,9 @@ class Package: accent: str = DEFAULT_ACCENT """The package’s brand accent, falling back to the scverse deep blue.""" + secondary_accent: str | None = None + """The package’s second brand colour, or :data:`None` if it has only one.""" + class _Entry(TypedDict): """The fields of an upstream listing entry that the registry reads.""" @@ -152,7 +155,8 @@ def _load() -> Mapping[str, Package]: inventory=meta.get("inventory"), repo=meta.get("project_home"), description=" ".join(meta.get("description", "").split()), - accent=_ACCENTS.get(name, DEFAULT_ACCENT), + accent=accents[0] if (accents := _ACCENTS.get(name, [])) else DEFAULT_ACCENT, + secondary_accent=accents[1] if len(accents) >= 2 else None, ) # Sorted here, because upstream’s listing order is not stable. for name, meta in sorted(((meta["name"].casefold(), meta) for meta in _fetch()), key=itemgetter(0)) diff --git a/src/scverse_doc/theme/__init__.py b/src/scverse_doc/theme/__init__.py index 80c5da0..76b177d 100644 --- a/src/scverse_doc/theme/__init__.py +++ b/src/scverse_doc/theme/__init__.py @@ -45,6 +45,15 @@ }}}} """ +_SECONDARY_ACCENT_CSS = f"""\ +:root {{{{ + --scverse-color-secondary-accent-text: light-dark( + oklch(from {{accent}} min(l, {_MAX_LIGHTNESS_ON_LIGHT}) c h), + oklch(from {{accent}} max(l, {_MIN_LIGHTNESS_ON_DARK}) c h) + ); +}}}} +""" + #: scverse.org itself is linked from the scverse menu beside them. _ICON_LINKS = ({"name": "Discourse", "url": "https://discourse.scverse.org/", "icon": "fa-brands fa-discourse"},) @@ -61,6 +70,15 @@ def _accent(config: Config) -> str: return DEFAULT_ACCENT +def _secondary_accent(config: Config) -> str | None: + accent: str | bool = config.html_theme_options.get("secondary_accent") + if accent is not True: + return accent or None + if pkg := packages.get(_package_name(config)): + return pkg.secondary_accent + return None + + def _add_chrome(config: Config) -> None: """Add the brand icon links and the colour mode, alongside whatever :mod:`scverse_doc.source` set.""" options = config.html_theme_options @@ -85,9 +103,12 @@ def _configure(app: Sphinx) -> None: _add_chrome(config) + css = _ACCENT_CSS.format(accent=_accent(config)) + if secondary := _secondary_accent(config): + css += _SECONDARY_ACCENT_CSS.format(accent=secondary) static_dir = _build_cache(app) / "static" static_dir.mkdir(parents=True, exist_ok=True) - (static_dir / "scverse-accent.css").write_text(_ACCENT_CSS.format(accent=_accent(config))) + (static_dir / "scverse-accent.css").write_text(css) config.html_static_path = [*config.html_static_path, str(static_dir)] app.add_css_file("scverse-accent.css") app.add_js_file("scripts/readthedocs-integration.js", type="module") diff --git a/src/scverse_doc/theme/scverse/static/styles/scverse.css b/src/scverse_doc/theme/scverse/static/styles/scverse.css index 9167626..9cbdefc 100644 --- a/src/scverse_doc/theme/scverse/static/styles/scverse.css +++ b/src/scverse_doc/theme/scverse/static/styles/scverse.css @@ -228,6 +228,13 @@ a.dropdown-item { } } +nav.bd-links p.caption { + color: var( + --scverse-color-secondary-accent-text, + var(--pst-color-text-base) + ); +} + readthedocs-flyout { --readthedocs-flyout-background-color: transparent; --readthedocs-flyout-font-family: inherit; diff --git a/src/scverse_doc/theme/scverse/theme.toml b/src/scverse_doc/theme/scverse/theme.toml index 336c8ab..7b51404 100644 --- a/src/scverse_doc/theme/scverse/theme.toml +++ b/src/scverse_doc/theme/scverse/theme.toml @@ -9,22 +9,23 @@ sidebars = ["navbar-logo", "search-button-field", "scverse-sidebar-nav"] [options] package = "" accent = "" -show_ecosystem_dropdown = "True" +secondary_accent = false +show_ecosystem_dropdown = true # No navbar: its parts live in the primary sidebar and the article header. -navbar_start = "" -navbar_center = "" -navbar_end = "" -navbar_persistent = "" +navbar_start = [] +navbar_center = [] +navbar_end = [] +navbar_persistent = [] article_header_start = ["toggle-primary-sidebar", "breadcrumbs"] article_header_end = ["theme-switcher", "navbar-icon-links", "scverse-ecosystem", "toggle-secondary-sidebar"] primary_sidebar_end = ["sidebar-ethical-ads", "sidebar-versions"] secondary_sidebar_items = ["page-toc", "edit-this-page", "sourcelink"] -footer_start = "scverse-footer" -footer_center = "" -footer_end = "" -show_toc_level = "2" -navigation_depth = "3" -collapse_navigation = "False" +footer_start = ["scverse-footer"] +footer_center = [] +footer_end = [] +show_toc_level = 2 +navigation_depth = 3 +collapse_navigation = false pygments_light_style = "tango" pygments_dark_style = "monokai" announcement = "https://scverse.org/announcement.html" diff --git a/tests/roots/minimal/conf.py b/tests/roots/minimal/conf.py index 4152546..a6b5146 100644 --- a/tests/roots/minimal/conf.py +++ b/tests/roots/minimal/conf.py @@ -3,4 +3,4 @@ extensions = ["scverse_doc", "sphinx.ext.linkcode"] source_repository = "https://github.com/scverse/pertpy" source_branch = "main" -html_theme_options = {"announcement": ""} +html_theme_options = {"announcement": "", "secondary_accent": True} diff --git a/tests/test_build.py b/tests/test_build.py index 5af9fe6..8ea53b3 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -80,6 +80,7 @@ def test_accent_is_derived_for_both_modes(minimal: tuple[SphinxTestApp, str]) -> css = (Path(app.outdir) / "_static" / "scverse-accent.css").read_text() assert "--scverse-color-accent-decorative: oklch(0.5 0.1 249);" in css assert "oklch(from var(--scverse-color-accent-decorative) min(l, 0.54) c h)" in css + assert "oklch(from oklch(0.595 0.177 14) min(l, 0.54) c h)" in css @needs_full @@ -125,7 +126,7 @@ def test_conf_py_can_pick_another_theme(tmp_path: Path) -> None: """A theme that declares none of the source options must not be given any.""" app, _ = build(ROOTS / "minimal", tmp_path, html_theme="alabaster") assert app.config.html_theme == "alabaster" - assert app.config.html_theme_options.keys() == {"announcement"} + assert app.config.html_theme_options.keys() == {"announcement", "secondary_accent"} @needs_full diff --git a/tests/test_color.py b/tests/test_color.py index 72d0a1b..41c1333 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -15,12 +15,13 @@ _MAX_LIGHTNESS_ON_LIGHT, _MIN_LIGHTNESS_ON_DARK, _accent, + _secondary_accent, ) #: Minimum contrast ratio for normal-size body text under WCAG 2.1 AA. WCAG_AA_NORMAL = 4.5 -ACCENTS = sorted({pkg.accent for pkg in packages.values()}) +ACCENTS = sorted({a for pkg in packages.values() for a in (pkg.accent, pkg.secondary_accent) if a}) def contrast(color: Color, background: str) -> float: @@ -60,3 +61,18 @@ def test_bounds_are_load_bearing() -> None: def test_accent_for_a_package_outside_the_registry(options: dict[str, str], expected: str) -> None: config = SimpleNamespace(html_theme_options=options, project="not-an-scverse-package") assert _accent(config) == expected # type: ignore[arg-type] + + +@pytest.mark.parametrize( + ("project", "options", "expected"), + [ + pytest.param("pertpy", {}, None, id="unset"), + pytest.param("pertpy", {"secondary_accent": True}, "oklch(0.595 0.177 14)", id="registry"), + pytest.param("anndata", {"secondary_accent": True}, None, id="single-colour"), + pytest.param("not-an-scverse-package", {"secondary_accent": True}, None, id="unknown"), + pytest.param("pertpy", {"secondary_accent": "#c0f"}, "#c0f", id="declared"), + ], +) +def test_secondary_accent(project: str, options: dict[str, str], expected: str | None) -> None: + config = SimpleNamespace(html_theme_options=options, project=project) + assert _secondary_accent(config) == expected # type: ignore[arg-type]