import { runtimeThemeContract, runtimeGradientGroups, type RuntimeThemeToken } from "./contract" import { invalidateThemeRequest } from "./requests" type SavedProperty = { property: string; value: string; priority: string } const applied = new WeakMap() const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value) /** Restore the host's previous inline values and scope. */ export function resetRuntimeTheme(root: HTMLElement) { invalidateThemeRequest(root) const previous = applied.get(root) if (!previous) return for (const { property, value, priority } of previous.properties) { if (value) root.style.setProperty(property, value, priority) else root.style.removeProperty(property) } if (previous.scope === null) root.removeAttribute("data-hm-theme") else root.setAttribute("data-hm-theme", previous.scope) applied.delete(root) } /** Validate without touching the DOM (editors/loaders can preserve their last valid value). */ export function isRuntimeTheme(theme: unknown): theme is import("./contract").RuntimeTheme { if (!isRecord(theme) || theme.schemaVersion !== 1 || !isRecord(theme.tokens)) return false const tokens = theme.tokens for (const group of runtimeGradientGroups) { const count = group.filter(key => Object.hasOwn(tokens, key)).length if (count !== 0 && count !== group.length) return false } for (const [key, value] of Object.entries(tokens)) { if (!Object.hasOwn(runtimeThemeContract, key) || typeof value !== "string") return false const definition = runtimeThemeContract[key as RuntimeThemeToken] const valid = definition.type === "radius" ? /^(?:[2-9]|1[0-2])px$/.test(value) : /^#[0-9a-fA-F]{6}$/.test(value) if (!valid) return false } return true } /** Atomic validation. Invalid documents release the previous override to host/default. */ export function applyRuntimeTheme(root: HTMLElement, theme: unknown): boolean { if (!isRuntimeTheme(theme)) { resetRuntimeTheme(root); return false } const values: (readonly [string, string])[] = Object.entries(theme.tokens).map(([key, value]) => [runtimeThemeContract[key as RuntimeThemeToken].property, value!] as const) // Reviewed Gray default comes from the existing palette. Retain custom cancel // overrides without changing the public token or the full Default preset. const cancel = theme.tokens["color.action.cancel"] if (cancel && cancel.toLowerCase() !== runtimeThemeContract["color.action.cancel"].defaultValue) values.push(["--hm-button-cancel-runtime-ink", cancel]) resetRuntimeTheme(root) const scope = root.getAttribute("data-hm-theme") const properties = values.map(([property]) => ({ property, value: root.style.getPropertyValue(property), priority: root.style.getPropertyPriority(property), })) root.setAttribute("data-hm-theme", "runtime") for (const [property, value] of values) root.style.setProperty(property, value) applied.set(root, { properties, scope }) return true }