diff options
-rw-r--r-- | src/tour.gleam | 6 | ||||
-rw-r--r-- | src/tour/render.gleam | 2 | ||||
-rw-r--r-- | src/tour/widgets.gleam | 100 | ||||
-rw-r--r-- | static/css/pages/lesson.css | 0 | ||||
-rw-r--r-- | static/css/theme.css | 25 |
5 files changed, 96 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 { diff --git a/static/css/pages/lesson.css b/static/css/pages/lesson.css new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/static/css/pages/lesson.css diff --git a/static/css/theme.css b/static/css/theme.css index c098cfa..a85ccb1 100644 --- a/static/css/theme.css +++ b/static/css/theme.css @@ -52,3 +52,28 @@ html.theme-dark { --color-text-accent: var(--color-accent); color-scheme: dark; } + +/* Hide body while theme init is occurring (50ms) */ +html.theme-init body { + opacity: 0 +} + +/* +removes all transitions in the page during first theme render to avoid FOUC +(the theme-init class is removed from DOM 1 frame after the page content loads) +*/ +html.theme-init * { + transition: none !important; +} + +body { + opacity: 1; + transition: opacity 300ms ease-out 0s; +} + +html * { + transition-duration: 150ms, 300ms; + transition-property: color, background; + transition-timing-function: ease-out; + transition-delay: 0; +}
\ No newline at end of file |