'#ffffff', 'text' => '#1d2327', 'button' => '#1d2327', 'button_text' => '#ffffff', 'border' => '#737980', 'font_size' => '16', 'spacing' => '24', 'radius' => '4', ); } public static function colors() { return array( 'background' => __( 'Form background', 'accessible-forms-by-accessible-org' ), 'text' => __( 'Text and focus outline', 'accessible-forms-by-accessible-org' ), 'button' => __( 'Button background', 'accessible-forms-by-accessible-org' ), 'button_text' => __( 'Button text', 'accessible-forms-by-accessible-org' ), 'border' => __( 'Field borders', 'accessible-forms-by-accessible-org' ), ); } public static function presets() { $presets = array( 'light' => array( 'label' => __( 'Classic light', 'accessible-forms-by-accessible-org' ), 'colors' => array_intersect_key( self::defaults(), self::colors() ), ), 'ocean' => array( 'label' => __( 'Ocean blue', 'accessible-forms-by-accessible-org' ), 'colors' => array( 'background' => '#f4f8ff', 'text' => '#172b4d', 'button' => '#1856a5', 'button_text' => '#ffffff', 'border' => '#687b96', ), ), 'forest' => array( 'label' => __( 'Forest green', 'accessible-forms-by-accessible-org' ), 'colors' => array( 'background' => '#f5faf6', 'text' => '#193c2a', 'button' => '#24613e', 'button_text' => '#ffffff', 'border' => '#687c6e', ), ), 'dark' => array( 'label' => __( 'Midnight', 'accessible-forms-by-accessible-org' ), 'colors' => array( 'background' => '#111827', 'text' => '#f9fafb', 'button' => '#93c5fd', 'button_text' => '#111827', 'border' => '#94a3b8', ), ), ); // Add-ons may add themes, such as saved brand presets. Every theme must pass the contrast rules. if ( ! function_exists( 'apply_filters' ) || ! class_exists( Extensions::class ) ) { return $presets; } $themes = array(); foreach ( (array) Extensions::filter( 'accessible_forms_appearance_presets', $presets ) as $key => $theme ) { [$values, $errors] = self::sanitize( $theme['colors'] ?? null ); if ( ! $errors && is_string( $theme['label'] ?? null ) ) { $themes[ sanitize_key( (string) $key ) ] = array( 'label' => $theme['label'], // Only what the theme sets: a colour theme must not reset text size or spacing. 'colors' => array_intersect_key( $values, (array) $theme['colors'] ), ); } } return $themes + $presets; } /** Text always meets the normal-size text threshold, regardless of font size. */ public static function rules() { return array( array( 'foreground' => 'text', 'background' => 'background', 'minimum' => 4.5, 'label' => __( 'Text on form background', 'accessible-forms-by-accessible-org' ), ), array( 'foreground' => 'button_text', 'background' => 'button', 'minimum' => 4.5, 'label' => __( 'Text on button', 'accessible-forms-by-accessible-org' ), ), array( 'foreground' => 'button', 'background' => 'background', 'minimum' => 3, 'label' => __( 'Button against form background', 'accessible-forms-by-accessible-org' ), ), array( 'foreground' => 'border', 'background' => 'background', 'minimum' => 3, 'label' => __( 'Field borders against form background', 'accessible-forms-by-accessible-org' ), ), ); } /** Relative luminance from the WCAG sRGB definition. */ private static function luminance( $hex ) { $channels = array(); foreach ( array( 1, 3, 5 ) as $offset ) { $value = hexdec( substr( $hex, $offset, 2 ) ) / 255; $channels[] = $value <= 0.04045 ? $value / 12.92 : pow( ( $value + 0.055 ) / 1.055, 2.4 ); } return 0.2126 * $channels[0] + 0.7152 * $channels[1] + 0.0722 * $channels[2]; } public static function contrast( $first, $second ) { $a = self::luminance( $first ); $b = self::luminance( $second ); return ( max( $a, $b ) + 0.05 ) / ( min( $a, $b ) + 0.05 ); } /** Reject invalid palettes on the server as well as in the editor. */ public static function sanitize( $raw ) { $appearance = self::defaults(); $errors = array(); if ( ! is_array( $raw ) ) { return array( $appearance, array( __( 'Choose valid form appearance settings.', 'accessible-forms-by-accessible-org' ) ) ); } foreach ( self::colors() as $key => $label ) { $value = $raw[ $key ] ?? $appearance[ $key ]; if ( ! is_string( $value ) || ! preg_match( '/^#[a-fA-F0-9]{6}$/D', $value ) ) { $errors[] = sprintf( /* translators: %s: color setting label. */ __( '%s needs a six-digit hex color, such as #ffffff.', 'accessible-forms-by-accessible-org' ), $label ); } else { $appearance[ $key ] = strtolower( $value ); } } if ( ! $errors ) { foreach ( self::rules() as $rule ) { $ratio = self::contrast( $appearance[ $rule['foreground'] ], $appearance[ $rule['background'] ] ); // Do not round before comparing: 4.499 is below 4.5. if ( $ratio < $rule['minimum'] ) { $errors[] = sprintf( /* translators: 1: color pair, 2: contrast ratio, 3: minimum contrast ratio. */ __( '%1$s has %2$s:1 contrast; at least %3$s:1 is required. Choose a darker or lighter color.', 'accessible-forms-by-accessible-org' ), $rule['label'], number_format( floor( $ratio * 100 ) / 100, 2 ), (string) $rule['minimum'] ); } } } foreach ( array( 'font_size' => array( '16', '18', '20' ), 'spacing' => array( '16', '24', '32' ), 'radius' => array( '0', '4', '8', '12' ), ) as $key => $allowed ) { $value = $raw[ $key ] ?? $appearance[ $key ]; if ( ! is_scalar( $value ) || ! in_array( (string) $value, $allowed, true ) ) { $errors[] = __( 'Choose one of the available text sizes, field spacings, and corner styles.', 'accessible-forms-by-accessible-org' ); } else { $appearance[ $key ] = (string) $value; } } return array( $appearance, array_unique( $errors ) ); } /** Older or externally changed data must not bypass the rendering safeguards. */ public static function safe( $raw ) { [$appearance, $errors] = self::sanitize( $raw ); return $errors ? self::defaults() : $appearance; } /** Values are allowlisted before being used in an escaped HTML attribute. */ public static function css( $raw ) { $appearance = self::safe( $raw ); $declarations = array(); foreach ( $appearance as $key => $value ) { $unit = isset( self::colors()[ $key ] ) ? '' : ( 'font_size' === $key ? 'rem' : 'px' ); // Relative text sizes respect the user's browser font settings. if ( 'font_size' === $key ) { $value = (string) ( (int) $value / 16 ); } $declarations[] = '--af-' . str_replace( '_', '-', $key ) . ':' . $value . $unit; } // Native date/time indicators use the browser's UI palette, not text color. // Derive it from the validated surface, including custom and older palettes. $declarations[] = '--af-color-scheme:' . ( self::contrast( '#ffffff', $appearance['background'] ) > self::contrast( '#000000', $appearance['background'] ) ? 'dark' : 'light' ); return implode( ';', $declarations ); } }