- Created CriticalCSSService (singleton) that queries BD directly in wp_head - Service generates CSS BEFORE components render (priority 1) - Renderers check is_critical flag and skip inline CSS if true - Made generateCSS() public in Renderers for CriticalCSSService to use - Removed CriticalCSSCollector pattern (timing issue with WordPress) Flow: 1. wp_head (priority 1) → CriticalCSSService::render() 2. Service queries BD for components with visibility.is_critical=true 3. Generates CSS using Renderer->generateCSS() methods 4. Outputs: <style id="roi-critical-css">...</style> 5. When Renderers execute, they detect is_critical and omit CSS inline 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
310 lines
10 KiB
PHP
310 lines
10 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace ROITheme\Public\Hero\Infrastructure\Ui;
|
|
|
|
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
|
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
|
use ROITheme\Shared\Domain\Entities\Component;
|
|
|
|
/**
|
|
* Class HeroRenderer
|
|
*
|
|
* Renderizador del componente Hero para el frontend.
|
|
*
|
|
* Responsabilidades:
|
|
* - Renderizar HTML del hero section con título del post/página
|
|
* - Mostrar badges de categorías (dinámicos desde WordPress)
|
|
* - Delegar generación de CSS a CSSGeneratorInterface
|
|
* - Validar visibilidad (is_enabled, show_on_pages, responsive)
|
|
* - Manejar visibilidad responsive con clases Bootstrap
|
|
*
|
|
* NO responsable de:
|
|
* - Generar string CSS (delega a CSSGeneratorService)
|
|
* - Persistir datos
|
|
* - Lógica de negocio
|
|
*
|
|
* Cumple con:
|
|
* - DIP: Recibe CSSGeneratorInterface por constructor
|
|
* - SRP: Una responsabilidad (renderizar hero)
|
|
* - Clean Architecture: Infrastructure puede usar WordPress
|
|
*
|
|
* @package ROITheme\Public\Hero\Infrastructure\Ui
|
|
*/
|
|
final class HeroRenderer implements RendererInterface
|
|
{
|
|
/**
|
|
* @param CSSGeneratorInterface $cssGenerator Servicio de generación de CSS
|
|
*/
|
|
public function __construct(
|
|
private CSSGeneratorInterface $cssGenerator
|
|
) {}
|
|
|
|
public function render(Component $component): string
|
|
{
|
|
$data = $component->getData();
|
|
|
|
if (!$this->isEnabled($data)) {
|
|
return '';
|
|
}
|
|
|
|
if (!$this->shouldShowOnCurrentPage($data)) {
|
|
return '';
|
|
}
|
|
|
|
$html = $this->buildHTML($data);
|
|
|
|
// Si is_critical=true, CSS ya fue inyectado en <head> por CriticalCSSService
|
|
$isCritical = $data['visibility']['is_critical'] ?? false;
|
|
|
|
if ($isCritical) {
|
|
return $html; // Solo HTML, sin CSS inline
|
|
}
|
|
|
|
// CSS inline para componentes no críticos
|
|
$css = $this->generateCSS($data);
|
|
return sprintf("<style>%s</style>\n%s", $css, $html);
|
|
}
|
|
|
|
public function supports(string $componentType): bool
|
|
{
|
|
return $componentType === 'hero';
|
|
}
|
|
|
|
private function isEnabled(array $data): bool
|
|
{
|
|
return ($data['visibility']['is_enabled'] ?? false) === true;
|
|
}
|
|
|
|
private function shouldShowOnCurrentPage(array $data): bool
|
|
{
|
|
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
|
|
|
switch ($showOn) {
|
|
case 'all':
|
|
return true;
|
|
case 'home':
|
|
return is_front_page() || is_home();
|
|
case 'posts':
|
|
return is_single();
|
|
case 'pages':
|
|
return is_page();
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generar CSS usando CSSGeneratorService
|
|
*
|
|
* Este método es público para que CriticalCSSService pueda
|
|
* generar CSS crítico antes de wp_head sin duplicar lógica.
|
|
*
|
|
* @param array $data Datos del componente
|
|
* @return string CSS generado
|
|
*/
|
|
public function generateCSS(array $data): string
|
|
{
|
|
$colors = $data['colors'] ?? [];
|
|
$typography = $data['typography'] ?? [];
|
|
$spacing = $data['spacing'] ?? [];
|
|
$effects = $data['visual_effects'] ?? [];
|
|
$visibility = $data['visibility'] ?? [];
|
|
|
|
$gradientStart = $colors['gradient_start'] ?? '#1e3a5f';
|
|
$gradientEnd = $colors['gradient_end'] ?? '#2c5282';
|
|
$titleColor = $colors['title_color'] ?? '#FFFFFF';
|
|
$badgeBgColor = $colors['badge_bg_color'] ?? '#FFFFFF';
|
|
$badgeTextColor = $colors['badge_text_color'] ?? '#FFFFFF';
|
|
$badgeIconColor = $colors['badge_icon_color'] ?? '#FFB800';
|
|
$badgeHoverBg = $colors['badge_hover_bg'] ?? '#FF8600';
|
|
|
|
$titleFontSize = $typography['title_font_size'] ?? '2.5rem';
|
|
$titleFontSizeMobile = $typography['title_font_size_mobile'] ?? '1.75rem';
|
|
$titleFontWeight = $typography['title_font_weight'] ?? '700';
|
|
$titleLineHeight = $typography['title_line_height'] ?? '1.4';
|
|
$badgeFontSize = $typography['badge_font_size'] ?? '0.813rem';
|
|
|
|
$paddingVertical = $spacing['padding_vertical'] ?? '3rem';
|
|
$marginBottom = $spacing['margin_bottom'] ?? '1.5rem';
|
|
$badgePadding = $spacing['badge_padding'] ?? '0.375rem 0.875rem';
|
|
$badgeBorderRadius = $spacing['badge_border_radius'] ?? '20px';
|
|
$minHeight = $spacing['min_height'] ?? '120px';
|
|
$titleMinHeight = $spacing['title_min_height'] ?? '3rem';
|
|
$badgeMinHeight = $spacing['badge_min_height'] ?? '32px';
|
|
|
|
$boxShadow = $effects['box_shadow'] ?? '0 4px 16px rgba(30, 58, 95, 0.25)';
|
|
$titleTextShadow = $effects['title_text_shadow'] ?? '1px 1px 2px rgba(0, 0, 0, 0.2)';
|
|
$badgeBackdropBlur = $effects['badge_backdrop_blur'] ?? '10px';
|
|
|
|
$showOnDesktop = $visibility['show_on_desktop'] ?? true;
|
|
$showOnMobile = $visibility['show_on_mobile'] ?? true;
|
|
|
|
$cssRules = [];
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.hero-section', [
|
|
'background' => "linear-gradient(135deg, {$gradientStart} 0%, {$gradientEnd} 100%)",
|
|
'box-shadow' => $boxShadow,
|
|
'padding' => "{$paddingVertical} 0",
|
|
'margin-bottom' => $marginBottom,
|
|
'min-height' => $minHeight,
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.hero-section__title', [
|
|
'color' => "{$titleColor} !important",
|
|
'font-weight' => $titleFontWeight,
|
|
'font-size' => $titleFontSize,
|
|
'line-height' => $titleLineHeight,
|
|
'text-shadow' => $titleTextShadow,
|
|
'margin-bottom' => '0',
|
|
'text-align' => 'center',
|
|
'min-height' => $titleMinHeight,
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.hero-section__badge', [
|
|
'background' => $this->hexToRgba($badgeBgColor, 0.15),
|
|
'backdrop-filter' => "blur({$badgeBackdropBlur})",
|
|
'-webkit-backdrop-filter' => "blur({$badgeBackdropBlur})",
|
|
'border' => '1px solid ' . $this->hexToRgba($badgeBgColor, 0.2),
|
|
'color' => $this->hexToRgba($badgeTextColor, 0.95),
|
|
'padding' => $badgePadding,
|
|
'border-radius' => $badgeBorderRadius,
|
|
'font-size' => $badgeFontSize,
|
|
'font-weight' => '500',
|
|
'text-decoration' => 'none',
|
|
'display' => 'inline-block',
|
|
'transition' => 'all 0.3s ease',
|
|
'min-height' => $badgeMinHeight,
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.hero-section__badge:hover', [
|
|
'background' => $this->hexToRgba($badgeHoverBg, 0.2),
|
|
'border-color' => $this->hexToRgba($badgeHoverBg, 0.4),
|
|
'color' => '#ffffff',
|
|
]);
|
|
|
|
$cssRules[] = $this->cssGenerator->generate('.hero-section__badge i', [
|
|
'color' => $badgeIconColor,
|
|
]);
|
|
|
|
$cssRules[] = "@media (max-width: 767.98px) {
|
|
.hero-section__title {
|
|
font-size: {$titleFontSizeMobile};
|
|
}
|
|
}";
|
|
|
|
if (!$showOnMobile) {
|
|
$cssRules[] = "@media (max-width: 767.98px) {
|
|
.hero-section { display: none !important; }
|
|
}";
|
|
}
|
|
|
|
if (!$showOnDesktop) {
|
|
$cssRules[] = "@media (min-width: 768px) {
|
|
.hero-section { display: none !important; }
|
|
}";
|
|
}
|
|
|
|
return implode("\n", $cssRules);
|
|
}
|
|
|
|
private function buildHTML(array $data): string
|
|
{
|
|
$content = $data['content'] ?? [];
|
|
$showCategories = $content['show_categories'] ?? true;
|
|
$showBadgeIcon = $content['show_badge_icon'] ?? true;
|
|
$badgeIconClass = $content['badge_icon_class'] ?? 'bi-folder-fill';
|
|
$titleTag = $content['title_tag'] ?? 'h1';
|
|
|
|
$allowedTags = ['h1', 'h2', 'div'];
|
|
if (!in_array($titleTag, $allowedTags, true)) {
|
|
$titleTag = 'h1';
|
|
}
|
|
|
|
$title = is_singular() ? get_the_title() : '';
|
|
if (empty($title)) {
|
|
$title = wp_title('', false);
|
|
}
|
|
|
|
$html = '<div class="container-fluid hero-section">';
|
|
$html .= '<div class="container">';
|
|
|
|
if ($showCategories && is_single()) {
|
|
$categories = get_the_category();
|
|
if (!empty($categories)) {
|
|
$html .= '<div class="mb-3 d-flex justify-content-center">';
|
|
$html .= '<div class="d-flex gap-2 flex-wrap justify-content-center">';
|
|
|
|
foreach ($categories as $category) {
|
|
$categoryLink = esc_url(get_category_link($category->term_id));
|
|
$categoryName = esc_html($category->name);
|
|
$iconHtml = $showBadgeIcon
|
|
? '<i class="bi ' . esc_attr($badgeIconClass) . ' me-1"></i>'
|
|
: '';
|
|
|
|
$html .= sprintf(
|
|
'<a href="%s" class="hero-section__badge">%s%s</a>',
|
|
$categoryLink,
|
|
$iconHtml,
|
|
$categoryName
|
|
);
|
|
}
|
|
|
|
$html .= '</div>';
|
|
$html .= '</div>';
|
|
}
|
|
}
|
|
|
|
$html .= sprintf(
|
|
'<%s class="hero-section__title">%s</%s>',
|
|
$titleTag,
|
|
esc_html($title),
|
|
$titleTag
|
|
);
|
|
|
|
$html .= '</div>';
|
|
$html .= '</div>';
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function hexToRgba(string $hex, float $alpha): string
|
|
{
|
|
$hex = ltrim($hex, '#');
|
|
|
|
if (strlen($hex) === 3) {
|
|
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
|
|
}
|
|
|
|
$r = hexdec(substr($hex, 0, 2));
|
|
$g = hexdec(substr($hex, 2, 2));
|
|
$b = hexdec(substr($hex, 4, 2));
|
|
|
|
return "rgba({$r}, {$g}, {$b}, {$alpha})";
|
|
}
|
|
|
|
/**
|
|
* Genera clases Bootstrap de visibilidad responsive
|
|
*
|
|
* @param bool $desktop Si debe mostrarse en desktop
|
|
* @param bool $mobile Si debe mostrarse en mobile
|
|
* @return string|null Clases Bootstrap o null si visible en todos
|
|
*/
|
|
private function getVisibilityClasses(bool $desktop, bool $mobile): ?string
|
|
{
|
|
if ($desktop && $mobile) {
|
|
return null;
|
|
}
|
|
|
|
if ($desktop && !$mobile) {
|
|
return 'd-none d-md-block';
|
|
}
|
|
|
|
if (!$desktop && $mobile) {
|
|
return 'd-block d-md-none';
|
|
}
|
|
|
|
return 'd-none';
|
|
}
|
|
}
|