fix(cls): Server-side device visibility + aspect-ratio for featured-image

- functions-addon.php: Validacion centralizada con wp_is_mobile()
  Componentes con show_on_mobile=false NO se renderizan en mobile
  Previene CLS de elementos ocultos con CSS

- FeaturedImageRenderer: Agrega aspect-ratio 16/9 para reservar espacio
  Imagen usa object-fit:cover con position:absolute
  Metodo generateCSS() ahora publico para CriticalCSSService

- CriticalCSSService: Agrega featured-image a CRITICAL_RENDERERS
  CSS se inyecta en <head> antes de que cargue contenido

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-12-01 10:43:22 -06:00
parent ee28baafd8
commit c0172467b3
3 changed files with 72 additions and 6 deletions

View File

@@ -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 <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);
}
@@ -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

View File

@@ -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,
];
/**

View File

@@ -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(