[ 'button_color', 'shape', 'site_width', 'content_width', 'shop_view', 'gallery_thumbs', 'gallery_arrows' ], 'typography' => [ 'body_font', 'heading_font', 'font_host' ], 'general' => [ 'part_header', 'part_footer', 'header_icon_color', 'menu_overlay', 'menu_source', 'phone_nav', 'glass', 'glass_menu_opacity', 'appearance_default' ], ]; public static function boot(): void { add_action( 'admin_menu', [ self::class, 'menu' ] ); add_action( 'admin_post_rooster_site_style', [ self::class, 'handle' ] ); add_action( 'admin_enqueue_scripts', [ self::class, 'assets' ] ); } public static function menu(): void { add_theme_page( __( 'Site style', 'ai-eshop-optimizer' ), __( 'Site style', 'ai-eshop-optimizer' ), 'edit_theme_options', self::SLUG, [ self::class, 'render' ] ); } public static function assets( string $hook ): void { if ( 'appearance_page_' . self::SLUG === $hook ) { wp_enqueue_style( 'wp-color-picker' ); wp_enqueue_script( 'wp-color-picker' ); wp_register_style( 'rooster-site-style', false, [], ROOSTER_VERSION ); wp_enqueue_style( 'rooster-site-style' ); wp_add_inline_style( 'rooster-site-style', self::css() ); } } public static function current(): array { $active = get_option( 'rooster_active_variations', [] ); $out = [ 'colors' => (string) ( ( is_array( $active ) ? $active : [] )['colors'] ?? 'default' ) ]; foreach ( self::KEYS as $group => $keys ) { foreach ( $keys as $key ) { $out[ $group ][ $key ] = rooster_setting( $group, $key ); } } $out['typography_variation'] = self::active_typography(); return $out; } private static function active_typography(): string { $active = get_option( 'rooster_active_variations', [] ); $slug = sanitize_key( (string) ( ( is_array( $active ) ? $active : [] )['typography'] ?? '' ) ); return isset( self::files( 'typography' )[ $slug ] ) ? $slug : ''; } private static function files( string $kind ): array { if ( method_exists( \Rooster\Core\Setup::class, 'variation_files' ) ) { return \Rooster\Core\Setup::variation_files( $kind ); } $out = []; foreach ( glob( get_template_directory() . '/styles/' . $kind . '/*.json' ) ?: [] as $file ) { $out[ basename( $file, '.json' ) ] = $file; } return $out; } public static function font_pairs(): array { $out = []; foreach ( self::files( 'typography' ) as $slug => $file ) { $data = json_decode( (string) file_get_contents( $file ), true ); $out[ $slug ] = (string) ( is_array( $data ) && isset( $data['title'] ) ? $data['title'] : $slug ); } return $out; } public static function schemes(): array { $theme = \WP_Theme_JSON_Resolver::get_theme_data()->get_settings()['color']['palette']['theme'] ?? []; $out = [ 'default' => [ 'title' => __( 'The theme\'s colours', 'ai-eshop-optimizer' ), 'swatches' => self::swatches( (array) $theme ) ] ]; foreach ( self::files( 'colors' ) as $file ) { $data = json_decode( (string) file_get_contents( $file ), true ); $out[ basename( $file, '.json' ) ] = [ 'title' => (string) ( $data['title'] ?? basename( $file, '.json' ) ), 'swatches' => self::swatches( (array) ( $data['settings']['color']['palette'] ?? [] ) ) ]; } return $out; } private static function swatches( array $palette ): array { $by = array_column( $palette, 'color', 'slug' ); return array_values( array_filter( [ $by['bg'] ?? '', $by['fg'] ?? '', $by['accent-2'] ?? ( $by['accent'] ?? '' ) ] ) ); } public static function clean( array $input ): array { $out = [ 'colors' => isset( self::schemes()[ (string) ( $input['colors'] ?? '' ) ] ) ? (string) $input['colors'] : 'default' ]; foreach ( self::KEYS as $group => $keys ) { $out[ $group ] = array_intersect_key( Settings::sanitize( $group, (array) ( $input[ $group ] ?? [] ) ), array_flip( $keys ) ); } $pair = is_scalar( $input['typography_variation'] ?? null ) ? sanitize_key( (string) $input['typography_variation'] ) : ''; $out['typography_variation'] = isset( self::files( 'typography' )[ $pair ] ) ? $pair : ''; return $out; } public static function ops( array $style ): array { $ops = []; foreach ( array_keys( self::KEYS ) as $group ) { $ops[] = [ 'type' => 'settings', 'args' => [ 'group' => $group, 'values' => $style[ $group ] ] ]; } $slug = $style['colors']; if ( 'default' === $slug ) { $color = \WP_Theme_JSON_Resolver::get_theme_data()->get_settings()['color'] ?? []; $data = [ 'settings' => [ 'color' => [ 'palette' => [ 'theme' => array_values( (array) ( $color['palette']['theme'] ?? [] ) ) ], 'duotone' => [ 'theme' => array_values( (array) ( $color['duotone']['theme'] ?? [] ) ) ] ] ] ]; } else { [ $data ] = Changes::variation( (array) json_decode( (string) file_get_contents( self::files( 'colors' )[ $slug ] ), true ) ); } $active = get_option( 'rooster_active_variations' ); $active = is_array( $active ) ? $active : [ 'colors' => 'default', 'typography' => 'system' ]; $active['colors'] = $slug; $ops[] = [ 'type' => 'global_styles', 'args' => [ 'data' => $data, 'replace' => [ 'settings.color.palette', 'settings.color.duotone' ] ] ]; $pair = (string) ( $style['typography_variation'] ?? '' ); $files = self::files( 'typography' ); if ( '' !== $pair && isset( $files[ $pair ] ) && $pair !== self::active_typography() ) { [ $fonts, $replace ] = Changes::variation( (array) json_decode( (string) file_get_contents( $files[ $pair ] ), true ) ); $ops[] = [ 'type' => 'global_styles', 'args' => [ 'data' => $fonts, 'replace' => $replace ] ]; $active['typography'] = $pair; } $ops[] = [ 'type' => 'option', 'args' => [ 'name' => 'rooster_active_variations', 'value' => $active ] ]; return $ops; } public static function handle(): void { if ( ! current_user_can( 'edit_theme_options' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to change the site style.', 'ai-eshop-optimizer' ), 403 ); } check_admin_referer( 'rooster_site_style' ); Modules::load( 'abilities/changes' ); $do = sanitize_key( (string) wp_unslash( $_POST['do'] ?? 'preview' ) ); $input = (array) wp_unslash( $_POST['style'] ?? [] ); if ( 'import' === $do ) { $file = $_FILES['config']['tmp_name'] ?? ''; $parsed = is_uploaded_file( (string) $file ) ? json_decode( (string) file_get_contents( (string) $file ), true ) : null; if ( ! is_array( $parsed ) || self::SCHEMA !== ( $parsed['$schema'] ?? '' ) ) { wp_safe_redirect( add_query_arg( [ 'page' => self::SLUG, 'notice' => 'bad-file' ], admin_url( 'themes.php' ) ) ); exit; } $input = $parsed; $do = 'preview'; } $style = self::clean( $input ); if ( 'export' === $do ) { nocache_headers(); header( 'Content-Type: application/json; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="site-style-' . sanitize_file_name( wp_parse_url( home_url(), PHP_URL_HOST ) ?: 'site' ) . '.json"' ); echo wp_json_encode( [ '$schema' => self::SCHEMA ] + $style, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); exit; } $draft = Changes::draft( 'site-style', $style, self::ops( $style ), __( 'Site style', 'ai-eshop-optimizer' ) ); if ( 'save' === $do ) { $applied = Changes::apply( $draft['change_id'] ); $args = [ 'page' => self::SLUG, 'notice' => is_wp_error( $applied ) ? 'failed' : 'saved' ]; } else { $args = [ 'page' => self::SLUG, 'draft' => $draft['change_id'] ]; } if ( ! empty( $_POST['ajax'] ) ) { wp_send_json( [ 'draft' => $args['draft'] ?? '', 'preview' => add_query_arg( Changes::QUERY, $args['draft'] ?? 'off', home_url( '/' ) ) ] ); } wp_safe_redirect( add_query_arg( $args, admin_url( 'themes.php' ) ) ); exit; } public static function render(): void { Modules::load( 'abilities/changes' ); $draft_id = isset( $_GET['draft'] ) ? preg_replace( '/[^a-zA-Z0-9]/', '', (string) wp_unslash( $_GET['draft'] ) ) : ''; $draft = $draft_id ? Changes::get_draft( $draft_id ) : null; $style = $draft ? (array) $draft['input'] : self::current(); $notice = sanitize_key( (string) wp_unslash( $_GET['notice'] ?? '' ) ); $fields = Settings::fields(); $preview = add_query_arg( Changes::QUERY, $draft ? $draft_id : 'off', home_url( '/' ) ); Chrome::ensure(); rooster_admin_page_open( __( 'Site style', 'ai-eshop-optimizer' ), [ 'icon' => 'palette', 'description' => __( 'Every look-and-feel choice of the site in one place. Changes show in the preview for you only; Save makes them the style every visitor gets. With no visitor controls ticked, visitors cannot change it.', 'ai-eshop-optimizer' ), 'class' => 'rooster-style', ] ); $messages = [ 'saved' => [ 'success', __( 'Saved: every visitor now gets this style. The change log can undo it.', 'ai-eshop-optimizer' ) ], 'failed' => [ 'error', __( 'The style could not be saved; nothing changed.', 'ai-eshop-optimizer' ) ], 'bad-file' => [ 'error', __( 'That file is not a site style config (rooster-site-style@1).', 'ai-eshop-optimizer' ) ], ]; if ( isset( $messages[ $notice ] ) ) { printf( '

%s

', esc_attr( $messages[ $notice ][0] ), esc_html( $messages[ $notice ][1] ) ); } echo '
'; wp_nonce_field( 'rooster_site_style' ); echo ''; foreach ( self::sections() as $key => [ $legend, $group, $keys, $icon ] ) { rooster_admin_card_open( Chrome::PRODUCT . '/design/site-style/' . $key, $legend, [ 'dom_id' => 'style=' . $key, 'icon' => $icon ] ); echo '
' . esc_html( $legend ) . '
'; if ( 'colours' === $key ) { echo '
' . esc_html__( 'Colour scheme', 'ai-eshop-optimizer' ) . '
'; foreach ( self::schemes() as $slug => $scheme ) { $dots = implode( '', array_map( static fn ( $c ) => '', $scheme['swatches'] ) ); printf( '', esc_attr( $slug ), checked( $style['colors'] ?? 'default', $slug, false ), $dots, esc_html( $scheme['title'] ) ); } echo '
'; } if ( 'fonts' === $key && [] !== self::font_pairs() ) { echo '

' . esc_html__( 'One of the theme\'s typography variations, applied when it changes. A text or heading font chosen below covers it.', 'ai-eshop-optimizer' ) . '

'; } self::fields_html( $group, $keys, $fields, $style ); echo '
'; rooster_admin_card_close(); } echo '
'; echo ''; echo ''; echo '
'; echo '
'; echo '
'; $devices = ''; echo '
'; rooster_admin_card_open( '', __( 'Preview', 'ai-eshop-optimizer' ), [ 'icon' => 'app-window', 'meta' => $devices, 'class' => 'rooster-style__previewcard' ] ); printf( '
', esc_attr__( 'Preview', 'ai-eshop-optimizer' ), esc_url( $preview ) ); rooster_admin_card_close(); echo '
'; rooster_admin_page_close(); self::inline_assets(); } public static function sections(): array { return [ 'colours' => [ __( 'Colours', 'ai-eshop-optimizer' ), 'design', [ 'button_color', 'shape' ], 'sun-moon', __( 'The colour scheme, the button colour and the corners of every element', 'ai-eshop-optimizer' ) ], 'fonts' => [ __( 'Fonts', 'ai-eshop-optimizer' ), 'typography', [ 'body_font', 'heading_font', 'font_host' ], 'pencil-line', __( 'The text and heading fonts, and where their files come from', 'ai-eshop-optimizer' ) ], 'layout' => [ __( 'Layout', 'ai-eshop-optimizer' ), 'design', [ 'site_width', 'content_width' ], 'layout-dashboard', __( 'The site width and the width of the page content', 'ai-eshop-optimizer' ) ], 'header' => [ __( 'Header and footer', 'ai-eshop-optimizer' ), 'general', [ 'part_header', 'part_footer', 'header_icon_color' ], 'rows-3', __( 'The header and footer variants and the icon colour', 'ai-eshop-optimizer' ) ], 'phones' => [ __( 'Phones', 'ai-eshop-optimizer' ), 'general', [ 'menu_overlay', 'menu_source', 'phone_nav', 'glass', 'glass_menu_opacity' ], 'sliders-horizontal', __( 'The phone menu and its source, the phone bar and the glass effect', 'ai-eshop-optimizer' ) ], 'shop' => [ __( 'Shop', 'ai-eshop-optimizer' ), 'design', [ 'shop_view', 'gallery_thumbs', 'gallery_arrows' ], 'shopping-cart', __( 'The shop view, the product gallery thumbnails and arrows', 'ai-eshop-optimizer' ) ], 'visitors' => [ __( 'Visitors', 'ai-eshop-optimizer' ), 'general', [ 'appearance_default' ], 'users', __( 'The theme until visitors choose (the controls they get are the header\'s actions)', 'ai-eshop-optimizer' ) ], ]; } private static function fields_html( string $group, array $keys, array $fields, array $style ): void { foreach ( $keys as $key ) { $field = $fields[ $group ][ $key ] ?? null; if ( ! $field ) { continue; } $name = 'style[' . $group . '][' . $key . ']'; $value = $style[ $group ][ $key ] ?? rooster_setting( $group, $key ); $id = 'rooster-style-' . $group . '-' . str_replace( '_', '-', $key ); $label = in_array( $field['type'], [ 'select', 'color' ], true ) ? '' : '' . esc_html( $field['label'] ) . ''; echo '
' . $label . '
'; switch ( $field['type'] ) { case 'select': echo ''; break; case 'multicheck': echo '
'; foreach ( $field['options'] as $opt => $label ) { printf( '', esc_attr( $name ), esc_attr( $opt ), checked( in_array( $opt, (array) $value, true ), true, false ), esc_html( $label ) ); } echo '
'; break; case 'color': printf( '', esc_attr( $id ), esc_attr( $name ), esc_attr( (string) $value ) ); break; case 'checkbox': printf( '', esc_attr( $name ) ); printf( '', esc_attr( $name ), checked( (bool) $value, true, false ), esc_html__( 'On', 'ai-eshop-optimizer' ) ); break; } echo '
'; if ( ! empty( $field['help'] ) ) { echo '

' . esc_html( (string) $field['help'] ) . '

'; } echo '
'; } } private static function css(): string { return '.rooster-style__layout { display: grid; grid-template-columns: minmax(20rem, 27rem) 1fr; gap: 1.25rem; align-items: start; }' . '.rooster-style__form { min-width: 0; }' . '.rooster-style__fieldset { min-inline-size: 0; margin: 0; padding: 0; border: 0; }' . '.rooster-style__form select { width: 100%; max-width: 100%; }' . '.rooster-style__scheme { margin: 0; }' . '.rooster-style__dots { display: inline-flex; }' . '.rooster-style__dots svg { width: 18px; height: 18px; margin-inline-end: -4px; }' . '.rooster-style__dots circle { stroke: var(--ra-border); stroke-width: 1px; }' . '.rooster-style__preview { position: sticky; top: var(--rooster-style-top, 100px); min-width: 0; }' . '.rooster-style__previewcard { margin-bottom: 0; }' . '.rooster-style__frame { position: relative; overflow: hidden; width: 100%; height: calc(100vh - var(--rooster-style-top, 100px) - 6.5rem); border: 1px solid var(--ra-border); border-radius: 6px; background: var(--ra-card); }' . '.rooster-style__frame iframe { position: absolute; top: 0; left: 0; width: 1440px; height: 100%; border: 0; transform-origin: 0 0; }' . '@media (max-width: 960px) { .rooster-style__layout { grid-template-columns: 1fr; } .rooster-style__preview { position: static; } }'; } private static function inline_assets(): void { ?>