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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions hypha/static_src/javascript/behaviours/theme-toggle.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,58 @@
let prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;

/**
* Read the stored theme preference.
*
* Storage can be unavailable (blocked cookies, some private browsing modes)
* and then throws. This script runs blocking in <head>, so an uncaught error
* would leave the page with no theme applied at all.
*
* @returns {string|null} "light", "dark", "auto", or null if nothing is stored.
*/
function getStoredTheme() {
try {
return localStorage.getItem("theme");
} catch (_e) {
return null;
}
}

/**
* Persist the theme preference, ignoring unavailable storage.
*
* @param {string} mode - "light", "dark" or "auto".
* @returns {boolean} Whether the preference could be stored.
*/
function storeTheme(mode) {
try {
localStorage.setItem("theme", mode);
return true;
} catch (_e) {
return false;
}
}

function setTheme(mode) {
if (mode !== "light" && mode !== "dark" && mode !== "auto") {
console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`);
mode = "auto";
}
document.documentElement.dataset.theme = mode;
localStorage.setItem("theme", mode);

// daisyUI applies the dark theme through `:root:not([data-theme])` inside a
// prefers-color-scheme media query, so auto mode has to leave the attribute
// off entirely. Setting data-theme="auto" matches no theme and silently
// falls back to light.
if (mode === "auto") {
delete document.documentElement.dataset.theme;
} else {
document.documentElement.dataset.theme = mode;
}

storeTheme(mode);
}

function cycleTheme() {
const currentTheme = localStorage.getItem("theme") || "auto";
const currentTheme = getStoredTheme() || "auto";

if (prefersDark) {
// Auto (dark) -> Light -> Dark
Expand All @@ -35,7 +77,7 @@ function cycleTheme() {

function initTheme() {
// set theme defined in localStorage if there is one, or fallback to auto mode
const currentTheme = localStorage.getItem("theme");
const currentTheme = getStoredTheme();
currentTheme ? setTheme(currentTheme) : setTheme("auto");
}

Expand Down
21 changes: 17 additions & 4 deletions hypha/static_src/javascript/tinymce-dark-mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@
});
});

// The theme toggle sets data-theme on <html>. It also re-sets the attribute
// when the OS preference changes while in "auto" mode, so this covers both.
new MutationObserver(function () {
/**
* Point every editor, current and future, at the stylesheet for the theme
* that is now in effect.
*/
function syncContentCss() {
const name = contentCss();

// Editors created from here on, e.g. by HTMX swapping in a new form.
Expand All @@ -69,8 +71,19 @@
for (const editor of tinymce.get() ?? []) {
swapContentCss(editor, name);
}
}).observe(document.documentElement, {
}

// The theme toggle sets data-theme on <html> for the light and dark modes,
// and removes it again for auto mode. Attribute removal is an attribute
// mutation too, so both directions are covered.
new MutationObserver(syncContentCss).observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});

// In auto mode there is no data-theme attribute, so an OS preference change
// repaints the page without mutating anything the observer above watches.
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", syncContentCss);
})();
4 changes: 2 additions & 2 deletions hypha/static_src/tailwind/components/theme-toggle.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
display: none;
}

html[data-theme="auto"] .theme-toggle svg.theme-icon-when-auto {
html:not([data-theme]) .theme-toggle svg.theme-icon-when-auto {
display: block;
}

Expand All @@ -18,7 +18,7 @@ html[data-theme="light"] .theme-toggle svg.theme-icon-when-light {
display: none;
}

html[data-theme="auto"] .theme-toggle .theme-label-when-auto {
html:not([data-theme]) .theme-toggle .theme-label-when-auto {
display: block;
}

Expand Down
15 changes: 15 additions & 0 deletions hypha/static_src/tailwind/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@
}
@import "./base/themes.css";

/* Tailwind's stock `dark:` variant keys off prefers-color-scheme, which ignores
the theme toggle. Follow the daisyUI themes instead: an explicit
data-theme="dark", or auto mode (no data-theme at all) on a dark OS. */
@custom-variant dark {
&:where([data-theme="dark"], [data-theme="dark"] *) {
@slot;
}

@media (prefers-color-scheme: dark) {
&:where(html:not([data-theme]), html:not([data-theme]) *) {
@slot;
}
}
}

@theme {
--default-border-width: var(--border);

Expand Down
6 changes: 1 addition & 5 deletions hypha/templates/base-apply.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,12 @@
{% block user_menu %}
<div class="flex gap-2 print-hidden">

<button class="theme-toggle btn btn-circle btn-soft btn-secondary" data-tippy-content="{% trans 'Toggle color theme' %}">
<button type="button" class="theme-toggle btn btn-circle btn-soft btn-secondary" data-tippy-content="{% trans 'Toggle color theme' %}">
<div class="sr-only theme-label-when-auto">{% trans "Toggle theme" %} ({% trans "current theme" %}: auto)</div>
<div class="sr-only theme-label-when-light">{% trans "Toggle theme" %} ({% trans "current theme" %}: light)</div>
<div class="sr-only theme-label-when-dark">{% trans "Toggle theme" %} ({% trans "current theme" %}: dark)</div>


<div class="sr-only">{% trans "Toggle Light / Dark / Auto color theme" %}</div>

<svg aria-hidden="true" class="w-5 h-5 theme-icon-when-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<!-- <path d="M0 0h24v24H0z" fill="currentColor"></path> -->
<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2V4a8 8 0 1 0 0 16z"></path>
</svg>
{% heroicon_solid "moon" class="w-5 h-5 theme-icon-when-dark" aria_hidden="true" %}
Expand Down