8
Mode Gelap dan Persistensi Tema
Berkas app-dark.scss tidak menimpa tema terang secara global. Seluruh impor Bootstrap di berkas itu dibungkus selector html[data-bs-theme="dark"] (src/assets/scss/themes/dark/app-dark.scss:111), jadi perpindahan tema cukup mengubah nilai atribut data-bs-theme pada elemen <html>. Nilai atribut itu diurus oleh dua berkas JavaScript dengan pembagian tugas berbeda, dan disimpan di localStorage dengan key theme.
Dua skrip dengan peran berbeda
initTheme.js hanya lima baris dan tidak mendefinisikan fungsi apa pun. Skrip ini mengambil localStorage.getItem('theme'), lalu — bila nilainya ada — memanggil document.documentElement.setAttribute('data-bs-theme', theme) (src/assets/static/js/initTheme.js:1-52). Skrip itu dimuat di dalam <body>, sebelum #app (src/layouts/master.html:173), sehingga atribut sudah terpasang sebelum konten dirender. Skrip ini tidak menyentuh document.body.classList sama sekali.
components/dark.js memegang mesin keadaan sebenarnya. Berkas itu mendefinisikan THEME_KEY = "theme" dan toggleDarkTheme() (src/assets/static/js/components/dark.js:2-104), fungsi setTheme(theme, persist = false) yang menambahkan class ke body, menyetel atribut data-bs-theme, dan menulis localStorage hanya bila persist benar (src/assets/static/js/components/dark.js:17-245), serta initTheme() (src/assets/static/js/components/dark.js:29-48). Bagian akhir berkas memasang listener DOMContentLoaded untuk toggler, lalu memanggil initTheme() pada baris terakhirnya (src/assets/static/js/components/dark.js:50-64). dark.js dimuat di akhir body (src/layouts/master.html:32).
Mesin keadaan tema
stateDiagram-v2
[*] --> TanpaTema
TanpaTema --> Terang: initTheme.js memasang atribut dari localStorage
TanpaTema --> Gelap: matchMedia prefers-color-scheme dark
Terang --> Gelap: setTheme dark dengan persist true
Gelap --> Terang: setTheme light dengan persist true
Gelap --> Gelap: input toggler checked
Terang --> Terang: input toggler unchecked
style TanpaTema fill:#dc2626,color:#fff
style Terang fill:#f59e0b,color:#000
style Gelap fill:#2d7d46,color:#fff
%% fig: Perpindahan keadaan tema light dan dark
%% desc: Halaman mulai tanpa tema, lalu memilih light atau dark dari localStorage atau preferensi sistem, dan toggler memindahkan keadaan di antara keduanya.initTheme() memakai urutan prioritas yang tegas (src/assets/static/js/components/dark.js:29-486). Nilai tersimpan di localStorage menang: return setTheme(storedTheme) dipanggil tanpa argumen persist, jadi nilai tersimpan tidak ditulis ulang. Bila tidak ada nilai tersimpan dan window.matchMedia tidak tersedia, fungsi berhenti. Bila tersedia, ia mendaftarkan listener change pada query (prefers-color-scheme: dark) yang memanggil setTheme(..., true), lalu menyetel tema awal dari mediaQuery.matches, juga dengan persist benar.
⚠️ Cabang terakhir memakai
persist = true. Kunjungan pertama tanpa nilai tersimpan langsung menulisthemekelocalStorage, sehingga sejak itu nilai tersimpan selalu menang atas preferensi sistem — meski listenerchangemasih bisa memperbaruinya ketika preferensi sistem berubah (src/assets/static/js/components/dark.js:29-486).
Aliran nilainya berjalan dari tiga sumber masukan ke satu atribut, lalu berhenti di CSS.
graph LR
LS["localStorage theme"] --> INIT["initTheme.js"]
LS --> SET["setTheme di dark.js"]
TGL["input#toggle-dark"] --> SET
INITD["initTheme di dark.js"] --> SET
MQ["matchMedia prefers-color-scheme dark"] --> INITD
INIT --> ATTR["atribut data-bs-theme di html"]
SET --> ATTR
SET --> CLS["class body dark atau light"]
ATTR --> CSS["blok selector dark di app-dark.scss"]
style LS fill:#336791,color:#fff
style INITD fill:#2d7d46,color:#fff
style SET fill:#2d7d46,color:#fff
style ATTR fill:#f59e0b,color:#000
style CSS fill:#8b5cf6,color:#fff
%% fig: Aliran nilai tema sampai ke CSS
%% desc: localStorage, toggler, dan preferensi sistem masuk ke setTheme atau initTheme, lalu atribut pada html memicu blok selector gelap di app-dark.scss.Penanda yang dipakai bersama
| Penanda | Nilai | Ditulis oleh | Dibaca oleh |
|---|---|---|---|
localStorage | key "theme", nilai "dark" atau "light" | setTheme(theme, true) (src/assets/static/js/components/dark.js:17-245) | initTheme.js (src/assets/static/js/initTheme.js:1-52) dan initTheme() (src/assets/static/js/components/dark.js:29-486) |
atribut <html> | data-bs-theme="dark" atau "light" | setTheme (src/assets/static/js/components/dark.js:17-245) dan initTheme.js (src/assets/static/js/initTheme.js:1-52) | selector SCSS (src/assets/scss/themes/dark/app-dark.scss:111) |
class <body> | dark atau light | setTheme lewat classList.add(theme) (src/assets/static/js/components/dark.js:17-245) | — |
| status toggler | checked bila nilai tersimpan "dark" | listener DOMContentLoaded (src/assets/static/js/components/dark.js:50-647) | — |
⚠️
setThemehanya memanggilclassList.add(theme)dan tidak pernahremove. Setelah pengguna berpindah dari gelap ke terang,bodymenyimpan classdarkdanlightsekaligus, sehingga urutan cascade dibodyyang menentukan hasil akhirnya (src/assets/static/js/components/dark.js:17-245).
Toggler di sidebar
Sakelar tema berada di header sidebar: satu div.theme-toggle berisi ikon matahari, sakelar form-check, dan ikon bulan (src/layouts/sidebar.html:79). Elemen yang diikat oleh JavaScript adalah input bertipe checkbox dengan id="toggle-dark" (src/layouts/sidebar.html:23-2610).
Listener di dark.js mengeset toggler.checked dari nilai localStorage — bukan dari atribut data-bs-theme — lalu menambahkan listener input yang memanggil setTheme(e.target.checked ? "dark" : "light", true) (src/assets/static/js/components/dark.js:50-647). Jadi setiap interaksi pengguna selalu dipersistensi. Perlu dicatat: toggler tidak memanggil toggleDarkTheme(), fungsi itu hanya tersedia sebagai API. Berkas _mazer-dark.scss mengambil bagian kecil pada tampilan sakelar, yaitu warna .theme-toggle menjadi #9899ac (src/assets/scss/themes/dark/_mazer-dark.scss:31-3411).
Lapisan SCSS khusus gelap
Urutan impor app-dark.scss menentukan bahwa variabel gelap dipasang setelah variabel Bootstrap: ./variables, bootstrap/scss/functions, variables, variables-dark, mixins, lalu ./variables-dark dan ./root (src/assets/scss/themes/dark/app-dark.scss:3-912). Setelah blok Bootstrap di dalam selector gelap selesai, berkas itu menambahkan ./../../fonts, ./../../mazer, ./mazer-dark, dan sekali lagi bootstrap/scss/utilities (src/assets/scss/themes/dark/app-dark.scss:52-5813).
_variables-dark.scss menyalakan flag $dark-mode: true (src/assets/scss/themes/dark/_variables-dark.scss:314), lalu menimpa peta $theme-colors-light dengan tint-color() dari warna tema — misalnya primary menjadi tint-color($primary, 25%) dan dark menjadi #666 (src/assets/scss/themes/dark/_variables-dark.scss:5-1315). Di bagian akhir, berkas itu mengganti variabel Mazer secara langsung: $sidebar-bg: #1e1e2d, $body-bg: #151521, dan $body-color: #c2c2d9 (src/assets/scss/themes/dark/_variables-dark.scss:113-12416). _root.scss melengkapi lapisan ini dengan mendeklarasikan ulang CSS custom property di dalam html[data-bs-theme="dark"] (src/assets/scss/themes/dark/_root.scss:117). Rincian fungsi $dark-mode dan peta warna tema dibahas di Arsitektur SCSS dan Variabel Tema.
Titik pemasangan script per layout
| Layout | Stylesheet tema | initTheme.js | dark.js |
|---|---|---|---|
master.html | app.scss lalu app-dark.scss (src/layouts/master.html:9-1018) | di dalam <body> (src/layouts/master.html:173) | akhir <body> (src/layouts/master.html:328) |
rtl.html | app.rtl.css lalu app-dark.rtl.css (src/layouts/rtl.html:9-1019) | di dalam <body> (src/layouts/rtl.html:1720) | akhir <body> (src/layouts/rtl.html:3221) |
error.html | hanya app.scss dan pages/error.scss (src/layouts/error.html:8-1522) | di dalam <body> (src/layouts/error.html:8-1522) | tidak dimuat |
⚠️
error.htmlmemasanginitTheme.jstetapi tidak memuatapp-dark.scss. Atributdata-bs-themetetap terpasang, namun tidak ada blok selector gelap yang membacanya, jadi halaman error tidak ikut berubah gelap (src/layouts/error.html:8-1522). Versi RTL memakai berkas hasil compileapp-dark.rtl.css; proses build-nya dijelaskan di Layout RTL dan Build rtlcss.
Pemakaian lewat CDN mengikuti urutan yang sama: dua stylesheet dimuat berurutan (README.md:71-7223), initTheme.js di awal <body>, dan dark.js di akhir <body> (README.md:76-8124).