62 lines
3.1 KiB
TypeScript
62 lines
3.1 KiB
TypeScript
import { runtimeThemeContract, runtimeGradientGroups, type RuntimeThemeToken } from "./contract"
|
|
import { invalidateThemeRequest } from "./requests"
|
|
|
|
type SavedProperty = { property: string; value: string; priority: string }
|
|
const applied = new WeakMap<HTMLElement, { properties: SavedProperty[]; scope: string | null }>()
|
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
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
|
|
}
|