'#2563eb', 'primary_dark' => '#1d4ed8', 'accent' => '#10b981', 'ink' => '#111827', 'muted' => '#4b5563', 'border' => '#e5e7eb', 'surface' => '#ffffff', 'soft' => '#f8fafc', 'soft_blue' => '#eff6ff', 'radius' => '22px', 'button_radius' => '999px', 'shadow' => '0 18px 50px rgba(15,23,42,.08)', 'max_width' => '1180px', 'layout_template' => 'standard_landing', 'section_count' => '4', 'card_count' => '3', 'faq_policy' => 'optional', 'cta_policy' => 'always', 'related_policy' => 'optional', 'media_source' => 'ai_or_library', 'media_position' => 'right_column', 'media_size' => 'medium', 'media_id' => '', 'media_url' => '', 'video_url' => '', ]; } public static function layout_keys() { return ['layout_template','section_count','card_count','faq_policy','cta_policy','related_policy','media_source','media_position','media_size','media_id','media_url','video_url']; } public static function color_keys() { return ['primary','primary_dark','accent','ink','muted','border','surface','soft','soft_blue']; } public static function size_keys() { return ['radius','button_radius','max_width']; } public static function sanitize_value($key, $value) { $value = trim((string) $value); if ($value === '') return ''; if (in_array($key, self::color_keys(), true)) { $hex = sanitize_hex_color($value); return $hex ?: ''; } if (in_array($key, self::size_keys(), true)) { return preg_match('/^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:px|rem|em|%)$/', $value) ? $value : ''; } if ($key === 'shadow') { return preg_match('/^(?:none|0\s+[0-9]+(?:\.[0-9]+)?px\s+[0-9]+(?:\.[0-9]+)?px\s+rgba?\([0-9.,\s%]+\))$/', $value) ? $value : ''; } if (in_array($key, self::layout_keys(), true)) { $allowed = [ 'layout_template' => ['standard_landing','guide','comparison','directory','product','minimal'], 'faq_policy' => ['never','optional','always'], 'cta_policy' => ['never','optional','always'], 'related_policy' => ['never','optional','always'], 'media_source' => ['none','ai','library','ai_or_library'], 'media_position' => ['above','below','left_column','right_column','section_background'], 'media_size' => ['thumbnail','small','medium','large','full_width'], ]; if ($key === 'section_count' || $key === 'card_count') { $n = absint($value); if ($key === 'section_count') return (string) max(1, min(8, $n)); return (string) max(1, min(6, $n)); } if ($key === 'media_id') return $value === '' ? '' : (string) absint($value); if ($key === 'media_url' || $key === 'video_url') return esc_url_raw($value); if (isset($allowed[$key])) return in_array($value, $allowed[$key], true) ? $value : ''; } return sanitize_text_field($value); } public static function sanitize_settings($settings) { $defaults = self::defaults(); $clean = []; if (!is_array($settings)) return $clean; foreach (array_keys($defaults) as $key) { if (!array_key_exists($key, $settings)) continue; $value = self::sanitize_value($key, $settings[$key]); if ($value !== '') $clean[$key] = $value; } return $clean; } public static function get($key = null) { $d = self::defaults(); $saved = self::sanitize_settings(get_option('aiwest_design_system', [])); if (is_array($saved)) { $d = array_merge($d, array_filter($saved, 'strlen')); } return $key ? ($d[$key] ?? null) : $d; } public static function style_prompt() { $d = self::get(); $layout_labels = [ 'standard_landing' => 'Hero/lead, content sections, cards, media, FAQ and CTA', 'guide' => 'Step-by-step guide layout', 'comparison' => 'Comparison-oriented layout with cards or tables', 'directory' => 'Directory or list-oriented layout', 'product' => 'Product/service layout with benefits, details, FAQ and CTA', 'minimal' => 'Minimal editorial layout', ]; $media_labels = [ 'none' => 'do not add media unless explicitly requested', 'ai' => 'generate an AI image', 'library' => 'use the selected WordPress Media Library asset when relevant', 'ai_or_library' => 'prefer the selected Media Library asset when available, otherwise generate a small AI image', ]; $media_position_labels = [ 'above' => 'above the text', 'below' => 'below the text', 'left_column' => 'left column with text on the right', 'right_column' => 'right column with text on the left', 'section_background' => 'as a subtle section background only when it improves readability', ]; return "USE ONE AI WEBMASTER DESIGN SYSTEM ACROSS THE ENTIRE WEBSITE:\n" . "- Do not generate random colors or arbitrary font sizes.\n" . "- Use these CSS classes: aiwest-section, aiwest-section-soft, aiwest-grid, aiwest-card, aiwest-kicker, aiwest-btn, aiwest-btn-secondary, aiwest-faq, aiwest-cta, aiwest-media-row, aiwest-media.\n" . "- Primary color: {$d['primary']}.\n" . "- Text color: {$d['ink']}.\n" . "- Soft backgrounds: {$d['soft']} or {$d['soft_blue']}.\n" . "- Cards: white background, subtle border, radius {$d['radius']}.\n" . "- Buttons: aiwest-btn or aiwest-btn-secondary class, no inline colors.\n" . "- Headings: use h2 for main sections and h3 for subsections.\n" . "- Return safe HTML consistent with the website design system.\n" . "- Do not use style=\"...\" attributes. Do not use custom HEX colors. Do not use h1 inside page content; use h2/h3.\n" . "- Every button must use aiwest-btn or aiwest-btn-secondary.\n" . "CONTENT LAYOUT SYSTEM:\n" . "- Preferred layout: " . ($layout_labels[$d['layout_template']] ?? $d['layout_template']) . ".\n" . "- Preferred number of content sections: {$d['section_count']}.\n" . "- Preferred number of cards per card section: {$d['card_count']}.\n" . "- FAQ policy: {$d['faq_policy']}. CTA policy: {$d['cta_policy']}. Related content policy: {$d['related_policy']}.\n" . "- Media policy: " . ($media_labels[$d['media_source']] ?? $d['media_source']) . ". Preferred media position: " . ($media_position_labels[$d['media_position']] ?? $d['media_position']) . ". Preferred media size: {$d['media_size']}.\n" . (($d['media_url'] ?? '') ? "- Selected WordPress Media Library asset URL: {$d['media_url']}. Use it only when it fits the section context.\n" : '') . (($d['video_url'] ?? '') ? "- Optional video asset URL: {$d['video_url']}. Use it only when a video section is useful.\n" : ''); } public static function enqueue_front_css() { wp_enqueue_style( 'aiwest-design-system', AIWEST_URL . 'assets/design-system.css', [], defined('AIWEST_VERSION') ? AIWEST_VERSION : null ); $aiwest_tokens_css_safe = self::css(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Every CSS token is allowlist-validated by sanitize_value() before this safe stylesheet API receives it. wp_add_inline_style('aiwest-design-system', $aiwest_tokens_css_safe); } public static function enqueue_admin_css() { if (!is_admin()) return; wp_enqueue_style( 'aiwest-design-system-admin', AIWEST_URL . 'assets/design-system.css', [], defined('AIWEST_VERSION') ? AIWEST_VERSION : null ); $aiwest_tokens_css_safe = self::css(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Every CSS token is allowlist-validated by sanitize_value() before this safe stylesheet API receives it. wp_add_inline_style('aiwest-design-system-admin', $aiwest_tokens_css_safe); } public static function css() { $d = self::get(); $tokens = [ "primary" => self::sanitize_value("primary", $d["primary"] ?? ""), "primary_dark" => self::sanitize_value("primary_dark", $d["primary_dark"] ?? ""), "accent" => self::sanitize_value("accent", $d["accent"] ?? ""), "ink" => self::sanitize_value("ink", $d["ink"] ?? ""), "muted" => self::sanitize_value("muted", $d["muted"] ?? ""), "border" => self::sanitize_value("border", $d["border"] ?? ""), "surface" => self::sanitize_value("surface", $d["surface"] ?? ""), "soft" => self::sanitize_value("soft", $d["soft"] ?? ""), "soft_blue" => self::sanitize_value("soft_blue", $d["soft_blue"] ?? ""), "radius" => self::sanitize_value("radius", $d["radius"] ?? ""), "button_radius" => self::sanitize_value("button_radius", $d["button_radius"] ?? ""), "shadow" => self::sanitize_value("shadow", $d["shadow"] ?? ""), "max_width" => self::sanitize_value("max_width", $d["max_width"] ?? ""), ]; $tokens = array_merge(array_intersect_key(self::defaults(), $tokens), array_filter($tokens, "strlen")); return "\n:root{\n" . "--aiwest-primary:{$tokens['primary']};\n" . "--aiwest-primary-dark:{$tokens['primary_dark']};\n" . "--aiwest-accent:{$tokens['accent']};\n" . "--aiwest-ink:{$tokens['ink']};\n" . "--aiwest-muted:{$tokens['muted']};\n" . "--aiwest-border:{$tokens['border']};\n" . "--aiwest-surface:{$tokens['surface']};\n" . "--aiwest-soft:{$tokens['soft']};\n" . "--aiwest-soft-blue:{$tokens['soft_blue']};\n" . "--aiwest-radius:{$tokens['radius']};\n" . "--aiwest-button-radius:{$tokens['button_radius']};\n" . "--aiwest-shadow:{$tokens['shadow']};\n" . "--aiwest-max-width:{$tokens['max_width']};\n" . "}\n"; } public static function normalize_generated_content($content) { if (is_admin()) return $content; // Non-destructive: only add a wrapper class for generated AI blocks if marker exists. if (strpos($content, 'aiwest-bulk-') !== false && strpos($content, 'aiwest-generated-content') === false) { $content = '