aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJean-Nicolas Veigel <art.jnveigel@gmail.com>2024-03-17 18:35:48 +0100
committerLouis Pilfold <louis@lpil.uk>2024-03-26 10:31:25 +0000
commitbf5e64da6c7f2e79d853b956d1842a1c45dae91b (patch)
tree9a36065d287a77340550c6aed7a60a8fb900b430 /src
parenta2c2f131b42b75b70f730da20d9b1aba9be68be2 (diff)
downloadtour-bf5e64da6c7f2e79d853b956d1842a1c45dae91b.tar.gz
tour-bf5e64da6c7f2e79d853b956d1842a1c45dae91b.zip
fix: FOUC on /everyting when on dark theme
Diffstat (limited to 'src')
-rw-r--r--src/tour.gleam6
-rw-r--r--src/tour/render.gleam2
-rw-r--r--src/tour/widgets.gleam100
3 files changed, 71 insertions, 37 deletions
diff --git a/src/tour.gleam b/src/tour.gleam
index 96136bd..1001252 100644
--- a/src/tour.gleam
+++ b/src/tour.gleam
@@ -609,11 +609,7 @@ fn lesson_page_render(lesson: Lesson) -> String {
],
scripts: ScriptConfig(
body: [
- render.dangerous_inline_script(
- widgets.theme_picker_js,
- render.ScriptOptions(module: True, defer: False),
- [],
- ),
+ widgets.theme_picker_script(),
h("script", [#("type", "gleam"), #("id", "code")], [
htmb.dangerous_unescaped_fragment(string_builder.from_string(
lesson.code,
diff --git a/src/tour/render.gleam b/src/tour/render.gleam
index 8aeb29c..3502f6e 100644
--- a/src/tour/render.gleam
+++ b/src/tour/render.gleam
@@ -200,7 +200,7 @@ pub fn render_html(page config: PageConfig) -> Html {
],
),
lang: "en-GB",
- attributes: [#("class", "theme-light")],
+ attributes: [#("class", "theme-light theme-init")],
body: BodyConfig(
attributes: [body_class],
scripts: config.scripts.body,
diff --git a/src/tour/widgets.gleam b/src/tour/widgets.gleam
index 774c76f..c475a03 100644
--- a/src/tour/widgets.gleam
+++ b/src/tour/widgets.gleam
@@ -91,48 +91,86 @@ pub fn theme_picker() -> Html {
// This script is inlined in the response to avoid FOUC when applying the theme
pub const theme_picker_js = "
-const mediaPrefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)')
+const mediaPrefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)');
+const themeStorageKey = 'theme';
-function selectTheme(selectedTheme) {
- // Apply and remember the specified theme.
- applyTheme(selectedTheme)
- if ((selectedTheme === 'dark') === mediaPrefersDarkTheme.matches) {
+function getPreferredTheme() {
+ return mediaPrefersDarkTheme.matches ? 'dark' : 'light';
+}
+
+function getAppliedTheme() {
+ return document.documentElement.classList.contains('theme-dark')
+ ? 'dark'
+ : 'light';
+}
+
+function getStoredTheme() {
+ return localStorage.getItem(themeStorageKey);
+}
+
+function storeTheme(selectedTheme) {
+ localStorage.setItem(themeStorageKey, selectedTheme);
+}
+
+function syncStoredTheme(theme) {
+ if (theme === getPreferredTheme()) {
// Selected theme is the same as the device's preferred theme, so we can forget this setting.
- localStorage.removeItem('theme')
+ localStorage.removeItem(themeStorageKey);
} else {
// Remember the selected theme to apply it on the next visit
- localStorage.setItem('theme', selectedTheme)
+ storeTheme(theme);
}
}
-function applyTheme(theme) {
- document.documentElement.classList.toggle('theme-dark', theme === 'dark')
- document.documentElement.classList.toggle('theme-light', theme !== 'dark')
+function applyTheme(theme, initial = false) {
+ // abort if theme is already applied
+ if (theme === getAppliedTheme()) return;
+ // apply theme css class
+ document.documentElement.classList.toggle('theme-dark', theme === 'dark');
+ document.documentElement.classList.toggle('theme-light', theme !== 'dark');
}
-// If user had selected a theme, load it. Otherwise, use device's preferred theme
-const selectedTheme = localStorage.getItem('theme')
-if (selectedTheme) {
- applyTheme(selectedTheme)
-} else {
- applyTheme(mediaPrefersDarkTheme.matches ? 'dark' : 'light')
+function setTheme(theme) {
+ syncStoredTheme(theme);
+ applyTheme(theme);
}
-// Watch the device's preferred theme and update theme if user did not select a theme
-mediaPrefersDarkTheme.addEventListener('change', () => {
- const selectedTheme = localStorage.getItem('theme')
- if (!selectedTheme) {
- applyTheme(mediaPrefersDarkTheme.matches ? 'dark' : 'light')
- }
-})
-
-// Add handlers for theme selection buttons.
-document.querySelector('[data-light-theme-toggle]').addEventListener('click', () => {
- selectTheme('light')
-})
-document.querySelector('[data-dark-theme-toggle]').addEventListener('click', () => {
- selectTheme('dark')
-})
+function toggleTheme() {
+ setTheme(getAppliedTheme() === 'dark' ? 'light' : 'dark');
+}
+
+function reEnableTransitions() {
+ // re-enable transitions when triggered after first-render to avoid fouc
+ // setTimeout(fn, 0) needed to give CSS at lease 1 frame without transitions and thus avoid FOUC
+ setTimeout(() => {
+ // executed after css has loaded & theme swiching has occured
+ document.documentElement.classList.toggle('theme-init', false);
+ }, 0);
+}
+
+function initThemeEvents() {
+ // Watch the device's preferred theme and update theme if user did not select a theme
+ mediaPrefersDarkTheme.addEventListener('change', () => {
+ // abort if the user already selected a theme
+ if (!!getStoredTheme()) return;
+ // update applied theme accordingly
+ applyTheme(getPreferredTheme());
+ });
+ // Add handlers for theme selection button
+ document
+ .querySelector('.theme-picker')
+ ?.addEventListener('click', toggleTheme);
+ // Re-enable transitons only when all content has loaded
+ window.addEventListener('DOMContentLoaded', reEnableTransitions);
+}
+
+function initTheme() {
+ // apply stored or preferred theme
+ applyTheme(getStoredTheme() ?? getPreferredTheme());
+ initThemeEvents();
+}
+
+initTheme();
"
pub fn theme_picker_script() -> Html {