[ $this, 'sanitize_options' ], 'default' => Helpers::get_defaults(), ) ); } public function sanitize_options( $input ) { $defaults = Helpers::get_defaults(); $sanitized = array(); // Migrate legacy style values saved before v2.2.0. $raw_style = $input['style'] ?? ''; if ( 'default' === $raw_style ) { $raw_style = 'modern'; } if ( 'custom' === $raw_style ) { $raw_style = 'academic'; } $valid_styles = array( 'modern', 'academic', 'minimal', 'card', 'ruled', 'editorial', 'summary', 'bulletin', 'default', 'custom' ); $sanitized['style'] = in_array( $raw_style, $valid_styles, true ) ? $raw_style : $defaults['style']; $sanitized['use_theme_css'] = ! empty( $input['use_theme_css'] ); $color_keys = array( 'title_color', 'text_color', 'bg_color', 'bg_color_end', 'accent_color', 'bullet_color', 'stripe_color' ); foreach ( $color_keys as $ck ) { $sanitized[ $ck ] = sanitize_hex_color( $input[ $ck ] ?? '' ) ?: $defaults[ $ck ]; } $sanitized['border_radius'] = absint( $input['border_radius'] ?? $defaults['border_radius'] ); $sanitized['border_radius'] = min( 50, max( 0, $sanitized['border_radius'] ) ); $valid_fonts = array( 'sans-serif', 'serif', 'humanist', 'monospace', 'slab', 'system' ); $sanitized['font_family'] = in_array( $input['font_family'] ?? '', $valid_fonts, true ) ? $input['font_family'] : $defaults['font_family']; $sanitized['enable_schema'] = ! empty( $input['enable_schema'] ); $valid_schema_types = array( 'CreativeWork', 'ScholarlyArticle', 'Article' ); $sanitized['schema_type'] = in_array( $input['schema_type'] ?? '', $valid_schema_types, true ) ? $input['schema_type'] : $defaults['schema_type']; $sanitized['custom_css_class'] = sanitize_html_class( $input['custom_css_class'] ?? '' ); $sanitized['hover_effect'] = ! empty( $input['hover_effect'] ); return $sanitized; } public function render_settings_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $tabs = array( 'appearance' => __( 'Appearance', 'abstract-box' ), 'schema' => __( 'Schema', 'abstract-box' ), 'advanced' => __( 'Advanced', 'abstract-box' ), 'usage' => __( 'Usage', 'abstract-box' ), ); $active_tab = 'appearance'; if ( isset( $_GET['tab'] ) ) { $requested = sanitize_key( wp_unslash( $_GET['tab'] ) ); if ( array_key_exists( $requested, $tabs ) ) { $active_tab = $requested; } } include ABSTRACT_BOX_DIR . 'includes/views/admin-settings.php'; } /* ── Card renderer helpers ────────────────────────────────────── */ /** * Render a setting row as a flex card. * * @param string $label Field label. * @param string $description Optional helper text (plain text, will be escaped). * @param string $control_html Already-escaped HTML for the control. * @param bool $stacked If true, control sits below label instead of inline-right. */ private function render_card( $label, $description, $control_html, $stacked = false ) { $modifier = $stacked ? ' ab-field-card--stacked' : ''; echo '
'; echo '
'; echo '' . esc_html( $label ) . ''; if ( $description ) { echo '' . esc_html( $description ) . ''; } echo '
'; echo '
' . $control_html . '
'; echo '
'; } /** * Render a toggle switch control. * Returns safe HTML — the markup is static, user value only affects the * checked attribute which is rendered via checked(). * * @param string $name options array key. * @param bool $checked Current value. * @return string */ private function toggle( $name, $checked ) { ob_start(); ?>

array( 'label' => __( 'Modern', 'abstract-box' ), 'sub' => __( 'Gradient · left bar · shadow', 'abstract-box' ), ), 'academic' => array( 'label' => __( 'Academic', 'abstract-box' ), 'sub' => __( 'Flat · dotted border · uppercase', 'abstract-box' ), ), 'minimal' => array( 'label' => __( 'Minimal', 'abstract-box' ), 'sub' => __( 'White · top accent line · clean', 'abstract-box' ), ), 'card' => array( 'label' => __( 'Card', 'abstract-box' ), 'sub' => __( 'Elevated · full border · shadow', 'abstract-box' ), ), 'ruled' => array( 'label' => __( 'Ruled', 'abstract-box' ), 'sub' => __( 'Thick left bar · title rule', 'abstract-box' ), ), 'editorial' => array( 'label' => __( 'Editorial', 'abstract-box' ), 'sub' => __( 'Solid full border · flat · no shadow', 'abstract-box' ), ), 'summary' => array( 'label' => __( 'Summary', 'abstract-box' ), 'sub' => __( 'Tinted background · no border · rounded', 'abstract-box' ), ), 'bulletin' => array( 'label' => __( 'Bulletin', 'abstract-box' ), 'sub' => __( 'Full border · diagonal stripe accent', 'abstract-box' ), ), ); // Normalise legacy saved value for selected state. $saved_style = $opts['style']; if ( 'default' === $saved_style ) { $saved_style = 'modern'; } if ( 'custom' === $saved_style ) { $saved_style = 'academic'; } echo '
'; foreach ( $style_opts as $val => $opt ) { $checked = ( $saved_style === $val ); ?> '; $style_html = ob_get_clean(); $this->render_card( __( 'Box Style', 'abstract-box' ), __( 'Choose the visual style of the abstract box.', 'abstract-box' ), $style_html, true ); /* Use Theme CSS toggle */ $this->render_card( __( 'Use Theme CSS', 'abstract-box' ), __( 'Disable plugin styles and rely on your theme instead.', 'abstract-box' ), $this->toggle( 'use_theme_css', $opts['use_theme_css'] ) ); ?>

render_card( __( 'Colour Presets', 'abstract-box' ), __( 'Apply a pre-configured palette to all colour fields at once.', 'abstract-box' ), $presets_html, true ); /* All five colour pickers — one compact card */ $color_fields = array( 'title_color' => __( 'Title', 'abstract-box' ), 'text_color' => __( 'Body Text', 'abstract-box' ), 'bg_color' => __( 'Background Start', 'abstract-box' ), 'bg_color_end' => __( 'Background End', 'abstract-box' ), 'accent_color' => __( 'Accent', 'abstract-box' ), 'bullet_color' => __( 'Bullet Dots', 'abstract-box' ), 'stripe_color' => __( 'Bulletin Stripe', 'abstract-box' ), ); ?>
$label ) : ?>

px
__( 'Sans-Serif (Modernist)', 'abstract-box' ), 'serif' => __( 'Serif (Traditional)', 'abstract-box' ), 'humanist' => __( 'Humanist Sans', 'abstract-box' ), 'monospace' => __( 'Monospace', 'abstract-box' ), 'slab' => __( 'Slab Serif (Academic)', 'abstract-box' ), 'system' => __( 'System Default', 'abstract-box' ), ); ?>

render_card( __( 'Enable Schema Output', 'abstract-box' ), __( 'Output schema.org JSON-LD structured data in the page head.', 'abstract-box' ), $this->toggle( 'enable_schema', $opts['enable_schema'] ) ); ob_start(); ?> render_card( __( 'Schema Type', 'abstract-box' ), __( 'CreativeWork is recommended to avoid conflicts with SEO plugin graphs.', 'abstract-box' ), ob_get_clean() ); } /* ── Tab: Advanced ────────────────────────────────────────────── */ public function render_advanced_tab() { $opts = Helpers::get_options(); settings_fields( 'abstract_box_group' ); ?>

render_card( __( 'Custom CSS Class', 'abstract-box' ), __( 'Add an extra CSS class to every abstract box for targeted theme styling.', 'abstract-box' ), ob_get_clean() ); $this->render_card( __( 'Hover Effect', 'abstract-box' ), __( 'Enable a subtle lift-and-shadow on hover (desktop only).', 'abstract-box' ), $this->toggle( 'hover_effect', $opts['hover_effect'] ) ); /* ── CSS class reference ─────────────────────────────────── */ ?>

.abstract-box
.abstract-box__title
.abstract-box__subtitle
.abstract-box__content
.abstract-box--custom

array( 'style', 'use_theme_css', 'title_color', 'text_color', 'bg_color', 'bg_color_end', 'accent_color', 'bullet_color', 'stripe_color', 'border_radius', 'font_family' ), 'schema' => array( 'enable_schema', 'schema_type' ), 'advanced' => array( 'custom_css_class', 'hover_effect' ), ); foreach ( $tab_keys as $tab => $keys ) { if ( $tab === $active_tab ) { continue; } foreach ( $keys as $key ) { $value = isset( $options[ $key ] ) ? $options[ $key ] : ( $defaults[ $key ] ?? '' ); if ( is_bool( $value ) ) { if ( $value ) { echo ''; } continue; } echo ''; } } } /* ── Live preview renderer ────────────────────────────────────── */ public function render_preview() { $options = Helpers::get_options(); $style_map = array( 'modern' => 'abstract-box--modern', 'academic' => 'abstract-box--academic', 'minimal' => 'abstract-box--minimal', 'card' => 'abstract-box--card', 'ruled' => 'abstract-box--ruled', 'editorial' => 'abstract-box--editorial', 'summary' => 'abstract-box--summary', 'bulletin' => 'abstract-box--bulletin', 'default' => '', 'custom' => 'abstract-box--academic', ); $modifier = $style_map[ $options['style'] ] ?? ''; $classes = 'abstract-box'; if ( $modifier ) { $classes .= ' ' . $modifier; } $font_stack = Helpers::font_stack( $options['font_family'] ); $radius = absint( $options['border_radius'] ); $css_vars = '--ab-bg-color: ' . sanitize_hex_color( $options['bg_color'] ) . '; '; $css_vars .= '--ab-bg-color-end: ' . sanitize_hex_color( $options['bg_color_end'] ) . '; '; $css_vars .= '--ab-accent-color: ' . sanitize_hex_color( $options['accent_color'] ) . '; '; if ( ! empty( $options['bullet_color'] ) ) { $css_vars .= '--ab-bullet-color: ' . sanitize_hex_color( $options['bullet_color'] ) . '; '; } if ( ! empty( $options['stripe_color'] ) ) { $css_vars .= '--ab-stripe-color: ' . sanitize_hex_color( $options['stripe_color'] ) . '; '; } $css_vars .= '--ab-title-color: ' . sanitize_hex_color( $options['title_color'] ) . '; '; $css_vars .= '--ab-text-color: ' . sanitize_hex_color( $options['text_color'] ) . '; '; $css_vars .= '--ab-border-radius: ' . $radius . 'px; '; $css_vars .= '--ab-font-family: ' . $font_stack . ';'; $html = '
'; $html .= '
' . esc_html__( 'Abstract', 'abstract-box' ) . '
'; $html .= '
' . esc_html__( 'Section 1.1', 'abstract-box' ) . '
'; $html .= '
'; $html .= '

' . esc_html__( 'This is a live preview of the abstract box. Changes to style, colours, font, and border radius are reflected here immediately without saving.', 'abstract-box' ) . '

'; $html .= '
'; $html .= '
'; return $html; } }