From 2de36c1b02aecca815c65affbbd7c272f85c8caf Mon Sep 17 00:00:00 2001 From: John Bowdre Date: Thu, 15 Aug 2024 15:56:47 -0500 Subject: [PATCH] implement dark/light toggle --- assets/js/set-theme.js | 40 +++++++++++++ layouts/partials/footer.html | 4 ++ layouts/partials/head.html | 12 ++++ layouts/partials/header.html | 6 +- static/css/custom.css | 108 ++++++++++++++++++++++++++++++++++- 5 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 assets/js/set-theme.js diff --git a/assets/js/set-theme.js b/assets/js/set-theme.js new file mode 100644 index 0000000..5ee4ac4 --- /dev/null +++ b/assets/js/set-theme.js @@ -0,0 +1,40 @@ +const toggleButton = document.getElementById('themeToggle'); +const htmlElement = document.documentElement; + +function setTheme(theme) { + htmlElement.setAttribute('data-theme', theme); + localStorage.setItem('theme', theme); + updateToggleButton(theme); +} + +function updateToggleButton(theme) { + toggleButton.setAttribute('aria-checked', theme === 'dark'); +} + +function getPreferredTheme() { + const storedTheme = localStorage.getItem('theme'); + if (storedTheme) { + return storedTheme; + } + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +// Ensure the toggle button state is correct on load +document.addEventListener('DOMContentLoaded', () => { + updateToggleButton(getPreferredTheme()); +}); + +// Listen for toggle button clicks +toggleButton.addEventListener('click', () => { + const currentTheme = htmlElement.getAttribute('data-theme') || getPreferredTheme(); + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + setTheme(newTheme); +}); + +// Listen for system preference changes +window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + if (!localStorage.getItem('theme')) { + const newTheme = e.matches ? 'dark' : 'light'; + setTheme(newTheme); + } +}); \ No newline at end of file diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html index 9f713ba..d1ba2ac 100644 --- a/layouts/partials/footer.html +++ b/layouts/partials/footer.html @@ -15,3 +15,7 @@ {{ $jsCopy := resources.Get "js/code-copy-button.js" | minify }} {{ end }} + + +{{ $themeToggle := resources.Get "js/set-theme.js" | minify }} + diff --git a/layouts/partials/head.html b/layouts/partials/head.html index bcf7dc5..4691e45 100644 --- a/layouts/partials/head.html +++ b/layouts/partials/head.html @@ -21,6 +21,18 @@ {{ partialCached "favicon" . }} {{ partial "opengraph" . }} + + + diff --git a/layouts/partials/header.html b/layouts/partials/header.html index 45c46ce..c3435de 100644 --- a/layouts/partials/header.html +++ b/layouts/partials/header.html @@ -7,4 +7,8 @@ {{ end }} - + \ No newline at end of file diff --git a/static/css/custom.css b/static/css/custom.css index ddabad1..0398121 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,9 +1,113 @@ -/* color and font overrides */ +/* define fonts */ :root { - --code: var(--base06); --font-monospace: 'Berkeley Mono', 'IBM Plex Mono', 'Cascadia Mono', 'Roboto Mono', 'Source Code Pro', 'Fira Mono', 'Courier New', monospace; } +/* dark/light theming */ +:root{ + /* Default dark theme */ + --bg: var(--dark-base00); + --code: var(--dark-base06); + --fg: var(--dark-base05); + --highlight: var(--dark-base0A); + --hover: var(--dark-base0C); + --inner-bg: var(--dark-base02); + --link: var(--dark-base0D); + --logo-text: var(--dark-base09); + --logo: var(--dark-base0B); + --muted: var(--dark-base03); + --off-bg: var(--dark-base01); + --off-fg: var(--dark-base04); + --root-prompt: var(--dark-base08); + --user-prompt: var(--dark-base07); +} + +:root[data-theme="light"] { + --bg: var(--light-base00); + --off-bg: var(--light-base01); + --inner-bg: var(--light-base02); + --muted: var(--light-base03); + --off-fg: var(--light-base04); + --fg: var(--light-base05); + --code: var(--light-base06); + --user-prompt: var(--light-base07); + --root-prompt: var(--light-base08); + --logo-text: var(--light-base09); + --highlight: var(--light-base0A); + --logo: var(--light-base0B); + --hover: var(--light-base0C); + --link: var(--light-base0D); +} + +@media (prefers-color-scheme: light) { + :root:not([data-theme="dark"]) { + --bg: var(--light-base00); + --off-bg: var(--light-base01); + --inner-bg: var(--light-base02); + --muted: var(--light-base03); + --off-fg: var(--light-base04); + --fg: var(--light-base05); + --code: var(--light-base06); + --user-prompt: var(--light-base07); + --root-prompt: var(--light-base08); + --logo-text: var(--light-base09); + --highlight: var(--light-base0A); + --logo: var(--light-base0B); + --hover: var(--light-base0C); + --link: var(--light-base0D); + } +} + +body { + transition: background-color 0.3s ease, color 0.3s ease; +} + +.theme-toggle { + position: absolute; + top: 1rem; + right: 1rem; + background: none; + border: none; + cursor: pointer; + padding: 0; + width: 2rem; + height: 2rem; + z-index: 1000; + transition: opacity 0.3s ease; +} + +.theme-toggle:hover { + opacity: 0.7; +} + +.theme-toggle:focus { + outline: none; +} + +.theme-toggle:focus-visible { + outline: 2px solid var(--hover); + outline-offset: 2px; +} + +.theme-toggle__icon { + width: 100%; + height: 100%; + transition: transform 0.3s ease, fill 0.3s ease; + fill: var(--off-fg); +} + +.theme-toggle:hover .theme-toggle__icon { + fill: var(--hover); +} + +[data-theme="dark"] .theme-toggle__icon { + transform: rotate(180deg); +} + +.page__nav ul { + padding-right: 3rem; +} + /* load preferred font */ @font-face { font-family: 'Berkeley Mono';