diff options
author | Jean-Nicolas Veigel <art.jnveigel@gmail.com> | 2024-03-17 18:35:48 +0100 |
---|---|---|
committer | Louis Pilfold <louis@lpil.uk> | 2024-03-26 10:31:25 +0000 |
commit | bf5e64da6c7f2e79d853b956d1842a1c45dae91b (patch) | |
tree | 9a36065d287a77340550c6aed7a60a8fb900b430 /src | |
parent | a2c2f131b42b75b70f730da20d9b1aba9be68be2 (diff) | |
download | tour-bf5e64da6c7f2e79d853b956d1842a1c45dae91b.tar.gz tour-bf5e64da6c7f2e79d853b956d1842a1c45dae91b.zip |
fix: FOUC on /everyting when on dark theme
Diffstat (limited to 'src')
-rw-r--r-- | src/tour.gleam | 6 | ||||
-rw-r--r-- | src/tour/render.gleam | 2 | ||||
-rw-r--r-- | src/tour/widgets.gleam | 100 |
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 { |