-`, and a schema writing `{color.brand}` then points straight at * that palette on the theme settings page. * * Why a cascade layer * The site palette is the default layer, and tokens a scene declares for itself have to be able to * override it — otherwise existing scenes (the reference site's demo, say) would have their appearance * changed by the site palette. Both write `:root` at equal specificity, so the only tie-breaker left is * source order, and order is fragile (a different hook priority flips it, silently). * Inside a cascade layer the rule becomes "unlayered always beats layered", regardless of order. * * Why literal values rather than var(--primary) * An alias (`--af-color-brand: var(--primary)`) works on the front end and not in the editor canvas: * the canvas is an iframe with none of the theme's CSS loaded, `var(--primary)` does not resolve, and * the colour silently turns transparent. Literal values let the front end and the canvas share one block * of CSS with a single code path. * Light and dark are handled by the [data-theme="dark"] block we emit ourselves, with a selector * matching the theme's. */ class SiteTokens { /** The cascade layer's name. Unlayered scene tokens always beat it. */ const LAYER = 'aumframe-site'; /** Legal characters for group and token names — they go straight into CSS custom property names. */ const NAME_RE = '/^[a-z][a-z0-9-]*$/'; /** * The site tokens the theme declares. * * Shape: ['color' => ['brand' => ['label'=>'Primary','light'=>'#3366cc','dark'=>'#6699ff']]] * A missing dark falls back to light (which is the case when the theme has no day/night switch). */ public static function all(): array { $raw = apply_filters('aumframe/site_tokens', []); if (!is_array($raw)) { return []; } $out = []; foreach ($raw as $group => $items) { if (!is_string($group) || !preg_match(self::NAME_RE, $group) || !is_array($items)) { continue; } foreach ($items as $name => $def) { if (!is_string($name) || !preg_match(self::NAME_RE, $name)) { continue; } $light = self::safe_value(is_array($def) ? ($def['light'] ?? null) : $def); if ($light === null) { continue; } $dark = is_array($def) ? self::safe_value($def['dark'] ?? null) : null; $out[$group][$name] = [ 'label' => is_array($def) && !empty($def['label']) ? (string) $def['label'] : $name, 'light' => $light, 'dark' => $dark ?? $light, ]; } } return $out; } /** * Only forms this recognises as a CSS colour or length are allowed through. * * These values end up verbatim inside ". * * Precise matching leaves residue: ``, * which does not actually escape (a `