20, 'js_kb' => 40, 'html_kb' => 60, 'lcp_ms' => 1200, 'fcp_ms' => 1000, 'tbt_ms' => 50, 'cls' => 0.01, 'inp_ms' => 100, 'dom_nodes_mobile' => 800, 'dom_nodes_desktop' => 1200 ]; public static function boot(): void { add_action( 'wp_abilities_api_categories_init', [ self::class, 'register_category' ] ); add_action( 'wp_abilities_api_init', [ self::class, 'register' ] ); } public static function register_category(): void { if ( function_exists( 'wp_register_ability_category' ) && ! wp_has_ability_category( self::CATEGORY ) ) { wp_register_ability_category( self::CATEGORY, [ 'label' => __( 'Rooster Zero theme', 'ai-eshop-optimizer' ), 'description' => __( 'Read the theme manifest, tokens, templates, patterns, slots and settings; change them through draft, preview, apply and undo.', 'ai-eshop-optimizer' ) ] ); } } public static function can_read(): bool { return current_user_can( 'edit_theme_options' ) || current_user_can( 'manage_woocommerce' ); } public static function can_write(): bool { if ( ! current_user_can( 'edit_theme_options' ) ) { return false; } if ( defined( 'AIEO_CAPABILITY' ) && ! current_user_can( (string) AIEO_CAPABILITY ) ) { return false; } return true; } public static function register(): void { if ( ! function_exists( 'wp_register_ability' ) ) { return; } $read = [ 'readonly' => true, 'idempotent' => true, 'destructive' => false ]; $write = [ 'readonly' => false, 'idempotent' => true, 'destructive' => false ]; $obj = static fn ( array $props = [], array $required = [] ): array => [ 'type' => 'object', 'properties' => $props, 'required' => $required, 'additionalProperties' => true ]; $mode = [ 'mode' => [ 'type' => 'string', 'enum' => [ 'draft', 'apply' ], 'default' => 'draft', 'description' => 'draft validates and stores the change and returns a preview URL; apply needs the change_id of a draft and returns an undo token.' ], 'change_id' => [ 'type' => 'string' ] ]; $out = $obj(); $reads = [ 'get-manifest' => [ __( 'Get theme manifest', 'ai-eshop-optimizer' ), __( 'Theme version and contracts, style variations, templates, parts, patterns, slots and pins, adapter modes, budgets and the last measured scores.', 'ai-eshop-optimizer' ), 'manifest' ], 'get-design-tokens' => [ __( 'Get design tokens', 'ai-eshop-optimizer' ), __( 'The palette, font families and sizes, spacing scale and custom tokens of the active theme.json plus the user overrides and the active variations.', 'ai-eshop-optimizer' ), 'design_tokens' ], 'list-templates' => [ __( 'List templates', 'ai-eshop-optimizer' ), __( 'Block templates and template parts with their source (theme or user).', 'ai-eshop-optimizer' ), 'templates' ], 'list-patterns' => [ __( 'List patterns', 'ai-eshop-optimizer' ), __( 'The theme patterns with titles, categories and keywords.', 'ai-eshop-optimizer' ), 'patterns' ], 'get-settings' => [ __( 'Get settings', 'ai-eshop-optimizer' ), __( 'The theme settings: general, performance, elementor, typography, interactivity.', 'ai-eshop-optimizer' ), 'settings' ], 'get-performance-report' => [ __( 'Get performance report', 'ai-eshop-optimizer' ), __( 'Budgets, the loading policy in force and the last imported Lighthouse matrix.', 'ai-eshop-optimizer' ), 'performance' ], 'list-slots' => [ __( 'List template slots', 'ai-eshop-optimizer' ), __( 'Every slot with its candidates and current pins.', 'ai-eshop-optimizer' ), 'slots' ], ]; foreach ( $reads as $name => [ $label, $description, $method ] ) { wp_register_ability( self::CATEGORY . '/' . $name, [ 'label' => $label, 'description' => $description, 'category' => self::CATEGORY, 'output_schema' => $out, 'execute_callback' => [ self::class, $method ], 'permission_callback' => [ self::class, 'can_read' ], 'meta' => [ 'annotations' => $read, 'show_in_rest' => true ], ] ); } $writes = [ 'set-design-tokens' => [ __( 'Set design tokens', 'ai-eshop-optimizer' ), __( 'Change palette colours, font families or custom tokens (written to the user global styles). Input: tokens { color: {slug: css colour}, fontFamily: {slug: stack}, custom: {name: value}, scheme: {slug: hex or rgb colour} (the other scheme\'s own colours, over the palette the theme derives for it) }.', 'ai-eshop-optimizer' ), $obj( $mode + [ 'tokens' => $obj( [ 'color' => $obj(), 'fontFamily' => $obj(), 'custom' => $obj(), 'scheme' => $obj() ] ) ] ) ], 'apply-style-variation' => [ __( 'Apply style variation', 'ai-eshop-optimizer' ), __( 'Apply a colour or typography variation shipped by the theme (slug from the manifest).', 'ai-eshop-optimizer' ), $obj( $mode + [ 'variation' => [ 'type' => 'string' ], 'kind' => [ 'type' => 'string', 'enum' => [ 'colors', 'typography' ] ] ] ) ], 'set-settings' => [ __( 'Set settings', 'ai-eshop-optimizer' ), __( 'Change theme settings. Input: group (general|design|performance|elementor|typography), values {key: value}. Design: authority (theme|plugins), shape (soft|square|round), buttons and cards (none|sm|md|lg|xl|pill, empty to follow the shape), accents (a list of families: slug, name, light, dark, textLight?, textDark?; each a block style accent- and a page Accent), header_actions (the one place that decides the header\'s actions, in order: [{action: search|language|scheme|text-size|fullscreen|academy|account|wishlist|cart, label, text}]; text: its word beside the icon on computers; a label makes the search a pill and the account a button for visitors; scheme, text-size and fullscreen are the appearance controls visitors get; a plugin adds actions through the filter rooster/header/actions; empty: no actions), header_actions_phone (the phone header\'s own list, icons only, empty included; null: the computers\' list in the theme\'s places), header_density (default|compact), header_border (always|scroll), header_surface (solid|transparent-then-glass), scheme_icon (current|target), logo_height_phone (the header logo\'s height on phones in px; empty: three quarters of logo_height).', 'ai-eshop-optimizer' ), $obj( $mode + [ 'group' => [ 'type' => 'string', 'enum' => [ 'general', 'design', 'performance', 'elementor', 'typography' ] ], 'values' => $obj() ] ) ], 'pin-template' => [ __( 'Pin template', 'ai-eshop-optimizer' ), __( 'Pin a candidate template to a slot, as its default or as a rule (match: {kind, ...}).', 'ai-eshop-optimizer' ), $obj( $mode + [ 'slot' => [ 'type' => 'string' ], 'template' => [ 'type' => 'string' ], 'match' => $obj(), 'priority' => [ 'type' => 'integer' ], 'dry_run' => [ 'type' => 'boolean' ] ] ) ], 'unpin-template' => [ __( 'Unpin template', 'ai-eshop-optimizer' ), __( 'Remove a slot default or a rule (match).', 'ai-eshop-optimizer' ), $obj( $mode + [ 'slot' => [ 'type' => 'string' ], 'match' => $obj() ] ) ], 'set-template-part' => [ __( 'Set template part variant', 'ai-eshop-optimizer' ), __( 'Swap the header or footer for another part shipped by the theme (slug), site wide.', 'ai-eshop-optimizer' ), $obj( $mode + [ 'area' => [ 'type' => 'string', 'enum' => [ 'header', 'footer' ] ], 'variant' => [ 'type' => 'string' ] ] ) ], 'set-defer-policy' => [ __( 'Set defer policy', 'ai-eshop-optimizer' ), __( 'Turn the deferred layer on or off and set the device variants.', 'ai-eshop-optimizer' ), $obj( $mode + [ 'defer' => [ 'type' => 'boolean' ], 'device_variants' => [ 'type' => 'boolean' ] ] ) ], 'set-speculation-policy' => [ __( 'Set speculation policy', 'ai-eshop-optimizer' ), __( 'Set the speculative loading eagerness: off, conservative, moderate or eager.', 'ai-eshop-optimizer' ), $obj( $mode + [ 'policy' => [ 'type' => 'string', 'enum' => [ 'off', 'conservative', 'moderate', 'eager' ] ] ] ) ], ]; foreach ( $writes as $name => [ $label, $description, $schema ] ) { wp_register_ability( self::CATEGORY . '/' . $name, [ 'label' => $label, 'description' => $description, 'category' => self::CATEGORY, 'input_schema' => $schema, 'output_schema' => $out, 'execute_callback' => static fn ( $input ) => self::write( $name, (array) $input ), 'permission_callback' => [ self::class, 'can_write' ], 'meta' => [ 'annotations' => $write, 'show_in_rest' => true ], ] ); } wp_register_ability( self::CATEGORY . '/undo', [ 'label' => __( 'Undo a change', 'ai-eshop-optimizer' ), 'description' => __( 'Restore the state recorded when a change was applied. Input: undo_token.', 'ai-eshop-optimizer' ), 'category' => self::CATEGORY, 'input_schema' => $obj( [ 'undo_token' => [ 'type' => 'string' ] ], [ 'undo_token' ] ), 'output_schema' => $out, 'execute_callback' => static fn ( $input ) => Changes::undo( (string) ( ( (array) $input )['undo_token'] ?? '' ) ), 'permission_callback' => [ self::class, 'can_write' ], 'meta' => [ 'annotations' => $write, 'show_in_rest' => true ], ] ); } public static function manifest(): array { $tokens = json_decode( (string) file_get_contents( ROOSTER_DIR . '/tokens/tokens.json' ), true ); $slots = Slots::instance(); $templates = self::templates(); return [ 'theme' => [ 'slug' => 'rooster', 'version' => ROOSTER_VERSION, 'contracts' => (array) ( $tokens['meta']['contracts'] ?? [] ) ], 'variations' => self::variations(), 'templates' => array_column( $templates['templates'], 'slug' ), 'parts' => array_column( $templates['parts'], 'slug' ), 'patterns' => array_column( self::patterns()['patterns'], 'name' ), 'slots' => array_keys( $slots->all() ), 'pins' => $slots->pins(), 'adapter_modes' => self::adapter_modes(), 'settings' => self::settings()['settings'], 'budgets' => self::BUDGETS, 'last_measured' => get_option( 'rooster_perf_last', null ), ]; } public static function design_tokens(): array { $theme = \WP_Theme_JSON_Resolver::get_theme_data()->get_data(); $user = \WP_Theme_JSON_Resolver::get_user_data()->get_data(); return [ 'theme' => [ 'color' => $theme['settings']['color']['palette'] ?? [], 'fontFamily' => $theme['settings']['typography']['fontFamilies'] ?? [], 'fontSize' => $theme['settings']['typography']['fontSizes'] ?? [], 'spacing' => $theme['settings']['spacing']['spacingSizes'] ?? [], 'custom' => $theme['settings']['custom'] ?? [], ], 'user' => [ 'settings' => $user['settings'] ?? [], 'styles' => $user['styles'] ?? [] ], 'active' => self::active_variations(), ]; } public static function templates(): array { $out = [ 'templates' => [], 'parts' => [] ]; foreach ( get_block_templates( [], 'wp_template' ) as $t ) { $out['templates'][] = [ 'slug' => $t->slug, 'title' => $t->title, 'source' => $t->source, 'description' => $t->description ]; } foreach ( get_block_templates( [], 'wp_template_part' ) as $t ) { $out['parts'][] = [ 'slug' => $t->slug, 'title' => $t->title, 'area' => $t->area, 'source' => $t->source ]; } return $out; } public static function patterns(): array { $out = []; foreach ( \WP_Block_Patterns_Registry::get_instance()->get_all_registered() as $p ) { if ( str_starts_with( (string) ( $p['name'] ?? '' ), 'rooster/' ) ) { $out[] = [ 'name' => $p['name'], 'title' => $p['title'] ?? '', 'categories' => $p['categories'] ?? [], 'keywords' => $p['keywords'] ?? [], 'inserter' => $p['inserter'] ?? true, 'blockTypes' => $p['blockTypes'] ?? [], 'postTypes' => $p['postTypes'] ?? [] ]; } } return [ 'patterns' => $out ]; } public static function settings(): array { \Rooster\Core\Modules::load( 'admin/settings' ); $out = []; foreach ( \Rooster\Modules\Admin\Settings::fields() as $group => $fields ) { foreach ( array_keys( $fields ) as $key ) { $out[ $group ][ $key ] = rooster_setting( $group, $key ); } } $out['interactivity'] = [ 'iapi_enabled' => (bool) get_option( 'rooster_iapi_enabled', false ) ]; return [ 'settings' => $out ]; } public static function performance(): array { return [ 'budgets' => self::BUDGETS, 'policy' => [ 'device_variants' => (bool) rooster_setting( 'performance', 'device_variants', true ), 'defer' => (bool) rooster_setting( 'performance', 'defer', true ), 'speculation' => (string) rooster_setting( 'performance', 'speculation', 'moderate' ), 'third_party' => (string) rooster_setting( 'performance', 'third_party', 'delay' ), ], 'last_measured' => get_option( 'rooster_perf_last', null ), 'footprint_url' => add_query_arg( 'rooster_footprint', '1', home_url( '/' ) ), 'changes' => Changes::recent( 10 ), ]; } public static function slots(): array { $registry = Slots::instance(); $out = []; foreach ( $registry->all() as $slot => $def ) { $out[] = [ 'slot' => $slot, 'label' => $def['label'], 'kind' => $def['kind'], 'default' => $def['default'], 'matches' => $def['matches'], 'candidates' => $registry->candidates( $slot ), 'pins' => $registry->pins()[ $slot ] ?? [ 'default' => '', 'rules' => [] ] ]; } return [ 'slots' => $out ]; } private static function variations(): array { $out = [ 'colors' => [], 'typography' => [] ]; foreach ( [ 'colors', 'typography' ] as $kind ) { foreach ( ( method_exists( \Rooster\Core\Setup::class, 'variation_files' ) ? \Rooster\Core\Setup::variation_files( $kind ) : ( glob( ROOSTER_DIR . '/styles/' . $kind . '/*.json' ) ?: [] ) ) as $file ) { $data = json_decode( (string) file_get_contents( $file ), true ); $out[ $kind ][] = [ 'slug' => basename( $file, '.json' ), 'title' => (string) ( $data['title'] ?? basename( $file, '.json' ) ) ]; } } return $out; } private static function active_variations(): array { return (array) get_option( 'rooster_active_variations', [ 'colors' => 'default', 'typography' => 'system' ] ); } private static function adapter_modes(): ?array { if ( ! class_exists( 'AIEO_Theme_Adapters' ) ) { return null; } $modes = []; foreach ( [ 'swatches', 'recos', 'gallery', 'lightbox', 'archive_cards' ] as $surface ) { try { $modes[ $surface ] = (string) \AIEO_Theme_Adapters::effective_mode( $surface ); } catch ( \Throwable $e ) { $modes[ $surface ] = ''; } } return $modes; } public static function write( string $name, array $input ) { $mode = (string) ( $input['mode'] ?? 'draft' ); if ( ! empty( $input['dry_run'] ) ) { $mode = 'draft'; } if ( 'apply' === $mode ) { if ( empty( $input['change_id'] ) ) { $plan = self::plan( $name, $input ); if ( is_wp_error( $plan ) ) { return $plan; } $draft = Changes::draft( self::CATEGORY . '/' . $name, $input, $plan['ops'], $plan['summary'] ); return Changes::apply( $draft['change_id'] ); } return Changes::apply( (string) $input['change_id'] ); } $plan = self::plan( $name, $input ); if ( is_wp_error( $plan ) ) { return $plan; } return Changes::draft( self::CATEGORY . '/' . $name, $input, $plan['ops'], $plan['summary'] ) + [ 'ops' => $plan['ops'] ]; } public static function plan( string $name, array $input ) { switch ( $name ) { case 'set-design-tokens': $tokens = (array) ( $input['tokens'] ?? [] ); $data = [ 'settings' => [] ]; $parts = []; if ( ! empty( $tokens['color'] ) ) { $known = array_column( \WP_Theme_JSON_Resolver::get_theme_data()->get_data()['settings']['color']['palette'] ?? [], 'name', 'slug' ); foreach ( (array) $tokens['color'] as $slug => $color ) { $slug = sanitize_key( (string) $slug ); if ( ! isset( $known[ $slug ] ) || ! preg_match( '/^(#[0-9a-f]{3,8}|rgba?\(.+\)|hsla?\(.+\)|oklch\(.+\)|color-mix\(.+\))$/i', (string) $color ) ) { return new \WP_Error( 'rooster_invalid_token', sprintf( __( 'Unknown palette slug or invalid colour: %s', 'ai-eshop-optimizer' ), $slug ), [ 'status' => 400 ] ); } $data['settings']['color']['palette']['custom'][] = [ 'slug' => $slug, 'name' => $known[ $slug ], 'color' => (string) $color ]; $parts[] = "$slug=$color"; } } if ( ! empty( $tokens['fontFamily'] ) ) { foreach ( (array) $tokens['fontFamily'] as $slug => $stack ) { $slug = sanitize_key( (string) $slug ); if ( ! in_array( $slug, [ 'body', 'heading', 'mono' ], true ) ) { return new \WP_Error( 'rooster_invalid_token', __( 'Font family slugs are body, heading and mono.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } $data['settings']['typography']['fontFamilies']['custom'][] = [ 'slug' => $slug, 'name' => ucfirst( $slug ), 'fontFamily' => sanitize_text_field( (string) $stack ) ]; $parts[] = "$slug font"; } } if ( ! empty( $tokens['custom'] ) ) { foreach ( (array) $tokens['custom'] as $key => $value ) { $path = array_values( array_filter( array_map( 'sanitize_key', explode( '.', (string) $key ) ) ) ); if ( ! $path ) { continue; } $node = &$data['settings']['custom']['rooster']; foreach ( $path as $step ) { if ( ! isset( $node[ $step ] ) || ! is_array( $node[ $step ] ) ) { $node[ $step ] = []; } $node = &$node[ $step ]; } $node = sanitize_text_field( (string) $value ); unset( $node ); $parts[] = implode( '.', $path ); } } $ops = []; if ( ! empty( $tokens['scheme'] ) ) { \Rooster\Core\Modules::load( 'admin/settings' ); $scheme = \Rooster\Modules\Admin\SchemePaletteField::sanitize( (array) $tokens['scheme'] ); if ( count( $scheme ) !== count( (array) $tokens['scheme'] ) ) { return new \WP_Error( 'rooster_invalid_token', __( 'The other scheme takes palette slugs with hex or rgb colours.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } $ops[] = [ 'type' => 'settings', 'args' => [ 'group' => 'design', 'values' => [ 'scheme_palette' => array_merge( (array) \Rooster\Core\Setup::setting( 'design', 'scheme_palette', [] ), $scheme ) ] ] ]; $parts[] = 'the other scheme: ' . implode( ', ', array_keys( $scheme ) ); } if ( ! $parts ) { return new \WP_Error( 'rooster_empty', __( 'No tokens given.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } if ( [] !== $data['settings'] ) { array_unshift( $ops, [ 'type' => 'global_styles', 'args' => [ 'data' => $data ] ] ); } return [ 'ops' => $ops, 'summary' => sprintf( __( 'Design tokens: %s', 'ai-eshop-optimizer' ), implode( ', ', $parts ) ) ]; case 'apply-style-variation': $slug = sanitize_key( (string) ( $input['variation'] ?? '' ) ); $kind = (string) ( $input['kind'] ?? '' ); $file = ''; foreach ( $kind ? [ $kind ] : [ 'colors', 'typography' ] as $k ) { $files = method_exists( \Rooster\Core\Setup::class, 'variation_files' ) ? \Rooster\Core\Setup::variation_files( $k ) : [ $slug => ROOSTER_DIR . '/styles/' . $k . '/' . $slug . '.json' ]; if ( isset( $files[ $slug ] ) && is_file( $files[ $slug ] ) ) { $file = $files[ $slug ]; $kind = $k; } } if ( '' === $file ) { return new \WP_Error( 'rooster_no_variation', __( 'Unknown variation.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } [ $data, $replace ] = Changes::variation( (array) json_decode( (string) file_get_contents( $file ), true ) ); $active = self::active_variations(); $active[ $kind ] = $slug; return [ 'ops' => [ [ 'type' => 'global_styles', 'args' => [ 'data' => $data, 'replace' => $replace ] ], [ 'type' => 'option', 'args' => [ 'name' => 'rooster_active_variations', 'value' => $active ] ] ], 'summary' => sprintf( __( 'Style variation %1$s: %2$s', 'ai-eshop-optimizer' ), $kind, $slug ), ]; case 'set-settings': \Rooster\Core\Modules::load( 'admin/settings' ); $group = (string) ( $input['group'] ?? '' ); if ( ! isset( \Rooster\Modules\Admin\Settings::fields()[ $group ] ) ) { return new \WP_Error( 'rooster_invalid_group', __( 'Unknown settings group.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } $values = (array) ( $input['values'] ?? [] ); $current = \Rooster\Core\Setup::group( $group ); $clean = \Rooster\Modules\Admin\Settings::sanitize( $group, array_merge( $current, $values ) ); $changed = array_intersect_key( $clean, $values ); if ( ! $changed ) { return new \WP_Error( 'rooster_empty', __( 'No known setting keys given.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } return [ 'ops' => [ [ 'type' => 'settings', 'args' => [ 'group' => $group, 'values' => $changed ] ] ], 'summary' => sprintf( __( 'Settings %1$s: %2$s', 'ai-eshop-optimizer' ), $group, implode( ', ', array_keys( $changed ) ) ) ]; case 'pin-template': case 'unpin-template': $registry = Slots::instance(); $slot = (string) ( $input['slot'] ?? '' ); $def = $registry->get( $slot ); if ( ! $def ) { return new \WP_Error( 'rooster_no_slot', __( 'Unknown slot.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } $pins = $registry->pins(); $entry = $pins[ $slot ] ?? [ 'default' => '', 'rules' => [] ]; $match = (array) ( $input['match'] ?? [] ); if ( 'pin-template' === $name ) { $template = Slots::clean_slug( (string) ( $input['template'] ?? '' ) ); if ( '' === $template || ! isset( $registry->candidates( $slot )[ $template ] ) ) { return new \WP_Error( 'rooster_no_candidate', __( 'The template is not a candidate of this slot.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } if ( $match ) { $rule = Slots::sanitize_rule( [ 'match' => $match, 'template' => $template, 'priority' => (int) ( $input['priority'] ?? 10 ) ] ); if ( ! $rule ) { return new \WP_Error( 'rooster_invalid_rule', __( 'The match is not valid.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } $entry['rules'] = array_values( array_filter( (array) $entry['rules'], static fn ( $r ) => ( $r['match'] ?? null ) != $rule['match'] ) ); $entry['rules'][] = $rule; $summary = sprintf( __( 'Pin %1$s to %2$s (rule)', 'ai-eshop-optimizer' ), $template, $slot ); } else { $entry['default'] = $template; $summary = sprintf( __( 'Pin %1$s to %2$s (default)', 'ai-eshop-optimizer' ), $template, $slot ); } } else { if ( $match ) { $entry['rules'] = array_values( array_filter( (array) $entry['rules'], static fn ( $r ) => ( $r['match'] ?? null ) != $match ) ); } else { $entry['default'] = ''; } $summary = sprintf( __( 'Unpin %s', 'ai-eshop-optimizer' ), $slot ); } $pins[ $slot ] = $entry; return [ 'ops' => [ [ 'type' => 'pins', 'args' => [ 'pins' => $pins ] ] ], 'summary' => $summary ]; case 'set-template-part': $area = (string) ( $input['area'] ?? '' ); $variant = Slots::clean_slug( (string) ( $input['variant'] ?? '' ) ); if ( ! in_array( $area, [ 'header', 'footer' ], true ) ) { return new \WP_Error( 'rooster_invalid_area', __( 'Area must be header or footer.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } if ( '' !== $variant && ! is_file( get_stylesheet_directory() . '/parts/' . $variant . '.html' ) && ! is_file( get_template_directory() . '/parts/' . $variant . '.html' ) ) { return new \WP_Error( 'rooster_no_part', __( 'Unknown part.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } return [ 'ops' => [ [ 'type' => 'settings', 'args' => [ 'group' => 'general', 'values' => [ 'part_' . $area => $variant ] ] ] ], 'summary' => sprintf( __( 'Template part %1$s: %2$s', 'ai-eshop-optimizer' ), $area, $variant ?: 'default' ) ]; case 'set-defer-policy': $values = []; if ( array_key_exists( 'defer', $input ) ) { $values['defer'] = (bool) $input['defer']; } if ( array_key_exists( 'device_variants', $input ) ) { $values['device_variants'] = (bool) $input['device_variants']; } if ( ! $values ) { return new \WP_Error( 'rooster_empty', __( 'Give defer and/or device_variants.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } return [ 'ops' => [ [ 'type' => 'settings', 'args' => [ 'group' => 'performance', 'values' => $values ] ] ], 'summary' => __( 'Defer policy', 'ai-eshop-optimizer' ) . ': ' . wp_json_encode( $values ) ]; case 'set-speculation-policy': $policy = (string) ( $input['policy'] ?? '' ); if ( ! in_array( $policy, [ 'off', 'conservative', 'moderate', 'eager' ], true ) ) { return new \WP_Error( 'rooster_invalid_policy', __( 'Policy must be off, conservative, moderate or eager.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } return [ 'ops' => [ [ 'type' => 'settings', 'args' => [ 'group' => 'performance', 'values' => [ 'speculation' => $policy ] ] ] ], 'summary' => __( 'Speculation policy', 'ai-eshop-optimizer' ) . ': ' . $policy ]; } return new \WP_Error( 'rooster_unknown_ability', __( 'Unknown ability.', 'ai-eshop-optimizer' ), [ 'status' => 400 ] ); } }