- Add i18n (English + 简体中文) with a locale store, $t() magic, and a language toggle (persisted) - Add light/dark/system theme switching via a theme store and [data-theme] token overrides - Set the theme before paint to avoid a flash of the wrong theme - Add frontend version constant web/version.js (window.YDROPBX_VERSION) shown in the app footer - Update AGENTS.md version-control convention to mention web/version.js - Bump version to 0.1.1 across frontend, webui changelog, and backend constant
65 lines
1.9 KiB
JavaScript
65 lines
1.9 KiB
JavaScript
/**
|
|
* Theme manager.
|
|
*
|
|
* Supports `light` / `dark` / `system`. The resolved theme is written to the
|
|
* `data-theme` attribute on <html>, which the CSS token layer keys off of.
|
|
* The `system` mode tracks the OS preference live. The choice is persisted to
|
|
* localStorage. A tiny inline script in each page sets the initial attribute
|
|
* before paint to avoid a flash of the wrong theme.
|
|
*/
|
|
(function () {
|
|
function register() {
|
|
Alpine.store('theme', {
|
|
mode: 'system', // 'light' | 'dark' | 'system'
|
|
resolved: 'light',
|
|
|
|
init() {
|
|
let saved = null;
|
|
try {
|
|
saved = localStorage.getItem('yd.theme');
|
|
} catch (e) {}
|
|
if (saved === 'light' || saved === 'dark' || saved === 'system') {
|
|
this.mode = saved;
|
|
}
|
|
this.apply();
|
|
|
|
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
const onChange = () => {
|
|
if (this.mode === 'system') this.apply();
|
|
};
|
|
if (mq.addEventListener) mq.addEventListener('change', onChange);
|
|
else if (mq.addListener) mq.addListener(onChange);
|
|
},
|
|
|
|
set(mode) {
|
|
this.mode = mode;
|
|
try {
|
|
localStorage.setItem('yd.theme', mode);
|
|
} catch (e) {}
|
|
this.apply();
|
|
},
|
|
|
|
cycle() {
|
|
const order = ['light', 'dark', 'system'];
|
|
const next = order[(order.indexOf(this.mode) + 1) % order.length];
|
|
this.set(next);
|
|
},
|
|
|
|
icon() {
|
|
if (this.mode === 'system') return 'monitor';
|
|
return this.mode === 'dark' ? 'moon' : 'sun';
|
|
},
|
|
|
|
apply() {
|
|
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
const resolved = this.mode === 'system' ? (mq.matches ? 'dark' : 'light') : this.mode;
|
|
this.resolved = resolved;
|
|
document.documentElement.setAttribute('data-theme', resolved);
|
|
},
|
|
});
|
|
}
|
|
|
|
if (window.Alpine) register();
|
|
else document.addEventListener('alpine:init', register);
|
|
})();
|