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:
@@ -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'],
|
||||||
|
|||||||
@@ -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" ';
|
||||||
|
|||||||
@@ -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>";
|
||||||
|
|||||||
@@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user