feat(theme-settings): agregar configuracion de ancho del contenedor

- Nuevo grupo 'Layout y Contenedor' en theme-settings
- Opciones: 1140px, 1200px, 1320px (default), 1400px, 100%
- CSS dinamico aplicado via ThemeSettingsRenderer
- Corregir selectores de hero en Rail Ads (.hero-section, .featured-image-container)
- Agregar setTimeout para esperar carga de DOM
- Aumentar gap de separacion a 30px
- Subir maxTop al 40% del viewport

🤖 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:03:43 -06:00
parent 2fa112ab7f
commit 0dfe3fcd2c
5 changed files with 198 additions and 15 deletions

View File

@@ -24,6 +24,10 @@ final class ThemeSettingsFieldMapper implements FieldMapperInterface
public function getFieldMapping(): array public function getFieldMapping(): array
{ {
return [ return [
// Layout
'themeSettingsContainerMaxWidth' => ['group' => 'layout', 'attribute' => 'container_max_width'],
'themeSettingsContentColumnWidth' => ['group' => 'layout', 'attribute' => 'content_column_width'],
// Custom Code // Custom Code
'themeSettingsCustomCss' => ['group' => 'custom_code', 'attribute' => 'custom_css'], 'themeSettingsCustomCss' => ['group' => 'custom_code', 'attribute' => 'custom_css'],
'themeSettingsCustomJsHeader' => ['group' => 'custom_code', 'attribute' => 'custom_js_header'], 'themeSettingsCustomJsHeader' => ['group' => 'custom_code', 'attribute' => 'custom_js_header'],

View File

@@ -27,6 +27,9 @@ final class ThemeSettingsFormBuilder
$html .= $this->buildHeader($componentId); $html .= $this->buildHeader($componentId);
// Layout Group (nueva seccion)
$html .= $this->buildLayoutGroup($componentId);
$html .= '<div class="row g-3">'; $html .= '<div class="row g-3">';
// Columna izquierda - CSS // Columna izquierda - CSS
@@ -44,6 +47,87 @@ final class ThemeSettingsFormBuilder
return $html; return $html;
} }
private function buildLayoutGroup(string $componentId): string
{
$html = '<div class="card shadow-sm mb-4" style="border-left: 4px solid #FF8600;">';
$html .= ' <div class="card-body">';
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
$html .= ' <i class="bi bi-layout-wtf me-2" style="color: #FF8600;"></i>';
$html .= ' Layout y Contenedor';
$html .= ' </h5>';
$html .= ' <div class="row g-3">';
// Container Max Width
$html .= ' <div class="col-md-6">';
$containerWidth = $this->renderer->getFieldValue($componentId, 'layout', 'container_max_width', '1320');
$containerWidthStr = is_string($containerWidth) ? $containerWidth : '1320';
$html .= $this->buildSelect(
'themeSettingsContainerMaxWidth',
'Ancho maximo del contenedor',
$containerWidthStr,
[
'1140' => '1140px (Bootstrap md)',
'1200' => '1200px (Compacto)',
'1320' => '1320px (Bootstrap xxl - Default)',
'1400' => '1400px (Amplio)',
'100%' => '100% (Fluido)'
],
'Valores menores dejan mas espacio para Rail Ads laterales'
);
$html .= ' </div>';
// Content Column Width
$html .= ' <div class="col-md-6">';
$columnWidth = $this->renderer->getFieldValue($componentId, 'layout', 'content_column_width', 'col-lg-9');
$columnWidthStr = is_string($columnWidth) ? $columnWidth : 'col-lg-9';
$html .= $this->buildSelect(
'themeSettingsContentColumnWidth',
'Ancho columna de contenido',
$columnWidthStr,
[
'col-lg-8' => '8 columnas (66.67%)',
'col-lg-9' => '9 columnas (75% - Default)',
'col-lg-10' => '10 columnas (83.33%)',
'col-lg-12' => '12 columnas (100% sin sidebar)'
],
'Proporcion de la columna principal vs sidebar'
);
$html .= ' </div>';
$html .= ' </div>';
$html .= ' <div class="alert alert-info small mb-0 mt-3">';
$html .= ' <i class="bi bi-info-circle me-1"></i>';
$html .= ' Reduce el ancho del contenedor para dar mas espacio a los Rail Ads en pantallas grandes.';
$html .= ' </div>';
$html .= ' </div>';
$html .= '</div>';
return $html;
}
private function buildSelect(string $id, string $label, string $value, array $options, string $helpText = ''): string
{
$html = ' <div class="mb-3">';
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
$html .= ' ' . esc_html($label);
$html .= ' </label>';
$html .= ' <select class="form-select form-select-sm" id="' . esc_attr($id) . '">';
foreach ($options as $optionValue => $optionLabel) {
$selected = ($value === (string) $optionValue) ? ' selected' : '';
$html .= ' <option value="' . esc_attr($optionValue) . '"' . $selected . '>' . esc_html($optionLabel) . '</option>';
}
$html .= ' </select>';
if (!empty($helpText)) {
$html .= ' <div class="form-text small">' . esc_html($helpText) . '</div>';
}
$html .= ' </div>';
return $html;
}
private function buildHeader(string $componentId): string private function buildHeader(string $componentId): string
{ {
$html = '<div class="rounded p-4 mb-4 shadow text-white" '; $html = '<div class="rounded p-4 mb-4 shadow text-white" ';

View File

@@ -333,17 +333,17 @@ final class AdsensePlacementRenderer
$css = implode("\n", $cssRules); $css = implode("\n", $cssRules);
// JavaScript para posicionamiento inteligente de Rail Ads // JavaScript para posicionamiento inteligente de Rail Ads
// Detecta dinamicamente el header/hero y ajusta posicion sin valores fijos // Detecta dinamicamente el header/hero/featured-image y ajusta posicion
$js = " $js = "
<script> <script>
(function() { (function() {
var railAds = document.querySelectorAll('.roi-rail-ad'); var railAds = document.querySelectorAll('.roi-rail-ad');
if (!railAds.length) return; if (!railAds.length) return;
// Buscar elementos de referencia (en orden de prioridad) // Selectores especificos del tema ROI
var navbar = document.querySelector('.navbar-fixed-top, .roi-navbar, .site-header, nav.navbar'); var navbar = document.querySelector('nav.navbar');
var hero = document.querySelector('.roi-hero, .hero-section, .hero, [class*=\"hero\"]'); var hero = document.querySelector('.hero-section');
var mainContent = document.querySelector('main, .site-main, .main-content, article'); var featuredImage = document.querySelector('.featured-image-container');
function getNavbarHeight() { function getNavbarHeight() {
if (navbar) { if (navbar) {
@@ -357,20 +357,33 @@ final class AdsensePlacementRenderer
function adjustRailPosition() { function adjustRailPosition() {
var navHeight = getNavbarHeight(); var navHeight = getNavbarHeight();
var gap = 20; // Espacio minimo de separacion var gap = 30; // Espacio de separacion
var newTop = navHeight + gap; var newTop = navHeight + gap;
// Si hay hero visible, posicionar debajo de el // Buscar el elemento mas bajo entre hero y featured-image
var lowestBottom = navHeight;
if (hero) { if (hero) {
var heroRect = hero.getBoundingClientRect(); var heroRect = hero.getBoundingClientRect();
// Si el bottom del hero esta visible (> navbar height), rails debajo del hero if (heroRect.bottom > 0) {
if (heroRect.bottom > navHeight) { lowestBottom = Math.max(lowestBottom, heroRect.bottom);
newTop = heroRect.bottom + gap;
} }
} }
// Limitar el top maximo para que no quede muy abajo if (featuredImage) {
var maxTop = window.innerHeight * 0.25; // Max 25% del viewport 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); newTop = Math.min(newTop, maxTop);
// Asegurar minimo respetando navbar // Asegurar minimo respetando navbar
@@ -400,7 +413,8 @@ final class AdsensePlacementRenderer
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', adjustRailPosition); document.addEventListener('DOMContentLoaded', adjustRailPosition);
} else { } else {
adjustRailPosition(); // Esperar un poco para que carguen los elementos
setTimeout(adjustRailPosition, 100);
} }
})(); })();
</script>"; </script>";

View File

@@ -50,6 +50,7 @@ final class ThemeSettingsRenderer implements RendererInterface
* Genera contenido para wp_head * Genera contenido para wp_head
* *
* Incluye: * Incluye:
* - Layout CSS (container width)
* - Custom CSS (si configurado) * - Custom CSS (si configurado)
* - Custom JS Header (si configurado) * - Custom JS Header (si configurado)
* *
@@ -60,6 +61,12 @@ final class ThemeSettingsRenderer implements RendererInterface
{ {
$output = ''; $output = '';
// Layout CSS (container width configurable)
$layoutOutput = $this->renderLayoutCSS($data);
if (!empty($layoutOutput)) {
$output .= $layoutOutput . "\n";
}
// Custom CSS // Custom CSS
$cssOutput = $this->renderCustomCSS($data); $cssOutput = $this->renderCustomCSS($data);
if (!empty($cssOutput)) { if (!empty($cssOutput)) {
@@ -75,6 +82,47 @@ final class ThemeSettingsRenderer implements RendererInterface
return $output; return $output;
} }
/**
* Genera CSS para el layout configurable
*
* @param array $data Datos del componente
* @return string Bloque style o vacio si usa defaults
*/
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 '';
}
// Generar el CSS
$widthValue = ($containerWidth === '100%') ? '100%' : $containerWidth . 'px';
return sprintf(
'<!-- Layout CSS (ROI Theme) -->
<style id="roi-theme-layout-css">
/* Container width override */
@media (min-width: 1200px) {
.container,
.container-lg,
.container-xl,
.container-xxl {
max-width: %s;
}
}
</style>',
esc_attr($widthValue)
);
}
/** /**
* Genera contenido para wp_footer * Genera contenido para wp_footer
* *

View File

@@ -1,8 +1,41 @@
{ {
"component_name": "theme-settings", "component_name": "theme-settings",
"version": "1.3.0", "version": "1.4.0",
"description": "Configuraciones globales del tema: codigo personalizado", "description": "Configuraciones globales del tema: layout y codigo personalizado",
"groups": { "groups": {
"layout": {
"label": "Layout y Contenedor",
"priority": 5,
"fields": {
"container_max_width": {
"type": "select",
"label": "Ancho maximo del contenedor principal",
"default": "1320",
"editable": true,
"options": {
"1140": "1140px (Bootstrap md)",
"1200": "1200px (Compacto)",
"1320": "1320px (Bootstrap xxl - Default)",
"1400": "1400px (Amplio)",
"100%": "100% (Fluido)"
},
"description": "Ancho maximo del .container principal. Valores menores dejan mas espacio para Rail Ads"
},
"content_column_width": {
"type": "select",
"label": "Ancho columna de contenido",
"default": "col-lg-9",
"editable": true,
"options": {
"col-lg-8": "8 columnas (66.67%)",
"col-lg-9": "9 columnas (75% - Default)",
"col-lg-10": "10 columnas (83.33%)",
"col-lg-12": "12 columnas (100% sin sidebar)"
},
"description": "Proporcion Bootstrap de la columna principal vs sidebar"
}
}
},
"custom_code": { "custom_code": {
"label": "Codigo Personalizado", "label": "Codigo Personalizado",
"priority": 10, "priority": 10,