Skip to content

Commit b48ba36

Browse files
committed
Add dark mode support.
The color scheme (dark or light) that is used is automatically detected from the browser's settings initially, but can be set via a color scheme chooser in the page header. PG problems are still rendered in light mode regardless of the color mode for the rest of the page. Converting PG to honor dark mode will take some work. Note that there are some small changes needed for the image view dialog and knowls of PG to make sure that those dialogs render in light mode. There is also a minor change to make the scaffold buttons stay in light mode as well. Note that there were some issues with light mode (i.e., the existing themes). The contrast of the links in the site nav were not good when they were focused or hovered over with the mouse. This was due to an override of the link focus styles that made the colors lighter in those cases. That was needed for links in the masthead (the page header), but generally was bad elsewhere. So instead this uses the Bootstrap defaults for the hover/active colors which darkens the colors instead of lightening them. So now there is just a special case for links in the masthead, and the other links use bootstraps default colors which gives higher contrast in the site nav. One side advantage of the above approach since it darkens links on hover and focus instead of lightening them is that I can finally switch to a link color for the math4-yellow theme that is not off theme. Previously it was a reddish color that I never really like in the scope of the theme, but was needed for contrast. Now it uses a rather dark yellow and gives a more consistent feel for the theme. The MathJax `no-dark-mode` extension has been converted into a `bs-color-scheme` extension which rewrites the MathJax styles to honor the `data-bs-theme` value instead of using media queries for the browser mode. This means that MathJax will display in the correct mode wherever the element is in the page. For example, on a problem page the math in the problem will always be in light mode. On the problem grader page the math in the problem there will also be in light mode, but the formula student answers will be in dark mode if the page is set to that. Note that the MathJax dialogs will always be in dark mode if the page is set to that since those are injected outside of the problem content div. The menu will also always be dark mode in a problem if the page is set to that since there isn't an override for the menu in the `data-bs-theme` extension. Its styles work differently. Note that there was a small change needed for the PG CodeMirror editor to give its default light theme a white background. This was done in the `pg-codemirror-editor` repository, and the new npm package published and included in this pull request. I am sure that I missed some colors that need to be adjusted in dark mode and perhaps issues with forcing PG into light mode, but I can't find anything right now. So please check this carefully. The `README` files in the theme directories have been deleted, and replaced with a single `README.md` file in the parent `htdocs/themes` directory. It has instructions on how to create a custom theme, and documents the Sass and CSS variables that can be set. There is a small change in `ConfigValues.pm` to only list directories when listing theme directories, and to skip this `README.md` file.
1 parent 62a8a67 commit b48ba36

40 files changed

+712
-275
lines changed

htdocs/js/GatewayQuiz/gateway.scss

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
/* gateway styles */
22

3-
div.gwMessage {
4-
background-color: #ffeeaa;
5-
box-shadow: 3px 3px 3px darkgray;
6-
margin: 0 0 1rem 0;
7-
padding: 0.25rem;
8-
border-radius: 3px;
9-
}
10-
113
#gwTimer {
124
position: sticky;
135
width: 15em;
@@ -60,6 +52,11 @@ table.attemptResults {
6052
border: 1px solid #ddd;
6153
border-radius: 3px;
6254

55+
[data-bs-theme='dark'] & {
56+
border-color: #555;
57+
background-color: var(--bs-primary-bg-subtle, 'black');
58+
}
59+
6360
h2.gw-problem-number {
6461
display: inline-block;
6562
font-size: 16px;
@@ -85,21 +82,27 @@ table.attemptResults {
8582
}
8683

8784
colgroup.page {
88-
border-left: solid 1pt black;
89-
border-right: solid 1pt black;
85+
border-left: solid 1pt var(--bs-emphasis-color, black);
86+
border-right: solid 1pt var(--bs-emphasis-color, black);
9087
}
9188

9289
.page.active {
9390
background-color: #ffeeaa;
91+
92+
[data-bs-theme='dark'] & {
93+
color: white;
94+
background-color: #80690a;
95+
}
9496
}
9597
}
9698

9799
div.gwDivider {
98100
margin: 0px 0px 10px 0px;
99101
}
100102

101-
/* Override the pg style so that the problem-content is not offset in gateway quizzes. */
103+
/* Override the pg style so that the problem-content is not offset in gateway quizzes and force a light color scheme. */
102104
.problem-content {
105+
color-scheme: light;
103106
padding: unset;
104107
background-color: unset;
105108
border: unset;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
if (MathJax.loader) MathJax.loader.checkVersion('[bs-color-scheme]', '4.1.0', 'extension');
2+
3+
const switchToBSStyle = (obj, key = '@media (prefers-color-scheme: dark)') => {
4+
obj["[data-bs-theme='dark']"] = obj[key];
5+
delete obj[key];
6+
obj["[data-bs-theme='light']"] = structuredClone(obj);
7+
};
8+
9+
for (const [immediate, extension, ready] of [
10+
[
11+
MathJax._.ui?.dialog,
12+
'core',
13+
() => {
14+
const { DraggableDialog } = MathJax._.ui.dialog.DraggableDialog;
15+
switchToBSStyle(DraggableDialog.styles);
16+
17+
// This is a workaround for a bug in MathJax 4.1.0. Delete this for the next version of MathJax.
18+
// See https://github.com/mathjax/MathJax-src/pull/1414.
19+
DraggableDialog.styles["[data-bs-theme='dark']"]['.mjx-dialog a[href]'] =
20+
DraggableDialog.styles["[data-bs-theme='dark']"]['a[href]'];
21+
delete DraggableDialog.styles["[data-bs-theme='dark']"]['a[href]'];
22+
DraggableDialog.styles["[data-bs-theme='dark']"]['.mjx-dialog a[href]:visited'] =
23+
DraggableDialog.styles["[data-bs-theme='dark']"]['a[href]:visited'];
24+
delete DraggableDialog.styles["[data-bs-theme='dark']"]['a[href]:visited'];
25+
}
26+
],
27+
[
28+
MathJax._.a11y?.explorer,
29+
'a11y/explorer',
30+
() => {
31+
const Region = MathJax._.a11y.explorer.Region;
32+
for (const region of ['LiveRegion', 'HoverRegion', 'ToolTip']) {
33+
if (':root' in Region[region].style.styles) {
34+
Region[region].style.styles["[data-bs-theme='light']"] = Region[region].style.styles[':root'];
35+
36+
// The variable --mjx-bg1-color is defined to be 'rgba(var(--mjx-bg-blue), var(--mjx-bg-alpha))'.
37+
// I suspect this is a typo as the variable -mjx-bg-alpha is not defined anywhere. In any case this
38+
// change is needed to get the correct background color on the focused element in the explorer.
39+
Region[region].style.styles["[data-bs-theme='light']"]['--mjx-bg1-color'] =
40+
'rgba(var(--mjx-bg-blue), var(--mjx-bg1-alpha))';
41+
}
42+
Region[region].style.styles["[data-bs-theme='dark']"] =
43+
Region[region].style.styles['@media (prefers-color-scheme: dark)'];
44+
if (':root' in Region[region].style.styles["[data-bs-theme='dark']"]) {
45+
Object.assign(
46+
Region[region].style.styles["[data-bs-theme='dark']"],
47+
Region[region].style.styles["[data-bs-theme='dark']"][':root']
48+
);
49+
delete Region[region].style.styles["[data-bs-theme='dark']"][':root'];
50+
}
51+
Region[region].style.styles['@media (prefers-color-scheme: dark)'] = {};
52+
}
53+
Region.LiveRegion.style.styles['@media (prefers-color-scheme: dark)']['mjx-ignore'] = { ignore: 1 };
54+
MathJax.startup.extendHandler((handler) => {
55+
switchToBSStyle(
56+
handler.documentClass.speechStyles,
57+
'@media (prefers-color-scheme: dark) /* explorer */'
58+
);
59+
return handler;
60+
});
61+
}
62+
],
63+
[
64+
MathJax._.output?.chtml,
65+
'output/chtml',
66+
() => {
67+
const { CHTML } = MathJax._.output.chtml_ts;
68+
switchToBSStyle(CHTML);
69+
const { ChtmlMaction } = MathJax._.output.chtml.Wrappers.maction;
70+
switchToBSStyle(ChtmlMaction.styles, '@media (prefers-color-scheme: dark) /* chtml maction */');
71+
}
72+
],
73+
[
74+
MathJax._.output?.svg,
75+
'output/svg',
76+
() => {
77+
const { SVG } = MathJax._.output.svg_ts;
78+
switchToBSStyle(SVG.commonStyles);
79+
const { SvgMaction } = MathJax._.output.svg.Wrappers.maction;
80+
switchToBSStyle(SvgMaction.styles, '@media (prefers-color-scheme: dark) /* svg maction */');
81+
}
82+
]
83+
]) {
84+
if (immediate) {
85+
ready();
86+
} else {
87+
const config = MathJax.config.loader;
88+
config[extension] ??= {};
89+
config[extension].extraLoads ??= [];
90+
const check = config[extension].checkReady;
91+
config[extension].checkReady = async () => {
92+
if (check) await check();
93+
return ready();
94+
};
95+
}
96+
}

htdocs/js/MathJaxConfig/mathjax-config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ if (!window.MathJax) {
22
window.MathJax = {
33
tex: { packages: { '[+]': webworkConfig?.showMathJaxErrors ? [] : ['noerrors'] } },
44
loader: {
5-
load: ['input/asciimath', '[tex]/noerrors', '[no-dark-mode]'],
6-
paths: { 'no-dark-mode': webworkConfig?.mathJaxDarkModeUrl ?? './no-dark-mode.js' }
5+
load: ['input/asciimath', '[tex]/noerrors', '[bs-color-scheme]'],
6+
paths: { 'bs-color-scheme': webworkConfig?.mathJaxBSColorSchemeUrl ?? './bs-color-scheme.js' }
77
},
88
startup: {
99
ready() {

htdocs/js/MathJaxConfig/no-dark-mode.js

Lines changed: 0 additions & 63 deletions
This file was deleted.

htdocs/js/PGCodeMirror/pgeditor.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.code-mirror-editor {
2-
border: 1px solid #ddd;
2+
border: 1px solid var(--ww-layout-border-color, #ddd);
33
min-height: 400px;
44
overflow: auto;
55
resize: vertical;
@@ -25,7 +25,7 @@
2525

2626
// This style is used if the CodeMirror editor is disabled in localOverrides.conf.
2727
.text-area-editor {
28-
border: 1px solid #ddd;
28+
border: 1px solid var(--ww-layout-border-color, #ddd);
2929
padding: 2px;
3030
height: 550px;
3131
min-height: 400px;

htdocs/js/PGProblemEditor/pgproblemeditor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@
423423
const iframe = document.createElement('iframe');
424424
iframe.title = 'Rendered content';
425425
iframe.id = 'pgedit-render-iframe';
426+
iframe.style.colorScheme = 'light';
426427

427428
// Adjust the height of the iframe when the window is resized and when the iframe loads.
428429
const adjustIFrameHeight = () => {

htdocs/js/RenderProblem/renderproblem.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
iframe = document.createElement('iframe');
7070
iframe.id = `${renderArea.id}_iframe`;
7171
iframe.style.border = 'none';
72+
iframe.style.colorScheme = 'light';
7273
while (renderArea.firstChild) renderArea.firstChild.remove();
7374
renderArea.append(iframe);
7475

htdocs/js/System/color-scheme.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict';
2+
3+
(() => {
4+
const getPreferredTheme = () => {
5+
const storedTheme = localStorage.getItem('WW.color-scheme');
6+
if (storedTheme) return storedTheme;
7+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
8+
};
9+
10+
let flatpickrDarkTheme;
11+
12+
const setTheme = (theme) => {
13+
const themeValue =
14+
theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme;
15+
document.documentElement.setAttribute('data-bs-theme', themeValue);
16+
17+
if (!flatpickrDarkTheme) flatpickrDarkTheme = document.getElementById('flatpickr-dark-theme');
18+
if (flatpickrDarkTheme) {
19+
if (themeValue === 'dark') document.head.append(flatpickrDarkTheme);
20+
else flatpickrDarkTheme.remove();
21+
}
22+
};
23+
24+
setTheme(getPreferredTheme());
25+
26+
const showActiveTheme = (theme, focus = false) => {
27+
const themeSwitcher = document.getElementById('color-scheme-chooser');
28+
if (!themeSwitcher) return;
29+
30+
const activeThemeIcon = themeSwitcher.querySelector('.theme-icon-active');
31+
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
32+
33+
for (const element of document.querySelectorAll('[data-bs-theme-value]')) {
34+
element.classList.remove('active');
35+
element.setAttribute('aria-pressed', 'false');
36+
}
37+
38+
btnToActive.classList.add('active');
39+
btnToActive.setAttribute('aria-pressed', 'true');
40+
activeThemeIcon.classList.remove('fa-sun', 'fa-moon', 'fa-circle-half-stroke');
41+
activeThemeIcon.classList.add(
42+
theme === 'light' ? 'fa-sun' : theme === 'dark' ? 'fa-moon' : 'fa-circle-half-stroke'
43+
);
44+
themeSwitcher.setAttribute(
45+
'aria-label',
46+
`${themeSwitcher.title} (${
47+
themeSwitcher.dataset[`${btnToActive.dataset.bsThemeValue}Text`] ?? btnToActive.dataset.bsThemeValue
48+
})`
49+
);
50+
51+
if (focus) themeSwitcher.focus();
52+
};
53+
54+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
55+
const storedTheme = localStorage.getItem('WW.color-scheme');
56+
if (storedTheme !== 'light' && storedTheme !== 'dark') {
57+
const preferredTheme = getPreferredTheme();
58+
setTheme(preferredTheme);
59+
showActiveTheme(preferredTheme);
60+
}
61+
});
62+
63+
window.addEventListener('DOMContentLoaded', () => {
64+
showActiveTheme(getPreferredTheme());
65+
66+
for (const toggle of document.querySelectorAll('[data-bs-theme-value]')) {
67+
toggle.addEventListener('click', () => {
68+
const theme = toggle.getAttribute('data-bs-theme-value');
69+
localStorage.setItem('WW.color-scheme', theme);
70+
setTheme(theme);
71+
showActiveTheme(theme, true);
72+
});
73+
}
74+
});
75+
})();

0 commit comments

Comments
 (0)