fix: Container width setting now applies correctly + Rail Ads improvements

- Fix container width not applying: css-global-responsive.css now uses
  CSS variable --roi-container-width instead of hardcoded values
- Add 8 Rail format options: slim-small (160x300), slim-medium (160x400),
  slim-large (160x500), skyscraper (160x600), slim-xlarge (160x700),
  wide-skyscraper (160x800), half-page (300x600), large-skyscraper (300x1050)
- Change rail_top_offset from text input to select with preset values
- Fix Rail Ads JavaScript positioning (moved after HTML, added retries)
- ThemeSettingsRenderer now always outputs CSS variables for layout

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
FrankZamora
2025-11-27 21:30:06 -06:00
parent 122bcd4750
commit 72ef7580fc
5 changed files with 195 additions and 134 deletions

View File

@@ -295,8 +295,12 @@ final class AdsensePlacementRenderer
$delayEnabled = ($settings['forms']['delay_enabled'] ?? true) === true;
// Dimensiones segun formato
// skyscraper: 160x600, wide-skyscraper: 160x800, half-page: 300x600, large-skyscraper: 300x1050
// Opciones de 160px de ancho con diferentes alturas + opciones anchas
[$width, $height] = match($format) {
'slim-small' => [160, 300],
'slim-medium' => [160, 400],
'slim-large' => [160, 500],
'slim-xlarge' => [160, 700],
'wide-skyscraper' => [160, 800],
'half-page' => [300, 600],
'large-skyscraper' => [300, 1050],
@@ -319,14 +323,15 @@ final class AdsensePlacementRenderer
]);
// Posicion rail izquierdo - usar max() para evitar valores negativos
// Formula: max(10px, calc((100vw - 1320px) / 2 - (width + 20)px))
// Usa CSS variable --roi-container-width-numeric de ThemeSettings (fallback 1320px)
// Formula: max(10px, calc((100vw - containerWidth) / 2 - (width + 20)px))
$cssRules[] = $this->cssGenerator->generate('.roi-rail-ad-left', [
'left' => 'max(10px, calc((100vw - 1320px) / 2 - ' . ($width + 20) . 'px))',
'left' => 'max(10px, calc((100vw - var(--roi-container-width-numeric, 1320px)) / 2 - ' . ($width + 20) . 'px))',
]);
// Posicion rail derecho - usar max() para consistencia
$cssRules[] = $this->cssGenerator->generate('.roi-rail-ad-right', [
'right' => 'max(10px, calc((100vw - 1320px) / 2 - ' . ($width + 20) . 'px))',
'right' => 'max(10px, calc((100vw - var(--roi-container-width-numeric, 1320px)) / 2 - ' . ($width + 20) . 'px))',
]);
// Media query para ocultar en pantallas < 1600px
@@ -337,94 +342,8 @@ final class AdsensePlacementRenderer
$css = implode("\n", $cssRules);
// JavaScript para posicionamiento inteligente de Rail Ads
// Detecta dinamicamente el header/hero/featured-image y ajusta posicion
$js = "
<script>
(function() {
var railAds = document.querySelectorAll('.roi-rail-ad');
if (!railAds.length) return;
// Selectores especificos del tema ROI
var navbar = document.querySelector('nav.navbar');
var hero = document.querySelector('.hero-section');
var featuredImage = document.querySelector('.featured-image-container');
function getNavbarHeight() {
if (navbar) {
var style = window.getComputedStyle(navbar);
if (style.position === 'fixed' || style.position === 'sticky') {
return navbar.offsetHeight;
}
}
return 0;
}
function adjustRailPosition() {
var navHeight = getNavbarHeight();
var gap = 30; // Espacio de separacion
var newTop = navHeight + gap;
// Buscar el elemento mas bajo entre hero y featured-image
var lowestBottom = navHeight;
if (hero) {
var heroRect = hero.getBoundingClientRect();
if (heroRect.bottom > 0) {
lowestBottom = Math.max(lowestBottom, heroRect.bottom);
}
}
if (featuredImage) {
var featuredRect = featuredImage.getBoundingClientRect();
if (featuredRect.bottom > 0) {
lowestBottom = Math.max(lowestBottom, featuredRect.bottom);
}
}
// Si algo esta visible en pantalla, posicionar debajo
if (lowestBottom > navHeight) {
newTop = lowestBottom + gap;
}
// Limitar el top maximo al 40% del viewport (mas generoso)
var maxTop = window.innerHeight * 0.4;
newTop = Math.min(newTop, maxTop);
// Asegurar minimo respetando navbar
newTop = Math.max(newTop, navHeight + gap);
railAds.forEach(function(rail) {
rail.style.top = newTop + 'px';
});
}
// Throttle para mejor rendimiento
var ticking = false;
function onScroll() {
if (!ticking) {
requestAnimationFrame(function() {
adjustRailPosition();
ticking = false;
});
ticking = true;
}
}
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', adjustRailPosition, { passive: true });
// Ejecutar despues de que el DOM este listo
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', adjustRailPosition);
} else {
// Esperar un poco para que carguen los elementos
setTimeout(adjustRailPosition, 100);
}
})();
</script>";
$html = "<style>{$css}</style>\n{$js}\n";
// HTML primero (CSS), JavaScript se añade AL FINAL
$html = "<style>{$css}</style>\n";
/**
* EXCEPCION DOCUMENTADA: CSS inline requerido por Google AdSense
@@ -457,6 +376,132 @@ final class AdsensePlacementRenderer
);
}
// JavaScript para posicionamiento inteligente de Rail Ads
// IMPORTANTE: Se añade DESPUES de los divs para garantizar que existan en el DOM
$html .= "
<script id=\"roi-rail-ads-positioning\">
(function() {
'use strict';
// Configuracion
var CONFIG = {
defaultTop: {$topOffset},
gap: 30,
maxTopPercent: 0.45,
retryDelay: 100,
maxRetries: 10
};
var retryCount = 0;
function initRailAds() {
var railAds = document.querySelectorAll('.roi-rail-ad');
// Si no hay rails, reintentar (puede que el DOM no este listo)
if (!railAds.length) {
if (retryCount < CONFIG.maxRetries) {
retryCount++;
setTimeout(initRailAds, CONFIG.retryDelay);
}
return;
}
// Selectores especificos del tema ROI
var navbar = document.querySelector('nav.navbar');
var hero = document.querySelector('.hero-section, .roi-hero, [class*=\"hero\"]');
var featuredImage = document.querySelector('.featured-image-container, .post-thumbnail, .entry-image');
function getNavbarHeight() {
if (navbar) {
var style = window.getComputedStyle(navbar);
if (style.position === 'fixed' || style.position === 'sticky') {
return navbar.offsetHeight || 0;
}
}
return 0;
}
function adjustRailPosition() {
var navHeight = getNavbarHeight();
var newTop = CONFIG.defaultTop;
// Buscar el elemento mas bajo visible en pantalla
var elementsToCheck = [hero, featuredImage].filter(Boolean);
if (elementsToCheck.length > 0) {
var lowestBottom = 0;
elementsToCheck.forEach(function(el) {
if (el) {
var rect = el.getBoundingClientRect();
// Solo considerar si el elemento esta visible (bottom > 0)
if (rect.bottom > 0 && rect.top < window.innerHeight) {
lowestBottom = Math.max(lowestBottom, rect.bottom);
}
}
});
// Si hay un elemento visible, posicionar debajo de el
if (lowestBottom > navHeight) {
newTop = lowestBottom + CONFIG.gap;
} else {
// Si no hay hero visible, usar navbar + gap o default
newTop = Math.max(navHeight + CONFIG.gap, CONFIG.defaultTop);
}
}
// Limitar el top maximo
var maxTop = window.innerHeight * CONFIG.maxTopPercent;
newTop = Math.min(newTop, maxTop);
// Asegurar minimo respetando navbar
newTop = Math.max(newTop, navHeight + CONFIG.gap);
// Aplicar posicion a todos los rails
for (var i = 0; i < railAds.length; i++) {
railAds[i].style.top = Math.round(newTop) + 'px';
}
}
// Throttle para scroll
var ticking = false;
function onScroll() {
if (!ticking) {
requestAnimationFrame(function() {
adjustRailPosition();
ticking = false;
});
ticking = true;
}
}
// Event listeners
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', function() {
adjustRailPosition();
}, { passive: true });
// Ejecutar inmediatamente
adjustRailPosition();
// Ejecutar varias veces para asegurar que las imagenes cargaron
setTimeout(adjustRailPosition, 200);
setTimeout(adjustRailPosition, 500);
setTimeout(adjustRailPosition, 1000);
// Tambien cuando las imagenes cargan
window.addEventListener('load', adjustRailPosition);
}
// Iniciar
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initRailAds);
} else {
initRailAds();
}
})();
</script>";
return $html;
}
}

View File

@@ -86,30 +86,37 @@ final class ThemeSettingsRenderer implements RendererInterface
* Genera CSS para el layout configurable
*
* @param array $data Datos del componente
* @return string Bloque style o vacio si usa defaults
* @return string Bloque style con variables CSS y overrides
*/
private function renderLayoutCSS(array $data): string
{
$containerWidth = $data['layout']['container_max_width'] ?? '1320';
// Si es el valor default, no generar CSS extra
if ($containerWidth === '1320') {
return '';
}
// Validar que el valor sea seguro
$allowedWidths = ['1140', '1200', '1320', '1400', '100%'];
if (!in_array($containerWidth, $allowedWidths, true)) {
return '';
$containerWidth = '1320'; // Fallback al default
}
// Generar el CSS
$widthValue = ($containerWidth === '100%') ? '100%' : $containerWidth . 'px';
$widthNumeric = ($containerWidth === '100%') ? '100vw' : $containerWidth . 'px';
return sprintf(
// Siempre generar CSS variable para que otros componentes puedan usarla
$css = sprintf(
'<!-- Layout CSS (ROI Theme) -->
<style id="roi-theme-layout-css">
/* Container width override */
:root {
--roi-container-width: %s;
--roi-container-width-numeric: %s;
}',
esc_attr($widthValue),
esc_attr($widthNumeric)
);
// Solo añadir override de container si NO es el default
if ($containerWidth !== '1320') {
$css .= sprintf('
@media (min-width: 1200px) {
.container,
.container-lg,
@@ -117,10 +124,15 @@ final class ThemeSettingsRenderer implements RendererInterface
.container-xxl {
max-width: %s;
}
}
</style>',
esc_attr($widthValue)
);
}',
esc_attr($widthValue)
);
}
$css .= '
</style>';
return $css;
}
/**