feat(web): add i18n, light/dark theme switching, and version constant
- 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
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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);
|
||||
})();
|
||||
Reference in New Issue
Block a user