mirror of
https://github.com/cotes2020/jekyll-theme-chirpy.git
synced 2026-06-21 23:38:39 +00:00
feat(theme): persist user theme preferences (#2756)
- Migrate theme persistence from `sessionStorage` to `localStorage` - Rename theme HTML attribute to `data-bs-theme` for Bootstrap compatibility - Trim and compile CSS according to the chosen theme mode
This commit is contained in:
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: يتوفر اصدار جديد للمحتوى.
|
||||
update: تحديث
|
||||
|
||||
theme:
|
||||
light: فاتح
|
||||
dark: داكن
|
||||
system: النظام
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: Налична е нова версия на съдържанието.
|
||||
update: Обнови
|
||||
|
||||
theme:
|
||||
light: Светла
|
||||
dark: Тъмна
|
||||
system: Системна
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: Hi ha una nova versió de contingut disponible.
|
||||
update: Actualitzar
|
||||
|
||||
theme:
|
||||
light: Clar
|
||||
dark: Fosc
|
||||
system: Sistema
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: Je k dispozici nová verze obsahu.
|
||||
update: Aktualizace
|
||||
|
||||
theme:
|
||||
light: Světlý
|
||||
dark: Tmavý
|
||||
system: Systém
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: En ny version af indholdet er fundet!
|
||||
update: Opdater
|
||||
|
||||
theme:
|
||||
light: Lys
|
||||
dark: Mørk
|
||||
system: System
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -48,6 +48,11 @@ notification:
|
||||
update_found: Eine neue Version ist verfügbar.
|
||||
update: Neue Version
|
||||
|
||||
theme:
|
||||
light: Hell
|
||||
dark: Dunkel
|
||||
system: System
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: ޔޫ ވާރޝަން ހުރިހާ.
|
||||
update: އޮޕްޑޭޓް
|
||||
|
||||
theme:
|
||||
light: އަލި
|
||||
dark: އަނދިރި
|
||||
system: ސިސްޓަމް
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: Υπάρχει διαθέσιμη μια νέα έκδοση του περιεχομένου.
|
||||
update: Ενημέρωση
|
||||
|
||||
theme:
|
||||
light: Φωτεινό
|
||||
dark: Σκοτεινό
|
||||
system: Σύστημα
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: A new version of content is available.
|
||||
update: Update
|
||||
|
||||
theme:
|
||||
light: Light
|
||||
dark: Dark
|
||||
system: System
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: Hay una nueva versión de contenido disponible.
|
||||
update: Actualizar
|
||||
|
||||
theme:
|
||||
light: Claro
|
||||
dark: Oscuro
|
||||
system: Sistema
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: نسخه جدیدی از محتوا موجود است.
|
||||
update: بهروزرسانی
|
||||
|
||||
theme:
|
||||
light: روشن
|
||||
dark: تیره
|
||||
system: سیستم
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -48,6 +48,11 @@ notification:
|
||||
update_found: Uusi versio sisällöstä on saatavilla.
|
||||
update: Päivitä
|
||||
|
||||
theme:
|
||||
light: Vaalea
|
||||
dark: Tumma
|
||||
system: Järjestelmä
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: Une nouvelle version du contenu est disponible.
|
||||
update: Mise à jour
|
||||
|
||||
theme:
|
||||
light: Clair
|
||||
dark: Sombre
|
||||
system: Système
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -50,6 +50,11 @@ notification:
|
||||
update_found: Elérhető a tartalom új verziója.
|
||||
update: Frissítés
|
||||
|
||||
theme:
|
||||
light: Világos
|
||||
dark: Sötét
|
||||
system: Rendszer
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: Versi konten baru tersedia.
|
||||
update: Perbarui
|
||||
|
||||
theme:
|
||||
light: Terang
|
||||
dark: Gelap
|
||||
system: Sistem
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -48,6 +48,11 @@ notification:
|
||||
update_found: Nuova versione del contenuto disponibile.
|
||||
update: Aggiornamento
|
||||
|
||||
theme:
|
||||
light: Chiaro
|
||||
dark: Scuro
|
||||
system: Sistema
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: 新しいバージョンが利用可能です。
|
||||
update: 更新
|
||||
|
||||
theme:
|
||||
light: ライト
|
||||
dark: ダーク
|
||||
system: システム
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: 새 버전의 콘텐츠를 사용할 수 있습니다.
|
||||
update: 업데이트
|
||||
|
||||
theme:
|
||||
light: 라이트
|
||||
dark: 다크
|
||||
system: 시스템
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: وەشانێکی نوێی ناوەڕۆک بەردەستە.
|
||||
update: نوێکردنەوە
|
||||
|
||||
theme:
|
||||
light: ڕووناک
|
||||
dark: تاریک
|
||||
system: سیستەم
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: အကြောင်းအရာဗားရှင်းအသစ်ကို ရနိုင်ပါပြီ။
|
||||
update: အပ်ဒိတ်
|
||||
|
||||
theme:
|
||||
light: အလင်း
|
||||
dark: အမှောင်
|
||||
system: စနစ်
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: Nieuwe versie van inhoud beschikbaar.
|
||||
update: Update
|
||||
|
||||
theme:
|
||||
light: Licht
|
||||
dark: Donker
|
||||
system: Systeem
|
||||
|
||||
# ----- Posts related labels -----
|
||||
post:
|
||||
written_by: Door
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: نوې نسخه شتون لري.
|
||||
update: تازه
|
||||
|
||||
theme:
|
||||
light: روښانه
|
||||
dark: تیاره
|
||||
system: سیستم
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: Uma nova versão do conteúdo está disponível.
|
||||
update: atualização
|
||||
|
||||
theme:
|
||||
light: Claro
|
||||
dark: Escuro
|
||||
system: Sistema
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -48,6 +48,11 @@ notification:
|
||||
update_found: Доступна новая версия контента.
|
||||
update: Обновить
|
||||
|
||||
theme:
|
||||
light: Светлая
|
||||
dark: Темная
|
||||
system: Системная
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: Novejša različica vsebine je na voljo. #A new version of content is available.
|
||||
update: Posodobi #Update
|
||||
|
||||
theme:
|
||||
light: Svetla
|
||||
dark: Temna
|
||||
system: Sistemska
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: Det finns en ny version av innehållet.
|
||||
update: Uppdatera sidan
|
||||
|
||||
theme:
|
||||
light: Ljust
|
||||
dark: Mörkt
|
||||
system: System
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: มีเวอร์ชันใหม่ของเนื้อหา
|
||||
update: อัปเดต
|
||||
|
||||
theme:
|
||||
light: สว่าง
|
||||
dark: มืด
|
||||
system: ระบบ
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: İçeriğin yeni bir sürümü mevcut.
|
||||
update: Güncelle
|
||||
|
||||
theme:
|
||||
light: Açık
|
||||
dark: Koyu
|
||||
system: Sistem
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: Доступна нова версія вмісту.
|
||||
update: Оновлення
|
||||
|
||||
theme:
|
||||
light: Світла
|
||||
dark: Темна
|
||||
system: Системна
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -49,6 +49,11 @@ notification:
|
||||
update_found: نیا مواد دستیاب ہے۔
|
||||
update: اپ ڈیٹ
|
||||
|
||||
theme:
|
||||
light: روشن
|
||||
dark: تاریک
|
||||
system: سسٹم
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -48,6 +48,11 @@ notification:
|
||||
update_found: Đã có phiên bản mới của nội dung.
|
||||
update: Cập nhật
|
||||
|
||||
theme:
|
||||
light: Sáng
|
||||
dark: Tối
|
||||
system: Hệ thống
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -48,6 +48,11 @@ notification:
|
||||
update_found: 发现新版本的内容。
|
||||
update: 更新
|
||||
|
||||
theme:
|
||||
light: 浅色
|
||||
dark: 深色
|
||||
system: 跟随系统
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -48,6 +48,11 @@ notification:
|
||||
update_found: 發現新版本更新。
|
||||
update: 更新
|
||||
|
||||
theme:
|
||||
light: 淺色
|
||||
dark: 深色
|
||||
system: 跟隨系統
|
||||
|
||||
# ----- Posts related labels -----
|
||||
|
||||
post:
|
||||
|
||||
@@ -19,6 +19,9 @@ webfonts: https://fonts.googleapis.com/css2?family=Lato:wght@300;400&family=Sour
|
||||
|
||||
# Libraries
|
||||
|
||||
bootstrap:
|
||||
css: https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css
|
||||
|
||||
toc:
|
||||
css: https://cdn.jsdelivr.net/npm/tocbot@4/dist/tocbot.min.css
|
||||
js: https://cdn.jsdelivr.net/npm/tocbot@4/dist/tocbot.min.js
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
{%- comment -%} Auto switch theme {%- endcomment -%}
|
||||
function reloadDisqus(event) {
|
||||
if (event.source === window && event.data && event.data.id === Theme.ID) {
|
||||
if (event.source === window && event.data && event.data.id === Theme.eventId) {
|
||||
{%- comment -%} Disqus hasn't been loaded {%- endcomment -%}
|
||||
if (typeof DISQUS === 'undefined') {
|
||||
return;
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
addDisqus();
|
||||
|
||||
if (Theme.switchable) {
|
||||
if (Theme.isToggleable) {
|
||||
addEventListener('message', reloadDisqus);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<!-- https://giscus.app/ -->
|
||||
<script>
|
||||
(function () {
|
||||
const themeMapper = Theme.getThemeMapper('light', 'dark_dimmed');
|
||||
const initTheme = themeMapper[Theme.visualState];
|
||||
const themeMap = Theme.newThemeMap('light', 'dark_dimmed');
|
||||
const initTheme = themeMap[Theme.resolvedTheme];
|
||||
|
||||
let lang = '{{ site.comments.giscus.lang | default: lang }}';
|
||||
{%- comment -%} https://github.com/giscus/giscus/tree/main/locales {%- endcomment -%}
|
||||
@@ -37,8 +37,9 @@
|
||||
$footer.insertAdjacentElement("beforebegin", giscusNode);
|
||||
|
||||
addEventListener('message', (event) => {
|
||||
if (event.source === window && event.data && event.data.id === Theme.ID) {
|
||||
const newTheme = themeMapper[Theme.visualState];
|
||||
if (event.source === window && event.data && event.data.id === Theme.eventId) {
|
||||
const newTheme = themeMap[Theme.resolvedTheme];
|
||||
|
||||
const message = {
|
||||
setConfig: {
|
||||
theme: newTheme
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<script>
|
||||
(function () {
|
||||
const origin = 'https://utteranc.es';
|
||||
const themeMapper = Theme.getThemeMapper('github-light', 'github-dark');
|
||||
const initTheme = themeMapper[Theme.visualState];
|
||||
const themeMap = Theme.newThemeMap('github-light', 'github-dark');
|
||||
const initTheme = themeMap[Theme.resolvedTheme];
|
||||
|
||||
let script = document.createElement('script');
|
||||
script.src = 'https://utteranc.es/client.js';
|
||||
@@ -22,8 +22,8 @@
|
||||
{%- comment -%}
|
||||
Credit to <https://github.com/utterance/utterances/issues/170#issuecomment-594036347>
|
||||
{%- endcomment -%}
|
||||
if (event.source === window && event.data && event.data.id === Theme.ID) {
|
||||
newTheme = themeMapper[Theme.visualState];
|
||||
if (event.source === window && event.data && event.data.id === Theme.eventId) {
|
||||
newTheme = themeMap[Theme.resolvedTheme];
|
||||
|
||||
const message = {
|
||||
type: 'set-theme',
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@
|
||||
|
||||
<!-- Bootstrap -->
|
||||
{% unless jekyll.environment == 'production' %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="{{ site.data.origin.cors.bootstrap.css }}">
|
||||
{% endunless %}
|
||||
|
||||
<!-- Theme style -->
|
||||
|
||||
+45
-3
@@ -41,10 +41,52 @@
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-bottom d-flex flex-wrap align-items-center w-100">
|
||||
{% unless site.theme_mode %}
|
||||
<button type="button" class="btn btn-link nav-link" aria-label="Switch Mode" id="mode-toggle">
|
||||
<i class="fas fa-adjust"></i>
|
||||
{% unless site.theme_mode == 'light' or site.theme_mode == 'dark' %}
|
||||
{%- capture icon_system -%}
|
||||
<i class="fa-solid fa-display" data-theme-mode="system"></i>
|
||||
{%- endcapture -%}
|
||||
|
||||
{%- capture icon_light -%}
|
||||
<i class="fa-regular fa-sun" data-theme-mode="light"></i>
|
||||
{%- endcapture -%}
|
||||
|
||||
{%- capture icon_dark -%}
|
||||
<i class="fa-regular fa-moon" data-theme-mode="dark"></i>
|
||||
{%- endcapture -%}
|
||||
|
||||
<div class="btn-group dropup">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link nav-link"
|
||||
aria-label="Switch Mode"
|
||||
id="mode-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
>
|
||||
{{- icon_light -}}
|
||||
{{- icon_dark -}}
|
||||
{{- icon_system -}}
|
||||
</button>
|
||||
<ul class="dropdown-menu rounded-3 mb-1 p-1">
|
||||
<li>
|
||||
<button class="dropdown-item d-flex align-items-center" type="button" data-theme-mode="light">
|
||||
{{- icon_light -}}
|
||||
{{- site.data.locales[lang].theme.light -}}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="dropdown-item d-flex align-items-center" type="button" data-theme-mode="dark">
|
||||
{{- icon_dark -}}
|
||||
{{- site.data.locales[lang].theme.dark -}}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="dropdown-item d-flex align-items-center" type="button" data-theme-mode="system">
|
||||
{{- icon_system -}}
|
||||
{{- site.data.locales[lang].theme.system -}}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% if site.data.contact.size > 0 %}
|
||||
<span class="icon-border"></span>
|
||||
|
||||
@@ -32,17 +32,21 @@ export function imgPopup() {
|
||||
document.querySelector('.popup.dark') === null
|
||||
);
|
||||
|
||||
if (Theme.visualState === Theme.DARK) {
|
||||
if (Theme.isDark) {
|
||||
selector = darkImages;
|
||||
}
|
||||
|
||||
let current = GLightbox({ selector: `${selector}` });
|
||||
|
||||
if (hasDualImages && Theme.switchable) {
|
||||
if (hasDualImages && Theme.isToggleable) {
|
||||
let reverse = null;
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.source === window && event.data && event.data.id === Theme.ID) {
|
||||
if (
|
||||
event.source === window &&
|
||||
event.data &&
|
||||
event.data.id === Theme.eventId
|
||||
) {
|
||||
[current, reverse] = swapImages(current, reverse);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
*/
|
||||
|
||||
const MERMAID = 'mermaid';
|
||||
const themeMapper = Theme.getThemeMapper('default', 'dark');
|
||||
const themeMap = Theme.newThemeMap('default', 'dark');
|
||||
|
||||
function refreshTheme(event) {
|
||||
if (event.source === window && event.data && event.data.id === Theme.ID) {
|
||||
if (
|
||||
event.source === window &&
|
||||
event.data &&
|
||||
event.data.id === Theme.eventId
|
||||
) {
|
||||
// Re-render the SVG › <https://github.com/mermaid-js/mermaid/issues/311#issuecomment-332557344>
|
||||
const mermaidList = document.getElementsByClassName(MERMAID);
|
||||
|
||||
@@ -16,7 +20,7 @@ function refreshTheme(event) {
|
||||
elem.removeAttribute('data-processed');
|
||||
});
|
||||
|
||||
const newTheme = themeMapper[Theme.visualState];
|
||||
const newTheme = themeMap[Theme.resolvedTheme];
|
||||
|
||||
mermaid.initialize({ theme: newTheme });
|
||||
mermaid.init(null, `.${MERMAID}`);
|
||||
@@ -43,7 +47,7 @@ export function loadMermaid() {
|
||||
return;
|
||||
}
|
||||
|
||||
const initTheme = themeMapper[Theme.visualState];
|
||||
const initTheme = themeMap[Theme.resolvedTheme];
|
||||
|
||||
let mermaidConf = {
|
||||
theme: initTheme
|
||||
@@ -54,7 +58,7 @@ export function loadMermaid() {
|
||||
|
||||
mermaid.initialize(mermaidConf);
|
||||
|
||||
if (Theme.switchable) {
|
||||
if (Theme.isToggleable) {
|
||||
window.addEventListener('message', refreshTheme);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,46 @@
|
||||
/**
|
||||
* Add listener for theme mode toggle
|
||||
* Sets up the mode toggle dropdown, allowing users to switch between light, dark, and system themes.
|
||||
*
|
||||
* Dependencies:
|
||||
* - Theme (${JS_ROOT}/theme.js)
|
||||
*/
|
||||
|
||||
const $toggle = document.getElementById('mode-toggle');
|
||||
import 'bootstrap/js/src/dropdown.js';
|
||||
|
||||
const ACTIVE_CLASS = 'active';
|
||||
const dropdown = document.querySelector('#mode-toggle + .dropdown-menu');
|
||||
const activeMode = Theme.isSystemTheme
|
||||
? Theme.Mode.SYSTEM
|
||||
: Theme.resolvedTheme;
|
||||
|
||||
export function modeWatcher() {
|
||||
if (!$toggle) {
|
||||
if (!Theme.isToggleable) {
|
||||
return;
|
||||
}
|
||||
|
||||
$toggle.addEventListener('click', () => {
|
||||
Theme.flip();
|
||||
dropdown.querySelectorAll('.dropdown-item').forEach((option) => {
|
||||
const mode = option.dataset.themeMode;
|
||||
if (mode === activeMode) {
|
||||
option.classList.add(ACTIVE_CLASS);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.addEventListener('click', (event) => {
|
||||
const current = event.target.closest('.dropdown-item');
|
||||
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastActive = dropdown.querySelector(`.${ACTIVE_CLASS}`);
|
||||
|
||||
if (lastActive === current) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastActive.classList.remove(ACTIVE_CLASS);
|
||||
current.classList.add(ACTIVE_CLASS);
|
||||
Theme.update(current.dataset.themeMode);
|
||||
});
|
||||
}
|
||||
|
||||
+105
-87
@@ -1,135 +1,153 @@
|
||||
/**
|
||||
* Theme management class
|
||||
* A utility class that manages the site's theme mode.
|
||||
*
|
||||
* To reduce flickering during page load, this script should be loaded synchronously.
|
||||
* Concepts:
|
||||
* - Mode: dark, light, or system. The latter follows the operating system's preference.
|
||||
* - Theme: The actual theme applied to the DOM, either dark or light. Determined by the mode or system preference.
|
||||
*/
|
||||
class Theme {
|
||||
static #modeKey = 'mode';
|
||||
static #modeAttr = 'data-mode';
|
||||
static #darkMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
static switchable = !document.documentElement.hasAttribute(this.#modeAttr);
|
||||
/** @type {string} LocalStorage key for the selected theme mode. */
|
||||
static #storageKey = 'theme';
|
||||
|
||||
static get DARK() {
|
||||
return 'dark';
|
||||
static Mode = Object.freeze({
|
||||
DARK: 'dark',
|
||||
LIGHT: 'light',
|
||||
SYSTEM: 'system'
|
||||
});
|
||||
|
||||
static #root = document.documentElement;
|
||||
|
||||
/** @type {MediaQueryList} System dark-mode preference query. */
|
||||
static #mediaDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
/** @returns {string|null} The theme currently set on the DOM. */
|
||||
static get #domTheme() {
|
||||
return this.#root.dataset.bsTheme || null;
|
||||
}
|
||||
|
||||
static get LIGHT() {
|
||||
return 'light';
|
||||
/** @returns {string|null} The theme stored on the client. */
|
||||
static get #storedTheme() {
|
||||
return localStorage.getItem(this.#storageKey);
|
||||
}
|
||||
|
||||
/** @returns {string} The theme preferred by the operating system. */
|
||||
static get #systemTheme() {
|
||||
return this.#prefersDark ? this.Mode.DARK : this.Mode.LIGHT;
|
||||
}
|
||||
|
||||
/** @returns {boolean} Whether the operating system prefers dark mode. */
|
||||
static get #prefersDark() {
|
||||
return this.#mediaDark.matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} Theme mode identifier
|
||||
*/
|
||||
static get ID() {
|
||||
return 'theme-mode';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current visual state of the theme.
|
||||
* Applies a theme and optionally persists it as a user preference.
|
||||
*
|
||||
* @returns {string} The current visual state, either the mode if it exists,
|
||||
* or the system dark mode state ('dark' or 'light').
|
||||
* @param {'light'|'dark'} theme
|
||||
* @param {{ persist?: boolean, domPersist?: boolean }} [options]
|
||||
* - `persist`: Whether the theme is persisted in localStorage.
|
||||
* - `domPersist`: Whether the theme is persisted in data attributes on the DOM.
|
||||
*/
|
||||
static get visualState() {
|
||||
if (this.#hasMode) {
|
||||
return this.#mode;
|
||||
} else {
|
||||
return this.#sysDark ? this.DARK : this.LIGHT;
|
||||
static #apply(theme, { persist = false, domPersist = false } = {}) {
|
||||
this.#root.dataset.bsTheme = theme;
|
||||
|
||||
if (persist) {
|
||||
localStorage.setItem(this.#storageKey, theme);
|
||||
}
|
||||
|
||||
if (domPersist || persist) {
|
||||
this.#root.toggleAttribute('data-theme-persisted', true);
|
||||
}
|
||||
}
|
||||
|
||||
static get #mode() {
|
||||
return (
|
||||
sessionStorage.getItem(this.#modeKey) ||
|
||||
document.documentElement.getAttribute(this.#modeAttr)
|
||||
);
|
||||
/** Removes the stored user preference. */
|
||||
static #clearStorage() {
|
||||
localStorage.removeItem(this.#storageKey);
|
||||
this.#root.toggleAttribute('data-theme-persisted', false);
|
||||
}
|
||||
|
||||
static get #isDarkMode() {
|
||||
return this.#mode === this.DARK;
|
||||
/** Broadcasts a theme change event to dependent modules. */
|
||||
static #notify() {
|
||||
window.postMessage({ id: this.eventId }, '*');
|
||||
}
|
||||
|
||||
static get #hasMode() {
|
||||
return this.#mode !== null;
|
||||
/** @type {boolean} Whether the current page allows theme toggling. */
|
||||
static isToggleable = this.#domTheme === null;
|
||||
|
||||
static eventId = 'theme-updated';
|
||||
|
||||
/** @returns {string} Resolved theme, falling back to the system preference. */
|
||||
static get resolvedTheme() {
|
||||
return this.#storedTheme || this.#systemTheme;
|
||||
}
|
||||
|
||||
static get #sysDark() {
|
||||
return this.#darkMedia.matches;
|
||||
/** @returns {boolean} Whether the theme is determined by the system preference. */
|
||||
static get isSystemTheme() {
|
||||
return this.#storedTheme === null;
|
||||
}
|
||||
|
||||
/** @returns {boolean} Whether the resolved theme is dark. */
|
||||
static get isDark() {
|
||||
return this.resolvedTheme === this.Mode.DARK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps theme modes to provided values
|
||||
* @param {string} light Value for light mode
|
||||
* @param {string} dark Value for dark mode
|
||||
* @returns {Object} Mapped values
|
||||
* Creates a mode-indexed value map.
|
||||
*
|
||||
* @template T
|
||||
* @param {T} light Value for light mode.
|
||||
* @param {T} dark Value for dark mode.
|
||||
* @returns {{ light: T, dark: T }}
|
||||
*/
|
||||
static getThemeMapper(light, dark) {
|
||||
static newThemeMap(light, dark) {
|
||||
return {
|
||||
[this.LIGHT]: light,
|
||||
[this.DARK]: dark
|
||||
[this.Mode.LIGHT]: light,
|
||||
[this.Mode.DARK]: dark
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the theme based on system preferences or stored mode
|
||||
*/
|
||||
/** Initializes the theme from the stored value or system preference. */
|
||||
static init() {
|
||||
if (!this.switchable) {
|
||||
if (!this.isToggleable) {
|
||||
this.#clearStorage();
|
||||
return;
|
||||
}
|
||||
|
||||
this.#darkMedia.addEventListener('change', () => {
|
||||
const lastMode = this.#mode;
|
||||
this.#clearMode();
|
||||
const storedTheme = this.#storedTheme;
|
||||
|
||||
if (lastMode !== this.visualState) {
|
||||
this.#notify();
|
||||
if (storedTheme) {
|
||||
this.#apply(storedTheme, { domPersist: true });
|
||||
} else {
|
||||
this.#apply(this.#systemTheme);
|
||||
}
|
||||
|
||||
this.#mediaDark.addEventListener('change', () => {
|
||||
if (this.#storedTheme) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#apply(this.#systemTheme);
|
||||
this.#notify();
|
||||
});
|
||||
|
||||
if (!this.#hasMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#isDarkMode) {
|
||||
this.#setDark();
|
||||
} else {
|
||||
this.#setLight();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flips the current theme mode
|
||||
* Updates the theme by the specified mode.
|
||||
*
|
||||
* @param {'light'|'dark'|'system'} mode
|
||||
*/
|
||||
static flip() {
|
||||
if (this.#hasMode) {
|
||||
this.#clearMode();
|
||||
} else {
|
||||
this.#sysDark ? this.#setLight() : this.#setDark();
|
||||
}
|
||||
static update(mode) {
|
||||
const newTheme = mode === this.Mode.SYSTEM ? this.#systemTheme : mode;
|
||||
|
||||
if (newTheme !== this.resolvedTheme) {
|
||||
this.#notify();
|
||||
}
|
||||
|
||||
static #setDark() {
|
||||
document.documentElement.setAttribute(this.#modeAttr, this.DARK);
|
||||
sessionStorage.setItem(this.#modeKey, this.DARK);
|
||||
}
|
||||
this.#apply(newTheme, { persist: mode !== this.Mode.SYSTEM });
|
||||
|
||||
static #setLight() {
|
||||
document.documentElement.setAttribute(this.#modeAttr, this.LIGHT);
|
||||
sessionStorage.setItem(this.#modeKey, this.LIGHT);
|
||||
if (mode === this.Mode.SYSTEM) {
|
||||
this.#clearStorage();
|
||||
}
|
||||
|
||||
static #clearMode() {
|
||||
document.documentElement.removeAttribute(this.#modeAttr);
|
||||
sessionStorage.removeItem(this.#modeKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies other plugins that the theme mode has changed
|
||||
*/
|
||||
static #notify() {
|
||||
window.postMessage({ id: this.ID }, '*');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ layout: compress
|
||||
|
||||
{% include lang.html %}
|
||||
|
||||
{% if site.theme_mode %}
|
||||
{% capture prefer_mode %}data-mode="{{ site.theme_mode }}"{% endcapture %}
|
||||
{% endif %}
|
||||
|
||||
<!-- `site.alt_lang` can specify a language different from the UI -->
|
||||
<html lang="{{ page.lang | default: site.alt_lang | default: site.lang }}" {{ prefer_mode }}>
|
||||
<html
|
||||
lang="{{ page.lang | default: site.alt_lang | default: site.lang }}"
|
||||
{%- if site.theme_mode == 'light' or site.theme_mode == 'dark' -%}
|
||||
data-bs-theme="{{ site.theme_mode }}"
|
||||
{%- endif -%}
|
||||
>
|
||||
{% include head.html lang=lang %}
|
||||
|
||||
<body>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
@mixin color-scheme($mode) {
|
||||
@media (prefers-color-scheme: #{$mode}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -28,3 +28,9 @@ $code-icon-width: 1.75rem !default;
|
||||
|
||||
$font-family-base: 'Source Sans Pro', 'Microsoft Yahei', sans-serif !default;
|
||||
$font-family-heading: Lato, 'Microsoft Yahei', sans-serif !default;
|
||||
|
||||
/* Theme mode settings */
|
||||
|
||||
$theme-attr: 'data-bs-theme'; /* the attribute used to indicate the resolved theme */
|
||||
$theme-options: light, dark;
|
||||
$theme: null !default; /* set by Jekyll site configuration */
|
||||
|
||||
+25
-18
@@ -1,3 +1,4 @@
|
||||
@use 'sass:list';
|
||||
@use '../abstracts/variables' as v;
|
||||
@use '../abstracts/breakpoints' as bp;
|
||||
@use '../abstracts/mixins' as mx;
|
||||
@@ -5,32 +6,36 @@
|
||||
@use '../themes/light';
|
||||
@use '../themes/dark';
|
||||
|
||||
:root {
|
||||
font-size: 16px;
|
||||
$enable-dual: not list.index(v.$theme-options, v.$theme);
|
||||
$enable-light: v.$theme == light or $enable-dual;
|
||||
$enable-dark: v.$theme == dark or $enable-dual;
|
||||
|
||||
@if $enable-light {
|
||||
:root[#{v.$theme-attr}='light'] {
|
||||
@include light.styles;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
@media (prefers-color-scheme: light) {
|
||||
&:not([data-mode]),
|
||||
&[data-mode='light'] {
|
||||
@if $enable-dark {
|
||||
:root[#{v.$theme-attr}='dark'] {
|
||||
@include dark.styles;
|
||||
}
|
||||
}
|
||||
|
||||
@if $enable-dual {
|
||||
:root:not([#{v.$theme-attr}]) {
|
||||
@include mx.color-scheme(light) {
|
||||
@include light.styles;
|
||||
}
|
||||
|
||||
&[data-mode='dark'] {
|
||||
@include mx.color-scheme(dark) {
|
||||
@include dark.styles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
&:not([data-mode]),
|
||||
&[data-mode='dark'] {
|
||||
@include dark.styles;
|
||||
}
|
||||
|
||||
&[data-mode='light'] {
|
||||
@include light.styles;
|
||||
}
|
||||
}
|
||||
:root {
|
||||
font-size: 16px;
|
||||
|
||||
@include bp.lg {
|
||||
overflow-y: scroll;
|
||||
@@ -369,7 +374,9 @@ main {
|
||||
box-shadow: none;
|
||||
border-color: var(--input-focus-border-color) !important;
|
||||
background: center !important;
|
||||
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
||||
transition:
|
||||
background-color 0.15s ease-in-out,
|
||||
border-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.left {
|
||||
|
||||
@@ -17,7 +17,6 @@ $sidebar-display: 'sidebar-display'; /* the attribute for sidebar display */
|
||||
overflow-y: auto;
|
||||
width: v.$sidebar-width;
|
||||
background: var(--sidebar-bg);
|
||||
border-right: 1px solid var(--sidebar-border-color);
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
@@ -105,6 +104,8 @@ $sidebar-display: 'sidebar-display'; /* the attribute for sidebar display */
|
||||
letter-spacing: 0.25px;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
color: var(--site-title-color);
|
||||
}
|
||||
@@ -207,7 +208,7 @@ $sidebar-display: 'sidebar-display'; /* the attribute for sidebar display */
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
> a {
|
||||
@extend %button;
|
||||
@extend %sidebar-link-hover;
|
||||
@extend %clickable-transition;
|
||||
@@ -227,13 +228,102 @@ $sidebar-display: 'sidebar-display'; /* the attribute for sidebar display */
|
||||
|
||||
#mode-toggle {
|
||||
@extend %button;
|
||||
@extend %sidebar-links;
|
||||
@extend %sidebar-link-hover;
|
||||
@extend %clickable-transition;
|
||||
|
||||
> i {
|
||||
display: none;
|
||||
|
||||
@at-root :root[data-bs-theme='light'][data-theme-persisted]
|
||||
&[data-theme-mode='light'] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@at-root :root[data-bs-theme='dark'][data-theme-persisted]
|
||||
&[data-theme-mode='dark'] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@at-root :root:not([data-theme-persisted]) &[data-theme-mode='system'] {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes menu-pop {
|
||||
from {
|
||||
opacity: 0;
|
||||
translate: 0 0.5rem;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
translate: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menu-pop {
|
||||
from {
|
||||
opacity: 0;
|
||||
translate: 0 0.5rem;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
translate: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
+ .dropdown-menu {
|
||||
background-color: var(--menu-bg);
|
||||
border-color: var(--menu-border-color);
|
||||
box-shadow: var(--menu-shadow-color) 0 1px 4px;
|
||||
border-radius: 0.75rem !important;
|
||||
|
||||
&.show {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
left: -0.25rem !important;
|
||||
-webkit-animation: menu-pop 0.2s ease-out;
|
||||
animation: menu-pop 0.2s ease-out;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
border-radius: 0.5rem;
|
||||
color: var(--sidebar-muted-color);
|
||||
font-size: 90%;
|
||||
|
||||
&.active {
|
||||
font-weight: 600;
|
||||
color: var(--menu-active-color);
|
||||
|
||||
&::after {
|
||||
content: '\f00c';
|
||||
font: var(--fa-font-solid);
|
||||
font-size: 0.75rem;
|
||||
color: var(--sidebar-btn-color);
|
||||
margin-left: auto;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--menu-highlight-bg);
|
||||
}
|
||||
|
||||
> i {
|
||||
color: var(--sidebar-btn-color);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-border {
|
||||
@extend %no-cursor;
|
||||
@include mx.ml-mr(calc((v.$sb-btn-gap - $btn-border-width) / 2));
|
||||
@include mx.ml-mr(0.6rem);
|
||||
|
||||
background-color: var(--sidebar-btn-color);
|
||||
content: '';
|
||||
@@ -241,10 +331,6 @@ $sidebar-display: 'sidebar-display'; /* the attribute for sidebar display */
|
||||
height: $btn-border-width;
|
||||
border-radius: 50%;
|
||||
margin-bottom: $btn-mb;
|
||||
|
||||
@include bp.xxxl {
|
||||
@include mx.ml-mr(calc((v.$sb-btn-gap-lg - $btn-border-width) / 2));
|
||||
}
|
||||
}
|
||||
} /* .sidebar-bottom */
|
||||
} /* #sidebar */
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
rgb(58 55 55 / 40%) 50%,
|
||||
rgb(255 255 255 / 0%) 100%
|
||||
);
|
||||
--menu-bg: rgb(30 30 30);
|
||||
--menu-border-color: rgb(77 77 77 / 60%);
|
||||
--menu-shadow-color: rgb(4 4 4 / 42%);
|
||||
--menu-active-color: rgb(240 248 255 / 63%);
|
||||
--menu-highlight-bg: rgb(90 91 92 / 12%);
|
||||
|
||||
/* Sidebar */
|
||||
--site-title-color: #717070;
|
||||
@@ -67,8 +72,8 @@
|
||||
--btn-share-hover-color: #bfc1ca;
|
||||
--card-bg: #1e1e1e;
|
||||
--card-hover-bg: #464d51;
|
||||
--card-shadow: rgb(21 21 21 / 72%) 0 6px 18px 0,
|
||||
rgb(137 135 135 / 24%) 0 0 0 1px;
|
||||
--card-shadow:
|
||||
rgb(21 21 21 / 72%) 0 6px 18px 0, rgb(137 135 135 / 24%) 0 0 0 1px;
|
||||
--kbd-wrap-color: #6a6a6a;
|
||||
--kbd-text-color: #d3d3d3;
|
||||
--kbd-bg-color: #242424;
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
rgb(232 230 230 / 100%) 50%,
|
||||
rgb(250 250 250 / 0%) 100%
|
||||
);
|
||||
--menu-bg: white;
|
||||
--menu-border-color: white;
|
||||
--menu-shadow-color: rgb(0 0 0 / 16%);
|
||||
--menu-active-color: rgb(91 91 91);
|
||||
--menu-highlight-bg: rgb(243 244 245 / 50%);
|
||||
|
||||
/* Sidebar */
|
||||
--site-title-color: rgb(113 113 113);
|
||||
@@ -59,8 +64,8 @@
|
||||
--btn-share-hover-color: #0d6efd;
|
||||
--card-bg: white;
|
||||
--card-hover-bg: #e2e2e2;
|
||||
--card-shadow: rgb(104 104 104 / 5%) 0 2px 6px 0,
|
||||
rgb(211 209 209 / 15%) 0 0 0 1px;
|
||||
--card-shadow:
|
||||
rgb(104 104 104 / 5%) 0 2px 6px 0, rgb(211 209 209 / 15%) 0 0 0 1px;
|
||||
--footnote-target-bg: lightcyan;
|
||||
--tb-odd-bg: #fbfcfd;
|
||||
--tb-border-color: #eaeaea;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
---
|
||||
---
|
||||
|
||||
@use 'abstracts/variables' with (
|
||||
$theme: '{{ site.theme_mode }}'
|
||||
);
|
||||
|
||||
/* prettier-ignore */
|
||||
@use 'main
|
||||
{%- if jekyll.environment == 'production' -%}
|
||||
|
||||
Reference in New Issue
Block a user