*/ private const LIGHT = [ 'surface' => '#ffffff', 'surface-2' => '#f6f7f8', 'ink' => '#1f2024', 'muted' => '#6b6b6b', 'border' => '#d6d6d6', 'accent' => '#1e874b', 'accent-ink' => '#ffffff', ]; /** * The dark palette. * * For a shop whose theme is dark. Chosen by the shop owner on the settings * screen, never detected. * * @var array */ private const DARK = [ 'surface' => '#1c1e22', 'surface-2' => '#26282d', 'ink' => '#f1f2f4', 'muted' => '#a7abb3', 'border' => '#3a3d44', 'accent' => '#3fae6d', 'accent-ink' => '#0d1f15', ]; /** * Theme-published custom properties, per token. * * Block themes publish their `theme.json` palette as `--wp--preset--color--*`, * which is the one naming convention that is actually standard rather than * guessed. `base` and `contrast` are the two every default theme defines. * * A token with no entry here has no standard source, so it keeps the light * value. Better a known colour than a guess at a variable that may mean * something else entirely in the theme that defined it. * * @var array */ private const THEME_VARS = [ 'surface' => '--wp--preset--color--base', 'ink' => '--wp--preset--color--contrast', 'accent' => '--wp--preset--color--primary', 'accent-ink' => '--wp--preset--color--base', ]; /** * Tokens a shop may override by hand, and the state tokens it may not. * * The state colours are deliberately not configurable: an error that is not * red and a success that is not green cost more in confusion than they gain * in brand fidelity. * * @var array */ private const STATES = [ 'error' => '#c0392b', 'success' => '#1e874b', 'notice' => '#8a6d1f', ]; /** * Shipped values for the non-colour tokens. */ private const RADIUS = 6; private const GAP = '0.75rem'; /** * The rendered block, built at most once per request. * * @var string|null */ private static ?string $css = null; /** * Whether the block has already been attached to a stylesheet. * * @var bool */ private static bool $attached = false; /** * Ship the tokens alongside a stylesheet of ours. * * Called straight after `wp_enqueue_style()` by whatever enqueued it. Only * the first caller in a request actually attaches anything; the rest are * no-ops, because one `:root` block serves every control on the page. * * @param string $handle The stylesheet handle just enqueued. */ public static function attach( string $handle ): void { if ( self::$attached ) { return; } self::$attached = true; // Every value is validated in safeValue(); stripping tags as well keeps // the output safe at the point it is written, whatever feeds the block. wp_add_inline_style( $handle, wp_strip_all_tags( self::css() ) ); } /** * The `:root` block. * * Public so the settings screen can show the shop owner exactly what their * choices produce, rather than describing it. */ public static function css(): string { if ( null !== self::$css ) { return self::$css; } $declarations = ''; foreach ( self::resolve() as $name => $value ) { $declarations .= sprintf( '--rahkar-%s:%s;', $name, $value ); } self::$css = ':root{' . $declarations . '}'; return self::$css; } /** * Every token with its final value. * * @return array */ public static function resolve(): array { $settings = Settings::all( self::SLUG ); $scheme = (string) ( $settings['scheme'] ?? self::SCHEME_LIGHT ); $base = self::SCHEME_DARK === $scheme ? self::DARK : self::LIGHT; $tokens = []; foreach ( self::LIGHT as $name => $light ) { // Indexed without a fallback on purpose: the two palettes must carry // the same keys, and static analysis says so at build time rather than // letting a half-added token quietly resolve to the light value in a // dark shop. $value = $base[ $name ]; // The theme scheme wraps each value in `var()` with our own colour as // the fallback, so it degrades by itself: a theme that publishes the // property wins, and a theme that does not is simply light. That is // what makes this a setting a shop owner can safely pick without // knowing anything about how their theme is built. if ( self::SCHEME_THEME === $scheme && isset( self::THEME_VARS[ $name ] ) ) { $value = sprintf( 'var(%s,%s)', self::THEME_VARS[ $name ], $value ); } $tokens[ $name ] = $value; } // A hand-written override beats everything, including the theme scheme: // it is the escape hatch for classic themes, which publish no standard // palette but do publish their own variables. foreach ( [ 'surface', 'ink', 'accent' ] as $name ) { $override = self::safeValue( (string) ( $settings[ $name . '_override' ] ?? '' ) ); if ( '' !== $override ) { $tokens[ $name ] = $override; } } $radius = (int) ( $settings['radius'] ?? self::RADIUS ); $tokens['radius'] = max( 0, min( 32, $radius ) ) . 'px'; $tokens['gap'] = self::GAP; return array_merge( $tokens, self::STATES ); } /** * Accept a hand-written value, or nothing. * * This ends up inside a `