diff --git a/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php b/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php index 94c7ce8c..430ccdbc 100644 --- a/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php +++ b/Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php @@ -47,9 +47,17 @@ final class FeaturedImageRenderer implements RendererInterface return ''; } - $css = $this->generateCSS($data); $html = $this->buildHTML($data); + // Si is_critical=true, CSS ya fue inyectado en
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("\n%s", $css, $html); } @@ -84,7 +92,16 @@ final class FeaturedImageRenderer implements RendererInterface return is_singular() && has_post_thumbnail(); } - private function generateCSS(array $data): string + /** + * 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 { $spacing = $data['spacing'] ?? []; $effects = $data['visual_effects'] ?? []; @@ -92,6 +109,8 @@ final class FeaturedImageRenderer implements RendererInterface $marginTop = $spacing['margin_top'] ?? '1rem'; $marginBottom = $spacing['margin_bottom'] ?? '2rem'; + // Aspect ratio para prevenir CLS - reserva espacio antes de que imagen cargue + $aspectRatio = $spacing['aspect_ratio'] ?? '16/9'; $borderRadius = $effects['border_radius'] ?? '12px'; $boxShadow = $effects['box_shadow'] ?? '0 8px 24px rgba(0, 0, 0, 0.1)'; @@ -104,7 +123,7 @@ final class FeaturedImageRenderer implements RendererInterface $cssRules = []; - // Container styles + // Container styles con aspect-ratio para prevenir CLS $cssRules[] = $this->cssGenerator->generate('.featured-image-container', [ 'border-radius' => $borderRadius, 'overflow' => 'hidden', @@ -112,14 +131,21 @@ final class FeaturedImageRenderer implements RendererInterface 'margin-top' => $marginTop, 'margin-bottom' => $marginBottom, 'transition' => "transform {$transitionDuration} ease, box-shadow {$transitionDuration} ease", + 'aspect-ratio' => $aspectRatio, + 'position' => 'relative', + 'background-color' => '#f0f0f0', ]); - // Image styles + // Image styles - object-fit para llenar el contenedor con aspect-ratio $cssRules[] = $this->cssGenerator->generate('.featured-image-container img', [ 'width' => '100%', - 'height' => 'auto', + 'height' => '100%', + 'object-fit' => 'cover', 'display' => 'block', 'transition' => "transform {$transitionDuration} ease", + 'position' => 'absolute', + 'top' => '0', + 'left' => '0', ]); // Hover effect diff --git a/Shared/Infrastructure/Services/CriticalCSSService.php b/Shared/Infrastructure/Services/CriticalCSSService.php index d6f3ecab..7e2c625a 100644 --- a/Shared/Infrastructure/Services/CriticalCSSService.php +++ b/Shared/Infrastructure/Services/CriticalCSSService.php @@ -53,6 +53,7 @@ final class CriticalCSSService 'top-notification-bar' => \ROITheme\Public\TopNotificationBar\Infrastructure\Ui\TopNotificationBarRenderer::class, 'navbar' => \ROITheme\Public\Navbar\Infrastructure\Ui\NavbarRenderer::class, 'hero' => \ROITheme\Public\Hero\Infrastructure\Ui\HeroRenderer::class, + 'featured-image' => \ROITheme\Public\FeaturedImage\Infrastructure\Ui\FeaturedImageRenderer::class, ]; /** diff --git a/functions-addon.php b/functions-addon.php index 1cd055d8..97e4d660 100644 --- a/functions-addon.php +++ b/functions-addon.php @@ -194,7 +194,46 @@ function roi_render_component(string $componentName): string { // Crear Value Objects requeridos $name = new \ROITheme\Shared\Domain\ValueObjects\ComponentName($componentName); $configuration = new \ROITheme\Shared\Domain\ValueObjects\ComponentConfiguration($data); - $visibility = \ROITheme\Shared\Domain\ValueObjects\ComponentVisibility::allDevices(); // Default: visible en todas partes + + // ===================================================================== + // VALIDACIÓN DE VISIBILIDAD POR DISPOSITIVO (Previene CLS) + // ===================================================================== + // Si el componente no debe mostrarse en el dispositivo actual, + // NO renderizar nada. Esto evita CLS causado por elementos que + // se renderizan y luego se ocultan con CSS. + // ===================================================================== + + // Leer configuración de visibilidad desde BD + $isEnabled = ($data['visibility']['is_enabled'] ?? true) === true; + $showOnDesktop = ($data['visibility']['show_on_desktop'] ?? true) === true; + $showOnMobile = ($data['visibility']['show_on_mobile'] ?? true) === true; + + // Si no está habilitado, no renderizar + if (!$isEnabled) { + return ''; + } + + // Detectar dispositivo actual y validar visibilidad + // wp_is_mobile() detecta móviles y tablets via User-Agent + $isMobileDevice = wp_is_mobile(); + + if ($isMobileDevice && !$showOnMobile) { + // Dispositivo móvil pero show_on_mobile = false → NO renderizar + return ''; + } + + if (!$isMobileDevice && !$showOnDesktop) { + // Dispositivo desktop pero show_on_desktop = false → NO renderizar + return ''; + } + + // Crear ComponentVisibility con datos reales de BD + $visibility = new \ROITheme\Shared\Domain\ValueObjects\ComponentVisibility( + enabled: $isEnabled, + visibleDesktop: $showOnDesktop, + visibleTablet: $showOnMobile, // tablet tratado como mobile + visibleMobile: $showOnMobile + ); // Crear instancia del componente $component = new \ROITheme\Shared\Domain\Entities\Component(