Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/theme-client-router.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"fulldev-ui": patch
---

`ThemeProvider` keeps the theme across Astro `<ClientRouter />` navigation. Before, each navigation briefly showed the light theme and saved it as the visitor's choice, so a dark theme was lost.
2 changes: 1 addition & 1 deletion public/r/styles/base-luma/theme-toggle.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
{
"path": "src/components/ui/theme-toggle/theme-provider.astro",
"content": "<script is:inline>\n // A stored \"dark\" or \"light\" is the visitor's choice. Without one, such as\n // no value or \"system\", the theme follows the system.\n const getStoredTheme = () => {\n if (typeof localStorage === \"undefined\") return null\n const theme = localStorage.getItem(\"theme\")\n return theme === \"dark\" || theme === \"light\" ? theme : null\n }\n\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\")\n\n const getThemePreference = () =>\n getStoredTheme() ?? (media.matches ? \"dark\" : \"light\")\n\n const disableTransitionsTemporarily = () => {\n const style = document.createElement(\"style\")\n\n style.appendChild(\n document.createTextNode(\n \"*, *::before, *::after { transition: none !important; }\"\n )\n )\n\n document.head.appendChild(style)\n\n return () => {\n void document.documentElement.offsetHeight\n\n requestAnimationFrame(() => {\n style.remove()\n })\n }\n }\n\n // The theme this provider applied last. A class change to another theme\n // comes from outside, such as a toggle without the provider, and is saved\n // as the visitor's choice.\n let appliedTheme\n\n const applyTheme = (theme, disableTransitions = false) => {\n const restoreTransitions = disableTransitions\n ? disableTransitionsTemporarily()\n : null\n\n appliedTheme = theme\n document.documentElement.classList[theme === \"dark\" ? \"add\" : \"remove\"](\n \"dark\"\n )\n\n restoreTransitions?.()\n }\n\n // \"dark\" or \"light\" saves a choice; \"system\" follows the system again.\n window.__setTheme = (theme) => {\n if (typeof localStorage !== \"undefined\")\n localStorage.setItem(\"theme\", theme)\n applyTheme(getThemePreference(), true)\n }\n\n applyTheme(getThemePreference())\n\n // Follow the system while there is no choice, and a choice made in\n // another window or frame of the site.\n media.addEventListener(\"change\", () => {\n if (!getStoredTheme()) applyTheme(getThemePreference(), true)\n })\n window.addEventListener(\"storage\", (event) => {\n if (event.storageArea !== localStorage) return\n if (event.key === \"theme\" || event.key === null)\n applyTheme(getThemePreference(), true)\n })\n\n if (typeof localStorage !== \"undefined\") {\n const observer = new MutationObserver(() => {\n const theme = document.documentElement.classList.contains(\"dark\")\n ? \"dark\"\n : \"light\"\n if (theme === appliedTheme) return\n appliedTheme = theme\n localStorage.setItem(\"theme\", theme)\n })\n\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n })\n }\n</script>\n",
"content": "<script is:inline>\n // A stored \"dark\" or \"light\" is the visitor's choice. Without one, such as\n // no value or \"system\", the theme follows the system.\n const getStoredTheme = () => {\n if (typeof localStorage === \"undefined\") return null\n const theme = localStorage.getItem(\"theme\")\n return theme === \"dark\" || theme === \"light\" ? theme : null\n }\n\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\")\n\n const getThemePreference = () =>\n getStoredTheme() ?? (media.matches ? \"dark\" : \"light\")\n\n const disableTransitionsTemporarily = () => {\n const style = document.createElement(\"style\")\n\n style.appendChild(\n document.createTextNode(\n \"*, *::before, *::after { transition: none !important; }\"\n )\n )\n\n document.head.appendChild(style)\n\n return () => {\n void document.documentElement.offsetHeight\n\n requestAnimationFrame(() => {\n style.remove()\n })\n }\n }\n\n // The theme this provider applied last. A class change to another theme\n // comes from outside, such as a toggle without the provider, and is saved\n // as the visitor's choice.\n let appliedTheme\n\n const applyTheme = (theme, disableTransitions = false) => {\n const restoreTransitions = disableTransitions\n ? disableTransitionsTemporarily()\n : null\n\n appliedTheme = theme\n document.documentElement.classList[theme === \"dark\" ? \"add\" : \"remove\"](\n \"dark\"\n )\n\n restoreTransitions?.()\n }\n\n // \"dark\" or \"light\" saves a choice; \"system\" follows the system again.\n window.__setTheme = (theme) => {\n if (typeof localStorage !== \"undefined\")\n localStorage.setItem(\"theme\", theme)\n applyTheme(getThemePreference(), true)\n }\n\n applyTheme(getThemePreference())\n\n // Follow the system while there is no choice, and a choice made in\n // another window or frame of the site.\n media.addEventListener(\"change\", () => {\n if (!getStoredTheme()) applyTheme(getThemePreference(), true)\n })\n window.addEventListener(\"storage\", (event) => {\n if (event.storageArea !== localStorage) return\n if (event.key === \"theme\" || event.key === null)\n applyTheme(getThemePreference(), true)\n })\n\n // Astro's <ClientRouter /> replaces the <html> attributes with those of the\n // next page, which cannot know the theme. Carry the theme over before the\n // swap, so it neither flashes nor reaches the observer below as a choice.\n document.addEventListener(\"astro:before-swap\", (event) => {\n const swap = event.swap\n event.swap = function (...args) {\n event.newDocument.documentElement.classList.toggle(\n \"dark\",\n appliedTheme === \"dark\"\n )\n return swap.apply(this, args)\n }\n })\n\n if (typeof localStorage !== \"undefined\") {\n const observer = new MutationObserver(() => {\n const theme = document.documentElement.classList.contains(\"dark\")\n ? \"dark\"\n : \"light\"\n if (theme === appliedTheme) return\n appliedTheme = theme\n localStorage.setItem(\"theme\", theme)\n })\n\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n })\n }\n</script>\n",
"type": "registry:ui"
},
{
Expand Down
2 changes: 1 addition & 1 deletion public/r/styles/base-lyra/theme-toggle.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
{
"path": "src/components/ui/theme-toggle/theme-provider.astro",
"content": "<script is:inline>\n // A stored \"dark\" or \"light\" is the visitor's choice. Without one, such as\n // no value or \"system\", the theme follows the system.\n const getStoredTheme = () => {\n if (typeof localStorage === \"undefined\") return null\n const theme = localStorage.getItem(\"theme\")\n return theme === \"dark\" || theme === \"light\" ? theme : null\n }\n\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\")\n\n const getThemePreference = () =>\n getStoredTheme() ?? (media.matches ? \"dark\" : \"light\")\n\n const disableTransitionsTemporarily = () => {\n const style = document.createElement(\"style\")\n\n style.appendChild(\n document.createTextNode(\n \"*, *::before, *::after { transition: none !important; }\"\n )\n )\n\n document.head.appendChild(style)\n\n return () => {\n void document.documentElement.offsetHeight\n\n requestAnimationFrame(() => {\n style.remove()\n })\n }\n }\n\n // The theme this provider applied last. A class change to another theme\n // comes from outside, such as a toggle without the provider, and is saved\n // as the visitor's choice.\n let appliedTheme\n\n const applyTheme = (theme, disableTransitions = false) => {\n const restoreTransitions = disableTransitions\n ? disableTransitionsTemporarily()\n : null\n\n appliedTheme = theme\n document.documentElement.classList[theme === \"dark\" ? \"add\" : \"remove\"](\n \"dark\"\n )\n\n restoreTransitions?.()\n }\n\n // \"dark\" or \"light\" saves a choice; \"system\" follows the system again.\n window.__setTheme = (theme) => {\n if (typeof localStorage !== \"undefined\")\n localStorage.setItem(\"theme\", theme)\n applyTheme(getThemePreference(), true)\n }\n\n applyTheme(getThemePreference())\n\n // Follow the system while there is no choice, and a choice made in\n // another window or frame of the site.\n media.addEventListener(\"change\", () => {\n if (!getStoredTheme()) applyTheme(getThemePreference(), true)\n })\n window.addEventListener(\"storage\", (event) => {\n if (event.storageArea !== localStorage) return\n if (event.key === \"theme\" || event.key === null)\n applyTheme(getThemePreference(), true)\n })\n\n if (typeof localStorage !== \"undefined\") {\n const observer = new MutationObserver(() => {\n const theme = document.documentElement.classList.contains(\"dark\")\n ? \"dark\"\n : \"light\"\n if (theme === appliedTheme) return\n appliedTheme = theme\n localStorage.setItem(\"theme\", theme)\n })\n\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n })\n }\n</script>\n",
"content": "<script is:inline>\n // A stored \"dark\" or \"light\" is the visitor's choice. Without one, such as\n // no value or \"system\", the theme follows the system.\n const getStoredTheme = () => {\n if (typeof localStorage === \"undefined\") return null\n const theme = localStorage.getItem(\"theme\")\n return theme === \"dark\" || theme === \"light\" ? theme : null\n }\n\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\")\n\n const getThemePreference = () =>\n getStoredTheme() ?? (media.matches ? \"dark\" : \"light\")\n\n const disableTransitionsTemporarily = () => {\n const style = document.createElement(\"style\")\n\n style.appendChild(\n document.createTextNode(\n \"*, *::before, *::after { transition: none !important; }\"\n )\n )\n\n document.head.appendChild(style)\n\n return () => {\n void document.documentElement.offsetHeight\n\n requestAnimationFrame(() => {\n style.remove()\n })\n }\n }\n\n // The theme this provider applied last. A class change to another theme\n // comes from outside, such as a toggle without the provider, and is saved\n // as the visitor's choice.\n let appliedTheme\n\n const applyTheme = (theme, disableTransitions = false) => {\n const restoreTransitions = disableTransitions\n ? disableTransitionsTemporarily()\n : null\n\n appliedTheme = theme\n document.documentElement.classList[theme === \"dark\" ? \"add\" : \"remove\"](\n \"dark\"\n )\n\n restoreTransitions?.()\n }\n\n // \"dark\" or \"light\" saves a choice; \"system\" follows the system again.\n window.__setTheme = (theme) => {\n if (typeof localStorage !== \"undefined\")\n localStorage.setItem(\"theme\", theme)\n applyTheme(getThemePreference(), true)\n }\n\n applyTheme(getThemePreference())\n\n // Follow the system while there is no choice, and a choice made in\n // another window or frame of the site.\n media.addEventListener(\"change\", () => {\n if (!getStoredTheme()) applyTheme(getThemePreference(), true)\n })\n window.addEventListener(\"storage\", (event) => {\n if (event.storageArea !== localStorage) return\n if (event.key === \"theme\" || event.key === null)\n applyTheme(getThemePreference(), true)\n })\n\n // Astro's <ClientRouter /> replaces the <html> attributes with those of the\n // next page, which cannot know the theme. Carry the theme over before the\n // swap, so it neither flashes nor reaches the observer below as a choice.\n document.addEventListener(\"astro:before-swap\", (event) => {\n const swap = event.swap\n event.swap = function (...args) {\n event.newDocument.documentElement.classList.toggle(\n \"dark\",\n appliedTheme === \"dark\"\n )\n return swap.apply(this, args)\n }\n })\n\n if (typeof localStorage !== \"undefined\") {\n const observer = new MutationObserver(() => {\n const theme = document.documentElement.classList.contains(\"dark\")\n ? \"dark\"\n : \"light\"\n if (theme === appliedTheme) return\n appliedTheme = theme\n localStorage.setItem(\"theme\", theme)\n })\n\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n })\n }\n</script>\n",
"type": "registry:ui"
},
{
Expand Down
2 changes: 1 addition & 1 deletion public/r/styles/base-maia/theme-toggle.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
},
{
"path": "src/components/ui/theme-toggle/theme-provider.astro",
"content": "<script is:inline>\n // A stored \"dark\" or \"light\" is the visitor's choice. Without one, such as\n // no value or \"system\", the theme follows the system.\n const getStoredTheme = () => {\n if (typeof localStorage === \"undefined\") return null\n const theme = localStorage.getItem(\"theme\")\n return theme === \"dark\" || theme === \"light\" ? theme : null\n }\n\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\")\n\n const getThemePreference = () =>\n getStoredTheme() ?? (media.matches ? \"dark\" : \"light\")\n\n const disableTransitionsTemporarily = () => {\n const style = document.createElement(\"style\")\n\n style.appendChild(\n document.createTextNode(\n \"*, *::before, *::after { transition: none !important; }\"\n )\n )\n\n document.head.appendChild(style)\n\n return () => {\n void document.documentElement.offsetHeight\n\n requestAnimationFrame(() => {\n style.remove()\n })\n }\n }\n\n // The theme this provider applied last. A class change to another theme\n // comes from outside, such as a toggle without the provider, and is saved\n // as the visitor's choice.\n let appliedTheme\n\n const applyTheme = (theme, disableTransitions = false) => {\n const restoreTransitions = disableTransitions\n ? disableTransitionsTemporarily()\n : null\n\n appliedTheme = theme\n document.documentElement.classList[theme === \"dark\" ? \"add\" : \"remove\"](\n \"dark\"\n )\n\n restoreTransitions?.()\n }\n\n // \"dark\" or \"light\" saves a choice; \"system\" follows the system again.\n window.__setTheme = (theme) => {\n if (typeof localStorage !== \"undefined\")\n localStorage.setItem(\"theme\", theme)\n applyTheme(getThemePreference(), true)\n }\n\n applyTheme(getThemePreference())\n\n // Follow the system while there is no choice, and a choice made in\n // another window or frame of the site.\n media.addEventListener(\"change\", () => {\n if (!getStoredTheme()) applyTheme(getThemePreference(), true)\n })\n window.addEventListener(\"storage\", (event) => {\n if (event.storageArea !== localStorage) return\n if (event.key === \"theme\" || event.key === null)\n applyTheme(getThemePreference(), true)\n })\n\n if (typeof localStorage !== \"undefined\") {\n const observer = new MutationObserver(() => {\n const theme = document.documentElement.classList.contains(\"dark\")\n ? \"dark\"\n : \"light\"\n if (theme === appliedTheme) return\n appliedTheme = theme\n localStorage.setItem(\"theme\", theme)\n })\n\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n })\n }\n</script>\n",
"content": "<script is:inline>\n // A stored \"dark\" or \"light\" is the visitor's choice. Without one, such as\n // no value or \"system\", the theme follows the system.\n const getStoredTheme = () => {\n if (typeof localStorage === \"undefined\") return null\n const theme = localStorage.getItem(\"theme\")\n return theme === \"dark\" || theme === \"light\" ? theme : null\n }\n\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\")\n\n const getThemePreference = () =>\n getStoredTheme() ?? (media.matches ? \"dark\" : \"light\")\n\n const disableTransitionsTemporarily = () => {\n const style = document.createElement(\"style\")\n\n style.appendChild(\n document.createTextNode(\n \"*, *::before, *::after { transition: none !important; }\"\n )\n )\n\n document.head.appendChild(style)\n\n return () => {\n void document.documentElement.offsetHeight\n\n requestAnimationFrame(() => {\n style.remove()\n })\n }\n }\n\n // The theme this provider applied last. A class change to another theme\n // comes from outside, such as a toggle without the provider, and is saved\n // as the visitor's choice.\n let appliedTheme\n\n const applyTheme = (theme, disableTransitions = false) => {\n const restoreTransitions = disableTransitions\n ? disableTransitionsTemporarily()\n : null\n\n appliedTheme = theme\n document.documentElement.classList[theme === \"dark\" ? \"add\" : \"remove\"](\n \"dark\"\n )\n\n restoreTransitions?.()\n }\n\n // \"dark\" or \"light\" saves a choice; \"system\" follows the system again.\n window.__setTheme = (theme) => {\n if (typeof localStorage !== \"undefined\")\n localStorage.setItem(\"theme\", theme)\n applyTheme(getThemePreference(), true)\n }\n\n applyTheme(getThemePreference())\n\n // Follow the system while there is no choice, and a choice made in\n // another window or frame of the site.\n media.addEventListener(\"change\", () => {\n if (!getStoredTheme()) applyTheme(getThemePreference(), true)\n })\n window.addEventListener(\"storage\", (event) => {\n if (event.storageArea !== localStorage) return\n if (event.key === \"theme\" || event.key === null)\n applyTheme(getThemePreference(), true)\n })\n\n // Astro's <ClientRouter /> replaces the <html> attributes with those of the\n // next page, which cannot know the theme. Carry the theme over before the\n // swap, so it neither flashes nor reaches the observer below as a choice.\n document.addEventListener(\"astro:before-swap\", (event) => {\n const swap = event.swap\n event.swap = function (...args) {\n event.newDocument.documentElement.classList.toggle(\n \"dark\",\n appliedTheme === \"dark\"\n )\n return swap.apply(this, args)\n }\n })\n\n if (typeof localStorage !== \"undefined\") {\n const observer = new MutationObserver(() => {\n const theme = document.documentElement.classList.contains(\"dark\")\n ? \"dark\"\n : \"light\"\n if (theme === appliedTheme) return\n appliedTheme = theme\n localStorage.setItem(\"theme\", theme)\n })\n\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\"],\n })\n }\n</script>\n",
"type": "registry:ui"
},
{
Expand Down
Loading
Loading