// Apply the server-resolved theme to the document as CSS custom properties. // // The effective token set is resolved server-side and arrives on // `settings.theme` (see server/src/utils/themeResolve.js). The client's only // job is to write it onto — and, crucially, to take back what it wrote // last time, which is the part with actual logic and the reason this lives in // its own testable module. // // Why removal matters: an admin who resets the theme, or switches from a preset // that sets --bg to one that does not, gets a payload that no longer mentions // that variable. Inline properties are not cleared by writing a smaller object // over them, so without an explicit removeProperty the old value would stick // until a reload. That would make "Reset to defaults" look broken. // // Everything written here is a value the server validated against a closed set // (hex color, curated font stack, bounded px length, listed shadow). The client // deliberately does not re-validate — it would be a second, drifting authority. // It does refuse anything that is not a `--custom-property`, which is the one // check that costs nothing and stops a token map from reaching an ordinary CSS // property. const CUSTOM_PROPERTY = /^--[a-zA-Z0-9-_]+$/ /** * @param {CSSStyleDeclaration} style usually document.documentElement.style * @param {Record|null|undefined} tokens the new theme, or * null/absent for "no admin theme" — which clears everything previously set * @param {string[]} [applied] the keys this function wrote last time * @returns {string[]} the keys now applied, to pass back on the next call */ export function applyThemeTokens(style, tokens, applied = []) { const next = [] if (tokens && typeof tokens === 'object') { for (const [name, value] of Object.entries(tokens)) { if (!CUSTOM_PROPERTY.test(name) || typeof value !== 'string' || value === '') continue style.setProperty(name, value) next.push(name) } } // Take back only what we set ourselves. Anything else on the element's inline // style belongs to someone else (SiteContext's own --accent line, a future // feature) and is not ours to clear. for (const name of applied) { if (!next.includes(name)) style.removeProperty(name) } return next }