From 67c698b949111277835302fdf88a2c1570dbea92 Mon Sep 17 00:00:00 2001 From: daudix Date: Sun, 18 Aug 2024 02:19:34 +0300 Subject: [PATCH] Add manual theme switcher (fixes #5) --- config.toml | 6 ++ i18n/ar.toml | 4 + i18n/en.toml | 4 + i18n/ru.toml | 4 + sass/_nav.scss | 176 +++++++++++++++++++++++------------ sass/_variables.scss | 97 +++++++++++-------- static/theme-switcher.js | 54 +++++++++++ templates/base.html | 2 +- templates/partials/head.html | 20 +++- templates/partials/nav.html | 26 ++++++ 10 files changed, 285 insertions(+), 108 deletions(-) create mode 100644 static/theme-switcher.js diff --git a/config.toml b/config.toml index 63cba5c..7135de6 100644 --- a/config.toml +++ b/config.toml @@ -52,6 +52,10 @@ generate_feeds = true taxonomies = [{ name = "tags", feed = true }] [extra] +# Which theme should be used by default (light/dark). +# Strongly recommended to use this only with the manual theme switcher enabled, +# it's important for a11y. +# default_theme = "dark" # Sets theme and browser theme color. # See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name/theme-color primary_color = "#ff7800" @@ -96,6 +100,8 @@ show_read_time = true [extra.nav] # Whether to show Atom/RSS feed button in the nav show_feed = true +# Whether to show manual theme switcher in the nav +show_theme_switcher = true # Links used in the nav. # For local files use same link format as in Markdown, # i.e "@/blog/_index.md". diff --git a/i18n/ar.toml b/i18n/ar.toml index 7ec024f..8433516 100644 --- a/i18n/ar.toml +++ b/i18n/ar.toml @@ -58,6 +58,10 @@ source = "مصدر الموقع الإلكتروني" table_of_contents = "جدول المحتويات" tags = "العلامات" tags_title = "العلامات" +theme = "السمة" +theme_dark = "التبديل إلى السمة الداكنة" +theme_light = "التبديل إلى السمة الفاتحة" +theme_system = "استخدام سمة النظام" tip = "نصيحة" trigger_warning = "تحذير الزناد" updated = "تحديث" diff --git a/i18n/en.toml b/i18n/en.toml index c8e0b75..c9412c7 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -62,6 +62,10 @@ source = "Website source" table_of_contents = "Table of Contents" tags = "tags" tags_title = "Tags" +theme = "Theme" +theme_dark = "Switch to dark theme" +theme_light = "Switch to light theme" +theme_system = "Use system theme" tip = "Tip" trigger_warning = "Trigger Warning" updated = "Updated on" diff --git a/i18n/ru.toml b/i18n/ru.toml index 9e6dd45..1f16f8a 100644 --- a/i18n/ru.toml +++ b/i18n/ru.toml @@ -66,6 +66,10 @@ source = "Исходный код веб-сайта" table_of_contents = "Оглавление" tags = "$NUMBER тегов" tags_title = "Теги" +theme = "Тема" +theme_dark = "Переключить на темную тему" +theme_light = "Переключить на светлую тему" +theme_system = "Использовать системную тему" tip = "Совет" trigger_warning = "Предупреждение о Тревоге" updated = "Обновлено" diff --git a/sass/_nav.scss b/sass/_nav.scss index eb7130f..7ec02b9 100644 --- a/sass/_nav.scss +++ b/sass/_nav.scss @@ -56,7 +56,7 @@ list-style: none; @media only screen and (max-width: 480px) { - &:not(#search, #language-switcher, #feed) { + &:not(#search, #language-switcher, #theme-switcher, #feed) { width: 100%; } } @@ -84,15 +84,7 @@ } @media only screen and (max-width: 480px) { - position: absolute; - top: unset; - right: unset; - bottom: -0.125rem; - left: -0.125rem; - background-color: var(--fg-muted-2); - width: calc(100% + 0.25rem); - height: max(1px, 0.0625em); - content: ""; + display: none; } } @@ -150,7 +142,9 @@ a, &#search button, - &#language-switcher summary { + &#language-switcher summary, + &#theme-switcher summary, + &#theme-switcher button { &:hover { box-shadow: var(--edge-highlight); background-color: var(--fg-muted-1); @@ -240,7 +234,9 @@ &#search button, &#language-switcher summary, - &#feed a { + &#feed a, + &#theme-switcher summary, + &#theme-switcher button { padding: 0.5rem 0.625rem; &:hover .icon { @@ -256,7 +252,7 @@ } } - &#search button { + button { -webkit-appearance: none; appearance: none; transition: var(--transition); @@ -266,15 +262,15 @@ background-color: transparent; font-weight: bold; font-size: 1rem; + } - .icon { - $icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M6.57.063c-3.578 0-6.5 2.921-6.5 6.5 0 3.578 2.922 6.5 6.5 6.5a6.46 6.46 0 0 0 3.83-1.256l2.975 2.974c.957.938 2.363-.5 1.406-1.437l-2.96-2.961a6.46 6.46 0 0 0 1.25-3.82c0-3.579-2.923-6.5-6.5-6.5m0 2c2.5 0 4.5 2.003 4.5 4.5 0 2.5-2 4.5-4.5 4.5-2.496 0-4.5-2-4.5-4.5 0-2.497 2.004-4.5 4.5-4.5'/%3E%3C/svg%3E"); - -webkit-mask-image: $icon; - mask-image: $icon; + &#search button .icon { + $icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M6.57.063c-3.578 0-6.5 2.921-6.5 6.5 0 3.578 2.922 6.5 6.5 6.5a6.46 6.46 0 0 0 3.83-1.256l2.975 2.974c.957.938 2.363-.5 1.406-1.437l-2.96-2.961a6.46 6.46 0 0 0 1.25-3.82c0-3.579-2.923-6.5-6.5-6.5m0 2c2.5 0 4.5 2.003 4.5 4.5 0 2.5-2 4.5-4.5 4.5-2.496 0-4.5-2-4.5-4.5 0-2.497 2.004-4.5 4.5-4.5'/%3E%3C/svg%3E"); + -webkit-mask-image: $icon; + mask-image: $icon; - :root[dir*="rtl"] & { - transform: scaleX(-100%); - } + :root[dir*="rtl"] & { + transform: scaleX(-100%); } } @@ -288,26 +284,63 @@ } } + details { + position: relative; + box-shadow: none; + border-radius: 0; + background-color: transparent; + padding: 0; + + summary { + transition: var(--transition); + border-radius: 999px; + background-color: transparent; + color: var(--fg-muted-4); + list-style: none; + + &::marker, + &::-webkit-details-marker { + display: none; + } + } + + &[open] ul { + animation: dropdown-open var(--transition); + + @keyframes dropdown-open { + from { + transform: translate(-50%, 0); + opacity: 0; + } + } + } + + ul { + -webkit-backdrop-filter: var(--blur); + text-wrap: nowrap; + position: absolute; + left: 50%; + transform: translate(-50%, 1rem); + z-index: 1; + backdrop-filter: var(--blur); + box-shadow: var(--edge-highlight), 0 0.75rem 1.5rem -1rem rgba(0, 0, 0, 0.5); + background-color: var(--nav-bg); + padding: 0.25rem; + + li { + width: 100%; + + a { + border-radius: var(--rounded-corner); + width: 100%; + } + } + } + } + &#language-switcher { details { - position: relative; - box-shadow: none; - border-radius: 0; - background-color: transparent; - padding: 0; - summary { - transition: var(--transition); - border-radius: 999px; - background-color: transparent; - color: var(--fg-muted-4); - list-style: none; - - &::marker, - &::-webkit-details-marker { - display: none; - } - .icon { $icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M3.98 1v3H1v2h2.947a4.8 4.8 0 0 1-.592 1.871c-.425.758-1.101 1.488-2.062 2.45l1.414 1.413c.92-.92 1.703-1.728 2.283-2.697.38.632.844 1.196 1.377 1.768l.668-2.309a6 6 0 0 1-.41-.625A4.75 4.75 0 0 1 6.033 6h1.53l.511-2H6V1zm5.24 1L6 15h2l.781-3h4.438L14 15h2L12.781 2zm1.562 2h.438l1.5 6H9.28z'/%3E%3C/svg%3E"); -webkit-mask-image: $icon; @@ -315,37 +348,58 @@ } } - &[open] ul { - animation: dropdown-open var(--transition); + ul { + border-radius: calc(var(--rounded-corner) + 0.25rem); + } + } + } - @keyframes dropdown-open { - from { - transform: translate(-50%, 0); - opacity: 0; + &#theme-switcher { + details { + summary { + .icon { + $icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M8 0C3.594 0 0 3.594 0 8s3.594 8 8 8 8-3.594 8-8-3.594-8-8-8m0 1.941c3.36 0 6.059 2.7 6.059 6.059s-2.7 6.059-6.059 6.059zm0 0'/%3E%3C/svg%3E"); + -webkit-mask-image: $icon; + mask-image: $icon; + + :root[dir*="rtl"] & { + transform: scaleX(-100%); } } } + } - ul { - -webkit-backdrop-filter: var(--blur); - text-wrap: nowrap; - position: absolute; - left: 50%; - transform: translate(-50%, 1rem); - z-index: 1; - backdrop-filter: var(--blur); - box-shadow: var(--edge-highlight), 0 0.75rem 1.5rem -1rem rgba(0, 0, 0, 0.5); - border-radius: calc(var(--rounded-corner) + 0.25rem); - background-color: var(--nav-bg); - padding: 0.25rem; + ul { + flex-wrap: nowrap; + border-radius: 999px; - li { - display: flex; - width: 100%; + li { + display: flex; + width: 100%; - a { - border-radius: var(--rounded-corner); - width: 100%; + #theme-light .icon { + $icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M8.004-.008a1 1 0 0 0-1 1v1a1 1 0 1 0 2 0v-1c0-.554-.445-1-1-1M3.053 2.035a1 1 0 0 0-.26.035.994.994 0 0 0-.45 1.672l.708.707a1 1 0 1 0 1.414-1.414l-.707-.707a1 1 0 0 0-.705-.293m9.9.012a1 1 0 0 0-.707.293l-.707.707a1 1 0 1 0 1.414 1.414l.707-.707a1 1 0 0 0-.707-1.707M8 4C5.785 4 4 5.785 4 8s1.785 4 4 4 4-1.785 4-4-1.785-4-4-4m0 2c1.098 0 2 .902 2 2s-.902 2-2 2-2-.902-2-2 .902-2 2-2m-7.004.984a1 1 0 1 0 0 2h1a1 1 0 1 0 0-2zM14 7c-.55 0-1 .45-1 1s.45 1 1 1h1c.55 0 1-.45 1-1s-.45-1-1-1zM3.748 11.234a1 1 0 0 0-.705.293l-.711.707a1.007 1.007 0 0 0 0 1.414c.39.391 1.027.391 1.418 0l.707-.707a1 1 0 0 0-.709-1.707m8.49.006q-.131 0-.261.033a1.01 1.01 0 0 0-.707.711 1 1 0 0 0 .261.965l.707.707a.995.995 0 0 0 1.672-.445 1 1 0 0 0-.258-.969l-.707-.707a1 1 0 0 0-.707-.295m-4.246 1.756c-.554 0-1 .445-1 1v1a1 1 0 1 0 2 0v-1a1 1 0 0 0-1-1'/%3E%3C/svg%3E"); + -webkit-mask-image: $icon; + mask-image: $icon; + } + + #theme-dark .icon { + $icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M.918 8.004a7.072 7.072 0 0 0 14.102.793 1.01 1.01 0 0 0-.457-.957 1 1 0 0 0-1.063-.004 3.9 3.9 0 0 1-2.031.578 3.89 3.89 0 0 1-3.883-3.883c0-.715.203-1.422.578-2.031a1 1 0 0 0-.004-1.062c-.207-.32-.578-.5-.957-.458A7.07 7.07 0 0 0 .918 8.004M5.586 4.53a5.877 5.877 0 0 0 8.965 5.004l-1.52-.96a5.09 5.09 0 0 1-5.035 4.507 5.09 5.09 0 0 1-5.078-5.078 5.09 5.09 0 0 1 4.508-5.035l-.961-1.52a5.9 5.9 0 0 0-.88 3.082m0 0'/%3E%3C/svg%3E"); + -webkit-mask-image: $icon; + mask-image: $icon; + + :root[dir*="rtl"] & { + transform: scaleX(-100%); + } + } + + #theme-system .icon { + $icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath d='M8 0C3.594 0 0 3.594 0 8s3.594 8 8 8 8-3.594 8-8-3.594-8-8-8m0 1.941c3.36 0 6.059 2.7 6.059 6.059s-2.7 6.059-6.059 6.059zm0 0'/%3E%3C/svg%3E"); + -webkit-mask-image: $icon; + mask-image: $icon; + + :root[dir*="rtl"] & { + transform: scaleX(-100%); } } } diff --git a/sass/_variables.scss b/sass/_variables.scss index 2778681..dbd60fc 100644 --- a/sass/_variables.scss +++ b/sass/_variables.scss @@ -1,24 +1,52 @@ +@mixin theme-variables($theme) { + @if $theme =="dark" { + --bg-color: linear-gradient(rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.9)); + --fg-color: rgb(255, 255, 255); + --fg-muted-1: rgba(255, 255, 255, 0.05); + --fg-muted-2: rgba(255, 255, 255, 0.1); + --fg-muted-3: rgba(255, 255, 255, 0.2); + --fg-muted-4: rgba(255, 255, 255, 0.5); + --fg-muted-5: rgba(255, 255, 255, 0.6); + --nav-bg: rgba(25, 25, 25, 0.7); + --blue-bg: rgba(153, 193, 241, 0.1); + --blue-fg: rgb(153, 193, 241); + --green-bg: rgba(143, 240, 164, 0.1); + --green-fg: rgb(143, 240, 164); + --purple-bg: rgba(220, 138, 221, 0.1); + --purple-fg: rgb(220, 138, 221); + --red-bg: rgba(226, 97, 81, 0.1); + --red-fg: rgb(246, 97, 81); + --yellow-bg: rgba(248, 228, 92, 0.1); + --yellow-fg: rgb(248, 228, 92); + --star-featured: rgba(248, 228, 92, 0.05); + color-scheme: dark; + } + + @else { + --bg-color: linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.8)); + --fg-color: rgba(0, 0, 0, 0.8); + --fg-muted-1: rgba(0, 0, 0, 0.05); + --fg-muted-2: rgba(0, 0, 0, 0.1); + --fg-muted-3: rgba(0, 0, 0, 0.2); + --fg-muted-4: rgba(0, 0, 0, 0.5); + --fg-muted-5: rgba(0, 0, 0, 0.6); + --nav-bg: rgba(242, 242, 242, 0.7); + --blue-bg: rgba(53, 132, 228, 0.1); + --blue-fg: rgb(53, 132, 228); + --green-bg: rgba(38, 162, 105, 0.1); + --green-fg: rgb(38, 162, 105); + --purple-bg: rgba(145, 65, 172, 0.1); + --purple-fg: rgb(145, 65, 172); + --red-bg: rgba(224, 27, 36, 0.1); + --red-fg: rgb(224, 27, 36); + --yellow-bg: rgba(156, 110, 3, 0.1); + --yellow-fg: rgb(156, 110, 3); + --star-featured: rgba(156, 110, 3, 0.15); + } +} + :root { - // COLORS - --bg-color: linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.8)); - --fg-color: rgba(0, 0, 0, 0.8); - --fg-muted-1: rgba(0, 0, 0, 0.05); - --fg-muted-2: rgba(0, 0, 0, 0.1); - --fg-muted-3: rgba(0, 0, 0, 0.2); - --fg-muted-4: rgba(0, 0, 0, 0.5); - --fg-muted-5: rgba(0, 0, 0, 0.6); - --nav-bg: rgba(242, 242, 242, 0.7); - --blue-bg: rgba(53, 132, 228, 0.1); - --blue-fg: rgb(53, 132, 228); - --green-bg: rgba(38, 162, 105, 0.1); - --green-fg: rgb(38, 162, 105); - --purple-bg: rgba(145, 65, 172, 0.1); - --purple-fg: rgb(145, 65, 172); - --red-bg: rgba(224, 27, 36, 0.1); - --red-fg: rgb(224, 27, 36); - --yellow-bg: rgba(156, 110, 3, 0.1); - --yellow-fg: rgb(156, 110, 3); - --star-featured: rgba(156, 110, 3, 0.15); + @include theme-variables("light"); // VARIABLES --active: 0.9; @@ -50,27 +78,14 @@ --font-didone: Didot, "Bodoni MT", "Noto Serif Display", "URW Palladio L", P052, Sylfaen, serif; --font-handwritten: "Segoe Print", "Bradley Hand", Chilanka, TSCu_Comic, casual, cursive; --font-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} - @media (prefers-color-scheme: dark) { - // COLORS - --bg-color: linear-gradient(rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.9)); - --fg-color: rgb(255, 255, 255); - --fg-muted-1: rgba(255, 255, 255, 0.05); - --fg-muted-2: rgba(255, 255, 255, 0.1); - --fg-muted-3: rgba(255, 255, 255, 0.2); - --fg-muted-4: rgba(255, 255, 255, 0.5); - --fg-muted-5: rgba(255, 255, 255, 0.6); - --nav-bg: rgba(25, 25, 25, 0.7); - --blue-bg: rgba(153, 193, 241, 0.1); - --blue-fg: rgb(153, 193, 241); - --green-bg: rgba(143, 240, 164, 0.1); - --green-fg: rgb(143, 240, 164); - --purple-bg: rgba(220, 138, 221, 0.1); - --purple-fg: rgb(220, 138, 221); - --red-bg: rgba(226, 97, 81, 0.1); - --red-fg: rgb(246, 97, 81); - --yellow-bg: rgba(248, 228, 92, 0.1); - --yellow-fg: rgb(248, 228, 92); - --star-featured: rgba(248, 228, 92, 0.05); +[data-theme="dark"] { + @include theme-variables("dark"); +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) { + @include theme-variables("dark"); } } diff --git a/static/theme-switcher.js b/static/theme-switcher.js new file mode 100644 index 0000000..165ee81 --- /dev/null +++ b/static/theme-switcher.js @@ -0,0 +1,54 @@ +(function () { + // Get the default theme from the HTML data-theme attribute. + const defaultTheme = document.documentElement.getAttribute("data-theme"); + + // Set the data-default-theme attribute only if defaultTheme is not null. + if (defaultTheme) { + document.documentElement.setAttribute("data-default-theme", defaultTheme); + } + + // Attempt to retrieve the current theme from the browser's local storage. + const storedTheme = localStorage.getItem("theme"); + + if (storedTheme && storedTheme !== "system") { + document.documentElement.setAttribute("data-theme", storedTheme); + } else if (defaultTheme && storedTheme !== "system") { + document.documentElement.setAttribute("data-theme", defaultTheme); + } else { + // If no theme is found in local storage and no default theme is set, CSS handles the theme. + document.documentElement.removeAttribute("data-theme"); + } +})(); + +const defaultTheme = document.documentElement.getAttribute("data-default-theme"); + +function setTheme(theme, saveToLocalStorage = false) { + if (theme === "system") { + document.documentElement.removeAttribute("data-theme"); + } else { + document.documentElement.setAttribute("data-theme", theme); + } + + if (saveToLocalStorage) { + localStorage.setItem("theme", theme); + } else { + localStorage.removeItem("theme"); + } + + // Dispatch a custom event for comment systems. + window.dispatchEvent(new CustomEvent("themeChanged", { detail: { theme } })); +} + +function resetTheme() { + // Reset the theme to the default or system preference if no default is set. + setTheme(defaultTheme || "system"); +} + +// Functions connected to buttons via `onclick` attributes. +function switchTheme(theme) { + if (theme === "system") { + resetTheme(); + } else { + setTheme(theme, true); + } +} diff --git a/templates/base.html b/templates/base.html index 865c760..18f47f6 100644 --- a/templates/base.html +++ b/templates/base.html @@ -8,7 +8,7 @@ {%- set rtl_languages = ["ar", "arc", "az", "dv", "ff", "he", "ku", "nqo", "fa", "rhg", "syc", "ur"] -%} - + {% include "partials/head.html" %} {%- if config.extra.nav.links %} diff --git a/templates/partials/head.html b/templates/partials/head.html index cbf90d1..8f5c1ca 100644 --- a/templates/partials/head.html +++ b/templates/partials/head.html @@ -46,8 +46,14 @@ } {%- if config.extra.primary_color_dark %} + [data-theme="dark"] { + --primary-color: {{ config.extra.primary_color_dark }}; + --primary-color-alpha: {{ config.extra.primary_color_dark_alpha }}; + --contrast-color: {% if config.extra.fix_contrast_dark %}rgba(0, 0, 0, 0.8){% else %}#fff{% endif %}; + } + @media (prefers-color-scheme: dark) { - :root { + :root:not([data-theme="light"]) { --primary-color: {{ config.extra.primary_color_dark }}; --primary-color-alpha: {{ config.extra.primary_color_dark_alpha }}; --contrast-color: {% if config.extra.fix_contrast_dark %}rgba(0, 0, 0, 0.8){% else %}#fff{% endif %}; @@ -83,15 +89,19 @@ {%- include "partials/copy_button.html" %} {%- endif %} + {%- set scripts = [] %} + {%- if config.build_search_index %} {%- include "partials/search.html" %} - {%- set scripts = ["elasticlunr.min.js"] %} - {%- else %} - {%- set scripts = [""] %} + {%- set scripts = scripts | concat(with=["elasticlunr.min.js"]) %} + {%- endif %} + + {%- if config.extra.nav.show_theme_switcher %} + {%- set scripts = scripts | concat(with=["theme-switcher.js"]) %} {%- endif %} {%- if config.extra.scripts %} - {%- set scripts = config.extra.scripts %} + {%- set scripts = scripts | concat(with=config.extra.scripts) %} {%- endif %} {%- if page.extra.scripts %} diff --git a/templates/partials/nav.html b/templates/partials/nav.html index daad935..aefeba0 100644 --- a/templates/partials/nav.html +++ b/templates/partials/nav.html @@ -46,6 +46,32 @@ {%- if config.languages | length > 0 %} {%- include "partials/language_switcher.html" %} {%- endif -%} + {%- if config.extra.nav.show_theme_switcher %} +
  • +
    + + + +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
  • + {%- endif %} {%- if config.generate_feeds and config.extra.nav.show_feed %}