Skip to content

Commit 454918d

Browse files
12rambaucholdgrafdamianavila
authored
Add dark theme and theme switcher (#540)
* docs: exclude files created by jupyter * set up js for theme change * demo with only background color change * set-up all the dark theme colors * overwrite the pygment css file * add default_theme parameter * small update to documentation * color support for ethical add * missing alpha channel in css * docs: fix badly written css comments * Update src/pydata_sphinx_theme/__init__.py Co-authored-by: Chris Holdgraf <[email protected]> * use typescript comment structure Co-authored-by: Chris Holdgraf <[email protected]> * make the switch as a btn * use lighter section headers * replace all colors with rgba defined colors * add comments on pygments theme switch * move ethical-ads to its own scss file * use theme options to control pygments styling * change theme switch color * remind that pygments_style is overwritten * add comment to js theme management functions * refactor cycleTheme * prevent flickering when switching theme * set the theme-switcher as a template * document the css trick Co-authored-by: Chris Holdgraf <[email protected]> * document the css trick * change colors to improve accecibility * change colors to improve accecibility * apply pre-commit checks * add the theme switcher to the tests * quote Furo theme in comments * add the css trick to all version modified widgets * include sidebar and topic in the color scheme * refactor navbar-toggler * support for version btn * fix blockquote colors * listen to the span instead of the a tag * use the same color for hov and focus * typo Co-authored-by: Damian Avila <[email protected]> * typo Co-authored-by: Damian Avila <[email protected]> * set the theme switcher in the navbar-end * set back the theme-switcher in pydata documentation * replace orange and green * use color in the gihub action report * test the theme switcher * remove the theme switcher container from the test * typo * docs: disclaimer on colors * docs: typo * reduce minscore for lighthouse SEO expectations * Cleaning up the colors for accessibility * Warning about stability * Update src/pydata_sphinx_theme/assets/styles/_navbar.scss Co-authored-by: Damian Avila <[email protected]> Co-authored-by: Chris Holdgraf <[email protected]> Co-authored-by: Damian Avila <[email protected]> Co-authored-by: Chris Holdgraf <[email protected]>
1 parent ba74dd8 commit 454918d

23 files changed

+856
-168
lines changed

.github/workflows/lighthouserc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"categories:performance": ["error", { "minScore": 0.5 }],
1212
"categories:accessibility": ["error", { "minScore": 0.8 }],
1313
"categories:best-practices": ["error", { "minScore": 0.8 }],
14-
"categories:seo": ["error", { "minScore": 0.8 }]
14+
"categories:seo": ["error", { "minScore": 0.7 }]
1515
}
1616
}
1717
}

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
6767
# Run tests under coverage
6868
- name: Run the tests
69-
run: pytest --cov pydata_sphinx_theme --cov-branch --cov-report term-missing:skip-covered --cov-fail-under ${{ env.COVERAGE_THRESHOLD }}
69+
run: pytest --color=yes --cov pydata_sphinx_theme --cov-branch --cov-report term-missing:skip-covered --cov-fail-under ${{ env.COVERAGE_THRESHOLD }}
7070

7171
- name: Upload coverage
7272
if: ${{ always() }}

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
# List of patterns, relative to source directory, that match files and
6262
# directories to ignore when looking for source files.
6363
# This pattern also affects html_static_path and html_extra_path.
64-
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
64+
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"]
6565

6666
# -- Extension options -------------------------------------------------------
6767

@@ -126,7 +126,7 @@
126126
# "navbar_align": "left", # [left, content, right] For testing that the navbar items align properly
127127
# "navbar_start": ["navbar-logo", "navbar-version"],
128128
# "navbar_center": ["navbar-nav", "navbar-version"], # Just for testing
129-
"navbar_end": ["version-switcher", "navbar-icon-links"],
129+
"navbar_end": ["theme-switcher", "version-switcher", "navbar-icon-links"],
130130
# "left_sidebar_end": ["custom-template.html", "sidebar-ethical-ads.html"],
131131
# "footer_items": ["copyright", "sphinx-version", ""]
132132
"switcher": {

docs/user_guide/configuring.rst

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ in your ``conf.py`` file. This is a dictionary with ``key: val`` pairs that
1010
you can configure in various ways. This page describes the options available to you.
1111

1212
Configure project logo
13-
==============================
13+
======================
1414

1515
To add a logo that's placed at the left of your nav bar, put a logo file under your
1616
doc path's _static folder, and use the following configuration:
@@ -31,6 +31,35 @@ If you'd like it to link to another page or use an external link instead, use th
3131
3232
.. _icon-links:
3333

34+
Configure default theme
35+
=======================
36+
37+
The theme mode can be changed by the user. By default landing on the documentation will switch the mode to ``auto``. You can specified this value to be one of ``auto``, ``dark``, ``light``.
38+
39+
.. code-block:: python
40+
41+
html_context = {
42+
...
43+
"default_mode": "auto"
44+
}
45+
46+
Configure pygment theme
47+
=======================
48+
49+
As the Sphinx theme supports multiple modes, the code highlighting colors can be modified for each one of them by modifying the `pygment_light_style`and `pygment_style_style`. You can check available Pygments colors on this `page <https://help.farbox.com/pygments.html>`__.
50+
51+
.. code-block:: python
52+
53+
html_contexts = {
54+
...
55+
"pygment_light_style": "tango",
56+
"pygment_dark_style": "native"
57+
}
58+
59+
.. danger::
60+
61+
The native Sphinx option `pygments_style` will be overwritten by this theme.
62+
3463
Configure icon links
3564
====================
3665

docs/user_guide/customizing.rst

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ Customizing the theme
77
In addition to the configuration options detailed at :ref:`configuration`, it
88
is also possible to customize the HTML layout and CSS style of the theme.
99

10+
.. danger::
11+
12+
This theme is still under active development, and we make no promises
13+
about the stability of any specific HTML structure, CSS variables, etc.
14+
Make these customizations at your own risk, and pin versions if you're
15+
worried about breaking changes!
16+
1017
.. _custom-css:
1118

1219
Custom CSS Stylesheets
@@ -30,6 +37,41 @@ To add a custom stylesheet, follow these steps:
3037
3138
When you build your documentation, this stylesheet should now be activated.
3239

40+
.. _manage-themes:
41+
42+
Manage themes
43+
=============
44+
45+
.. danger::
46+
47+
Theming is still a beta feature so the variables related to the theme switch are likely to change in the future. No backward compatibily is guaranteed when customization is done.
48+
49+
Pydata sphinx theme embed 3 different theming mode:
50+
51+
- ``auto``: the documentation theme will follow the one provided by your computer
52+
- ``dark``: the documentation is displayed with the dark theme
53+
- ``light``: the documentation is displayed with the light theme
54+
55+
In order to customize the display of any of the theme element you need to encaspulate your modifications in the approriate css rules:
56+
57+
.. code-block:: css
58+
59+
/* anything related to the light theme */
60+
html[data-theme="light"] {
61+
62+
/* whatever you want to change */
63+
background: white;
64+
}
65+
66+
/* anything related to the dark theme */
67+
html[data-theme="dark"] {
68+
69+
/* whatever you want to change */
70+
background: black;
71+
}
72+
73+
A complete list of the used colors for this theme can be found in the `pydata default css colors file <pydata-css-colors_>`__.
74+
3375
.. _css-variables:
3476

3577
CSS Theme variables
@@ -58,7 +100,8 @@ In order to change a variable, follow these steps:
58100

59101
Note that these are `CSS variables <css-variable-help_>`_ and not
60102
`SASS variables <https://sass-lang.com/documentation/variables>`_.
61-
The theme is defined with CSS variables, not SASS variables!
103+
The theme is defined with CSS variables, not SASS variables! Refer to the previous section if
104+
you desire a different behavior between the light and dark theme.
62105

63106
For a complete list of the theme variables that you may override, see the
64107
`theme variables defaults CSS file <pydata-css-variables_>`_:
@@ -126,6 +169,7 @@ The default body and header fonts can be changed as follows:
126169
before waiting for the CSS to be parsed, but should be used with care.
127170

128171
.. _pydata-css-variables: https://github.com/pydata/pydata-sphinx-theme/blob/master/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/static/styles/theme.css
172+
.. _pydata-css-colors: https://github.com/pydata/pydata-sphinx-theme/blob/master/src/pydata_sphinx_theme/assets/style/color.scss
129173
.. _css-variable-help: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties
130174

131175
.. meta::

docs/user_guide/sections.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ will be named accordingly).
138138
- ``sidebar-nav-bs.html``
139139
- ``sphinx-version.html``
140140
- ``version-switcher.html``
141+
- ``theme-switcher.html``
141142

142143
Add your own HTML templates to theme sections
143144
=============================================

src/pydata_sphinx_theme/__init__.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from sphinx.environment.adapters.toctree import TocTree
1111
from sphinx.errors import ExtensionError
1212
from sphinx.util import logging
13+
from pygments.formatters import HtmlFormatter
14+
from pygments.styles import get_all_styles
1315

1416
from .bootstrap_html_translator import BootstrapHTML5Translator
1517

@@ -547,6 +549,87 @@ def get_edit_url():
547549
context["theme_show_toc_level"] = int(context.get("theme_show_toc_level", 1))
548550

549551

552+
# ------------------------------------------------------------------------------
553+
# handle pygment css
554+
# ------------------------------------------------------------------------------
555+
556+
# inspired by the Furo theme
557+
# https://github.com/pradyunsg/furo/blob/main/src/furo/__init__.py
558+
559+
560+
def _get_styles(formatter, prefix):
561+
"""
562+
Get styles out of a formatter, where everything has the correct prefix.
563+
"""
564+
565+
for line in formatter.get_linenos_style_defs():
566+
yield f"{prefix} {line}"
567+
yield from formatter.get_background_style_defs(prefix)
568+
yield from formatter.get_token_style_defs(prefix)
569+
570+
571+
def get_pygments_stylesheet(light_style, dark_style):
572+
"""
573+
Generate the theme-specific pygments.css.
574+
There is no way to tell Sphinx how the theme handles modes
575+
"""
576+
light_formatter = HtmlFormatter(style=light_style)
577+
dark_formatter = HtmlFormatter(style=dark_style)
578+
579+
lines = []
580+
581+
light_prefix = 'html[data-theme="light"] .highlight'
582+
lines.extend(_get_styles(light_formatter, prefix=light_prefix))
583+
584+
dark_prefix = 'html[data-theme="dark"] .highlight'
585+
lines.extend(_get_styles(dark_formatter, prefix=dark_prefix))
586+
587+
return "\n".join(lines)
588+
589+
590+
def _overwrite_pygments_css(app, exception=None):
591+
"""
592+
Sphinx is not build to host multiple sphinx formatter and there is no way
593+
to tell which one to use and when.
594+
So yes, at build time we overwrite the pygment.css file so that it embeds
595+
2 versions:
596+
- the light theme prefixed with "[data-theme="light"]" using tango
597+
- the dark theme prefixed with "[data-theme="dark"]" using native
598+
599+
When the theme is switched, Pygments will be using one of the preset css
600+
style.
601+
"""
602+
default_light_theme = "tango"
603+
default_dark_theme = "native"
604+
605+
if exception is not None:
606+
return
607+
608+
assert app.builder
609+
610+
# check the theme specified in the theme options
611+
theme_options = app.config["html_theme_options"]
612+
pygments_styles = list(get_all_styles())
613+
light_theme = theme_options.get("pygment_light_style", default_light_theme)
614+
if light_theme not in pygments_styles:
615+
logger.warn(
616+
f"{light_theme}, is not part of the available pygments style,"
617+
f' defaulting to "{default_light_theme}".'
618+
)
619+
light_theme = default_light_theme
620+
dark_theme = theme_options.get("pygment_dark_style", default_dark_theme)
621+
if dark_theme not in pygments_styles:
622+
logger.warn(
623+
f"{dark_theme}, is not part of the available pygments style,"
624+
f' defaulting to "{default_dark_theme}".'
625+
)
626+
dark_theme = default_dark_theme
627+
628+
pygment_css = Path(app.builder.outdir) / "_static" / "pygments.css"
629+
with pygment_css.open("w") as f:
630+
f.write(get_pygments_stylesheet(light_theme, dark_theme))
631+
632+
550633
# -----------------------------------------------------------------------------
551634

552635

@@ -567,6 +650,7 @@ def setup(app):
567650
app.connect("html-page-context", setup_edit_url)
568651
app.connect("html-page-context", add_toctree_functions)
569652
app.connect("html-page-context", update_templates)
653+
app.connect("build-finished", _overwrite_pygments_css)
570654

571655
# Include templates for sidebar
572656
app.config.templates_path.append(str(theme_path / "_templates"))

src/pydata_sphinx_theme/assets/scripts/index.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,89 @@ import "bootstrap";
99

1010
import "../styles/index.scss";
1111

12+
/*******************************************************************************
13+
* Theme interaction
14+
*/
15+
16+
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
17+
18+
/**
19+
* set the the body theme to the one specified by the user browser
20+
* @param {event} e
21+
*/
22+
function autoTheme(e) {
23+
document.documentElement.dataset.theme = prefersDark.matches
24+
? "dark"
25+
: "light";
26+
}
27+
28+
/**
29+
* Set the theme using the specified mode.
30+
* It can be one of ["auto", "dark", "light"]
31+
* @param {str} mode
32+
*/
33+
function setTheme(mode) {
34+
if (mode !== "light" && mode !== "dark" && mode !== "auto") {
35+
console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`);
36+
mode = "auto";
37+
}
38+
39+
// get the theme
40+
var colorScheme = prefersDark.matches ? "dark" : "light";
41+
document.documentElement.dataset.mode = mode;
42+
var theme = mode == "auto" ? colorScheme : mode;
43+
document.documentElement.dataset.theme = theme;
44+
45+
// save mode and theme
46+
localStorage.setItem("mode", mode);
47+
localStorage.setItem("theme", theme);
48+
console.log(`Changed to ${mode} mode using the ${theme} theme.`);
49+
50+
// add a listener if set on auto
51+
prefersDark.onchange = mode == "auto" ? autoTheme : "";
52+
}
53+
54+
/**
55+
* Change the theme option order so that clicking on the btn is always a change
56+
* from "auto"
57+
*/
58+
function cycleMode() {
59+
const defaultMode = document.documentElement.dataset.defaultMode || "auto";
60+
const currentMode = localStorage.getItem("mode") || defaultMode;
61+
62+
var loopArray = (arr, current) => {
63+
var nextPosition = arr.indexOf(current) + 1;
64+
if (nextPosition === arr.length) {
65+
nextPosition = 0;
66+
}
67+
return arr[nextPosition];
68+
};
69+
70+
// make sure the next theme after auto is always a change
71+
var modeList = prefersDark.matches
72+
? ["auto", "light", "dark"]
73+
: ["auto", "dark", "light"];
74+
var newMode = loopArray(modeList, currentMode);
75+
setTheme(newMode);
76+
}
77+
78+
/**
79+
* add the theme listener on the btns of the navbar
80+
*/
81+
function addModeListener() {
82+
// the theme was set a first time using the initial mini-script
83+
// running setMode will ensure the use of the dark mode if auto is selected
84+
setTheme(document.documentElement.dataset.mode);
85+
86+
// Attach event handlers for toggling themes colors
87+
const btn = document.getElementById("theme-switch");
88+
btn.addEventListener("click", cycleMode);
89+
}
90+
91+
/*******************************************************************************
92+
* TOC interactivity
93+
*/
94+
1295
function addTOCInteractivity() {
1396
// TOC sidebar - add "active" class to parent list
1497
//
@@ -31,6 +114,10 @@ function addTOCInteractivity() {
31114
});
32115
}
33116

117+
/*******************************************************************************
118+
* Scroll
119+
*/
120+
34121
// Navigation sidebar scrolling to active page
35122
function scrollToActive() {
36123
var sidebar = document.querySelector("div.bd-sidebar");
@@ -73,5 +160,6 @@ function scrollToActive() {
73160

74161
// This is equivalent to the .ready() function as described in
75162
// https://api.jquery.com/ready/
163+
$(addModeListener);
76164
$(scrollToActive);
77165
$(addTOCInteractivity);

0 commit comments

Comments
 (0)