Files
vendoo/public/design-system/theme-runtime.js
T

159 lines
6.3 KiB
JavaScript

const STORAGE_KEY = 'vendoo-theme-state-v2';
const LEGACY_KEY = 'vendoo-theme';
const BUILTIN = new Set(['light', 'dark', 'contrast', 'system']);
let activeCustomProperties = [];
function mediaDark() {
return window.matchMedia?.('(prefers-color-scheme: dark)').matches;
}
function mediaReducedMotion() {
return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
}
function clearCustomProperties() {
for (const property of activeCustomProperties) document.documentElement.style.removeProperty(property);
activeCustomProperties = [];
}
function effectiveBuiltin(themeId) {
return themeId === 'system' ? (mediaDark() ? 'dark' : 'light') : themeId;
}
function normalizeState(input = {}) {
const themeId = String(input.theme_id || input.themeId || 'system');
return {
theme_id: themeId,
density: ['compact', 'comfortable', 'spacious'].includes(input.density) ? input.density : 'comfortable',
reduced_motion: ['system', 'reduce', 'allow'].includes(input.reduced_motion || input.reducedMotion) ? (input.reduced_motion || input.reducedMotion) : 'system',
custom_theme: input.custom_theme || input.customTheme || null,
};
}
function applyState(input, { persist = true, dispatch = true } = {}) {
const state = normalizeState(input);
clearCustomProperties();
const custom = state.custom_theme && state.custom_theme.id === state.theme_id ? state.custom_theme : null;
const baseTheme = custom ? custom.base_theme : state.theme_id;
const resolved = effectiveBuiltin(BUILTIN.has(baseTheme) ? baseTheme : 'light');
document.documentElement.dataset.theme = resolved;
document.documentElement.dataset.themePreference = state.theme_id;
document.documentElement.dataset.density = state.density;
const reduce = state.reduced_motion === 'reduce' || (state.reduced_motion === 'system' && mediaReducedMotion());
document.documentElement.dataset.reducedMotion = reduce ? 'true' : 'false';
if (custom?.css_values) {
for (const [property, value] of Object.entries(custom.css_values)) {
if (!/^--vd-[a-z0-9-]+$/.test(property)) continue;
document.documentElement.style.setProperty(property, String(value));
activeCustomProperties.push(property);
}
}
if (persist) localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
if (dispatch) window.dispatchEvent(new CustomEvent('vendoo:theme-changed', { detail: { ...state, resolved } }));
return { ...state, resolved };
}
function readStoredState() {
try {
const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
if (parsed) return normalizeState(parsed);
} catch {}
const legacy = localStorage.getItem(LEGACY_KEY);
return normalizeState({ theme_id: BUILTIN.has(legacy) ? legacy : 'system' });
}
function previewTheme({ base_theme = 'light', css_values = {}, density = 'comfortable', reduced_motion = 'system' } = {}) {
return applyState({ theme_id: '__preview__', density, reduced_motion, custom_theme: { id: '__preview__', base_theme, css_values } }, { persist: false });
}
function restoreStored() {
return applyState(readStoredState(), { persist: false });
}
function loadUxFoundation() {
if (!document.querySelector('link[data-vendoo-ux-foundation]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/ux-foundation.css?v=1.44.0';
stylesheet.dataset.vendooUxFoundation = '1.44.0';
document.head.append(stylesheet);
}
const loadBehavior = () => {
if (document.querySelector('script[data-vendoo-ux-foundation]')) return;
const script = document.createElement('script');
script.src = '/ux-foundation.js?v=1.44.0';
script.dataset.vendooUxFoundation = '1.44.0';
script.async = false;
document.body.append(script);
};
if (document.readyState === 'complete') loadBehavior();
else window.addEventListener('load', loadBehavior, { once: true });
}
function loadFluxStudioAssets() {
if (!document.querySelector('link[data-ux-flux-studio]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/flux-studio-ux.css?v=1.44.0';
stylesheet.setAttribute('data-ux-flux-studio', 'style');
document.head.append(stylesheet);
}
const loadBehavior = () => {
if (document.querySelector('script[data-ux-flux-studio]')) return;
const script = document.createElement('script');
script.src = '/flux-studio-ux.js?v=1.44.0';
script.setAttribute('data-ux-flux-studio', 'script');
script.async = false;
document.body.append(script);
};
if (document.readyState === 'complete') loadBehavior();
else window.addEventListener('load', loadBehavior, { once: true });
}
function loadQualityCenterAssets() {
if (!document.querySelector('link[data-ux-quality-center]')) {
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = '/quality-center-ux.css?v=1.44.0';
stylesheet.setAttribute('data-ux-quality-center', 'style');
document.head.append(stylesheet);
}
const loadBehavior = () => {
if (document.querySelector('script[data-ux-quality-center]')) return;
const script = document.createElement('script');
script.src = '/quality-center-ux.js?v=1.44.0';
script.setAttribute('data-ux-quality-center', 'script');
script.async = false;
document.body.append(script);
};
if (document.readyState === 'complete') loadBehavior();
else window.addEventListener('load', loadBehavior, { once: true });
}
applyState(readStoredState(), { persist: false, dispatch: false });
loadUxFoundation();
loadFluxStudioAssets();
loadQualityCenterAssets();
window.matchMedia?.('(prefers-color-scheme: dark)').addEventListener?.('change', () => {
if (document.documentElement.dataset.themePreference === 'system') restoreStored();
});
window.matchMedia?.('(prefers-reduced-motion: reduce)').addEventListener?.('change', () => restoreStored());
window.VendooTheme = Object.freeze({
get preference() { return document.documentElement.dataset.themePreference || 'system'; },
get resolved() { return document.documentElement.dataset.theme || 'light'; },
applyState,
previewTheme,
restoreStored,
setTheme(themeId, { persist = true } = {}) {
const current = readStoredState();
return applyState({ ...current, theme_id: BUILTIN.has(themeId) ? themeId : 'system', custom_theme: null }, { persist });
},
supported: Object.freeze([...BUILTIN]),
});